446cf2
commit f1f00c072138af90ae6da180f260111f09afe7a3
446cf2
Author: Florian Weimer <fweimer@redhat.com>
446cf2
Date:   Wed Oct 14 10:54:39 2020 +0200
446cf2
446cf2
    resolv: Handle transaction ID collisions in parallel queries (bug 26600)
446cf2
    
446cf2
    If the transaction IDs are equal, the old check attributed both
446cf2
    responses to the first query, not recognizing the second response.
446cf2
    This fixes bug 26600.
446cf2
446cf2
diff --git a/resolv/Makefile b/resolv/Makefile
446cf2
index 72a0f196506ac489..cee5225f8933f245 100644
446cf2
--- a/resolv/Makefile
446cf2
+++ b/resolv/Makefile
446cf2
@@ -62,6 +62,11 @@ tests += \
446cf2
   tst-resolv-search \
446cf2
   tst-resolv-trailing \
446cf2
 
446cf2
+# This test calls __res_context_send directly, which is not exported
446cf2
+# from libresolv.
446cf2
+tests-internal += tst-resolv-txnid-collision
446cf2
+tests-static += tst-resolv-txnid-collision
446cf2
+
446cf2
 # These tests need libdl.
446cf2
 ifeq (yes,$(build-shared))
446cf2
 tests += \
446cf2
@@ -202,6 +207,8 @@ $(objpfx)tst-resolv-search: $(objpfx)libresolv.so $(shared-thread-library)
446cf2
 $(objpfx)tst-resolv-trailing: $(objpfx)libresolv.so $(shared-thread-library)
446cf2
 $(objpfx)tst-resolv-threads: \
446cf2
   $(libdl) $(objpfx)libresolv.so $(shared-thread-library)
446cf2
+$(objpfx)tst-resolv-txnid-collision: $(objpfx)libresolv.a \
446cf2
+  $(static-thread-library)
446cf2
 $(objpfx)tst-resolv-canonname: \
446cf2
   $(libdl) $(objpfx)libresolv.so $(shared-thread-library)
446cf2
 
446cf2
diff --git a/resolv/res_send.c b/resolv/res_send.c
446cf2
index c9b02cca130bc20d..ac19627634281c2f 100644
446cf2
--- a/resolv/res_send.c
446cf2
+++ b/resolv/res_send.c
446cf2
@@ -1315,15 +1315,6 @@ send_dg(res_state statp,
446cf2
 			*terrno = EMSGSIZE;
446cf2
 			return close_and_return_error (statp, resplen2);
446cf2
 		}
446cf2
-		if ((recvresp1 || hp->id != anhp->id)
446cf2
-		    && (recvresp2 || hp2->id != anhp->id)) {
446cf2
-			/*
446cf2
-			 * response from old query, ignore it.
446cf2
-			 * XXX - potential security hazard could
446cf2
-			 *	 be detected here.
446cf2
-			 */
446cf2
-			goto wait;
446cf2
-		}
446cf2
 
446cf2
 		/* Paranoia check.  Due to the connected UDP socket,
446cf2
 		   the kernel has already filtered invalid addresses
446cf2
@@ -1333,15 +1324,24 @@ send_dg(res_state statp,
446cf2
 
446cf2
 		/* Check for the correct header layout and a matching
446cf2
 		   question.  */
446cf2
-		if ((recvresp1 || !res_queriesmatch(buf, buf + buflen,
446cf2
-						       *thisansp,
446cf2
-						       *thisansp
446cf2
-						       + *thisanssizp))
446cf2
-		    && (recvresp2 || !res_queriesmatch(buf2, buf2 + buflen2,
446cf2
-						       *thisansp,
446cf2
-						       *thisansp
446cf2
-						       + *thisanssizp)))
446cf2
-		  goto wait;
446cf2
+		int matching_query = 0; /* Default to no matching query.  */
446cf2
+		if (!recvresp1
446cf2
+		    && anhp->id == hp->id
446cf2
+		    && res_queriesmatch (buf, buf + buflen,
446cf2
+					 *thisansp, *thisansp + *thisanssizp))
446cf2
+		  matching_query = 1;
446cf2
+		if (!recvresp2
446cf2
+		    && anhp->id == hp2->id
446cf2
+		    && res_queriesmatch (buf2, buf2 + buflen2,
446cf2
+					 *thisansp, *thisansp + *thisanssizp))
446cf2
+		  matching_query = 2;
446cf2
+		if (matching_query == 0)
446cf2
+		  /* Spurious UDP packet.  Drop it and continue
446cf2
+		     waiting.  */
446cf2
+		  {
446cf2
+		    need_recompute = 1;
446cf2
+		    goto wait;
446cf2
+		  }
446cf2
 
