# DF-4786 — VERDICT

**Finding:** NULL-pointer write in `drm_atomic_normalize_zpos()`
(sys/dev/drm/drm_blend.c:502-503) when a plane in the atomic transaction has
a changed `zpos` while its CRTC has no state in the transaction.

## Reproduced? YES — kernel panic, then fix validated

### Baseline (stock kernel objects, guest DragonFly 6.5-DEVELOPMENT x86_64)

1. Built `zposnull.ko` (this pack) against /usr/src — a kld that calls the
   *unmodified* `drm_atomic_normalize_zpos()` exported by the stock
   `/boot/kernel/drm.ko`, passing the exact state shape produced by the ioctl
   path for a plane-only zpos change:
   - `dev->mode_config.num_total_plane = 1`, `num_crtc = 1`
   - `state->planes[0] = {plane, old(zpos=0), new(zpos=1, crtc=<fake crtc index 0>)}`
   - `state->crtcs[0].new_state = NULL` (CRTC not part of the transaction)
2. `kldload drm` (stock) → ok, 0xffffffff82601000 bbe000 drm.ko.
3. `kldload /root/zposnull/zposnull.ko` → **instant kernel panic**, guest
   wedged at `db>`, ssh dead. Serial console (panic.txt):

       zposnull: calling stock drm_atomic_normalize_zpos (expect NULL-deref panic)
       Fatal user address access from kernel mode from kldload at ffffffff8260c81b
       Fatal trap 12: page fault while in kernel mode
       fault virtual address    = 0xa
       fault code               = supervisor write data, page not present
       Stopped at      drm_atomic_normalize_zpos+0x6b: orb $0x10,0xa(%rax)

`orb $0x10,0xa(%rax)` with `%rax == 0` *is* the source statement
`new_crtc_state->zpos_changed = true;` (drm_blend.c:503): `zpos_changed` is
the `0x10` bit of the byte at offset `0xa` of `struct drm_crtc_state`, and
`new_crtc_state` is NULL. This is a bit-level match between the disassembly
and the cited source line — the finding is proven in the kernel, not merely
inferred from source.

### Why the PoC models the userspace path faithfully

- Plane-only zpos change: `set_property_atomic()` (drm_mode_object.c:458)
  or DRM_IOCTL_MODE_ATOMIC builds a state with the plane and its new zpos;
  nothing materializes the bound CRTC's state:
  - `drm_atomic_check_only()` (drm_atomic.c:961-1006) adds no CRTC states;
  - `drm_atomic_set_crtc_for_plane()` returns 0 early when the CRTC is
    unchanged (`plane_state->crtc == crtc`), so even transactions that
    restate CRTC_ID don't create the CRTC state;
  - `drm_atomic_get_new_crtc_state()` is by-contract NULL for a CRTC outside
    the transaction (drm_atomic.h:437-447).
- Driver entry point: `drm_atomic_helper_check()` calls
  `drm_atomic_normalize_zpos()` whenever `mode_config.normalize_zpos` is set
  (drm_atomic_helper.c:916-920) — this is the standard helper stack any zpos
  driver uses.

### Reachability honesty

- **In-tree today: latent.** No DF driver sets `mode_config.normalize_zpos`
  (grep: only drm_atomic_helper.c:916 reads it) and none of the zpos
  property creators are called in-tree (i915 uses only
  drm_plane_create_rotation_property). So no unprivileged userspace trigger
  exists on a stock DF install right now; the PoC legitimately substitutes
  the driver-flag half (root kldload) to exercise the stock kernel half.
- The kernel-side defect is real, one flag away, and matches code that is
  still identical in current Linux mainline (checked master 2026: no NULL
  check after drm_atomic_get_new_crtc_state in drm_atomic_normalize_zpos).
  Any future driver sync that enables zpos normalization turns a desktop
  session user's `MODE_OBJ_SETPROPERTY`/`MODE_ATOMIC` into a kernel panic.
- Impact ceiling: local DoS (panic) — a NULL write with no attacker-chosen
  address (offset is fixed at 0xa; no corruption of neighboring memory),
  gated on DRM master. Not an escalation primitive.

## Fix validation

1. `vm.sh reset with-src` (clean source), applied `fix.diff` (NULL check +
   `return -EINVAL`) to guest `/usr/src/sys/dev/drm/drm_blend.c`.
2. Rebuilt drm.ko from source (drm-build-patched.log; module-only build, no
   kernel rebuild — the defect lives entirely in drm.ko).
3. Rebuilt the *same* zposnull.ko PoC; loaded the patched drm.ko, then the
   PoC (run.patched.log):

       zposnull: calling stock drm_atomic_normalize_zpos (expect NULL-deref panic)
       zposnull: SURVIVED drm_atomic_normalize_zpos
       KLDLOAD_RETURNED_0 (no panic)

   Guest stayed up; no trap on the console. Baseline panicked, patched
   survives — fix confirmed.

## Classification

- status: **reproduced** (kernel-side NULL-write panic via stock drm.ko)
- impact: **panic** (in-tree trigger latent; realistic ceiling: local DoS by
  the DRM-master session user once a normalize_zpos driver exists)
- confidence: certain (source + disassembly-level proof + patched-build
  differential)

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