# 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).
