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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2682 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_testableConfirmed 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
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).
No comments yet.