home intel cve-2026-5435-glibc-ns-sprintrrf-tsig-oob-write
CVE Analysis 2026-04-28 · 7 min read

CVE-2026-5435: glibc ns_sprintrrf TSIG Out-of-Bounds Write

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.

#buffer-overflow#dns-tsig#glibc#remote-code-execution#out-of-bounds-write
Technical mode — for security professionals
▶ Attack flow — CVE-2026-5435 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-5435Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

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.

Memory Layout


/*
 * Caller stack frame (typical ns_printrrf usage):
 */
struct caller_frame {
    /* +0x000 */ char     buf[1024];   // output buffer passed to ns_printrrf
    /* +0x400 */ size_t   buflen;      // remaining length, passed as &buflen
    /* +0x408 */ void    *saved_rbp;
    /* +0x410 */ void    *saved_rip;   // <-- overwrite target if buf is on stack
};

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.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →