Commit 4a97559bc3 for bind

commit 4a97559bc3b2d9b518f5cb9306a94a780d67a33d
Author: OndÅ™ej Surý <ondrej@isc.org>
Date:   Fri May 1 13:36:23 2026 +0200

    Restrict name decompression to RFC-permitted rdata types

    The DNS message parser previously accepted compression pointers in
    all rdata types, regardless of whether the type's specification
    allows compression. RFC 3597 §4 limits compressed names to the RR
    types defined by RFC 1035 (and a small set of subsequent
    extensions); all other types must transmit names uncompressed.

    Switch dns_message_parse() from DNS_DECOMPRESS_ALWAYS to
    DNS_DECOMPRESS_DEFAULT so per-RR-type fromwire handlers must opt
    into decompression via dns_decompress_setpermitted(); compression
    pointers in unauthorized types are then rejected with
    DNS_R_DISALLOWED in dns_name_fromwire().

diff --git a/bin/tests/system/formerr/tests_formerr.py b/bin/tests/system/formerr/tests_formerr.py
index dd9483d144..c6ba3b7d03 100644
--- a/bin/tests/system/formerr/tests_formerr.py
+++ b/bin/tests/system/formerr/tests_formerr.py
@@ -531,6 +531,25 @@ def query_raw_tcp(host: str, port: int, packet_wire: bytes) -> bytes:
             formerr_response_header(message_id=67),
             id="tsignotlast",
         ),
