# REPRO-2026-00358: Linux kernel af_unix GC race-condition UAF — unix_del_edge() frees dead SCC without unlinking scc_entry ## Summary Status: published Severity: high CVSS: Unknown CWE: CWE-416 (Use After Free) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00358 CVE: CVE-2026-80521 ## Package Name: Unknown Ecosystem: Unknown Affected: Unknown Fixed: Unknown ## Root Cause # RCA Report — CVE-2026-80521: Linux kernel af_unix GC race-condition UAF ## Summary The Linux kernel AF_UNIX garbage collector (`net/unix/garbage.c`), rewritten in commit `4090fa373f0e` ("af_unix: Replace garbage collection algorithm."), keeps per-SCC vertex linkage in `unix_vertex.scc_entry`. When `unix_del_edge()` drops the last outgoing edge of a vertex it frees that vertex (moves it to the `scm_fp_list` free list, later `kfree()`d) **without unlinking `scc_entry`**. A race between (2-1) `sendmsg()` passing an AF_UNIX fd with `SCM_RIGHTS` and (2-2) `close()` of the sockets forming a dead SCC lets the GC judge SCC `{A,B}` dead during the tiny window in `unix_dgram_sendmsg()` where `unix_add_edges()` has already published the new `B->B` edge but `skb_queue_tail()` has not yet queued the skb carrying it. `B` survives (its not-yet-queued fpl cannot be collected), `A`'s vertex is freed with its `scc_entry` still linked into `{A,B}`'s SCC chain, and a later GC pass — forced into `unix_walk_scc_fast()` by a still-live cyclic SCC `{X}` — iterates `B`'s `scc_entry` chain into the freed `A` vertex: kernel use-after-free. ## Impact - Component: Linux kernel, AF_UNIX socket garbage collector (`net/unix/garbage.c`). - Affected versions: kernels containing the SCC-based GC rewrite (`4090fa373f0e`, upstream 6.x era and stable backports); fixed upstream by `594d905195024b228c962627ae5ae7c17bd582a4` ("af_unix: Unlink scc_entry in unix_del_edge()"), backported e.g. `e3702470ced94fad74d71e2232f022d2eb752a6d`. - Risk: local unprivileged attacker (plain AF_UNIX sockets + SCM_RIGHTS fd passing, no privileges, no namespaces) triggers kernel memory corruption. Disclosed as potential LPE / container escape or DoS; CVSS 7.8 (NVD). ## Impact Parity - Disclosed/claimed maximum impact: kernel memory corruption (UAF), potential LPE. - Reproduced impact from this run: (updated after runtime; see Evidence) KASAN-detected use-after-free in the `unix_walk_scc_fast()` / `unix_scc_dead()` GC path on the vulnerable kernel, with the fixed kernel as negative control. - Parity: full for the claimed `memory_corruption` impact class (no privilege-escalation chain is claimed or demonstrated here). ## Root Cause `unix_del_edge()` (vulnerable tree, `net/unix/garbage.c`): ```c static void unix_del_edge(struct scm_fp_list *fpl, struct unix_edge *edge) { ... if (!vertex->out_degree) { edge->predecessor->vertex = NULL; list_move_tail(&vertex->entry, &fpl->vertices); /* missing: list_del(&vertex->scc_entry); */ } } ``` The SCC lists threaded through `scc_entry` are the persistent grouping produced by `__unix_walk_scc()` and consumed by `unix_walk_scc_fast()` / `unix_scc_dead()` / `unix_collect_skb()`. Freeing a vertex while its `scc_entry` remains linked leaves a dangling list node; the next fast SCC walk does `list_add(&scc, &vertex->scc_entry)` / reverse iteration over the stale chain and dereferences freed memory. Race window (confirmed by source inspection of the vulnerable tree): in `unix_dgram_sendmsg()`, `scm_stat_add()` → `unix_add_edges()` publishes the edge under `unix_gc_lock`, and only afterwards is the skb queued by `skb_queue_tail()`. A concurrent `unix_gc()` that takes `unix_gc_lock` between those two operations sees the `B->B` edge but not the skb, so `unix_vertex_dead(B)` (refcount == out_degree) still judges SCC `{A,B}` dead; `unix_collect_skb()` cannot collect the un-queued skb, so only `A`'s vertex is freed — with the stale `scc_entry`. - Fix commit: https://github.com/torvalds/linux/commit/594d905195024b228c962627ae5ae7c17bd582a4 (adds `list_del(&vertex->scc_entry);` before freeing the vertex). ## Reproduction Steps 1. `bundle/repro/reproduction_steps.sh` (self-contained; uses the prepared project cache when available). 2. The script: - fetches torvalds/linux at the fix commit (depth=2) and resolves the vulnerable parent `315f4bd234b3b8a3ed3a71fd4c53b110cf373720`; - verifies the fix hunk is absent in the vulnerable checkout and present in the fixed checkout; - builds two KASAN kernels (`CONFIG_KASAN=y`, outline mode): vulnerable parent and fixed commit; - builds a static initramfs whose `/init` (root) spawns an **unprivileged uid-1000** trigger that: - creates persistent socket `X` holding its own fd (live cyclic SCC keeps the GC graph in `UNIX_GRAPH_CYCLIC` state, forcing `unix_walk_scc_fast()`); - inflates `user->unix_inflight` past `UNIX_INFLIGHT_SANE_USER` (2024) so every SCM_RIGHTS send schedules GC; a hammer thread keeps `flush_work(unix_gc_work)` hot; - per iteration creates datagram sockets `A`,`B`, forms the `A<->B` cycle, then races `sendmsg(X, fd=B -> B)` against `close(A); close(B)` from two pinned threads; - boots QEMU/KVM (4 vCPUs+) twice on the vulnerable kernel and twice on the fixed kernel; the init scans dmesg for `BUG: KASAN` / use-after-free touching `unix_walk_scc*` / `unix_scc_dead` / `unix_collect_skb` / `unix_vertex_dead` / `unix_del_edge` and prints `REPRO_UAF_DETECTED` or `REPRO_CLEAN` before poweroff. 3. Expected evidence: `REPRO_UAF_DETECTED` with a KASAN splat naming the af_unix GC path on the vulnerable kernel, and `REPRO_CLEAN` on the fixed kernel. ## Evidence - `bundle/logs/reproduction_steps.log` — full driver log. - `bundle/logs/vm_vuln_attempt{1,2}.log` — serial consoles, vulnerable kernel. - `bundle/logs/vm_fixed_attempt{1,2}.log` — serial consoles, fixed kernel (negative control). - `bundle/repro/kasan_evidence.txt` — extracted KASAN/unix GC splat lines. - `bundle/repro/runtime_manifest.json` — runtime evidence manifest with per-artifact SHA-256 and target identity (`git:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux@315f4bd234b3b8a3ed3a71fd4c53b110cf373720`). - Environment: QEMU 8.2.2 (KVM), x86_64, KASAN outline, 6 vCPUs, 4 GiB RAM, unprivileged uid 1000 in-guest. Key excerpt from `bundle/logs/vm_vuln_attempt1.log` (identical pattern in attempt 2, kernel `7.2.0-rc6-g315f4bd234b3`): ``` [trigger] running as uid=1000 for 120 seconds [trigger] inflight inflation done, hammering GC BUG: KASAN: slab-use-after-free in unix_gc+0x808/0x8f0 Write of size 8 at addr ffff888103eea6a8 by task kworker/u24:3/84 Workqueue: events_unbound unix_gc ... Allocated by task 102: unix_prepare_fpl+0x62/0x1d0 unix_scm_to_skb+0x16f/0x1e0 unix_dgram_sendmsg+0x1ff/0xbc0 ____sys_sendmsg+0x4e0/0x500 Freed by task 84: kfree+0x121/0x380 unix_destroy_fpl+0xb7/0xf0 unix_wfree+0xb2/0x120 sk_skb_reason_drop+0x64/0x200 unix_gc+0x663/0x8f0 The buggy address belongs to the object at ffff888103eea680 which belongs to the cache kmalloc-96 of size 96 <-- struct unix_vertex Oops: general protection fault, probably for non-canonical address 0xdead000000000108: 0000 [#1] SMP KASAN NOPTI <-- LIST_POISON1+8 RIP: 0010:unix_scc_dead+0x8c/0x250 <-- stale scc_entry walk Workqueue: events_unbound unix_gc REPRO_UAF_DETECTED kasan=1 unix_gc_path=1 ``` The allocation trace shows the victim `unix_vertex` was allocated by the unprivileged trigger's own `sendmsg()` (`unix_prepare_fpl`), freed by the GC itself (`unix_destroy_fpl` via `skb` purge in `unix_gc`), and then written to (offset 40 = `scc_entry` list splice) and walked (`unix_scc_dead` dereferences `LIST_POISON1`-poisoned linkage) by a subsequent GC pass — exactly the CVE-2026-80521 mechanism. Fixed-kernel control runs print `REPRO_CLEAN kasan=0 unix_hit=0 oops=0` after the identical stress. ## Recommendations / Next Steps - Fix: unlink `scc_entry` before freeing the vertex in `unix_del_edge()` (upstream `594d9051...`); the fix is exactly one line and is already backported to stable series. - Upgrade guidance: any kernel carrying the af_unix SCC GC rewrite without this fix is exposed; apply stable updates containing `594d9051` / `e3702470` (and the listed stable backports). - Testing: stress AF_UNIX SCM_RIGHTS fd-passing with concurrent close under KASAN; syzbot-style fuzzing of `unix_gc` races. ## Additional Notes - The script is idempotent: fetched source, kernel builds, and the initramfs are cached under the project cache directory and re-used on re-run (SHA-marker guarded); QEMU evidence is regenerated every run. - The race window is a few instructions wide; KASAN instrumentation and the synchronous `flush_work()` GC path widen it. Attempt counts and stress duration (`VULN_SECS`/`FIX_SECS` env overrides) may need tuning on slower hosts (TCG fallback when /dev/kvm is unavailable is supported but slower). ## Reproduction Details Reproduced: 2026-09-24T07:20:28.220Z Duration: 4712 seconds Tool calls: 159 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00358 pruva-verify CVE-2026-80521 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00358&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00358/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-80521 - Source: https://access.redhat.com/security/cve/cve-2026-80521 ## Artifacts - bundle/repro/rca_report.md (analysis, 8727 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 21005 bytes) - bundle/repro/kasan_evidence.txt (other, 2423 bytes) - bundle/repro/runtime_manifest.json (other, 1616 bytes) - bundle/repro/validation_verdict.json (other, 1436 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00358 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00358/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00358 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev