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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2929 Β· 15 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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)

  1. hrtimer_start_range_ns() #1 β†’ systimer_init_oneshot(&timer->st, ...) (sys/dev/drm/linux_hrtimer.c:115) β†’ timer linked into gd_systimerq (1 ms timeout, SYSTF_ONQUEUE set, neighbours' links point at it).
  2. hrtimer_start_range_ns() #2 (same timer, still armed β€” legal Linux hrtimer_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_prev cleared while still linked (ghost links), - SYSTF_ONQUEUE cleared β†’ systimer_add()'s KKASSERT at :148 passes.
  3. 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.
  4. On the next timer interrupt the dispatcher dequeues nodes whose back-links are stale β†’ TAILQ_REMOVE through a dangling tqe_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_start on 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 via fw_domain_arm_timer() (line 74-78, unconditional hrtimer_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

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (kernel unchanged; drm.ko rebuilt from patched /usr/src/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.