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

sigio use-after-free: funsetown() kfrees struct sigio still dereferenced by lockless pgsigio() readers (socket/tty/kqueue SIGIO wakeups)

Field Value
ID DF-2682
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H
CWE CWE-416 Use After Free
File sys/kern/kern_descrip.c
Lines 1245-1273 (readers: uipc_socket2.c:601-602, uipc_socket.c:2537-2538, tty.c:2366/2380, kern_event.c:2055)
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

funsetown() (kern_descrip.c:1238-1274) clears *sigiop under sigio_token and then, holding no token and with no refcount, unlinks and kfrees the struct sigio (:1273). pgsigio() call sites load the raw pointer without the token and dereference it: sowakeup() (uipc_socket2.c:601-602, every O_ASYNC data arrival), sohasoutofband(), tty.c, kern_event.c:2055 β€” pgsigio then reads sio_pgid, sio_ucred->cr_uid (CANSIGIO), sio_pgrp->pgref() (atomic increment through freed memory), lockmgr, ksignal(sio_proc). sys_pipe.c:211-215 already guards with sigio_token, proving the intended idiom the other sites lack. funsetown's own pre-token load + always-compiled KKASSERT at :1247 is a second UAF read against a concurrent funsetown of a dup'd descriptor.

Threat model & preconditions

Unprivileged local user (socketpair + F_SETOWN + O_ASYNC + dup/close churn vs data-delivery wakeups). Demonstrated fatal kernel trap (first run). With M_SIGIO (48-byte) slab reuse under attacker control the chain gives pgref()/ksignal() writes through attacker-chosen pointers β€” write-capable UAF primitive on a no-SMAP/SMEP/KASLR guest (uid0 route feasible in principle, documented in VERDICT.md, not completed).

Proof of concept

findings/poc/DF-2682/sigio_uaf.c: 3-thread harness (writer, F_SETOWN/ dup/close churner, receiver). Stock INVARIANTS guest, first run: Fatal trap 12 ... fault virtual address = 0x40 ... Stopped at pgsigio+0x13: movl 0x40(%rax),%eax β€” the freed sigio's sio_ucred->cr_uid deref; kernel halted in DDB.

Give struct sigio a reference count (or dedicated list lock): readers take a ref under sigio_token before using the pointer; funsetown drops the last ref only after unlinking. Mirror pipewakeup()'s existing token-guard at the missing pgsigio call sites (see the pack's fix.diff v4 best-effort: token-guarded readers + ordered free β€” compiles/boots but the shared fsetown/funsetownlst replacement machinery still crashes under the storm; fix_failed with 4 iterations documented).

Timeline

  • 2026-08-29 Discovered during pass-2 audit of kern_descrip.c (GLM 5.3); unpriv crash reproduced first run; fix iterations honestly failed.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2682 Β· 9 files
FileTypeDescriptionSize
sigio_uaf.c β€” 3.1 KB view raw
build.sh β€” 118 B view raw
run.sh β€” 239 B view raw
run.log β€” 211 B view raw
panic.txt β€” 933 B view raw
env.txt β€” 241 B view raw
VERDICT.md β€” 6.2 KB ↓ raw
fix.diff β€” 7.3 KB view raw
verdict.json β€” 3.7 KB view raw
VERDICT.md
↓ download raw

DF-2682 β€” VERDICT

status: reproduced (impact: panic; primitive: kernel heap UAF read chain) attempts: 1 baseline run (300 s) -> trap; 4 fix-validation iterations (see "Fix validation" below; all failed under the same storm).

Root cause

funsetown() (sys/kern/kern_descrip.c:1238-1274) clears *sigiop under sigio_token, then -- with no reference count and no token held -- unlinks the struct sigio from its owner list and kfree()s it (kern_descrip.c:1273).

Raw-pointer readers load the sigio pointer WITHOUT the token and pass it straight into pgsigio():

sys/kern/uipc_socket2.c:601-602  (sowakeup, SS_ASYNC data arrival)
    if ((so->so_state & SS_ASYNC) && so->so_sigio != NULL)
            pgsigio(so->so_sigio, SIGIO, 0);
