# 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:

```c
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.
