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

drm_atomic_state holds no drm_device reference while nonblocking commits outlive the ioctl on system_unbound_wq β€” missing backport of Linux 4e076c73e4f6 (stable-fix use-after-free)

Summary

state_init stores state->dev without drm_dev_get and __drm_atomic_state_free reads state->dev->mode_config without drm_dev_put, while a NONBLOCK atomic commit queues commit_work on the system-wide workqueue and the final state_put happens in the async tail after the ioctl returns. If the drm_device kref drops to zero in that window (kldunload of the loadable radeon/amdgpu/i915kms, or drm_dev_unplug), the commit tail operates on freed memory including an indirect call through freed config->funcs. Exact gap fixed upstream by stable-cc'd 4e076c73e4f6; file is otherwise v4.19-faithful. Unpriv DRM master supplies the pending-commit half; free half needs privilege. Backport fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4439 Β· 11 files
FileTypeDescriptionSize
probe_drm.c β€” 2.5 KB view raw
build.sh β€” 108 B view raw
run.sh β€” 95 B view raw
build.log β€” 159 B view raw
run.log β€” 380 B view raw
env.txt β€” 229 B view raw
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 4.2 KB ↓ raw
fix.diff β€” 1006 B view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 3.8 KB view raw

DF-4439 β€” drm_atomic_state holds no drm_device reference (missing backport of upstream 4e076c73e4f6)

sys/dev/drm/drm_atomic.c (DragonFly DRM port, Linux v4.19-faithful)

What

drm_atomic_state_init() (sys/dev/drm/drm_atomic.c:77) never takes a reference on the drm_device, and __drm_atomic_state_free() (sys/dev/drm/drm_atomic.c:251) never drops one β€” while a nonblocking atomic commit (drm_atomic_nonblocking_commit, sys/dev/drm/drm_atomic.c:1069, called from the DRM_MODE_ATOMIC ioctl at sys/dev/drm/drm_atomic_uapi.c:1368) queues state->commit_work onto the system-wide system_unbound_wq (sys/dev/drm/drm_atomic_helper.c:1701,1745). The ioctl then returns and the drm_atomic_state (and its state->dev pointer) is only released from the asynchronous commit tail.

Upstream fixed exactly this in Linux commit 4e076c73e4f6 ("drm/atomic: Fix potential use-after-free in nonblocking commits", Cc: stable, merged 2023): grab drm_dev_get() in drm_atomic_state_init() and drm_dev_put() in __drm_atomic_state_free(). That fix is absent here.

Consequence

If the drm_device is freed while a nonblocking commit is still in flight β€” driver unload (kldunload drm/radeon/amdgpu/i915kms, all built as modules in /boot/kernel) or any drm_dev_unplug() caller β€” the commit tail and the final drm_atomic_state_put() run drm_atomic_state_clear(state) and __drm_atomic_state_free() against freed memory:

  • drm_atomic_state_default_clear() reads state->dev->mode_config (drm_atomic.c:144) and walks config->num_crtc / num_total_plane,
  • __drm_atomic_state_free() calls config->funcs->atomic_state_free (drm_atomic.c:261) β€” an indirect call through freed memory.

Unprivileged half: any DRM master (the desktop user) can keep nonblocking commits queued. Privileged half: the unload. Upstream shipped the fix to stable because the drm_dev_unplugged() checks are by design racy against outstanding ioctls/work.

Verification on this audit guest

