home intel cve-2026-43500-rxrpc-shared-frag-rce
CVE Analysis 2026-05-11 · 9 min read

CVE-2026-43500: rxrpc Shared Fragment In-Place Decryption RCE

Linux rxrpc fails to unshare skbs with externally-owned paged fragments before AEAD decryption, enabling in-place crypto corruption via splice-loopback. CVSS 7.8.

#rxrpc#packet-handling#memory-corruption#kernel-vulnerability#udp-socket
Technical mode — for security professionals
▶ Attack flow — CVE-2026-43500 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-43500Linux · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-43500 is a kernel memory corruption vulnerability in the Linux rxrpc subsystem. The DATA-packet handler (rxrpc_input_call_event()) and RESPONSE handler (rxrpc_verify_response()) gate their skb-unshare logic exclusively on skb_cloned(). An skb that is not cloned but carries externally-owned paged fragments — set via SKBFL_SHARED_FRAG through splice()/vmsplice() into a UDP socket, or via a chained skb_frag_list — bypasses this gate entirely and falls through to the in-place decryption path. That path feeds the frag pages directly into the AEAD/skcipher scatter-gather list via skb_to_sgvec(), performing cryptographic operations on pages the kernel does not exclusively own.

Root cause: The skb unshare guard in rxrpc's crypto path checks only skb_cloned(), missing the skb_has_frag_list() and skb_has_shared_frag() conditions that also indicate externally-owned pages, allowing in-place AEAD decryption to corrupt userspace-accessible memory.

Affected Component

  • Subsystem: net/rxrpc — AF_RXRPC socket family, rxkad security module
  • Functions: rxrpc_input_call_event(), rxrpc_verify_response(), rxkad_secure_packet()
  • Crypto path: crypto_pcbc_encryptskcipher_walk_doneflush_dcache_page
  • Trigger vector: splice() or vmsplice() into a UDP socket feeding rxrpc, causing __ip_append_data() to set SKBFL_SHARED_FRAG
  • Confirmed affected: Linux kernel through 6.17.x; tested on linux-aws 6.17.0-1013-aws (Ubuntu 24.04.4 LTS, AWS Graviton arm64)

Root Cause Analysis

The vulnerable gate in both handlers follows the same pattern. Here is the DATA handler path in rxrpc_input_call_event() as it existed before the fix:

/* net/rxrpc/input.c — rxrpc_input_call_event(), pre-patch */
static void rxrpc_input_call_event(struct rxrpc_call *call, struct sk_buff *skb)
{
    struct rxrpc_skb_priv *sp = rxrpc_skb(skb);

    if (sp->hdr.type == RXRPC_PACKET_TYPE_DATA) {
        /*
         * BUG: Only unshares when skb_cloned() is true.
         * An skb with SKBFL_SHARED_FRAG (splice path) or
         * a frag_list chain is NOT cloned — it passes this
         * gate and goes straight to in-place decryption.
         */
        if (skb_cloned(skb)) {                      // BUG: incomplete unshare guard
            skb = skb_unshare(skb, GFP_ATOMIC);
            if (!skb)
                return;
        }

        /* Falls through to security op with shared pages still attached */
        call->security->verify_packet(call, skb, /*...*/);
        /* -> rxkad_verify_packet()
         *    -> rxkad_verify_packet_1() / _2()
         *       -> skb_to_sgvec(skb, sg, ...)   // maps shared frag pages into SGL
         *       -> crypto_skcipher_decrypt(req)  // in-place decrypt onto shared pages
         */
    }
}

The same missing check appears in rxrpc_verify_response() for RESPONSE packets:

/* net/rxrpc/conn_service.c — rxrpc_verify_response(), pre-patch */
static int rxrpc_verify_response(struct rxrpc_connection *conn,
                                  struct sk_buff *skb)
{
    // BUG: skb_has_shared_frag() and skb_has_frag_list() not checked
    if (skb_cloned(skb)) {                          // BUG: incomplete guard
        skb = skb_unshare(skb, GFP_ATOMIC);
        if (!skb)
            return -ENOMEM;
    }

    return conn->security->verify_response(conn, skb, /*...*/);
    /* -> rxkad_verify_response()
     *    -> skb_to_sgvec() -> crypto_skcipher_decrypt()
     *       in-place on externally-owned pages
     */
}

