Commit 0ecacc6e83 for bind

commit 0ecacc6e83bf45c48ea96cbb67622ad15b83b48c
Author: OndÅ™ej Surý <ondrej@isc.org>
Date:   Sun May 3 08:02:00 2026 +0200

    Fix dns_compress_rollback() count underflow and stale-entry skip

    When dns_message_renderend() re-renders a truncated response with
    TC + OPT/TSIG/SIG(0), it calls dns_compress_rollback(cctx, 0) to
    discard every compression entry. Two defects in that walk: (1) the
    unsigned comparison `set[slot].coff < 0` is always false, so empty
    slots also entered the deletion path and decremented count once per
    slot — underflowing the uint16_t and locking out later
    insert_label() calls in the same render via the load-factor guard;
    (2) after a deletion shifted entries down to preserve the probe
    sequence, the outer scan's unconditional `slot++` skipped
    re-examining the entry the cascade had just shifted into `slot`.

    For coff = 0, memset the slot table — every entry is discarded
    anyway. For coff > 0, walk the table once and only advance `slot`
    when the inner cascade did not shift, so the entry now sitting in
    `slot` is re-checked.

diff --git a/lib/dns/compress.c b/lib/dns/compress.c
index 9fc067a469..4f5a093dee 100644
--- a/lib/dns/compress.c
+++ b/lib/dns/compress.c
@@ -361,17 +361,31 @@ void
 dns_compress_rollback(dns_compress_t *cctx, unsigned int coff) {
 	REQUIRE(CCTX_VALID(cctx));

-	for (unsigned int slot = 0; slot <= cctx->mask; slot++) {
+	/*
+	 * coff == 0 is the dns_message_renderend() TC + OPT/TSIG/SIG(0)
+	 * re-render path: every entry is being discarded, so wipe the
+	 * whole table in one shot rather than walking it slot by slot.
+	 */
+	if (coff == 0) {
+		memset(cctx->set, 0,
+		       (size_t)(cctx->mask + 1) * sizeof(*cctx->set));
+		cctx->count = 0;
+		return;
+	}
+
+	/*
+	 * Selective rollback: remove every entry whose buffer offset is
+	 * at or past `coff`. When entries are slid down to preserve the
+	 * probe sequence, the entry shifted into `slot` must be
+	 * re-examined — it may itself be eligible for deletion. Don't
+	 * advance `slot` after a shift.
+	 */
+	unsigned int slot = 0;
+	while (slot <= cctx->mask) {
 		if (cctx->set[slot].coff < coff) {
+			slot++;
 			continue;
 		}
-		/*
-		 * The next few elements might be part of the deleted element's
-		 * probe sequence, so we slide them down to overwrite the entry
-		 * we are deleting and preserve the probe sequence. Moving an
-		 * element to the previous slot reduces its probe distance, so
-		 * we stop when we find an element whose probe distance is zero.
-		 */
 		unsigned int prev = slot;
 		unsigned int next = slot_index(cctx, prev, 1);
 		while (cctx->set[next].coff != 0 &&
@@ -384,5 +398,9 @@ dns_compress_rollback(dns_compress_t *cctx, unsigned int coff) {
 		cctx->set[prev].coff = 0;
 		cctx->set[prev].hash = 0;
 		cctx->count--;
+		if (prev == slot) {
+			slot++;
+		}
+		/* else: leave `slot` to re-examine the shifted entry */
 	}
 }
diff --git a/tests/dns/compress_test.c b/tests/dns/compress_test.c
new file mode 100644
index 0000000000..e03f562fe3
--- /dev/null
+++ b/tests/dns/compress_test.c
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+#include <sched.h> /* IWYU pragma: keep */
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#define UNIT_TESTING
+#include <cmocka.h>
+
+#include <isc/buffer.h>
+#include <isc/lib.h>
+#include <isc/mem.h>
+
+#include <dns/compress.h>
+#include <dns/fixedname.h>
+#include <dns/message.h>
+#include <dns/name.h>
+
+#include <tests/isc.h>
+
+/*
+ * dns_message_renderend() invokes dns_compress_rollback(cctx, 0) when a
+ * truncated response is re-rendered with TC+OPT/TSIG/SIG(0). The walk
+ * must clear every populated slot and leave count == 0 — and crucially
+ * must not decrement count for empty slots, which would underflow the
+ * uint16_t and lock out subsequent insert_label() calls in the same
+ * render via the load-factor guard.
+ */
+ISC_RUN_TEST_IMPL(rollback_zero_clears_all) {
+	dns_compress_t cctx;
+	isc_buffer_t buffer;
+	unsigned char bufdata[1024];
+	const char *names[] = {
+		"www.example.com.",  "mail.example.com.", "ns1.example.com.",
+		"ns2.example.com.",  "a.b.c.d.example.",  "x.y.z.example.",
+		"foo.bar.baz.test.", "alpha.beta.gamma.",
+	};
+
+	isc_buffer_init(&buffer, bufdata, sizeof(bufdata));
+	isc_buffer_add(&buffer, DNS_MESSAGE_HEADERLEN);
+
+	dns_compress_init(&cctx, isc_g_mctx, 0);
+
+	for (size_t i = 0; i < ARRAY_SIZE(names); i++) {
+		dns_fixedname_t fixed;
+		dns_name_t *name = dns_fixedname_initname(&fixed);
+		assert_int_equal(
+			dns_name_fromstring(name, names[i], NULL, 0, NULL),
+			ISC_R_SUCCESS);
+
+		assert_int_equal(dns_name_towire(name, &cctx, &buffer),
+				 ISC_R_SUCCESS);
+	}
+
+	assert_true(cctx.count > 0);
+
+	dns_compress_rollback(&cctx, 0);
+
+	assert_int_equal(cctx.count, 0);
+	for (unsigned int i = 0; i <= cctx.mask; i++) {
+		assert_int_equal(cctx.set[i].coff, 0);
+		assert_int_equal(cctx.set[i].hash, 0);
+	}
+
+	dns_compress_invalidate(&cctx);
+}
+
+/*
+ * Edge case: rollback(0) on a fresh, empty compression context must be
+ * a no-op. The pre-fix code decremented count for every empty slot,
+ * underflowing uint16_t to ~65535.
+ */
+ISC_RUN_TEST_IMPL(rollback_zero_on_empty) {
+	dns_compress_t cctx;
+
+	dns_compress_init(&cctx, isc_g_mctx, 0);
+	assert_int_equal(cctx.count, 0);
+
+	dns_compress_rollback(&cctx, 0);
+
+	assert_int_equal(cctx.count, 0);
+
+	dns_compress_invalidate(&cctx);
+}
+
+/*
+ * After rollback(0) the table must accept new insertions. Pre-fix, the
+ * underflowed count tripped the load-factor guard in insert_label() and
+ * silently dropped every subsequent compression entry.
+ */
+ISC_RUN_TEST_IMPL(rollback_zero_then_reuse) {
+	dns_compress_t cctx;
+	isc_buffer_t buffer;
+	unsigned char bufdata[1024];
+	dns_fixedname_t fixed;
+	dns_name_t *name = NULL;
+
+	isc_buffer_init(&buffer, bufdata, sizeof(bufdata));
+	isc_buffer_add(&buffer, DNS_MESSAGE_HEADERLEN);
+
+	dns_compress_init(&cctx, isc_g_mctx, 0);
+
+	dns_compress_rollback(&cctx, 0);
+
+	name = dns_fixedname_initname(&fixed);
+	assert_int_equal(dns_name_fromstring(name, "after.rollback.test.", NULL,
+					     0, NULL),
+			 ISC_R_SUCCESS);
+	assert_int_equal(dns_name_towire(name, &cctx, &buffer), ISC_R_SUCCESS);
+	assert_true(cctx.count > 0);
+	assert_true(cctx.count <= cctx.mask + 1U);
+
+	dns_compress_invalidate(&cctx);
+}
+
+ISC_TEST_LIST_START
+ISC_TEST_ENTRY(rollback_zero_clears_all)
+ISC_TEST_ENTRY(rollback_zero_on_empty)
+ISC_TEST_ENTRY(rollback_zero_then_reuse)
+ISC_TEST_LIST_END
+ISC_TEST_MAIN
diff --git a/tests/dns/meson.build b/tests/dns/meson.build
index 54820af731..5d407d7497 100644
--- a/tests/dns/meson.build
+++ b/tests/dns/meson.build
@@ -13,6 +13,7 @@ dns_tests = [
     'acl',
     'badcache',
     'byaddr',
+    'compress',
     'db',
     'dbdiff',
     'dbiterator',