1. Overview

A use-after-free vulnerability exists in ISC BIND 9’s DNS-over-HTTPS (DoH) implementation. When a DoH response has been sent, the response buffer is freed but a dangling pointer (socket->h2->wbuf) is left pointing to the freed memory. If a client floods HTTP/2 SETTINGS frames that change INITIAL_WINDOW_SIZE, the nghttp2 library re-evaluates stream flow control and calls the data provider callback (server_read_callback), which reads from the freed buffer via memmove(). The UAF read is confirmed by AddressSanitizer and reliably crashes ASAN-instrumented builds (~40% per round). Against production BIND builds using jemalloc, the freed memory remains mapped and the read succeeds silently — the server does not crash. Information disclosure via the HTTP/2 DATA stream was not confirmed: although server_read_callback reads freed heap bytes, nghttp2 discards the result because the stream’s data provider had already signaled EOF; no extra bytes are transmitted to the attacker. The practical impact is therefore denial of service against hardened builds, and a latent memory safety violation in production that could become exploitable if nghttp2’s internal handling changes. ISC addressed this vulnerability in BIND 9.20.23 and 9.21.22.


2. Vulnerability Type

FieldValue
Primary CWECWE-416: Use After Free
Related CWECWE-825: Expired Pointer Dereference

3. Severity

CVSS 3.1 (from ISC Advisory)

FieldValue
Score7.4 (High)
VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H

Our Assessment (CVSS 4.0)

Metric GroupMetricValue
Base — ExploitabilityAttack Vector (AV)Network
Attack Complexity (AC)High
Attack Requirements (AT)Present
Privileges Required (PR)None
User Interaction (UI)None
Base — Vulnerable SystemConfidentiality (VC)None
Integrity (VI)None
Availability (VA)High
Base — Subsequent SystemConfidentiality (SC)None
Integrity (SI)None
Availability (SA)Low
ThreatExploit Maturity (E)Proof-of-Concept
FieldValue
CVSS 4.0 Score6.3 (Medium)
CVSS 4.0 VectorCVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:L/E:P

Notes:

  • AC:High — the UAF requires specific timing; reproduction rate is approximately 40% per round
  • AT:Present — DoH must be enabled in the named configuration (not enabled by default)
  • VC:None — although server_read_callback reads freed heap bytes, nghttp2 discards the result (stream already EOF); no data is transmitted to the attacker. 30 rounds of testing confirmed 0 extra bytes leaked.
  • SA:Low — named crash (under ASAN/hardened builds) affects all DNS resolution clients relying on the server

4. Affected Products

Affected Versions

ProductVersion RangeCPE 2.3
ISC BIND 9 (Stable)9.20.0 – 9.20.22cpe:2.3:a:isc:bind:*:*:*:*:*:*:*:* (versionStartIncluding=9.20.0, versionEndIncluding=9.20.22)
ISC BIND 9 (Development)9.21.0 – 9.21.21cpe:2.3:a:isc:bind:*:*:*:*:*:*:*:* (versionStartIncluding=9.21.0, versionEndIncluding=9.21.21)
ISC BIND 9 Supported Preview9.20.9-S1 – 9.20.22-S1cpe:2.3:a:isc:bind:*:*:*:*:supported_preview:*:*:*

Not affected: BIND 9.18.x (ESV) — the DoH code containing the vulnerability was introduced in 9.20.0.

Fixed Versions

ProductFixed Version
ISC BIND 9 (Stable)9.20.23
ISC BIND 9 (Development)9.21.22
ISC BIND 9 Supported Preview9.20.23-S1

Tested Environment (Vulnerable)

FieldValue
SoftwareISC BIND 9.20.22 (Stable Release)
Build IDe609907
Build Flags--enable-doh, ASAN (-fsanitize=address), --without-jemalloc
PlatformUbuntu 24.04 (x86_64), GCC 13.3.0
libnghttp21.59.0
libuv1.48.0

Tested Environment (Patched)

FieldValue
SoftwareISC BIND 9.20.23
Build FlagsSame as vulnerable (ASAN-enabled)
Fix Commit3be272e26d

5. Root Cause Analysis

5a. Detailed Description

The vulnerability is in lib/isc/netmgr/http.c, in the server_httpsend() function that handles sending DoH responses.

When a DNS query arrives over HTTP/2, the server processes it and calls server_httpsend() to send the DNS response. This function:

  1. Initializes sock->h2->wbuf to point to the DNS response data:
// server_httpsend (http.c, around offset +0x16)
isc_buffer_init(&sock->h2->wbuf, req->uvbuf.base, req->uvbuf.len);
isc_buffer_add(&sock->h2->wbuf, req->uvbuf.len);
  1. Registers server_read_callback as the nghttp2 data provider callback for the HTTP/2 stream:
