home intel cve-2025-39946-linux-tls-skb-overflow
CVE Analysis 2025-10-04 · 9 min read

CVE-2025-39946: Linux Kernel TLS SKB Overflow via Bogus Record Headers

A heap overflow in the Linux kernel TLS subsystem allows overflow of allocated SKB space when bogus record headers arrive in small OOB chunks. CVSS 9.8 critical.

#tls-protocol#buffer-management#input-validation#denial-of-service#kernel-vulnerability
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2025-39946 · Vulnerability
ATTACKERLinuxVULNERABILITYCVE-2025-39946CRITICALSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2025-39946 is a critical (CVSS 9.8) heap overflow in the Linux kernel's in-kernel TLS implementation (net/tls/). The vulnerability exists in the record parsing path of the TLS receive side. When the kernel is under socket-buffer pressure, it reads record data out of the socket earlier than usual — before a full record has arrived — to prevent connection stalls. If the record header that eventually arrives is invalid (bogus length or type), the kernel fails to abort the stream promptly. Because more data is copied into the pre-allocated SKB on each retry pass, the kernel can write past the end of the allocated SKB data space, producing a classic heap overflow with attacker-influenced content.

Exploitation was demonstrated against the lts-6.12.48 kCTF kernel instance by farazsth98. The attack vector is network-accessible: a malicious TLS peer serves record headers in small out-of-band (OOB) sends to force the early-read path, then floods the receive buffer with a crafted payload designed to overflow the SKB allocation.

Root cause: When TLS record parsing is retried under socket-buffer pressure, each retry copies additional data into the SKB before re-validating the header, and a missing stream-abort on header validation failure allows the accumulated copies to overflow the original SKB allocation.

Affected Component

  • Subsystem: net/tls/tls_sw.c — software TLS record layer
  • Function: tls_sw_recvmsg() and the underlying decrypt_skb_update() / tls_strparser_msg_parser() path
  • Affected versions: Linux kernel prior to the fixing commit in the 6.x stable series (see NVD for exact range)
  • Trigger condition: Socket receive buffer small enough that the strparser calls back before a full TLS record is buffered; header delivered via TCP OOB/urgent data

Root Cause Analysis

The TLS software path uses the kernel's strparser (net/strparser.c) to frame TLS records out of the TCP stream. The strparser calls tls_strp_read_copyin() to accumulate record bytes. Under normal conditions the socket buffers the entire record before the strparser fires. Under buffer pressure the strparser fires early, before the full record is present, and tls_strp_msg_load() is called to read what is available so far into an SKB that was sized based on the advertised (but not yet validated) record length.

The critical path is inside tls_strp_msg_load() / tls_strp_read_sock():


/* net/tls/tls_strp.c — simplified, pre-patch */

static int tls_strp_read_sock(struct tls_strparser *strp)
{
    struct strp_msg *rxm = strp_msg(strp->anchor);
    int sz;

    /*
     * BUG: SKB was allocated using the length field from the TLS header
     * (strp->anchor->len = TLS_HEADER_SIZE + record_len).
     * If record_len is attacker-controlled and header is bogus, we
     * allocated exactly that many bytes — but we keep copying here
     * on every retry without re-checking whether the header is valid,
     * and without aborting when it is found invalid.
     */
    sz = tcp_read_sock(strp->sk, &strp->desc, tls_strp_copyin);  // BUG: copies more data each retry

    return sz;
}

static int tls_strp_copyin(read_descriptor_t *desc, struct sk_buff *in_skb,
                           unsigned int offset, size_t in_len)
{
    struct tls_strparser *strp = desc->arg.data;
    struct sk_buff *skb = strp->anchor;
    struct strp_msg *rxm = strp_msg(skb);
    size_t chunk;
    int err;

    if (skb->len >= rxm->full_len)          // already have enough?
        return 0;

    chunk = min_t(size_t, in_len, rxm->full_len - skb->len);

    /* Copies directly into skb tail — no re-validation of rxm->full_len
     * against actual allocated skb data space on retried calls.        */
    err = skb_add_data_nocache(strp->sk, skb, in_skb->data + offset, chunk);
    // BUG: if header validation later fails, we already wrote past alloc
    return err ?: chunk;
}

