Use-after-free race on non-refcounted DRM_MODE_OBJECT_MODE objects in drm_mode_obj_set_property_ioctl β freed mode reclaimed with attacker data drives a fully controlled properties/property/dispatch chain ending in a controlled indirect call (plane->funcs->set_property)
Summary
SETPROPERTY (DRM_MASTER|DRM_UNLOCKED) uses find()'s object with no ref and no lock; MODE/CRTC/PLANE/PROPERTY classes have no kref, and MODE objects are destroyed at RUNTIME by unpriv GETCONNECTOR reprobes (stale-mode prune -> unregister+kfree). A racing master SETPROP derefs the freed 240-byte block: properties walk -> fake property -> property->dev -> legacy dispatch plane->funcs->set_property = attacker-chosen function pointer. GUEST-VERIFIED with stock drm.ko: 110-295 spontaneous UAF-window hits per 30-60s; forced-interleave fired the canary indirect call through the stock dispatch (14-15/run) and panicked at groomed targets (fp 0x4141..., property->dev 0x5151...); honest limit: the 3-instruction compiled window did not hit spontaneously in ~7.3e10 calls. Hold-mode_config.mutex-across-find fix validated A/B (0 hits in 4.2e8 forced; 2e9 real calls, no deadlock).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4840 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 5.2 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| modeuaf.c | β | 14.7 KB | view raw | |
| Makefile | β | 346 B | β download | |
| build.sh | β | 95 B | view raw | |
| run.sh | β | 251 B | view raw | |
| build_patched.sh | β | 952 B | view raw | |
| build.log | β | 1.6 KB | view raw | |
| run.log | β | 887 B | view raw | |
| run.2.log | β | 166 B | view raw | |
| run.3.log | β | 1.0 KB | view raw | |
| panic.txt | β | 4.8 KB | view raw | |
| panic.2.txt | β | 2.2 KB | view raw | |
| env.txt | β | 641 B | view raw | |
| fix.diff | β | 1.0 KB | view raw | |
| verdict.json | β | 6.9 KB | view raw |
DF-4840 β use-after-free race on non-refcounted mode objects in drm_mode_obj_set_property_ioctl()
(sys/dev/drm/drm_mode_object.c:504-533)
What
drm_mode_obj_set_property_ioctl() looks up the target object with
drm_mode_object_find() and then uses the returned pointer with no reference
and no lock:
515: arg_obj = drm_mode_object_find(dev, file_priv, arg->obj_id, arg->obj_type);
517: if (!arg_obj) return -ENOENT;
519: if (!arg_obj->properties) goto out_unref; /* UAF READ */
522: property = drm_mode_obj_find_prop_id(arg_obj, ...); /* walks freed memory */
526: if (drm_drv_uses_atomic_modeset(property->dev)) /* via fake property */
527/529: set_property_atomic/legacy(arg_obj, property, value);
532: drm_mode_object_put(arg_obj); /* UAF READ (free_cb) */
__drm_mode_object_find() only takes a kref when obj->free_cb != 0
(drm_mode_object.c:145-148). The classes registered without a free_cb are
MODE (drm_mode_create, drm_modes.c:74), CRTC (drm_crtc.c:296), PLANE
(drm_plane.c:193) and PROPERTY (drm_property.c:117).
DRM_MODE_OBJECT_MODE objects are freed at runtime, by unprivileged
userspace: DRM_IOCTL_MODE_GETCONNECTOR with count_modes==0 (flags
DRM_UNLOCKED, no master needed β drm_ioctl.c:663) runs
connector->funcs->fill_modes() under mode_config.mutex
(drm_connector.c:1759-1763) β drm_helper_probe_single_connector_modes()
marks all old modes MODE_STALE (drm_probe_helper.c:415-417) and prunes them
(drm_probe_helper.c:514) β drm_mode_prune_invalid() β drm_mode_destroy()
= drm_mode_object_unregister() + kfree (drm_modes.c:90-98).
drm_mode_obj_set_property_ioctl() takes no lock at any point
(compare drm_mode_obj_get_properties_ioctl(), which correctly holds
drm_modeset_lock_all, drm_mode_object.c:392) β so a concurrent reprobe
frees the object between the find and every subsequent use. With the freed
240-byte drm_display_mode block reclaimed by attacker-influenced data,
obj->properties (block+24) becomes an attacker-chosen pointer; the walk at
:522 consumes a fully attacker-fabricated drm_object_properties and
drm_property; the legacy dispatch reaches plane->funcs->set_property()
(drm_plane.c:462-463) where both plane (container_of the freed block) and
funcs (block+112) are attacker data β controlled indirect call.
(On this no-SMAP guest the fabricated structures could equally live in
userland pages.)
Threat model
Attacker needs one fd with DRM master for the SETPROPERTY side (session compositor user, or any first opener on a headless system β same model as the verified DF-4786) plus any unprivileged fd for the GETCONNECTOR destroy side.
Proof (this directory)
modeuaf.ko β fabricated minimal drm_device (the "card" half the guest lacks;
DF-4786 precedent) + two threads racing the stock drm.ko functions:
- Destroyer (models the reprobe):
mutex_lock(mode_config.mutex)βdrm_mode_destroy()β unlock β groom (kzalloc same-size block, plant fakeproperties/type/plane->funcs, kfree) βdrm_mode_create()(idr reuses the same id, kmalloc reuses the same block). - Ioctl thread: loops the stock
drm_mode_obj_set_property_ioctl()/ an observation probe of its exact findβobj->propertieswindow.
Modes (sysctl hw.modeuaf.mode):
| mode | what | result |
|---|---|---|
| 1 | stock ioctl, no instrumentation | 7.3e10 calls, 0 spontaneous hits (window β3 instructions; see run.3.log) |
| 4 | same window + one atomic-counter store (observation) | 110β295 spontaneous UAF window hits per 30β60 s |
| 5 | forced interleave (yield exactly at the preemption point between :515 and :519), then the verbatim :519-532 dispatch | CONTROLLED INDIRECT CALL canary fires, dispatch ret=0, repeatedly |
| 7 | same chain, property->dev = 0x5151515151515100 |
GPF at attacker-chosen deref (panic.2.txt) |
| 8 | same chain, plane->funcs->set_property = 0x4141414141414140 |
GPF in stock drm_mode_plane_set_obj_prop+0x36 via groomed function pointer (panic.txt) |
| 9 | fixed sequence (mode_config.mutex held across findβread, as in fix.diff) | 0 hits in 4.2e8 forced iterations (fix validation) |
Fix validation
build_patched.sh applies fix.diff (hold mode_config.mutex across
findβdispatch; CANRECURSE makes the nested drm_modeset_lock_all() in
set_property_legacy() safe) to guest /usr/src, rebuilds drm.ko.
- objdump: patched ioctl acquires the lock (lea 0x2c8(%rdi); mov $0x2,%esi;
call lockmgr) before the inlined find β see VERDICT.md.
- A/B on the patched load: mode 5 (old sequence) still hits (36 hits /
14 controlled calls β the harness races); mode 9 (fixed sequence) 0
hits; mode 1 through the real patched ioctl: 2e9 calls, no deadlock,
no regression.
Run
./build.sh # build modeuaf.ko
kldload drm # stock
kldload ./modeuaf.ko # default mode 1
sysctl hw.modeuaf.mode=4; sleep 30 # spontaneous window hits on console/dmesg
sysctl hw.modeuaf.mode=5; sleep 5 # controlled indirect call (canary)
sysctl hw.modeuaf.mode=8 # panic in stock drm_plane code (controlled fp)
# teardown: sysctl hw.modeuaf.stop=1; kldunload modeuaf
DF-4840 β VERDICT
Finding: use-after-free race on non-refcounted mode objects in
drm_mode_obj_set_property_ioctl() (sys/dev/drm/drm_mode_object.c:504-533),
with a fully attacker-controlled pointer chase ending in a controlled
indirect call through the freed+reclaimed object.
Reproduced? YES β with precise scoping of what was and was not demonstrated
All DRM functions exercised are the stock /boot/kernel/drm.ko symbols
(nm: drm_mode_obj_set_property_ioctl, drm_mode_object_find,
drm_mode_create, drm_mode_destroy, drm_mode_plane_set_obj_prop).
The kld only fabricates the drm_device/driver the guest lacks (no
/dev/dri on the audit guest; DF-4786 precedent) and drives the race.
1. The unprotected window is real and is hit spontaneously (mode 4)
Probe = stock drm_mode_object_find() + the ioctl's exact
obj->properties read, with one added atomic-counter store between them
(so the window is observable; it is ~10-20x wider than the compiled
ioctl's). Destroyer = stock drm_mode_destroy() under
mode_config.mutex (as fill_modes runs it) + groom
(kzalloc same-size, plant, kfree β stand-in for the userspace heap
spray; on this no-SMAP guest the planted pointers could be userland
addresses).
hw.modeuaf.calls: 1138728638 finds: 112481917 destroys: 59734211 hits: 295 (60 s) hw.modeuaf.calls: 42494140360(=40.5e9 mode1 + 30 s probe) hits: 110 (30 s probe)
dmesg: modeuaf: UAF WINDOW HIT (spontaneous): find() returned freed+groomed
block; obj->properties attacker-controlled β 110-295 times per run.
That is: the stock find handed out a live mode pointer, the mode was
destroyed (unregister+kfree) before the read, the block was reclaimed with
attacker data, and the read returned the attacker value.
2. The stock compiled ioctl window did NOT hit spontaneously (honest negative)
objdump -d /boot/kernel/drm.ko shows the window is 3 instructions
(callq find; test rax,rax; mov 0x8(%rax),%rdx β
drm_mode_obj_set_property_ioctl+0x3b..+0x47). Across ~7.3e10 stock ioctl
calls (runs A-D in run.3.log; incl. B-thread pinned to the IRQ cpu under a
100k-packet host ping flood) β 0 spontaneous stock-window hits on this
QEMU/KVM guest. The window exists (proven structurally and by the probe);
its 5 ns compiled form simply was never preempted in-window here. On real
hardware with real interrupt loads the rate is of the class that hits β
but this pack does NOT claim a spontaneous stock-ioctl hit.
3. Full consequence chain under forced interleave (mode 5)
lwkt_yield() placed exactly where a preemption of the ioctl thread
between drm_mode_object.c:515 and :519 would occur; then the verbatim
:519-532 sequence: obj->properties β fabricated
drm_object_properties (count=1, properties[0] β fabricated
drm_property with matching id) β property->dev β stock
drm_mode_plane_set_obj_prop() (obj->type planted as
DRM_MODE_OBJECT_PLANE) β plane->funcs->set_property() where funcs
was planted at freed-block+112:
modeuaf: CONTROLLED INDIRECT CALL plane=0xfffff8008f8af4a8 property=0xfffff80089996a48 value=0x0 modeuaf: verbatim dispatch ret=0 (canary fired above if 0)
14-15 chains per 6-8 s run. The stock dispatch returns success through 100% attacker-fabricated objects. (The destroyer is quiesced during the chained dispatch β modeling the attacker-owned heap where the planted reallocation persists; the findβfreeβgroomed-read race itself is genuine either way.)
4. Panic at attacker-chosen data (modes 8 and 7)
- mode 8:
plane->funcs->set_property = 0x4141414141414140βFatal trap 9: general protection fault,Stopped at drm_mode_plane_set_obj_prop+0x36: retβ the controlled indirect call faulted inside the stock drm.ko function (panic.txt). - mode 7:
property->dev = 0x5151515151515100β GPF at the attacker-chosen dereference ofproperty->dev(panic.2.txt,probe_window+0x176: movq 0x20(%rdx),%rax).
5. Fix validation β PASSED
fix.diff holds mode_config.mutex (the lock all runtime mode-destroy
paths run under) across findβdispatch in the real ioctl.
- Patched drm.ko rebuilds clean (drm-build-patched.log).
- objdump: lockmgr acquisition (lea 0x2c8(%rdi); mov $0x2,%esi; callq)
precedes the inlined find in the patched ioctl.
- A/B on the patched load: old sequence (mode 5) still hits β 36 hits /
14 controlled calls (harness control); fixed sequence (mode 9):
0 hits in 4.2e8 forced iterations; real patched ioctl (mode 1):
2e9 calls, 60.8e6 destroys, no deadlock, no regression.
Exploit ceiling (uid0 direction, documented β not completed)
The chain gives: controlled kernel deref chain + controlled indirect
call target. On this no-KASLR/no-SMAP guest a full uid0 chain would
place the fabricated drm_object_properties/drm_property/drm_driver
structures in userland pages and pivot through the stock dispatch into a
write primitive; the realistic blocker is winning the stock 5 ns window
reliably plus obtaining stable kernel addresses for dev. This run
characterized the primitive (controlled call + controlled deref β panic)
and did not complete the privilege-escalation endgame; the in-tree
userspace trigger additionally requires a DRM card (mode objects must
exist), which this guest lacks β same "latent on the audit guest, live
on any real GPU system" posture as DF-4786.
Bottom line
Race-condition UAF with demonstrated: spontaneous window hits (probe), full attacker-controlled chain + controlled indirect call (forced interleave), and kernel panics at attacker-chosen addresses through stock drm.ko code. Severity Medium-High (master-gated ioctl; race-window dependent; requires a DRM device for the userspace trigger).
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, drm.ko rebuilt (drm-build-patched.log). Baseline (unfixed sequence, forced interleave): 36 hits / 14 controlled indirect calls. Patched-sequence control (mode_config.mutex across find->read, identical forced interleave): 0 hits in 4.2e8 iterations. Real patched ioctl: 2e9 calls, no deadlock/regression (mode_config.mutex is LK_CANRECURSE so set_property_legacy's drm_modeset_lock_all nesting is safe). objdump of the patched ioctl shows lockmgr acquisition before the inlined find.
findings/poc/DF-4840/fix.diff + build_patched.sh + drm-build-patched.log + VERDICT.md section 5
Confirmed kernel references
- sys/dev/drm/drm_mode_object.c:504
- sys/dev/drm/drm_mode_object.c:515
- sys/dev/drm/drm_mode_object.c:519
- sys/dev/drm/drm_mode_object.c:522
- sys/dev/drm/drm_mode_object.c:145
- sys/dev/drm/drm_modes.c:74
- sys/dev/drm/drm_modes.c:90
- sys/dev/drm/drm_connector.c:1759
- sys/dev/drm/drm_probe_helper.c:415
- sys/dev/drm/drm_probe_helper.c:514
- sys/dev/drm/drm_ioctl.c:663
- sys/dev/drm/drm_ioctl.c:679
- sys/dev/drm/drm_plane.c:462
Detail
Exploit chain
unpriv GETCONNECTOR (count_modes=0) destroys modes <-> DRM-master MODE_OBJ_SETPROPERTY(mode id) races find->use on the freed 240-byte drm_display_mode; heap groom reclaims the block so obj->properties (block+24) points at fabricated drm_object_properties; drm_mode_obj_find_prop_id walk (drm_mode_object.c:421-423) returns fabricated drm_property (id match); dispatch reads property->dev (controlled deref) and legacy path calls drm_mode_plane_set_obj_prop -> plane->funcs->set_property where plane is container_of(freed block) and funcs is block+112 (controlled indirect call). Demonstrated: controlled call canary + GPF panics at attacker-chosen addresses through stock drm.ko code. uid0 endgame (fabricated structures in userland pages on the no-SMAP/no-KASLR guest, pivot via stock dispatch to a write primitive) documented but not completed; requires a real DRM card for the pure-userspace trigger (guest has none - kld fabricated the environment, DF-4786 precedent).
Evidence (decisive lines)
run.log (mode4 spontaneous 110 hits/30s + mode5 15 CONTROLLED INDIRECT CALL lines, mode1 40.5e9-call contrast); panic.txt (mode8: 'Stopped at drm_mode_plane_set_obj_prop+0x36: ret' GPF via groomed fp 0x4141414141414140); panic.2.txt (mode7 GPF at property->dev=0x5151515151515100); run.3.log (stock-window 7.3e10-call honest negative incl. IRQ-cpu+ping-flood); VERDICT.md (A/B fix numbers: mode5 36 hits vs mode9 0 hits in 4.2e8 forced iterations; patched objdump lock-before-find); build_patched.sh + drm-build-patched.log
PoC changes
No seed pack existed. Authored from scratch: kld fabricates a minimal drm_device (idr_mutex/crtc_idr/mutex/connection_mutex, DRIVER_MODESET non-atomic driver) and races stock drm.ko functions; struct-offset pitfalls found and fixed during bring-up: drm_display_mode does NOT start with drm_mode_object (head[16] first, drm_modes.h:208-229 -> properties at block+24, not +8) and plane->funcs sits at base+96 within the 240-byte block; DFly kthread_create has no flags arg; linux mutex shim needs lockinit.
Verified recommended fix
Hold dev->mode_config.mutex across find->dispatch in drm_mode_obj_set_property_ioctl() (see fix.diff); longer-term, register mode objects with a free_cb/kref like connectors/fbs/blobs.
Verdict
drm_mode_obj_set_property_ioctl() (drm_mode_object.c:504-533) uses the object returned by drm_mode_object_find() with no reference and no lock. Objects registered without free_cb (MODE drm_modes.c:74, CRTC drm_crtc.c:296, PLANE drm_plane.c:193, PROPERTY drm_property.c:117) get no kref (drm_mode_object.c:145-148). DRM_MODE_OBJECT_MODE objects are destroyed at runtime by unprivileged DRM_IOCTL_MODE_GETCONNECTOR reprobes (drm_connector.c:1759 fill_modes under mode_config.mutex -> drm_probe_helper.c:415/514 prune -> drm_mode_destroy=unregister+kfree drm_modes.c:90-98) while the SETPROPERTY ioctl (DRM_MASTER-gated, drm_ioctl.c:679) holds no lock. Verified on the guest with stock drm.ko functions: (1) 110-295 SPONTANEOUS UAF window hits per 30-60s in an observation probe of the exact find->obj->properties window (run.log); (2) full attacker-controlled chain under forced interleave at the exact preemption point between :515 and :519 - fabricated drm_object_properties/drm_property consumed by the verbatim :519-532 dispatch, canary indirect call via plane->funcs (planted at freed-block+112) fired through STOCK drm_mode_plane_set_obj_prop, dispatch returned 0, 14-15 chains per run; (3) panic at attacker-chosen data: mode 8 = GPF 'Stopped at drm_mode_plane_set_obj_prop+0x36' via groomed function pointer 0x4141414141414140, mode 7 = GPF at property->dev=0x5151515151515100. Honest negative: the stock COMPILED ioctl window is 3 instructions (objdump) and never hit spontaneously in ~7.3e10 calls on this QEMU/KVM guest (run.3.log) - the race is certain, the spontaneous stock-window win-rate on this guest is below observable threshold. Fix validated: mode_config.mutex held across find->dispatch (fix.diff, CANRECURSE makes set_property_legacy's nested lock_all safe): patched drm.ko rebuilt, objdump confirms lock-before-find, A/B shows old sequence 36 hits/14 controlled calls vs fixed sequence 0 hits in 4.2e8 forced iterations, real patched ioctl 2e9 calls no regression.
No comments yet.