probe_drm.c was built and run on the DragonFly 6.5-DEVELOPMENT guest (cc 8.3): no DRM device node exists (/dev/drm*, /dev/dri/* absent); the DRM core module loads but no KMS driver in the port (i915/radeon/amdgpu) attaches to the QEMU std VGA (0x1234:0x1111). The race therefore cannot be driven on this guest β†’ verdict inconclusive (runtime), while the code defect itself is certain (verified by function-for-function diff against upstream v4.19 plus the stable-cc'd fix commit).

Fix

fix.diff is the direct backport of upstream 4e076c73e4f6 against sys/dev/drm/drm_atomic.c. Fix validation (build patched kernel + rerun) is not_testable for the same reason (baseline cannot run on this guest).

VERDICT.md
↓ download raw

VERDICT β€” DF-4439

Status: inconclusive (runtime) / defect certain (source) Impact ceiling: use-after-free β†’ kernel memory corruption (root-assisted race)

Was it reproduced?

No β€” and the reason is environmental, not evidentiary. The audit guest (DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC, QEMU std VGA 0x1234:0x1111) has no DRM device: probe_drm.c (pack) enumerates /dev/drm* and /dev/dri/* β€” all absent (run.log). The port's KMS drivers (i915, radeon, amdgpu) exist as loadable modules (/boot/kernel/radeon.ko, amdgpu.ko, …) but none binds the emulated VGA, so the DRM_IOCTL_MODE_ATOMIC entry path cannot even be opened here. drm.ko loads but creates no device without a driver attach.

Why the defect is certain anyway

  1. sys/dev/drm/drm_atomic.c is a function-for-function copy of Linux v4.19 drivers/gpu/drm/drm_atomic.c (verified by extraction diff β€” only two cosmetic deviations: %jx vs %llx at drm_atomic.c:554 and M_DRM in the krealloc at drm_atomic.c:726).
  2. Nonblocking commits run past the ioctl lifetime: * drm_mode_atomic ioctl β†’ drm_atomic_nonblocking_commit (sys/dev/drm/drm_atomic_uapi.c:1368 β†’ sys/dev/drm/drm_atomic.c:1080), * β†’ drm_atomic_helper_commit() nonblock path: INIT_WORK(&state->commit_work, commit_work) and queue_work(system_unbound_wq, &state->commit_work) (sys/dev/drm/drm_atomic_helper.c:1701,1745), * the last drm_atomic_state_put() for that state happens in the commit tail / completion β€” after the ioctl has returned.
  3. drm_atomic_state_init() (sys/dev/drm/drm_atomic.c:77-104) sets state->dev = dev with no drm_dev_get(); __drm_atomic_state_free() (sys/dev/drm/drm_atomic.c:251-266) dereferences state->dev->mode_config and calls config->funcs->atomic_state_free with no drm_dev_put(). The only references keeping drm_device alive are the open drm file (drm_file) and minors β€” exactly the situation upstream describes in 4e076c73e4f6: "the atomic modeset ioctl can be run in a nonblocking fashion, and in that case it cannot rely on the implied drm_device reference provided by the ioctl calling context. This can result in a use-after-free if a nonblocking atomic commit is carefully raced against a driver unload."
  4. drm_dev_get/drm_dev_put exist in the port (sys/dev/drm/drm_drv.c:761, :354; release frees via kref at :737), and the port carries the drm_dev_unplug/drm_dev_enter API (sys/dev/drm/drm_drv.c:360-400) β€” the backport applies cleanly.

The race (on real hardware)

  • Attacker A (unprivileged, DRM master β€” the logged-in desktop user): issue DRM_IOCTL_MODE_ATOMIC with DRM_MODE_ATOMIC_NONBLOCK in a tight loop, keeping commit work queued on system_unbound_wq.
  • Attacker B (root): kldunload radeon (or amdgpu/i915kms, all modules) at the same moment β€” or any future drm_dev_unplug() caller.
  • If the drm_device kref hits zero between the ioctl returning and the commit tail's drm_atomic_state_put(), __drm_atomic_state_free() and drm_atomic_state_default_clear() operate on freed memory, including an indirect call config->funcs->atomic_state_free(state) β€” classic UAF primitive (attacker-influenceable heap contents via slab reuse of the drm_device).

Both halves are needed, hence severity Medium rather than High (the free side requires privilege; DF default GENERIC does not auto-load these drivers β€” but every real GPU laptop/userland stack loads them).

Fix validation

fix.diff (this pack) is the exact backport of upstream 4e076c73e4f6. Baseline-vs-patched kernel testing is not_testable on this guest: the baseline itself cannot run (no DRM device). The patch is trivially reviewable against the upstream commit.

Guest state

Guest left clean (probe binary under /tmp, source under /root/poc; no kernel changes, no panic, no reset needed).

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff is the exact backport of upstream 4e076c73e4f6; baseline-vs-patched kernel validation cannot run because the guest has no DRM device (probe_drm.c run.log).

findings/poc/DF-4439/fix.diff; findings/poc/DF-4439/run.log
↓ fix.diffper-fix-DF-4439

Confirmed kernel references

Detail

Exploit chain

On real hardware: (1) unprivileged DRM master issues DRM_IOCTL_MODE_ATOMIC with DRM_MODE_ATOMIC_NONBLOCK in a loop, keeping commit_work pending on system_unbound_wq; (2) root kldunload's the KMS driver (radeon.ko/amdgpu.ko/i915kms.ko, all modules) or any future drm_dev_unplug() caller drops the last drm_device kref; (3) commit tail's drm_atomic_state_put() -> __drm_atomic_state_free() dereferences freed drm_device (state->dev->mode_config at drm_atomic.c:254) and performs an indirect call config->funcs->atomic_state_free (drm_atomic.c:261) through freed memory β€” UAF primitive; free side requires privilege, hence Medium.

Evidence (decisive lines)

run.log (probe: /dev/drm* and /dev/dri/* all absent on guest); VERDICT.md (full path:line chain + upstream commit match); fix.diff (backport of 4e076c73e4f6); env.txt (guest uname, no KMS driver bind)

PoC changes

No seed pack existed; wrote probe_drm.c from scratch to document DRM availability on the guest (the original race harness cannot run without hardware).

Verified recommended fix

Backport Linux 4e076c73e4f6: drm_dev_get(dev) in drm_atomic_state_init() and drm_dev_put(dev) in __drm_atomic_state_free() (see fix.diff)

Verdict

Source-level defect is certain: drm_atomic_state_init() (drm_atomic.c:77) never drm_dev_get()s the device and __drm_atomic_state_free() (drm_atomic.c:251) never drm_dev_put()s, while nonblocking atomic commits (drm_atomic_uapi.c:1368 -> drm_atomic.c:1080 -> drm_atomic_helper.c:1745 queue_work on system_unbound_wq) release the state only from the async commit tail, after the ioctl returned β€” the exact UAF fixed upstream by stable-cc'd Linux commit 4e076c73e4f6, whose backport (fix.diff) is absent here. Runtime reproduction on the audit guest is impossible: the QEMU std-VGA guest has no DRM device node and no port KMS driver (i915/radeon/amdgpu) attaches, verified by probe_drm.c (run.log), so the verdict is inconclusive rather than not_reproduced.