# DF-5177 VERDICT — REPRODUCED (deterministic use-after-free write)

## 1. The bug

`hrtimer_cancel` (sys/dev/drm/linux_hrtimer.c:121-150):

```c
lwkt_gettoken(&timer->timer_token);
if (timer->active) {
        struct taskqueue *tq = taskqueue_thread[timer->gd->gd_cpuid];  /* :128 */
        bool running = timer->running;                                 /* :129 */
        ret = 1;
        timer->cancel = true;
        lwkt_reltoken(&timer->timer_token);                           /* :133 ! */
        if (running) {
                /* Nothing */
        } else if (timer->gd == mycpu) { ... systimer_del ... }
        ...
        while (taskqueue_cancel(tq, &timer->task, NULL))              /* :144 */
                taskqueue_drain(tq, &timer->task);
}
```

Everything after the token release operates on unlocked, re-readable state.
`hrtimer_start_range_ns` (linux_hrtimer.c:109-118), called concurrently from
another CPU while the callback executes (the token is deliberately dropped
around `timer->function(timer)`, linux_hrtimer.c:55), then sets:

```c
timer->cancel = false;
timer->running = false;   /* :112  LIE — __hrtimer_task is mid-callback  */
timer->active = true;
timer->gd = mycpu;        /* :114  moved to the new CPU                 */
```

So cancel():
1. snapshots `tq` = the NEW cpu's queue (or a stale one),
2. reads `running == false` (the lie),
3. deletes the systimer on the (correct) new cpu — fine,
4. `taskqueue_cancel(tq_new, &timer->task)`: the task is *executing* on the
   OLD cpu's queue thread (`ta_pending == 0` there, `tq_running` on the new
   queue is not this task) → returns 0 → **no drain** →
5. returns `ret=1` ("was active, cancelled") **while `timer->function` is
   still running**.

The Linux API contract (`hrtimer_cancel()` waits for a running callback) is
what every teardown caller relies on: `i915_pmu_unregister` (i915_pmu.c:1089),
`intel_uncore` teardown (intel_uncore.c:1390), `i915_perf` stream teardown
(i915_perf.c:1952) — they free the surrounding object right after cancel.

Once the caller frees the `struct hrtimer`, the in-flight
`__hrtimer_task` epilogue (linux_hrtimer.c:58-68) touches freed memory:

```c
lwkt_gettoken(&timer->timer_token);   /* token ops on freed memory */
timer->running = false;               /* UAF write */
timer->active = false;                /* UAF write */
if (!timer->cancel && restart == HRTIMER_RESTART) {
        timer->active = true;
        systimer_init_oneshot(&timer->st, __hrtimer_function,
            timer, timer->timeout_us);  /* bzero of freed memory +
                                           insertion into a LIVE per-cpu
                                           systimer queue */
}
```

Distinct from the known pair: DF-2174 is the epilogue's unconditional
`active=false` clobber (un-cancellable timer → UAF on next fire);
DF-2175 is `hrtimer_start` re-initializing an ARMED systimer. This finding
is the cancel-side break: non-atomic cancel + the `running=false` lie →
early return → UAF of the epilogue.

## 2. PoC and result (deterministic — sleep-orchestrated, no race)

`df5177.c` (KLD, `MODULE_DEPEND(drm)`), all operations on dedicated
per-CPU kernel threads:

1. cpu0 thread: `hrtimer_start(1ms)` → fire → `__hrtimer_task` runs the
   callback on `taskqueue_thread[0]`; callback sets `cb_running` and sleeps
   333 ms.
2. cpu1 thread: `hrtimer_start(50ms)` — legal re-arm while the callback is
   in flight (`running=false` lie, `gd=cpu1`).
3. cpu2 thread: `hrtimer_cancel()` → returns immediately; the PoC records
   `cancel_saw_cb_done = cb_done` at that moment.
4. Caller-side teardown exactly like i915: `kfree(timer)`; immediately
   reallocate the same-size chunk (same address, `same=1`), poison the
   state bytes (`running/active/cancel/is_rel = 0x5A`) and re-init the
   token slot so the demonstration converges instead of hanging.
5. After the callback's sleep expires, the epilogue writes into the freed
   chunk → scan the reallocated victim for changes.

Baseline run (run.log):

```
[df5177] hrtimer_cancel() returned ret=1, cb_done=0 (Linux API requires cb_done==1 here!)
[df5177] timer freed and reallocated: victim=0xfffff8008bb481f0 same=1
[df5177] UAF-WRITE victim[152]=0x00 (was 0x5A: epilogue cleared active)
[df5177] UAF-WRITE victim[153]=0x00 (was 0x5A: epilogue cleared running)
[df5177] EXPLOIT-PRIMITIVE PROVEN: cancel returned before callback + 2 post-free writes into a reallocated object
```

Guest survives — this is a *controllable* corruption primitive, not merely
a panic.

## 3. Escalation path (why this is more than a DoS)

The demonstrated writes are the epilogue's fixed fields, but the same
interleave with a callback returning `HRTIMER_RESTART` (and
`timer->cancel == false` in the freed chunk) reaches
`systimer_init_oneshot(&timer->st, __hrtimer_function, timer, ...)` on the
freed memory: that `bzero`s ~120 bytes of the freed chunk and inserts the
stale `st` into the *current CPU's live systimer queue*; on expiry the
kernel calls `__hrtimer_function` with `info->data` read from reallocated
memory, and `__hrtimer_task` then calls `timer->function(timer)` — an
attacker-reclaimed chunk yields a controlled function pointer call with a
controlled first argument. (Not chained to uid=0 in this run: on the audit
guest drm's hrtimer users are not hardware-attached, so the organic trigger
path is GPU-gated exactly like DF-5169.)

## 4. Fix validation

`fix.diff` (shared with DF-5176):

- `hrtimer_start` no longer writes the `running=false` lie;
- `hrtimer_cancel` loops until `timer->active == false` under the token,
  snapshots `tq`/`gd` under the token, keeps the token held across
  `systimer_del` (including the CPU migration) so a concurrent start cannot
  move `gd` mid-delete, and re-checks after each drain.

Applied in the guest `/usr/src`, `drm.ko` rebuilt + installed, reboot, PoC
rebuilt against the patched header, re-run (run.fix.log):

```
[df5177] hrtimer_cancel() returned ret=1, cb_done=1 (Linux API requires cb_done==1 here!)
[df5177] victim[152] still 0x5A (no write)
[df5177] victim[153] still 0x5A (no write)
[df5177] changed=0 cancel_saw_cb_done=1
```

Cancel now waits out the in-flight callback and returns only when the
timer is quiescent; zero post-free writes.

## 5. Harness incidents (transparency)

Two early crashes were PoC-harness bugs, not kernel findings (both
documented, eliminated):

1. Passing the handler directly to DF's `DECLARE_MODULE` (which expects a
   `moduledata_t` variable) made the kernel `strcmp()` the handler's first
   machine-code bytes as a module name → GPF in `module_lookupbyname`.
   `panic.serial.txt` records it.
2. Migrating the kldload *user* thread with `lwkt_setcpu_self` wedged the
   guest (two CPUs spinning in `acpi_hpet_early_get_timecount`); v2 uses
   dedicated per-CPU kernel threads via `lwkt_create(..., cpuid)`.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