// server_send_response (http.c)
data_prd.source.ptr = socket;
data_prd.read_callback = server_read_callback;
rv = nghttp2_submit_response(ngsession, stream_id, nva, nvlen, &data_prd);
  1. Drives the nghttp2 session to send the response via http_do_bio().

  2. Frees the request structure, which triggers detachment of the network handle:

// server_httpsend (http.c, final line — VULNERABLE)
isc__nm_uvreq_put(&req);

The isc__nm_uvreq_put() call detaches the handle (isc_nmhandle_detach), and when the handle’s reference count reaches zero, ns__client_put_cb is called which frees the ns_client_t structure — including the DNS response data that sock->h2->wbuf still points to.

The server_read_callback function reads from this dangling pointer:

// server_read_callback (http.c:2387)
static ssize_t
server_read_callback(nghttp2_session *ngsession, int32_t stream_id,
                     uint8_t *buf, size_t length, uint32_t *data_flags,
                     nghttp2_data_source *source, void *user_data) {
    isc_nmsocket_t *socket = (isc_nmsocket_t *)source->ptr;
    size_t buflen;

    buflen = isc_buffer_remaininglength(&socket->h2->wbuf);  // reads freed memory
    if (buflen > length) {
        buflen = length;
    }
    if (buflen > 0) {
        (void)memmove(buf, isc_buffer_current(&socket->h2->wbuf), buflen);  // UAF READ
        isc_buffer_forward(&socket->h2->wbuf, buflen);
    }
    // ...
}

Trigger mechanism: When the client floods HTTP/2 SETTINGS frames that change INITIAL_WINDOW_SIZE, the nghttp2 library adjusts the flow control windows for all active streams. Per RFC 7540 Section 6.9.2, when INITIAL_WINDOW_SIZE changes, the receiver must adjust the size of all stream flow-control windows by the difference between the new and old values. This adjustment causes nghttp2 to re-evaluate pending data for streams, calling server_read_callback to check if there is more data to send. If the response buffer has already been freed but wbuf still points to it, the callback reads from freed heap memory.

5b. ASAN Stack Trace

USE (freed memory read):

==39547==ERROR: AddressSanitizer: heap-use-after-free on address 0x5240002bcc11
READ of size 1 at 0x5240002bcc11 thread T10
    #0  memmove (sanitizer_common_interceptors_memintrinsics.inc:98)
    #1  server_read_callback (netmgr/http.c:2395)
    #2  (libnghttp2.so.14+0xc25e)
    #3  nghttp2_session_mem_send (libnghttp2.so.14+0xd06e)
    #4  http_send_outgoing (netmgr/http.c:1435)
    #5  http_do_bio (netmgr/http.c:1706)
    #6  http_readcb (netmgr/http.c:1303)
    #7  tls_do_bio (netmgr/tlsstream.c:680)
    #8  tls_readcb (netmgr/tlsstream.c:841)
    #9  isc___nm_readcb (netmgr/netmgr.c:1876)
    #10 isc__nm_readcb (netmgr/netmgr.c:1891)
    #11 isc__nm_tcp_read_cb (netmgr/tcp.c:793)
    #12 (libuv.so.1 — uv__read)
    #13 (libuv.so.1 — uv__io_poll)
    #14 uv_run
    #15 loop_thread (lib/isc/loop.c:328)

FREE (when the response data was released):

freed by thread T10:
    #0  free (asan_malloc_linux.cpp:52)
    #1  sdallocx (jemalloc_shim.h:60)
    #2  mem_put (mem.c:417)
    #3  isc__mem_put (mem.c:762)
    #4  ns__client_put_cb (client.c:1727)
    #5  nmhandle_free (netmgr.c:926)
    #6  nmhandle__destroy (netmgr.c:938)
    #7  nmhandle_destroy (netmgr.c:991)
    #8  isc_nmhandle_unref (netmgr.c:1007)
    #9  isc_nmhandle_detach (netmgr.c:1007)
    #10 http_writecb (netmgr/http.c:1339)
    #11 tls_senddone (netmgr/tlsstream.c:224)
    #12 isc___nm_sendcb (netmgr.c:1902)

ALLOCATION (original DNS response data):

previously allocated by thread T10:
    #0  malloc (asan_malloc_linux.cpp:69)
    #1  mallocx (jemalloc_shim.h:41)
    #2  mem_get (mem.c:394)
    #3  isc__mem_get (mem.c:747)
    #4  ns_client_request (client.c:1799)
    #5  server_call_cb (netmgr/http.c:2536)
    #6  server_on_request_recv (netmgr/http.c:2633)
    #7  server_on_frame_recv_callback (netmgr/http.c:2844)
    #8  nghttp2_session_mem_recv

