Backport fixes for stale CNAME chains.

Resolves: RHEL-142289
This commit is contained in:
Fedor Vorobev 2026-01-19 15:03:43 +01:00
parent 5efcb6a5a9
commit 208f366e42
3 changed files with 810 additions and 1 deletions

View File

@ -0,0 +1,94 @@
diff --git a/bin/tests/system/serve-stale/ns1/stale.test.db b/bin/tests/system/serve-stale/ns1/stale.test.db
index d389e7c6a6..128fb25a10 100644
--- a/bin/tests/system/serve-stale/ns1/stale.test.db
+++ b/bin/tests/system/serve-stale/ns1/stale.test.db
@@ -17,3 +17,11 @@ cname1.stale.test. 1 CNAME a1.stale.test.
a1.stale.test. 1 A 192.0.2.1
cname2.stale.test. 1 CNAME a2.stale.test.
a2.stale.test. 300 A 192.0.2.2
+
+cname-a1 1 CNAME cname-a2
+cname-a2 300 CNAME cname-a3
+cname-a3 300 A 192.0.2.1
+
+cname-b1 300 CNAME cname-b2
+cname-b2 1 CNAME cname-b3
+cname-b3 1 A 192.0.2.2
diff --git a/bin/tests/system/serve-stale/tests.sh b/bin/tests/system/serve-stale/tests.sh
index 96cd26505f..a30896b6a4 100755
--- a/bin/tests/system/serve-stale/tests.sh
+++ b/bin/tests/system/serve-stale/tests.sh
@@ -2256,6 +2256,73 @@ if [ $ret != 0 ]; then
fi
status=$((status + ret))
+# New CNAME scenario (GL #5243)
+n=$((n + 1))
+echo_i "prime cache cname-a1.stale.test A (stale-answer-client-timeout 0) ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.3 cname-a1.stale.test A >dig.out.test$n || ret=1
+grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
+grep "ANSWER: 3," dig.out.test$n >/dev/null || ret=1
+grep "cname-a1\.stale\.test\..*1.*IN.*CNAME.*cname-a2\.stale\.test\." dig.out.test$n >/dev/null || ret=1
+grep "cname-a2\.stale\.test\..*300.*IN.*CNAME.*cname-a3\.stale\.test\." dig.out.test$n >/dev/null || ret=1
+grep "cname-a3\.stale\.test\..*300.*IN.*A.*192\.0\.2\.1" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "prime cache cname-b1.stale.test A (stale-answer-client-timeout 0) ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.3 cname-b1.stale.test A >dig.out.test$n || ret=1
+grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
+grep "ANSWER: 3," dig.out.test$n >/dev/null || ret=1
+grep "cname-b1\.stale\.test\..*300.*IN.*CNAME.*cname-b2\.stale\.test\." dig.out.test$n >/dev/null || ret=1
+grep "cname-b2\.stale\.test\..*1.*IN.*CNAME.*cname-b3\.stale\.test\." dig.out.test$n >/dev/null || ret=1
+grep "cname-b3\.stale\.test\..*1.*IN.*A.*192\.0\.2\.2" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+# Allow RRset to become stale.
+sleep 1
+
+n=$((n + 1))
+ret=0
+echo_i "check stale cname-a1.stale.test A comes from cache (stale-answer-client-timeout 0) ($n)"
+nextpart ns3/named.run >/dev/null
+$DIG -p ${PORT} @10.53.0.3 cname-a1.stale.test A >dig.out.test$n || ret=1
+wait_for_log 5 "cname-a1.stale.test A stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
+# Other records in chain are still good, so do not attempt a refresh
+grep "cname-a2.stale.test A stale answer used, an attempt to refresh the RRset" ns3/named.run && ret=1
+grep "cname-a3.stale.test A stale answer used, an attempt to refresh the RRset" ns3/named.run && ret=1
+# Check answer
+grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
+grep "EDE: 3 (Stale Answer): (stale data prioritized over lookup)" dig.out.test$n >/dev/null || ret=1
+grep "ANSWER: 3," dig.out.test$n >/dev/null || ret=1
+grep "cname-a1\.stale\.test\..*3.*IN.*CNAME.*cname-a2\.stale\.test\." dig.out.test$n >/dev/null || ret=1
+grep "cname-a2\.stale\.test\..*29[0-9].*IN.*CNAME.*cname-a3\.stale\.test\." dig.out.test$n >/dev/null || ret=1
+grep "cname-a3\.stale\.test\..*29[0-9].*IN.*A.*192\.0\.2\.1" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+ret=0
+echo_i "check stale cname-b1.stale.test A comes from cache (stale-answer-client-timeout 0) ($n)"
+nextpart ns3/named.run >/dev/null
+$DIG -p ${PORT} @10.53.0.3 cname-b1.stale.test A >dig.out.test$n || ret=1
+wait_for_log 5 "cname-b2.stale.test A stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
+# The next one in the chain (cname-b3.stale.test) is likely not logged because
+# there is already a refresh in progress. And the first record in the chain is
+# still good, so do not attempt a refresh.
+grep "cname-b1.stale.test A stale answer used, an attempt to refresh the RRset" ns3/named.run && ret=1
+# Check answer
+grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
+grep "EDE: 3 (Stale Answer): (stale data prioritized over lookup)" dig.out.test$n >/dev/null || ret=1
+grep "ANSWER: 3," dig.out.test$n >/dev/null || ret=1
+grep "cname-b1\.stale\.test\..*29[0-9].*IN.*CNAME.*cname-b2\.stale\.test\." dig.out.test$n >/dev/null || ret=1
+grep "cname-b2\.stale\.test\..*3.*IN.*CNAME.*cname-b3\.stale\.test\." dig.out.test$n >/dev/null || ret=1
+grep "cname-b3\.stale\.test\..*3.*IN.*A.*192\.0\.2\.2" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
####################################################################
# Test for stale-answer-client-timeout 0 and stale-refresh-time 4. #
####################################################################

