CVE-2026-80521: Verified Reproduction
CVE-2026-80521: Linux kernel af unix GC race-condition UAF — unix del edge frees dead SCC without unlinking scc entry
CVE-2026-80521 is verified against the affected target. Vulnerability class: Use-After-Free. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00358.
What Is CVE-2026-80521?
CVE-2026-80521 is a high-severity Use-After-Free vulnerability. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00358).
CVE-2026-80521 Severity
CVE-2026-80521 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
How to Reproduce CVE-2026-80521
pruva-verify REPRO-2026-00358 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00358/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-80521
- reached the target end-to-end
- crash observed
- on the real production code path
- high confidence
unprivileged AF_UNIX datagram sockets: SCM_RIGHTS fd passing (sendmsg of sk-B's fd to sk-B from sk-X) raced with close() of the A<->B socket pair forming a dead SCC
- unix_dgram_sendmsg
- unix_add_edges (publishes B
- B edge) || close(A),close(B)
- unix_gc
- unix_walk_scc_fast
- unix_scc_dead iterates freed unix_vertex via stale scc_entry
How the agent worked
Root Cause and Exploit Chain for CVE-2026-80521
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.
- 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 by594d905195024b228c962627ae5ae7c17bd582a4("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_corruptionimpact class (no privilege-escalation chain is claimed or demonstrated here).
Root Cause
unix_del_edge() (vulnerable tree, net/unix/garbage.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
bundle/repro/reproduction_steps.sh(self-contained; uses the prepared project cache when available).- 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
Xholding its own fd (live cyclic SCC keeps the GC graph inUNIX_GRAPH_CYCLICstate, forcingunix_walk_scc_fast()); - inflates
user->unix_inflightpastUNIX_INFLIGHT_SANE_USER(2024) so every SCM_RIGHTS send schedules GC; a hammer thread keepsflush_work(unix_gc_work)hot; - per iteration creates datagram sockets
A,B, forms theA<->Bcycle, then racessendmsg(X, fd=B -> B)againstclose(A); close(B)from two pinned threads;
- creates persistent socket
- 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 touchingunix_walk_scc*/unix_scc_dead/unix_collect_skb/unix_vertex_dead/unix_del_edgeand printsREPRO_UAF_DETECTEDorREPRO_CLEANbefore poweroff.
- fetches torvalds/linux at the fix commit (depth=2) and resolves the
vulnerable parent
- Expected evidence:
REPRO_UAF_DETECTEDwith a KASAN splat naming the af_unix GC path on the vulnerable kernel, andREPRO_CLEANon 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_entrybefore freeing the vertex inunix_del_edge()(upstream594d9051...); 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_gcraces.
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_SECSenv overrides) may need tuning on slower hosts (TCG fallback when /dev/kvm is unavailable is supported but slower).
CVE-2026-80521 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
ls -la /data/pruva/project-cache/.run-views/bc0c95d1852115734884b96e5c8b70790606d70dbafffd3c60ae240107c0930f/repo-mirrors/ 2>/dev/null; cat /data/pruva/project-cache/.run-views/bc0c95d1852115734884b96e5c8b70790606d70dbafffd3c60ae240107c0930f/.pruva/cache_manifest.json 2>/dev/null; echo ===; ls bundle/learning bundle/support bundle/repro 2>/dev/nulltotal 8
drwx------ 2 pruva pruva 4096 Sep 23 13:26 .
drwx------ 4 pruva pruva 4096 Sep 23 13:26 ..
{
"schema_version": 1,
"entries": [
{
"path": "repo-mirrors",ls /data/pruva/project-cache/ 2>/dev/null | head -30; echo ===; which qemu-system-x86_64; qemu-system-x86_64 --version 2>/dev/null | head -1; echo ===; nproc; free -g | head -2; df -h /data /tmp | tail -3; echo ===; gcc --version | head -1; ls /boot 2>/dev/null | head{"cwd":"/data/pruva/runs/81b3bf3f-e693-4d7d-a041-181f1829c771","duration_ms":1067,"exit_code":0,"stderr":"","stdout":"empty-cache-generation-v1\nproject-0cc7526f-59a8-44be-817c-85eda342467d-generation-abf16512e4554428ac98effbfc9ea033\nproject-1b32eb77-2993-4f6d-b197-c6547e683bde-generation-bd818fee90e641f2a228463cb3a3f5ce\nproject-25efce6a-b32f-4f70-b6f7-c782b8f8512e-generation-06e8c3e823d34d5da0e09a625474f5c8\nproject-29067a79-6285-4b08-8e77-3fdf98e4e5d0-generation-3d64103755f54ccc90e07ee0d19c55a0\nproject-3439cc92-7d8a-4f3f-88c0-1a192a0363fc-generation-3ba17638076741b6a33c6c73e766a24e\nproject-362de50f-936d-4d7b-b5c6-38cf6e7ac143-generation-24a738e81f104a708b4e6a7df80bdc9c\nproject-3b4963e4-09b2-44ed-ae35-cf85543e2c6e-generation-0cc484837f3f427c926ad2fe5c1f1e5c\nproject-3bb30334-dfa8-4ffb-b1d2-f4c3350fb9af-generation-e3d0fa5f08ab4e01a7808a8830028ec3\nproject-470a16ed-e666-49df-b897-3d8193522fd3-generation-d752fdbceb6c4d7d9555319d316d6e25\nproject-4f9f8aca-cebb-47cf-b225-e508949c76d0-generation-14788dc7bd374cf3bfc2257b14b0e65b\nproject-55fa7b9c-5351-43fb-a3af-30af408a6666-generation-a7ad3e9c114f48ef9b4bca08efb6b398\nproject-688af86e-dc4c-4d20-bb02-ccdf8da2c7d0-generation-a0afd4a93… [truncated]ls -l /dev/kvm 2>&1; echo ===; curl -sL "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux/patch/?id=594d905195024b228c962627ae5ae7c17bd582a4" -o /tmp/fix_upstream.patch && head -100 /tmp/fix_upstream.patch{"cwd":"/data/pruva/runs/81b3bf3f-e693-4d7d-a041-181f1829c771","duration_ms":174,"exit_code":0,"stderr":"","stdout":"crw-rw---- 1 root kvm 10, 232 Sep 18 23:13 /dev/kvm\n===\n<!DOCTYPE html>\n<html lang='en'>\n<head>\n<title></title>\n<meta name='generator' content='cgit 1.3.1-korg'/>\n<meta name='robots' content='noindex, nofollow'/>\n<link rel='stylesheet' type='text/css' href='/cgit-data/cgit.css'/>\n<script type='text/javascript' src='/cgit-data/cgit.js'></script>\n<link rel='shortcut icon' href='/favicon.ico'/>\n</head>\n<body>\n<div id='cgit'><table id='header'>\n<tr>\n<td class='logo' rowspan='2'><a href='/'><img src='/cgit-data/cgit.png' alt='cgit logo'/></a></td>\n<td class='main'>Kernel.org git repositories</td></tr>\n<tr><td class='sub'>Git repositories hosted at kernel.org</td></tr></table>\n<table class='tabs'><tr><td>\n<a class='active' href='/'>index</a></td><td class='form'><form method='get' action='/pub/scm/linux/kernel/git/torvalds/linux/patch/'>\n<input type='search' name='q' size='10' value=''/>\n<input type='submit' value='search'/>\n</form></td></tr></table>\n<div class='content'><div class='error'>No repositories found</div>\n</div> <!-- class=content -->\n<… [truncated]curl -sL "https://github.com/torvalds/linux/commit/594d905195024b228c962627ae5ae7c17bd582a4.patch" -o /tmp/fix_upstream.patch && wc -l /tmp/fix_upstream.patch && head -120 /tmp/fix_upstream.patch{"cwd":"/data/pruva/runs/81b3bf3f-e693-4d7d-a041-181f1829c771","duration_ms":373,"exit_code":0,"stderr":"","stdout":"64 /tmp/fix_upstream.patch\nFrom 594d905195024b228c962627ae5ae7c17bd582a4 Mon Sep 17 00:00:00 2001\nFrom: Kuniyuki Iwashima <kuniyu@google.com>\nDate: Tue, 4 Aug 2026 00:21:54 +0000\nSubject: [PATCH] af_unix: Unlink scc_entry in unix_del_edge().\n\nKyle Zeng reported that GC could free a dead SCC partially.\n\nThe scenario is as follows:\n\n 1) Create two SCCs:\n\n X -. A <-> B\n ^--'\n\n 2) Run the following concurrently:\n\n 2-1) send() sk-B to sk-B from sk-X\n 2-2) close() both A and B\n\nAt 2-1), there is a small window where unix_add_edges()\npublishes a new edge (B <-> B) to GC but its skb is not queued\nby skb_queue_tail().\n\nIf 2-2) completes before skb_queue_tail() and GC is triggered,\nit judges A <-> B as dead, but B is not freed because GC cannot\ncollect the not-yet-queued skb holding the B <-> B edge.\n\n X -. A <-> B -. This edge is visible\n ^--' ^..' but skb is not\n\nThis itself is not a problem since the next GC run will judge\nB as dead as well and free it finally.\n\n X -. A <.> B -.\n … [truncated]id; ls -l /dev/kvm; groups; echo ===; apt list --installed 2>/dev/null | grep -Ei 'busybox|flex|bison|libelf|libssl-dev|bc ' | head; which flex bison bc pahole 2>&1uid=1000(pruva) gid=1001(pruva) groups=1001(pruva),27(sudo),993(kvm),1000(docker) crw-rw---- 1 root kvm 10, 232 Sep 18 23:13 /dev/kvm pruva sudo kvm docker === bison/noble-updates,noble-security,now 2:3.8.2+dfsg-1ubuntu0.24.04.1 amd64 [installed] busybox-initramfs/noble-updates,noble-security,now 1:1.36.1-6ubuntu3.1 amd64 [installed,automatic] busybox-static/noble-updates,noble-security,now 1:1.36.1-6ubuntu3.1 amd64 [installed,automatic] flex/noble,now 2.6.4-8.2build1 amd64 [installed]
Artifacts and Evidence for CVE-2026-80521
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-80521
FAQ: CVE-2026-80521
Is CVE-2026-80521 exploitable?
How severe is CVE-2026-80521?
What type of vulnerability is CVE-2026-80521?
How can I reproduce CVE-2026-80521?
Is the CVE-2026-80521 reproduction verified?
References for CVE-2026-80521
Authoritative sources for CVE-2026-80521 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.