Delphos LabsDelphos Labs

May 15, 2026

·
Security Research

DirtyCBC: When Linux Kernel Decrypt-Before-MAC Turns Authenticated Encryption Into a Page-Cache Write

Linux kernel page-cache poisoning via AES-256 chosen-plaintext on the RxGK RESPONSE path and why authenticated encryption did not stop it.

KL
Kamil Leoniak
Cover ImageCover Image
The interesting part is the cryptography. DirtyFrag abused the RxKAD path, where the legacy FCRYPT construction made brute force practical. DirtyCBC targets RxGK, which uses AES-256-CTS-HMAC-SHA1. Brute force is irrelevant. Exploitability comes from a chosen-plaintext construction over CBC decryption and from the fact that the kernel wrote decrypted bytes back into skb scatterlist pages before integrity verification failed.
Authenticated encryption does not help if unauthenticated plaintext is written into externally aliased kernel pages before authentication succeeds. DirtyCBC is the proof of that. It is a local page-cache poisoning primitive in Linux AF_RXRPC’s YFS-RxGK security path, and a sibling of DirtyFrag: both bugs come from the same structural mistake, in-place cryptographic transforms over skb-backed pages that may originate from attacker-controlled splice() / MSG_SPLICE_PAGES flows.
Upstream has already addressed the active vulnerability. Commit aa54b1d27fe0 was authored on May 8, 2026 and committed by Linus on May 10, 2026. It mitigated the known MSG_SPLICE_PAGES path by copying RXRPC DATA/RESPONSE packets when the skb is cloned, has a fraglist, or has SKBFL_SHARED_FRAG. David Howells’ May 14 v3 series goes further: RESPONSE verification extracts packet contents into a linear buffer before crypto, and DATA handling decrypts from a flat receive buffer. That later shape removes the risky invariant: received skb contents are no longer modified by RXRPC security verification.
This post documents the RxGK construction because it is a useful defensive pattern.

What Made the RxGK RESPONSE Path Exploitable?

The affected code path was the RxGK RESPONSE-packet token-decrypt path:
  • net/rxrpc/rxgk.c:rxgk_verify_response() parses the inbound RESPONSE.
  • net/rxrpc/rxgk_app.c:rxgk_extract_token() decrypts the encrypted token.
  • rxgk_decrypt_skb() maps the skb region with skb_to_sgvec().
  • crypto_krb5_decrypt() decrypts in place over that scatterlist.
If the skb scatterlist contains page-cache pages supplied through MSG_SPLICE_PAGES, decrypting the packet mutates those page-cache pages.
The end-to-end PoC poisoned the page cache of a readable SUID-root binary, then executed that binary. The on-disk file was unchanged. The cached first page was replaced with a tiny ELF that performed:
notion image
The result was a root shell from an unprivileged local user.
Inside rxgk_extract_token(), the kernel decoded the RXGK_TokenContainer, looked up the selected server key, derived the Kerberos encryption context, and decrypted the encrypted token in place:
notion image
The important helper was rxgk_decrypt_skb():
notion image
If any scatterlist entry aliases a page-cache page, decrypting the packet writes into the page cache.
That is the primitive. skb_to_sgvec() maps the selected skb byte range into a scatterlist, and crypto_krb5_decrypt() decrypts with source and destination pointing at the same scatterlist.

Why Authentication Did Not Save It?

The problem is the order of operations: decrypt first, verify MAC second. The Kerberos krb5enc construction used by RxGK wraps AES-CTS-CBC with HMAC-SHA1 integrity.
Conceptually:
notion image
For ordinary private memory, this is just a failed authentication. For skb fragments that alias page-cache pages, the failed authentication still leaves a write behind.
The exploit does not forge the HMAC. It intentionally lets verification fail after the write has happened.

Why Does the skb Contain Page-Cache Pages?

The attacker controls both ends of a local exchange:
  • An AF_RXRPC service socket bound on loopback with an attacker-chosen rxrpc_s server key.
  • A plain UDP sender that hand-crafts RXRPC wire packets.
The malicious RESPONSE body is assembled through pipe-backed pages:
notion image
On the affected path, the received skb’s paged fragments could preserve those references. Later, skb_to_sgvec() exposed the same interleaved pages to the crypto layer.
The resulting scatterlist looked like this:
notion image
The attacker controls both ends of a local exchange, so the received skb’s paged fragments can preserve references to page-cache pages supplied through MSG_SPLICE_PAGES.

How Does the AES-CBC Chosen-Plaintext Construction Work?

For CBC decryption, each 16-byte block obeys:
notion image
The attacker controls the RxGK server secret K, because the local service installs its own rxrpc_s key. From that, userspace derives the token encryption key:
notion image
For RxGK, RXGK_SERVER_ENC_TOKEN is usage value 1036.
For each target block placed at C[2i+1], the previous block C[2i] is attacker-controlled. The kernel writes this plaintext into the target page-cache block:
notion image
Solving for an arbitrary desired plaintext block:
notion image
The last block has to account for RFC 3962 CTS-CS3 swapping. For the final logical block:
notion image
So the attacker computes:
notion image
The attacker is not searching the keyspace. The attacker controls the key and uses CBC algebra to choose the plaintext written into the target page cache.
This is why AES-256 does not prevent exploitation here. The cost is small: derive Ke once, then perform one AES-256 block operation per chosen 16-byte block.

How Much Payload Can One RESPONSE Carry?

Upstream commonly uses MAX_SKB_FRAGS = 17. The exploit needs alternating attacker and target fragments, plus framing and authentication/tag material. In the demonstrated construction, one RESPONSE carried six (attacker, target) pairs, giving 96 bytes of chosen plaintext per RESPONSE.
One RESPONSE carried six attacker-target page pairs, giving 96 bytes of chosen plaintext. That was enough that two RESPONSE packets placed a complete ELF payload into the first page of a SUID-root binary's page cache.
The demonstration used two RESPONSE packets to place a 192-byte x86_64 ELF payload into the first page of a readable SUID-root binary’s page cache. The file on disk was unchanged; direct I/O still read the original file. Ordinary cached execution read the poisoned page.

How Does the End-to-End Exploit Work?

The PoC runs as an unprivileged local user:
notion image
The kernel decrypts the RESPONSE token in place, HMAC fails, the connection aborts, and the target page-cache blocks remain modified.
The key practical details were:
  • Do not use a private network namespace. rxrpc_s keys carry a network-domain tag, so the key must be visible to the RXRPC worker’s lookup context.
  • Charge the service accept pool. listen() alone is not enough; without RXRPC_CHARGE_ACCEPT, the kernel rejects the incoming call as busy.
  • Set RXRPC_CLIENT_INITIATED on the malicious RESPONSE. The I/O thread discards RESPONSE packets without it.
  • Drain the ACK before expecting the CHALLENGE. The kernel can send an ACK before the CHALLENGE.
  • Split the payload across two RESPONSE packets because of the skb fragment budget.

What Survives a Reboot?

DirtyCBC modifies page cache, not disk.
That matters operationally:
  • Package hashes and direct disk reads can still show the original file.
  • Cached reads and exec() can observe the poisoned page.
  • If a process keeps the poisoned binary mapped, the page can remain resident.
  • Reboot, inode invalidation, memory pressure, or dropping caches after all mappings are gone can clear the effect.
DirtyCBC changes what the kernel executes without changing what storage contains.
This is why the primitive is useful even though it is not an on-disk file write.

Why Did the May 10 Mitigation Help?

The normal IPv4 UDP MSG_SPLICE_PAGES path marks such skbs with SKBFL_SHARED_FRAG:
notion image
aa54b1d27fe0 extended RXRPC’s DATA/RESPONSE checks to copy packets before in-place decrypt when shared fragments are present:
notion image
The mitigation blocks the demonstrated path because userspace cannot set the internal MSG_NO_SHARED_FRAGS flag.

Why Is the Later Fix Better?

The May 10 mitigation catches the known unsafe skb shapes at RXRPC’s DATA/RESPONSE dispatch points. The more robust fix is to avoid decrypting received skb storage at all.
David Howells’ May 14 v3 series changes RESPONSE verification to copy the packet body into a kmalloc’d linear buffer before security verification:
notion image
RxGK then decrypts token and authenticator data from that flat buffer:
notion image
The old skb scatterlist decrypt helper is removed from the RESPONSE path:
notion image
The stronger security boundary is that RXRPC security verification no longer mutates received skb data while processing RESPONSE packets.
The same series also moves DATA handling toward a flat receive buffer. The invariant should be: unauthenticated network data must not be decrypted in place into storage that might be shared outside the protocol layer.

How Did Delphos Surface This Path?

Our starting point was not a signature for a specific vulnerable function. It was a semantic hypothesis derived from DirtyFrag: an unauthenticated network buffer becomes dangerous if it is mapped into a scatterlist and passed to an in-place cryptographic operation before authenticity is established.
We used an agentic analysis workflow to generate and rank candidate bug patterns across the RxRPC implementation, combining source-level reasoning with binary-side validation so that hypotheses stayed tied to executable behavior. The process was iterative: propose candidate invariants, test them against the implementation, discard low-signal paths, and escalate paths where the source and compiled evidence agreed.
One of the proposed invariants produced a successful hit:
notion image
The RxGK RESPONSE token path matched that shape. Manual review then confirmed two facts that made exploitation practical:
  • The local service scenario lets the attacker choose the rxrpc_s server secret and therefore derive the AES-256 token-encryption key.
  • AES-CBC decryption permits chosen plaintext writes when attacker-controlled blocks are interleaved before target ciphertext blocks.
The Delphos agentic analysis platform surfaced the RxGK RESPONSE token path as the highest-signal candidate.
The exploitability proof was then done manually: construct the SGL, derive Ke, solve the CBC equations, and verify that the page cache changes before HMAC failure aborts the connection.

Writeup and PoC

The full technical writeup and proof-of-concept are available in our GitHub repository: DirtyCBC writeup and PoC.
The repository contains the materials needed to reproduce and review the issue:
  • README.md: root cause, affected code path, cryptographic construction, fragment budget, persistence model, and end-to-end exploit walkthrough.
  • poc.c: single-file C proof-of-concept.
  • poc.py: Python proof-of-concept for environments without a compiler.
The PoCs are published for defensive validation and research. They demonstrate page-cache poisoning through the RxGK RESPONSE path and should be tested only on systems you own or are explicitly authorized to assess.

What Should Defenders Audit?

The defensive pattern is broader than RXRPC:
notion image
The bug is distributed across subsystems:
  • RXRPC owns packet routing and RESPONSE verification.
  • RxGK owns token/authenticator parsing.
  • Kerberos crypto owns decrypt-before-MAC behavior.
  • skb/splice owns page sharing and MSG_SPLICE_PAGES.
  • The page cache observes the write when skb frags alias file pages.
The audit question is semantic: can unauthenticated data cause an in-place write to storage that is not private to this protocol layer?
Unauthenticated network data must not be decrypted in place into storage that might be shared outside the protocol layer.
If yes, authenticated encryption may still leave a mutation primitive behind.

Timeline

  • May 8, 2026: aa54b1d27fe0 authored by Hyunwoo Kim.
  • May 10, 2026: aa54b1d27fe0 committed to Linus’ tree, extending RXRPC DATA/RESPONSE unsharing to skb_has_frag_list() and skb_has_shared_frag().
  • May 14, 2026: David Howells posted [PATCH net v3 0/4] rxrpc: Better fix for DATA/RESPONSE decrypt vs splice(), including a RESPONSE-side linear-buffer fix and DATA-side receive-buffer fix.
  • May 15, 2026: Kernel security coordination confirmed the issue was fixed and publication was acceptable.

References

  • Hyunwoo Kim, Commit aa54b1d27fe0: rxrpc: Also unshare DATA/RESPONSE packets when paged frags are present
  • David Howells, [PATCH net v3 0/4] rxrpc: Better fix for DATA/RESPONSE decrypt vs splice()
  • Kernel Source Files
    • net/rxrpc/rxgk.c
    • net/rxrpc/rxgk_app.c
    • net/rxrpc/rxgk_common.h
    • include/linux/skbuff.h
  • CopyFail: https://github.com/theori-io/copy-fail-CVE-2026-31431/
  • DirtyFrag: https://github.com/V4bel/dirtyfrag
  • Fragnesia: https://github.com/v12-security/pocs/tree/main/fragnesia

Company

About UsBlogCareersSecurity Trust CenterVulnerability Disclosure Policy

Account

Privacy PolicyTerms of Service

Help & Feedback

Contact SupportEmail Us

Social

LinkedInX

Copyright © 2026 Delphos Labs Inc.