# DF-4196 VERDICT

**Status: inconclusive (hardware-gated) — defect proven deterministically in a
faithful model harness on the DragonFly guest; in-kernel manifestation
requires DRM hardware the guest does not have.**
Impact class if it fires in-kernel: integer divide exception (#DE) → kernel
panic = local DoS. Not memory corruption; a uid=0 chain is not applicable to
this bug class.

## Root cause (path:line)

* `sys/dev/drm/drm_vblank.c:1169` — `vblank->hwmode.crtc_clock = 0;`
  executes **after** `spin_unlock_irqrestore(&dev->event_lock)` (:1165) and
  long after `lockmgr(&dev->vbl_lock, LK_RELEASE)` (:1150). No lock is held.
* `sys/dev/drm/drm_vblank.c:618` — sentinel check `if (mode->crtc_clock == 0)
  return false;`
* `sys/dev/drm/drm_vblank.c:637-641` — opaque external
  `dev->driver->get_scanout_position(dev, pipe, in_vblank_irq, &vpos, &hpos,
  &stime, &etime, mode)` — receives `mode`, so the compiler **must** re-load
  `mode->crtc_clock` after the call.
* `sys/dev/drm/drm_vblank.c:671-672` — `div_s64(..., mode->crtc_clock)` with
  the re-loaded value → 0 → #DE.

## Why the race window is real (full lock analysis)

Store side (modeset thread, `drm_crtc_vblank_off`):
| line | action | locks held |
|---|---|---|
| 1129 | event_lock | event |
| 1131 | vbl_lock | event+vbl |
| 1138 | drm_vblank_disable_and_save (vblank_time_lock inside; enabled=false) | event+vbl+vblank_time |
| 1150 | vbl release | event |
| 1155-1164 | flush queued vblank events | event |
| 1165 | event release | — |
| **1169** | **`vblank->hwmode.crtc_clock = 0`** | **— (LOCK-FREE)** |

Reader side — the only calc caller that holds **no** `event_lock`:
`drm_vblank_get` (:988 vbl_lock) → `drm_vblank_enable` (:946 vblank_time_lock,
all while vbl_lock held) → `drm_update_vblank_count` (:961) →
`drm_get_last_vbltimestamp` → `drm_calc_vbltimestamp_from_scanoutpos`
(inside the :618→:671 window for the duration of up to 3 MMIO
scanout-position queries — microseconds).

Other calc callers are safe by construction and were checked and killed:
* vblank IRQ handler `drm_handle_vblank` (:1695 event_lock, :1704 re-checks
  `vblank->enabled` under the lock) — serialized against :1129-1165, and if
  it arrives after :1165 it sees `enabled == false` and bails before the calc.
* `drm_crtc_arm_vblank_event` — asserts event_lock (callers hold it:
  e.g. amdgpu_dm.c:295 pflip handler holds `adev->ddev->event_lock`).
* `drm_crtc_accurate_vblank_count` callers: i915 CRC irq (i915_irq.c:1772)
  and amdgpu CRC work (amdgpu_dm_crc.c:150) hold no event_lock — **these are
  also exposed** to the same window, but are debugfs-gated (root arms CRC
  capture); the ioctl path above is the unprivileged-relevant one.

The `drm_vblank_get` reader can enter while the pipe is being shut down
because driver `->enable_vblank` hooks (e.g. i915's) merely unmask the
interrupt and can succeed during the disable window; if the pipe is fully
dead, `get_scanout_position` usually fails and the function returns before
the division — which is exactly why this needs the *timing* overlap, not a
logic guarantee.

## Reproduction (guest, DragonFly 6.5-DEVELOPMENT, cc 8.3)

```
$ cc -O2 -Wall -pthread -o vblank_toctou vblank_toctou.c
$ ./vblank_toctou race
[main] mode=race  (initial hwmode.crtc_clock=148500 kHz, htotal=2200)
[vblank_off] event_lock released; executing lock-free vblank->hwmode.crtc_clock = 0 (drm_vblank.c:1169)
Floating point exception (core dumped)          # rc=136 = SIGFPE = userland #DE
$ ./vblank_toctou race_fixed
[main] SURVIVED: delta_ns=1481818 (check passed BEFORE zero store, division used a cached-nonzero value)   # rc=0
```
Determinism: 3/3 `race` runs SIGFPE (rc=136); 3/3 `race_fixed` survive
(rc=0). See `run.log`.

The harness uses the verbatim expressions from :618/:671-672 (the division
reads the shared field, not a local) and the verbatim :1169 store; the MMIO
window is modeled by a barrier + spin inside a `noinline` opaque callee that
receives the mode pointer (preserving the forced reload property).

## Why not full kernel verification

The guest QEMU VM has no GPU: `ls /dev/dri` → No such file or directory;
`kldstat` shows only ehci/xhci. i915/radeon/amdgpu (the only DF drm drivers)
cannot bind, so neither the modeset side nor the query side of the race can
run in-kernel here. Model-harness + source-level proof is the maximum
achievable on this guest (same convention as DF-3869, the dce_calcs #DE
family).

## Fix validation

`fix.diff` (against the read-only sys/ tree) caches `crtc_clock`/`crtc_htotal`
into locals before the zero check and divides by the cached local — the
reloaded-field race becomes impossible; a store landing mid-window either
happens before the snapshot (calc sees 0 and fails gracefully) or after the
division. Validated in the harness: `race_fixed` survives 3/3 with a sane
`delta_ns`. A full DRM kernel build/boot test is not possible on this guest
(no GPU), so fix validation is harness-level: `fix_status: inconclusive`.

## Killed negatives (pass-2 sweep of the same file)

* Event lifecycle vs. file close (`drm_events_release` drm_file.c:184-204 +
  `drm_send_event_locked` drm_file.c:925-953): pending events are unlinked
  with `file_priv = NULL` and stay on `dev->vblank_event_list`; later freed
  under event_lock by `drm_send_event_locked` (`kfree` on `!file_priv`).
  Sound (modern dual-link design; the 2013-era vblank event UAFs are not
  present).
* vblank get/put refcount pairing incl. error paths (:959, :994, :1359,
  :1459/:1476, :1666, :1897/:1911) — balanced; lock order
  event_lock → vbl_lock → vblank_time_lock is consistent everywhere.
* `drm_handle_vblank` from the IRQ thread: lockmgr from DF ithread context is
  legal in the drm port (spin_lock_irqsave shim = local_irq_save +
  lockmgr, include/linux/spinlock.h:61-88); instant-off path (:1721-1730)
  matches upstream.
* `vblank_passed`/`widen_32_to_64` 64-bit arithmetic — sound.
* HZ conversions (`mod_timer` offdelay math :1037, `msecs_to_jiffies` :1082,
  `DRM_WAIT_ON 3*HZ` :1626) — int math, admin-only module params, no
  user-triggerable overflow.
* crc debugfs hooks: no drm_debugfs_crc.c exists in this tree — surface absent.

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