{"repro_id":"REPRO-2026-00358","version":6,"title":"Linux kernel af_unix GC race-condition UAF — unix_del_edge() frees dead SCC without unlinking scc_entry","repro_type":"security","status":"published","severity":"high","description":"Race-condition use-after-free in the Linux kernel af_unix garbage collector (net/unix/garbage.c); fixed upstream by \"af_unix: Unlink scc_entry in unix_del_edge()\" (reported by Kyle Zeng). GC can partially free a dead strongly connected component (SCC): (1) create two SCCs, X -> A <-> B; (2) concurrently (a) sendmsg from sk-X toward sk-B, publishing a new edge for GC, and (b) close A and B. A tiny race window exists where the edge is visible to GC but its skb is not yet queued; unix_del_edge() then frees the dead SCC without unlinking its scc_entry, and a later GC pass (forced by X's SCC) calls unix_walk_scc_fast(), iterating over freed memory via scc_entry -> kernel UAF. Fix: unlink scc_entry before freeing the vertex in unix_del_edge(). Impact: local unprivileged user (requires only AF_UNIX sockets + SCM_RIGHTS fd passing) -> kernel memory corruption; potential LPE/container escape or DoS. Severity: CVSS 7.8 High (NVD); 7.0 Moderate (Red Hat). Affected: kernels with the af_unix GC/SCC rewrite (upstream 4090fa373f0e, backported to stable series); fixed upstream in 7.2 (594d9051), backports incl. 7.1.10 (e3702470), stable refs 6.1.141 / 6.6.93. No public PoC known. Reproduction context: build vulnerable kernel with KASAN (optionally DEBUG_OBJECTS), boot under QEMU/KVM, stress the race with paired AF_UNIX sockets passing SCM_RIGHTS fds while closing; observe UAF walk in unix_walk_scc_fast(); validate against fixed kernel for control.","root_cause":"# RCA Report — CVE-2026-80521: Linux kernel af_unix GC race-condition UAF\n\n## Summary\n\nThe Linux kernel AF_UNIX garbage collector (`net/unix/garbage.c`), rewritten in\ncommit `4090fa373f0e` (\"af_unix: Replace garbage collection algorithm.\"), keeps\nper-SCC vertex linkage in `unix_vertex.scc_entry`. When `unix_del_edge()` drops\nthe last outgoing edge of a vertex it frees that vertex (moves it to the\n`scm_fp_list` free list, later `kfree()`d) **without unlinking `scc_entry`**.\nA race between (2-1) `sendmsg()` passing an AF_UNIX fd with `SCM_RIGHTS` and\n(2-2) `close()` of the sockets forming a dead SCC lets the GC judge SCC\n`{A,B}` dead during the tiny window in `unix_dgram_sendmsg()` where\n`unix_add_edges()` has already published the new `B->B` edge but\n`skb_queue_tail()` has not yet queued the skb carrying it. `B` survives\n(its not-yet-queued fpl cannot be collected), `A`'s vertex is freed with its\n`scc_entry` still linked into `{A,B}`'s SCC chain, and a later GC pass —\nforced into `unix_walk_scc_fast()` by a still-live cyclic SCC `{X}` — iterates\n`B`'s `scc_entry` chain into the freed `A` vertex: kernel use-after-free.\n\n## Impact\n\n- Component: Linux kernel, AF_UNIX socket garbage collector (`net/unix/garbage.c`).\n- Affected versions: kernels containing the SCC-based GC rewrite\n  (`4090fa373f0e`, upstream 6.x era and stable backports); fixed upstream by\n  `594d905195024b228c962627ae5ae7c17bd582a4` (\"af_unix: Unlink scc_entry in\n  unix_del_edge()\"), backported e.g. `e3702470ced94fad74d71e2232f022d2eb752a6d`.\n- Risk: local unprivileged attacker (plain AF_UNIX sockets + SCM_RIGHTS fd\n  passing, no privileges, no namespaces) triggers kernel memory corruption.\n  Disclosed as potential LPE / container escape or DoS; CVSS 7.8 (NVD).\n\n## Impact Parity\n\n- Disclosed/claimed maximum impact: kernel memory corruption (UAF), potential LPE.\n- Reproduced impact from this run: (updated after runtime; see Evidence)\n  KASAN-detected use-after-free in the `unix_walk_scc_fast()` /\n  `unix_scc_dead()` GC path on the vulnerable kernel, with the fixed kernel as\n  negative control.\n- Parity: full for the claimed `memory_corruption` impact class\n  (no privilege-escalation chain is claimed or demonstrated here).\n\n## Root Cause\n\n`unix_del_edge()` (vulnerable tree, `net/unix/garbage.c`):\n\n```c\nstatic void unix_del_edge(struct scm_fp_list *fpl, struct unix_edge *edge)\n{\n        ...\n        if (!vertex->out_degree) {\n                edge->predecessor->vertex = NULL;\n                list_move_tail(&vertex->entry, &fpl->vertices);\n                /* missing: list_del(&vertex->scc_entry); */\n        }\n}\n```\n\nThe SCC lists threaded through `scc_entry` are the persistent grouping\nproduced by `__unix_walk_scc()` and consumed by `unix_walk_scc_fast()` /\n`unix_scc_dead()` / `unix_collect_skb()`. Freeing a vertex while its\n`scc_entry` remains linked leaves a dangling list node; the next fast SCC\nwalk does `list_add(&scc, &vertex->scc_entry)` / reverse iteration over the\nstale chain and dereferences freed memory.\n\nRace window (confirmed by source inspection of the vulnerable tree): in\n`unix_dgram_sendmsg()`, `scm_stat_add()` → `unix_add_edges()` publishes the\nedge under `unix_gc_lock`, and only afterwards is the skb queued by\n`skb_queue_tail()`. A concurrent `unix_gc()` that takes `unix_gc_lock`\nbetween those two operations sees the `B->B` edge but not the skb, so\n`unix_vertex_dead(B)` (refcount == out_degree) still judges SCC `{A,B}`\ndead; `unix_collect_skb()` cannot collect the un-queued skb, so only `A`'s\nvertex is freed — with the stale `scc_entry`.\n\n- Fix commit: https://github.com/torvalds/linux/commit/594d905195024b228c962627ae5ae7c17bd582a4\n  (adds `list_del(&vertex->scc_entry);` before freeing the vertex).\n\n## Reproduction Steps\n\n1. `bundle/repro/reproduction_steps.sh` (self-contained; uses the prepared\n   project cache when available).\n2. The script:\n   - fetches torvalds/linux at the fix commit (depth=2) and resolves the\n     vulnerable parent `315f4bd234b3b8a3ed3a71fd4c53b110cf373720`;\n   - verifies the fix hunk is absent in the vulnerable checkout and present in\n     the fixed checkout;\n   - builds two KASAN kernels (`CONFIG_KASAN=y`, outline mode): vulnerable\n     parent and fixed commit;\n   - builds a static initramfs whose `/init` (root) spawns an **unprivileged\n     uid-1000** trigger that:\n     - creates persistent socket `X` holding its own fd (live cyclic SCC keeps\n       the GC graph in `UNIX_GRAPH_CYCLIC` state, forcing\n       `unix_walk_scc_fast()`);\n     - inflates `user->unix_inflight` past `UNIX_INFLIGHT_SANE_USER` (2024) so\n       every SCM_RIGHTS send schedules GC; a hammer thread keeps\n       `flush_work(unix_gc_work)` hot;\n     - per iteration creates datagram sockets `A`,`B`, forms the `A<->B` cycle,\n       then races `sendmsg(X, fd=B -> B)` against `close(A); close(B)` from two\n       pinned threads;\n   - boots QEMU/KVM (4 vCPUs+) twice on the vulnerable kernel and twice on the\n     fixed kernel; the init scans dmesg for `BUG: KASAN` /\n     use-after-free touching `unix_walk_scc*` / `unix_scc_dead` /\n     `unix_collect_skb` / `unix_vertex_dead` / `unix_del_edge` and prints\n     `REPRO_UAF_DETECTED` or `REPRO_CLEAN` before poweroff.\n3. Expected evidence: `REPRO_UAF_DETECTED` with a KASAN splat naming the\n   af_unix GC path on the vulnerable kernel, and `REPRO_CLEAN` on the fixed\n   kernel.\n\n## Evidence\n\n- `bundle/logs/reproduction_steps.log` — full driver log.\n- `bundle/logs/vm_vuln_attempt{1,2}.log` — serial consoles, vulnerable kernel.\n- `bundle/logs/vm_fixed_attempt{1,2}.log` — serial consoles, fixed kernel\n  (negative control).\n- `bundle/repro/kasan_evidence.txt` — extracted KASAN/unix GC splat lines.\n- `bundle/repro/runtime_manifest.json` — runtime evidence manifest with\n  per-artifact SHA-256 and target identity\n  (`git:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux@315f4bd234b3b8a3ed3a71fd4c53b110cf373720`).\n- Environment: QEMU 8.2.2 (KVM), x86_64, KASAN outline, 6 vCPUs, 4 GiB RAM,\n  unprivileged uid 1000 in-guest.\n\nKey excerpt from `bundle/logs/vm_vuln_attempt1.log` (identical pattern in\nattempt 2, kernel `7.2.0-rc6-g315f4bd234b3`):\n\n```\n[trigger] running as uid=1000 for 120 seconds\n[trigger] inflight inflation done, hammering GC\nBUG: KASAN: slab-use-after-free in unix_gc+0x808/0x8f0\nWrite of size 8 at addr ffff888103eea6a8 by task kworker/u24:3/84\nWorkqueue: events_unbound unix_gc\n...\nAllocated by task 102:\n  unix_prepare_fpl+0x62/0x1d0\n  unix_scm_to_skb+0x16f/0x1e0\n  unix_dgram_sendmsg+0x1ff/0xbc0\n  ____sys_sendmsg+0x4e0/0x500\nFreed by task 84:\n  kfree+0x121/0x380\n  unix_destroy_fpl+0xb7/0xf0\n  unix_wfree+0xb2/0x120\n  sk_skb_reason_drop+0x64/0x200\n  unix_gc+0x663/0x8f0\nThe buggy address belongs to the object at ffff888103eea680\n which belongs to the cache kmalloc-96 of size 96   <-- struct unix_vertex\nOops: general protection fault, probably for non-canonical address\n 0xdead000000000108: 0000 [#1] SMP KASAN NOPTI      <-- LIST_POISON1+8\nRIP: 0010:unix_scc_dead+0x8c/0x250                  <-- stale scc_entry walk\nWorkqueue: events_unbound unix_gc\nREPRO_UAF_DETECTED kasan=1 unix_gc_path=1\n```\n\nThe allocation trace shows the victim `unix_vertex` was allocated by the\nunprivileged trigger's own `sendmsg()` (`unix_prepare_fpl`), freed by the GC\nitself (`unix_destroy_fpl` via `skb` purge in `unix_gc`), and then written to\n(offset 40 = `scc_entry` list splice) and walked (`unix_scc_dead` dereferences\n`LIST_POISON1`-poisoned linkage) by a subsequent GC pass — exactly the\nCVE-2026-80521 mechanism. Fixed-kernel control runs print\n`REPRO_CLEAN kasan=0 unix_hit=0 oops=0` after the identical stress.\n\n## Recommendations / Next Steps\n\n- Fix: unlink `scc_entry` before freeing the vertex in `unix_del_edge()`\n  (upstream `594d9051...`); the fix is exactly one line and is already\n  backported to stable series.\n- Upgrade guidance: any kernel carrying the af_unix SCC GC rewrite without\n  this fix is exposed; apply stable updates containing `594d9051` /\n  `e3702470` (and the listed stable backports).\n- Testing: stress AF_UNIX SCM_RIGHTS fd-passing with concurrent close under\n  KASAN; syzbot-style fuzzing of `unix_gc` races.\n\n## Additional Notes\n\n- The script is idempotent: fetched source, kernel builds, and the initramfs\n  are cached under the project cache directory and re-used on re-run\n  (SHA-marker guarded); QEMU evidence is regenerated every run.\n- The race window is a few instructions wide; KASAN instrumentation and the\n  synchronous `flush_work()` GC path widen it. Attempt counts and stress\n  duration (`VULN_SECS`/`FIX_SECS` env overrides) may need tuning on slower\n  hosts (TCG fallback when /dev/kvm is unavailable is supported but slower).\n","cve_id":"CVE-2026-80521","cwe_id":"CWE-416","source_url":"https://access.redhat.com/security/cve/cve-2026-80521","reproduced_at":"2026-09-24T07:20:28.220061+00:00","duration_secs":4712.0,"tool_calls":159,"handoffs":2,"total_cost_usd":4.484742,"agent_costs":{"claim_matcher":0.023581,"judge":0.543858,"learning_policy":0.009688,"repro":2.557395,"support":0.082282,"vuln_variant":1.267938},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.023581},"judge":{"gpt-5.6-sol":0.543858},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.009688},"repro":{"accounts/fireworks/models/kimi-k3":2.557395},"support":{"accounts/fireworks/models/kimi-k3":0.082282},"vuln_variant":{"accounts/fireworks/models/kimi-k3":1.267938}},"vulnerable_version_variant_outcome":"unknown","fix_bypass_outcome":"unknown","variant_disclosure_state":"unknown","quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-09-24T07:20:29.029095+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":8727,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":21005,"category":"reproduction_script"},{"path":"bundle/repro/kasan_evidence.txt","filename":"kasan_evidence.txt","size":2423,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1616,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1436,"category":"other"}]}