dns_packet_append_name() added every name it appends to the packet's compression map, including names written at offsets >= 0x4000 that an RFC 1035 compression pointer (14 bits) can never reference. Such entries are not just useless -- when the same name occurs again, the lookup finds it, the pointer cannot be emitted, the code falls through to appending the labels again, and the re-insertion into the map then fails the whole packet build with hashmap_ensure_put() returning -EEXIST. This is easily triggered with mDNS: an unsolicited DNS-SD announcement covering a zone with a few hundred services exceeds 16KiB, and dns_scope_announce() then fails with "Failed to build reply packet: File exists". This lifts the failure only: dns_scope_announce() still builds the whole zone into one packet and hands it to the socket as a single datagram, fragmented as the link requires. Splitting announcements across MTU-sized packets, as RFC 6762 section 17 asks, is done separately. Only insert a name into the map if its offset is pointer-expressible, making "mapped implies referenceable" a map invariant. Re-occurrences of out-of-range names are emitted as labels again -- the only encoding RFC 1035 section 4.1.4 permits in that case -- still compressed from the first suffix that is mapped in range. The lookup restates that range as control flow rather than asserting it. Under -Db_ndebug=true, which CI builds, assert() is __builtin_unreachable(), so an out-of-range entry would put 0xC000 | n truncated to 14 bits on the wire -- a pointer to an arbitrary earlier offset -- where falling through emits the labels instead. This layer is shared with unicast DNS, DoT and LLMNR. The pointer's tag bits and its offset bound are now named once, in dns-def.h next to the other wire-format constants rather than in dns-packet.h, since the format is RFC 1035's and not this packet abstraction's -- dhcp-message.c open-codes the same two bits to parse compressed names out of DHCP options. Compile-time checks pin that the two partition the 16-bit field and that the tag is its top two bits, which is what lets the reader take it from the first octet alone. The offset's other bound becomes control flow along with it: it was an assert(), which under -Db_ndebug=true is no check at all, for the bound whose violation puts a pointer to the wrong bytes on the wire. It is also tightened to the start of the name being appended rather than the end of the packet, which is the "prior occurrence" RFC 1035 section 4.1.4 allows a pointer to address and what dns_packet_read_name() enforces on the way in. Add regression tests, including one through dns_packet_append_rr() -- the path the reported failure came from, where the name append is nested inside the rdlength backpatch and two further rollbacks -- and one for the reader's label-type dispatch, which the renamed constants touch.
30 lines
1.3 KiB
C
30 lines
1.3 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
#pragma once
|
|
|
|
#include "forward.h"
|
|
|
|
/* Length of a single label, with all escaping removed, excluding any trailing dot or NUL byte */
|
|
#define DNS_LABEL_MAX 63
|
|
|
|
/* Worst case length of a single label, with all escaping applied and room for a trailing NUL byte. */
|
|
#define DNS_LABEL_ESCAPED_MAX (DNS_LABEL_MAX*4+1)
|
|
|
|
/* Maximum length of a full hostname, consisting of a series of unescaped labels, and no trailing dot or NUL byte */
|
|
#define DNS_HOSTNAME_MAX 253
|
|
|
|
/* Maximum length of a full hostname, on the wire, including the final NUL byte */
|
|
#define DNS_WIRE_FORMAT_HOSTNAME_MAX 255
|
|
|
|
/* Maximum number of labels per valid hostname */
|
|
#define DNS_N_LABELS_MAX 127
|
|
|
|
/* RFC 1035 § 4.1.4 name compression: the two high bits tag a 16-bit field as a pointer, the
|
|
* remaining 14 bits carry the offset, which is hence also the last position a name a pointer can
|
|
* reference may start at. */
|
|
#define DNS_COMPRESSION_POINTER_FLAG UINT16_C(0xC000)
|
|
#define DNS_COMPRESSION_OFFSET_MAX UINT16_C(0x3FFF)
|
|
assert_cc((DNS_COMPRESSION_POINTER_FLAG & DNS_COMPRESSION_OFFSET_MAX) == 0);
|
|
assert_cc((DNS_COMPRESSION_POINTER_FLAG | DNS_COMPRESSION_OFFSET_MAX) == UINT16_MAX);
|
|
/* The tag is the top two bits, which is what lets a reader take it from the first octet alone. */
|
|
assert_cc((DNS_COMPRESSION_POINTER_FLAG & UINT16_C(0xFF)) == 0);
|