static int tls_strp_msg_parser(struct tls_strparser *strp)
{
    struct tls_record_info *rec;
    int header_err;

    header_err = tls_strp_check_rcv_hdr(strp);  // validates record type+len
    if (header_err) {
        /* MISSING: strp_done() / tls_strp_abort_strp() call here.
         * Without abort, the strparser will retry, copy more data,
         * and eventually overflow the skb allocation.               */
        return header_err;                       // BUG: no abort
    }
    /* ... */
}

The key invariant that breaks: rxm->full_len is written from the (attacker-supplied) TLS header before the header is validated. The SKB's data area is sized to full_len. On the first retry the copy stays within bounds. On subsequent retries, each call to tls_strp_copyin appends another chunk. Because rxm->full_len was never corrected (the abort never happened), the guard skb->len >= rxm->full_len never trips, and the write advances beyond the end of the allocated tailroom.

Exploitation Mechanics


EXPLOIT CHAIN (farazsth98, lts-6.12.48 kCTF):

1. Attacker opens a TLS connection to the victim kernel socket.

2. Attacker sends the 5-byte TLS record header (type + version + length)
   split across multiple small TCP OOB/urgent sends. This forces the
   strparser to fire before the full record body has arrived, triggering
   the early-read (buffer-pressure) path.

3. The bogus header encodes a record length L such that the kernel
   allocates an SKB with exactly L bytes of tailroom.

4. Attacker fills the TCP receive buffer with a crafted payload longer
   than L bytes. The strparser retries tls_strp_read_sock() on each
   select/poll wake-up.

5. Each retry appends another chunk (up to recv_window size) into the
   SKB past the original L-byte allocation. The overflow overwrites
   adjacent heap objects (typically other skb_shared_info or
   neighbouring slab objects on the same page).

6. With a heap grooming primitive (e.g., spray of fixed-size kmalloc
   objects) the attacker places a target object (e.g., a file struct,
   a cred struct, or a pipe_buffer) immediately after the TLS SKB.

7. Overwritten object fields are used to build a read/write primitive.
   farazsth98's PoC targets core_pattern for an lpe-to-code-execution
   path: overwrite core_pattern with a reverse-shell path, then trigger
   a crash in an unprivileged process.

8. Shell executes as root via the kernel's core dump handler.

The exploit author notes the only per-kernel tunable is CORE_PATTERN_OFFSET — the distance from _text to core_pattern — which can be extracted from a bzImage loaded with root privileges by comparing addresses in /proc/kallsyms.

Memory Layout


SLAB STATE BEFORE OVERFLOW (kmalloc-2k example, record length L = 0x400):

  [ skb->head                                                    ]
  [ skb_shared_info @ head + L      <-- allocation boundary      ]
  [ adjacent kmalloc obj @ head + L + sizeof(skb_shared_info)    ]
     e.g.: pipe_buffer / file * / cred *

skb tailroom:
  offset 0x000: [ TLS record data — legitimate copies            ]
  offset 0x3f8: [ last valid byte of L-byte allocation           ]
  offset 0x400: [ skb_shared_info.frags[0] — first overflow word ]

HEAP STATE AFTER N RETRIES (overflow of ~0x80 bytes past L):

  offset 0x400: skb_shared_info.frags[0].page.p    <- attacker data
  offset 0x408: skb_shared_info.frags[0].page_offset <- attacker data
  offset 0x410: skb_shared_info.frags[0].size        <- attacker data
  ...
  offset 0x440: adjacent object header CORRUPTED
                e.g. pipe_buffer->ops ptr = attacker-controlled fn ptr