The skb_to_sgvec() call builds a scatterlist directly from the skb's frag array. When those frags were injected via splice(), the backing pages are shared with a pipe buffer the attacker still holds a reference to. The AEAD/skcipher then decrypts — writes — onto those pages.

/* Relevant skb predicate macros — include/linux/skbuff.h */

static inline bool skb_cloned(const struct sk_buff *skb) {
    return skb->cloned &&
           (atomic_read(&skb_shinfo(skb)->dataref) & SKB_DATAREF_MASK) != 1;
}

/* These were NOT in the gate — the missing conditions: */
static inline bool skb_has_frag_list(const struct sk_buff *skb) {
    return skb_shinfo(skb)->frag_list != NULL;
}

static inline bool skb_has_shared_frag(const struct sk_buff *skb) {
    return skb_shinfo(skb)->flags & SKBFL_SHARED_FRAG;
}

Exploitation Mechanics

The upstream PoC (dirtyfrag) and its arm64 port (dirtyfrag-arm64) demonstrate two exploitation vectors. On x86_64 both the ESP/xfrm path and the rxrpc path are viable for privilege escalation. On arm64, flush_dcache_page() is non-trivial and the rxrpc path kernel-oopses (see Architecture Delta below).

EXPLOIT CHAIN (x86_64, rxrpc path — corrupts /etc/passwd):

1. Attacker opens AF_RXRPC socket bound over UDP; configures rxkad security level
   RXRPC_SECURITY_ENCRYPT on a loopback connection.

2. Construct a pipe with vmsplice(): map a page containing crafted plaintext,
   splice it into the UDP socket's send queue. __ip_append_data() sets
   SKBFL_SHARED_FRAG on the resulting skb — attacker retains pipe-buffer ref.

3. Kernel receives the DATA packet on the rxrpc loopback path.
   rxrpc_input_call_event() evaluates:
     skb_cloned(skb) == false  (refcount == 1, not cloned)
     → unshare gate NOT taken
     → skb with SKBFL_SHARED_FRAG pages proceeds to crypto

4. rxkad_verify_packet() calls skb_to_sgvec(skb, sg, offset, len).
   The scatterlist entries point directly to attacker-spliced pages.

5. crypto_skcipher_decrypt(req) performs PCBC decryption IN PLACE on those
   shared pages. Attacker's pipe buffer now contains the decrypted (modified)
   page data — effective write primitive to any page reachable via splice.

6. Target the pipe buffer at a page backing /etc/passwd or /usr/bin/su
   in the page cache (dirty-page-cache technique). The in-place decrypt
   writes attacker-controlled plaintext to the file's page cache entry.

7. msync() / fsync() flushes the corrupted cache page to disk.
   /etc/passwd now contains a root-equivalent entry. LPE achieved.

Memory Layout

SKB STRUCTURE — SHARED FRAG CASE (splice path):

sk_buff:
  +0x00  data          → linear head (normal)
  +0x?? skb_shinfo ptr → skb_shared_info below

skb_shared_info:
  /* +0x00 */ __u8     nr_frags      = 1
  /* +0x01 */ __u8     flags         = SKBFL_SHARED_FRAG (0x01)  ← SET by splice
  /* +0x08 */ skb_frag_t frags[1]:
                page   → struct page* (pipe_buffer.page — attacker holds ref)
                offset = 0x0
                size   = 0x1000
  /* +0x??*/ struct sk_buff *frag_list = NULL

skb_cloned() evaluation:
  skb->cloned = 0                     → skb_cloned() returns FALSE
  skb_has_shared_frag() = TRUE        ← NOT checked pre-patch
  skb_has_frag_list()   = FALSE

AFTER skb_to_sgvec() builds scatterlist:
  sg[0].page_link → pipe_buffer.page  ← attacker still has fd to pipe
  sg[0].offset    = 0
  sg[0].length    = 0x1000

AFTER crypto_skcipher_decrypt():
  pipe_buffer.page contents OVERWRITTEN with PCBC-decrypted data
  If page_cache_get_page(inode, index) returns same page → file corrupted
arm64 CRASH PATH (rxrpc path — oops, not exploit):

