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
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 withskb_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:

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:
The important helper was
rxgk_decrypt_skb():
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:

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_sserver key.
- A plain UDP sender that hand-crafts RXRPC wire packets.
The malicious RESPONSE body is assembled through pipe-backed pages:

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:

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:

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:
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:
Solving for an arbitrary desired plaintext block:

The last block has to account for RFC 3962 CTS-CS3 swapping. For the final logical block:

So the attacker computes:

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:

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_skeys 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; withoutRXRPC_CHARGE_ACCEPT, the kernel rejects the incoming call as busy.
- Set
RXRPC_CLIENT_INITIATEDon 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:
aa54b1d27fe0 extended RXRPC’s DATA/RESPONSE checks to copy packets before in-place decrypt when shared fragments are present:
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:

RxGK then decrypts token and authenticator data from that flat buffer:

The old skb scatterlist decrypt helper is removed from the RESPONSE path:

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:

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_sserver 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:

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:
aa54b1d27fe0authored by Hyunwoo Kim.
- May 10, 2026:
aa54b1d27fe0committed to Linus’ tree, extending RXRPC DATA/RESPONSE unsharing toskb_has_frag_list()andskb_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
- Kernel Source Files
net/rxrpc/rxgk.cnet/rxrpc/rxgk_app.cnet/rxrpc/rxgk_common.hinclude/linux/skbuff.h
- DirtyFrag: https://github.com/V4bel/dirtyfrag