The freed region is 6,928 bytes (at offset 2,833 bytes into the allocation), confirming the DNS client structure containing the response data.

5c. Fix (Patched Version)

The fix in BIND 9.20.23 (commit 3be272e26d) adds a single line to server_httpsend():

--- a/lib/isc/netmgr/http.c
+++ b/lib/isc/netmgr/http.c
@@ -2753,6 +2753,8 @@
 	} else {
 		cb(handle, result, cbarg);
 	}
+
+	isc_buffer_initnull(&sock->h2->wbuf);
 	isc__nm_uvreq_put(&req);
 }

isc_buffer_initnull() zeros out all fields of the buffer structure:

static inline void
isc_buffer_initnull(isc_buffer_t *restrict b) {
    *b = (isc_buffer_t){
        .link = ISC_LINK_INITIALIZER,
        .magic = ISC_BUFFER_MAGIC,
    };
}

After the fix, when server_read_callback is invoked by nghttp2 during SETTINGS processing, isc_buffer_remaininglength(&socket->h2->wbuf) returns 0 (null base, zero length). The callback returns 0 bytes with NGHTTP2_DATA_FLAG_EOF set, preventing any read from freed memory.

5d. Impact

Denial of service (confirmed under ASAN): Under AddressSanitizer, the UAF is immediately detected and the named process aborts with SIGABRT. In testing against an ASAN-instrumented build, the server crashed within 4–6 rounds of SETTINGS flooding, with an approximate per-round reproduction rate of 40%. Similarly, builds with guard pages or heap hardening (e.g., MALLOC_CONF=junk:true) would convert the stale read into a SIGSEGV crash.

No crash on production builds (jemalloc): Against the default BIND build with jemalloc, freed memory is returned to jemalloc’s thread-local caches and remains mapped. The UAF read succeeds silently — the server does not crash. In testing, over 100 rounds of the PoC against a jemalloc build produced zero crashes.

Information disclosure NOT confirmed: Although server_read_callback reads up to 6,376 bytes of freed heap content (from a 6,928-byte ns_client_t allocation) into nghttp2’s internal buffer, this data is not transmitted back to the attacker. The reason: during the initial http_do_bio() call, server_read_callback reads all response data and sets NGHTTP2_DATA_FLAG_EOF. When SETTINGS processing later causes nghttp2 to re-invoke the callback, the read from freed memory occurs (confirmed by ASAN), but nghttp2 discards the result because the stream was already marked as fully submitted. In 30 rounds of testing with heap marker seeding, exactly 49 bytes (the expected DNS response) were received per stream — zero extra bytes leaked. The ISC advisory’s CVSS C:H (Confidentiality: High) appears to be based on the theoretical read path, not a confirmed data exfiltration.

Latent risk: While the information leak is not currently exploitable due to nghttp2’s EOF tracking, this is a real memory safety violation. Changes to nghttp2’s internal stream handling, or a different code path that doesn’t set EOF before the buffer is freed, could make the leak exploitable in the future. The fix is the correct remediation regardless of current exploitability.


6. Proof-of-Concept

6a. PoC Code

Download poc_cve_2026_3593.py (enterprise email verification required)

FileDescription
poc_cve_2026_3593.pyPython PoC using h2 and dnspython libraries

6b. Reproduce Instructions

Prerequisites:

  • BIND 9.20.22 or earlier compiled with DoH support (--enable-doh)
  • For reliable UAF detection: build with -fsanitize=address and --without-jemalloc
  • Python 3 with h2 and dnspython packages (pip install h2 dnspython)
  • DoH must be enabled in named.conf (see configuration below)

DoH configuration for named.conf:

tls local-tls {
    cert-file "/path/to/server.crt";
    key-file "/path/to/server.key";
};
http local-http-server {
    endpoints { "/dns-query"; };
};
options {
    listen-on port 443 tls local-tls http local-http-server { any; };
};

