Bali, Indonesia
Insights

Dissecting DirtyDecrypt: Why In-Kernel RxGK Decryption Broke SKB Bounds in Linux 6.18

October 15, 2026
0:00
When network protocols are terminated directly within kernel space, memory management errors cease to be simple application-level crashes and become direct vectors for ring-0 compromise. The Linux kernel's implementation of the RxRPC protocol suite (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.
RxRPC divides network interaction into structured calls multiplexed over UDP (typically port 7001). Security negotiation occurs in-band via specialized security packets. When a client initiates a secure connection, the server challenges the client, and the client responds with an authentication token (an RxGK ticket and authenticator):
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 keys
Under AF_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)
Because authentication validation happens directly inside the kernel's network processing path, any structural assumption violated by the incoming packet must be caught before cryptographic transforms are executed.
The vulnerability exists in the transition between validating the envelope of an RxRPC response packet and passing its encrypted segments to the crypto subsystem. In 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;
}
The function faithfully validates that the 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.
To understand how an unchecked integer length translates to ring-0 memory corruption, we must trace how Linux network buffers (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 |
                  +---------------------------------------+
When 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;
}
If the attacker supplies an 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.
  1. Slab Cache Corruption (kmalloc-512 / kmalloc-1024): The cipher engine writes decrypted plaintext sequentially across the memory space immediately following dst_buf. This overwrites slab management freelist pointers (such as the freelist pointer in SLUB) or adjacent objects.
  2. skb_shared_info Overwrite: In scenarios where decryption is configured in-place within the sk_buff linear area, the write operation exceeds skb->end. Immediately following skb->end sits struct skb_shared_info. An attacker can overwrite destructor_arg, which contains a callback executed when the SKB is recycled via kfree_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;
};
By overwriting callback with a target kernel address and ensuring the SKB is freed, execution flow pivots directly to arbitrary ring-0 control.
To observe this corruption empirically, we can inspect a system running an unpatched kernel (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.
When the unvalidated 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>
The call trace confirms that rxgk_verify_response() allocated and freed an object whose redzone was overwritten with decrypted attacker data (represented above by the byte pattern 0x41).
To pinpoint this behavior in production or lab testing without enabling full 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]);
}
Executing this trace during an exploit attempt yields immediate visibility into the parameter divergence:
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 
The packet contains only 384 bytes of total data, yet the crypto engine is commanded to decrypt 1024 bytes—conclusive proof of an out-of-bounds scatter-gather operation.
Patching production kernels requires maintenance windows, reboot coordination, or live-patching infrastructure (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 ]
The following C code implements the boundary validation check:
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";
To build and load the mitigation without downtime on a running system:
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 ingress
TEXT
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 jited

The permanent fix released in Linux kernel stable branches (6.18.23 and backported trees) addresses both the bounds checking logic in rxgk_verify_response() and the defensive sizing inside rxgk_decrypt_skb().
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;
Key changes implemented in the fix:
  1. check_add_overflow(): Eliminates integer wrap-around possibilities where a massive auth_len could wrap offset + ticket_len + auth_len back to a small value, passing simple inequality tests.
  2. RXGK_MAX_AUTH_SIZE Enforcement: Clamps input validation against architectural maximums before allocating heap memory or touching the scatterlist API.
  3. Sanity Verification on Cryptographic Sizing: Upstream added assertions inside rxgk_decrypt_skb() to verify that len <= skb->len - offset immediately before calling crypto_skcipher_decrypt().

If you maintain infrastructure hosting Linux kernels within the vulnerable version window (6.16.1 to 6.18.22), apply the following hardening measures immediately.
Most modern enterprise servers do not use in-kernel AFS. The most effective mitigation is blacklisting the kernel module to prevent dynamic autoloading by unprivileged users:
BASH
# /etc/modprobe.d/rxrpc-blacklist.conf
install rxrpc /bin/true
install af_rxrpc /bin/true
blacklist rxrpc
blacklist af_rxrpc
Verify that the module cannot be loaded by an unprivileged user creating a socket:
BASH
# Attempt to force socket creation
python3 -c 'import socket; socket.socket(socket.AF_RXRPC, socket.SOCK_DGRAM, socket.IPPROTO_IP)'
Expected output if blacklisting is effective:
TEXT
OSError: [Errno 93] Protocol not supported
Many kernel network vulnerabilities (including those in 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 = FZPU
Apply the sysctl changes immediately:
BASH
sysctl -p /etc/sysctl.d/99-hardened-namespaces.conf
Verification VectorClean / Hardened StateVulnerable State
Module Loadmodprobe rxrpc fails with /bin/trueModule loads silently into kernel
Socket Creationsocket(AF_RXRPC, ...) returns EPROTONOSUPPORTSocket handle returned successfully
Namespace Isolationunshare -U -n fails with EPERMEnters new network namespace cleanly
TC Filter AuditIngress drops malformed auth_len > 512Malformed packets hit rxrpc_input_packet
Kernel Version>= 6.18.23 or backported vendor tree6.16.1 through 6.18.22 unpatched

The DirtyDecrypt vulnerability exposes a classic architectural trap: coupling cryptographic state transitions directly to the softirq network ingress path without strict isolation. When protocols like TLS (kTLS) or RxRPC (RxGK) push decryption into the kernel, they bypass the fault isolation that user-space services inherently possess. A buffer overrun in user space terminates a single worker process; a buffer overrun in the 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.