
Blocking the DirtyFrag Linux LPE chain (CVE-2026-43284 / CVE-2026-43500) at runtime with a Cilium Tetragon TracingPolicy
Blocking the DirtyFrag Linux privilege-escalation chain (CVE-2026-43284 / CVE-2026-43500) at runtime with a Cilium Tetragon TracingPolicy. The policy SIGKILLs the public proof-of-concept at its socket-setup step, before it reaches the page-cache write that would give it root.
What this is. A lab writeup. I set up Tetragon for the first time and wanted to see whether it could actually stop a real, current kernel LPE. It could. This documents exactly what I ran, what fired, and, just as importantly, the limits of what it proves. It is not a substitute for patching.
Files: this README (self-contained, evidence inline) · block-dirtyfrag.yaml (the policy)
| Exploit | DirtyFrag - public PoC: V4bel/dirtyfrag |
| CVEs | CVE-2026-43284 (xfrm-ESP page-cache write), CVE-2026-43500 (RxRPC page-cache write) |
| Host | Ubuntu 24.04.4 LTS, Tetragon v1.7.0 (standalone, systemd) |
Baseline - no policy, kernel 6.8.0-88 (vulnerable) | PoC → uid=0(root) |
With policy - kernel 6.8.0-88 | PoC SIGKILLed at socket(AF_RXRPC); user stays uid=1000 |
Patched kernel 6.8.0-134 | PoC fails on its own (rc=4); socket hook still fires, detection, not mitigation (note) |
| Nature of control | Compensating control / virtual patch, not a kernel fix |
DirtyFrag is a local privilege-escalation chain built from two independent page-cache write primitives in the Linux kernel: one in the xfrm/ESP (IPsec) in-place decryption path (CVE-2026-43284), and one in the RxRPC path (CVE-2026-43500). Each lets an unprivileged local user write attacker-controlled bytes into read-only page-cache pages, for example the cached image of a setuid-root binary such as /bin/su - and from there obtain root. The write lands in memory only; the file on disk is never modified, so file-integrity monitoring sees nothing. It is the same bug class as Dirty Pipe and Copy Fail. The two CVEs are chained deliberately: if one path is unavailable in a given environment, the other still works.
On this host, unprivileged user namespaces are restricted by AppArmor (kernel.apparmor_restrict_unprivileged_userns = 1), which blocks the ESP half of the chain. That leaves the RxRPC path, which opens an AF_RXRPC socket (address family 33) - and that is the step this policy kills.
The real fix is a patched kernel. Everything here is a stopgap for a host you cannot patch yet. Ubuntu shipped DirtyFrag fixes some time before this test, so this is a known N-day, not a live 0-day - the point is to show what a runtime control can do on a host that is still, for whatever reason, running a vulnerable kernel.
Full policy: block-dirtyfrag.yaml. It installs two kprobes.
Hook 1 - grooming sockets (sys_socket). The load-bearing control. The exploit's RxRPC path must call socket(AF_RXRPC, …) on every run, whether or not any kernel module is already loaded. The policy matches on the address family:
AF_RXRPC) → Sigkill. On any host that is not an AFS client, effectively nothing legitimate opens an AF_RXRPC socket, so a blanket kill here is safe and high-confidence.AF_ALG) → Post (audit only, no kill). AF_ALG is the userspace kernel-crypto API and does have legitimate users (cryptsetup, libkcapi tooling, some FIPS workflows). Killing on family alone would cause false positives, so this leg only logs. The path to enforcement is: audit for a while, build a NotIn allowlist from the binaries actually observed, then consider promoting to Sigkill.Hook 2 - vulnerable module autoload (security_kernel_module_request). Defence in depth. Fires when the kernel is asked to auto-load a vulnerable module family (esp4, esp6, rxrpc, the net-pf-33/net-pf-38 socket aliases, the pcbc/fcrypt crypto templates). Limitation: it only fires when the module is not already resident — after the first exploit run in a boot, those modules are loaded and this hook goes silent. It's a cold-boot layer, not the primary control.
Together: Hook 1 catches the exploit whether modules are warm or cold; Hook 2 adds an earlier, more specific trip on a cold host.
6.8.0-88-generic, which is vulnerable (the baseline reaches root). The box also has 6.8.0-134-generic installed — the patched kernel — and this was confirmed directly: rebooting into -134 makes the PoC fail on its own (rc=4, no root), with or without the policy (see the note below). To reproduce the mitigation demo, boot 6.8.0-88 from GRUB (it's still installed) — on any patched kernel there is nothing to mitigate.AF_ALG leg.lockdown,capability,landlock,yama,apparmor — no bpf). So enforcement uses Sigkill from the kprobe, not an in-kernel LSM deny. The kill lands at the socket() syscall — before the write primitive — so it terminates the process at a mandatory early step rather than "blocking the vulnerability" itself.Steps 1–5 are all from the same 6.8.0-88 boot (the vulnerable kernel).
1. Environment - Ubuntu 24.04.4, kernel 6.8.0-88-generic, Tetragon v1.7.0 active under systemd, unprivileged userns restricted.
$ uname -r
6.8.0-88-generic
$ tetra version
CLI version: v1.7.0
$ systemctl is-active tetragon
active
2. Preconditions (policy OFF) - clean starting point: no vulnerable modules resident, no xfrm state, no policy loaded.
$ lsmod | grep -E 'esp4|esp6|rxrpc' || echo "(none loaded)"
(none loaded)
$ sudo ip xfrm state # (empty)
$ sudo tetra tp list
ID NAME STATE FILTERID NAMESPACE SENSORS KERNELMEMORY MODE NPOST NENFORCE NMONITOR
3. Baseline (policy OFF) - the exploit works. This is what proves the kernel is actually vulnerable; the whole writeup rests on it.
$ ./exp ; id
uid=0(root) gid=0(root) groups=0(root)
4. Load the policy.
$ sudo tetra tp add /etc/tetragon/tetragon.tp.d/block-dirtyfrag.yaml
tracing policy "…/block-dirtyfrag.yaml" added
5. Enforced (policy ON) - the kill. The exploit is terminated by signal at the socket() call and never reaches su:
🚀 process /home/…/dirtyfrag/exp
❓ syscall /home/…/dirtyfrag/exp __x64_sys_socket
Kernel:
0x0: __x64_sys_socket+0x5
0x0: do_syscall_64+0x7f
0x0: entry_SYSCALL_64_after_hwframe+0x78
💥 exit /home/…/dirtyfrag/exp SIGKILL
The structured event confirms both the match and the kill - a process_kprobe on the socket syscall, then a process_exit by signal for the same PID:
// process_kprobe — the AF_RXRPC match
{ "process_kprobe": {
"process": { "pid": 24083, "uid": 1000, "binary": ".../dirtyfrag/exp" },
"function_name": "__x64_sys_socket",
"args": [ { "int_arg": 33, "label": "family" } ],
"policy_name": "block-dirtyfrag",
"action": "KPROBE_ACTION_POST"
} }
// process_exit — same PID, killed by signal
{ "process_exit": {
"process": { "pid": 24083, "binary": ".../dirtyfrag/exp" },
"signal": "SIGKILL"
} }
The kprobe's action reads KPROBE_ACTION_POST because the Post action is what emits the visible event; the Sigkill action is what produces the separate SIGKILL exit. The two events together are the proof, the match and the kill.
Note: the raw
07capture also carries amessagestring from an earlier revision of the policy (before theAF_ALGleg was split to audit-only). Thefamily: 33match and the resultingSIGKILLare identical across revisions; only that text differs.
After the vulnerable-kernel run, the host was rebooted into 6.8.0-134-generic (Ubuntu's patched kernel) and the PoC was run again:
$ ./exp # policy OFF
dirtyfrag: failed (rc=4) # exploit fails on its own — no root
$ ./exp # policy ON
Killed # SIGKILL at socket(AF_RXRPC)
Being precise about what this shows:
rc=4, no root) because the kernel is patched. There is nothing for the policy to prevent. The before/after that carries the mitigation claim exists only on the vulnerable -88 kernel.socket(AF_RXRPC), so Tetragon still SIGKILLs it and surfaces the attempt in the log, on a patched kernel just as on an unpatched one. That has value as detection of an attempt and as defence in depth, but it is not the same as stopping a working exploit.AF_RXRPC is legitimate and a blanket kill would break it. Verify per node.AF_ALG leg is audit-only by design. A variant using only the AF_ALG path with modules already warm would be logged, not killed, until you promote that leg to enforce (after allowlisting).AF_RXRPC socket is opened before the write primitive. That ordering is what makes an early kill effective; it is not a guarantee about every possible exploit.Setup: Tetragon v1.7.0 standalone, started via systemctl; policies loaded with tetra tracingpolicy add.