systimer_init_oneshot() bzero of a queued timer silently corrupts gd_systimerq; reached via DRM hrtimer_start re-arm on every i915 auto-forcewake register access
| Field | Value |
|---|---|
| ID | DF-2929 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-672 (re-init of live-listed timer β CWE-787-shaped wild queue unlinks) |
| File | sys/kern/kern_systimer.c |
| Lines | 360-371 (bzero :363, assert-defeat of :148); wrapper: linux_hrtimer.c:88-119; trigger: intel_uncore.c:74-78/1165-1176 |
| Area | kern/timer + drm |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
systimer_init_oneshot() unconditionally bzero()s the struct systimer. When the timer is still armed (SYSTF_ONQUEUE, linked into the owning cpu's gd_systimerq) the bzero erases node.tqe_next/tqe_prev while neighbours/head still point at the node, and clears SYSTF_ONQUEUE itself β defeating systimer_add()'s KKASSERT at :148, so the corruption is silent even on INVARIANTS kernels. The re-add leaves ghost links/cycles; systimer_intr() and systimer_del() then TAILQ_REMOVE through stale tqe_prev pointers. In-tree trigger: linux_hrtimer.c hrtimer_start_range_ns() implements Linux hrtimer_start() semantics (re-arm of an armed timer is legal) by re-running systimer_init_oneshot() with no dequeue; intel_uncore.c forcewake_auto() β fw_domain_arm_timer() calls it unconditionally on EVERY auto-forcewake register access, so any two accesses within the 1ms window execute the corrupting sequence (routine from unprivileged GPU ioctls on i915). Upgrades DF-0154's 'no in-tree caller violates today' to a concrete, live violator. On production kernels the unlink is a real wild write (*(elm->tqe_prev) = elm->tqe_next through stale tqe_prev) plus BUG_ON in hrtimer_function on double task-enqueue and potential infinite re-dispatch (clock hang).
Proof of contest
VERIFIED (findings/poc/DF-2929/): KLD harness mode 2 (raw engine sequence) β structural queue verification 'stβP cycle, 8 revisits/24 hops' + 'panic: Bad link elm' in systimer_del; mode 5 (REAL in-tree wrapper: hrtimer_init + two hrtimer_start_range_ns 100Β΅s apart) β panic in systimer_intr β pcpu_timer_process_oncpu β splz_timer within ~100Β΅s; control mode 1 'queue ok'. userβroot route: corruption primitive is a stale-pointer queue unlink whose node lives in driver-owned memory; full uid=0 additionally requires controlling reused node contents β not demonstrated, documented as hardening blocker. Fix validated in-guest: rebuilt drm.ko with delete-before-re-arm (mirror hrtimer_cancel's owning-cpu dance); identical PoC 8/8 clean, exactly one fire per cycle, no panic.
Recommended fix
Validated fix.diff (linux_hrtimer.c delete-before-re-arm) in findings/poc/DF-2929/. Engine-side hardening (DF-0154 territory): systimer_init_oneshot()/_systimer_init_periodic() should detect a queued timer BEFORE the bzero.
Timeline
- 2026-09-02 Discovered during pass-2 audit of kern_systimer.c (GLM 5.3); queue corruption + panic reproduced via real DRM wrapper
- fix validated 8/8.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2929 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 6.3 KB | β raw | |
| df2929_mod.c | β | 6.5 KB | view raw | |
| df2929_ppollrace.c | β | 3.3 KB | view raw | |
| build.sh | β | 136 B | view raw | |
| run.sh | β | 340 B | view raw | |
| build.log | β | 499 B | view raw | |
| run.log | β | 884 B | view raw | |
| run.2.log | β | 546 B | view raw | |
| run.fix.log | β | 824 B | view raw | |
| panic.txt | β | 1.8 KB | view raw | |
| env.txt | β | 648 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 5.2 KB | view raw |
DF-2929 β systimer re-init of a queued timer (via DRM hrtimer wrapper) corrupts gd_systimerq
What
sys/kern/kern_systimer.c:systimer_init_oneshot() (line 359-371) re-initializes
a struct systimer with an unconditional bzero() at line 363. If the timer is
still armed (SYSTF_ONQUEUE, linked into the owning cpu's gd_systimerq), the
bzero erases its node.tqe_next/node.tqe_prev links while the neighbours
and possibly the queue head still point at it, and also clears the very
SYSTF_ONQUEUE flag that systimer_add()'s KKASSERT at line 148 checks β so
the subsequent re-add is silent even on INVARIANTS kernels. The queue is
left with ghost links / cycles; the dispatcher (systimer_intr) and
systimer_del() then unlink through stale pointers.
The in-tree trigger is the DRM Linux-compat wrapper
sys/dev/drm/linux_hrtimer.c:hrtimer_start_range_ns() (line 88-119): it
implements Linux hrtimer_start() semantics (re-arm an armed timer is legal
Linux API) by calling systimer_init_oneshot() on a possibly-armed timer with
no dequeue first. intel_uncore.c:___force_wake_auto() (line 1165-1176)
calls fw_domain_arm_timer() β hrtimer_start_range_ns() on every
auto-forcewake register access, so any two i915 register accesses within the
1 ms timer window execute the corrupting sequence (routinely, from unprivileged
GPU ioctls).
Impact
Kernel memory corruption of the per-cpu timer queue (the queue that drives
hardclock/statclock): ghost links, cycles, orphaned nodes; double-dispatch of
one-shot timers; wild TAILQ_REMOVE writes through stale tqe_prev pointers
on production kernels; demonstrated kernel panics in systimer_intr() and
systimer_del() on the INVARIANTS guest.
Reproduce (on the QEMU guest)
# build the KLD harness (as root; it MODULE_DEPENDs on drm.ko) cd /root/df2929 && make # control: arm P/st/N one-shots, verify queue, clean up -> "queue ok" sysctl kern.df2929_run=1 # raw engine sequence (bzero of queued timer + re-add): -> "QUEUE CORRUPT" sysctl kern.df2929_run=2 # + panic "Bad link elm" in systimer_del() # REAL in-tree wrapper path (hrtimer_start_range_ns twice): -> panic in # systimer_intr() via pcpu_timer_process_oncpu/splz_timer sysctl kern.df2929_run=5
Fix
fix.diff β make hrtimer_start_range_ns() delete the pending systimer
(on its owning cpu, mirroring hrtimer_cancel()) before re-initializing.
Validated: rebuild drm.ko with the fix β same PoC runs clean Γ8, no panic,
queue integrity ok, exactly one fire per cycle.
DF-2929 VERDICT β reproduced (kernel panic / timer-queue corruption); fix validated
Bottom line
Reproduced on the stock INVARIANTS guest (DragonFly 6.5-DEVELOPMENT #0
X86_64_GENERIC, 6 vCPU): calling the in-tree DRM wrapper
hrtimer_start_range_ns() twice within the timer window β the exact sequence
intel_uncore.c:___force_wake_auto() performs on every auto-forcewake i915
register access β corrupts the owning cpu's gd_systimerq and panics the
kernel inside the timer dispatcher (systimer_intr() via
pcpu_timer_process_oncpu()/splz_timer()). The raw engine sequence
(systimer_init_oneshot() on a SYSTF_ONQUEUE-linked timer) shows the
corruption structurally: a st<->P traversal cycle (8 revisits in 24 hops)
plus a one-sided ghost link, followed by panic: Bad link elm ... prev->next
!= elm in systimer_del(). Root cause: systimer_init_oneshot()
bzero()s a possibly-queued struct systimer
(sys/kern/kern_systimer.c:363) and the bzero also erases the
SYSTF_ONQUEUE flag that would have made systimer_add()'s KKASSERT
(:148) catch the violation β silent even on INVARIANTS kernels.
Why it happens (line-accurate)
hrtimer_start_range_ns()#1 βsystimer_init_oneshot(&timer->st, ...)(sys/dev/drm/linux_hrtimer.c:115) β timer linked intogd_systimerq(1 ms timeout,SYSTF_ONQUEUEset, neighbours' links point at it).hrtimer_start_range_ns()#2 (same timer, still armed β legal Linuxhrtimer_start()semantics, no dequeue performed by the wrapper) βsystimer_init_oneshot()βbzero(info, sizeof(struct systimer))at sys/kern/kern_systimer.c:363: -info->node.tqe_next/tqe_prevcleared while still linked (ghost links), -SYSTF_ONQUEUEcleared βsystimer_add()'s KKASSERT at :148 passes.systimer_add()inserts the zeroed node per the new expiry: neighbours keep stale links; with P(st's predecessor) still pointing at st the forward walk cycles (st->P->st->P...), and the timer that was after st is orphaned.- On the next timer interrupt the dispatcher dequeues nodes whose
back-links are stale β
TAILQ_REMOVEthrough a danglingtqe_prev(a wild write on production kernels; on this INVARIANTS guest the QMD check catches it as "Bad link elm ... prev->next != elm").
Reachability / threat model
- The wrapper is in-tree and its Linux semantics (
hrtimer_starton an armed timer) are relied upon by ported drivers;intel_uncore.c:___force_wake_auto(line 1165-1176) hits it on every auto-forcewake register access viafw_domain_arm_timer()(line 74-78, unconditionalhrtimer_start_range_ns). On i915 hardware, auto-forcewake register access is driven by ordinary GPU use (rendering / ioctl submission by any user with /dev/dri access). - Secondary callers with the same shape:
i915_pmu.c:132,i915_perf.c:1914,amdgpu/dce_virtual.c:709,732. - On this QEMU guest there is no i915 device, so the PoC drives the wrapper
directly from a KLD harness (
kern.df2929_run=5) β root loads the harness, but the executed kernel code path (hrtimer_start_range_nsβsystimer_init_oneshotβsystimer_add) is the production path.
Reproduced evidence
Baseline (stock drm.ko, stock INVARIANTS kernel):
DF2929 mode=1: ... seen_st=1 ... => queue ok (control clean)
DF2929 mode=2: forward hops=24 seen_st=8 seen_p=8 ... => QUEUE CORRUPT
panic: Bad link elm 0xffffffff82600400 prev->next != elm
systimer_del() at systimer_del+0xd4
df2929_run() ... sysctl_root ... sys___sysctl
(separate boot, real wrapper path:)
DF2929 mode=5: hrtimer path: ... (dispatcher fires 100us later)
panic: Bad link elm 0xffffffff826005c8 prev->next != elm
cpuid = 4
systimer_intr() at systimer_intr+0x235
pcpu_timer_process_oncpu() at pcpu_timer_process_oncpu+0x82
splz_timer() at splz_timer+0x13
Fix validation (patched drm.ko, same kernel, same harness):
DF2929 mode=5: hops=6 dht.st seen=1 ... => queue ok DF2929 mode=5: cancelled, dht_fires=1..4 (exactly one fire per run) ... x8 runs, guest stays up, no panic.
Exploit chain (characterization)
Impact on this guest: panic (dispatcher unlink trips the INVARIANTS/QMD
check). On production (no-INVARIANTS) kernels the same corruption performs
real out-of-bounds queue writes: TAILQ_REMOVE stores
*(elm->tqe_prev) = elm->tqe_next through stale tqe_prev values, i.e. a
write-what-where-shaped primitive confined to the timer queue's link fields,
plus double-dispatch of one-shot callbacks (__hrtimer_function's
BUG_ON(taskqueue_enqueue != 0) provides a second panic path) and a
potential infinite re-dispatch loop of expired one-shots (clock hang).
The corrupted node here lives in driver-owned memory (struct hrtimer.st
inside the device structure), so turning the primitive into a full
uid=0 chain would additionally require controlling the reused node contents
(e.g. a driver struct freed/reallocated under the attacker's influence) β
not demonstrated; recorded as the hardening blocker. The memory-safety
violation itself (ghost-linked queue, wild unlink) is proven.
Reachability analysis that was killed during pass 2 (negative results)
The cross-CPU systimer_del() hypothesis (stack-allocated systimers in
clock_nanosleep1() sys/kern/kern_time.c:506-527 and precise_sleep()
sys/kern/kern_event.c:2114-2131, deleted after wakeup) was investigated and
killed: _wakeup() (sys/kern/kern_synch.c:1000-1030) always
lwkt_schedule()s the sleeper on its own td_gd; the nanosleep path parks
the thread via lwkt_deschedule_self(); user-scheduler migration
(dfly_acquire_curproc, sys/platform/pc64/x86_64/trap.c:336) only happens
at return-to-userland β after systimer_del() ran. Empirically: 1.2M+
ppoll/nanosleep precise-sleep iterations across 6 vCPUs produced zero
KKASSERT failures (probe source retained: df2929_ppollrace.c).
Fix
fix.diff (against sys/dev/drm/linux_hrtimer.c): hrtimer_start_range_ns()
now deletes the pending systimer first, on its owning cpu (mirroring
hrtimer_cancel()'s lwkt_setcpu_self() dance), before re-initializing.
Validated as above. Suggested engine-side hardening (separate from this fix,
relates to DF-0154): have systimer_init_oneshot()/
_systimer_init_periodic() detect a still-queued timer before the bzero
(while the flag is still meaningful).
Fix verification
fixedApplied fix.diff to guest /usr/src/sys/dev/drm/linux_hrtimer.c, rebuilt drm.ko (make obj && make in sys/dev/drm/drm), installed, rebooted, re-ran the identical PoC (kern.df2929_run=5) 8 times: queue integrity 'ok' every run, dht_fires increments by exactly 1 per run, no panic, guest stays up. Baseline with stock drm.ko panicked in systimer_intr on the first run. Bad behavior gone.
["run.fix.log: 8x 'DF2929 mode=5: ... => queue ok' + 'cancelled, dht_fires=1..4' + uptime output", 'env.txt: running drm.ko MD5 == patched build MD5 (52965c2c7a18026a7d9089264ab796b7)', 'fix.diff: git-apply-able unified diff against sys/dev/drm/linux_hrtimer.c']
Confirmed kernel references
Detail
Exploit chain
hrtimer_start_range_ns on armed timer -> bzero of queued struct systimer -> ghost links + cycle in gd_systimerq -> systimer_intr/TAILQ_REMOVE through stale tqe_prev (wild write; INVARIANTS catches as 'Bad link elm') -> panic / clock corruption. Production-kernel escalation beyond DoS additionally requires controlling the reused node contents (node lives in driver-owned memory) β not demonstrated, recorded as hardening blocker.
Evidence (decisive lines)
["run.log: 'DF2929 mode=2: forward hops=24 seen_st=8 seen_p=8 ... => QUEUE CORRUPT'", "panic.txt: panic 'Bad link elm' traces through systimer_del+0xd4 (mode 2) and systimer_intr+0x235 <- pcpu_timer_process_oncpu+0x82 <- splz_timer+0x13 cpuid=4 (mode 5, real drm.ko path)", "run.fix.log: 8x 'queue ok' + dht_fires=+1/run, guest stays up with patched drm.ko", 'VERDICT.md: line-accurate root cause and the killed cross-cpu-del reachability hypothesis']
PoC changes
No prior seed β pack authored fresh. PoC iterated during verification: (1) unprivileged ppoll/nanosleep precise-sleep racer (reachability probe; hypothesis killed: wakeups are cpu-local, 1.2M iterations zero asserts β kept as run.2.log/df2929_ppollrace.c); (2) KLD harness modes 1/2 (raw engine sequence, structural corruption detector); (3) mode 5 driving the real in-tree hrtimer_start_range_ns via MODULE_DEPEND(drm). Removed an initial lwkt_setcpu_self() from the sysctl handler that caused an unrelated lockmgr artifact-panic.
Verified recommended fix
linux_hrtimer.c: hrtimer_start_range_ns() must delete the pending systimer on its owning cpu (mirror hrtimer_cancel()'s lwkt_setcpu_self dance) before systimer_init_oneshot(); see fix.diff
Verdict
Reproduced on the stock INVARIANTS guest: the in-tree DRM wrapper hrtimer_start_range_ns() (sys/dev/drm/linux_hrtimer.c:88-119), when called on an already-armed timer β exactly what intel_uncore.c:___force_wake_auto() does on every auto-forcewake i915 register access β re-runs systimer_init_oneshot() on a SYSTF_ONQUEUE-linked timer; the bzero() at sys/kern/kern_systimer.c:363 erases the node links while neighbours still point at it and also clears the ONQUEUE flag that would make systimer_add()'s KKASSERT (:148) catch it, so the queue silently gains ghost links/cycles and the dispatcher then unlinks through stale pointers: panic 'Bad link elm ... prev->next != elm' in systimer_intr() (via pcpu_timer_process_oncpu/splz_timer) and in systimer_del(). Raw engine sequence (harness mode 2) shows the corruption structurally (st<->P cycle, 8 revisits in 24 hops). On production kernels the same unlink is a wild write through a stale tqe_prev plus double-dispatch of one-shot callbacks. fix.diff (wrapper deletes the pending systimer on its owning cpu before re-init) validated: 8/8 clean runs, no panic, one fire per cycle.
No comments yet.