β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2683

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2683 Β· 11 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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_testable
↓ fix.diffper-fix-DF-2683

Confirmed 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 include and a per-500-round progress print vs the seed sketch; core choreography unchanged.

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.