β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4786

NULL-pointer write in drm_atomic_normalize_zpos when a zpos-changed plane's CRTC is absent from the atomic transaction β€” kernel panic via DRM_IOCTL_MODE_OBJ_SETPROPERTY/MODE_ATOMIC on any normalize_zpos driver

Summary

get_new_crtc_state returns NULL when the CRTC is not in the transaction; a plane-only mutable-zpos change on an already-bound plane produces exactly that shape and zpos_changed=true is written through NULL (offset 0xa). LIVE-REPRODUCED on guest via kld calling the stock drm.ko export with the exact ioctl-built state: fatal trap 12, 'orb $0x10,0xa(%rax)'; fix validated on rebuilt drm.ko (SURVIVED). Latent in-tree (no DF driver sets normalize_zpos today; same bug still in Linux mainline). NULL-guard fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4786 Β· 16 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 5.0 KB ↓ raw
zposnull.c β€” 4.4 KB view raw
Makefile β€” 348 B ↓ download
build.sh β€” 106 B view raw
run.sh β€” 278 B view raw
build_patched.sh β€” 963 B view raw
run_patched.sh β€” 539 B view raw
build.log β€” 6.2 KB view raw
drm-build-patched.log β€” 123.7 KB view raw
run.log β€” 944 B view raw
run.patched.log β€” 658 B view raw
panic.txt β€” 822 B view raw
env.txt β€” 632 B view raw
fix.diff β€” 707 B view raw
verdict.json β€” 3.9 KB view raw

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:

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)
VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src/sys/dev/drm/drm_blend.c, drm.ko rebuilt from source (module-only), identical PoC re-run: no panic, module load returns 0, guest stays up (run.patched.log). Baseline stock drm.ko panicked identically before the fix (panic.txt).

run.patched.log ('zposnull: SURVIVED drm_atomic_normalize_zpos', 'KLDLOAD_RETURNED_0 (no panic)'); drm-build-patched.log; panic.txt (baseline); build_patched.sh / run_patched.sh exact commands
↓ fix.diffper-fix-DF-4786

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt: 'Stopped at drm_atomic_normalize_zpos+0x6b: orb $0x10,0xa(%rax)', 'fault virtual address = 0xa', 'supervisor write data, page not present'; run.patched.log: 'zposnull: SURVIVED drm_atomic_normalize_zpos' + 'KLDLOAD_RETURNED_0 (no panic)' with fix.diff-applied drm.ko; VERDICT.md for the full chain

PoC changes

PoC authored fresh for this finding (kld module zposnull.c + Makefile mirroring the drm module's include flags; needed SRCS+=opt_ddb.h opt_drm.h bus_if.h device_if.h pci_if.h for generated headers and removal of a redundant extern already declared in drm_blend.h)

Verified recommended fix

In drm_atomic_normalize_zpos, return -EINVAL when drm_atomic_get_new_crtc_state() returns NULL before writing zpos_changed (see fix.diff)

Verdict

NULL-pointer write in drm_atomic_normalize_zpos() (drm_blend.c:502-503) proven live: calling the stock drm.ko's exported drm_atomic_normalize_zpos with the exact state shape a plane-only zpos property change produces (plane zpos 0->1, plane bound to crtc index 0, crtcs[0].new_state==NULL) panics instantly at drm_atomic_normalize_zpos+0x6b: orb $0x10,0xa(%rax) - fault VA 0xa, supervisor write - a bit-level match for new_crtc_state->zpos_changed=true with new_crtc_state==NULL. drm_atomic_get_new_crtc_state() is by contract NULL for CRTCs outside the transaction (drm_atomic.h:441) and nothing in the ioctl path (set_property_atomic drm_mode_object.c:458, drm_atomic_check_only drm_atomic.c:961) materializes the CRTC state for a plane-only transaction. Reachable from DRM_IOCTL_MODE_OBJ_SETPROPERTY / MODE_ATOMIC by the DRM-master session user on any driver that sets mode_config.normalize_zpos (drm_atomic_helper.c:916-920); NO in-tree DF driver sets that flag yet, so the in-tree trigger is latent - the PoC kld substitutes the driver half and exercises the unmodified kernel half. Impact ceiling: local DoS (fixed-offset NULL write, no controlled corruption). fix.diff (NULL check + -EINVAL) validated: patched drm.ko rebuilt from source, same PoC SURVIVES, guest stays up.