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