logtimeout() use-after-free read of sc_sigio: pgsigio() on a struct sigio freed by a concurrent FIOSETOWN ioctl
| Field | Value |
|---|---|
| ID | DF-2953 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-362 β CWE-416 / CWE-476 |
| File | sys/kern/subr_log.c |
| Lines | 247-248, 292-301 (free side: kern_descrip.c:1239-1272) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The periodic softclock callout logtimeout() loads logsoftc.sc_sigio and calls pgsigio() on the raw pointer with no sigio_token and no reference. A concurrent FIOSETOWN/TIOCSPGRP ioctl on the open klog fd calls fsetown(), which under sigio_token frees the old sigio via funsetown(). pgsigio() then dereferences the freed chunk: pgref of the NULLed sio_pgrp and CANSIGIO's (sio_ucred)->cr_uid on the NULLed ucred. VERIFIED on the guest: Fatal trap 12, fault address 0x40, instruction pointer pgsigio+0xcd, current process Idle (softclock thread), within one second of PoC launch β reproduced twice. Every other klog-path accessor (fsetown/funsetown/fgetown) holds sigio_token; logtimeout is the only unlocked reader. logclose's funsetown is NOT a racer β callout_terminate waits for the in-progress callback. Host root gets a reliable local DoS; combined with DF-0190 (no jail check), a jailed root on a host whose devfs ruleset exposes /dev/klog can panic the HOST kernel from inside the jail. Read-side UAF on a dedicated M_SIGIO zone with NULLed freed fields β practical primitive is deterministic panic, not a write; uid0 not pursued (trigger already privileged). FreeBSD fixed this class by passing struct sigio ** to pgsigio with internal locking. Fix validated: driver-local struct lock interlocking every sc_sigio access β baseline panics <1s; fixed kernel survived identical 300s race soak. Do NOT use lwkt tokens here (empirically refuted: tokens drop when the holder blocks β the same broken pattern exists at sys_pipe.c:211-214, noted as a latent instance for sys_pipe.c).
Timeline
- 2026-09-02 Discovered during pass-2 audit of subr_log.c (GLM 5.3); panic reproduced <1s twice + fix validated (300s soak clean).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2953 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| klog_sigio_race.c | β | 3.2 KB | view raw | |
| build.sh | β | 117 B | view raw | |
| run.sh | β | 585 B | view raw | |
| build.log | β | 5.6 MB | β download | |
| run.log | β | 2.8 KB | view raw | |
| panic.txt | β | 870 B | view raw | |
| run2_brokenfix_panic.txt | β | 1.7 KB | view raw | |
| env.txt | β | 492 B | view raw | |
| fix.diff | β | 2.5 KB | view raw | |
| fix.attempt2-systemic-sigio-ref.diff.unvalidated | β | 27.0 KB | β download | |
| VERDICT.md | β | 7.9 KB | β raw | |
| README.md | β | 3.0 KB | β raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 6.2 KB | view raw |
DF-2953 β /dev/klog logtimeout() use-after-free read of sc_sigio
What
sys/kern/subr_log.c:247-248 β the periodic logtimeout() softclock callout
loads logsoftc.sc_sigio without sigio_token and passes the raw
struct sigio * to pgsigio():
if ((logsoftc.sc_state & LOG_ASYNC) && logsoftc.sc_sigio != NULL)
pgsigio(logsoftc.sc_sigio, SIGIO, 0);
A concurrent FIOSETOWN / TIOCSPGRP ioctl on the (open) klog fd calls
fsetown() (sys/kern/subr_log.c:292-301 β sys/kern/kern_descrip.c:1296),
which β under sigio_token β replaces and frees the old sigio
(funsetown(), sys/kern/kern_descrip.c:1239-1272: sio_pgrp=NULL,
sio_ucred=NULL, kfree(sigio, M_SIGIO)).
pgsigio() (sys/kern/kern_sig.c:2639-2662) then dereferences the freed
chunk: sigio->sio_pgid, sigio->sio_pgrp β pgref(NULL), and
CANSIGIO(sigio->sio_ruid, sigio->sio_ucred, p) β
(sigio->sio_ucred)->cr_uid on a NULL/stale ucred
(sys/kern/kern_sig.c:99-104).
All other accessors of a sc_sigio-style pointer (fsetown, funsetown,
fgetown) hold sigio_token; logtimeout is the only unlocked reader in
the tree's klog path. (FreeBSD fixed this class by passing
struct sigio ** to pgsigio and locking internally.)
The logclose() β funsetown() path is NOT a racer: callout_terminate()
synchronously waits for an in-progress callback
(sys/kern/kern_timeout.c:1104-1126, wait loop at :926-941).
Impact
Kernel NULL-pointer deref panic in the softclock thread (verified: fatal
trap 12, fault virtual address = 0x40, Stopped at pgsigio+0xcd). If the
freed M_SIGIO chunk is recycled before the deref, the kernel reads
attacker-adjacent slab data and follows garbage sio_pgrp/sio_ucred
pointers. Trigger requires an open fd on /dev/klog (mode 0600
root:wheel, sys/kern/subr_log.c:317): host root, or jailed root where a
devfs ruleset exposes klog (the DF-0190 jail-check gap) β the latter turns
this into a host-kernel panic from inside a jail.
Reproduce (guest is single-tenant, stock INVARIANTS kernel)
# build (in guest, as root)
cc -O2 -Wall -o /root/klog_sigio_race /root/klog_sigio_race.c
# run (as root)
/etc/rc.d/syslogd stop # syslogd holds /dev/klog
sysctl -w kern.log_wakeups_per_second=100
/root/klog_sigio_race
Expected (vulnerable kernel): panic within seconds β
Fatal trap 12: page fault while in kernel mode,
fault virtual address = 0x40, Stopped at pgsigio+0xcd: movl 0x40(%rdx),%edx,
current process = Idle (softclock thread).
Mechanism of the PoC: opens klog, sets FIOASYNC (LOG_ASYNC), becomes a
process-group leader with 16 members (widens pgsigio()'s member loop β
microsecond-scale window), one child hammers kill(-pg, 0) for pg_lock
contention, one child writes to /dev/console in a loop
(log_console() sets msgbuftrigger=1, sys/kern/subr_prf.c:296), and
three processes hammer ioctl(FIOSETOWN, -pgid) / ioctl(FIOSETOWN, 0)
(install pgrp-owner sigio / free it).
Fixed kernel: identical run completes cleanly (no panic).
DF-2953 β VERDICT
Status: REPRODUCED (impact: panic / local kernel DoS). Fix validated on guest (baseline panics <1 s; patched kernel survives a 300 s race soak).
What was audited and found
Pass-2 audit of sys/kern/subr_log.c (/dev/klog driver, 321 lines). Beyond
the four known findings (DF-0189/0190/0191/0192 β not re-reported), one new
memory-safety race was found and proven:
logtimeout() β the periodic softclock callout β reads
logsoftc.sc_sigio and calls pgsigio(logsoftc.sc_sigio, SIGIO, 0)
at sys/kern/subr_log.c:247-248 without sigio_token and without a
reference, while
logioctlFIOSETOWN / TIOCSPGRP (sys/kern/subr_log.c:292-301) βfsetown()(sys/kern/kern_descrip.c:1296) frees the old sigio under the token (funsetown(),kern_descrip.c:1239-1272:sio_pgrp = NULL,sio_ucred = NULL,kfree(M_SIGIO)), andlogopen()'sfsetown()(sys/kern/subr_log.c:102) does the same on the DF-0189 double-open path.
pgsigio() (sys/kern/kern_sig.c:2639-2662) then dereferences the freed
chunk: sigio->sio_pgid, pgref(sigio->sio_pgrp) with sio_pgrp == NULL,
and CANSIGIO(sigio->sio_ruid, sigio->sio_ucred, p) whose first evaluation
is (uc)->cr_uid (kern_sig.c:99-104) on the NULLed ucred.
Every other accessor of a sigio * pointer in the klog path
(fsetown/funsetown/fgetown) holds sigio_token
(kern_descrip.c:1246,1368,1391); logtimeout is the only unlocked
reader. The logclose() β funsetown() combination is not a racer:
callout_terminate() synchronously waits for an in-progress callback
(sys/kern/kern_timeout.c:1104-1126, wait loop at :926-941). FreeBSD
fixed this class years ago by passing struct sigio ** to pgsigio with
internal locking; DragonFly still passes the raw pointer.
Reproduction (run 1 β baseline)
Guest: DragonFly 6.5-DEVELOPMENT, stock INVARIANTS kernel
#0 Thu Jul 2 06:02:54 UTC 2026. PoC klog_sigio_race.c (root):
opens /dev/klog (after stopping syslogd), sets FIOASYNC, becomes a
process-group leader with 16 members (widens pgsigio()'s member loop to
a microsecond-scale window), one child hammers kill(-pgrp, 0) (pg_lock
contention stretches pgsigio()), one child writes /dev/console in a
loop (log_console() sets msgbuftrigger = 1, subr_prf.c:296), and
three processes hammer ioctl(FIOSETOWN, -pgid) / ioctl(FIOSETOWN, 0)
(install pgrp-owned sigio / free it).
Result: panic in under one second β
Fatal trap 12: page fault while in kernel mode cpuid = 3; lapic id = 3 fault virtual address = 0x40 <- (NULL ucred/pgrp) + field offset instruction pointer = 0x8:0xffffffff8066212d (pgsigio+0xcd) current process = Idle <- softclock kernel thread current thread = pri 12 Stopped at pgsigio+0xcd: movl 0x40(%rdx),%edx
The faulting context (Idle/softclock, inside pgsigio, constant small
fault address) matches the source-level prediction exactly: dereference of
funsetown()-NULLed fields in a freed struct sigio.
Exploitability ceiling
Honest classification: panic (local DoS). Triggering requires an open
fd on /dev/klog (mode 0600 root:wheel, subr_log.c:317), i.e. host root
β or jailed root where a devfs ruleset exposes klog (the DF-0190 jail-check
gap), which turns this into a host-kernel panic from inside a jail. The
read-side UAF on the M_SIGIO chunk is not straightforwardly convertible to
a write primitive: the zone is dedicated to struct sigio, and the
post-free field values are NULLed pointers (hence the deterministic NULL
deref). A recycled chunk yields at most a spurious SIGIO to a stale
target. uid0 escalation was not pursued further because the trigger is
already privileged.
Fix validation (mandatory for memory-corruption class)
- Attempt 1 β hold
sigio_tokenacrosspgsigio()inlogtimeout: rebuilt, same PoC, panicked identically. Root cause: lwkt tokens are soft β "If the thread blocks all tokens are released, then reacquired when the thread resumes" (sys/kern/lwkt_token.c:40-42) β andpgsigio()blocks inlockmgr(&pg->pg_lock). Note the tree already uses this broken pattern atsys/kern/sys_pipe.c:211-214(related latent instance, out of this file's scope β recommend the orchestrator file it against sys_pipe.c). - Attempt 2 β systemic
pgsigio(struct sigio **)rewrite (snapshot pgid/ruid/ucred/proc/pgrp +crhold/PHOLD/pgrefunder sharedsigio_token, 13 files): rebuilt, same PoC, panicked withassertion "count > 0" failed in sess_relefrompgrelinside the newpgsigioβ the snapshotted pgrp still hit a destroy/destroy race (suspected lwkt shared/exclusive exclusion subtleties; root cause not fully determined). Kept asfix.attempt2-systemic-sigio-ref.diff.unvalidated; not shipped. - Final fix (shipped as
fix.diff) β driver-local: add a realstruct lock sc_locktologsoftc,lockinit()inlog_drvinit, and take itLK_EXCLUSIVEaround every klog-path access ofsc_sigio(logopenfsetown,logclosefunsetown,logtimeoutpgsigio, FIOSETOWN/TIOCSPGRP ioctls).lockmgrlocks are held across blocking, which is exactly the property the token approaches lacked. Lock order is strictlysc_lock β {sigio_token, pg_token, p_token};sc_lockappears nowhere else, so no inversion is possible. - Run 4 (validated): kernel
#1 Fri Sep 4 03:14:52 UTC 2026(clean build,build.log), identical PoC undertimeout 300: no panic; guest stayed up, PoC exited normally at the timeout, load average 5.04 during the soak confirms the race machinery was fully exercised. Baseline died in β€1 s on the same workload β the behavioral delta is decisive.
Negative results (classes hunted and killed in this pass)
logreadOOB: impossible β every access is bounded bylindex % msg_sizeandn = min(msg_size - lindex_modulo, xindex - lindex, uio_resid)(subr_log.c:171-182); wrap/torn reads are the DF-0192 data-integrity family only.msg_size < 2048corner in the wrap-correction (subr_log.c:163-164): unreachable at runtime βmsg_sizeis fixed at boot from compile-timeMSGBUF_SIZE(1 MB default;machdep.c:2557,msgbufinit), never resized.FIONREADtruncation:n β€ msg_size - 1024uncorrected orβ€ msg_size - 2048corrected β fitsint(subr_log.c:277-282).logtimeoutre-arm aftercallout_terminate: closed β terminate waits for INPROG (kern_timeout.c:926-941); stray trailingcallout_resetcan only occur before termination completes, and the!log_openguard makes any post-close firing benign (subr_log.c:238-239).hz / log_wakeups_per_secondnegative/huge values: root-only sysctl; callout clamps to β₯1 tick (kern_timeout.c:788-791) β worst case is CPU burn by root; the 0 case is DF-0191 (not re-reported).sysctl_kern_msgbuf(subr_prf.c:1153-1198) branch 3 (n - rindex_modulo): modular arithmetic makes the branch unreachable except in the corrected corner (xindex_modulo == 0,rindex_modulo == 2048) where the copy stays in-bounds (truncating only). No OOB.msgbuf_clearracinglogread: cursor may regress and the reader may see bzero'd bytes β data integrity only, all accesses still modulo-bounded; root-only CTLFLAG_SECURE sysctl. DF-0192 family.- Missed wakeup in
logread's sleep loop: self-heals on the next kprintf (msgbuftrigger re-arms), PCATCH allows signal interruption. Liveness nit only. sc_stateplain store inlogcloseracing atomic bit ops: hint bits only, benign.kqfilter/knote lifecycle:logsoftcis static, knotes reference no per-open data; fd-close detaches vialogfiltdetach. No lifetime bug.logopencallout_init_mpon an armed callout: only reachable via the DF-0189 double-open (same root cause, amplifier noted there).TIOCSPGRPprivilege:fsetownenforces same-session policy (kern_descrip.c:1326,1341) β no cross-session signaling.
Fix verification
fixedBaseline (stock) kernel panicked at pgsigio+0xcd within 1s of PoC launch (twice, plus once under a broken fix attempt). Kernel built with the driver-local sc_lock fix survived the identical PoC under timeout 300 with no panic (load avg 5.04 during soak; PoC exited cleanly at timeout; guest up). Two intermediate fix approaches were built and empirically refuted (lwkt token across pgsigio - drops on block per lwkt_token.c:40-42; systemic 13-file snapshot rewrite - still hit a pgrp destroy race, root cause undetermined); both are documented and the systemic diff is retained as unvalidated.
['run.log run 4: NO PANIC, guest up, pgrep=0 after timeout, load 5.04', 'build.log: fixed-kernel build completed 0 errors', 'fix.diff: the validated 6-hunk driver-local fix', 'run2_brokenfix_panic.txt / run.log runs 2-3: refuted fix attempts']
Confirmed kernel references
- sys/kern/subr_log.c:247
- sys/kern/subr_log.c:248
- sys/kern/subr_log.c:102
- sys/kern/subr_log.c:115
- sys/kern/subr_log.c:292
- sys/kern/subr_log.c:301
- sys/kern/subr_log.c:317
- sys/kern/kern_descrip.c:1239
- sys/kern/kern_descrip.c:1272
- sys/kern/kern_descrip.c:1296
- sys/kern/kern_descrip.c:1368
- sys/kern/kern_descrip.c:1386
- sys/kern/kern_sig.c:2639
- sys/kern/kern_sig.c:99
- sys/kern/kern_timeout.c:1104
- sys/kern/kern_timeout.c:926
- sys/kern/lwkt_token.c:40
- sys/kern/subr_prf.c:296
Detail
Exploit chain
root (or jailed root w/ klog devfs exposure): open /dev/klog; FIOASYNC=1; FIOSETOWN(-own_pgrp) installs pgrp-owned sigio; 16 pgrp members + killpg(0) hammer stretch pgsigio()'s member loop to microseconds; /dev/console write loop keeps msgbuftrigger set so logtimeout (hz/wakeups per sec) calls pgsigio; concurrent FIOSETOWN(0)/FIOSETOWN(-pgrp) hammer frees the sigio inside pgsigio's deref window -> NULL-deref panic in softclock. Ceiling: local DoS (panic); read-side UAF of a dedicated M_SIGIO chunk with NULLed pointers - no practical write primitive.
Evidence (decisive lines)
['panic.txt: Fatal trap 12, fault virtual address=0x40, instruction pointer=pgsigio+0xcd, current process=Idle (softclock), Stopped at pgsigio+0xcd: movl 0x40(%rdx),%edx - baseline panic, <1s after launch', 'run.log: all four runs (baseline panic <1s; attempt1 token fix panic; attempt2 systemic fix panic sess_rele assert; final fix 300s clean soak, load 5.04, guest up)', "run2_brokenfix_panic.txt: identical panic on the token-wrapped 'fix' proving lwkt tokens drop on block (lwkt_token.c:40-42)", 'fix.diff: shipped driver-local lockmgr fix (subr_log.c, 6 hunks) - validated', 'fix.attempt2-systemic-sigio-ref.diff.unvalidated: 13-file pgsigio(struct sigio**) snapshot rewrite - built+ran, still raced, NOT shipped', 'build.log: final fixed-kernel build, 0 errors (Kernel build for X86_64_GENERIC completed Fri Sep 4 03:19:15 UTC 2026)', 'klog_sigio_race.c: PoC source; build.sh/run.sh exact commands']
PoC changes
PoC authored fresh for this finding (no seed). Widening strategies that made the sub-microsecond window deterministic: (1) pgrp owner instead of proc owner so pgsigio takes the long pg_lock path; (2) 16 forked pgrp members stretch the signal loop; (3) killpg(pgrp,0) hammer contends pg_lock inside pgsigio; (4) /dev/console write loop keeps msgbuftrigger set so every logtimeout tick signals; (5) kern.log_wakeups_per_second raised to 100 (must stay >=1 - 0 is DF-0191); (6) three FIOSETOWN hammer processes for CPU spread.
Verified recommended fix
Serialize every klog-driver access of sc_sigio with a driver-local lockmgr lock held across the blocking pgsigio() path (fix.diff in this pack); alternatively upstream a reference-counted pgsigio(struct sigio **) that does not rely on lwkt tokens across blocking.
Verdict
logtimeout() (softclock callout) reads logsoftc.sc_sigio without sigio_token and calls pgsigio() on the raw pointer (sys/kern/subr_log.c:247-248); concurrent FIOSETOWN/TIOCSPGRP on the klog fd frees the struct sigio (fsetown->funsetown->kfree, kern_descrip.c:1239-1372) and pgsigio then dereferences NULLed/freed fields (pgref(NULL), (NULL ucred)->cr_uid via CANSIGIO, kern_sig.c:99-104,2639-2662). Verified on the guest: kernel panic (Fatal trap 12, fault 0x40, pgsigio+0xcd, softclock/Idle context) within one second of PoC launch. Trigger requires an open /dev/klog fd (root:wheel 0600), so host-root DoS; via DF-0190 (jail root + devfs-exposed klog) it becomes a host-kernel panic from inside a jail. Validated fix: driver-local lockmgr sc_lock serializing every sc_sigio access in logopen/logclose/logtimeout/FIOSETOWN/TIOCSPGRP; patched kernel survived a 300s race soak (baseline died <1s). Two earlier fix attempts (lwkt token around pgsigio; systemic 13-file pgsigio(struct sigio**) snapshot rewrite) were built, re-panicked, and are documented as not-shipped.
No comments yet.