# DF-4786 — NULL-pointer write in drm_atomic_normalize_zpos (drm_blend.c:502-503)

## What

`drm_atomic_normalize_zpos()` iterates the planes of an atomic transaction.
For a plane whose `zpos` changed and which is bound to a CRTC it does:

```c
new_crtc_state = drm_atomic_get_new_crtc_state(state, crtc);   /* may return NULL */
new_crtc_state->zpos_changed = true;                           /* NULL write  */
```

`drm_atomic_get_new_crtc_state()` is documented (and implemented) to return
`NULL` "if the crtc is not part of the global atomic state"
(sys/dev/drm/include/drm/drm_atomic.h:441-447). A plane-only transaction that
only changes the plane's mutable `zpos` property (plane already bound to the
CRTC, no CRTC property in the transaction) produces exactly that state shape —
`drm_atomic_check_only()` (drm_atomic.c:961-1006) never materializes CRTC
states for planes, and `drm_atomic_set_crtc_for_plane()` returns early when
the CRTC is unchanged. The result is a write to `NULL + offsetof(zpos_changed)`
= `0xa` in the observed build.

## Trigger (who)

Unprivileged-ish local user holding DRM master (the desktop session user) on
any driver that sets `dev->mode_config.normalize_zpos` and exposes a mutable
zpos property:

- `DRM_IOCTL_MODE_OBJ_SETPROPERTY` on the plane's "zpos" property
  (drm_mode_object.c:458-502, `set_property_atomic()`), or
- `DRM_IOCTL_MODE_ATOMIC` (DRM_MASTER-gated, drm_ioctl.c:681) touching only
  the plane,

both run the full atomic check → `drm_atomic_helper_check()`
(drm_atomic_helper.c:916-920) → `drm_atomic_normalize_zpos()` → panic.

**In-tree status today: LATENT.** No in-tree DF driver currently sets
`mode_config.normalize_zpos` (only drm_atomic_helper.c:916 reads it), so no
in-tree userspace path reaches the deref — the finding is filed against the
core-code defect (one driver-flag away; DF syncs drm drivers from Linux
regularly, and the identical code is still in current Linux mainline).

## Proof (this directory)

The PoC substitutes for the missing "normalize_zpos driver" half and calls the
*stock, unmodified* `drm_atomic_normalize_zpos()` from drm.ko with the exact
state shape the ioctl path builds. Nothing in drm.ko is modified for the
baseline run.

- `zposnull.c` / `Makefile` — PoC kld (fabricated 1-plane/1-crtc atomic
  state: zpos 0→1, plane bound to crtc index 0, `crtcs[0].new_state = NULL`)
- `build.sh` — builds zposnull.ko on the guest (needs /usr/src)
- `run.sh` — baseline run: `kldload drm; kldload ./zposnull.ko`
- `panic.txt` — decisive console output (see below)
- `build.log` — full zposnull build output
- `fix.diff` — the fix (NULL check + `-EINVAL`), `git apply`-clean vs sys/
- `build_patched.sh` / `run_patched.sh` — fix validation (rebuilds drm.ko)
- `drm-build-patched.log` — full patched drm.ko build output
- `run.patched.log` — patched run: SURVIVED, no panic
- `env.txt` — guest uname, compiler, kldstat

## Expected output

Baseline (stock drm.ko):

    zposnull: calling stock drm_atomic_normalize_zpos (expect NULL-deref panic)
    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)

Patched (fix.diff applied, drm.ko rebuilt):

    zposnull: SURVIVED drm_atomic_normalize_zpos
    KLDLOAD_RETURNED_0 (no panic)
