A missing bounds check in glibc's ns_sprintrrf TSIG handling path allows up to 6 bytes of out-of-bounds write via sprintf, affecting all glibc versions since 2.2.
# A Hidden Flaw in How Computers Handle Internet Addresses
Deep inside the GNU C Library—software that runs on millions of computers worldwide—there's a dangerous flaw in some old, rarely-used code. Think of it like discovering a structural weakness in a building blueprint that's been copied thousands of times. The problem has been there since 2002, but most people never knew it existed.
Here's what's happening: when your computer looks up a website address on the internet, it sends DNS requests. This vulnerability sits in the code that handles a special type of security check called TSIG records. An attacker could craft a fake DNS response that causes this old code to write data beyond its intended boundaries—like overfilling a water container until it spills into the machinery next to it.
If exploited, this could give a hacker complete control over your computer. They could steal your passwords, install spyware, or use your machine to attack others. The good news is that these vulnerable functions are "deprecated," meaning they're outdated and most modern software doesn't use them anymore.
Who should worry most? System administrators running older Linux servers, and companies still relying on legacy applications. Regular home computer users are at lower risk unless they're running very old setups.
What can you actually do? First, check with your software providers if they use these specific functions and ask about updates. Second, if you manage servers, prioritize updating your GNU C Library and apply security patches as they're released. Third, consider using DNS security tools like DNSSEC to verify that DNS responses are genuine. Most importantly, don't wait for active attacks to appear—update now while this is still just a theoretical threat.
Want the full technical analysis? Click "Technical" above.
CVE-2026-5435 is a stack/heap out-of-bounds write in the GNU C Library's DNS record printing subsystem, specifically within the TSIG record handling path of ns_sprintrrf(). The vulnerability was introduced in glibc 2.2 at commit b43b13ac2544b11f35be301d1589b51a8473e32b and affects every release since. It was reported by researcher shinobu and publicly disclosed on 2026-04-02 as GLIBC-SA-2026-0011.
The defect is a classic length-unchecked sprintf() into a caller-supplied buffer. The vulnerable write can push up to 6 bytes past the end of the buffer. Because the affected functions (ns_printrrf, ns_printrr, fp_nquery) are debugging interfaces deprecated since glibc 2.34, they are not in the live DNS resolver path — but any application still invoking them against attacker-influenced DNS responses is directly exposed.
Root cause: Inside ns_sprintrrf(), the TSIG record case performs a formatted write with sprintf() directly into the remaining output buffer without first verifying that sufficient space exists, permitting up to 6 bytes of overflow past the caller-supplied bound.
Affected Component
The affected code lives in resolv/ns_print.c within the glibc source tree. The three exported symbols that reach the vulnerable code are:
ns_printrrf() — prints a pre-parsed resource record to a file descriptor with explicit buffer
ns_printrr() — thin wrapper over ns_printrrf()
fp_nquery() — iterates all RRs in a raw DNS message, calling ns_sprintrrf() for each
All three are declared in <arpa/nameser.h> and resolve through ns_sprintrrf(), the internal workhorse that formats a single resource record into a fixed-size character buffer.
Root Cause Analysis
The following is a reconstructed pseudocode representation of the vulnerable section inside ns_sprintrrf(), based on the advisory description and the glibc source history at the identified commit:
/*
* ns_sprintrrf() - resolv/ns_print.c
*
* Formats one DNS resource record into buf[0..*buflen].
* The caller passes (buf, buflen) where buflen is the
* remaining space. We must not write past buf + *buflen.
*/
int
ns_sprintrrf(const u_char *msg, size_t msglen,
const char *name, ns_class class, ns_type type,
u_long ttl, const u_char *rdata, size_t rdlen,
const char *contextp, const char *comment,
char *buf, size_t *buflen)
{
char *cp = buf;
size_t n;
/* ... common RR header formatting (name, TTL, class, type) ...
Each field advances cp and decrements *buflen via T() macro */
switch (type) {
/* --- dozens of RR types handled correctly above --- */
case ns_t_tsig: {
/*
* TSIG rdata layout:
* algorithm name (wire-format domain)
* 6 bytes: time_high (u16) + time_low (u32)
* 2 bytes: fudge (u16)
* 2 bytes: mac_size (u16)
* mac_size bytes: MAC
* ...
*/
const u_char *rp = rdata;
char algorithm[NS_MAXDNAME];
u_int32_t time_low;
u_int16_t time_high, fudge;
/* decompress algorithm name - length validated */
n = ns_name_uncompress(msg, msg + msglen, rp,
algorithm, sizeof(algorithm));
rp += n;
time_high = ns_get16(rp); rp += NS_INT16SZ;
time_low = ns_get32(rp); rp += NS_INT32SZ;
fudge = ns_get16(rp); rp += NS_INT16SZ;
/*
* BUG: sprintf writes directly into cp without checking *buflen.
* The format " %s %u %u" with algorithm/time_high/fudge can
* produce up to (NS_MAXDNAME + 6) characters, but only the
* *remaining* bytes in the caller's buffer follow cp.
* When the buffer is nearly full, this writes up to 6 bytes
* past buf + original_buflen.
*/
n = sprintf(cp, " %s %u %u", // BUG: missing bounds check here
algorithm,
(u_int32_t)time_high << 16 | time_low,
fudge);
cp += n;
*buflen -= n; /* underflows if n > *buflen; cp now past end */
/* If assertions are enabled, the subsequent T() macro check
* "len <= *buflen" fires as an assertion failure — but only
* after the out-of-bounds bytes are already written. */
break;
}
/* ... */
}
}
The T() macro used throughout the rest of the function performs the standard pattern:
#define T(x) do { \
if ((x) > *buflen) goto overflow; \
cp += (x); *buflen -= (x); \
} while (0)
Every other record type runs through T(). The TSIG case bypasses it entirely, calling sprintf() naked. The maximum overwrite is bounded by the formatting of time_high (max 5 digits) and fudge (max 5 digits) plus the space and null terminator — roughly 14 bytes total for the numeric portion, with the advisory conservatively citing 6 bytes for the numeric overflow beyond a near-full buffer.
STACK STATE — before TSIG sprintf (buffer nearly exhausted):
[ buf+0x000 ] "example.com. 300 IN TSIG hmac-sha256" <- formatted so far
...
[ buf+0x3F8 ] last 8 bytes of legitimate content
[ buf+0x400 ] buflen = 0x00000004 (only 4 bytes remain)
^--- cp points here, *buflen = 4
sprintf(cp, " %s %u %u", "hmac-sha256.", 0x000162A8B3, 300)
produces: " hmac-sha256. 23243443 300\0" (28 bytes)
STACK STATE — after sprintf:
[ buf+0x400 ] " hma" <- 4 bytes within bounds
[ buf+0x404 ] "c-sh" <- OOB +0 to +3 (buflen field corrupted)
[ buf+0x408 ] "a256" <- OOB +4 to +7 (saved_rbp low bytes)
[ buf+0x40C ] ". 23" <- OOB +8 to +11 (saved_rbp high bytes)
[ buf+0x410 ] "2434" <- OOB +12 (saved_rip corrupted)
...
*buflen has wrapped (size_t underflow): 0x4 - 28 = 0xFFFFFFFFFFFFFFEC
Subsequent assertion: (len <= *buflen) is FALSE -> abort() if NDEBUG unset
Exploitation Mechanics
EXPLOIT CHAIN (application using ns_printrr against attacker DNS response):
1. Attacker operates a malicious DNS server (or performs MitM on UDP/53).
2. Target application calls fp_nquery() or ns_printrr() on a raw DNS
response — common in DNS debugging tools, custom resolvers, or
network monitoring daemons.
3. Attacker crafts a DNS response containing a TSIG record where:
- Algorithm name is valid but long (e.g., 200-char label)
- time_high / fudge are set to produce maximum-length decimal strings
- The response is otherwise well-formed enough to pass early checks
4. ns_sprintrrf() formats all preceding RR fields into buf, consuming
space until *buflen is small (attacker controls record ordering and
field lengths to tune remaining space to ~4-8 bytes).
5. TSIG case invokes sprintf() without checking *buflen.
Up to ~14 bytes are written past the end of buf.
6a. Stack buffer scenario:
Saved RIP / return address is partially overwritten.
On function return, control flow is hijacked.
ASLR limits direct RIP control but partial overwrites remain viable.
6b. Heap buffer scenario (buf from malloc):
Heap metadata in adjacent chunk is corrupted.
Next heap operation (free/malloc) processes corrupted metadata.
Exploitable via standard glibc heap feng shui techniques.
7. If assertions are compiled in (debug builds), process aborts before
step 6 — turning the bug into a reliable DoS at minimum.
Patch Analysis
The correct fix replaces the bare sprintf() with a bounds-checked snprintf() and routes the result through the existing T() macro pattern, matching every other record type in the function:
// BEFORE (vulnerable — resolv/ns_print.c, glibc <= affected):
case ns_t_tsig: {
/* ... rdata parsing ... */
n = sprintf(cp, " %s %u %u", // BUG: unchecked write into cp
algorithm,
(u_int32_t)time_high << 16 | time_low,
fudge);
cp += n;
*buflen -= n;
break;
}
// AFTER (patched):
case ns_t_tsig: {
/* ... rdata parsing ... */
n = snprintf(cp, *buflen, " %s %u %u", // bounded by remaining space
algorithm,
(u_int32_t)time_high << 16 | time_low,
fudge);
if (n >= *buflen)
goto overflow; // consistent with T() behavior
cp += n;
*buflen -= n;
break;
}
The goto overflow path was already present in the function for all T()-guarded paths; the patch simply makes TSIG consistent with the existing error handling discipline. Note that the advisory explicitly states the functions are deprecated since 2.34 — the patch is a correctness fix, not a rehabilitation of the API.
Detection and Indicators
Because ns_printrrf / ns_printrr / fp_nquery are not in the live resolver path, detection focuses on application-level invocation:
Binary grep:objdump -d target_binary | grep -E 'ns_printrr|fp_nquery|ns_sprintrrf' — any match indicates exposure.
Dynamic trace:ltrace -e ns_printrr ./app will catch runtime calls before crash.
ASan/crash signature: AddressSanitizer will report WRITE of size N at 0x... overflows destination buffer with ns_sprintrrf in the stack trace.
Assertion failure log: On debug glibc builds the process prints Assertion 'len <= *buflen' failed from resolv/ns_print.c before aborting — this is a reliable indicator of exploitation attempt even without a crash.
TSIG record in response: Any DNS response delivering a TSIG record to a process using these deprecated functions is a candidate trigger input.
Remediation
Immediate: Upgrade glibc to the patched release incorporating the fix for GLIBC-SA-2026-0011. Consult your distribution's security tracker for the specific package version.
Code-level: Audit all codebases for calls to ns_printrrf, ns_printrr, and fp_nquery. These functions have been deprecated since glibc 2.34 (2021-08-02) and carry no guarantee of continued availability. Replace with structured DNS parsing via ns_initparse() / ns_parserr() and application-controlled formatting, or use a purpose-built DNS library (ldns, c-ares) that does not expose raw record-printing interfaces.
Defense-in-depth: Compile with -D_FORTIFY_SOURCE=2 — glibc's fortified __sprintf_chk will detect the overflow and abort before memory is corrupted, converting a potential RCE into a hard crash. Enable stack canaries (-fstack-protector-strong) to catch the stack-buffer variant. ASLR and full RELRO remain relevant mitigations for the heap variant.