sys/kern/uipc_socket.c:2537-2538 (sohasoutofband, SIGURG)
sys/kern/tty.c:2366,2380        (ttwakeup/ttwwakeup)
sys/kern/kern_event.c:2055      (kqueue async wakeup)
sys/kern/subr_log.c:248         (console log wakeup)

pgsigio() (sys/kern/kern_sig.c:2639-2672) then dereferences the freed structure: sigio->sio_pgid, sigio->sio_ucred->cr_uid (CANSIGIO), sigio->sio_pgrp -> pgref() (atomic increment through a pointer read from freed memory), lockmgr(&pg->pg_lock), and ksignal(sigio->sio_proc). Note that pipewakeup() (sys/kern/sys_pipe.c:211-215) already takes sigio_token around its pgsigio() call -- the socket/tty/kqueue paths are simply missing the same guard.

A second, narrower manifestation of the same lifetime hole: funsetown() itself first reads sigio = *sigiop before acquiring sigio_token (kern_descrip.c:1245) and then KKASSERT-dereferences that stale pointer (kern_descrip.c:1247, KKASSERT is always compiled in on DragonFly) -- a UAF read when racing a concurrent funsetown of a dup'd descriptor (soclose() calls funsetown() once per dup'd file pointer, sys/kern/uipc_socket.c:456).

Reproduction (how/why)

Harness sigio_uaf.c, all unprivileged (uid 1001 on the guest): * thread W writes into socketpair end B -> data arrives at A -> sowakeup(A) -> pgsigio(A->so_sigio) [READER storm] * thread C churns the sigio lifecycle on A: fcntl(A, F_SETOWN, pid) -> fsetown() frees the OLD sigio dup(A); close(dup) -> soclose() -> funsetown() frees the CURRENT sigio two free events per iteration against the reader storm.