446cf2
 		if (anhp->rcode == SERVFAIL ||
446cf2
 		    anhp->rcode == NOTIMP ||
446cf2
@@ -1356,7 +1356,7 @@ send_dg(res_state statp,
446cf2
 			    /* No data from the first reply.  */
446cf2
 			    resplen = 0;
446cf2
 			    /* We are waiting for a possible second reply.  */
446cf2
-			    if (hp->id == anhp->id)
446cf2
+			    if (matching_query == 1)
446cf2
 			      recvresp1 = 1;
446cf2
 			    else
446cf2
 			      recvresp2 = 1;
446cf2
@@ -1387,7 +1387,7 @@ send_dg(res_state statp,
446cf2
 			return (1);
446cf2
 		}
446cf2
 		/* Mark which reply we received.  */
446cf2
-		if (recvresp1 == 0 && hp->id == anhp->id)
446cf2
+		if (matching_query == 1)
446cf2
 			recvresp1 = 1;
446cf2
 		else
446cf2
 			recvresp2 = 1;
446cf2
diff --git a/resolv/tst-resolv-txnid-collision.c b/resolv/tst-resolv-txnid-collision.c
446cf2
new file mode 100644
446cf2
index 0000000000000000..611d37362f3e5e89
446cf2
--- /dev/null
446cf2
+++ b/resolv/tst-resolv-txnid-collision.c
446cf2
@@ -0,0 +1,329 @@
446cf2
+/* Test parallel queries with transaction ID collisions.
446cf2
+   Copyright (C) 2020 Free Software Foundation, Inc.
446cf2
+   This file is part of the GNU C Library.
446cf2
+
446cf2
+   The GNU C Library is free software; you can redistribute it and/or
446cf2
+   modify it under the terms of the GNU Lesser General Public
446cf2
+   License as published by the Free Software Foundation; either
446cf2
+   version 2.1 of the License, or (at your option) any later version.
446cf2
+
446cf2
+   The GNU C Library is distributed in the hope that it will be useful,
446cf2
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
446cf2
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
446cf2
+   Lesser General Public License for more details.
446cf2
+
446cf2
+   You should have received a copy of the GNU Lesser General Public
446cf2
+   License along with the GNU C Library; if not, see
446cf2
+   <https://www.gnu.org/licenses/>.  */
446cf2
+
446cf2
+#include <arpa/nameser.h>
446cf2
+#include <array_length.h>
446cf2
+#include <resolv-internal.h>
446cf2
+#include <resolv_context.h>
446cf2
+#include <stdbool.h>
446cf2
+#include <stdio.h>
446cf2
+#include <string.h>
446cf2
+#include <support/check.h>
446cf2
+#include <support/check_nss.h>
446cf2
+#include <support/resolv_test.h>
446cf2
+#include <support/support.h>
446cf2
+#include <support/test-driver.h>
446cf2
+
446cf2
+/* Result of parsing a DNS question name.
446cf2
+
446cf2
+   A question name has the form reorder-N-M-rcode-C.example.net, where
446cf2
+   N and M are either 0 and 1, corresponding to the reorder member,
446cf2
+   and C is a number that will be stored in the rcode field.
446cf2
+
446cf2
+   Also see parse_qname below.  */
446cf2
+struct parsed_qname
446cf2
+{
446cf2
+  /* The DNS response code requested from the first server.  The
446cf2
+     second server always responds with RCODE zero.  */
446cf2
+  int rcode;
446cf2
+
446cf2
+  /* Indicates whether to perform reordering in the responses from the
446cf2
+     respective server.  */
446cf2
+  bool reorder[2];
446cf2
+};
446cf2
+
446cf2
+/* Fills *PARSED based on QNAME.  */
446cf2
+static void
446cf2
+parse_qname (struct parsed_qname *parsed, const char *qname)
446cf2
+{
446cf2
+  int reorder0;
446cf2
+  int reorder1;
446cf2
+  int rcode;
446cf2
+  char *suffix;
446cf2
+  if (sscanf (qname, "reorder-%d-%d.rcode-%d.%ms",
446cf2
+              &reorder0, &reorder1, &rcode, &suffix) == 4)
446cf2
+    {
446cf2
+      if (reorder0 != 0)
446cf2
+        TEST_COMPARE (reorder0, 1);
446cf2
+      if (reorder1 != 0)
446cf2
+        TEST_COMPARE (reorder1, 1);
446cf2
+      TEST_VERIFY (rcode >= 0 && rcode <= 15);
446cf2
+      TEST_COMPARE_STRING (suffix, "example.net");
446cf2
+      free (suffix);
446cf2
+
446cf2
+      parsed->rcode = rcode;
446cf2
+      parsed->reorder[0] = reorder0;
446cf2
+      parsed->reorder[1] = reorder1;
446cf2
+    }
446cf2
+  else
446cf2
+    FAIL_EXIT1 ("unexpected query: %s", qname);
446cf2
+}
446cf2
+
446cf2
+/* Used to construct a response. The first server responds with an
446cf2
+   error, the second server succeeds.  */
446cf2
+static void
446cf2
+build_response (const struct resolv_response_context *ctx,
446cf2
+                struct resolv_response_builder *b,
446cf2
+                const char *qname, uint16_t qclass, uint16_t qtype)
446cf2
+{
446cf2
+  struct parsed_qname parsed;
446cf2
+  parse_qname (&parsed, qname);
446cf2
+
446cf2
+  switch (ctx->server_index)
446cf2
+    {
446cf2
+    case 0:
446cf2
+      {
446cf2
+        struct resolv_response_flags flags = { 0 };
446cf2
+        if (parsed.rcode == 0)
446cf2
+          /* Simulate a delegation in case a NODATA (RCODE zero)
446cf2
+             response is requested.  */
446cf2
+          flags.clear_ra = true;
446cf2
+        else
446cf2
+          flags.rcode = parsed.rcode;
446cf2
+
446cf2
+        resolv_response_init (b, flags);
446cf2
+        resolv_response_add_question (b, qname, qclass, qtype);
446cf2
+      }
446cf2
+      break;
446cf2
+
446cf2
+    case 1:
446cf2
+      {
446cf2
+        struct resolv_response_flags flags = { 0, };
446cf2
+        resolv_response_init (b, flags);
446cf2
+        resolv_response_add_question (b, qname, qclass, qtype);
446cf2
+
446cf2
+        resolv_response_section (b, ns_s_an);
446cf2
+        resolv_response_open_record (b, qname, qclass, qtype, 0);
446cf2
+        if (qtype == T_A)
446cf2
+          {
446cf2
+            char ipv4[4] = { 192, 0, 2, 1 };
446cf2
+            resolv_response_add_data (b, &ipv4, sizeof (ipv4));
446cf2
+          }
446cf2
+        else
446cf2
+          {
446cf2
+            char ipv6[16]
446cf2
+              = { 0x20, 0x01, 0xd, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
446cf2
+            resolv_response_add_data (b, &ipv6, sizeof (ipv6));
446cf2
+          }
446cf2
+        resolv_response_close_record (b);
446cf2
+      }
446cf2
+      break;
446cf2
+    }
446cf2
+}
446cf2
+
446cf2
+/* Used to reorder responses.  */
446cf2
+struct resolv_response_context *previous_query;
446cf2
+
446cf2
+/* Used to keep track of the queries received.  */
446cf2
+static int previous_server_index = -1;
446cf2
+static uint16_t previous_qtype;
446cf2
+
446cf2
+/* For each server, buffer the first query and then send both answers
446cf2
+   to the second query, reordered if requested.  */
446cf2
+static void
446cf2
+response (const struct resolv_response_context *ctx,
446cf2
+          struct resolv_response_builder *b,
446cf2
+          const char *qname, uint16_t qclass, uint16_t qtype)
446cf2
+{
446cf2
+  TEST_VERIFY (qtype == T_A || qtype == T_AAAA);
446cf2
+  if (ctx->server_index != 0)
446cf2
+    TEST_COMPARE (ctx->server_index, 1);
446cf2
+
446cf2
+  struct parsed_qname parsed;
446cf2
+  parse_qname (&parsed, qname);
446cf2
+
446cf2
+  if (previous_query == NULL)
446cf2
+    {
446cf2
+      /* No buffered query.  Record this query and do not send a
446cf2
+         response.  */
446cf2
+      TEST_COMPARE (previous_qtype, 0);
446cf2
+      previous_query = resolv_response_context_duplicate (ctx);
446cf2
+      previous_qtype = qtype;
446cf2
+      resolv_response_drop (b);
446cf2
+      previous_server_index = ctx->server_index;
446cf2
+
446cf2
+      if (test_verbose)
446cf2
+        printf ("info: buffering first query for: %s\n", qname);
446cf2
+    }
446cf2
+  else
446cf2
+    {
446cf2
+      TEST_VERIFY (previous_query != 0);
446cf2
+      TEST_COMPARE (ctx->server_index, previous_server_index);
446cf2
+      TEST_VERIFY (previous_qtype != qtype); /* Not a duplicate.  */
446cf2
+
446cf2
+      /* If reordering, send a response for this query explicitly, and
446cf2
+         then skip the implicit send.  */
446cf2
+      if (parsed.reorder[ctx->server_index])
446cf2
+        {
446cf2
+          if (test_verbose)
446cf2
+            printf ("info: sending reordered second response for: %s\n",
446cf2
+                    qname);
446cf2
+          build_response (ctx, b, qname, qclass, qtype);
446cf2
+          resolv_response_send_udp (ctx, b);
446cf2
+          resolv_response_drop (b);
446cf2
+        }
446cf2
+
446cf2
+      /* Build a response for the previous query and send it, thus
446cf2
+         reordering the two responses.  */
446cf2
+      {
446cf2
+        if (test_verbose)
446cf2
+          printf ("info: sending first response for: %s\n", qname);
446cf2
+        struct resolv_response_builder *btmp
446cf2
+          = resolv_response_builder_allocate (previous_query->query_buffer,
446cf2
+                                              previous_query->query_length);
446cf2
+        build_response (ctx, btmp, qname, qclass, previous_qtype);
446cf2
+        resolv_response_send_udp (ctx, btmp);
446cf2
+        resolv_response_builder_free (btmp);
446cf2
+      }
446cf2
+
446cf2
+      /* If not reordering, send the reply as usual.  */
446cf2
+      if (!parsed.reorder[ctx->server_index])
446cf2
+        {
446cf2
+          if (test_verbose)
446cf2
+            printf ("info: sending non-reordered second response for: %s\n",
446cf2
+                    qname);
446cf2
+          build_response (ctx, b, qname, qclass, qtype);
446cf2
+        }
446cf2
+
446cf2
+      /* Unbuffer the response and prepare for the next query.  */
446cf2
+      resolv_response_context_free (previous_query);
446cf2
+      previous_query = NULL;
446cf2
+      previous_qtype = 0;
446cf2
+      previous_server_index = -1;
446cf2
+    }
446cf2
+}
446cf2
+
446cf2
+/* Runs a query for QNAME and checks for the expected reply.  See
446cf2
+   struct parsed_qname for the expected format for QNAME.  */
446cf2
+static void
446cf2
+test_qname (const char *qname, int rcode)
446cf2
+{
446cf2
+  struct resolv_context *ctx = __resolv_context_get ();
446cf2
+  TEST_VERIFY_EXIT (ctx != NULL);
446cf2
+
446cf2
+  unsigned char q1[512];
446cf2
+  int q1len = res_mkquery (QUERY, qname, C_IN, T_A, NULL, 0, NULL,
446cf2
+                           q1, sizeof (q1));
446cf2
+  TEST_VERIFY_EXIT (q1len > 12);
446cf2
+
446cf2
+  unsigned char q2[512];
446cf2
+  int q2len = res_mkquery (QUERY, qname, C_IN, T_AAAA, NULL, 0, NULL,
446cf2
+                           q2, sizeof (q2));
446cf2
+  TEST_VERIFY_EXIT (q2len > 12);
446cf2
+
446cf2
+  /* Produce a transaction ID collision.  */
446cf2
+  memcpy (q2, q1, 2);
446cf2
+
446cf2
+  unsigned char ans1[512];
446cf2
+  unsigned char *ans1p = ans1;
446cf2
+  unsigned char *ans2p = NULL;
446cf2
+  int nans2p = 0;
446cf2
+  int resplen2 = 0;
446cf2
+  int ans2p_malloced = 0;
446cf2
+
446cf2
+  /* Perform a parallel A/AAAA query.  */
446cf2
+  int resplen1 = __res_context_send (ctx, q1, q1len, q2, q2len,
446cf2
+                                     ans1, sizeof (ans1), &ans1p,
446cf2
+                                     &ans2p, &nans2p,
446cf2
+                                     &resplen2, &ans2p_malloced);
446cf2
+
446cf2
+  TEST_VERIFY (resplen1 > 12);
446cf2
+  TEST_VERIFY (resplen2 > 12);
446cf2
+  if (resplen1 <= 12 || resplen2 <= 12)
446cf2
+    return;
446cf2
+
446cf2
+  if (rcode == 1 || rcode == 3)
446cf2
+    {
446cf2
+      /* Format Error and Name Error responses does not trigger
446cf2
+         switching to the next server.  */
446cf2
+      TEST_COMPARE (ans1p[3] & 0x0f, rcode);
446cf2
+      TEST_COMPARE (ans2p[3] & 0x0f, rcode);
446cf2
+      return;
446cf2
+    }
446cf2
+
446cf2
+  /* The response should be successful.  */
446cf2
+  TEST_COMPARE (ans1p[3] & 0x0f, 0);
446cf2
+  TEST_COMPARE (ans2p[3] & 0x0f, 0);
446cf2
+
446cf2
+  /* Due to bug 19691, the answer may not be in the slot matching the
446cf2
+     query.  Assume that the AAAA response is the longer one.  */
446cf2
+  unsigned char *a_answer;
446cf2
+  int a_answer_length;
446cf2
+  unsigned char *aaaa_answer;
446cf2
+  int aaaa_answer_length;
446cf2
+  if (resplen2 > resplen1)
446cf2
+    {
446cf2
+      a_answer = ans1p;
446cf2
+      a_answer_length = resplen1;
446cf2
+      aaaa_answer = ans2p;
446cf2
+      aaaa_answer_length = resplen2;
446cf2
+    }
446cf2
+  else
446cf2
+    {
446cf2
+      a_answer = ans2p;
446cf2
+      a_answer_length = resplen2;
446cf2
+      aaaa_answer = ans1p;
446cf2
+      aaaa_answer_length = resplen1;
446cf2
+    }
446cf2
+
446cf2
+  {
446cf2
+    char *expected = xasprintf ("name: %s\n"
446cf2
+                                "address: 192.0.2.1\n",
446cf2
+                                qname);
446cf2
+    check_dns_packet (qname, a_answer, a_answer_length, expected);
446cf2
+    free (expected);
446cf2
+  }
446cf2
+  {
446cf2
+    char *expected = xasprintf ("name: %s\n"
446cf2
+                                "address: 2001:db8::1\n",
446cf2
+                                qname);
446cf2
+    check_dns_packet (qname, aaaa_answer, aaaa_answer_length, expected);
446cf2
+    free (expected);
446cf2
+  }
446cf2
+
446cf2
+  if (ans2p_malloced)
446cf2
+    free (ans2p);
446cf2
+
446cf2
+  __resolv_context_put (ctx);
446cf2
+}
446cf2
+
446cf2
+static int
446cf2
+do_test (void)
446cf2
+{
446cf2
+  struct resolv_test *aux = resolv_test_start
446cf2
+    ((struct resolv_redirect_config)
446cf2
+     {
446cf2
+       .response_callback = response,
446cf2
+     });
446cf2
+
446cf2
+  for (int rcode = 0; rcode <= 5; ++rcode)
446cf2
+    for (int do_reorder_0 = 0; do_reorder_0 < 2; ++do_reorder_0)
446cf2
+      for (int do_reorder_1 = 0; do_reorder_1 < 2; ++do_reorder_1)
446cf2
+        {
446cf2
+          char *qname = xasprintf ("reorder-%d-%d.rcode-%d.example.net",
446cf2
+                                   do_reorder_0, do_reorder_1, rcode);
446cf2
+          test_qname (qname, rcode);
446cf2
+          free (qname);
446cf2
+        }
446cf2
+
446cf2
+  resolv_test_end (aux);
446cf2
+
446cf2
+  return 0;
446cf2
+}
446cf2
+
446cf2
+#include <support/test-driver.c>