]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Test serve-stale with upstream zones and CNAMEs
authorMatthijs Mekking <matthijs@isc.org>
Thu, 19 Feb 2026 11:06:14 +0000 (12:06 +0100)
committerMatthijs Mekking <matthijs@isc.org>
Wed, 25 Feb 2026 10:30:34 +0000 (11:30 +0100)
Three variants of YWH-PGM40640-56: Stale/Wrong DNS Data Served via
CNAME Flag Leak (DNS_DBFIND_STALEOK persistence) are presented in
GitLab issue #5751. All these variants have been converted to system
tests.

Variant 1 forwards source.stale to another server, that provides a
CNAME record, while the resolver is authoritative for target.stale.
The CNAME points to a non-existing name. A stale CNAME record should
result in a stale NXDOMAIN (instead of SERVFAIL).

Variant 2 forwards both source.stale and target.stale to other servers.
This time the CNAME points to an A RRset. If the source.stale server
is not available (and stale-answer-client-timeout is off), the cached
CNAME should be followed and pick up the fresh RRset (instead of the
stale A RRset).

Variant 3 is similar to variant 2, but this time the CNAME points to
a non-existing name again. After flushing the target, BIND should
return a stale NXDOMAIN (instead of SERVFAIL).

(cherry picked from commit c32de7df957ea638f80bd12927029c68906c944b)

bin/tests/system/serve-stale/ans2/ans.pl
bin/tests/system/serve-stale/ans8/ans.pl [new file with mode: 0644]
bin/tests/system/serve-stale/ns6/stale.db
bin/tests/system/serve-stale/ns7/named.conf.j2 [new file with mode: 0644]
bin/tests/system/serve-stale/ns7/named1.conf.j2 [new file with mode: 0644]
bin/tests/system/serve-stale/ns7/root.db [new symlink]
bin/tests/system/serve-stale/ns7/target.stale.db [new file with mode: 0644]
bin/tests/system/serve-stale/tests.sh

index 7c231e2a80849115b9d5c70bdd4f5c6e5e060a9c..b437f732e1f595e3b3d0e4f7bdbdb23352c87d5c 100644 (file)
@@ -75,6 +75,15 @@ my $TARGET = "target.example 9 IN A $localaddr";
 my $SHORTCNAME = "shortttl.cname.example 1 IN CNAME longttl.target.example";
 my $LONGTARGET = "longttl.target.example 600 IN A $localaddr";
 