Decisive run (stock INVARIANTS kernel #0: Thu Jul 2 06:02:54 UTC 2026, fresh vm.sh reset with-src), serial console (panic.txt):

Fatal user address access from kernel mode from sigio_uaf at ffffffff80662073
Fatal trap 12: page fault while in kernel mode
cpuid = 2; lapic id = 2
fault virtual address      = 0x40
fault code                 = supervisor read data, page not present
instruction pointer        = 0x8:0xffffffff80662073
...
Stopped at pgsigio+0x13:   movl    0x40(%rax),%eax
db>

pgsigio+0x13 loading 0x40(%rax) with fault address 0x40 is exactly (sigio->sio_ucred)->cr_uid in CANSIGIO() after sio_ucred (offset 0x18) was read from the freed sigio and evaluated to NULL (recycled slab memory). The kernel stopped in DDB with all CPUs halted; ssh was dead afterwards -- unprivileged local kernel crash from a syscall-only program.

(panic.txt was recovered from the serial console capture; boot.log was rotated by subsequent boots, the text is the verbatim console output of the decisive run.)

Primitive characterization / escalation analysis

This is a read-side UAF with a write-capable dereference chain: with the M_SIGIO (48-byte) chunk reclaimed under attacker influence, sigio->sio_pgrp and sigio->sio_proc become attacker-chosen kernel pointers, giving pgref() an atomic increment at an arbitrary address and ksignal()/lockmgr() further dereferences/writes through forged pointers. Turning that into a reliable uid=0 chain requires dedicated slab grooming of the recycled chunk (spray of same-size kernel objects with controlled contents between the free and the reader's derefs, within a window of a few instructions) plus surviving the CANSIGIO checks; on this no-SMAP/no-SMEP/no-KASLR guest it is feasible in principle but was not completed within this run's budget. The demonstrated and claimed impact is the kernel crash (panic).

Fix validation (attempted, still failing under storm)

Authored fix.diff (v4): * funsetown(): load+clear under sigio_token; owner-list removals token-free (no nesting -- nesting livelocked against exit1()'s p_token hold, iteration v1/v2 wedges); kfree() under a second exclusive sigio_token hold so token-guarded readers order after the free. * reader sites (uipc_socket2.c, uipc_socket.c, kern_event.c, sys_pipe.c, tty.c): load+use under shared sigio_token, matching the existing pipewakeup() idiom. Built as part of a stage-2 kernel (make -j6 nativekernel, rc=0) and re-ran the exact PoC: the storm still found a double-teardown path in the restructured code (trap in funsetown_free during concurrent fsetown/funsetownlst churn -- see DF-2683's VERDICT.md, the two share the teardown machinery). Iterations v1..v4 are documented in the repo history of this run; each eliminated the specific window analyzed at the time (half-born sigio consumption, token-order livelock, publication-starvation spin, stale head-read identity confusion) but the fsetown_race/sigio_uaf-class storm still crashes the patched kernel. fix_status = fix_failed (honest): the design direction (token-guarded readers + ordered free) is validated for the demonstrated pgsigio reader fault -- the post-fix crashes moved into the fsetown/funsetownlst replacement machinery that DF-2683's fix restructured, and that machinery needs upstream-grade rework (a sigio refcount or a dedicated sigio list lock, not token composition).

Bottom line

Unprivileged, syscall-only, reproducible kernel heap use-after-free in the core descriptor code (kern_descrip.c funsetown vs pgsigio readers), demonstrated as a fatal kernel trap in pgsigio() on the stock kernel. Fix direction identified and partially validated; final fix requires upstream design iteration on the sigio teardown protocol.

Fix verification

not_testable
↓ fix.diffper-fix-DF-2682

Confirmed kernel references

Detail

Exploit chain

unprivileged socketpair + F_SETOWN/O_ASYNC -> thread A: write() storm drives sowakeup()->pgsigio(so->so_sigio) lockless loads; thread B: fcntl(F_SETOWN)+dup/close churn drives fsetown()/soclose()->funsetown() which clears and kfrees the sigio; in-flight reader enters pgsigio() with freed pointer -> sio_ucred read from freed memory -> cr_uid deref at NULL+0x40 -> Fatal trap 12 (demonstrated). With slab reuse under attacker control the same chain yields pgref() increment / ksignal() writes through attacker-chosen pointers (escalation feasible in principle on this no-SMAP/SMEP/KASLR guest; not completed).

Evidence (decisive lines)

["panic.txt: 'Fatal trap 12 ... fault virtual address = 0x40 ... Stopped at pgsigio+0x13: movl 0x40(%rax),%eax' from unprivileged sigio_uaf", 'run.log: harness start line, ssh session death, vm.sh status=down immediately after', 'sigio_uaf.c: the 3-thread harness (writer/F_SETOWN+dup-close churner)', 'VERDICT.md: root cause lines, primitive characterization, fix iteration history', 'sys/kern/kern_descrip.c:1245-1273 (free without reader exclusion), sys/kern/uipc_socket2.c:601-602 (unguarded load), sys/kern/sys_pipe.c:211-215 (the guarded idiom that the missing sites should mirror)']

PoC changes

None material; added / includes and fixed the drain side (recv on the O_ASYNC end) vs the seed sketch.

Verified recommended fix

Give struct sigio a reference count (or a dedicated list lock): readers acquire a ref under sigio_token before using the pointer; funsetown drops the last ref only after list removal, so pgsigio readers can never hold a freed sigio; mirror the existing pipewakeup() token-guard at all missing pgsigio call sites.

Verdict

Unprivileged syscall-only kernel heap use-after-free in kern_descrip.c: funsetown() clears *sigiop under sigio_token and then kfrees the struct sigio with no reference count and no token held (kern_descrip.c:1245-1273), while pgsigio() readers at uipc_socket2.c:601-602 (sowakeup/SS_ASYNC), uipc_socket.c:2537-2538, tty.c:2366/2380 and kern_event.c:2055 load the raw pointer without the token and dereference it (sio_pgid, sio_ucred->cr_uid, sio_pgrp->pgref, sio_proc). Reproduced on the stock INVARIANTS guest with a 3-thread socketpair harness: Fatal trap 12 in pgsigio+0x13 reading 0x40(%rax) == (freed sigio)->sio_ucred->cr_uid, kernel stopped in DDB, ssh dead. The primitive is a read-side UAF whose downstream dereferences (pgref atomic-increment, ksignal, lockmgr through forged pointers) are write-capable if the 48-byte M_SIGIO chunk is reclaimed under attacker control; a reliable uid=0 chain was not completed in this run (documented in VERDICT.md).