Steps:

  1. Start the vulnerable BIND 9 server:

    ASAN_OPTIONS="detect_leaks=0:abort_on_error=1" \
      LD_LIBRARY_PATH=/path/to/bind9/lib \
      /path/to/bind9/sbin/named -c named.conf -f -g
    
  2. Verify DoH is working:

    curl -k --http2 'https://10.0.0.1:443/dns-query?dns=AAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB' \
      -H 'Accept: application/dns-message' -w '%{http_code}'
    # Expected: 200
    
  3. Run the PoC:

    python3 poc_cve_2026_3593.py 10.0.0.1 443
    
  4. Expected output (within 4–10 rounds):

    [*] CVE-2026-3593 - BIND 9 DoH Heap Use-After-Free PoC
    [*] Target: 10.0.0.1:443
    [*] Rounds: 20
    
      [*] Round 1/20...
      [*] Round 2/20...
      ...
      [!] Connection error at round 6: [Errno 111] Connection refused
      [+] SERVER CRASHED! Connection refused.
    
  5. Check ASAN output on the server for the heap-use-after-free confirmation in server_read_callback.

6c. Test Results

MetricVulnerable (9.20.22)Patched (9.20.23)
Server status after PoCCRASHED (round 6)ALIVE (all 20 rounds)
ASAN errorsheap-use-after-free in server_read_callbackNone
Reproduction rate~40% per round0%

6d. Patched System Verification

The same PoC was run against BIND 9.20.23 (ASAN-enabled build). After 20 complete rounds (81,920 SETTINGS frames sent), the server remained fully operational with zero ASAN errors logged. The fix (isc_buffer_initnull) prevents server_read_callback from reading any freed memory by zeroing the buffer pointer before the request structure is released.


7. Detection

Note: The detection rules below are provided as a starting point. Validate and tune them in your own environment before deploying to production.

7a. Network-Based Detection

Signature-Based Detection

The attack manifests as a rapid burst of HTTP/2 SETTINGS frames on a TLS-encrypted connection to the DoH port (typically 443). While the SETTINGS frames themselves are encrypted within the TLS tunnel, detection can focus on:

  1. Connection-level anomaly: An abnormally high number of TLS records in a short time window from a single source to the DoH endpoint, indicating SETTINGS frame flooding.
  2. If TLS inspection is available: HTTP/2 SETTINGS frames (type 0x04) with rapidly changing INITIAL_WINDOW_SIZE (setting ID 0x03) values, sent in bursts of hundreds per connection.

Suricata Rules

# CVE-2026-3593: Detect rapid TLS record bursts to DoH endpoints
# (heuristic - detects the flood pattern, not individual SETTINGS frames)
alert tcp $EXTERNAL_NET any -> $HOME_NET 443 (msg:"CVE-2026-3593 Potential BIND DoH SETTINGS Flood - high TLS record rate"; \
  flow:to_server,established; \
  content:"|17 03 03|"; depth:3; \
  threshold:type both, track by_src, count 200, seconds 5; \
  reference:cve,2026-3593; \
  classtype:attempted-dos; \
  sid:2026003593; rev:1;)

# If TLS decryption is available (e.g., via mitmproxy or TLS termination):
# Detect HTTP/2 SETTINGS frame flood with INITIAL_WINDOW_SIZE changes
alert http2 any any -> $HOME_NET 443 (msg:"CVE-2026-3593 BIND DoH HTTP/2 SETTINGS INITIAL_WINDOW_SIZE Flood"; \
  http2.frametype:4; \
  content:"|00 03|"; offset:0; depth:2; \
  threshold:type both, track by_src, count 100, seconds 10; \
  reference:cve,2026-3593; \
  classtype:attempted-dos; \
  sid:2026003594; rev:1;)

7b. Host-Based Detection

Version Check

The most reliable detection is querying the BIND version directly:

named -V 2>&1 | head -1

Match the reported version against the affected ranges:

BranchVulnerableFixed
Stable9.20.0 – 9.20.229.20.23+
Development9.21.0 – 9.21.219.21.22+
Supported Preview9.20.9-S1 – 9.20.22-S19.20.23-S1+
ESV (9.18.x)Not affected

Additionally, the vulnerability only applies when DoH is enabled. Check the running configuration:

# Check if DoH is configured (listen-on with http)
named-checkconf -p 2>/dev/null | grep -E 'http|listen-on.*tls'

If named is not in $PATH or multiple versions are installed, scan for all instances:

find / -name "named" -type f -executable 2>/dev/null -exec sh -c '
  ver=$("{}" -V 2>&1 | head -1)
  echo "{}: $ver"
' \;

8. References

SourceURL
ISC Advisoryhttps://kb.isc.org/docs/cve-2026-3593
MITRE CVEhttps://www.cve.org/CVERecord?id=CVE-2026-3593
NVDhttps://nvd.nist.gov/vuln/detail/CVE-2026-3593
GitLab Issuehttps://gitlab.isc.org/isc-projects/bind9/-/issues/5755
Fix Commit3be272e26d (BIND 9.20.23)
Release Noteshttps://bind9.readthedocs.io/en/stable/changelog.html