AF_RXRPC)—historically deployed to support the Andrew File System (AFS)—embodies this architectural hazard. While RxRPC has long relied on legacy security classes such as rxkad, modern Linux kernels (up to 6.18) introduced and expanded support for RxGK (Kerberos-based GSS-API negotiation for RxRPC).
In early 2026, researchers dissected a vulnerability dubbed DirtyDecrypt (tracked as CVE-2026-31635). The core vulnerability is deceptively simple: a missing boundary check in rxgk_verify_response() allowed an oversized response packet to bypass validation, feeding an unconstrained length descriptor into the kernel's cryptographic scatter-gather routines (rxgk_decrypt_skb()). What followed was not a graceful drop of a malformed packet, but a deterministic heap corruption where the kernel crypto API wrote decrypted plaintext directly over adjacent slab allocations.
To understand why this vulnerability allows local privilege escalation (LPE) and remote denial of service, we must dissect the packet life cycle, how socket buffers (struct sk_buff) interface with the kernel crypto scatterlist, and how eBPF can enforce ingress boundaries long before buggy legacy subsystems touch untrusted network bytes.
The RxGK Authentication Lifecycle and the In-Kernel Boundary
TEXT
| RxRPC / RxGK Handshake Lifecycle |
+--------------------------------------------------------------------------------+
Client (Initiator) Kernel Server (Target)
| |
| 1. Client initiates call (Type: DATA / ACK) |
|------------------------------------------------->| Allocates rxrpc_call
| | State: RXRPC_CALL_SERVER_ACCEPTING
| |
| 2. Server issues challenge (Type: CHALLENGE) |
|<-------------------------------------------------|
| |
| 3. Client returns auth (Type: RESPONSE) |
| [RxGK Ticket + Authenticator + Nonce] |
|------------------------------------------------->| rxgk_verify_response()
| | * Length sanity checks
| | * Decrypts authenticator
| | * Sets up session keysAF_RXRPC, the packet handling pipeline processes these control frames in the softirq or workqueue context (rxrpc_input_packet()). If the security index indicates RxGK (Security Index 6), execution transfers to the RxGK handler:
TEXT
[ NIC Ingress ]
|
v
[ napi_gro_receive() ]
|
v
[ rxrpc_input_packet() ]
|
+-> [ Type == RXRPC_PACKET_TYPE_CHALLENGE ] --> rxrpc_input_challenge()
|
+-> [ Type == RXRPC_PACKET_TYPE_RESPONSE ] --> rxrpc_input_response()
|
v
[ rxgk_verify_response() ]
|
v
[ rxgk_decrypt_skb() ]
|
(Scatterlist Decryption)The Anomaly: Flawed Arithmetic in rxgk_verify_response
net/rxrpc/rxgk.c, rxgk_verify_response() unpacks the response frame. The frame must contain an authentication header, an encrypted ticket, and an encrypted authenticator block. Let us examine the vulnerability pattern inside the kernel's unpack logic:
C
/* Simplified representation of the vulnerability in net/rxrpc/rxgk.c */
static int rxgk_verify_response(struct rxrpc_connection *conn, struct sk_buff *skb)
{
struct rxgk_response_hdr hdr;
size_t offset = sizeof(struct rxrpc_wire_header);
u32 ticket_len, auth_len;
int ret;
/* Copy out the response header containing segment offsets */
if (skb_copy_bits(skb, offset, &hdr, sizeof(hdr)) < 0)
return -EPROTO;
offset += sizeof(hdr);
ticket_len = ntohl(hdr.ticket_len);
auth_len = ntohl(hdr.auth_len);
/*
* FLAW: The code verified that ticket_len did not exceed the packet,
* but failed to properly enforce the maximum bounds on auth_len relative
* to the SKB linear data buffer size (skb_headlen).
*/
if (offset + ticket_len > skb->len)
return -EBADMSG;
offset += ticket_len;
/*
* Missing check: offset + auth_len > skb->len
* Or worse: integer wrap-around when computing the total decryption target.
*/
/* Kernel proceeds directly to allocate decrypt workspace based on auth_len */
ret = rxgk_decrypt_skb(conn->cipher_tfm, skb, offset, auth_len);
return ret;
}ticket_len does not exceed the packet length skb->len. However, it calculates the subsequent auth_len without validating that offset + auth_len <= skb->len. Even more critical: the kernel initializes a cryptographic scatterlist pointing into an internal heap buffer sized according to an expected fixed authenticator struct, but uses the untrusted, unverified auth_len as the decryption length parameter.
The Memory Corruption Mechanics: Shattering the sk_buff
struct sk_buff) interface with the Crypto API.
TEXT
+---------------------------------------------------------------------------------+
| SKB Layout and Out-of-Bounds Decryption Target |
+---------------------------------------------------------------------------------+
kmalloc Slab Allocation (Head buffer)
+--------------------+-------------------------+--------------------+---------+
| struct sk_buff | Linear Packet Data | skb_shared_info | Unused/ |
| metadata | (RxRPC Headers + Auth) | (shinfo) | Next |
+--------------------+-------------------------+--------------------+ Slab |
| head | data tail | end | Object |
+--------------------+-------------------------+--------------------+---------+
| |
+-- auth_len -+
|
v
[ crypto_skcipher_decrypt() ]
|
| Overwrites beyond skb->end
v
+---------------------------------------+
| Corrupted: shinfo->destructor_arg |
| Corrupted: shinfo->nr_frags |
| Corrupted: Adjacent kmalloc-1024 slab |
+---------------------------------------+rxgk_decrypt_skb() is called, it constructs a scatterlist (struct scatterlist sg) to decrypt the ciphertext in-place or into a destination buffer. The kernel uses the symmetric cipher API (crypto_sync_skcipher or crypto_skcipher):
C
static int rxgk_decrypt_skb(struct crypto_sync_skcipher *tfm,
struct sk_buff *skb,
unsigned int offset,
unsigned int len)
{
SYNC_SKCIPHER_REQUEST_ON_STACK(req, tfm);
struct scatterlist sg[2];
struct page *page;
void *dst_buf;
int ret;
/* Allocation sized for expected normal authenticator (e.g., 256 bytes) */
dst_buf = kmalloc(RXGK_MAX_AUTH_SIZE, GFP_ATOMIC);
if (!dst_buf)
return -ENOMEM;
/* sg is initialized with the allocated destination buffer */
sg_init_one(&sg[0], dst_buf, len); /* BUG: len is untrusted auth_len! */
skcipher_request_set_sync_tfm(req, tfm);
skcipher_request_set_crypt(req, &sg[0], &sg[0], len, iv);
/* In-place or source-to-destination decryption */
ret = crypto_skcipher_decrypt(req);
/* ... free request and copy back ... */
kfree(dst_buf);
return ret;
}auth_len of 1024 bytes, but dst_buf was allocated from the kmalloc-256 or kmalloc-512 slab via kmalloc(RXGK_MAX_AUTH_SIZE, ...), the cipher engine will decrypt 1024 bytes directly into a buffer that can only accommodate 256 bytes.
The Direct Consequences
- Slab Cache Corruption (
kmalloc-512/kmalloc-1024): The cipher engine writes decrypted plaintext sequentially across the memory space immediately followingdst_buf. This overwrites slab management freelist pointers (such as thefreelistpointer in SLUB) or adjacent objects. skb_shared_infoOverwrite: In scenarios where decryption is configured in-place within thesk_bufflinear area, the write operation exceedsskb->end. Immediately followingskb->endsitsstruct skb_shared_info. An attacker can overwritedestructor_arg, which contains a callback executed when the SKB is recycled viakfree_skb():
C
struct skb_ubuf_info {
void (*callback)(struct skb_ubuf_info *, bool zerocopy_success);
union {
struct { unsigned long desc; void *ctx; };
/* ... */
};
refcount_t refcnt;
};callback with a target kernel address and ensuring the SKB is freed, execution flow pivots directly to arbitrary ring-0 control.
Reconstructing the Failure in the Lab
v6.18.0) under test conditions. We simulate an anomalous RxRPC client response using a raw UDP socket targeting the kernel's RxRPC listener on port 7001.
Kernel Log Telemetry (SLUB Poisoning Failure)
auth_len triggers an overrun in the kmalloc-512 slab, Linux's SLUB debug facilities immediately flag corrupted redzones or invalid freelist pointers upon the subsequent kfree():
TEXT
[ 128.491024] =============================================================================
[ 128.491031] BUG kmalloc-512 (Not tainted): Redzone overwritten
[ 128.491034] -----------------------------------------------------------------------------
[ 128.491038] 0xffff8881048b2300-0xffff8881048b23ff @offset=0x300. Bottom Redzone corrupted
[ 128.491043] Padding corrupted: 0xffff8881048b2380-0xffff8881048b23ff
[ 128.491047] Data: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 ...
[ 128.491055] Fix: Poison overwritten redzone, restore slab integrity
[ 128.491058] CPU: 3 PID: 421 Comm: kworker/u8:2 Not tainted 6.18.0-rc1-dirty #4
[ 128.491062] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2
[ 128.491066] Workqueue: rxrpc_call rxrpc_process_call [rxrpc]
[ 128.491078] Call Trace:
[ 128.491081] <TASK>
[ 128.491085] dump_stack_lvl+0x48/0x70
[ 128.491092] check_bytes_and_report+0xd8/0x110
[ 128.491098] check_object+0x18a/0x280
[ 128.491104] free_to_partial_list+0x9f/0x450
[ 128.491111] ? rxgk_verify_response+0x24a/0x380 [rxrpc]
[ 128.491122] kfree+0x214/0x320
[ 128.491128] rxgk_verify_response+0x24a/0x380 [rxrpc]
[ 128.491138] rxrpc_input_response+0x8f/0x1c0 [rxrpc]
[ 128.491148] rxrpc_input_packet+0x310/0x940 [rxrpc]
[ 128.491158] rxrpc_io_thread+0x180/0x4a0 [rxrpc]
[ 128.491167] kthread+0xe5/0x120
[ 128.491173] ret_from_fork+0x2d/0x50
[ 128.491179] ret_from_fork_asm+0x11/0x20
[ 128.491185] </TASK>rxgk_verify_response() allocated and freed an object whose redzone was overwritten with decrypted attacker data (represented above by the byte pattern 0x41).
Tracing with bpftrace
slub_debug (which introduces severe performance overhead), we can instrument rxgk_verify_response and the cipher execution entry points with bpftrace.
The following script hooks into the RxRPC input subsystem, measuring the relationship between the packet length in the sk_buff and the argument passed to the skcipher API:
TEXT
#!/usr/bin/env bpftrace
#include <linux/skbuff.h>
#include <linux/net.h>
kprobe:rxgk_verify_response
{
$skb = (struct sk_buff *)arg1;
@skb_len[tid] = $skb->len;
@skb_data_len[tid] = $skb->data_len;
}
kprobe:crypto_skcipher_decrypt
{
if (@skb_len[tid]) {
$req = (struct skcipher_request *)arg0;
cryptlen =req->cryptlen;
/*
* If cryptlen exceeds the original skb length or standard auth limits,
* flag the event immediately.
*/
if (cryptlen > @skb_len[tid] ||cryptlen > 512) {
printf("CRITICAL: Anomalous Decryption Length Detected!
");
printf(" TID: %-6d | Comm: %-16s
", tid, comm);
printf(" SKB Total Len: %-5d | SKB Data Len: %-5d
",
@skb_len[tid], @skb_data_len[tid]);
printf(" Cipher Target Len: %-5u
", $cryptlen);
}
}
}
kretprobe:rxgk_verify_response
{
delete(@skb_len[tid]);
delete(@skb_data_len[tid]);
}Running the Probe
BASH
# bpftrace rxgk_trace.bt
Attaching 3 probes...
CRITICAL: Anomalous Decryption Length Detected!
TID: 421 | Comm: kworker/u8:2
SKB Total Len: 384 | SKB Data Len: 0
Cipher Target Len: 1024 Structural Ingress Defense with eBPF (TC Layer)
kpatch/livepatch). When an LPE or remote memory corruption vulnerability exists in a kernel-space protocol, we can eliminate the attack surface at the ingress boundary using an eBPF Traffic Control (TC) filter.
Because RxRPC operates over UDP (standard port 7001), an eBPF program attached to the clsact ingress qdisc can inspect incoming packets, parse the RxRPC header, and drop malformed RxGK RESPONSE packets before they are ever queued to rxrpc_input_packet().
TEXT
[ Physical NIC ]
|
v
[ TC Ingress Hook (tc-bpf) ] <--- Intercepts UDP 7001 here!
| Checks: Type == RESPONSE (3)
| Checks: SecurityIndex == 6 (RxGK)
| Validates: auth_len <= packet_len
|
+-- (Malformed) ---------> [ BPF_DROP ] (Silent Drop / Counter)
|
+-- (Legitimate) --------> [ TC_ACT_OK ] --> [ AF_RXRPC Subsystem ]Production eBPF Mitigation Filter
C
#include <vmlinux.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#define ETH_P_IP 0x0800
#define IPPROTO_UDP 17
#define RXRPC_PORT 7001
#define RXRPC_PACKET_TYPE_RESPONSE 3
#define RXRPC_SECURITY_RXGK 6
/* RxRPC Wire Header */
struct rxrpc_wire_header {
__be32 epoch;
__be32 cid;
__be32 callNumber;
__be32 seq;
__be32 serial;
__u8 type;
__u8 flags;
__u8 userStatus;
__u8 securityIndex;
__be16 spare;
__be16 serviceId;
} __attribute__((packed));
/* RxGK Response Header */
struct rxgk_response_hdr {
__be32 ticket_len;
__be32 auth_len;
} __attribute__((packed));
SEC("classifier/ingress")
int filter_rxgk_ingress(struct __sk_buff *skb)
{
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return 0; // TC_ACT_OK
if (eth->h_proto != bpf_htons(ETH_P_IP))
return 0;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return 0;
if (ip->protocol != IPPROTO_UDP)
return 0;
struct udphdr *udp = (void *)((char *)ip + (ip->ihl * 4));
if ((void *)(udp + 1) > data_end)
return 0;
/* Filter strictly for RxRPC target traffic */
if (udp->dest != bpf_htons(RXRPC_PORT))
return 0;
struct rxrpc_wire_header *rx = (void *)(udp + 1);
if ((void *)(rx + 1) > data_end)
return 0;
/* Check if this is an RxGK Response packet */
if (rx->type == RXRPC_PACKET_TYPE_RESPONSE &&
rx->securityIndex == RXRPC_SECURITY_RXGK) {
struct rxgk_response_hdr *rxgk = (void *)(rx + 1);
if ((void *)(rxgk + 1) > data_end) {
/* Truncated header: Drop immediately */
return 2; // TC_ACT_SHOT
}
__u32 ticket_len = bpf_ntohl(rxgk->ticket_len);
__u32 auth_len = bpf_ntohl(rxgk->auth_len);
/* Calculate available bytes remaining in UDP payload */
__u32 payload_len = bpf_ntohs(udp->len) - sizeof(struct udphdr);
__u32 consumed = sizeof(struct rxrpc_wire_header) + sizeof(struct rxgk_response_hdr);
/* Enforce absolute bounds checks */
if (consumed + ticket_len + auth_len > payload_len) {
bpf_printk("eBPF: Blocked oversized RxGK response! ticket: %u, auth: %u
",
ticket_len, auth_len);
return 2; // TC_ACT_SHOT
}
/* Authenticator size sanity cap: authenticators cannot exceed 512 bytes */
if (auth_len > 512) {
bpf_printk("eBPF: Blocked anomalous RxGK auth_len (%u bytes)
", auth_len);
return 2; // TC_ACT_SHOT
}
}
return 0; // TC_ACT_OK
}
char _license[] SEC("license") = "GPL";Compiling and Attaching to the Interface
BASH
# 1. Compile the eBPF filter into BPF bytecode
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 -c filter_rxgk.c -o filter_rxgk.o
# 2. Add clsact qdisc to the primary network interface (e.g., eth0)
tc qdisc add dev eth0 clsact 2>/dev/null || true
# 3. Attach the compiled filter to the ingress path
tc filter replace dev eth0 ingress prio 1 handle 1 bpf da obj filter_rxgk.o sec classifier/ingress
# 4. Verify the filter is active and inspecting frames
tc filter show dev eth0 ingressTEXT
filter protocol all pref 1 bpf chain 0
filter protocol all pref 1 bpf chain 0 handle 0x1 filter_rxgk.o:[classifier/ingress] direct-action not_in_hw id 189 tag a0c7932d4b9b9a11 jitedOfficial Upstream Remediation
6.18.23 and backported trees) addresses both the bounds checking logic in rxgk_verify_response() and the defensive sizing inside rxgk_decrypt_skb().
Upstream Patch Analysis
C
--- a/net/rxrpc/rxgk.c
+++ b/net/rxrpc/rxgk.c
@@ -342,6 +342,7 @@ static int rxgk_verify_response(struct rxrpc_connection *conn, struct sk_buff *skb)
struct rxgk_response_hdr hdr;
size_t offset = sizeof(struct rxrpc_wire_header);
u32 ticket_len, auth_len;
+ u32 total_len;
int ret;
if (skb_copy_bits(skb, offset, &hdr, sizeof(hdr)) < 0)
@@ -351,12 +352,22 @@ static int rxgk_verify_response(struct rxrpc_connection *conn, struct sk_buff *skb)
ticket_len = ntohl(hdr.ticket_len);
auth_len = ntohl(hdr.auth_len);
- if (offset + ticket_len > skb->len)
+ /* Check for arithmetic overflow */
+ if (check_add_overflow(ticket_len, auth_len, &total_len))
+ return -EBADMSG;
+
+ if (check_add_overflow(offset, total_len, &total_len))
+ return -EBADMSG;
+
+ if (total_len > skb->len)
+ return -EBADMSG;
+
+ /* Cap authenticator size to the maximum theoretical token size */
+ if (auth_len > RXGK_MAX_AUTH_SIZE)
return -EBADMSG;
offset += ticket_len;
ret = rxgk_decrypt_skb(conn->cipher_tfm, skb, offset, auth_len);
return ret;check_add_overflow(): Eliminates integer wrap-around possibilities where a massiveauth_lencould wrapoffset + ticket_len + auth_lenback to a small value, passing simple inequality tests.RXGK_MAX_AUTH_SIZEEnforcement: Clamps input validation against architectural maximums before allocating heap memory or touching the scatterlist API.- Sanity Verification on Cryptographic Sizing: Upstream added assertions inside
rxgk_decrypt_skb()to verify thatlen <= skb->len - offsetimmediately before callingcrypto_skcipher_decrypt().
Defense-in-Depth and Mitigation Strategy
6.16.1 to 6.18.22), apply the following hardening measures immediately.
1. Disable the RxRPC Module (If AFS is Not Required)
BASH
# /etc/modprobe.d/rxrpc-blacklist.conf
install rxrpc /bin/true
install af_rxrpc /bin/true
blacklist rxrpc
blacklist af_rxrpcBASH
# Attempt to force socket creation
python3 -c 'import socket; socket.socket(socket.AF_RXRPC, socket.SOCK_DGRAM, socket.IPPROTO_IP)'TEXT
OSError: [Errno 93] Protocol not supported2. Restrict Unprivileged Namespace Creation
AF_RXRPC) are reachable by unprivileged users through user namespaces (CLONE_NEWUSER / CLONE_NEWNET), which allow non-root users to bring up loopback interfaces and load network protocols.
Disable unprivileged user namespaces on production hosts where containers are managed by trusted daemons:
INI
# /etc/sysctl.d/99-hardened-namespaces.conf
# Disable unprivileged user namespace creation (Debian/Ubuntu)
kernel.unprivileged_userns_clone = 0
# Restrict unprivileged eBPF to prevent side-channel probing
kernel.unprivileged_bpf_disabled = 1
# Enable strict SLUB allocator freelist integrity checks
slub_debug = FZPUBASH
sysctl -p /etc/sysctl.d/99-hardened-namespaces.conf3. Verification and Audit Matrix
| Verification Vector | Clean / Hardened State | Vulnerable State |
|---|---|---|
| Module Load | modprobe rxrpc fails with /bin/true | Module loads silently into kernel |
| Socket Creation | socket(AF_RXRPC, ...) returns EPROTONOSUPPORT | Socket handle returned successfully |
| Namespace Isolation | unshare -U -n fails with EPERM | Enters new network namespace cleanly |
| TC Filter Audit | Ingress drops malformed auth_len > 512 | Malformed packets hit rxrpc_input_packet |
| Kernel Version | >= 6.18.23 or backported vendor tree | 6.16.1 through 6.18.22 unpatched |
Architectural Takeaways
sk_buff linear boundary corrupts the slab allocator, poisons kernel freelists, and provides a direct path to root execution.
Until critical systems can be fully updated to patched kernels, deploying eBPF at the TC ingress layer remains the most effective technique for mitigating in-kernel vulnerabilities: it enforces memory-safe boundary checks on untrusted network packets before they ever interact with complex, legacy kernel state machines.