drm_gem_mmap_single passes the user-controlled mmap length and in-object offset (bits 0..39 of the fake offset) to the GEM device pager with no bounds check β the only upstream clamp is dead in #if 0
Summary
The BSD mmap path hands TWO attacker-controlled quantities to cdev_pager_allocate with no validation against gem_obj->size: the mmap length (the pager GROWS the vm_object to foff+size instead of clipping) and bits 0..39 of the fake offset (the in-object byte offset - only the KEY is validated). The only upstream guard for exactly this sits dead inside #if 0 because DF replaced ->mmap with d_mmap_single without re-implementing it. Every fault in [maoff, maoff+size) reaches the driver pager unbounded; on i915 the already-filed DF-1146 converts it into cross-object GGTT read/write - this core-side fix closes the OOB for every BSD-path GEM driver at once. Distinct from DF-1146 (driver sink) and DF-1633 (access control). Clamp-before-pager fix, compile-validated on guest.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4458 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.3 KB | β raw | |
| VERDICT.md | β | 6.7 KB | β raw | |
| df4458.c | β | 4.7 KB | view raw | |
| run.sh | β | 104 B | view raw | |
| build.sh | β | 123 B | view raw | |
| run.log | β | 98 B | view raw | |
| env.txt | β | 630 B | view raw | |
| build.log | β | 3.4 KB | view raw | |
| fix.diff | β | 1.3 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 4.5 KB | view raw |
DF-4458 β drm_gem_mmap_single passes user-controlled mmap length and in-object offset to the GEM pager without any bounds check
Reproduce (on a GPU machine β e.g. any Intel i915 system)
cc -O2 -Wall -Wextra -o df4458 df4458.c ./df4458 /dev/dri/card0
Success criterion (vulnerable kernel): line [A] oversized mmap(β¦)
SUCCEEDED with non-OUT OF BOUNDS-safe page contents past page 1,
and/or [B] offset+mmap SUCCEEDED (mapping 2 MiB past a 1-page
object). With the core fix: both attacks return EINVAL.
On the audit QEMU guest
sh run.sh β no /dev/dri exists (QEMU bochs VGA, no KMS driver), so
the PoC fails at open(); runtime reproduction is impossible. The
defect is proven by source trace (VERDICT.md: vm_mmap.c:1377 β
drm_drv.c:1270 β drm_gem.c:1099 β device_pager.c:109 β i915_gem.c:2229,
with the only upstream clamp dead at drm_gem.c:944-946 #if 0).
Build (fix compile-validation)
sh build.sh (inside guest, fixed drm_gem.c staged) β drm.ko rebuilds
-Werror clean, RC=0 (build.log).
Files
df4458.cβ unprivileged trigger (dumb buffer + oversized mmap + forged in-object offset)VERDICT.mdβ full hop-by-hop data-flow proof + negative resultsfix.diffβ core-side clamp (git-apply-able; compile-validated)run.log,env.txt,build.log,run.sh,build.sh
DF-4458 β PoC Verification Verdict
File: sys/dev/drm/drm_gem.c (pass 2)
Related (NOT re-reported): DF-1633 (same function, missing per-file
ACL), DF-1146 (i915_gem_fault missing driver-side bounds check β the
exploitable sink of this core gap).
Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026
(X86_64_GENERIC), cc 8.3.
Verdict: INCONCLUSIVE at runtime (code defect Certain; GPU-gated)
Root cause (source-trace proof)
The BSD mmap path is:
mmap(2) -> vm_mmap (vm_mmap.c:1349-1394)
-> dev_dmmap_single(rdev, &foff, objsize, ...) vm_mmap.c:1377
-> drm_mmap_single (drm_drv.c:1256-1274) [cdevsw .d_mmap_single, drm_drv.c:1155]
-> drm_gem_mmap_single (drm_gem.c:1082-1109)
-> cdev_pager_allocate(gem_obj, OBJT_MGTDEVICE, gem_vm_ops,
size, nprot, MAPOFF(*offset), cred) drm_gem.c:1099-1101
-> (fault) i915_gem_fault(vm_obj, offset, ...) i915_gem.c:2229
Both user-controlled quantities reach the pager unvalidated:
sizeβ the mmap(2) length. vm_mmap passesobjsizethrough verbatim (vm_mmap.c:1377); drm_mmap_single forwards it (drm_drv.c:1270); drm_gem_mmap_single hands it straight to cdev_pager_allocate (drm_gem.c:1099). cdev_pager_allocate does NOT clip it β it grows the vm_object toOFF_TO_IDX(foff + size)(device_pager.c:109-127, esp. theobject->size = β¦growth branch), and i915'scdev_pg_ctoris a no-op that always returns 0 (i915_gem.c:6383-6389). A 1-page GEM object can back a terabyte of mapping.foff(in-object offset) β bits 0..39 of the fake offset. The ioctl-published offset isKEY | idx<<40with zero low bits (i915_gem_mmap_gtt, i915_gem.c:2647-2649), but nothing stops the user from adding any page-aligned N < 2^40 in the mmap() call: drm_gem_object_from_offset only checks the KEY (drm_gem.c:1070) and the 22-bit idx against the hash (drm_gem.c:1074-1077); MAPOFF strips KEY+idx and keeps N (drm_gem.h:45-46).
The only upstream guard for exactly this is
drm_gem_mmap_obj()'s if (obj_size < vma->vm_end - vma->vm_start)
return -EINVAL; β which sits dead inside #if 0 in this very file
(drm_gem.c:914-1041, check at 944-946) because DragonFly replaced the
Linux ->mmap path with d_mmap_single, without re-implementing the
check.
Every fault in [N, N+size) then reaches the driver pager with an
offset the core never proved is inside the object. On i915 (the only
DRIVER_GEM driver on DF that takes this path β radeon/amd go through
drm_ttm_bdev/ttm_bo_mmap_single instead, drm_drv.c:1266-1268), the
known-missing driver-side bound (DF-1146) turns that into
vm_phys_fictitious_to_vm_page(ggtt->gmadr.start + vma->node.start +
offset) β mapping GGTT aperture pages belonging to other objects
(cross-object/cross-user GPU memory read+write), or SIGBUS past the
aperture.
Core-side fix value: validating MAPOFF(offset) + size <=
gem_obj->size in drm_gem_mmap_single closes the OOB path for every
BSD-path GEM driver at once, independent of driver fault-handler
hygiene β which is why this is filed separately from DF-1146.
Reproduction attempt on the audit guest
df4458.c (this pack) is a complete unprivileged trigger: create a
1-page dumb buffer, obtain its fake offset via MODE_MAP_DUMB, then
(A) mmap 64x the object size at the base offset and walk pages, and
(B) mmap one page at fake + 2MiB (forged in-object offset). On a GPU
machine, any SUCCESS line demonstrates the missing bounds check
directly (patched kernel: EINVAL).
On the audit guest: cc -O2 -Wall -Wextra builds it clean (BUILD_RC=0,
run.log), but there is no DRM device β open("/dev/dri/card0") β
ENOENT (run.log; env.txt: no /dev/dri, drm.ko/i915.ko present but not
attached β the guest's only VGA is the QEMU bochs 1111:1234 vgapci
with no KMS driver). Runtime reproduction is therefore impossible on
this guest; the data-flow above is proven by source trace with every
hop cited. (Note: force-loading drm.ko on this guest was attempted and
wedges the loader with no hardware β not repeated; guest was reset
clean.)
Fix validation
fix.diff adds the missing clamp to drm_gem_mmap_single:
maoff = DRM_GEM_MAPPING_MAPOFF(*offset);
if (size > gem_obj->size || maoff > gem_obj->size - size) {
drm_gem_object_unreference_unlocked(gem_obj);
return (EINVAL);
}
Compile-validated on the guest: drm.ko rebuilt with the patch,
-Werror, RC=0 (build.log β combined with the sibling DF-4459/DF-4460
hunks, all three in drm_gem.c). git apply --check passes against the
pristine tree. Behavioral baseline/patched runs impossible on this
guest (no GPU), hence fix_status=not_testable, compile-validated.
Negative results (adjacent classes killed, pass 2)
drm_gem_open_ioctlargs->size = obj->sizeafter the final put (drm_gem.c:744-750): NOT a UAF β drm_gem_handle_create_tail's handle_count++ β drm_gem_object_get (line 450-451) keeps β₯1 ref alive past the put; verified count arithmetic.drm_gem_handle_create_tailerror unwind (drm_gem.c:484-492): balanced (idr_alloc-failure drops exactly the handle_count+ref taken at 450-451; revoke precedes idr_remove; object_name_lock is dropped exactly once at 465 on every path).drm_gem_flink_ioctlclose race (drm_gem.c:687-708): handle_count re-check under object_name_lock + the lookup ref make the name-publish vs last-handle-close race safe.drm_gem_object_releasevm_object_deallocate(NULL)for private objects (drm_gem.c:802): safe β vm_object_deallocate returns immediately on NULL (vm_object.c:617-618).drm_gem_handle_deleteidr_replace-NULL pattern (drm_gem.c:349-363): concurrent lookups see NULL and fail; double-decrement impossible.drm_gem_releaseidr_for_each + handle_put freeing objects during iteration: operates on file-private idr; name-idr removal happens under object_name_lock; DF idr destroy does not deref values.drm_gem_object_inituncheckeddefault_pager_allocresult (drm_gem.c:197-202): DF vm_object_allocate cannot return NULL (objcache-backed); acceptable.drm_gem_dmabuf_exportis NOT in this file (drm_prime.c:363) β the only export lifecycle piece here,drm_gem_object_exported_dma_buf_free(drm_gem.c:267-274), is correctly serialized under object_name_lock and only breaks the cycle at last-handle.
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 (git apply --check OK) adds the maoff+size <= obj->size clamp before cdev_pager_allocate. Compile-validated on the guest: drm.ko rebuilt with the fix (combined with sibling DF-4459/DF-4460 hunks), -Werror, RC=0 (build.log). Behavioral baseline/patched runs impossible: no GPU on the guest (fix_status not_testable, compile-validated).
fix.diff; build.log (patched drm.ko full -Werror compile+link, DRM_BUILD_RC=0); VERDICT.md 'Fix validation'
Confirmed kernel references
Detail
Exploit chain
on GPU hardware: open /dev/dri/card0 (0666) -> CREATE_DUMB (1 page) -> MODE_MAP_DUMB -> fake offset -> mmap(fd, fake + N, len up to 2^40) -> fault at obj->base.size+delta -> i915_gem_fault maps foreign GGTT fictitious pages into the process (read+write other users'/objects' GPU buffers). Sink mechanics already filed as DF-1146; access-control gap as DF-1633.
Evidence (decisive lines)
VERDICT.md hop-by-hop trace (vm_mmap.c:1377, drm_drv.c:1256-1274, drm_gem.c:1082-1109 + dead check 944-946, device_pager.c:109-127, i915_gem.c:6383-6389/2229); run.log (BUILD_RC=0 then open ENOENT); build.log (drm.ko rebuilt with fix, -Werror, RC=0); fix.diff (git apply --check OK)
PoC changes
no seed; wrote complete trigger from scratch (dumb-buffer + two attack shapes), DragonFly UAPI structs inlined; compiled clean on guest cc 8.3 first try after fixing an argv ternary typo
Verified recommended fix
In drm_gem_mmap_single, reject mappings that leave the object: maoff = DRM_GEM_MAPPING_MAPOFF(*offset); if (size > gem_obj->size || maoff > gem_obj->size - size) return EINVAL
Verdict
Code defect certain, runtime GPU-gated: drm_gem_mmap_single (drm_gem.c:1082-1109) creates the OBJT_MGTDEVICE pager with cdev_pager_allocate(..., size, nprot, DRM_GEM_MAPPING_MAPOFF(*offset), ...) where BOTH operands are attacker-controlled and never validated against gem_obj->size: vm_mmap passes the mmap(2) length verbatim (vm_mmap.c:1377 -> drm_drv.c:1270), cdev_pager_allocate grows the vm_object to foff+size instead of clipping (device_pager.c:109-127), i915's cdev_pg_ctor is a no-op (i915_gem.c:6383-6389), and the user adds arbitrary bits 0..39 (in-object offset) to the published fake offset since drm_gem_object_from_offset only validates the KEY and the 22-bit idx (drm_gem.c:1070-1077, drm_gem.h:40-46). The only upstream clamp, drm_gem_mmap_obj's 'obj_size < vma->vm_end - vma->vm_start -> -EINVAL', is #if 0'd in this file (drm_gem.c:914-1041, check at 944-946) because DragonFly replaced ->mmap with d_mmap_single without re-implementing it. Every fault in [maoff, maoff+size) therefore reaches the driver pager with an unbounded offset; on i915 (the sole DRIVER_GEM user of this path; radeon/amd take ttm_bo_mmap_single, drm_drv.c:1266-1268) the known driver-side gap DF-1146 converts it into vm_phys_fictitious_to_vm_page(ggtt->gmadr.start + vma->node.start + offset) β cross-object GGTT read/write. Core-side fix closes the OOB for every BSD-path GEM driver at once. Runtime reproduction impossible on the audit guest: no /dev/dri (QEMU bochs 1111:1234 VGA, no KMS); trigger df4458.c compiles -Wall -Wextra clean on guest cc 8.3 and fails exactly at the gate (open ENOENT, run.log). Attempting to force-load drm.ko on the guest wedges the loader (no hardware) β not repeated.
No comments yet.