funsetown() KKASSERT race: pre-token *sigiop load dereferenced after concurrent fsetown() frees and slab-recycles the sigio β unprivileged kernel panic on INVARIANTS (stock) builds via pipe F_SETOWN/close/exit (DF-2682 sibling)
| Field | Value |
|---|---|
| ID | DF-2751 |
| 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-416 / CWE-362 |
| File | sys/kern/sys_pipe.c |
| Lines | 1023-1032, 1104 (sink kern_descrip.c:1245-1247) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
funsetown() loads *sigiop BEFORE acquiring sigio_token and then
dereferences that stale pointer in KKASSERT(sigiop == sigio->sio_myref)
after taking the token. Callers arriving without the token β fsetown
(pgid==0) dispatched through pipe_ioctl's FIOSETOWN, pipe_close, and
owner-exit funsetownlst β can park at lwkt_gettoken(&sigio_token)
holding a sigio pointer that a concurrent fsetown() on the same sigiop
then kfrees (its kmalloc happens pre-token, M_ZERO, recycling the
48-byte chunk for a different pipebuf so sio_myref mismatches). On
INVARIANTS kernels β the stock X86_64_GENERIC config β the assertion
dereferences freed memory and fails: panic. On production kernels the
KKASSERT is not evaluated, but the post-assert reload then frees the
sigio another thread just installed, silently reverting a completed
F_SETOWN. Distinct from DF-2682: different sink (funsetown's own
KKASSERT vs lockless pgsigio readers), different racers, different
impact.
Threat model & preconditions
Any local unprivileged user (F_SETOWN to own same-session pid/pgid needs no privilege) can panic a stock INVARIANTS kernel with a minutes-scale fcntl storm; on production builds the same race silently uninstalls a concurrently-completed F_SETOWN. Not a write primitive (read-only assert on debug builds).
Proof of concept
VERIFIED on the stock guest (findings/poc/DF-2751/funsetown_race3.c):
O_ASYNC pgrp writers hold sigio_token across pgsigio (parking
victims), 2 victim threads loop fcntl(A, F_SETOWN, 0), 8 killer
threads alternate F_SETOWN on two pipes so the recycling kmalloc
stamps a foreign sio_myref β panic: assertion "sigiop ==
sigio->sio_myref" failed in funsetown at kern_descrip.c:1247 with
stack funsetownβfsetownβpipe_ioctlβkern_fcntlβsys_fcntl, within 4
minutes. Fix (acquire sigio_token BEFORE loading *sigiop) validated by
full kernel rebuild + identical PoC rerun: panic gone, 360s survived
at 98.8M victim iterations.
Recommended fix
--- a/sys/kern/kern_descrip.c
+++ b/sys/kern/kern_descrip.c
@@ -1243,12 +1243,12 @@ funsetown(struct sigio **sigiop)
struct pgrp *pgrp;
struct proc *p;
struct sigio *sigio;
- if ((sigio = *sigiop) != NULL) {
- lwkt_gettoken(&sigio_token); /* protect sigio */
+ sigio = NULL;
+ lwkt_gettoken(&sigio_token); /* protect sigio */
+ if ((sigio = *sigiop) != NULL) {
KKASSERT(sigiop == sigio->sio_myref);
- sigio = *sigiop;
*sigiop = NULL;
- lwkt_reltoken(&sigio_token);
}
+ lwkt_reltoken(&sigio_token);
(also fixes the production-build silent F_SETOWN reversion)
Timeline
- 2026-08-30 Discovered during pass-2 audit of sys_pipe.c (GLM 5.3); unpriv stock-kernel panic reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2751 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| funsetown_race.c | β | 4.6 KB | view raw | |
| funsetown_race2.c | β | 4.3 KB | view raw | |
| funsetown_race3.c | β | 3.3 KB | view raw | |
| build.sh | β | 128 B | view raw | |
| run.sh | β | 443 B | view raw | |
| build.log | β | 353 B | view raw | |
| run.log | β | 970 B | view raw | |
| panic.txt | β | 441 B | view raw | |
| env.txt | β | 415 B | view raw | |
| fix.diff | β | 551 B | view raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| README.md | β | 2.9 KB | β raw |
DF-2751 β funsetown() KKASSERT race: stale pre-token *sigiop load
dereferenced (and cross-installed sigio silently freed) after a
concurrent fsetown() frees + recycles the struct sigio
Sibling of DF-2682 (same function family, different sink and racers).
Reached from sys/kern/sys_pipe.c endpoints: pipe_ioctl() FIOSETOWN /
TIOCSPGRP (sys_pipe.c:1023-1032) and pipe_close() -> funsetown
(sys_pipe.c:1104).
Root cause (sys/kern/kern_descrip.c:1245-1251):
if ((sigio = *sigiop) != NULL) { /* load, NO token held */
lwkt_gettoken(&sigio_token);
KKASSERT(sigiop == sigio->sio_myref); /* derefs stale load */
sigio = *sigiop;
*sigiop = NULL;
lwkt_reltoken(&sigio_token);
}
Callers that arrive without the token: fsetown(pgid==0)
(kern_descrip.c:1298-1301, i.e. fcntl(fd, F_SETOWN, 0) -- unprivileged,
same-session pid/pgid only), pipe_close (sys_pipe.c:1104), and
owner-exit funsetownlst (kern_descrip.c:1281-1287). While such a caller
is parked at lwkt_gettoken(&sigio_token), a concurrent fsetown() on the
same sigiop kfrees the loaded sigio X and installs a fresh X'; a third
thread's fsetown kmalloc (M_ZERO, done before taking the token) can
recycle X's 48-byte chunk for a different pipebuf (sio_myref mismatch)
before the parked thread resumes. Results:
- INVARIANTS kernels (options INVARIANTS is in the stock X86_64_GENERIC config): KKASSERT dereferences freed memory; recycled chunk -> assertion failure -> panic. Demonstrated.
- non-INVARIANTS kernels: KKASSERT is not evaluated; the post-assert reload then picks up the concurrently-installed X' and this caller frees it, silently reverting a completed F_SETOWN (functional race, no memory unsafety at this line).
Severity: Low/Medium (local unprivileged DoS on INVARIANTS builds, including the stock config in this tree; silent F_SETOWN reversion on production builds).
Files
funsetown_race.c v1: 4-thread F_SETOWN(0)/F_SETOWN(pid) churn -- no hit in 300s funsetown_race2.c v2: + pgsigio token-hold amplification -- no hit in 300s funsetown_race3.c v3: + cross-pipe (A/D) chunk recycling -- PANIC in <=4 min build.sh / run.sh exact commands (v3 is the decisive build) panic.txt serial-console panic (assertion at kern_descrip.c:1247) fix.diff minimal fix: load *sigiop under sigio_token VERDICT.md full narrative incl. fix validation
Reproduce (guest, unprivileged)
cc -O2 -pthread -o fr3 funsetown_race3.c ./fr3 300 # kernel panics within minutes
Expected (vulnerable): panic: assertion "sigiop == sigio->sio_myref" failed in funsetown at /usr/src/sys/kern/kern_descrip.c:1247 ... fsetown() at fsetown+0xdb ... pipe_ioctl() at pipe_ioctl+0x11f ... kern_fcntl() / sys_fcntl() guest sits in DDB (db>), ssh dead.
Expected (fixed): "survived: victim=... killer=... writes=..." then exit 2.
DF-2751 VERDICT β funsetown() KKASSERT stale-load race (DF-2682 sibling)
Finding
funsetown() loads *sigiop before acquiring sigio_token
(kern_descrip.c:1245) and dereferences that stale pointer in
KKASSERT(sigiop == sigio->sio_myref) (kern_descrip.c:1247) after
acquiring the token. Three unprivileged caller classes reach that
window without the token: fsetown(pgid==0) (kern_descrip.c:1298-1301,
i.e. fcntl(fd, F_SETOWN, 0)), pipe_close() (sys/kern/sys_pipe.c:1104),
and owner-exit funsetownlst() (kern_descrip.c:1281-1287).
While such a caller V is parked at lwkt_gettoken(&sigio_token), a
concurrent fsetown() on the same sigiop (running under the token)
kfrees the sigio V loaded, and any other thread's fsetown kmalloc β
which happens before taking the token and uses M_ZERO β can recycle
the freed 48-byte chunk for a different pipebuf, stamping
sio_myref = &other_pipebuf.sigio. When V resumes:
- INVARIANTS kernels β
options INVARIANTSis in the stock X86_64_GENERIC config, so this includes default builds of this tree β execute the KKASSERT against freed (and typically recycled) memory: assertion failure β panic. - non-INVARIANTS kernels β KKASSERT is compiled out (expression not
evaluated); the post-assert reload
sigio = *sigiopthen picks up the sigio the winner just installed and V kfrees it, silently reverting a completedF_SETOWN(functional race; no memory unsafety at this line).
Distinct from DF-2682: different sink (funsetown's own KKASSERT vs lockless pgsigio() readers), different racers (two funsetown/fsetown calls on one sigiop vs funsetown vs pgsigio user), different impact (INVARIANTS-only deref vs always-on UAF read chain). Same family.
Reproduction (unprivileged, pipes only, guest stock kernel)
Three harness iterations (all under uid 1001 maxx):
- v1
funsetown_race.cβ 4 threads, F_SETOWN(0)/F_SETOWN(pid) churn on 3 pipes. 300 s: no panic (210M victim iterations). The interleave requires the killer to take the token ahead of the parked victim and the freed chunk to be recycled before the victim resumes β too tight. - v2
funsetown_race2.cβ added O_ASYNC pgrp pipe writers whosepipewakeup()->pgsigio()(sys_pipe.c:211-215) holdssigio_tokenacrosspgref+lockmgr+ksignal, parking victims reliably. 300 s: no panic. Diagnosis: the recycler usually re-installs the chunk for the same pipebuf, sosio_myrefstill matches β assertion passes. - v3
funsetown_race3.cβ killers alternate two pipes (A, D): thefsetown(D)kmalloc pops A's freed chunk per-CPU LIFO and stampssio_myref = &pbD.sigio, guaranteeing mismatch for a victim parked on&pbA.sigio. Panic in β€ 4 minutes:
panic: assertion "sigiop == sigio->sio_myref" failed in funsetown at /usr/src/sys/kern/kern_descrip.c:1247
cpuid = 5
funsetown() at funsetown+0x159
funsetown() at funsetown+0x159
fsetown() at fsetown+0xdb
pipe_ioctl() at pipe_ioctl+0x11f
kern_fcntl() at kern_fcntl+0x301
sys_fcntl() at sys_fcntl+0x6a
Debugger("panic") β db>
The pipe_ioctl frame is sys/kern/sys_pipe.c's FIOSETOWN handler β
the pipe file-op path this audit was assigned. Full serial-console
capture in panic.txt, run transcript in run.log.
Impact classification: local unprivileged kernel panic β dos on INVARIANTS builds (stock config in this tree). Not a write primitive: the freed read feeds only an assertion comparison; on production (non-INVARIANTS) builds the line vanishes. No escalation attempted beyond DoS β and none available from this line alone.
Fix
fix.diff β load *sigiop under sigio_token (hoist the token
acquisition above the initial read). Then the KKASSERT deref cannot
observe a concurrently-freed sigio: any free of the loaded value must
clear *sigiop first, under the same token. This also fixes the
production-build silent F_SETOWN reversion (racing funsetowns fully
serialize: first clears+returns, second reloads NULL). No lock-order
change: the p_token/pg_token list-removal section remains outside
sigio_token, exactly as before; recursive acquisition from fsetown's
while (*sigiop) funsetown(sigiop) loop is supported by LWKT.
Cost: one extra (uncontended, recursive) token acquire per funsetown on an empty ref β funsetown runs on close/exit/F_SETOWN paths, not hot I/O.
Fix validation
- Baseline (stock kernel): panic reproduced, above (β€ 4 minutes, v3).
- Patched:
vm.sh reset with-src, applied fix.diff in guest /usr/src,make -j6 nativekernel(rc=0) +make installkernel(rc=0), rebooted, re-ran the identical v3 PoC binary: - 20 s smoke: survived (victim=6.06M killer=13.6M writes=16.7M).
- 360 s decisive: survived (victim=98,870,384 killer=237,281,500
writes=250,070,255), guest remained up (
vm.sh statusβ up). The previously-observed panic is GONE; fix_status = fixed (fix_baseline_reproduced=1, fix_patched_reproduced=0). Logs:fix_run.log,fix_build.log.
Guest was reset to the clean-source snapshot (vm.sh reset with-src)
after validation, per protocol.
Fix verification
fixedApplied fix.diff in guest /usr/src, make -j6 nativekernel (rc=0) + make installkernel (rc=0), rebooted. Identical PoC binary: 20s smoke survived; decisive 360s run survived with 98.8M victim / 237M killer / 250M write iterations and guest up, versus baseline panic within 4 minutes. Previously-observed panic is gone; no regression observed. Guest reset to clean-source snapshot afterwards.
fix.diff, fix_build.log, fix_run.log
Confirmed kernel references
Detail
Evidence (decisive lines)
panic.txt (assertion at kern_descrip.c:1247 with pipe_ioctl frame), run.log (3 generations incl. 240s panic run), funsetown_race3.c (decisive harness: 2 victims F_SETOWN(0) + 8 killers alternating F_SETOWN(pid) across pipes A/D + 2 O_ASYNC pgrp pipe writers), fix_run.log (patched kernel: survived 360s at 98.8M victim iterations), fix_build.log, VERDICT.md
PoC changes
Orchestrator seed assumed socket-style readers (DF-2682 shape); this sibling needed a purpose-built harness: v1 basic F_SETOWN churn (no hit), v2 added pgrp-O_ASYNC pipe writers whose pipewakeup->pgsigio holds sigio_token across pgref+lockmgr+ksignal (parking victims), v3 made killers alternate two pipes so the recycling fsetown kmalloc stamps a foreign sio_myref (deterministic assert mismatch). All fcntl-based, unprivileged, same-session pid/pgid.
Verified recommended fix
funsetown(): acquire sigio_token BEFORE loading *sigiop so the KKASSERT (and the subsequent clear) operate on a value that cannot be freed concurrently
Verdict
Unprivileged kernel panic reproduced on the stock INVARIANTS kernel: funsetown() loads sigiop before acquiring sigio_token (kern_descrip.c:1245) and dereferences that stale pointer in KKASSERT at kern_descrip.c:1247; while a caller arriving without the token (fcntl(F_SETOWN,0) via pipe_ioctl, pipe_close, owner-exit funsetownlst) is parked at the token, a concurrent fsetown() on the same sigiop kfrees the loaded sigio and a third thread's fsetown kmalloc recycles the 48-byte chunk for a different pipebuf, so the KKASSERT compares against recycled memory and fails. Panic stack: funsetown <- fsetown <- pipe_ioctl <- kern_fcntl <- sys_fcntl. Two earlier harness generations (300s each) proved the interleave needed pgsigio token-hold amplification and cross-pipe chunk recycling to hit; v3 panicked within 4 minutes. On non-INVARIANTS builds the KKASSERT is not evaluated and the same interleave instead silently frees a concurrently-installed sigio (completed F_SETOWN reverted). Not a write primitive; impact ceiling is local DoS on INVARIANTS builds (options INVARIANTS is in the stock X86_64_GENERIC config of this tree). fix.diff (load sigiop under the token) validated by full kernel rebuild: baseline panic gone, 360s full-churn rerun survived.
No comments yet.