/* Relevant struct layout — skb_shared_info (abridged) */
struct skb_shared_info {
    /* +0x00 */ __u8         flags;
    /* +0x01 */ __u8         meta_len;
    /* +0x02 */ __u8         nr_frags;
    /* +0x03 */ __u8         tx_flags;
    /* +0x04 */ unsigned short gso_size;
    /* +0x06 */ unsigned short gso_segs;
    /* +0x08 */ struct sk_buff *frag_list;
    /* +0x10 */ struct skb_shared_hwtstamps hwtstamps;
    /* +0x18 */ unsigned int   gso_type;
    /* +0x1c */ u32            tskey;
    /* +0x20 */ atomic_t       dataref;
    /* +0x28 */ skb_frag_t     frags[MAX_SKB_FRAGS];  // overflow lands here
};

Patch Analysis

The fix adds an explicit stream abort in the header-validation failure path, preventing any further copies into the already-allocated SKB:


// BEFORE (vulnerable — net/tls/tls_strp.c):
static int tls_strp_msg_parser(struct tls_strparser *strp)
{
    int header_err;

    header_err = tls_strp_check_rcv_hdr(strp);
    if (header_err) {
        return header_err;   // returns but does NOT abort stream
                             // strparser retries → more copies → overflow
    }
    tls_strp_msg_load(strp);
    return 0;
}

// AFTER (patched):
static int tls_strp_msg_parser(struct tls_strparser *strp)
{
    int header_err;

    header_err = tls_strp_check_rcv_hdr(strp);
    if (header_err) {
        tls_strp_abort_strp(strp, header_err);  // ADDED: aborts stream
        return header_err;                       // no further copies possible
    }
    tls_strp_msg_load(strp);
    return 0;
}

// tls_strp_abort_strp() sets strp->stopped = 1, which causes
// subsequent calls to tls_strp_read_sock() to return immediately:

static void tls_strp_abort_strp(struct tls_strparser *strp, int err)
{
    if (strp->stopped)
        return;
    strp->stopped = 1;
    /* Wake up any waiting recvmsg — returns error to userspace */
    strp->sk->sk_err = -err;
    sk_error_report(strp->sk);
}

The fix is minimal and surgical: a single tls_strp_abort_strp() call gates the retry path. Once strp->stopped is set, tcp_read_sock() is never called again for this stream, so no further data can accumulate in the over-allocated SKB.

Detection and Indicators

Runtime indicators of active exploitation:

  • Kernel SLUB/KASAN reports: BUG: KASAN: slab-out-of-bounds in tls_strp_copyin on kernels with sanitizers enabled.
  • Anomalous TLS OOB traffic: TCP urgent pointer set on a TLS connection; multiple 1–2 byte sends preceding a large payload burst on the same connection.
  • core_pattern mutation: /proc/sys/kernel/core_pattern changed to a pipe command or unexpected path — a post-exploitation artifact of the PoC's privilege escalation path.
  • Kernel crash / skb_over_panic: panic string skb_over_panic: text:ffffffff... len:N put:M head:... data:... tail:... end:... dev:<NULL> in dmesg indicates the overflow was detected before exploitation succeeded.

// Indicative dmesg on unpatched kernel hitting the panic path:
[  142.887431] skb_over_panic: text:ffffffff81a3c290 len:1152 put:128
               head:ffff888107b40000 data:ffff888107b40054
               tail:0x4c0 end:0x440 dev:
[  142.887441] ------------[ cut here ]------------
[  142.887442] kernel BUG at net/core/skbuff.c:192!

Remediation

  • Patch immediately: Apply the upstream fix to net/tls/tls_strp.c adding tls_strp_abort_strp() on header validation failure. Verify with git log --oneline net/tls/ that the fixing commit is present.
  • Kernel config mitigations: Build with CONFIG_KASAN=y and CONFIG_KASAN_INLINE=y on staging/CI systems to catch out-of-bounds writes early.
  • Network-level filtering: Drop or rate-limit TCP urgent/OOB segments at the border (iptables -m u32 or equivalent) if not required by any application.
  • Disable in-kernel TLS if unused: CONFIG_TLS=n or unload the tls module (rmmod tls) if userspace TLS termination (e.g., OpenSSL) is used instead.
  • Monitor core_pattern: Alert on unexpected writes to /proc/sys/kernel/core_pattern via auditd rule: -w /proc/sys/kernel/core_pattern -p w -k tls_exploit_ioc.
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 →