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