# DF-5059 — VERDICT

**Status: REPRODUCED** (kernel-space primitive + unprivileged trigger, on the
stock-kernel QEMU guest, DF-5008 methodology), High severity, certain
confidence.

## The bug, traced in sys/ (no guessing)

* `drm_sysctl.c:268` — the handler takes `DRM_LOCK(dev)`.
* `drmP.h:114-115` — `DRM_LOCK(dev)` = `lockmgr(&dev->struct_mutex, LK_EXCLUSIVE)`.
* `drm_device.h:88-89` / `drm_file.h:243` — `filelist` is protected by
  `filelist_mutex` (a *different* lock).
* `drm_file.c:514-516` (drm_open) — `list_add(&priv->lhead, &dev->filelist)`
  under `mutex_lock(&dev->filelist_mutex)` only.
* `drm_file.c:380-382` (drm_close) — `list_del(&file_priv->lhead)` under
  `filelist_mutex` (+ `drm_global_mutex`) only; then `drm_file.c:614` →
  `drm_file_free()` → `kfree(file)` at `drm_file.c:267`, no RCU/grace period.
* `drm_sysctl.c:271` — count walk sizes `privcount`; `:274` —
  `kmalloc(sizeof(struct drm_file) * privcount, M_DRM, M_WAITOK|M_NULLOK)`
  (M_WAITOK sleeps — the attacker-stretchable window);
  `:281-282` — copy walk `tempprivs[i++] = *priv;` bounded by the LIST.

Nothing serializes the two walks against open/close.  Growth between the
walks ⇒ heap overflow write (544 B/elem, pointer-rich, semi-controlled);
close between/during the walks ⇒ UAF read of a freed drm_file (copied and
partially printed).

Contrast: `drm_vm_info`'s `maplist` walk IS correctly serialized — every
maplist mutation takes `struct_mutex` (`drm_bufs.c:347,464,561,574,611…`),
so only the known DF-2075 lock-leak applies there.  `amdgpu_gem.c:97-113`
shows the correct in-tree precedent for walking `filelist` (under
`filelist_mutex`).

## What ran on the guest (stock X86_64_GENERIC, no custom kernel needed)

`clirace.ko` (sources in `kernel/`): the verbatim handler body with marked
[ADAPT]ations (fake dev carrying only `filelist`; observation shadow behind
the exact-size kmalloc; a sync window that models opens landing in the
M_WAITOK sleep — an interleave the shipped code permits), plus a churn
thread executing the `drm_file.c:514-516` critical section verbatim under
`filelist_mutex`.  Because the mutator already takes the *correct* lock, the
demonstration is airtight: the walker simply never takes it.

Decisive results (full text in `run.log`, raw console in `dmesg.txt`):

| Scenario | Result |
|---|---|
| S1 sync+shadow (root-driven) | `OOB WRITE copies=6 counted=4 (+2 elems x 544B) … pid=40002 magic=0x6d610002 … w3=fffff801179e7ed8` (kernel heap ptr in the spilled element) |
| S4 sync+fix pattern | `fixmode: copies=4 == counted=4 (churn blocked=1) -- IN BOUNDS` |
| S3a unpriv deterministic (uid=1001 sysctl read) | `clients pass: copies=6 counted=4  <-- OOB WRITE` — every read |
| S3b organic (uid=1001, 100k reads, free-running churn) | `rounds_oob=138 oob_max_elems=4`; leaked kernel module pointer `ffffffff831ded20` in an OOB element; hit rate varies 7–138/100k across boots |
| S2 groomed exact-size (2176 B) in guard hole | `GUARD DAMAGE … guard[9] word 0: expected a5a5000900000000 got 0000000000000000` — **real cross-object kernel-heap corruption**; guest survived (victim was a harness guard) |

`sizeof(struct drm_file) = 544` on this build.

## Why this is a faithful model of the production path

* The sysctl OID `hw.dri.N.clients` is world-readable (`CTLFLAG_RD`,
  `drm_sysctl.c:90-99`) — the trigger needs no privileges; S3a executed it
  as uid=1001.
* The mutator side (open/close) requires only `video`-group membership on
  production systems (`/dev/dri/cardN` is `root:video 0660`,
  `drm_drv.c:1178` + `DRM_DEV_MODE` in `drmP.h`).
* The copy walk is bounded by the list, exactly as shipped — the harness
  changed no loop bounds.
* The kmalloc is the real `kmalloc(…, M_DRM, M_WAITOK|M_NULLOK)`; S2's
  2176-byte allocation is byte-for-byte what the driver allocates for
  privcount=4.

## Exploitability

The primitive is a repeatable, unprivileged, pointer-rich heap overflow write
into an adjacent M_DRM object of the attacker's sizing class (steerable via
privcount = number of open DRM clients).  The UAF-read side additionally
exposes stale `pid`/`magic` of freed clients.  On the audit guest no DRM
device exists (no GPU), so a full uid=0 chain on the live path could not be
exercised end-to-end; the chain on real hardware is the standard one
(open/close storm to size privcount → groom M_DRM size class → victim object
adjacent to tempprivs → overwrite with drm_file copies containing kernel
pointers).  Even unsophisticated use is a reliable local panic/DoS.

## Fix validation

* Compile: `fix.diff` (swap to `mutex_lock(&dev->filelist_mutex)` around both
  walks) applied to the guest's `/usr/src`; `drm.ko` rebuilt with `-Werror` —
  rc=0, 0 errors/warnings (`fix_build.log`, 105 compile lines, drm_sysctl.c
  recompiled at the `-c …/drm_sysctl.c` line); patched module loads
  (`PATCHED_DRM_LOADED`).
* Behavioral: S4 executed the identical locking pattern with the real
  lockmgr locks in-kernel — mutator blocked (`churn blocked=1`), count ==
  copies, zero out-of-bounds writes, while the same interleave without the
  lock overflows every time (S1/S3a/S3b/S2).

## Honest notes / deviations

* The guest has no DRM hardware, so the literal `hw.dri.N.clients` OID and
  real `drm_open()`/`drm_close()` could not be used; the harness reproduces
  the exact code shapes with the real allocators instead (same methodology
  as the accepted DF-5008 verification).
* Harness iterations that did NOT count as reproduction: one self-inflicted
  NULL-deref panic in my first teardown (my bug, `dmesg` in early session),
  one livelock from an unbounded churn loop (my bug, redesigned to bounded
  bursts), a stale-module run and a missing-forward-decl build failure —
  all documented in the shell history; the decisive artifacts above are
  from the final clean sequence.
* `kldunload drm` of an idle drm core module wedged the guest once
  (drm-core teardown, unrelated to this finding); the guest is reverted
  after evidence capture regardless.

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