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

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.

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

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2751 Β· 12 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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 INVARIANTS is 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 = *sigiop then picks up the sigio the winner just installed and V kfrees it, silently reverting a completed F_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 whose pipewakeup()->pgsigio() (sys_pipe.c:211-215) holds sigio_token across pgref+lockmgr+ksignal, parking victims reliably. 300 s: no panic. Diagnosis: the recycler usually re-installs the chunk for the same pipebuf, so sio_myref still matches β†’ assertion passes.
  • v3 funsetown_race3.c β€” killers alternate two pipes (A, D): the fsetown(D) kmalloc pops A's freed chunk per-CPU LIFO and stamps sio_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

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (rebuilt with fix.diff; version string unchanged by rebuild - identity confirmed by /boot/kernel/kernel mtime Aug 31 19:02 and the behavioral delta)

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.