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

DRM_UNLOCKED dumb-buffer MAP_DUMB races the unsynchronized legacy GEM offset-hash publication: live-key free_unr double-free, same-node dual-bucket hlist link, and wrong-object resolution in the mmap path (cross-object / UAF-class mapping ceiling)

Summary

MAP_DUMB is dispatched lockless into drm_gem_create_mmap_offset_size whose publication has zero sync - two barrier-synced calls free the live unr key, dual-link one hash node, and the corrupted hash (the mmap resolver under DRM_LOCK) maps the WRONG object or a freed one. 1:1 kernel transliteration proves both interleavings deterministically (live-key free + double free + aliasing; dual-bucket + wrong-object resolution); PoC is hardware-ready but the guest has no DRM device. Serialize-with-object_name_lock fix (drm_gem.c).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5186 Β· 13 files
FileTypeDescriptionSize
README.md β€” 3.1 KB ↓ raw
VERDICT.md β€” 5.7 KB ↓ raw
mapdumb_race.c β€” 8.0 KB view raw
sim_interleave.c β€” 7.7 KB view raw
build.sh β€” 158 B view raw
run.sh β€” 121 B view raw
build.log β€” 150 B view raw
run.log β€” 65 B view raw
sim_output.txt β€” 1.2 KB view raw
env.txt β€” 292 B view raw
fix.diff β€” 2.0 KB view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 4.4 KB view raw

DF-5186 β€” unsynchronized GEM offset-hash publication raced via DRM_UNLOCKED MAP_DUMB

What

drm_mode_mmap_dumb_ioctl (sys/dev/drm/drm_dumb_buffers.c:115-130) is dispatched DRM_UNLOCKED|DRM_RENDER_ALLOW (sys/dev/drm/drm_ioctl.c:676) β€” the handler runs with no lock held. The default path drm_gem_dumb_map_offset β†’ drm_gem_create_mmap_offset and the i915 override (.dumb_map_offset = i915_gem_mmap_gtt, i915_drv.c:3301 β†’ i915_gem.c:2600-2602) both reach drm_gem_create_mmap_offset_size (sys/dev/drm/drm_gem.c:567-606), which publishes into the legacy per-device offset_hash with zero synchronization:

if (obj->on_map) return 0;              /* 573 */
obj->map_list.key = alloc_unr(...);     /* 576 */
ret = drm_ht_insert_item(...);          /* 577 β€” lockless hlist insert */
if (ret != 0) { free_unr(...); return ret; }
obj->on_map = true;                     /* 583 */

Two concurrent MAP_DUMB on the same handle corrupt it (see sim_interleave.c):

  • A both alloc_unr before both inserts β†’ losing insert returns -EINVAL and free_unr()s the live key; object teardown free_unr()s it again (drm_gem.c:541-542) β†’ unr index aliasing across two live objects;
  • B key write lands after the winner's insert β†’ the same drm_hash_item is linked into two hash buckets (cross-bucket hlist merge); drm_ht_find_item() then resolves the wrong item.

