[CVE-2026-3591] sec: usr: Fix a stack use-after-return flaw in SIG(0) handling code

A stack use-after-return flaw in SIG(0) handling code could enable ACL
bypass and/or assertion failures in certain circumstances. This flaw has
been fixed.

ISC would like to thank Mcsky23 for bringing this vulnerability to our
attention.

Closes isc-projects/bind9#5754

Merge branch '5754-stack-use-after-free-sig0' into 'v9.21.20-release'

See merge request isc-private/bind9!920
This commit is contained in:
Michał Kępień 2026-03-13 14:31:40 +01:00
commit c64392c731
4 changed files with 191 additions and 10 deletions

View file

@ -24,6 +24,8 @@
#include <sys/types.h>
#include <unistd.h>
#include <dns/acl.h>
#ifdef HAVE_DNSTAP
#include <fstrm.h>
#endif
@ -280,10 +282,10 @@ struct zonelistentry {
* asynchronously.
*/
typedef struct matching_view_ctx {
isc_netaddr_t *srcaddr;
isc_netaddr_t *destaddr;
isc_netaddr_t srcaddr;
isc_netaddr_t destaddr;
dns_message_t *message;
dns_aclenv_t *env;
dns_aclenv_t *aclenv;
ns_server_t *sctx;
isc_loop_t *loop;
isc_job_cb cb;
@ -9299,6 +9301,8 @@ get_matching_view_done(void *cbarg) {
mvctx->cb(mvctx->cbarg);
dns_aclenv_detach(&mvctx->aclenv);
if (mvctx->quota_result == ISC_R_SUCCESS) {
isc_quota_release(&mvctx->sctx->sig0checksquota);
}
@ -9340,10 +9344,10 @@ get_matching_view_continue(void *cbarg, isc_result_t result) {
tsig = dns_tsigkey_identity(mvctx->message->tsigkey);
}
if (dns_acl_allowed(mvctx->srcaddr, tsig, mvctx->view->matchclients,
mvctx->env) &&
dns_acl_allowed(mvctx->destaddr, tsig,
mvctx->view->matchdestinations, mvctx->env) &&
if (dns_acl_allowed(&mvctx->srcaddr, tsig, mvctx->view->matchclients,
mvctx->aclenv) &&
dns_acl_allowed(&mvctx->destaddr, tsig,
mvctx->view->matchdestinations, mvctx->aclenv) &&
!(mvctx->view->matchrecursiveonly &&
(mvctx->message->flags & DNS_MESSAGEFLAG_RD) == 0))
{
@ -9415,9 +9419,9 @@ get_matching_view(isc_netaddr_t *srcaddr, isc_netaddr_t *destaddr,
matching_view_ctx_t *mvctx = isc_mem_get(message->mctx, sizeof(*mvctx));
*mvctx = (matching_view_ctx_t){
.srcaddr = srcaddr,
.destaddr = destaddr,
.env = env,
.srcaddr = *srcaddr,
.destaddr = *destaddr,
.aclenv = dns_aclenv_ref(env),
.cb = cb,
.cbarg = cbarg,
.sigresult = sigresult,

View file

@ -0,0 +1,41 @@
/*
* 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.
*/
options {
query-source address 10.53.0.1;
notify-source 10.53.0.1;
transfer-source 10.53.0.1;
port @PORT@;
pid-file "named.pid";
listen-on { 10.53.0.1; };
listen-on-v6 { none; };
recursion no;
notify no;
};
key rndc_key {
secret "1234abcd8765";
algorithm @DEFAULT_HMAC@;
};
controls {
inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
};
view "v1" {
match-clients { any; };
zone "." {
type hint;
file "/dev/null";
};
};

View file

@ -0,0 +1,17 @@
#!/bin/sh -e
# 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.
# shellcheck source=conf.sh
. ../conf.sh
key=$($KEYGEN -q -a RSASHA256 -b 2048 sig0.)

View file

@ -0,0 +1,119 @@
# 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.
import base64
import glob
import os
import struct
import time
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
import dns.flags
import dns.message
import dns.name
import dns.rdata
import dns.rdataclass
import dns.rdatatype
import dns.renderer
import dns.rrset
import isctest
def load_bind_private_key(filename):
"""Parses a BIND 9 .private key file."""
with open(filename, "r", encoding="utf-8") as f:
lines = f.readlines()
data = {}
for line in lines:
if ":" in line:
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
def b64int(k):
return int.from_bytes(base64.b64decode(data[k]), byteorder="big")
rsa_key = rsa.RSAPrivateNumbers(
p=b64int("Prime1"),
q=b64int("Prime2"),
d=b64int("PrivateExponent"),
dmp1=b64int("Exponent1"),
dmq1=b64int("Exponent2"),
iqmp=b64int("Coefficient"),
public_numbers=rsa.RSAPublicNumbers(
e=b64int("PublicExponent"), n=b64int("Modulus")
),
).private_key(default_backend())
return rsa_key
def make_sig0_query(key_file, key_name_str):
private_key = load_bind_private_key(key_file)
qname = dns.name.from_text(".")
query = dns.message.make_query(qname, dns.rdatatype.SOA)
query.flags |= dns.flags.RD
# Render message to bytes (needed for signing)
renderer = dns.renderer.Renderer()
query.to_wire(renderer)
msg_bytes = renderer.get_wire()
# SIG(0) Constants
basename = os.path.basename(key_file)
key_tag = int(basename.split("+")[2].split(".")[0])
now = int(time.time())
expiration = now + 3600
inception = now - 3600
signer_name = dns.name.from_text(key_name_str)
# Construct SIG RDATA header (0=SIG(0), 8=RSASHA256, 0=Labels)
sig_rdata_header = struct.pack(
"!HBBIIIH", 0, 8, 0, 0, expiration, inception, key_tag
)
sig_rdata_pre_sig = sig_rdata_header + signer_name.to_wire()
# Sign: ( SIG RDATA sans signature ) + ( Message )
signature = private_key.sign(
sig_rdata_pre_sig + msg_bytes, padding.PKCS1v15(), hashes.SHA256()
)
# Create the SIG RR
full_sig_rdata = sig_rdata_pre_sig + signature
sig_rr = dns.rdata.from_wire(
dns.rdataclass.ANY,
dns.rdatatype.SIG,
full_sig_rdata,
0,
len(full_sig_rdata),
)
sig_rrset = dns.rrset.from_rdata(qname, 0, sig_rr)
query.additional.append(sig_rrset)
return query
def test_sig0_acl_bypass():
key_files = glob.glob("Ksig0.+*.private")
assert len(key_files) == 1
query = make_sig0_query(key_files[0], "sig0.")
# Send the query
res = isctest.query.tcp(query, "10.53.0.1")
isctest.check.servfail(res)