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)
PoC verification
Evidence pack
findings/poc/DF-4439 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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()readsstate->dev->mode_config(drm_atomic.c:144) and walksconfig->num_crtc/num_total_plane,__drm_atomic_state_free()callsconfig->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 β 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
sys/dev/drm/drm_atomic.cis a function-for-function copy of Linux v4.19drivers/gpu/drm/drm_atomic.c(verified by extraction diff β only two cosmetic deviations:%jxvs%llxat drm_atomic.c:554 andM_DRMin the krealloc at drm_atomic.c:726).- Nonblocking commits run past the ioctl lifetime:
*
drm_mode_atomicioctl β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)andqueue_work(system_unbound_wq, &state->commit_work)(sys/dev/drm/drm_atomic_helper.c:1701,1745), * the lastdrm_atomic_state_put()for that state happens in the commit tail / completion β after the ioctl has returned. drm_atomic_state_init()(sys/dev/drm/drm_atomic.c:77-104) setsstate->dev = devwith nodrm_dev_get();__drm_atomic_state_free()(sys/dev/drm/drm_atomic.c:251-266) dereferencesstate->dev->mode_configand callsconfig->funcs->atomic_state_freewith nodrm_dev_put(). The only references keepingdrm_devicealive 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."drm_dev_get/drm_dev_putexist in the port (sys/dev/drm/drm_drv.c:761, :354; release frees via kref at :737), and the port carries thedrm_dev_unplug/drm_dev_enterAPI (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_ATOMICwithDRM_MODE_ATOMIC_NONBLOCKin a tight loop, keeping commit work queued onsystem_unbound_wq. - Attacker B (root):
kldunload radeon(or amdgpu/i915kms, all modules) at the same moment β or any futuredrm_dev_unplug()caller. - If the
drm_devicekref hits zero between the ioctl returning and the commit tail'sdrm_atomic_state_put(),__drm_atomic_state_free()anddrm_atomic_state_default_clear()operate on freed memory, including an indirect callconfig->funcs->atomic_state_free(state)β classic UAF primitive (attacker-influenceable heap contents via slab reuse of thedrm_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_testablefix.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
Confirmed kernel references
- sys/dev/drm/drm_atomic.c:77
- sys/dev/drm/drm_atomic.c:95
- sys/dev/drm/drm_atomic.c:251
- sys/dev/drm/drm_atomic.c:254
- sys/dev/drm/drm_atomic.c:260
- sys/dev/drm/drm_atomic.c:1069
- sys/dev/drm/drm_atomic_uapi.c:1368
- sys/dev/drm/drm_atomic_helper.c:1701
- sys/dev/drm/drm_atomic_helper.c:1745
- sys/dev/drm/drm_drv.c:737
- sys/dev/drm/drm_drv.c:761
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.
No comments yet.