709
bind-9.20-stale-cname.patch Normal file
View File

@ -0,0 +1,709 @@
diff --git a/bin/tests/system/serve-stale/tests.sh b/bin/tests/system/serve-stale/tests.sh
index c001e7a071..96cd26505f 100755
--- a/bin/tests/system/serve-stale/tests.sh
+++ b/bin/tests/system/serve-stale/tests.sh
@@ -2053,7 +2053,7 @@ ret=0
echo_i "check stale nodata.example TXT comes from cache (stale-answer-client-timeout 0) ($n)"
nextpart ns3/named.run >/dev/null
$DIG -p ${PORT} @10.53.0.3 nodata.example TXT >dig.out.test$n || ret=1
-wait_for_log 5 "nodata.example stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
+wait_for_log 5 "nodata.example TXT stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
grep "EDE: 3 (Stale Answer): (stale data prioritized over lookup)" dig.out.test$n >/dev/null || ret=1
grep "ANSWER: 0," dig.out.test$n >/dev/null || ret=1
@@ -2066,7 +2066,7 @@ ret=0
echo_i "check stale data.example TXT comes from cache (stale-answer-client-timeout 0) ($n)"
nextpart ns3/named.run >/dev/null
$DIG -p ${PORT} @10.53.0.3 data.example TXT >dig.out.test$n || ret=1
-wait_for_log 5 "data.example stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
+wait_for_log 5 "data.example TXT stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
grep "EDE: 3 (Stale Answer): (stale data prioritized over lookup)" dig.out.test$n >/dev/null || ret=1
grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
@@ -2196,7 +2196,7 @@ ret=0
echo_i "check stale cname1.stale.test A comes from cache (stale-answer-client-timeout 0) ($n)"
nextpart ns3/named.run >/dev/null
$DIG -p ${PORT} @10.53.0.3 cname1.stale.test A >dig.out.test$n || ret=1
-wait_for_log 5 "cname1.stale.test stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
+wait_for_log 5 "cname1.stale.test A stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
grep "EDE: 3 (Stale Answer): (stale data prioritized over lookup)" dig.out.test$n >/dev/null || ret=1
grep "ANSWER: 2," dig.out.test$n >/dev/null || ret=1
@@ -2235,7 +2235,7 @@ ret=0
echo_i "check stale cname2.stale.test A comes from cache (stale-answer-client-timeout 0) ($n)"
nextpart ns3/named.run >/dev/null
$DIG -p ${PORT} @10.53.0.3 cname2.stale.test A >dig.out.test$n || ret=1
-wait_for_log 5 "cname2.stale.test stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
+wait_for_log 5 "cname2.stale.test A stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
grep "EDE: 3 (Stale Answer): (stale data prioritized over lookup)" dig.out.test$n >/dev/null || ret=1
grep "ANSWER: 2," dig.out.test$n >/dev/null || ret=1
@@ -2312,7 +2312,7 @@ ret=0
echo_i "check stale data.example TXT comes from cache (stale-answer-client-timeout 0 stale-refresh-time 4) ($n)"
nextpart ns3/named.run >/dev/null
$DIG -p ${PORT} @10.53.0.3 data.example TXT >dig.out.test$n || ret=1
-wait_for_log 5 "data.example stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
+wait_for_log 5 "data.example TXT stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
grep "EDE: 3 (Stale Answer): (stale data prioritized over lookup)" dig.out.test$n >/dev/null || ret=1
grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
@@ -2356,7 +2356,7 @@ ret=0
echo_i "check stale data.example TXT comes from cache (stale-answer-client-timeout 0 stale-refresh-time 4) ($n)"
nextpart ns3/named.run >/dev/null
$DIG -p ${PORT} @10.53.0.3 data.example TXT >dig.out.test$n || ret=1
-wait_for_log 5 "data.example stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
+wait_for_log 5 "data.example TXT stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
grep "EDE: 3 (Stale Answer): (stale data prioritized over lookup)" dig.out.test$n >/dev/null || ret=1
grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
@@ -2368,7 +2368,7 @@ status=$((status + ret))
n=$((n + 1))
ret=0
echo_i "wait until resolver query times out, activating stale-refresh-time"
-wait_for_log 15 "data.example resolver failure, stale answer used" ns3/named.run || ret=1
+wait_for_log 15 "data.example/TXT stale refresh failed: timed out" ns3/named.run || ret=1
if [ $ret != 0 ]; then echo_i "failed"; fi
status=$((status + ret))
@@ -2419,7 +2419,7 @@ n=$((n + 1))
ret=0
echo_i "check stale data.example TXT comes from cache (stale-answer-client-timeout 0 stale-refresh-time 4) ($n)"
$DIG -p ${PORT} @10.53.0.3 data.example TXT >dig.out.test$n || ret=1
-wait_for_log 5 "data.example stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
+wait_for_log 5 "data.example TXT stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1
grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
grep "EDE: 3 (Stale Answer): (stale data prioritized over lookup)" dig.out.test$n >/dev/null || ret=1
grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
diff --git a/lib/dns/include/dns/rdataset.h b/lib/dns/include/dns/rdataset.h
index 3a85226836..b04e65afc7 100644
--- a/lib/dns/include/dns/rdataset.h
+++ b/lib/dns/include/dns/rdataset.h
@@ -201,7 +201,7 @@ struct dns_rdataset {
#define DNS_RDATASETATTR_STALE 0x01000000
#define DNS_RDATASETATTR_ANCIENT 0x02000000
#define DNS_RDATASETATTR_STALE_WINDOW 0x04000000
-#define DNS_RDATASETATTR_STALE_ADDED 0x08000000
+#define DNS_RDATASETATTR_KEEPCASE 0x10000000
#define DNS_RDATASETATTR_STATICSTUB 0x20000000
/*%
diff --git a/lib/ns/include/ns/client.h b/lib/ns/include/ns/client.h
index ea2d83e079..db17f01d79 100644
--- a/lib/ns/include/ns/client.h
+++ b/lib/ns/include/ns/client.h
@@ -184,6 +184,7 @@ struct ns_client {
(query, update, notify) */
isc_nmhandle_t *fetchhandle; /* Waiting for recursive fetch */
isc_nmhandle_t *prefetchhandle; /* Waiting for prefetch / rpzfetch */
+ isc_nmhandle_t *stalerefreshhandle;
isc_nmhandle_t *updatehandle; /* Waiting for update callback */
unsigned char *tcpbuf;
size_t tcpbuf_size;
diff --git a/lib/ns/include/ns/query.h b/lib/ns/include/ns/query.h
index c090754526..10a5931dca 100644
--- a/lib/ns/include/ns/query.h
+++ b/lib/ns/include/ns/query.h
@@ -69,6 +69,7 @@ struct ns_query {
isc_mutex_t fetchlock;
dns_fetch_t *fetch;
dns_fetch_t *prefetch;
+ dns_fetch_t *stalerefresh;
ns_hookasync_t *hookactx;
dns_rpz_st_t *rpz_st;
isc_bufferlist_t namebufs;
@@ -120,7 +121,6 @@ struct ns_query {
#define NS_QUERYATTR_RRL_CHECKED 0x010000
#define NS_QUERYATTR_REDIRECT 0x020000
#define NS_QUERYATTR_ANSWERED 0x040000
-#define NS_QUERYATTR_STALEOK 0x080000
#define NS_QUERYATTR_STALEPENDING 0x100000
typedef struct query_ctx query_ctx_t;
@@ -149,7 +149,6 @@ struct query_ctx {
bool authoritative; /* authoritative query? */
bool want_restart; /* CNAME chain or other
* restart needed */
- bool refresh_rrset; /* stale RRset refresh needed */
bool need_wildcardproof; /* wildcard proof needed */
bool nxrewrite; /* negative answer from RPZ */
bool findcoveringnsec; /* lookup covering NSEC */
diff --git a/lib/ns/query.c b/lib/ns/query.c
index 11d2520c61..114cb0bf6b 100644
--- a/lib/ns/query.c
+++ b/lib/ns/query.c
@@ -140,9 +140,6 @@
#define QUERY_STALEPENDING(q) \
(((q)->attributes & NS_QUERYATTR_STALEPENDING) != 0)
-/*% Does the query allow stale data in the response? */
-#define QUERY_STALEOK(q) (((q)->attributes & NS_QUERYATTR_STALEOK) != 0)
-
/*% Does the query wants to check for stale RRset due to a timeout? */
#define QUERY_STALETIMEOUT(q) (((q)->dboptions & DNS_DBFIND_STALETIMEOUT) != 0)
@@ -249,6 +246,15 @@ query_addanswer(query_ctx_t *qctx);
static isc_result_t
query_prepare_delegation_response(query_ctx_t *qctx);
+static isc_result_t
+qctx_prepare_buffers(query_ctx_t *qctx, isc_buffer_t *buffer);
+
+static void
+qctx_freedata(query_ctx_t *qctx);
+
+static void
+qctx_destroy(query_ctx_t *qctx);
+
/*
* Return the hooktable in use with 'qctx', or if there isn't one
* set, return the default hooktable.
@@ -504,9 +510,6 @@ query_addwildcardproof(query_ctx_t *qctx, bool ispositive, bool nodata);
static void
query_addauth(query_ctx_t *qctx);
-static void
-query_clear_stale(ns_client_t *client);
-
/*
* Increment query statistics counters.
*/
@@ -815,6 +818,7 @@ ns_query_init(ns_client_t *client) {
client->query.fetch = NULL;
client->query.prefetch = NULL;
+ client->query.stalerefresh = NULL;
client->query.authdb = NULL;
client->query.authzone = NULL;
client->query.authdbset = false;
@@ -2247,10 +2251,6 @@ query_addrrset(query_ctx_t *qctx, dns_name_t **namep,
if ((rdataset->attributes & DNS_RDATASETATTR_REQUIRED) != 0) {
mrdataset->attributes |= DNS_RDATASETATTR_REQUIRED;
}
- if ((rdataset->attributes & DNS_RDATASETATTR_STALE_ADDED) != 0)
- {
- mrdataset->attributes |= DNS_RDATASETATTR_STALE_ADDED;
- }
return;
} else if (result == DNS_R_NXDOMAIN) {
/*
@@ -2538,6 +2538,88 @@ free_devent(ns_client_t *client, isc_event_t **eventp,
isc_event_free(eventp);
}
+static void
+stale_refresh_aftermath(ns_client_t *client, isc_result_t result) {
+ dns_db_t *db = NULL;
+ unsigned int dboptions;
+ isc_buffer_t buffer;
+ query_ctx_t qctx;
+ dns_clientinfomethods_t cm;
+ dns_clientinfo_t ci;
+ char namebuf[DNS_NAME_FORMATSIZE];
+ char typebuf[DNS_RDATATYPE_FORMATSIZE];
+
+ /*
+ * If refreshing a stale RRset failed, we need to set the
+ * stale-refresh-time window, so that on future requests for this
+ * RRset the stale entry may be used immediately.
+ */
+ switch (result) {
+ case ISC_R_SUCCESS:
+ case DNS_R_GLUE:
+ case DNS_R_ZONECUT:
+ case ISC_R_NOTFOUND:
+ case DNS_R_DELEGATION:
+ case DNS_R_EMPTYNAME:
+ case DNS_R_NXRRSET:
+ case DNS_R_EMPTYWILD:
+ case DNS_R_NXDOMAIN:
+ case DNS_R_COVERINGNSEC:
+ case DNS_R_NCACHENXDOMAIN:
+ case DNS_R_NCACHENXRRSET:
+ case DNS_R_CNAME:
+ case DNS_R_DNAME:
+ break;
+ default:
+ dns_name_format(client->query.qname, namebuf, sizeof(namebuf));
+ dns_rdatatype_format(client->query.qtype, typebuf,
+ sizeof(typebuf));
+ ns_client_log(client, NS_LOGCATEGORY_SERVE_STALE,
+ NS_LOGMODULE_QUERY, ISC_LOG_NOTICE,
+ "%s/%s stale refresh failed: timed out", namebuf,
+ typebuf);
+
+ /*
+ * Set up a short lived query context, solely to set the
+ * last refresh failure time on the RRset in the cache
+ * database, starting the stale-refresh-time window for it.
+ * This is a condensed form of query_lookup().
+ */
+ isc_stdtime_get(&client->now);
+ client->query.attributes &= ~NS_QUERYATTR_RECURSIONOK;
+ qctx_init(client, NULL, 0, &qctx);
+
+ dns_clientinfomethods_init(&cm, ns_client_sourceip);
+ dns_clientinfo_init(&ci, qctx.client, NULL);
+ if (HAVEECS(qctx.client)) {
+ dns_clientinfo_setecs(&ci, &qctx.client->ecs);
+ }
+
+ result = qctx_prepare_buffers(&qctx, &buffer);
+ if (result != ISC_R_SUCCESS) {
+ goto cleanup;
+ }
+
+ dboptions = qctx.client->query.dboptions;
+ dboptions |= DNS_DBFIND_STALEOK;
+ dboptions |= DNS_DBFIND_STALESTART;
+
+ dns_db_attach(qctx.client->view->cachedb, &db);
+ (void)dns_db_findext(db, qctx.client->query.qname, NULL,
+ qctx.client->query.qtype, dboptions,
+ qctx.client->now, &qctx.node, qctx.fname,
+ &cm, &ci, qctx.rdataset, qctx.sigrdataset);
+ if (qctx.node != NULL) {
+ dns_db_detachnode(db, &qctx.node);
+ }
+ dns_db_detach(&db);
+
+ cleanup:
+ qctx_freedata(&qctx);
+ qctx_destroy(&qctx);
+ }
+}
+
static void
prefetch_done(isc_task_t *task, isc_event_t *event) {
dns_fetchevent_t *devent = (dns_fetchevent_t *)event;
@@ -2572,6 +2654,133 @@ prefetch_done(isc_task_t *task, isc_event_t *event) {
isc_nmhandle_detach(&client->prefetchhandle);
}
+static void
+refresh_done(isc_task_t *task, isc_event_t *event) {
+ dns_fetchevent_t *devent = (dns_fetchevent_t *)event;
+ ns_client_t *client;
+
+ UNUSED(task);
+
+ REQUIRE(event->ev_type == DNS_EVENT_FETCHDONE);
+ client = devent->ev_arg;
+ REQUIRE(NS_CLIENT_VALID(client));
+ REQUIRE(task == client->task);
+
+ CTRACE(ISC_LOG_DEBUG(3), "refresh_done");
+
+ LOCK(&client->query.fetchlock);
+ if (client->query.stalerefresh != NULL) {
+ INSIST(devent->fetch == client->query.stalerefresh);
+ client->query.stalerefresh = NULL;
+ }
+ UNLOCK(&client->query.fetchlock);
+
+ stale_refresh_aftermath(client, devent->result);
+
+ if (client->recursionquota != NULL) {
+ isc_quota_detach(&client->recursionquota);
+ ns_stats_decrement(client->sctx->nsstats,
+ ns_statscounter_recursclients);
+ }
+
+ free_devent(client, &event, &devent);
+ isc_nmhandle_detach(&client->stalerefreshhandle);
+}
+
+static void
+query_stale_refresh(ns_client_t *client, dns_name_t *qname,
+ dns_rdataset_t *rdataset) {
+ isc_result_t result;
+ isc_sockaddr_t *peeraddr;
+ dns_rdataset_t *tmprdataset;
+ unsigned int options;
+
+ CTRACE(ISC_LOG_DEBUG(3), "query_stale_refresh");
+
+ bool stale_refresh_window = false;
+ bool stale_rrset = true;
+
+ if (rdataset != NULL) {
+ stale_refresh_window =
+ (STALE_WINDOW(rdataset) &&
+ (client->query.dboptions & DNS_DBFIND_STALEENABLED) != 0);
+ stale_rrset = STALE(rdataset);
+ }
+
+ if (client->query.stalerefresh != NULL ||
+ (client->query.dboptions & DNS_DBFIND_STALETIMEOUT) == 0 ||
+ !stale_rrset || stale_refresh_window)
+ {
+ return;
+ }
+
+ char namebuf[DNS_NAME_FORMATSIZE];
+ char typebuf[DNS_RDATATYPE_FORMATSIZE];
+ dns_name_format(qname, namebuf, sizeof(namebuf));
+ dns_rdatatype_format(client->query.qtype, typebuf, sizeof(typebuf));
+ isc_log_write(ns_lctx, NS_LOGCATEGORY_SERVE_STALE, NS_LOGMODULE_QUERY,
+ ISC_LOG_INFO,
+ "%s %s stale answer used, an attempt "
+ "to refresh the RRset will still be "
+ "made",
+ namebuf, typebuf);
+
+ client->query.dboptions &= ~(DNS_DBFIND_STALETIMEOUT |
+ DNS_DBFIND_STALEOK |
+ DNS_DBFIND_STALEENABLED);
+
+ if (client->recursionquota == NULL) {
+ result = isc_quota_attach(&client->sctx->recursionquota,
+ &client->recursionquota);
+ switch (result) {
+ case ISC_R_SUCCESS:
+ ns_stats_increment(client->sctx->nsstats,
+ ns_statscounter_recursclients);
+ break;
+ case ISC_R_SOFTQUOTA:
+ isc_quota_detach(&client->recursionquota);
+ FALLTHROUGH;
+ default:
+ return;
+ }
+ }
+
+ tmprdataset = ns_client_newrdataset(client);
+ if (tmprdataset == NULL) {
+ return;
+ }
+
+ if (!TCP(client)) {
+ peeraddr = &client->peeraddr;
+ } else {
+ peeraddr = NULL;
+ }
+
+ isc_nmhandle_attach(client->handle, &client->stalerefreshhandle);
+ options = client->query.fetchoptions;
+ result = dns_resolver_createfetch(
+ client->view->resolver, qname, client->query.qtype, NULL, NULL, NULL,
+ peeraddr, client->message->id, options, 0, NULL, NULL,
+ client->task, refresh_done, client, tmprdataset, NULL,
+ &client->query.stalerefresh);
+ if (result != ISC_R_SUCCESS) {
+ ns_client_putrdataset(client, &tmprdataset);
+ isc_nmhandle_detach(&client->stalerefreshhandle);
+ }
+}
+
+static void
+query_stale_refresh_ncache(ns_client_t *client) {
+ dns_name_t *qname;
+
+ if (client->query.origqname != NULL) {
+ qname = client->query.origqname;
+ } else {
+ qname = client->query.qname;
+ }
+ query_stale_refresh(client, qname, NULL);
+}
+
static void
query_prefetch(ns_client_t *client, dns_name_t *qname,
dns_rdataset_t *rdataset) {
@@ -2587,6 +2796,7 @@ query_prefetch(ns_client_t *client, dns_name_t *qname,
rdataset->ttl > client->view->prefetch_trigger ||
(rdataset->attributes & DNS_RDATASETATTR_PREFETCH) == 0)
{
+ query_stale_refresh(client, qname, rdataset);
return;
}
@@ -2638,6 +2848,7 @@ query_prefetch(ns_client_t *client, dns_name_t *qname,
dns_rdataset_clearprefetch(rdataset);
ns_stats_increment(client->sctx->nsstats, ns_statscounter_prefetch);
+ return;
}
static void
@@ -5824,55 +6035,6 @@ error:
return ISC_R_NOMEMORY;
}
-/*
- * Setup a new query context for resolving a query.
- *
- * This function is only called if both these conditions are met:
- * 1. BIND is configured with stale-answer-client-timeout 0.
- * 2. A stale RRset is found in cache during initial query
- * database lookup.
- *
- * We continue with this function for refreshing/resolving an RRset
- * after answering a client with stale data.
- */
-static void
-query_refresh_rrset(query_ctx_t *orig_qctx) {
- isc_buffer_t buffer;
- query_ctx_t qctx;
-
- REQUIRE(orig_qctx != NULL);
- REQUIRE(orig_qctx->client != NULL);
-
- qctx_copy(orig_qctx, &qctx);
- qctx.client->query.dboptions &= ~(DNS_DBFIND_STALETIMEOUT |
- DNS_DBFIND_STALEOK |
- DNS_DBFIND_STALEENABLED);
- qctx.client->nodetach = false;
-
- /*
- * We'll need some resources...
- */
- if (qctx_prepare_buffers(&qctx, &buffer) != ISC_R_SUCCESS) {
- dns_db_detach(&qctx.db);
- qctx_destroy(&qctx);
- return;
- }
-
- /*
- * Pretend we didn't find anything in cache.
- */
- (void)query_gotanswer(&qctx, ISC_R_NOTFOUND);
-
- if (qctx.fname != NULL) {
- ns_client_releasename(qctx.client, &qctx.fname);
- }
- if (qctx.rdataset != NULL) {
- ns_client_putrdataset(qctx.client, &qctx.rdataset);
- }
-
- qctx_destroy(&qctx);
-}
-
/*%
* Depending on the db lookup result, we can respond to the
* client this stale answer.
@@ -5944,7 +6106,7 @@ query_lookup(query_ctx_t *qctx) {
rpzqname = qctx->client->query.qname;
}
- if ((qctx->options & DNS_GETDB_STALEFIRST) != 0) {
+ if ((qctx->options & DNS_GETDB_STALEFIRST) != 0 && !qctx->is_zone) {
/*
* If DNS_GETDB_STALEFIRST is set, it means that a stale
* RRset may be returned as part of this lookup. An attempt
@@ -5954,19 +6116,19 @@ query_lookup(query_ctx_t *qctx) {
qctx->client->query.dboptions |= DNS_DBFIND_STALETIMEOUT;
}
- dboptions = qctx->client->query.dboptions;
- if (!qctx->is_zone && qctx->findcoveringnsec &&
- (qctx->type != dns_rdatatype_null || !dns_name_istat(rpzqname)))
- {
- dboptions |= DNS_DBFIND_COVERINGNSEC;
- }
-
(void)dns_db_getservestalerefresh(qctx->client->view->cachedb,
&stale_refresh);
if (stale_refresh > 0 &&
dns_view_staleanswerenabled(qctx->client->view))
{
- dboptions |= DNS_DBFIND_STALEENABLED;
+ qctx->client->query.dboptions |= DNS_DBFIND_STALEENABLED;
+ }
+
+ dboptions = qctx->client->query.dboptions;
+ if (!qctx->is_zone && qctx->findcoveringnsec &&
+ (qctx->type != dns_rdatatype_null || !dns_name_istat(rpzqname)))
+ {
+ dboptions |= DNS_DBFIND_COVERINGNSEC;
}
result = dns_db_findext(qctx->db, rpzqname, qctx->version, qctx->type,
@@ -6115,19 +6277,7 @@ query_lookup(query_ctx_t *qctx) {
* Immediately return the stale answer, start a
* resolver fetch to refresh the data in cache.
*/
- isc_log_write(
- ns_lctx, NS_LOGCATEGORY_SERVE_STALE,
- NS_LOGMODULE_QUERY, ISC_LOG_INFO,
- "%s stale answer used, an attempt to "
- "refresh the RRset will still be made",
- namebuf);
-
- qctx->refresh_rrset = STALE(qctx->rdataset);
- /*
- * If we are refreshing the RRSet, we must not
- * detach from the client in query_send().
- */
- qctx->client->nodetach = qctx->refresh_rrset;
+ qctx->client->nodetach = false;
if (stale_found) {
ns_client_extendederror(
@@ -6167,76 +6317,12 @@ query_lookup(query_ctx_t *qctx) {
}
}
- if (stale_timeout && (answer_found || stale_found)) {
- /*
- * Mark RRsets that we are adding to the client message on a
- * lookup during 'stale-answer-client-timeout', so we can
- * clean it up if needed when we resume from recursion.
- */
- qctx->client->query.attributes |= NS_QUERYATTR_STALEOK;
- qctx->rdataset->attributes |= DNS_RDATASETATTR_STALE_ADDED;
- }
-
result = query_gotanswer(qctx, result);
cleanup:
return result;
}
-/*
- * Clear all rdatasets from the message that are in the given section and
- * that have the 'attr' attribute set.
- */
-static void
-message_clearrdataset(dns_message_t *msg, unsigned int attr) {
- unsigned int i;
- dns_name_t *name, *next_name;
- dns_rdataset_t *rds, *next_rds;
-
- /*
- * Clean up name lists by calling the rdataset disassociate function.
- */
- for (i = DNS_SECTION_ANSWER; i < DNS_SECTION_MAX; i++) {
- name = ISC_LIST_HEAD(msg->sections[i]);
- while (name != NULL) {
- next_name = ISC_LIST_NEXT(name, link);
-
- rds = ISC_LIST_HEAD(name->list);
- while (rds != NULL) {
- next_rds = ISC_LIST_NEXT(rds, link);
- if ((rds->attributes & attr) != attr) {
- rds = next_rds;
- continue;
- }
- ISC_LIST_UNLINK(name->list, rds, link);
- INSIST(dns_rdataset_isassociated(rds));
- dns_rdataset_disassociate(rds);
- isc_mempool_put(msg->rdspool, rds);
- rds = next_rds;
- }
-
- if (ISC_LIST_EMPTY(name->list)) {
- ISC_LIST_UNLINK(msg->sections[i], name, link);
- if (dns_name_dynamic(name)) {
- dns_name_free(name, msg->mctx);
- }
- isc_mempool_put(msg->namepool, name);
- }
-
- name = next_name;
- }
- }
-}
-
-/*
- * Clear any rdatasets from the client's message that were added on a lookup
- * due to a client timeout.
- */
-static void
-query_clear_stale(ns_client_t *client) {
- message_clearrdataset(client->message, DNS_RDATASETATTR_STALE_ADDED);
-}
-
/*
* Create a new query context with the sole intent of looking up for a stale
* RRset in cache. If an entry is found, we mark the original query as
@@ -6316,6 +6402,7 @@ fetch_callback(isc_task_t *task, isc_event_t *event) {
}
client->query.fetchoptions &= ~DNS_FETCHOPT_TRYSTALE_ONTIMEOUT;
client->query.dboptions &= ~DNS_DBFIND_STALETIMEOUT;
+ client->query.dboptions &= ~DNS_DBFIND_STALEENABLED;
client->nodetach = false;
LOCK(&client->query.fetchlock);
@@ -7709,14 +7796,6 @@ query_usestale(query_ctx_t *qctx, isc_result_t result) {
return false;
}
- if (qctx->refresh_rrset) {
- /*
- * This is a refreshing query, we have already prioritized
- * stale data, so don't enable serve-stale again.
- */
- return false;
- }
-
if (result == DNS_R_DUPLICATE || result == DNS_R_DROP ||
result == ISC_R_ALREADYRUNNING)
{
@@ -8256,24 +8335,6 @@ query_addanswer(query_ctx_t *qctx) {
CALL_HOOK(NS_QUERY_ADDANSWER_BEGIN, qctx);
- /*
- * On normal lookups, clear any rdatasets that were added on a
- * lookup due to stale-answer-client-timeout. Do not clear if we
- * are going to refresh the RRset, because the stale contents are
- * prioritized.
- */
- if (QUERY_STALEOK(&qctx->client->query) &&
- !QUERY_STALETIMEOUT(&qctx->client->query) && !qctx->refresh_rrset)
- {
- CCTRACE(ISC_LOG_DEBUG(3), "query_clear_stale");
- query_clear_stale(qctx->client);
- /*
- * We can clear the attribute to prevent redundant clearing
- * in subsequent lookups.
- */
- qctx->client->query.attributes &= ~NS_QUERYATTR_STALEOK;
- }
-
if (qctx->dns64) {
result = query_dns64(qctx);
qctx->noqname = NULL;
@@ -8307,9 +8368,7 @@ query_addanswer(query_ctx_t *qctx) {
query_filter64(qctx);
ns_client_putrdataset(qctx->client, &qctx->rdataset);
} else {
- if (!qctx->is_zone && RECURSIONOK(qctx->client) &&
- !QUERY_STALETIMEOUT(&qctx->client->query))
- {
+ if (!qctx->is_zone && RECURSIONOK(qctx->client)) {
query_prefetch(qctx->client, qctx->fname,
qctx->rdataset);
}
@@ -10591,6 +10650,10 @@ query_ncache(query_ctx_t *qctx, isc_result_t result) {
}
}
+ if (!qctx->is_zone && RECURSIONOK(qctx->client)) {
+ query_stale_refresh_ncache(qctx->client);
+ }
+
return query_nodata(qctx, result);
cleanup:
@@ -12093,20 +12156,6 @@ ns_query_done(query_ctx_t *qctx) {
nodetach = qctx->client->nodetach;
query_send(qctx->client);
- if (qctx->refresh_rrset) {
- /*
- * If we reached this point then it means that we have found a
- * stale RRset entry in cache and BIND is configured to allow
- * queries to be answered with stale data if no active RRset
- * is available, i.e. "stale-anwer-client-timeout 0". But, we
- * still need to refresh the RRset. To prevent adding duplicate
- * RRsets, clear the RRsets from the message before doing the
- * refresh.
- */
- message_clearrdataset(qctx->client->message, 0);
- query_refresh_rrset(qctx);
- }
-
if (!nodetach) {
qctx->detach_client = true;
}

View File

@ -80,7 +80,7 @@ License: MPL-2.0 AND ISC AND MIT AND BSD-3-Clause AND BSD-2-Clause
# Before rebasing bind, ensure bind-dyndb-ldap is ready to be rebuild and use side-tag with it.
# Updating just bind will cause freeipa-dns-server package to be uninstallable.
Version: 9.18.33
Release: 16%{?dist}
Release: 17%{?dist}
Epoch: 32
Url: https://www.isc.org/downloads/bind/
#
@ -160,6 +160,9 @@ Patch227: bind-9.20-CVE-2025-8677-dual-signing-test.patch
# https://gitlab.isc.org/isc-projects/bind9/-/merge_requests/11329
Patch228: bind-9.20-robust-key-rollovers.patch
Patch229: bind-9.20-robust-key-rollovers-tests.patch
# https://gitlab.isc.org/isc-projects/bind9/-/merge_requests/10767
Patch230: bind-9.20-stale-cname.patch
Patch231: bind-9.20-stale-cname-tests.patch
%{?systemd_ordering}
# https://fedoraproject.org/wiki/Changes/RPMSuportForSystemdSysusers
@ -961,6 +964,9 @@ fi;
%endif
%changelog
* Thu Feb 26 2026 Fedor Vorobev <fvorobev@redhat.com> - 32:9.18.33-17
- Backport fixes for stale CNAME chains. (RHEL-142289)
* Wed Jan 28 2026 Fedor Vorobev <fvorobev@redhat.com> - 32:9.18.33-16
- Backport fix for manual DNSSEC key rolllovers. (RHEL-144421)