# DF-4840 — use-after-free race on non-refcounted mode objects in `drm_mode_obj_set_property_ioctl()`
(sys/dev/drm/drm_mode_object.c:504-533)

## What

`drm_mode_obj_set_property_ioctl()` looks up the target object with
`drm_mode_object_find()` and then uses the returned pointer **with no reference
and no lock**:

```c
515: arg_obj = drm_mode_object_find(dev, file_priv, arg->obj_id, arg->obj_type);
517:     if (!arg_obj) return -ENOENT;
519: if (!arg_obj->properties) goto out_unref;         /* UAF READ */
522: property = drm_mode_obj_find_prop_id(arg_obj, ...); /* walks freed memory */
526: if (drm_drv_uses_atomic_modeset(property->dev))     /* via fake property */
527/529:     set_property_atomic/legacy(arg_obj, property, value);
532: drm_mode_object_put(arg_obj);                      /* UAF READ (free_cb) */
```

`__drm_mode_object_find()` only takes a kref when `obj->free_cb != 0`
(drm_mode_object.c:145-148). The classes registered **without** a free_cb are
MODE (`drm_mode_create`, drm_modes.c:74), CRTC (drm_crtc.c:296), PLANE
(drm_plane.c:193) and PROPERTY (drm_property.c:117).

`DRM_MODE_OBJECT_MODE` objects are freed **at runtime, by unprivileged
userspace**: `DRM_IOCTL_MODE_GETCONNECTOR` with `count_modes==0` (flags
`DRM_UNLOCKED`, no master needed — drm_ioctl.c:663) runs
`connector->funcs->fill_modes()` under `mode_config.mutex`
(drm_connector.c:1759-1763) → `drm_helper_probe_single_connector_modes()`
marks all old modes MODE_STALE (drm_probe_helper.c:415-417) and prunes them
(drm_probe_helper.c:514) → `drm_mode_prune_invalid()` → `drm_mode_destroy()`
= `drm_mode_object_unregister()` + `kfree` (drm_modes.c:90-98).

`drm_mode_obj_set_property_ioctl()` takes **no** lock at any point
(compare `drm_mode_obj_get_properties_ioctl()`, which correctly holds
`drm_modeset_lock_all`, drm_mode_object.c:392) — so a concurrent reprobe
frees the object between the find and every subsequent use. With the freed
240-byte `drm_display_mode` block reclaimed by attacker-influenced data,
`obj->properties` (block+24) becomes an attacker-chosen pointer; the walk at
:522 consumes a fully attacker-fabricated `drm_object_properties` and
`drm_property`; the legacy dispatch reaches `plane->funcs->set_property()`
(drm_plane.c:462-463) where both `plane` (container_of the freed block) and
`funcs` (block+112) are attacker data → **controlled indirect call**.
(On this no-SMAP guest the fabricated structures could equally live in
userland pages.)

## Threat model

Attacker needs one fd with DRM master for the SETPROPERTY side (session
compositor user, or any first opener on a headless system — same model as the
verified DF-4786) plus any unprivileged fd for the GETCONNECTOR destroy side.

## Proof (this directory)

`modeuaf.ko` — fabricated minimal drm_device (the "card" half the guest lacks;
DF-4786 precedent) + two threads racing the **stock drm.ko functions**:

- **Destroyer** (models the reprobe): `mutex_lock(mode_config.mutex)` →
  `drm_mode_destroy()` → unlock → groom (kzalloc same-size block, plant
  fake `properties`/`type`/`plane->funcs`, kfree) → `drm_mode_create()`
  (idr reuses the same id, kmalloc reuses the same block).
- **Ioctl thread**: loops the stock `drm_mode_obj_set_property_ioctl()` /
  an observation probe of its exact find→`obj->properties` window.

Modes (`sysctl hw.modeuaf.mode`):

| mode | what | result |
|---|---|---|
| 1 | stock ioctl, no instrumentation | 7.3e10 calls, 0 spontaneous hits (window ≈3 instructions; see run.3.log) |
| 4 | same window + one atomic-counter store (observation) | **110–295 spontaneous UAF window hits per 30–60 s** |
| 5 | forced interleave (yield exactly at the preemption point between :515 and :519), then the verbatim :519-532 dispatch | **CONTROLLED INDIRECT CALL canary fires, dispatch ret=0, repeatedly** |
| 7 | same chain, `property->dev` = 0x5151515151515100 | **GPF at attacker-chosen deref** (panic.2.txt) |
| 8 | same chain, `plane->funcs->set_property` = 0x4141414141414140 | **GPF in stock `drm_mode_plane_set_obj_prop+0x36` via groomed function pointer** (panic.txt) |
| 9 | fixed sequence (mode_config.mutex held across find→read, as in fix.diff) | **0 hits in 4.2e8 forced iterations** (fix validation) |

## Fix validation

`build_patched.sh` applies `fix.diff` (hold `mode_config.mutex` across
find→dispatch; CANRECURSE makes the nested `drm_modeset_lock_all()` in
`set_property_legacy()` safe) to guest /usr/src, rebuilds drm.ko.
- objdump: patched ioctl acquires the lock (`lea 0x2c8(%rdi); mov $0x2,%esi;
  call lockmgr`) **before** the inlined find — see VERDICT.md.
- A/B on the patched load: mode 5 (old sequence) still hits (36 hits /
  14 controlled calls — the harness races); mode 9 (fixed sequence) **0
  hits**; mode 1 through the real patched ioctl: 2e9 calls, no deadlock,
  no regression.

## Run

```sh
./build.sh                      # build modeuaf.ko
kldload drm                     # stock
kldload ./modeuaf.ko            # default mode 1
sysctl hw.modeuaf.mode=4; sleep 30   # spontaneous window hits on console/dmesg
sysctl hw.modeuaf.mode=5; sleep 5    # controlled indirect call (canary)
sysctl hw.modeuaf.mode=8             # panic in stock drm_plane code (controlled fp)
# teardown: sysctl hw.modeuaf.stop=1; kldunload modeuaf
```
