test: dns-packet: pin suffix compression past the pointer range

The existing case only covered a fully-uncompressed re-occurrence. Every
DNS-SD name shares an in-range suffix, so also pin that a name first
seen beyond the pointer range still compresses from its mapped suffix --
one label plus a two-byte pointer per occurrence, map unchanged, and the
pointer addressing the suffix's prior occurrence rather than any
in-range octet.

A padding helper puts the next append at a chosen offset, for the two
sides of the boundary: at 0x3FFF the name is still mapped and re-occurs
as a pointer whose wire bytes are checked independently of the parser;
one past it, it is not mapped.
This commit is contained in:
Christian Glombek
2026-09-07 04:26:36 +02:00
parent a95358da6f
commit a4b109f01f
+119 -14
View File
@@ -12,6 +12,7 @@
#include "log.h"
#include "stdio-util.h"
#include "tests.h"
#include "unaligned.h"
#define BIT_QR (1 << 7)
#define BIT_AA (1 << 2)
@@ -1302,20 +1303,6 @@ TEST(packet_append_key_name_too_long) {
ASSERT_EQ(packet->size, 12U);
}
/* Pad the packet to exactly `target` bytes, so the next append starts at a chosen offset. Opaque
* filler: nothing parses it, and it leaves the compression map empty. */
static void pad_packet_to(DnsPacket *p, size_t target) {
_cleanup_free_ void *filler = NULL;
size_t gap;
ASSERT_GT(target, p->size);
gap = target - p->size;
ASSERT_NOT_NULL(filler = malloc0(gap));
ASSERT_OK(dns_packet_append_blob(p, filler, gap, NULL));
ASSERT_EQ(p->size, target);
ASSERT_EQ(hashmap_size(p->names), 0u);
}
/* The reader's label-type dispatch. RFC 1035 section 4.1.4 gives a length octet the top bits 00 and
* a compression pointer 11, and reserves 01 and 10: it takes both bits to make a pointer. The
* reserved octet points at a real name here, so a dispatch accepting either bit follows it and
@@ -1345,6 +1332,14 @@ TEST(packet_read_name_rejects_reserved_label_types) {
}
}
/* Every offset in the compression map must be pointer-expressible. */
static void assert_offsets_in_pointer_range(const DnsPacket *p) {
void *v;
HASHMAP_FOREACH(v, p->names)
ASSERT_LE(PTR_TO_SIZE(v), (size_t) DNS_COMPRESSION_OFFSET_MAX);
}
TEST(packet_append_name_beyond_compression_pointer_range) {
_cleanup_(dns_packet_unrefp) DnsPacket *packet = NULL;
size_t start;
@@ -1382,6 +1377,7 @@ TEST(packet_append_name_beyond_compression_pointer_range) {
/* allow_compression= */ true, /* canonical_candidate= */ false,
NULL));
ASSERT_EQ(hashmap_size(packet->names), n_mapped);
assert_offsets_in_pointer_range(packet);
/* Names first seen within pointer range still compress: re-appending one takes exactly the
* two bytes of a compression pointer. */
@@ -1402,6 +1398,115 @@ TEST(packet_append_name_beyond_compression_pointer_range) {
dns_packet_rewind(packet, before);
ASSERT_OK(dns_packet_read_name(packet, &parsed, /* allow_compression= */ true, NULL));
ASSERT_STREQ(parsed, "filler0.example.com");
/* A name beyond the pointer range whose suffix IS mapped in range -- the shape every DNS-SD
* name hits via ".local" -- must still compress from that suffix: one label plus a two-byte
* pointer, on every occurrence, with the map unchanged. */
n_mapped = hashmap_size(packet->names);
for (size_t i = 0; i < 2; i++) {
size_t at, occurrence_start = packet->size;
ASSERT_OK(dns_packet_append_name(packet, "far.example.com",
/* allow_compression= */ true,
/* canonical_candidate= */ false,
&at));
ASSERT_GT(at, (size_t) DNS_COMPRESSION_OFFSET_MAX);
ASSERT_EQ(packet->size, occurrence_start + 1 + STRLEN("far") + 2);
ASSERT_EQ(hashmap_size(packet->names), n_mapped);
assert_offsets_in_pointer_range(packet);
/* The pointer must address the suffix's mapped prior occurrence, not just any
* in-range length octet (RFC 1035 section 4.1.4): compare it with the map. */
size_t suffix = PTR_TO_SIZE(hashmap_get(packet->names, "example.com"));
size_t pointer_at = occurrence_start + 1 + STRLEN("far");
ASSERT_GT(suffix, 0u);
ASSERT_LT(suffix, occurrence_start);
ASSERT_EQ(unaligned_read_be16(DNS_PACKET_DATA(packet) + pointer_at),
(uint16_t) (DNS_COMPRESSION_POINTER_FLAG | suffix));
parsed = mfree(parsed);
dns_packet_rewind(packet, at);
ASSERT_OK(dns_packet_read_name(packet, &parsed, /* allow_compression= */ true, NULL));
ASSERT_STREQ(parsed, "far.example.com");
}
}
/* Pad the packet to exactly `target` bytes, so the next append starts at a chosen offset. Opaque
* filler: nothing parses it, and it leaves the compression map empty. */
static void pad_packet_to(DnsPacket *p, size_t target) {
_cleanup_free_ void *filler = NULL;
size_t gap;
ASSERT_GT(target, p->size);
gap = target - p->size;
ASSERT_NOT_NULL(filler = malloc0(gap));
ASSERT_OK(dns_packet_append_blob(p, filler, gap, NULL));
ASSERT_EQ(p->size, target);
ASSERT_EQ(hashmap_size(p->names), 0u);
}
TEST(packet_append_name_at_compression_offset_max) {
_cleanup_(dns_packet_unrefp) DnsPacket *packet = NULL;
size_t start;
/* A name whose first label starts at exactly the last pointer-expressible offset must be
* mapped and referenced by pointer... */
ASSERT_OK(dns_packet_new(&packet, DNS_PROTOCOL_DNS, 0, DNS_PACKET_SIZE_MAX));
pad_packet_to(packet, DNS_COMPRESSION_OFFSET_MAX);
ASSERT_OK(dns_packet_append_name(packet, "edge.example.org",
/* allow_compression= */ true, /* canonical_candidate= */ false,
&start));
ASSERT_EQ(start, (size_t) DNS_COMPRESSION_OFFSET_MAX);
/* Only the whole name is mapped: its suffixes start past the bound. */
ASSERT_EQ(hashmap_size(packet->names), 1u);
size_t before = packet->size;
ASSERT_OK(dns_packet_append_name(packet, "edge.example.org",
/* allow_compression= */ true, /* canonical_candidate= */ false,
NULL));
ASSERT_EQ(packet->size, before + 2);
/* The pointer's wire bytes, independent of the parser: offset 0x3FFF under the tag bits. */
ASSERT_EQ(DNS_PACKET_DATA(packet)[before], 0xFFu);
ASSERT_EQ(DNS_PACKET_DATA(packet)[before + 1], 0xFFu);
_cleanup_free_ char *parsed = NULL;
dns_packet_rewind(packet, before);
ASSERT_OK(dns_packet_read_name(packet, &parsed, /* allow_compression= */ true, NULL));
ASSERT_STREQ(parsed, "edge.example.org");
/* The same two bytes must be refused, not followed, where compression is not allowed. */
parsed = mfree(parsed);
dns_packet_rewind(packet, before);
ASSERT_ERROR(dns_packet_read_name(packet, &parsed, /* allow_compression= */ false, NULL),
EBADMSG);
}
TEST(packet_append_name_past_compression_offset_max) {
_cleanup_(dns_packet_unrefp) DnsPacket *packet = NULL;
_cleanup_free_ char *parsed = NULL;
size_t start, before;
/* One byte past the last pointer-expressible offset, a name stays out of the map and every
* occurrence is re-emitted as labels. */
ASSERT_OK(dns_packet_new(&packet, DNS_PROTOCOL_DNS, 0, DNS_PACKET_SIZE_MAX));
pad_packet_to(packet, DNS_COMPRESSION_OFFSET_MAX + 1);
ASSERT_OK(dns_packet_append_name(packet, "past.example.org",
/* allow_compression= */ true, /* canonical_candidate= */ false,
&start));
ASSERT_EQ(start, (size_t) DNS_COMPRESSION_OFFSET_MAX + 1);
ASSERT_EQ(hashmap_size(packet->names), 0u);
before = packet->size;
ASSERT_OK(dns_packet_append_name(packet, "past.example.org",
/* allow_compression= */ true, /* canonical_candidate= */ false,
NULL));
ASSERT_EQ(packet->size, before + STRLEN("past.example.org") + 2);
dns_packet_rewind(packet, before);
ASSERT_OK(dns_packet_read_name(packet, &parsed, /* allow_compression= */ true, NULL));
ASSERT_STREQ(parsed, "past.example.org");
}
/* The same case via dns_packet_append_rr(), where the name append is nested inside the rdlength