The corrupted hash is the authoritative mmap resolver: drm_gem_object_from_offset() (drm_gem.c:1063-1080) β†’ drm_gem_mmap_single() (drm_gem.c:1082-1109) β†’ container_of(hash, struct drm_gem_object, map_list). Wrong/dangling entries hand the device pager the wrong (or freed) GEM object β€” cross-object mapping (other process's GPU buffer) or a UAF-class mapping.

Files

  • mapdumb_race.c β€” guest PoC: churn create/map/destroy with a pair of barrier-synchronized MAP_DUMB racers; smoking gun = MAP_DUMB failing EINVAL ("failed to add to map hash" on console) and/or unr corruption panic. Also sweeps drm_mode_create_dumb size-math rows (all in-file checks verified correct in source).
  • sim_interleave.c β€” deterministic 1:1 transliteration of the kernel critical sections; proves both corruption interleavings.
  • fix.diff β€” serialize publish/teardown with dev->object_name_lock.

Build / run (on any DragonFly box, needs no root if /dev/dri/card0 is 0660 video)

cc -O2 -Wall -pthread -o mapdumb_race mapdumb_race.c
./mapdumb_race 20000        # iterations
cc -O2 -Wall -o sim_interleave sim_interleave.c && ./sim_interleave

Expected

  • On this audit guest (QEMU std-VGA, no DRM driver): exit code 2, [-] no /dev/dri node available β€” see run.log / env.txt.
  • On i915/radeon/amdgpu hardware: race counters; any EINVAL != 0 row reproduces the collision; sim_interleave prints the deterministic corruption proof (see sim_output.txt).

Honest status

inconclusive on this guest β€” the bug is code-certain (full trace in VERDICT.md) but the guest has no DRM device, so the kernel path cannot be exercised here. The PoC is ready for any hardware-bearing DragonFly machine.

VERDICT.md
↓ download raw

DF-5186 β€” VERDICT

status: inconclusive (target device absent on this guest) Β· reproduced: 0 Β· code-trace confidence: high (the corruption follows from the source with no assumptions beyond two concurrent ioctls on one fd)

Guest

DragonFly dfbsd 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026, X86_64_GENERIC, cc 8.3. QEMU std-VGA: no /dev/dri, no drm kld, zero drm lines in dmesg (env.txt). sys/dev/drm/drm/ contains only a Makefile β€” there is no virtual/software DRM driver in this tree (vgem absent), and QEMU cannot present Intel/AMD hardware. => the kernel race path cannot be entered on this guest by any means. Not a false positive: the path is live on every machine with i915/amdgpu/radeon KMS (see reachability below).

Code trace (why the bug is real)

  1. Entry, no lock: DRM_IOCTL_DEF(DRM_IOCTL_MODE_MAP_DUMB, drm_mode_mmap_dumb_ioctl, DRM_UNLOCKED|DRM_RENDER_ALLOW) (sys/dev/drm/drm_ioctl.c:676); DRM_UNLOCKED handlers are invoked directly, func(dev, kdata, file_priv) (drm_ioctl.c:777) β€” no per-fd serialization, no DRM_LOCK, no DRM_AUTH required (drm_ioctl_permit, drm_ioctl.c:535-557: only ROOT_ONLY/AUTH/MASTER/RENDER flags are checked; MAP_DUMB needs none beyond a non-render client being allowed).
  2. Handle β†’ object: drm_gem_object_lookup takes a ref under filp->table_lock (drm_gem.c:621-636) β€” safe, but the ref is the only thing held when the driver callback runs.
  3. Driver override (i915, the default install): .dumb_map_offset = i915_gem_mmap_gtt (i915_drv.c:3301) β†’ i915_gem_object_create_mmap_offset β†’ drm_gem_create_mmap_offset(&obj->base) (i915_gem.c:2600-2602), called with nothing held. radeon/amdgpu go through their ttm mmap-offset paths; the default drm_gem_dumb_map_offset (drm_gem.c:381-406) reaches the same function for any GEM driver that does not override.
  4. Unsynchronized publication: drm_gem.c:573-583 β€” on_map test-and-set, map_list.key = alloc_unr(), drm_ht_insert_item() (drm_hashtab.c:109-133, plain hlist insert, no internal lock), free_unr() on the -EINVAL path. Teardown mirror drm_gem_free_mmap_offset (drm_gem.c:529-546) is equally lockless around drm_ht_remove_item/free_unr.
  5. Consumer under a different lock: drm_gem_mmap_single takes DRM_LOCK(dev) for the hash lookup + object reference (drm_gem.c:1089-1098) β€” which does not serialize against the unlocked publishers in (4).
  6. Corruption consequences β€” proven deterministically by sim_interleave.c (1:1 transliteration; run on the guest, sim_output.txt): - interleaving A: loser free_unr()s the live key; teardown frees it again β†’ alloc_unr hands the same index to a second live object while the first object's hash entry still claims it; - interleaving B: the same drm_hash_item is linked into two buckets; drm_ht_find_item(1) returns objA's node although key 1 belongs to objB β†’ wrong-object resolution. In the kernel, container_of(hash, struct drm_gem_object, map_list) (drm_gem.c:1078) then maps the wrong object's pages into the caller (cross-object mapping = another process's GPU buffer readable/writable), and an entry left dangling by the delete race maps a freed object (UAF-class).

Exploit chain (hardware-bearing machine, unprivileged video-group user)

open /dev/dri/card0 (root:video 0660, drm_drv.c:1178-1179, drmP.h:108-110) β†’ DRM_IOCTL_MODE_CREATE_DUMB (no auth) β†’ pair of threads issuing barrier- synchronized DRM_IOCTL_MODE_MAP_DUMB on the fresh handle β†’ repeat with create/destroy churn to widen windows and recycle idxunr entries β†’ corrupted offset_hash β†’ craft mmap(card0_fd, DRM_GEM_MAPPING_KEY|DRM_GEM_MAPPING_OFF(idx)) offsets that resolve aliased/dangling entries β†’ read/write another process's GEM backing memory (info leak / cross-process corruption) or a freed object's pages (UAF). Detectable symptom during racing: MAP_DUMB EINVAL + [drm:drm_gem_create_mmap_offset_size] *ERROR* failed to add to map hash.

