/dev/devctl FIOASYNC stores unreferenced curproc in devsoftc.async_proc; devclose never clears it -> ksignal() on freed struct proc (UAF write)
| Field | Value |
|---|---|
| ID | DF-2680 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:H |
| CWE | CWE-416 UAF, CWE-772 Missing Protection of Alternate Path |
| File | sys/kern/subr_bus.c |
| Lines | 412-417 (store), 352-361 (missing clear), 521-523 (ksignal) |
| 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
devioctl() FIOASYNC stores raw curproc into devsoftc.async_proc with no
reference (subr_bus.c:412-417). When the owner exits, devclose() resets
inuse but leaves async_proc dangling (:352-361); the chunk is freed at
reap (kern_exit.c:1336). Every later devctl event runs
ksignal(stale_proc, SIGIO) (:521-523): lwpsignal() executes
PHOLD(p), lwkt_gettoken(&p->p_token) and the lwp walk on
freed/reused memory.
Threat model & preconditions
Root-gated setup (devctl is 0600 + SYSCAP_RESTRICTEDROOT) but the corruption persists and triggers after the owner exits: demonstrated cross-process SIGIO delivery to an unrelated process that recycled the freed proc chunk (5/5 rounds); with non-proc reuse, PHOLD/token writes corrupt foreign kernel objects.
Proof of concept
findings/poc/DF-2680/run_seq.sh: victim (root) opens /dev/devctl,
FIOASYNC=1, exits; catcher processes (never open devctl) recycle the
freed struct proc; kldload of the DF-2679 churn module fires 1024
devctl events β GOT_SIGIO pid=<victim+1> (never opened devctl) in
5/5 rounds; live-path control confirms the FIOASYNCβksignal path.
Recommended fix
Clear async_proc under devsoftc.lock in devclose() (two-line diff in findings/poc/DF-2680/fix.diff); full fix holds a PHOLD/PRELE reference in devioctl or converts to sigio-list semantics.
Timeline
- 2026-08-29 Discovered during pass-2 audit of subr_bus.c (GLM 5.3); reproduced 5/5 same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2680 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| victim.c | β | 785 B | view raw | |
| victim2.c | β | 774 B | view raw | |
| catcher.c | β | 757 B | view raw | |
| run_seq.sh | β | 1.3 KB | view raw | |
| run.log | β | 1.5 KB | view raw | |
| env.txt | β | 532 B | view raw | |
| fix.diff | β | 470 B | view raw | |
| VERDICT.md | β | 2.6 KB | β raw | |
| verdict.json | β | 3.2 KB | view raw |
DF-2680 β VERDICT
status: reproduced, impact: dos (kernel UAF write; benign-but- visible manifestation), confidence: certain.
Reproduced how
-
Control (live path) β
victim2(root) opens/dev/devctl, setsFIOASYNC=1with a SIGIO handler installed, and stays alive. Loading the dfrace churn module (512 device attaches βdevaddedβdevaddqβdevctl_queue_data) delivers SIGIO:victim2: GOT_SIGIO (live path works) pid=1961. (Necessary because SIGIO's default disposition in DragonFly is ignore β a handler is required to observe delivery.) -
Stale path β
victim(root) setsFIOASYNC=1and exits without clearing it. Itsstruct procis reaped and freed (kern_exit.c:1336).devclose()leavesdevsoftc.async_procpointing at the freed chunk. The next fork β acatcherprocess that never opened/dev/devctlβ recycles the chunk. Firing devctl events then executesksignal(async_proc, SIGIO)on the recycled memory and the catcher receives SIGIO:
victim: pid=1993 enabled FIOASYNC on /dev/devctl, exiting
GOT_SIGIO pid=1994 (never opened devctl)
5/5 rounds reproduced (run.log).
Why this is the bug and not something else
devopen()clearsasync_procon the next open, so during the window the pointer is exactly the exited victim's proc.- The catcher demonstrably never opened
/dev/devctl; SIGIO can only have arrived viadevctl_queue_data()'sksignal(devsoftc.async_proc). lwpsignal()wrotePHOLD(p)/ token state into the recycled chunk before delivering β i.e. kernel writes through a dangling pointer occurred; the signal delivery is merely the visible edge.
Impact ceiling
If the freed chunk is recycled by a non-proc kernel allocation (any
M_PROC-bucket-sized object) instead of a new process, PHOLD(p) (a
refcount increment at the chunk's p_lock offset) and
lwkt_gettoken(&p->p_token) corrupt that object. No escalation chain
was developed because the setup itself requires root (devopen is
SYSCAP_RESTRICTEDROOT-gated and the node is 0600), so the finding is
filed Low severity; the demonstrated runtime effect is cross-process
signal delivery through freed kernel memory (a correctness/security
defect any devd-adjacent root daemon can trip over by exiting while
FIOASYNC is set).
Fix validation
Not rebuilt for this Low-severity finding (fix is a two-line close-path
clear + optional PHOLD/PRELE); fix.diff provided for the record and
reasoned through: clearing async_proc under devsoftc.lock in
devclose removes the dangling target; devopen already clears it on
reuse.
Fix verification
not_testableConfirmed kernel references
Detail
Exploit chain
root opens /dev/devctl + FIOASYNC -> exits without clearing -> async_proc dangles -> any later devctl event ksignal()s freed memory -> PHOLD/token writes into whatever recycled the chunk (cross-process SIGIO demonstrated; foreign-object corruption if recycled by non-proc data). No uid0 chain (root-gated setup).
Evidence (decisive lines)
run.log (control GOT_SIGIO pid=1961 live path; stale rounds GOT_SIGIO pid=1994/2032/2048/2075/2091 'never opened devctl'); victim.c/victim2.c/catcher.c; VERDICT.md; fix.diff
PoC changes
PoC authored fresh. Iterations: initial runs showed no SIGIO because (a) SIGIO's default disposition is ignore, so both the control and the catcher need handlers installed, and (b) the DF-2679 churn module's children never attached, so no devadded/devremoved devctl events fired at all - fixed by adding a real driver to the churn module; devd must be stopped because /dev/devctl allows a single reader.
Verified recommended fix
devclose(): devsoftc.async_proc = NULL under devsoftc.lock (and hold a PHOLD/PRELE reference on the stored proc in devioctl if SIGIO-after-exit is ever desired).
Verdict
Use-after-free of devsoftc.async_proc reproduced on the stock kernel with a visible, benign manifestation: a root process opened /dev/devctl, issued FIOASYNC=1 (devioctl stores raw curproc, subr_bus.c:412-417, no reference) and exited; devclose (subr_bus.c:352-361) left async_proc dangling after the struct proc was reaped and kfree'd (kern_exit.c:1336). The next fork recycled the freed chunk, and every subsequent devctl event (device attach/detach -> devctl_queue_data, subr_bus.c:521-523) executed ksignal() on the recycled memory: lwpsignal() ran PHOLD(p) and lwkt_gettoken(&p->p_token) (kernel writes through the dangling pointer) and delivered SIGIO to a process that never opened /dev/devctl. 5/5 rounds reproduced (catcher pid = victim pid + 1 each time); a live-path control (victim stays alive, handler installed) confirmed the FIOASYNC->ksignal delivery path first. Trigger requires root to open the 0600 RESTRICTEDROOT-gated device, hence Low severity despite the kernel-memory-write primitive.
No comments yet.