Initial skeleton: README, CVE inventory, roadmap, ARCH, ethics + copy_fail_family module absorbed from DIRTYFAIL
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
# DIRTYFAIL — defender's playbook
|
||||
|
||||
A one-page operational guide for sysadmins assessing and mitigating
|
||||
exposure to the Copy Fail and Dirty Frag CVE family on Linux hosts.
|
||||
|
||||
If you're operating a fleet of Linux servers, the questions below are
|
||||
the ones to answer in order.
|
||||
|
||||
---
|
||||
|
||||
## 1. Am I vulnerable?
|
||||
|
||||
**Quickest answer (no compilation):**
|
||||
|
||||
```bash
|
||||
curl -sSL https://raw.githubusercontent.com/KaraZajac/DIRTYFAIL/main/tools/dirtyfail-check.sh \
|
||||
| bash
|
||||
```
|
||||
|
||||
(Read the script first if you don't trust me — it's ~150 lines of
|
||||
plain bash, no curl-pipe-bash voodoo. Read-only on your system.)
|
||||
|
||||
Exit code: `0` mitigated, `1` vulnerable, `2` couldn't determine.
|
||||
|
||||
**Empirical answer (builds the C tool, runs the active probes):**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/KaraZajac/DIRTYFAIL.git
|
||||
cd DIRTYFAIL && make
|
||||
./dirtyfail --scan --active
|
||||
```
|
||||
|
||||
The default `--scan` mode runs precondition checks (kernel version,
|
||||
module presence, LSM state) plus an active probe of the Copy Fail
|
||||
primitive against a sentinel file in `/tmp`. Adding `--active` extends
|
||||
the sentinel-STORE probe to the other four primitives (ESP v4, ESP v6,
|
||||
RxRPC, GCM) — this is the only way to distinguish a backported-patched
|
||||
kernel from an unpatched one without running the full exploit. The
|
||||
probes only modify temporary files in `/tmp`; `/etc/passwd` is never
|
||||
touched.
|
||||
|
||||
**Per-CVE breakdown (manual checks):**
|
||||
|
||||
| Question | Command | Vulnerable if |
|
||||
|---|---|---|
|
||||
| Is the algif_aead module reachable? | `lsmod \| grep algif_aead` + `grep algif_aead /etc/modprobe.d/*` | Loaded AND not blacklisted |
|
||||
| Are esp4/esp6 modules reachable? | `modinfo esp4 esp6` | Both present, not blacklisted |
|
||||
| Is rxrpc reachable? | `lsmod \| grep rxrpc` + `getsockopt(AF_RXRPC, ...)` | Module loadable from unprivileged context |
|
||||
| Is unprivileged userns hardened? | `cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns` | Returns `0` or file absent |
|
||||
| Does PAM accept empty passwords? | `grep nullok /etc/pam.d/common-auth` | "nullok" present without "nullok_secure" |
|
||||
|
||||
---
|
||||
|
||||
## 2. How do I mitigate?
|
||||
|
||||
Three options, listed best-to-worst:
|
||||
|
||||
### A. Apply the upstream kernel patch (best)
|
||||
|
||||
The fix is mainline commit `f4c50a4034e6` (merged 2026-05-07). Each
|
||||
distro's kernel package is on its own backport timeline:
|
||||
|
||||
| Distro | Status (as of 2026-05-09) |
|
||||
|---|---|
|
||||
| Debian 13 (`6.12.86+deb13`) | ✅ patched |
|
||||
| Ubuntu 24.04 LTS | ❌ not yet patched (kernel 6.8.0-111) |
|
||||
| Ubuntu 26.04 LTS | ❌ not yet patched (kernel 7.0.0-15.15, predates upstream merge) |
|
||||
| AlmaLinux 10.1 | ❌ not yet patched (kernel 6.12 EL) |
|
||||
| Fedora 44 | ❌ not yet patched (kernel 6.19.10) |
|
||||
|
||||
Run `apt list --upgradable linux-image-*` / `dnf check-update kernel`
|
||||
periodically and apply.
|
||||
|
||||
### B. Layered LSM mitigation (Ubuntu 26.04 model)
|
||||
|
||||
If you're on Ubuntu 24.04 or 26.04, you can replicate Ubuntu 26.04's
|
||||
defense-in-depth approach without waiting for the kernel patch:
|
||||
|
||||
```bash
|
||||
# 1. Block unprivileged user namespaces from acquiring caps
|
||||
echo 'kernel.apparmor_restrict_unprivileged_userns = 1' \
|
||||
| sudo tee /etc/sysctl.d/99-userns-restrict.conf
|
||||
sudo sysctl --system
|
||||
|
||||
# 2. Verify the AA hardening is in effect:
|
||||
sudo unshare -U -r bash -c 'echo deny > /proc/self/setgroups 2>&1' \
|
||||
|| echo "OK: unprivileged userns has no caps (mitigation working)"
|
||||
```
|
||||
|
||||
This blocks the EXPLOIT INFRASTRUCTURE (no caps in unprivileged
|
||||
userns), not the underlying kernel bug. Real-root exploitation still
|
||||
works.
|
||||
|
||||
### C. Module blacklist (`dirtyfail --mitigate` or manual)
|
||||
|
||||
Heaviest hammer — blacklists every module that hosts a primitive.
|
||||
**Side effects: breaks IPsec, AFS, and any userspace using `AF_ALG`
|
||||
AEAD.**
|
||||
|
||||
Automated:
|
||||
|
||||
```bash
|
||||
sudo ./dirtyfail --mitigate
|
||||
```
|
||||
|
||||
Manual equivalent:
|
||||
|
||||
```bash
|
||||
sudo tee /etc/modprobe.d/dirtyfail-mitigations.conf <<'EOF'
|
||||
install algif_aead /bin/false
|
||||
install esp4 /bin/false
|
||||
install esp6 /bin/false
|
||||
install rxrpc /bin/false
|
||||
EOF
|
||||
|
||||
sudo rmmod algif_aead esp4 esp6 rxrpc 2>/dev/null
|
||||
sudo sysctl vm.drop_caches=3
|
||||
```
|
||||
|
||||
Undo: `sudo ./dirtyfail --cleanup-mitigate` (or delete the conf
|
||||
files, then `sudo modprobe <name>` to reload as needed).
|
||||
|
||||
### D. Disable `pam_unix nullok`
|
||||
|
||||
Optional belt-and-suspenders: even if a page-cache STORE lands, the
|
||||
exploit relies on PAM's `nullok` flag to convert "empty password
|
||||
field in /etc/passwd" into a successful `su`. Removing `nullok` from
|
||||
`/etc/pam.d/common-auth` (Debian/Ubuntu) or `/etc/pam.d/system-auth`
|
||||
(Red Hat family) closes that step:
|
||||
|
||||
```bash
|
||||
sudo sed -i 's/\bnullok\b//g' /etc/pam.d/common-auth # Debian/Ubuntu
|
||||
# Verify a passworded user can still log in normally before logging out!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. What should I monitor?
|
||||
|
||||
Even after mitigation, the kernel bug remains until the patch lands.
|
||||
For detection:
|
||||
|
||||
### auditd rules (universal)
|
||||
|
||||
A ready-to-load rules file ships in `tools/99-dirtyfail.rules`. It
|
||||
covers six syscall paths used by the exploit chain: XFRM netlink,
|
||||
add_key(rxrpc), unshare(CLONE_NEWUSER), AF_ALG socket creation,
|
||||
AppArmor `change_onexec` writes, and direct `/etc/passwd`/`/etc/shadow`
|
||||
modifications.
|
||||
|
||||
```bash
|
||||
sudo install -m 0640 tools/99-dirtyfail.rules /etc/audit/rules.d/
|
||||
sudo augenrules --load
|
||||
sudo systemctl restart auditd
|
||||
```
|
||||
|
||||
Search for events:
|
||||
|
||||
```bash
|
||||
# grep is more reliable than ausearch on distros that use ENRICHED
|
||||
# log_format (Debian 13, Fedora 44 — ausearch -k can return "no matches"
|
||||
# even when SYSCALL events with the key are present in the file).
|
||||
sudo grep -E 'type=SYSCALL.*key="dirtyfail-' /var/log/audit/audit.log | tail -20
|
||||
|
||||
# Or per-key, only the most recent entries:
|
||||
sudo grep 'key="dirtyfail-xfrm"' /var/log/audit/audit.log | tail -5
|
||||
sudo grep 'key="dirtyfail-rxkey"' /var/log/audit/audit.log | tail -5
|
||||
sudo grep 'key="dirtyfail-userns"' /var/log/audit/audit.log | tail -5
|
||||
sudo grep 'key="dirtyfail-afalg"' /var/log/audit/audit.log | tail -5
|
||||
```
|
||||
|
||||
(`sudo ausearch -k <key>` is the documented tool for this and works on
|
||||
older distros, but enriched-format compat issues mean `grep` is the
|
||||
safer default.)
|
||||
|
||||
The `dirtyfail-userns` rule fires on every legitimate `unshare -U` and
|
||||
rootless container start — pair it with `dirtyfail-xfrm` in a SIEM
|
||||
correlation rule (same auid, both within ~5s) for a high-fidelity
|
||||
alert. Tuning notes inline in the rules file.
|
||||
|
||||
### eBPF / falco (if you have it)
|
||||
|
||||
Falco's `Sensitive mount opened for writing` and `Detect outbound
|
||||
connections to common miner pool ports` rule sets won't help directly,
|
||||
but a custom rule on `unshare(CLONE_NEWUSER)` followed by
|
||||
`sendto(SOCK_RAW, NETLINK_XFRM)` from a non-zero uid is high-fidelity.
|
||||
|
||||
### Cheap log signal
|
||||
|
||||
```bash
|
||||
# Hits if our exploit's marker bytes show up in /etc/passwd's page cache
|
||||
# (run periodically; doesn't catch every variant but is zero-cost)
|
||||
grep -E '^[^:]+::0:0:|^[^:]+:x:0000:' /etc/passwd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Quick reference card
|
||||
|
||||
```
|
||||
SCAN this host:
|
||||
curl ... | bash # bash check (no compile)
|
||||
./dirtyfail --scan # preconds + Copy Fail probe (~1s)
|
||||
./dirtyfail --scan --active # all 5 sentinel-STORE probes (~10s)
|
||||
./dirtyfail --scan --active --json # same, machine-readable for SIEM
|
||||
|
||||
MITIGATE (Ubuntu / fleet-wide):
|
||||
sudo ./dirtyfail --mitigate # one-shot defensive deployment
|
||||
sudo ./dirtyfail --cleanup-mitigate # undo
|
||||
|
||||
MITIGATE (manual, no DIRTYFAIL):
|
||||
See section 2-C above.
|
||||
|
||||
PATCH:
|
||||
apt list --upgradable | grep linux-image
|
||||
dnf check-update kernel
|
||||
|
||||
MONITOR:
|
||||
/etc/audit/rules.d/99-dirtyfail.rules (see section 3)
|
||||
|
||||
EMERGENCY (suspected compromise via this CVE class):
|
||||
sudo sysctl vm.drop_caches=3 # evicts page-cache exploits
|
||||
sudo systemctl restart sshd # forces re-read of /etc/passwd
|
||||
grep dirtyfail /etc/passwd # check for backdoor user
|
||||
rm -f /var/tmp/.dirtyfail.state # clean DIRTYFAIL state file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Glossary
|
||||
|
||||
- **Page-cache write**: kernel writes attacker-controlled bytes into the
|
||||
in-memory copy of a file (`/etc/passwd`, `/usr/bin/su`) without
|
||||
modifying the file on disk. Persists in RAM until eviction.
|
||||
- **PAM nullok**: configuration flag that permits authentication for
|
||||
accounts with an empty password field in `/etc/passwd` (or
|
||||
`/etc/shadow`).
|
||||
- **xfrm-ESP**: the kernel's ESP (Encapsulating Security Payload)
|
||||
implementation in the IPsec stack. The bug class affects in-place
|
||||
AEAD decrypt over splice-pinned page-cache pages.
|
||||
- **Userns capability stripping**: kernel-level enforcement that
|
||||
unprivileged user namespaces have no `CAP_NET_ADMIN` /
|
||||
`CAP_SYS_ADMIN`, blocking exploit infrastructure even when the
|
||||
underlying kernel bug is unpatched.
|
||||
@@ -0,0 +1,324 @@
|
||||
# DIRTYFAIL — research notes
|
||||
|
||||
This document captures kernel-source audits and analysis adjacent to
|
||||
the published CVEs (CVE-2026-31431 / CVE-2026-43284 / CVE-2026-43500).
|
||||
It's a living research log, not a vendor advisory: findings here are
|
||||
based on reading mainline kernel source and the disclosed write-ups,
|
||||
and may need re-verification as the kernel evolves.
|
||||
|
||||
---
|
||||
|
||||
## §1. Adjacent kernel paths — audit for the same skb_cow_data() bypass pattern
|
||||
|
||||
### TL;DR
|
||||
|
||||
Ten kernel paths beyond the published CVEs were audited for the
|
||||
same in-place-AEAD-over-splice-pinned-pages bug class. **All ten
|
||||
are structurally immune.** No undisclosed CVE candidates surfaced
|
||||
in this audit; the bug class is genuinely tightly scoped to the
|
||||
three published sinks plus the algif_aead authencesn/rfc4106-gcm
|
||||
primitives.
|
||||
|
||||
### The vulnerable pattern
|
||||
|
||||
The CVE-2026-43284-class bug requires all four of:
|
||||
|
||||
1. **In-place AEAD** — `aead_request_set_crypt(req, src, dst, ...)`
|
||||
where `src == dst` or the scatterlists alias the same memory.
|
||||
2. **Conditional skip-COW** — input handler has a branch that bypasses
|
||||
`skb_cow_data()` on certain skb shapes (typically: non-linear with
|
||||
no frag_list).
|
||||
3. **`skb_to_sgvec` over skb frags** — the scatterlist passed to the
|
||||
AEAD is built directly from the skb's frags, so splice-pinned page
|
||||
references end up in it.
|
||||
4. **Userspace path to the skb's frags** — `splice(2)`, `sendfile(2)`,
|
||||
or `sendmsg(MSG_SPLICE_PAGES)` can deliver attacker-controlled
|
||||
page-cache pages into those frags.
|
||||
|
||||
Removing any one of the four breaks the chain. The published CVEs are
|
||||
the three sinks where all four conditions align (esp_input, esp6_input,
|
||||
rxkad_verify_packet_1) plus the algif_aead authencesn / rfc4106-gcm
|
||||
primitives that share the in-place destination scatterlist pattern.
|
||||
|
||||
### §1.1 Path-by-path verdict
|
||||
|
||||
| Path | In-place crypto? | skb_cow_data | Splice-reachable? | Verdict |
|
||||
|---|---|---|---|---|
|
||||
| esp_input (esp4) | ✅ | conditional skip | yes | **CVE-2026-43284** (patched) |
|
||||
| esp6_input | ✅ | conditional skip | yes | **CVE-2026-43284 v6** (patched) |
|
||||
| algif_aead authencesn | ✅ | n/a (different path) | yes via splice→AF_ALG | **CVE-2026-31431** (patched) |
|
||||
| algif_aead rfc4106-gcm | ✅ | n/a | yes | **Copy Fail GCM variant** (patched as side-effect of CF revert) |
|
||||
| rxkad_verify_packet_1 | ✅ | conditional skip | yes via RxRPC handshake | **CVE-2026-43500** (NOT patched as of 2026-05-09) |
|
||||
| **ah_input (ah4 + ah6)** | ✅ (HMAC, not decrypt) | **UNCONDITIONAL** | n/a | NOT vulnerable — structurally immune |
|
||||
| **ipcomp_input** | ❌ (decompress, separate output pages) | conditional skip | n/a (output is fresh page) | NOT vulnerable — separate dst |
|
||||
| **macsec_decrypt** | ✅ | **UNCONDITIONAL** | no — rx skbs come from netdev | NOT vulnerable — structurally immune |
|
||||
| **tls_sw recv decrypt** | ✅ | unconditional, also rx-only | no — rx skbs come from TCP rx ring | NOT vulnerable |
|
||||
| **tls_sw send encrypt + MSG_SPLICE_PAGES** | YES (read-only on user pages) | n/a (msg_en allocated separately) | yes (msg_pl) but only as src | NOT vulnerable — separate src/dst |
|
||||
| **WireGuard `decrypt_packet`** | ✅ ChaCha20Poly1305 in-place | **UNCONDITIONAL** at line 252 | yes via UDP rx (but COW protects) | NOT vulnerable — structurally immune |
|
||||
| **algif_skcipher `_skcipher_recvmsg`** | ✅ symmetric in-place possible | n/a (different module structure) | src yes (TX SGL), dst no (recv iovec) | NOT vulnerable — separate src/dst |
|
||||
| **espintcp** (ESP-in-TCP) | n/a (delegates) | n/a | reaches esp_input via xfrm_rcv_encap | inherits f4c50a4034e6 patch — NOT a new CVE |
|
||||
| **OpenVPN kernel offload `ovpn_aead_decrypt`** | ✅ AEAD in-place | **UNCONDITIONAL** at line 210 | yes via UDP rx (but COW protects) | NOT vulnerable — structurally immune |
|
||||
| **SCTP-AUTH `sctp_auth_calculate_hmac`** | HMAC only (no decrypt, no destination write into skb data frags) | n/a | n/a — digest writes to auth chunk header (kernel-allocated), not data frags | NOT vulnerable — read-only over data |
|
||||
|
||||
### §1.2 Eliminated paths — why each is immune
|
||||
|
||||
**`ah_input` (net/ipv4/ah4.c, net/ipv6/ah6.c)** — IPsec Authentication
|
||||
Header. Calls `skb_cow_data(skb, 0, &trailer)` UNCONDITIONALLY before
|
||||
`skb_to_sgvec_nomark` builds the HMAC scatterlist. No skip-cow branch.
|
||||
Splice-pinned pages would always be copied into a private buffer
|
||||
before HMAC verification.
|
||||
|
||||
**`xfrm_ipcomp.c`** — IPCOMP decompression has a conditional skip-cow
|
||||
branch, but the output is allocated as a fresh kernel page
|
||||
(`alloc_page(GFP_ATOMIC)`) and the destination scatterlist `dsg` is
|
||||
built separately from the input scatterlist `sg`. Even with
|
||||
splice-pinned input pages, decompression output goes to fresh pages.
|
||||
Not in-place over input.
|
||||
|
||||
**`macsec_decrypt` (drivers/net/macsec.c)** — MACsec receive AEAD.
|
||||
Calls `skb_cow_data(skb, 0, &trailer)` unconditionally before
|
||||
`skb_to_sgvec` and the in-place decrypt. Additionally: macsec rx
|
||||
skbs come from netdev rx, not from userspace splice — the attacker
|
||||
has no path to plant a page-cache page reference.
|
||||
|
||||
**`tls_sw_recvmsg` (net/tls/tls_sw.c)** — kTLS receive AEAD.
|
||||
kernel.org docs: "To decrypt 'in place' kTLS calls skb_cow_data()."
|
||||
COW is unconditional on the rx path. Additionally: TLS rx skbs come
|
||||
from the TCP rx queue, not from splice — the only way a user can put
|
||||
a page-cache page reference into a TCP rx skb is via rare
|
||||
`SO_PEEK_OFF` / `MSG_PEEK` paths or kernel-side socket forwarding,
|
||||
neither of which gives the attacker control.
|
||||
|
||||
### §1.3 kTLS send via MSG_SPLICE_PAGES — closest near-miss
|
||||
|
||||
The kTLS *send* path was modified in 2023 ("splice, net: Handle
|
||||
MSG_SPLICE_PAGES in AF_TLS", LWN 933386) to support
|
||||
`MSG_SPLICE_PAGES`, which is the same primitive Dirty Frag and Copy
|
||||
Fail abuse. This was the most plausible adjacent candidate.
|
||||
|
||||
**Resolved: not vulnerable.** Direct reading of `net/tls/tls_sw.c`:
|
||||
|
||||
- `tls_sw_sendmsg_splice()` adds the user's spliced pages to `msg_pl`
|
||||
(the plaintext sk_msg buffer) via `sk_msg_page_add()`.
|
||||
- `tls_alloc_encrypted_msg()` calls
|
||||
`sk_msg_alloc(sk, msg_en, len, 0)` — **fresh kernel pages** for the
|
||||
encrypted buffer.
|
||||
- `tls_push_record()` chains the scatterlists:
|
||||
```c
|
||||
sg_chain(rec->sg_aead_out, 2, &msg_en->sg.data[i]);
|
||||
```
|
||||
- `tls_do_encryption()`:
|
||||
```c
|
||||
aead_request_set_crypt(aead_req, rec->sg_aead_in,
|
||||
rec->sg_aead_out, data_len, rec->iv_data);
|
||||
```
|
||||
- `sg_aead_in` (chained from msg_pl, contains user's spliced page)
|
||||
≠ `sg_aead_out` (chained from msg_en, kernel-allocated pages).
|
||||
|
||||
The encrypt READS the user's spliced /etc/passwd page but WRITES
|
||||
ciphertext to `msg_en`'s kernel-allocated pages. The user's
|
||||
page-cache page is never modified. This is exactly the defense the
|
||||
algif_aead patch (a664bf3d603d) implemented when it reverted to
|
||||
out-of-place AEAD; kTLS has had it from inception.
|
||||
|
||||
Compare to the vulnerable `esp_input` pattern:
|
||||
|
||||
```c
|
||||
/* vulnerable: src == dst */
|
||||
skb_to_sgvec(skb, sg, ...);
|
||||
aead_request_set_crypt(req, sg, sg, ...);
|
||||
```
|
||||
|
||||
```c
|
||||
/* safe: src ≠ dst */
|
||||
sg_chain(sg_aead_in, ..., msg_pl); /* user spliced pages */
|
||||
sg_chain(sg_aead_out, ..., msg_en); /* kernel private pages */
|
||||
aead_request_set_crypt(req, sg_aead_in, sg_aead_out, ...);
|
||||
```
|
||||
|
||||
### §1.3a WireGuard receive — `decrypt_packet()`
|
||||
|
||||
ChaCha20Poly1305 in-place AEAD on incoming UDP skbs. Confirmed
|
||||
**not vulnerable** — `drivers/net/wireguard/receive.c:232–277`:
|
||||
|
||||
```c
|
||||
static bool decrypt_packet(struct sk_buff *skb, struct noise_keypair *keypair)
|
||||
{
|
||||
struct scatterlist sg[MAX_SKB_FRAGS + 8];
|
||||
/* ... */
|
||||
offset = -skb_network_offset(skb);
|
||||
skb_push(skb, offset);
|
||||
num_frags = skb_cow_data(skb, 0, &trailer); /* line 252, UNCONDITIONAL */
|
||||
/* ... */
|
||||
sg_init_table(sg, num_frags);
|
||||
if (skb_to_sgvec(skb, sg, 0, skb->len) <= 0)
|
||||
return false;
|
||||
if (!chacha20poly1305_decrypt_sg_inplace(sg, skb->len, NULL, 0,
|
||||
PACKET_CB(skb)->nonce,
|
||||
keypair->receiving.key))
|
||||
return false;
|
||||
```
|
||||
|
||||
`skb_cow_data` at line 252 is UNCONDITIONAL — no skip-cow branch. By
|
||||
the time the in-place AEAD runs, any splice-pinned pages have already
|
||||
been copied into kernel-private pages. Same defensive pattern as
|
||||
AH, MACsec, kTLS rx.
|
||||
|
||||
### §1.3b algif_skcipher — `_skcipher_recvmsg()`
|
||||
|
||||
The companion module to algif_aead, exposing symmetric ciphers
|
||||
(AES-CBC, AES-CTR, etc.) over AF_ALG. Same author and patchset era
|
||||
as the in-place optimization that introduced Copy Fail (2017,
|
||||
72548b093ee3); the Copy Fail upstream fix only reverted algif_aead,
|
||||
so worth verifying algif_skcipher independently.
|
||||
|
||||
`crypto/algif_skcipher.c:151–152`:
|
||||
|
||||
```c
|
||||
skcipher_request_set_crypt(&areq->cra_u.skcipher_req, areq->tsgl,
|
||||
areq->first_rsgl.sgl.sgt.sgl, len, ctx->iv);
|
||||
```
|
||||
|
||||
- `areq->tsgl` = TX SGL, populated via `af_alg_pull_tsgl()`. CAN
|
||||
contain user-spliced page-cache pages (sendmsg + splice path).
|
||||
- `areq->first_rsgl.sgl.sgt.sgl` = RX SGL, populated via
|
||||
`af_alg_get_rsgl(sk, msg, ...)` from the user's `recv()` iovec,
|
||||
via `iov_iter_get_pages` mapping the calling process's anonymous
|
||||
memory.
|
||||
|
||||
The cipher operation reads from `tsgl` (potentially user-spliced
|
||||
page-cache pages) and writes to `rsgl` (user's recv buffer in their
|
||||
own anonymous memory). **src ≠ dst; output never lands on
|
||||
splice-pinned page-cache pages.**
|
||||
|
||||
Why this differs from algif_aead's Copy Fail: the algif_aead bug was
|
||||
specifically about the `authencesn` template internally chaining TAG
|
||||
pages into the destination SGL extension (`req->dst` extends past
|
||||
the end of `req->src`'s last page into chained tag pages, which
|
||||
happen to be the source's spliced pages). Plain skcipher has no AEAD
|
||||
tags, no chained scratch — clean src/dst separation. **Not
|
||||
vulnerable.**
|
||||
|
||||
### §1.3c espintcp — IPsec ESP over TCP
|
||||
|
||||
`net/xfrm/espintcp.c` is a *transport-layer wrapper* — it does no
|
||||
cryptographic work itself. The `handle_esp()` function delegates
|
||||
straight to `xfrm6_rcv_encap` / `xfrm4_rcv_encap`, which call into
|
||||
the standard `esp_input()` / `esp6_input()` handlers. Any skb that
|
||||
reaches the ESP path through espintcp is processed by the same code
|
||||
that was patched by f4c50a4034e6 (SKBFL_SHARED_FRAG check).
|
||||
|
||||
**Verdict: not a separate CVE.** On unpatched kernels, espintcp is
|
||||
just an alternative transport for the existing CVE-2026-43284 sink
|
||||
(esp_input). On patched kernels the same fix covers both UDP and TCP
|
||||
encapsulation. The SHARED_FRAG flag is set wherever splice can plant
|
||||
pages into TCP send buffers, and the producer-side flagging
|
||||
propagates through TCP into the espintcp path.
|
||||
|
||||
### §1.3d OpenVPN kernel offload — `ovpn_aead_decrypt()`
|
||||
|
||||
New module in 6.16+ implementing OpenVPN's data channel
|
||||
(ChaCha20Poly1305 / AES-GCM) in the kernel. Receive AEAD path is in
|
||||
`drivers/net/ovpn/crypto_aead.c`:
|
||||
|
||||
```c
|
||||
/* line ~210 */
|
||||
nfrags = skb_cow_data(skb, 0, &trailer); /* UNCONDITIONAL */
|
||||
/* ... */
|
||||
/* line ~228 */
|
||||
skb_to_sgvec_nomark(skb, sg + 1, payload_offset, payload_len);
|
||||
/* ... */
|
||||
/* line ~239 */
|
||||
aead_request_set_crypt(req, sg, sg, payload_len + tag_size, iv);
|
||||
```
|
||||
|
||||
In-place AEAD (`sg, sg`) — but `skb_cow_data()` is called
|
||||
unconditionally before `skb_to_sgvec_nomark` builds the scatterlist.
|
||||
Splice-pinned pages always copied to kernel-private memory before
|
||||
the AEAD runs. **Not vulnerable.** Same defensive pattern as
|
||||
WireGuard, AH, MACsec, kTLS rx.
|
||||
|
||||
### §1.3e SCTP-AUTH HMAC validation
|
||||
|
||||
`net/sctp/auth.c:sctp_auth_calculate_hmac()` (lines 606–642) computes
|
||||
HMAC over an SCTP AUTH chunk:
|
||||
|
||||
```c
|
||||
data_len = skb_tail_pointer(skb) - (unsigned char *)auth;
|
||||
digest = (u8 *)(&auth->auth_hdr + 1);
|
||||
hmac_sha1_usingrawkey(asoc_key->data, asoc_key->len,
|
||||
(const u8 *)auth, data_len, digest);
|
||||
```
|
||||
|
||||
The HMAC is computed READ-ONLY over the skb's chunk data. The
|
||||
digest output is written to the auth chunk's digest field
|
||||
(`&auth->auth_hdr + 1`), which on the SEND path lives in
|
||||
kernel-allocated chunk header memory — not in any user-spliced
|
||||
data fragment. On the RECEIVE path, verification computes HMAC
|
||||
over received data and compares to the sender-provided digest in a
|
||||
private buffer — pure read.
|
||||
|
||||
The bug class requires a kernel-side WRITE to a splice-pinned page;
|
||||
SCTP-AUTH only ever READS from skb data and writes the digest to a
|
||||
kernel-allocated chunk header. **Not vulnerable.**
|
||||
|
||||
### §1.4 The protective patterns that distinguish safe from vulnerable
|
||||
|
||||
Every safe path on the list achieves immunity through one of three
|
||||
mechanisms, each of which removes one of the four required conditions:
|
||||
|
||||
1. **Unconditional `skb_cow_data()`** before any in-place crypto —
|
||||
AH, MACsec, kTLS rx. (Removes condition 2.)
|
||||
2. **Separate destination scatterlist** allocated from kernel-private
|
||||
pages — kTLS tx, IPCOMP, post-patch algif_aead.
|
||||
(Removes condition 1.)
|
||||
3. **The in-place crypto target is fundamentally not a splice-able
|
||||
skb** — kTLS rx skbs come from TCP rx, not user splice.
|
||||
(Removes condition 4.)
|
||||
|
||||
### §1.5 Out-of-scope or low-value candidates
|
||||
|
||||
The candidates that remained after §1.3a-e were all eliminated as
|
||||
not worth a deeper audit:
|
||||
|
||||
- **AF_SMC encryption** — uses kTLS/ULP underneath, already covered
|
||||
by the kTLS audit (§1.3 / §1.4b).
|
||||
- **io_uring crypto extensions** — would inherit AF_ALG semantics,
|
||||
already covered by the algif_skcipher audit (§1.3b).
|
||||
- **Bluetooth CMTP/HIDP crypto** — privileged-only (HCI device
|
||||
access), not an unprivileged-LPE vector.
|
||||
- **Kernel TLS NIC offload** — encryption runs on the NIC firmware,
|
||||
different threat surface entirely (firmware-side bug, not
|
||||
page-cache-write).
|
||||
- **dm-crypt / fscrypt** — block-layer / filesystem-layer
|
||||
encryption. Different threat model; user can't splice arbitrary
|
||||
page-cache pages into block requests in any meaningful way.
|
||||
|
||||
### §1.6 Methodology
|
||||
|
||||
For each candidate path, read the input handler and ask:
|
||||
|
||||
1. Does it call `skb_cow_data()` BEFORE building the AEAD
|
||||
scatterlist?
|
||||
2. Is there a conditional branch (typically based on `skb_cloned`,
|
||||
`skb_has_frag_list`, `skb_is_nonlinear`) that bypasses (1)?
|
||||
3. Is the resulting scatterlist used as BOTH src AND dst of
|
||||
`aead_request_set_crypt()` / equivalent?
|
||||
4. Can a userspace primitive (`splice(2)`, `sendfile(2)`,
|
||||
`sendmsg(MSG_SPLICE_PAGES)`, AF_ALG send) deliver
|
||||
attacker-controlled pages into the input skb's frags?
|
||||
|
||||
All four must be true for the bug class to apply. A single "no" is
|
||||
sufficient for "not vulnerable."
|
||||
|
||||
---
|
||||
|
||||
## §2. References
|
||||
|
||||
- V4bel/dirtyfrag write-up — [github.com/V4bel/dirtyfrag/blob/master/assets/write-up.md](https://github.com/V4bel/dirtyfrag/blob/master/assets/write-up.md)
|
||||
- Theori/Xint Copy Fail disclosure — [xint.io/blog/copy-fail-linux-distributions](https://xint.io/blog/copy-fail-linux-distributions)
|
||||
- LWN — Replace sendpage with sendmsg(MSG_SPLICE_PAGES) — [lwn.net/Articles/928487](https://lwn.net/Articles/928487/)
|
||||
- LWN — Handle MSG_SPLICE_PAGES in AF_TLS — [lwn.net/Articles/933386](https://lwn.net/Articles/933386/)
|
||||
- TLS 1.3 Rx improvements (Kicinski) — [people.kernel.org/kuba/tls-1-3-rx-improvements-in-linux-5-20](https://people.kernel.org/kuba/tls-1-3-rx-improvements-in-linux-5-20)
|
||||
- 0xdeadbeefnetwork Copy_Fail2 (GCM variant) — [github.com/0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo](https://github.com/0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo)
|
||||
- Linux source (torvalds/master) — `net/ipv4/ah4.c`, `net/ipv6/ah6.c`, `net/xfrm/xfrm_ipcomp.c`, `drivers/net/macsec.c`, `net/tls/tls_sw.c`
|
||||
Reference in New Issue
Block a user