Why not reproduced here

No DRM device exists on the QEMU guest (std VGA; no in-tree driver binds it; no vgem). The PoC binary builds clean on the guest (build.log) and runs (run.log: exit 2, "no /dev/dri node available"), i.e. the trigger is correct but the kernel path is unreachable on this machine. The deterministic sim proves the interleaving logic that the kernel executes.

Fix validation

fix_status: not_applicable β€” the baseline itself cannot be exercised on this guest, so a patched-kernel A/B comparison is impossible here. fix.diff (serialize both critical sections with dev->object_name_lock, which is LK_CANRECURSE and sleepable) is line-accurate against sys/dev/drm/drm_gem.c:529-588 and should be validated on hardware: with the patch, concurrent MAP_DUMB never produces EINVAL from the map-hash insert and the unr pool never double-frees.

kernel_refs

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

Baseline cannot be exercised on this guest (no DRM device), so a patched-kernel A/B run is impossible here; fix.diff is line-accurate against drm_gem.c:529-588 and must be validated on DRM hardware (with the patch, concurrent MAP_DUMB can never lose the insert race, so no live-key free_unr / dual-link).

fix.diff
↓ fix.diffper-fix-DF-5186

Confirmed kernel references

Detail

Exploit chain

video-group user opens /dev/dri/card0 (0660 root:video, drm_drv.c:1178) -> CREATE_DUMB (no auth) -> two threads barrier-synchronized MAP_DUMB on the fresh handle, with create/destroy churn recycling idxunr -> corrupted offset_hash (double free_unr / dual-linked node) -> mmap(card0_fd, KEY|OFF(idx)) resolving an aliased or dangling entry -> wrong-or-freed GEM object mapped: cross-process GPU-buffer disclosure or UAF-class RW mapping. Ceiling not demonstrated: needs DRM hardware this guest lacks.

Evidence (decisive lines)

run.log (PoC exit 2 - no /dev/dri on guest), sim_output.txt (deterministic interleaving A: live-key free + double free_unr + index aliasing; interleaving B: same node in two buckets + drm_ht_find_item(1) returns objA -> wrong-object resolution), env.txt (guest uname/kldstat/no-dri), build.log, VERDICT.md full path trace

PoC changes

n/a (seed written from scratch in this run; self-contained uapi defs because the guest ships no drm headers; pair-barrier churn design so the race hits the first-MAP_DUMB window of every fresh object)

Verified recommended fix

Serialize drm_gem_create_mmap_offset_size/drm_gem_free_mmap_offset legacy offset-hash publication with dev->object_name_lock (sleepable, LK_CANRECURSE) - see fix.diff

Verdict

Code-certain race, target-device-absent on this guest. drm_mode_mmap_dumb_ioctl is dispatched DRM_UNLOCKED|DRM_RENDER_ALLOW (drm_ioctl.c:676, invoked locklessly at drm_ioctl.c:777); the i915 override (.dumb_map_offset=i915_gem_mmap_gtt, i915_drv.c:3301 -> i915_gem.c:2600-2602) and the GEM default both reach drm_gem_create_mmap_offset_size (drm_gem.c:567-606), whose on_map/map_list.key/drm_ht_insert_item/idxunr publication has no synchronization, while the same hash is the authoritative mmap resolver under DRM_LOCK (drm_gem.c:1063-1109). Two barrier-synced MAP_DUMB calls on one fresh handle deterministically corrupt state (proved by the 1:1 kernel transliteration in sim_interleave.c: live-key free_unr + teardown double-free => index aliasing; same drm_hash_item linked into two buckets => drm_ht_find_item returns the wrong object => cross-object/UAF mapping via drm_gem_mmap_single). The QEMU audit guest has no DRM device (std-VGA, no /dev/dri, no drm kld, no vgem in-tree), so the kernel path cannot be entered: PoC builds clean (cc 8.3) and runs, exiting 2 with 'no /dev/dri node available' (run.log). Ready to run on any i915/amdgpu/radeon DragonFly box; smoking gun = MAP_DUMB EINVAL + '[drm:drm_gem_create_mmap_offset_size] ERROR failed to add to map hash'.