pc : flush_dcache_page+0x18/0x58
lr : skcipher_walk_done+0xbc/0x260
Call trace:
 skcipher_walk_done+0xbc/0x260
 crypto_pcbc_encrypt+0xe8/0x1c8 [pcbc]
 crypto_skcipher_encrypt+0x48/0xb8
 rxkad_secure_packet+0x108/0x270 [rxrpc]
 rxrpc_send_data+0x264/0x550 [rxrpc]
 ...

Cause: flush_dcache_page() on arm64 dereferences struct page* metadata
for real dcache maintenance. When page refcount has been manipulated
through the splice/vmsplice chain, the page struct may be in an
inconsistent state → translation fault → oops.

On x86_64: flush_dcache_page() is a no-op (hardware-coherent caches).
On arm64:  flush_dcache_page() = real dcache line flush via page PFN.

Patch Analysis

The fix extends the unshare guard to cover all three sharing conditions. Both rxrpc_input_call_event() and rxrpc_verify_response() receive identical treatment:

/* BEFORE (vulnerable) — net/rxrpc/input.c and net/rxrpc/conn_service.c */
if (skb_cloned(skb)) {
    skb = skb_unshare(skb, GFP_ATOMIC);
    if (!skb)
        return;  /* or return -ENOMEM */
}


/* AFTER (patched) — extends gate to all externally-owned-page conditions */
if (skb_cloned(skb) ||
    skb_has_frag_list(skb) ||           // catches chained frag_list skbs
    skb_has_shared_frag(skb)) {         // catches splice SKBFL_SHARED_FRAG path
    skb = skb_unshare(skb, GFP_ATOMIC);
    if (!skb)
        return;  /* or return -ENOMEM */
}
/* skb is now a fully linearized, exclusively-owned copy.
 * skb_to_sgvec() maps kernel-owned pages only.
 * In-place AEAD decrypt is safe.
 */

skb_unshare() internally calls skb_copy() which allocates a new skb with a fresh linear data area, copies all fragment data into it, and drops the reference to the shared pages. The original externally-owned pages are untouched by the subsequent crypto operation.

Detection and Indicators

There is no in-kernel audit trail for the vulnerable path by default. Relevant detection signals:

  • Kernel oops on arm64 with flush_dcache_page in the trace and rxkad_secure_packet / rxrpc_send_data above it — indicates attempted rxrpc exploit path (oopses rather than escalates).
  • Unexpected page-cache modifications to /etc/passwd or setuid binaries detectable via inotify/fanotify on the affected inode.
  • splice()/vmsplice() calls into UDP sockets combined with AF_RXRPC socket activity from an unprivileged process — correlate via execve/socket/splice syscall audit records.
  • LKRG / kernel integrity monitors will flag unexpected writes to read-only page cache entries if configured for inode integrity checking.
AUDIT SYSCALL PATTERN (splice-loopback vector):
  socket(AF_RXRPC, SOCK_DGRAM, ...)         ← rxrpc socket
  socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)  ← underlying UDP
  pipe2(pipefd, ...)
  vmsplice(pipefd[1], iov, ...)             ← inject attacker page
  splice(pipefd[0], udp_fd, ...)            ← sets SKBFL_SHARED_FRAG
  sendmsg(rxrpc_fd, ...)                    ← triggers vuln path

Remediation

  • Patch: Apply the upstream kernel fix that extends the unshare gate in both rxrpc_input_call_event() and rxrpc_verify_response() to include skb_has_frag_list() and skb_has_shared_frag(). Verify with grep -n "skb_has_shared_frag\|skb_has_frag_list" net/rxrpc/ post-patch.
  • Mitigation (no patch available): Disable AF_RXRPC if not required: install rxrpc /bin/false in /etc/modprobe.d/. This prevents socket creation entirely.
  • arm64 note: The rxrpc path oopses rather than escalates on arm64, but the oops itself is a denial-of-service. The ESP/xfrm path (CVE-2026-43284, also addressed by dirtyfrag) requires user namespace with uid_map write capability — restrict via kernel.unprivileged_userns_clone=0 where applicable to neutralize that vector.
  • AppArmor/seccomp: Restrict splice and vmsplice syscalls for untrusted processes; block AF_RXRPC socket creation in container profiles.
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 →