# DF-5169 — VERDICT

**Status: reproduced** (deterministic, 16/16 baseline rounds across two runs;
0/16 + 3 stability reruns after the fix). **Impact: kernel heap UAF
write (memory corruption class)** — an 8-byte NULL write at container offset
+16 (`work->worker`) and a 1-byte zero write at +25 (`work->running`) into a
freed-then-reclaimed kernel allocation, plus `wakeup()` on a dangling wait
channel. Guest survives (the primitive is small controlled writes, not a
wild pointer chase), which is *better* proof than a panic: the write lands
exactly where an attacker's reclaimed victim object lives.

## 1. Reproduced — how

The mechanism half (DFly LinuxKPI workqueue) was proven on the audit QEMU
guest with `selffree.c`, a KLD that reproduces `i915_clflush_work()`'s exact
self-free choreography on the real `system_wq` — no Intel GPU required:

- `run.log` / `run.dmesg.log` (v1 module): 8/8 rounds —
  `freed+reclaimed @0xfffff8004f1ae1xx; running byte=00 (poison=41) worker
  qword=0000000000000000` → both post-func writes (`work->running=false`,
  `work->worker=NULL`) landed in the freed-then-reclaimed-and-poisoned chunk.
  `struct work_struct` offsets printed by the module: worker=16, on_queue=24,
  running=25, canceled=26.
- `run.2.log` (v2 module with FIFO marker-work completion detection): 8/8
  again on a freshly-loaded module instance.
- The `canceled` poison byte was 0x41 ⇒ `didcan==true` ⇒ `wakeup(work)` also
  executed on the freed address each round (code-derived; wakeup uses the
  address only as a wait-channel key, so it is silent by design).

## 2. The trigger half — why i915 reaches it (code-traced; HW-gated on QEMU)

`i915_gem_clflush_object()` queues an async clflush (execbuffer path,
i915_gem_execbuffer.c:1147, 1847) whose callback `i915_clflush_work()`
(i915_gem_clflush.c:71-90) drops the fence's creator reference at line 89.
The fence ref tripod:

- ref#1 creator: `dma_fence_init` (i915_gem_clflush.c:148)
- ref#2 sw-fence: `dma_fence_get` (i915_gem_clflush.c:158) — dropped at
  FENCE_FREE (`i915_clflush_notify`, line 104), which fires at sw-fence
  completion *after* FENCE_COMPLETE→`schedule_work`
  (i915_sw_fence.c:187-195) — i.e. typically before the worker runs;
- ref#3 reservation object: taken by `reservation_object_add_excl_fence`
  (linux_reservation.c:243-244, called at i915_gem_clflush.c:166) — dropped
  by the *next* exclusive-fence installation on the same object: every
  subsequent write-execbuf does `reservation_object_add_excl_fence(resv,
  &rq->fence)` (i915_vma.c:926-934), and a second async clflush on the same
  object replaces it the same way (old excl put at linux_reservation.c:260).

So: two userspace-execbufs back-to-back on the same cache-dirty object
(non-LLC i915 hardware) + any worker-scheduling lag ⇒ the work's line-89 put
is the last reference ⇒ `i915_clflush_release` → `dma_fence_free` →
`kfree(clflush)` **inside the callback** (a plain M_DRM kmalloc,
`sizeof(struct clflush)` ≈ 200-byte class). Then
`process_all_work()` (linux_workqueue.c:92-98, the lines that run right
after `work->func(work)` returns) reads/writes the freed struct:
`work->on_queue` read, `work->worker = NULL` (8-byte NULL write),
`work->canceled` read, `work->running = false` (1-byte zero write),
`wakeup(work)` on the dangling address. Upstream Linux allows self-freeing
works (its `process_one_work()` does all post-callback bookkeeping on the
*worker*), so every ported LinuxKPI driver carries this idiom — the clflush
work is the proven in-tree instance (the only self-freeing work item found
by an audit of all `container_of(work, …)` callbacks under sys/dev/drm).

## 3. Exploitability assessment (honest ceiling)

- Primitive: two *fixed-value* writes at fixed offsets (+16 NULL qword,
  +25 zero byte) into a freed DRM-zone chunk, plus a dangling-channel
  `wakeup()` (wakes whatever sleeps on that address). Attacker-influenced
  reallocation (same-size `M_DRM` kmalloc spray — GEM ioctls, contexts,
  requests) decides what field gets NULLed/zeroed: a pointer nulled → later
  NULL-deref DoS; a refcount/flags byte zeroed at +25 → premature-free /
  state confusion with a stronger follow-on UAF. Not a direct arbitrary
  write; a reliable uid=0 chain was **not** developed — the trigger needs
  Intel-GPU (pre-LLC) hardware which the QEMU guest cannot emulate, so
  end-to-end escalation could not be pursued on this guest. The
  deterministic same-address reclaim shown by the PoC (8/8, stride-0x60
  sequential zone chunks) demonstrates the grooming half is trivial.
- Preconditions: i915 GPU present + drm in use + two execbufs on the same
  cache-dirty object (unprivileged GPU client); OR any other self-freeing
  work item ported into the LinuxKPI in the future.

## 4. Fix — authored, built, validated on the guest

`fix.diff` moves all post-callback completion bookkeeping to worker-owned
state (`worker->running_work`), never touches `*work` after `func()`
returns, and updates the idle/flush/cancel readers
(`work_busy`, `flush_work`, `flush_workqueue`, `_cancel_work`,
`process_all_work`'s concurrency re-check) to consult the authoritative
worker state. Pre-func cancel snapshot keeps the cancel_work_sync wakeup
(address-only use; free-after-cancel API contract).

Validation (baseline vs patched, same PoC, same module build flags):

| kernel | result |
|---|---|
| unpatched 6.5-DEVELOPMENT | 8/8 + 8/8 rounds: both UAF writes in poison (`run.log`, `run.2.log`) |
| patched (`git apply fix.diff` → `make nativekernel` → `installkernel` → reboot) | 0/8 rounds: poison intact, `VERDICT FIXED` (`run.fix.log`); +3 stability reruns and kldunload/kldload clean (`run.fix.stability.log`); marker work executes every round ⇒ normal workqueue function preserved |

`git apply --check` passes against the pristine `sys/` tree (fix authored
there, never applied to the audit copy; applied only inside the guest's
`/usr/src`).

## 5. Environment

See `env.txt`: DragonFly 6.5-DEVELOPMENT #0/#1 X86_64_GENERIC, cc 8.3,
drm.ko loaded explicitly (`kldload drm`). Baseline runs on the pre-fix
kernel (kernel #0 Jul 2), fix runs on kernel #1 Sep 30 16:10
(`kern.version` in `dmesg.fix.txt`).

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