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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4458 Β· 11 files
FileTypeDescriptionSize
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 results
  • fix.diff β€” core-side clamp (git-apply-able; compile-validated)
  • run.log, env.txt, build.log, run.sh, build.sh
VERDICT.md
↓ download raw

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:

  1. size β€” the mmap(2) length. vm_mmap passes objsize through 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 to OFF_TO_IDX(foff + size) (device_pager.c:109-127, esp. the object->size = … growth branch), and i915's cdev_pg_ctor is a no-op that always returns 0 (i915_gem.c:6383-6389). A 1-page GEM object can back a terabyte of mapping.
  2. foff (in-object offset) β€” bits 0..39 of the fake offset. The ioctl-published offset is KEY | idx<<40 with 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_ioctl args->size = obj->size after 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_tail error 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_ioctl close 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_release vm_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_delete idr_replace-NULL pattern (drm_gem.c:349-363): concurrent lookups see NULL and fail; double-decrement impossible.
  • drm_gem_release idr_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_init unchecked default_pager_alloc result (drm_gem.c:197-202): DF vm_object_allocate cannot return NULL (objcache-backed); acceptable.
  • drm_gem_dmabuf_export is 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_testable
baseline no→ patch + rebuild →patched clean

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

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.