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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4840 Β· 16 files
FileTypeDescriptionSize
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 fake properties/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->properties window.

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

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 of property->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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4840

Confirmed kernel references

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.