+#
+# YWH records
+#
+my $ywhSOA = "source.stale 300 IN SOA . . 0 0 0 0 300";
+my $ywhNS = "source.stale 300 IN NS ns.source.stale";
+my $ywhA = "ns.source.stale 300 IN A $localaddr";
+my $ywhCNAME = "alias.source.stale 2 IN CNAME www.target.stale";
+my $ywhCNAMENX = "aliasnx.source.stale 2 IN CNAME nonexist.target.stale";
+
 sub reply_handler {
     my ($qname, $qclass, $qtype) = @_;
     my ($rcode, @ans, @auth, @add);
@@ -306,6 +315,34 @@ sub reply_handler {
            push @auth, $rr;
        }
        $rcode = "NOERROR";
+    } elsif ($qname eq "source.stale") {
+       if ($qtype eq "SOA") {
+            my $rr = new Net::DNS::RR($ywhSOA);
+            push @ans, $rr;
+        } elsif ($qtype eq "NS") {
+            my $rr = new Net::DNS::RR($ywhNS);
+            push @ans, $rr;
+           $rr = new Net::DNS::RR($ywhA);
+           push @add, $rr;
+        }
+       $rcode = "NOERROR";
+    } elsif ($qname eq "ns.source.stale") {
+       if ($qtype eq "A") {
+           my $rr = new Net::DNS::RR($ywhA);
+           push @ans, $rr;
+       } else {
+           my $rr = new Net::DNS::RR($ywhSOA);
+           push @auth, $rr;
+       }
+       $rcode = "NOERROR";
+    } elsif ($qname eq "alias.source.stale") {
+        my $rr = new Net::DNS::RR($ywhCNAME);
+        push @ans, $rr;
+       $rcode = "NOERROR";
+    } elsif ($qname eq "aliasnx.source.stale") {
+        my $rr = new Net::DNS::RR($ywhCNAMENX);
+        push @ans, $rr;
+       $rcode = "NOERROR";
     } else {
         my $rr = new Net::DNS::RR($SOA);
        push @auth, $rr;
diff --git a/bin/tests/system/serve-stale/ans8/ans.pl b/bin/tests/system/serve-stale/ans8/ans.pl
new file mode 100644 (file)
index 0000000..af3f8f0
--- /dev/null
@@ -0,0 +1,164 @@
+#!/usr/bin/env perl
+
+# 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.
+
+use strict;
+use warnings;
+
+use IO::File;
+use IO::Socket;
+use Getopt::Long;
+use Net::DNS;
+use Time::HiRes qw(usleep nanosleep);
+
+my $pidf = new IO::File "ans.pid", "w" or die "cannot open pid file: $!";
+print $pidf "$$\n" or die "cannot write pid file: $!";
+$pidf->close or die "cannot close pid file: $!";
+sub rmpid { unlink "ans.pid"; exit 1; };
+
+$SIG{INT} = \&rmpid;
+$SIG{TERM} = \&rmpid;
+
+my $localaddr = "10.53.0.8";
+
+my $localport = int($ENV{'PORT'});
+if (!$localport) { $localport = 5300; }
+
+my $udpsock = IO::Socket::INET->new(LocalAddr => "$localaddr",
+   LocalPort => $localport, Proto => "udp", Reuse => 1) or die "$!";
+
+#
+# YWH records
+#
+my $ywhSOA = "target.stale 300 IN SOA . . 0 0 0 0 300";
+my $ywhNS = "target.stale 300 IN NS ns.target.stale";
+my $ywhA = "ns.target.stale 300 IN A $localaddr";
+my $ywhWWW = "www.target.stale 2 IN A 10.0.0.1";
+
+sub reply_handler {
+    my ($qname, $qclass, $qtype) = @_;
+    my ($rcode, @ans, @auth, @add);
+
+    print ("request: $qname/$qtype\n");
+    STDOUT->flush();
+
+    # Control what response we send.
+    if ($qname eq "update" ) {
+        if ($qtype eq "TXT") {
+            $ywhWWW = "www.target.stale 2 IN A 10.0.0.2";
+            my $rr = new Net::DNS::RR("$qname 0 $qclass TXT \"update\"");
+            push @ans, $rr;
+        }
+        $rcode = "NOERROR";
+        return ($rcode, \@ans, \@auth, \@add, { aa => 1 });
+    } elsif ($qname eq "restore" ) {
+        if ($qtype eq "TXT") {
+            $ywhWWW = "www.target.stale 2 IN A 10.0.0.1";
+            my $rr = new Net::DNS::RR("$qname 0 $qclass TXT \"restore\"");
+            push @ans, $rr;
+        }
+        $rcode = "NOERROR";
+        return ($rcode, \@ans, \@auth, \@add, { aa => 1 });
+    }
+
+    if ($qname eq "target.stale") {
+       if ($qtype eq "SOA") {
+            my $rr = new Net::DNS::RR($ywhSOA);
+            push @ans, $rr;
+        } elsif ($qtype eq "NS") {
+            my $rr = new Net::DNS::RR($ywhNS);
+            push @ans, $rr;
+           $rr = new Net::DNS::RR($ywhA);
+           push @add, $rr;
+        }
+       $rcode = "NOERROR";
+    } elsif ($qname eq "ns.target.stale") {
+       if ($qtype eq "A") {
+           my $rr = new Net::DNS::RR($ywhA);
+           push @ans, $rr;
+       } else {
+           my $rr = new Net::DNS::RR($ywhSOA);
+           push @auth, $rr;
+       }
+       $rcode = "NOERROR";
+    } elsif ($qname eq "www.target.stale") {
+       if ($qtype eq "A") {
+           my $rr = new Net::DNS::RR($ywhWWW);
+           push @ans, $rr;
+       } else {
+           my $rr = new Net::DNS::RR($ywhSOA);
+           push @auth, $rr;
+       }
+       $rcode = "NOERROR";
+    } else {
+        my $rr = new Net::DNS::RR($ywhSOA);
+       push @auth, $rr;
+       $rcode = "NXDOMAIN";
+    }
+
+    # mark the answer as authoritative (by setting the 'aa' flag)
+    return ($rcode, \@ans, \@auth, \@add, { aa => 1 });
+}
+
+GetOptions(
+    'port=i' => \$localport,
+);
+
+my $rin;
+my $rout;
+
+for (;;) {
+       $rin = '';
+       vec($rin, fileno($udpsock), 1) = 1;
+
+       select($rout = $rin, undef, undef, undef);
+
+       if (vec($rout, fileno($udpsock), 1)) {
+               my ($buf, $request, $err);
+               $udpsock->recv($buf, 512);
+
+               if ($Net::DNS::VERSION > 0.68) {
+                       $request = new Net::DNS::Packet(\$buf, 0);
+                       $@ and die $@;
+               } else {
+                       my $err;
+                       ($request, $err) = new Net::DNS::Packet(\$buf, 0);
+                       $err and die $err;
+               }
+
+               my @questions = $request->question;
+               my $qname = $questions[0]->qname;
+               my $qclass = $questions[0]->qclass;
+               my $qtype = $questions[0]->qtype;
+               my $id = $request->header->id;
+
+               my ($rcode, $ans, $auth, $add, $headermask) = reply_handler($qname, $qclass, $qtype);
+
+               if (!defined($rcode)) {
+                       print "  Silently ignoring query\n";
+                       next;
+               }
+
+               my $reply = Net::DNS::Packet->new();
+               $reply->header->qr(1);
+               $reply->header->aa(1) if $headermask->{'aa'};
+               $reply->header->id($id);
+               $reply->header->rcode($rcode);
+               $reply->push("question",   @questions);
+               $reply->push("answer",     @$ans)  if $ans;
+               $reply->push("authority",  @$auth) if $auth;
+               $reply->push("additional", @$add)  if $add;
+
+               my $num_chars = $udpsock->send($reply->data);
+               print "  Sent $num_chars bytes via UDP\n";
+       }
+}
index 4ae006802ba35acd11c0f109e33b1ca9f0d9de07..8cb4224ef33051854d31015b08133f99136d37b1 100644 (file)
@@ -9,9 +9,12 @@
 ; See the COPYRIGHT file distributed with this work for additional
 ; information regarding copyright ownership.
 
-stale.         IN SOA  ns.stale. matthijs.isc.org. 1 0 0 0 0
-stale.         IN NS   ns.stale.
-ns.stale.      IN A    10.53.0.6
+stale.                 IN SOA  ns.stale. matthijs.isc.org. 1 0 0 0 0
+stale.                 IN NS   ns.stale.
+ns.stale.              IN A    10.53.0.6
 
-serve.stale.   IN NS   ns.serve.stale.
-ns.serve.stale.        IN A    10.53.0.6
+serve.stale.           IN NS   ns.serve.stale.
+ns.serve.stale.                IN A    10.53.0.6
+
+target.stale.          IN NS   ns.target.stale.
+ns.target.stale.       IN A    10.53.0.7
diff --git a/bin/tests/system/serve-stale/ns7/named.conf.j2 b/bin/tests/system/serve-stale/ns7/named.conf.j2
new file mode 100644 (file)
index 0000000..c1600fe
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+key rndc_key {
+       secret "1234abcd8765";
+       algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+       inet 10.53.0.7 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+options {
+       query-source address 10.53.0.7;
+       notify-source 10.53.0.7;
+       transfer-source 10.53.0.7;
+       port @PORT@;
+       pid-file "named.pid";
+       listen-on { 10.53.0.7; };
+       listen-on-v6 { none; };
+       recursion yes;
+       dnssec-validation no;
+       qname-minimization off;
+
+       stale-answer-enable yes;
+       stale-cache-enable yes;
+       max-stale-ttl 3600;
+
+       stale-answer-client-timeout off;
+       stale-refresh-time 30;
+
+       max-cache-ttl 300;
+       max-ncache-ttl 300;
+};
+
+zone "." {
+       type hint;
+       file "root.db";
+};
+
+// Authoritative zone: nonexist.target.stale -> NXDOMAIN
+zone "target.stale" {
+    type primary;
+    file "target.stale.db";
+};
+
+// Forward source.stale queries to ans2
+zone "source.stale" {
+    type forward;
+    forward only;
+    forwarders { 10.53.0.2 port @PORT@; };
+};
diff --git a/bin/tests/system/serve-stale/ns7/named1.conf.j2 b/bin/tests/system/serve-stale/ns7/named1.conf.j2
new file mode 100644 (file)
index 0000000..bc1992d
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+key rndc_key {
+       secret "1234abcd8765";
+       algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+       inet 10.53.0.7 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+options {
+       query-source address 10.53.0.7;
+       notify-source 10.53.0.7;
+       transfer-source 10.53.0.7;
+       port @PORT@;
+       pid-file "named.pid";
+       listen-on { 10.53.0.7; };
+       listen-on-v6 { none; };
+       recursion yes;
+       dnssec-validation no;
+       qname-minimization off;
+
+       stale-answer-enable yes;
+       stale-cache-enable yes;
+       max-stale-ttl 3600;
+
+       stale-answer-client-timeout off;
+       stale-refresh-time 30;
+
+       max-cache-ttl 300;
+       max-ncache-ttl 300;
+};
+
+zone "." {
+       type hint;
+       file "root.db";
+};
+
+// Forward source.stale queries to ans2
+zone "source.stale" {
+    type forward;
+    forward only;
+    forwarders { 10.53.0.2 port @PORT@; };
+};
+
+// Forward target.stale queries to ans8
+zone "target.stale" {
+    type forward;
+    forward only;
+    forwarders { 10.53.0.8 port @PORT@; };
+};
diff --git a/bin/tests/system/serve-stale/ns7/root.db b/bin/tests/system/serve-stale/ns7/root.db
new file mode 120000 (symlink)
index 0000000..c354694
--- /dev/null
@@ -0,0 +1 @@
+../ns1/root.db
\ No newline at end of file
diff --git a/bin/tests/system/serve-stale/ns7/target.stale.db b/bin/tests/system/serve-stale/ns7/target.stale.db
new file mode 100644 (file)
index 0000000..a39edf2
--- /dev/null
@@ -0,0 +1,18 @@
+; 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.
+
+target.stale.          IN SOA  ns.target.stale. ywh. 1 0 0 0 0
+target.stale.          IN NS   ns.target.stale.
+ns.target.stale.       IN A    10.53.0.6
+
+; NOTE: "nonexist.target.stale." is NOT defined here.
+; Queries for it will return authoritative NXDOMAIN.
+; This is the CNAME target from alias.source.stale.
index 807700f816eaeea4daf83f02e9de1f3514811925..3a7be4ae0fd27c9a8c6e693d03805a57f845a93c 100755 (executable)
@@ -24,6 +24,212 @@ stale_answer_ttl=$(sed -ne 's,^[[:space:]]*stale-answer-ttl \([[:digit:]]*\).*,\
 status=0
 n=0
 
+#
+# YWH-PGM40640-56:
+# Stale/Wrong DNS Data Served via CNAME Flag Leak.
+#
+echo_i "test server with serve-stale options set"
+
+#
+# Variant 1: local authoritative zone
+#
+
+# Initial query — populates cache, gets correct NXDOMAIN
+n=$((n + 1))
+echo_i "prime cache aliasnx.source.stale A ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.7 aliasnx.source.stale A >dig.out.test$n || ret=1
+grep "status: NXDOMAIN" dig.out.test$n >/dev/null || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Wait for CNAME TTL to expire
+sleep 3
+# Kill auth server — source.test becomes unreachable
+n=$((n + 1))
+echo_i "disable responses from authoritative server ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.2 txt disable >dig.out.test$n || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+grep "TXT.\"0\"" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Query via stale CNAME — triggers the bug
+n=$((n + 1))
+echo_i "check stale aliasnx.source.stale A ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.7 aliasnx.source.stale A >dig.out.test$n || ret=1
+grep "status: NXDOMAIN" dig.out.test$n >/dev/null || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Restore auth server
+n=$((n + 1))
+echo_i "enable responses from authoritative server ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.2 txt enable >dig.out.test$n || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+grep "TXT.\"1\"" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+#
+# Variant 2: stale/wrong data served
+#
+n=$((n + 1))
+echo_i "updating ns7/named.conf ($n)"
+ret=0
+cp ns7/named1.conf ns7/named.conf
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "running 'rndc reload' ($n)"
+ret=0
+rndc_reload ns7 10.53.0.7
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Initial query — caches both CNAME and A record
+n=$((n + 1))
+echo_i "prime cache alias.source.stale A ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.7 alias.source.stale A >dig.out.test$n || ret=1
+grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
+grep "ANSWER: 2," dig.out.test$n >/dev/null || ret=1
+grep "alias.source.stale.*2.*IN.*CNAME.*www.target.stale." dig.out.test$n >/dev/null || ret=1
+grep "www.target.stale.*2.*IN.*A.*10.0.0.1" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Wait for both TTLs to expire
+sleep 3
+# Kill source.test auth (CNAME becomes stale)
+n=$((n + 1))
+echo_i "disable responses from authoritative server ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.2 txt disable >dig.out.test$n || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+grep "TXT.\"0\"" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Kill target auth, restart with NEW IP (10.0.0.2)
+n=$((n + 1))
+echo_i "update target authoritative server ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.8 txt update >dig.out.test$n || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+grep "TXT.\"update\"" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Query via stale CNAME — triggers the bug
+n=$((n + 1))
+echo_i "check stale alias.source.stale A ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.7 alias.source.stale A >dig.out.test$n || ret=1
+grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
+grep "ANSWER: 2," dig.out.test$n >/dev/null || ret=1
+grep "alias.source.stale.*30.*IN.*CNAME.*www.target.stale." dig.out.test$n >/dev/null || ret=1
+grep "www.target.stale.*2.*IN.*A.*10.0.0.2" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Control: direct query for same name (no stale CNAME involved)
+n=$((n + 1))
+echo_i "check target www.target.stale A ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.7 www.target.stale A >dig.out.test$n || ret=1
+grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+grep "www.target.stale.*IN.*A.*10.0.0.2" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Restore auth servers
+n=$((n + 1))
+echo_i "enable responses from authoritative server ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.2 txt enable >dig.out.test$n || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+grep "TXT.\"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 "update target authoritative server ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.8 txt restore >dig.out.test$n || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+grep "TXT.\"restore\"" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+#
+# Variant 3: recursion blocked, servfail
+#
+
+# Flush stale data
+n=$((n + 1))
+echo_i "flush stale data ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 flushtree stale >/dev/null 2>&1 || ret=1
+sleep 1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Initial query — NXDOMAIN via CNAME chain through BOTH forwarders
+n=$((n + 1))
+echo_i "prime cache aliasnx.source.stale A ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.7 aliasnx.source.stale A >dig.out.test$n || ret=1
+grep "status: NXDOMAIN" dig.out.test$n >/dev/null || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+grep "aliasnx.source.stale.*2.*IN.*CNAME.*nonexist.target.stale." dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Wait for CNAME TTL to expire
+sleep 3
+# Kill source.test auth ONLY (target.test auth stays alive!)
+n=$((n + 1))
+echo_i "disable responses from authoritative server ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.2 txt disable >dig.out.test$n || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+grep "TXT.\"0\"" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Flush target's negative cache entry (simulates cache eviction/pressure)
+n=$((n + 1))
+echo_i "flush name nonexist.target.stale ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 flushname nonexist.target.stale >/dev/null 2>&1 || ret=1
+sleep 1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Verify target auth is STILL ALIVE and returns correct NXDOMAIN
+n=$((n + 1))
+echo_i "verify nonexist.target.stale A ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.8 nonexist.target.stale A >dig.out.test$n || ret=1
+grep "status: NXDOMAIN" dig.out.test$n >/dev/null || ret=1
+grep "ANSWER: 0," dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+# Query via stale CNAME — triggers the bug
+n=$((n + 1))
+echo_i "check stale aliasnx.source.stale A ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.7 aliasnx.source.stale A >dig.out.test$n || ret=1
+grep "status: NXDOMAIN" dig.out.test$n >/dev/null || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+grep "aliasnx.source.stale.*30.*IN.*CNAME.*nonexist.target.stale." dig.out.test$n >/dev/null || ret=1
+# Restore auth server
+n=$((n + 1))
+echo_i "enable responses from authoritative server ($n)"
+ret=0
+$DIG -p ${PORT} @10.53.0.2 txt enable >dig.out.test$n || ret=1
+grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
+grep "TXT.\"1\"" dig.out.test$n >/dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
 #
 # First test server with serve-stale options set.
 #