# 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).
