fsetown() publishes an uninitialized sigio into the owner list β exit1()/pgdelete() teardown consumes the half-born entry: NULL-page fault or wrong-list SLIST_REMOVE walk-off (plus funsetownlst livelock variant)
| Field | Value |
|---|---|
| ID | DF-2683 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-362 Race Condition (wrong lock) |
| File | sys/kern/kern_descrip.c |
| Lines | 1351-1366 (walkers: kern_exit.c:376, kern_proc.c:680; funsetownlst :1281-1287) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-29 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
fsetown() inserts the M_ZERO sigio into proc->p_sigiolst /
pgrp->pg_sigiolst (:1351/:1357), drops the owner token, and only then
initializes sio_pgid/sio_ucred/sio_ruid/sio_myref (:1362-1366).
funsetownlst walkers β exit1() and pgdelete() β consume list entries
unlocked: funsetown(NULL->sio_myref) NULL-faults; sio_pgid still 0
selects the wrong owner branch and SLIST_REMOVE walks off the list
(NULL+8); myref-set-but-unpublished makes funsetown return without
unlinking so funsetownlst spins forever (hard machine wedge, no console
output).
Threat model & preconditions
Unprivileged local user forks short-lived same-session children and
hammers fcntl(sock, F_SETOWN, child_pid): each child's exit walk that
lands in the ~100ns-1ms init window crashes or wedges the kernel.
Impact ceiling: local DoS (panic/permanent wedge); no control primitive
on this path.
Proof of concept
VERIFIED twice on the stock guest (findings/poc/DF-2683/fsetown_race.c):
(a) run 1 β ssh died, guest unresponsive, QEMU spinning ~3 vCPUs, zero
serial output (funsetownlst livelock wedge); (b) run 2 after fresh
reset β Fatal trap 12 ... fault virtual address = 0x8 ... Stopped at
funsetown+0x93, kernel in DDB.
Recommended fix
Initialize every sigio field BEFORE the owner-list insertion, publish *sigiop after the entry is fully linked, and make funsetownlst claim the head atomically under sigio_token with a sleep-retry for publication-in-flight entries. The pack's fix.diff (v4) compiles/boots but the 20000-round storm still crashes the restructured replacement machinery via recycled-chunk double teardown β fix_failed with v1-v4 history; a per-sigio refcount or dedicated list lock is the recommended upstream shape.
Timeline
- 2026-08-29 Discovered during pass-2 audit of kern_descrip.c (GLM 5.3); panic + total-freeze both reproduced; fix iterations honestly failed.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2683 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fsetown_race.c | β | 2.5 KB | view raw | |
| build.sh | β | 115 B | view raw | |
| run.sh | β | 222 B | view raw | |
| run.log | β | 525 B | view raw | |
| panic.txt | β | 13.6 KB | view raw | |
| panic.dbg.txt | β | 701 B | view raw | |
| env.txt | β | 213 B | view raw | |
| VERDICT.md | β | 5.1 KB | β raw | |
| fix.diff | β | 5.8 KB | view raw | |
| verdict.json | β | 3.3 KB | view raw | |
| manifest.json | β | 825 B | view raw |
DF-2683 β VERDICT
status: reproduced (impact: panic; hard-wedge variant = dos) attempts: 2 stock-kernel reproductions (1 silent machine wedge, 1 clean fatal trap), plus 4 fix-validation iterations (v1/v2 wedges, v3/v4 crashes -- see "Fix validation").
Root cause
fsetown() (sys/kern/kern_descrip.c:1296-1380) allocates the sigio
zeroed, links it into the owner's list, and only initializes it
afterwards:
1351/1357: SLIST_INSERT_HEAD(&proc->p_sigiolst / &pgrp->pg_sigiolst,
sigio, sio_pgsigio);
1353/1359: lwkt_reltoken(&proc->p_token / &pgrp->pg_token);
1362-1366: sigio->sio_pgid / sio_ucred / sio_ruid / sio_myref set
HERE, with no lock held.
So between the insert and the field initialization the sigio is on an owner list with sio_pgid == 0, sio_ucred == NULL, sio_ruid == 0, sio_myref == NULL. Concurrent teardown walkers consume list entries with no synchronization against this window:
exit1() -> funsetownlst(&p->p_sigiolst) (kern_exit.c:376)
pgdelete() -> funsetownlst(&pgrp->pg_sigiolst) (kern_proc.c:680)
funsetownlst: while ((sigio = SLIST_FIRST(list)) != NULL)
funsetown(sigio->sio_myref);
Consuming a half-born sigio gives, depending on exactly where in the
window the walker lands:
* funsetown(NULL): sigio = *sigiop with sigiop == NULL -> NULL-page
read fault;
* sio_pgid still 0 -> funsetown takes the pgid>0/proc branch (or the
union pointer is still NULL) and SLIST_REMOVE walks off the owner
list -> fault at NULL+8 (movq 0x8(%rdx),%rax reading
curelm->sio_pgsigio.sle_next with curelm == NULL);
* myref set but *sigiop not yet published -> funsetown returns
without unlinking -> funsetownlst spins forever (machine wedge
under token pressure, no console output).
Additionally the same walkers re-read the head without any lock, so a
freed-and-recycled sigio chunk (same address, same sio_myref slot,
different owner list) can be confused with the listed entry -- the
wrong-list SLIST_REMOVE walk-off again (this survived several fix
iterations; see below).
Reproduction (how/why)
Harness fsetown_race.c, unprivileged (uid 1001): parent forks short-
lived children (same session -- passes the session check in fsetown)
and hammers fcntl(sv0, F_SETOWN, child_pid) while each child exits.
Each child's exit1() walks its p_sigiolst exactly when the parent's
fsetown() is inside the insert->init window.
Run 1 (stock kernel): ssh session died mid-run, guest unresponsive, QEMU alive at ~3 vCPUs, zero serial output, no reboot -- total machine wedge (funsetownlst livelock variant).
Run 2 (stock kernel, fresh vm.sh reset with-src), serial console
(panic.txt):
Fatal user address access from kernel mode from fsetown_race at ffffffff8062fe83 Fatal trap 12: page fault while in kernel mode cpuid = 0; lapic id = 0 fault virtual address = 0x8 fault code = supervisor read data, page not present ... Stopped at funsetown+0x93: movq 0x8(%rdx),%rax db>
Fatal trap INSIDE funsetown() at a NULL-derived address, attributed to the unprivileged fsetown_race process; kernel halted in DDB; ssh dead. Reproduced within the first 500 rounds both times (per-round hit probability is high because the parent re-arms the window hundreds of times per child lifetime).
Fix validation (attempted, still failing under storm)
Shipped fix.diff (v4): * fsetown(): full field initialization BEFORE the owner-list insert (kills the half-born consumption -- the demonstrated crash class); * funsetown(): load+clear under sigio_token; owner-list removals moved into funsetown_free() with no token nesting (v1/v2 that held sigio_token across the owner token livelocked against exit1()'s p_token -> sigio_token order); kfree() under a second exclusive sigio_token hold; * funsetownlst(): head-read/check/clear under sigio_token, with a 1-tick tsleep retry for publication-in-flight entries (the tight retry loop starved the publisher of sigio_token and wedged the machine; tsleep releases lwkt tokens so the publisher can run). Iterations v1..v4 each eliminated the window analyzed at the time, but the 20000-round storm still crashes the patched kernel with a double-teardown of a recycled sigio chunk (funsetown_free+0x63 / +0x95 walk-off; instrumented console output in panic.dbg.txt shows two funsetown_free() calls interleaving on two adjacent chunks with inconsistent list heads). The residual defect is in the restructured replacement machinery itself, not in the original demonstrated window; a correct fix needs a per-sigio reference count or a dedicated sigio list lock rather than composing the existing tokens. fix_status = fix_failed (honest, fully documented).
Bottom line
Unprivileged, syscall-only, twice-reproduced kernel crash (fatal trap inside funsetown()/funsetown_free() plus a no-output machine-wedge variant) from a publish-then-initialize defect in the core descriptor code. Fix direction validated stepwise; final fix requires upstream design iteration.
Fix verification
not_testableConfirmed kernel references
Detail
Exploit chain
unprivileged fork storm + fcntl(F_SETOWN, child_pid) hammer -> child's exit1() funsetownlst() walk lands inside fsetown()'s insert->init window -> NULL-deref / wrong-list SLIST_REMOVE -> fatal kernel trap (demonstrated). Impact ceiling is local DoS (panic / permanent machine wedge); no memory-disclosure or control primitive identified for this path.
Evidence (decisive lines)
["panic.txt: 'Fatal trap 12 ... fault virtual address = 0x8 ... Stopped at funsetown+0x93' from unprivileged fsetown_race", "run.log: both stock runs (run 1 silent wedge, run 2 trap after 'round 0 (162 sets)')", 'panic.dbg.txt: instrumented-fix crash (funsetown_free+0x95) documenting the fix-failed iteration', 'VERDICT.md: root cause with exact line numbers, all three consumption variants, fix iteration history v1-v4']
PoC changes
Added
Verified recommended fix
Initialize every sigio field (including sio_proc/sio_pgrp and sio_myref) before the owner-list insertion, publish *sigiop only after the entry is fully linked, and give funsetownlst an atomic check-and-claim of the head entry under sigio_token (ideally replace the token composition with a per-sigio refcount).
Verdict
fsetown() (kern_descrip.c:1296-1380) inserts the zeroed sigio into proc->p_sigiolst / pgrp->pg_sigiolst at lines 1351/1357 and initializes sio_pgid/sio_ucred/sio_ruid/sio_myref only afterwards (1362-1366, after the owner token is dropped). Teardown walkers -- exit1()->funsetownlst() (kern_exit.c:376) and pgdelete()->funsetownlst() (kern_proc.c:680) -- consume the half-born entry: funsetown(NULL) NULL-page fault, or with sio_pgid still 0 the wrong owner branch makes SLIST_REMOVE walk off the list (fault at NULL+8). Unprivileged harness (fork short-lived same-session children + hammer F_SETOWN(child_pid)) reproduced twice on the stock kernel: run 1 hard-wedged the machine (funsetownlst livelock, no console output); run 2 gave 'Fatal trap 12 ... fault virtual address = 0x8 ... Stopped at funsetown+0x93: movq 0x8(%rdx),%rax' attributed to the unprivileged fsetown_race process, kernel halted in DDB.
No comments yet.