+        pytest.param(
+            wire(
+                header(qdcount=1, arcount=1),
+                question(root(), dns.rdatatype.RdataType.A),
+                rr(
+                    root(),
+                    dns.rdatatype.RdataType.DNAME,
+                    dns.rdataclass.RdataClass.IN,
+                    # DNAME rdata may not be compressed (RFC 3597
+                    # section 4); this pointer aims at the QNAME.
+                    rdata=b"\xc0\x0c",
+                ),
+            ),
+            wire(
+                formerr_response_header(qdcount=1),
+                question(root(), dns.rdatatype.RdataType.A),
+            ),
+            id="compressedrdata",
+        ),
     ],
 )
 def test_formerr(
diff --git a/doc/dev/dev.md b/doc/dev/dev.md
index 4c2ff05962..ac083cc357 100644
--- a/doc/dev/dev.md
+++ b/doc/dev/dev.md
@@ -1191,7 +1191,7 @@ presentation format, and stores it in a buffer for later printing.
         static isc_result_t
         fromwire[_<class>]_<type>(int rdclass, dns_rdatatype_t type,
                                   isc_buffer_t *source,
-                                  dns_decompress_t *dctx,
+                                  dns_decompress_t dctx,
                                   unsigned int options,
                                   isc_buffer_t *target_t);

diff --git a/fuzz/dns_name_fromwire.c b/fuzz/dns_name_fromwire.c
index 0ec8f8710d..72ae7a11e6 100644
--- a/fuzz/dns_name_fromwire.c
+++ b/fuzz/dns_name_fromwire.c
@@ -41,7 +41,6 @@ LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
 	dns_fixedname_t old_fixed;
 	dns_name_t *new_name = dns_fixedname_initname(&new_fixed);
 	dns_name_t *old_name = dns_fixedname_initname(&old_fixed);
-	dns_decompress_t dctx = DNS_DECOMPRESS_PERMITTED;
 	isc_buffer_t new_buf;
 	isc_buffer_t old_buf;

@@ -69,13 +68,15 @@ LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
 	isc_buffer_add(&new_buf, size);
 	isc_buffer_setactive(&new_buf, size);
 	isc_buffer_forward(&new_buf, size / 2);
-	new_result = dns_name_fromwire(new_name, &new_buf, dctx, NULL);
+	new_result = dns_name_fromwire(new_name, &new_buf,
+				       DNS_DECOMPRESS_PERMITTED, NULL);

 	isc_buffer_constinit(&old_buf, data, size);
 	isc_buffer_add(&old_buf, size);
 	isc_buffer_setactive(&old_buf, size);
 	isc_buffer_forward(&old_buf, size / 2);
-	old_result = old_name_fromwire(old_name, &old_buf, dctx, 0, NULL);
+	old_result = old_name_fromwire(old_name, &old_buf,
+				       DNS_DECOMPRESS_PERMITTED, 0, NULL);

 	REQUIRE(new_result == old_result);
 	REQUIRE(dns_name_equal(new_name, old_name));
diff --git a/lib/dns/include/dns/compress.h b/lib/dns/include/dns/compress.h
index 05aa2955d4..c6cf2fb45e 100644
--- a/lib/dns/include/dns/compress.h
+++ b/lib/dns/include/dns/compress.h
@@ -99,7 +99,7 @@ struct dns_compress {
 };

 /*
- * Deompression context
+ * Decompression context
  */
 enum dns_decompress {
 	DNS_DECOMPRESS_DEFAULT,
@@ -234,7 +234,7 @@ dns_compress_rollback(dns_compress_t *cctx, unsigned int offset);
  *	Set whether decompression is allowed, according to RFC 3597
  */
 static inline dns_decompress_t /* inline to suppress code generation */
-dns_decompress_setpermitted(dns_decompress_t dctx, bool permitted) {
+dns_decompress_setpermitted(const dns_decompress_t dctx, bool permitted) {
 	if (dctx == DNS_DECOMPRESS_NEVER || dctx == DNS_DECOMPRESS_ALWAYS) {
 		return dctx;
 	} else if (permitted) {
@@ -248,7 +248,7 @@ dns_decompress_setpermitted(dns_decompress_t dctx, bool permitted) {
  *	Returns whether decompression is allowed here
  */
 static inline bool /* inline to suppress code generation */
-dns_decompress_getpermitted(dns_decompress_t dctx) {
+dns_decompress_getpermitted(const dns_decompress_t dctx) {
 	return dctx == DNS_DECOMPRESS_ALWAYS ||
 	       dctx == DNS_DECOMPRESS_PERMITTED;
 }
diff --git a/lib/dns/include/dns/name.h b/lib/dns/include/dns/name.h
index b207c3767c..d45aa859df 100644
--- a/lib/dns/include/dns/name.h
+++ b/lib/dns/include/dns/name.h
@@ -734,8 +734,8 @@ dns_name_toregion(const dns_name_t *name, isc_region_t *r) {
  */

 isc_result_t
-dns_name_fromwire(dns_name_t *name, isc_buffer_t *source, dns_decompress_t dctx,
-		  isc_buffer_t *target);
+dns_name_fromwire(dns_name_t *name, isc_buffer_t *source,
+		  const dns_decompress_t dctx, isc_buffer_t *target);
 /*%<
  * Copy the possibly-compressed name at source (active region) into target,
  * decompressing it.
diff --git a/lib/dns/journal.c b/lib/dns/journal.c
index 3ce88ede2d..fd7a49ad2b 100644
--- a/lib/dns/journal.c
+++ b/lib/dns/journal.c
@@ -328,7 +328,6 @@ struct dns_journal {
 		uint32_t current_serial; /*%< Current SOA serial */
 		isc_buffer_t source;	 /*%< Data from disk */
 		isc_buffer_t target;	 /*%< Data from _fromwire check */
-		dns_decompress_t dctx;	 /*%< Dummy decompression ctx */
 		dns_name_t name;	 /*%< Current domain name */
 		dns_rdata_t rdata;	 /*%< Current rdata */
 		uint32_t ttl;		 /*%< Current TTL */
@@ -702,7 +701,6 @@ journal_open(isc_mem_t *mctx, const char *filename, bool writable, bool create,
 	 */
 	isc_buffer_initnull(&j->it.source);
 	isc_buffer_initnull(&j->it.target);
-	j->it.dctx = DNS_DECOMPRESS_NEVER;

 	j->state = writable ? JOURNAL_STATE_WRITE : JOURNAL_STATE_READ;

@@ -2007,8 +2005,8 @@ read_one_rr(dns_journal_t *j) {
 	 */
 	isc_buffer_setactive(&j->it.source,
 			     j->it.source.used - j->it.source.current);
-	CHECK(dns_name_fromwire(&j->it.name, &j->it.source, j->it.dctx,
-				&j->it.target));
+	CHECK(dns_name_fromwire(&j->it.name, &j->it.source,
+				DNS_DECOMPRESS_NEVER, &j->it.target));

 	/*
 	 * Check that the RR header is there, and parse it.
@@ -2040,7 +2038,7 @@ read_one_rr(dns_journal_t *j) {
 	isc_buffer_setactive(&j->it.source, rdlen);
 	dns_rdata_reset(&j->it.rdata);
 	CHECK(dns_rdata_fromwire(&j->it.rdata, rdclass, rdtype, &j->it.source,
-				 j->it.dctx, &j->it.target));
+				 DNS_DECOMPRESS_NEVER, &j->it.target));
 	j->it.ttl = ttl;

 	j->it.xpos += sizeof(journal_rawrrhdr_t) + rrhdr.size;
diff --git a/lib/dns/master.c b/lib/dns/master.c
index cc8b0d5a9c..910aaddd06 100644
--- a/lib/dns/master.c
+++ b/lib/dns/master.c
@@ -2290,10 +2290,8 @@ load_raw(dns_loadctx_t *lctx) {
 	int target_size = TSIZ;
 	isc_buffer_t target, buf;
 	unsigned char *target_mem = NULL;
-	dns_decompress_t dctx;

 	callbacks = lctx->callbacks;
-	dctx = DNS_DECOMPRESS_NEVER;

 	if (lctx->first) {
 		RETERR(load_header(lctx));
@@ -2412,7 +2410,8 @@ load_raw(dns_loadctx_t *lctx) {
 				     &totallen));

 		isc_buffer_setactive(&target, (unsigned int)namelen);
-		CHECK(dns_name_fromwire(name, &target, dctx, NULL));
+		CHECK(dns_name_fromwire(name, &target, DNS_DECOMPRESS_NEVER,
+					NULL));

 		if ((lctx->options & DNS_MASTER_CHECKTTL) != 0 &&
 		    rdatalist.ttl > lctx->maxttl)
@@ -2492,8 +2491,8 @@ load_raw(dns_loadctx_t *lctx) {
 			isc_buffer_init(&buf, isc_buffer_current(&target),
 					(unsigned int)rdlen);
 			CHECK(dns_rdata_fromwire(&rdata[i], rdatalist.rdclass,
-						 rdatalist.type, &target, dctx,
-						 &buf));
+						 rdatalist.type, &target,
+						 DNS_DECOMPRESS_NEVER, &buf));
 			ISC_LIST_APPEND(rdatalist.rdata, &rdata[i], link);
 		}

diff --git a/lib/dns/message.c b/lib/dns/message.c
index 7b7af72769..8acac1f57b 100644
--- a/lib/dns/message.c
+++ b/lib/dns/message.c
@@ -826,8 +826,7 @@ dns_message_findtype(dns_name_t *name, dns_rdatatype_t type,
  * Read a name from buffer "source".
  */
 static isc_result_t
-getname(dns_name_t *name, isc_buffer_t *source, dns_message_t *msg,
-	dns_decompress_t dctx) {
+getname(dns_name_t *name, isc_buffer_t *source, dns_message_t *msg) {
 	isc_buffer_t *scratch;
 	isc_result_t result;
 	unsigned int tries;
@@ -840,7 +839,8 @@ getname(dns_name_t *name, isc_buffer_t *source, dns_message_t *msg,
 	 */
 	tries = 0;
 	while (tries < 2) {
-		result = dns_name_fromwire(name, source, dctx, scratch);
+		result = dns_name_fromwire(name, source, DNS_DECOMPRESS_ALWAYS,
+					   scratch);

 		if (result == ISC_R_NOSPACE) {
 			tries++;
@@ -857,9 +857,8 @@ getname(dns_name_t *name, isc_buffer_t *source, dns_message_t *msg,
 }

 static isc_result_t
-getrdata(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx,
-	 dns_rdataclass_t rdclass, dns_rdatatype_t rdtype,
-	 unsigned int rdatalen, dns_rdata_t *rdata) {
+getrdata(isc_buffer_t *source, dns_message_t *msg, dns_rdataclass_t rdclass,
+	 dns_rdatatype_t rdtype, unsigned int rdatalen, dns_rdata_t *rdata) {
 	isc_buffer_t *scratch;
 	isc_result_t result;
 	unsigned int tries;
@@ -881,7 +880,7 @@ getrdata(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx,
 	/* XXX possibly change this to a while (tries < 2) loop */
 	for (;;) {
 		result = dns_rdata_fromwire(rdata, rdclass, rdtype, source,
-					    dctx, scratch);
+					    DNS_DECOMPRESS_DEFAULT, scratch);

 		if (result == ISC_R_NOSPACE) {
 			if (tries == 0) {
@@ -901,6 +900,15 @@ getrdata(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx,
 			newbuffer(msg, trysize);

 			scratch = currentbuffer(msg);
+		} else if (result == DNS_R_DISALLOWED) {
+			/*
+			 * A compression pointer in a type whose
+			 * specification forbids one (RFC 3597 section 4)
+			 * is a malformed message, not something we have
+			 * declined by policy, so report it the way every
+			 * other bad name is reported: as a format error.
+			 */
+			return DNS_R_FORMERR;
 		} else {
 			return result;
 		}
@@ -927,8 +935,7 @@ cleanup_name_hashmaps(dns_namelist_t *section) {
 }

 static isc_result_t
-getquestions(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx,
-	     unsigned int options) {
+getquestions(isc_buffer_t *source, dns_message_t *msg, unsigned int options) {
 	isc_region_t r;
 	unsigned int count;
 	dns_name_t *name = NULL;
@@ -954,7 +961,7 @@ getquestions(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx,
 		 */
 		isc_buffer_remainingregion(source, &r);
 		isc_buffer_setactive(source, r.length);
-		CHECK(getname(name, source, msg, dctx));
+		CHECK(getname(name, source, msg));

 		ISC_LIST_APPEND(*section, name, link);

@@ -1052,8 +1059,8 @@ update(dns_section_t section, dns_rdataclass_t rdclass) {
 }

 static isc_result_t
-getsection(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx,
-	   dns_section_t sectionid, unsigned int options) {
+getsection(isc_buffer_t *source, dns_message_t *msg, dns_section_t sectionid,
+	   unsigned int options) {
 	isc_region_t r;
 	unsigned int count, rdatalen;
 	dns_name_t *name = NULL;
@@ -1098,7 +1105,7 @@ getsection(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx,
 		 */
 		isc_buffer_remainingregion(source, &r);
 		isc_buffer_setactive(source, r.length);
-		CHECK(getname(name, source, msg, dctx));
+		CHECK(getname(name, source, msg));

 		/*
 		 * Get type, class, ttl, and rdatalen.  Verify that at least
@@ -1249,10 +1256,10 @@ getsection(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx,
 			   msg->opcode == dns_opcode_update &&
 			   sectionid == DNS_SECTION_UPDATE)
 		{
-			result = getrdata(source, msg, dctx, msg->rdclass,
-					  rdtype, rdatalen, rdata);
+			result = getrdata(source, msg, msg->rdclass, rdtype,
+					  rdatalen, rdata);
 		} else {
-			result = getrdata(source, msg, dctx, rdclass, rdtype,
+			result = getrdata(source, msg, rdclass, rdtype,
 					  rdatalen, rdata);
 		}
 		if (result != ISC_R_SUCCESS) {
@@ -1574,7 +1581,6 @@ isc_result_t
 dns_message_parse(dns_message_t *msg, isc_buffer_t *source,
 		  unsigned int options) {
 	isc_region_t r;
-	dns_decompress_t dctx;
 	isc_result_t result;
 	uint16_t tmpflags;
 	isc_buffer_t origsource;
@@ -1622,15 +1628,13 @@ dns_message_parse(dns_message_t *msg, isc_buffer_t *source,
 	msg->header_ok = 1;
 	msg->state = DNS_SECTION_QUESTION;

-	dctx = DNS_DECOMPRESS_ALWAYS;
-
 	bool strict_parse = ((options & DNS_MESSAGEPARSE_BESTEFFORT) == 0);
 	isc_result_t early_check_ret = early_sanity_check(msg);
 	if (strict_parse && (early_check_ret != ISC_R_SUCCESS)) {
 		return early_check_ret;
 	}

-	result = getquestions(source, msg, dctx, options);
+	result = getquestions(source, msg, options);

 	if (result == ISC_R_UNEXPECTEDEND && ignore_tc) {
 		goto truncated;
@@ -1644,7 +1648,7 @@ dns_message_parse(dns_message_t *msg, isc_buffer_t *source,
 	}
 	msg->question_ok = 1;

-	result = getsection(source, msg, dctx, DNS_SECTION_ANSWER, options);
+	result = getsection(source, msg, DNS_SECTION_ANSWER, options);
 	if (result == ISC_R_UNEXPECTEDEND && ignore_tc) {
 		goto truncated;
 	}
@@ -1656,7 +1660,7 @@ dns_message_parse(dns_message_t *msg, isc_buffer_t *source,
 		return result;
 	}

-	result = getsection(source, msg, dctx, DNS_SECTION_AUTHORITY, options);
+	result = getsection(source, msg, DNS_SECTION_AUTHORITY, options);
 	if (result == ISC_R_UNEXPECTEDEND && ignore_tc) {
 		goto truncated;
 	}
@@ -1668,7 +1672,7 @@ dns_message_parse(dns_message_t *msg, isc_buffer_t *source,
 		return result;
 	}

-	result = getsection(source, msg, dctx, DNS_SECTION_ADDITIONAL, options);
+	result = getsection(source, msg, DNS_SECTION_ADDITIONAL, options);
 	if (result == ISC_R_UNEXPECTEDEND && ignore_tc) {
 		goto truncated;
 	}
@@ -3461,13 +3465,12 @@ cleanup:

 static isc_result_t
 render_nameopt(isc_buffer_t *optbuf, bool yaml, isc_buffer_t *target) {
-	dns_decompress_t dctx = DNS_DECOMPRESS_NEVER;
 	dns_fixedname_t fixed;
 	dns_name_t *name = dns_fixedname_initname(&fixed);
 	char namebuf[DNS_NAME_FORMATSIZE];
 	isc_result_t result;

-	result = dns_name_fromwire(name, optbuf, dctx, NULL);
+	result = dns_name_fromwire(name, optbuf, DNS_DECOMPRESS_NEVER, NULL);
 	if (result == ISC_R_SUCCESS && isc_buffer_activelength(optbuf) == 0) {
 		dns_name_format(name, namebuf, sizeof(namebuf));
 		ADD_STRING(target, " \"");
diff --git a/lib/dns/rdata/generic/lp_107.c b/lib/dns/rdata/generic/lp_107.c
index f2fe025af8..293822e340 100644
--- a/lib/dns/rdata/generic/lp_107.c
+++ b/lib/dns/rdata/generic/lp_107.c
@@ -87,8 +87,6 @@ fromwire_lp(ARGS_FROMWIRE) {
 	UNUSED(type);
 	UNUSED(rdclass);

-	dctx = dns_decompress_setpermitted(dctx, true);
-
 	dns_name_init(&name);

 	isc_buffer_activeregion(source, &sregion);
diff --git a/tests/bench/dns_name_fromwire.c b/tests/bench/dns_name_fromwire.c
index ef7d393661..7f0bd32911 100644
--- a/tests/bench/dns_name_fromwire.c
+++ b/tests/bench/dns_name_fromwire.c
@@ -34,7 +34,6 @@ old_bench(const uint8_t *data, size_t size) {
 	isc_result_t result;
 	dns_fixedname_t fixed;
 	dns_name_t *name = dns_fixedname_initname(&fixed);
-	dns_decompress_t dctx = DNS_DECOMPRESS_PERMITTED;
 	isc_buffer_t buf;
 	uint32_t count = 0;

@@ -43,7 +42,8 @@ old_bench(const uint8_t *data, size_t size) {
 	isc_buffer_setactive(&buf, size);

 	while (isc_buffer_consumedlength(&buf) < size) {
-		result = old_name_fromwire(name, &buf, dctx, 0, NULL);
+		result = old_name_fromwire(name, &buf, DNS_DECOMPRESS_PERMITTED,
+					   0, NULL);
 		if (result != ISC_R_SUCCESS) {
 			isc_buffer_forward(&buf, 1);
 		}
@@ -57,7 +57,6 @@ new_bench(const uint8_t *data, size_t size) {
 	isc_result_t result;
 	dns_fixedname_t fixed;
 	dns_name_t *name = dns_fixedname_initname(&fixed);
-	dns_decompress_t dctx = DNS_DECOMPRESS_PERMITTED;
 	isc_buffer_t buf;
 	uint32_t count = 0;

@@ -66,7 +65,8 @@ new_bench(const uint8_t *data, size_t size) {
 	isc_buffer_setactive(&buf, size);

 	while (isc_buffer_consumedlength(&buf) < size) {
-		result = dns_name_fromwire(name, &buf, dctx, NULL);
+		result = dns_name_fromwire(name, &buf, DNS_DECOMPRESS_PERMITTED,
+					   NULL);
 		if (result != ISC_R_SUCCESS) {
 			isc_buffer_forward(&buf, 1);
 		}