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)
PoC verification
Evidence pack
findings/poc/DF-4786 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_SETPROPERTYon the plane's "zpos" property (drm_mode_object.c:458-502,set_property_atomic()), orDRM_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.kopanic.txtβ decisive console output (see below)build.logβ full zposnull build outputfix.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 outputrun.patched.logβ patched run: SURVIVED, no panicenv.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)
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)
- Built
zposnull.ko(this pack) against /usr/src β a kld that calls the unmodifieddrm_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) kldload drm(stock) β ok, 0xffffffff82601000 bbe000 drm.ko.-
kldload /root/zposnull/zposnull.koβ instant kernel panic, guest wedged atdb>, 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()callsdrm_atomic_normalize_zpos()whenevermode_config.normalize_zposis 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_ATOMICinto 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
vm.sh reset with-src(clean source), appliedfix.diff(NULL check +return -EINVAL) to guest/usr/src/sys/dev/drm/drm_blend.c.- Rebuilt drm.ko from source (drm-build-patched.log; module-only build, no kernel rebuild β the defect lives entirely in drm.ko).
-
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
fixedfix.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
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.
No comments yet.