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