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

DragonFly-only drm_vma_node_unmap() cannot revoke a single user mapping: user CPU PTEs into the physical GGTT aperture survive VMA unbind/evict/fence-steal/object-free and read+write whatever object later reuses the aperture range (unprivileged cross-object/cross-user GPU memory access)

Summary

The DF-authored drm_vma_node_unmap walks the OBJT_MGTDEVICE vm_object's resident pages - but i915_gem_fault never inserts aperture pages into that object (pages go straight into the pmap), so vm_object_page_remove and the busy-wait loop operate on an EMPTY object and nothing is removed. It is the only PTE-revocation mechanism in the entire DF drm tree (grep-proven), yet callers clear USERFAULT bookkeeping as if revocation happened. A user's WC mapping of a GTT-mapped object keeps valid CPU PTEs after eviction/unbind; when the freed aperture range is reused by a victim object (another process's buffer, root's scanout) the stale PTEs read and write the victim with no intervening fault. Trigger compiled and executed on guest to the device-absent gate (virtio-gpu only). pmap_page_protect-per-page zap fix (twin in i915_gem.c:2149).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4444 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.7 KB ↓ raw
VERDICT.md β€” 6.2 KB ↓ raw
df4444_trigger.c β€” 5.5 KB view raw
build.sh β€” 104 B view raw
run.sh β€” 92 B view raw
build.log β€” 9 B view raw
run.log β€” 88 B view raw
env.txt β€” 268 B view raw
fix.diff β€” 3.4 KB view raw
verdict.json β€” 4.9 KB view raw
manifest.json β€” 1.3 KB view raw

DF-4444 evidence pack

  • What: DragonFly-only drm_vma_node_unmap() in sys/dev/drm/i915/i915_vma.c:857-890 cannot revoke any user CPU mapping: the OBJT_MGTDEVICE vm_object never contains pages (fault path enters fictitious aperture pages directly into the pmap), so both vm_object_page_remove() and the vm_page_lookup_busy_wait() loop are no-ops. After VMA unbind/evict (or object free β€” twin at i915_gem.c:2149), user PTEs into the physical GTT aperture stay valid and read/write whatever object is later bound into that aperture range.
  • Impact (on real Intel-GPU hardware): unprivileged cross-object / cross-user GPU memory disclosure and corruption (High).
  • This guest: QEMU virtio-gpu only β€” trigger builds and runs to its hardware gate (run.log); runtime verdict inconclusive.

Reproduce

scp df4444_trigger.c dfbsd:/tmp/
ssh dfbsd 'cc -O2 -Wall -o /tmp/df4444 /tmp/df4444_trigger.c && /tmp/df4444'

Expected on hardware: exit 0 with VULNERABLE: stale mapping of A shows V's marker (+ cross-object write confirmation). Expected on this guest: no DRI device exit=2 (see run.log). On a fixed kernel: A's page touch after eviction either re-faults (reads A's own fill) or SIGBUSes β€” never V's marker.

Files

  • df4444_trigger.c β€” self-contained trigger (inline DRM uapi; no libdrm)
  • build.sh / build.log β€” guest compile (clean, 1 benign warning)
  • run.sh / run.log β€” guest run: exact hardware-gate failure, exit=2
  • env.txt β€” guest uname, drm sysctls, no /dev/dri
  • VERDICT.md β€” full line-cited static proof + attack chain
  • fix.diff β€” pmap-level revocation of the VMA's aperture window
  • manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-4444 β€” DF-specific drm_vma_node_unmap is a structural no-op: user CPU PTEs mapping the GGTT aperture are never revoked on VMA unbind/evict β†’ stale mappings survive into reallocated aperture space (cross-object / cross-user GPU memory read+write)

File: sys/dev/drm/i915/i915_vma.c:857-890 (called at i915_vma.c:906-907).

Verdict

Status: inconclusive at runtime (guest has no Intel GPU β€” the only VGA device is QEMU virtio-gpu 0x1234:1111; i915.ko exists in /boot/kernel but can never attach). The static proof below is certain: the revocation hook is mechanically a no-op, and it is the only revocation mechanism in the entire DF drm tree.

Severity (static): High β€” unprivileged local user with DRI device access keeps valid CPU PTEs into the physical GTT aperture window after the owning VMA is unbound; when the aperture range is re-allocated to another object (other process / the X server's framebuffers), the stale PTE reads and writes that object's memory. Confidentiality + integrity break between GPU clients, up to cross-user. Requires Intel graphics hardware (all i915 generations that support I915_MMAP_GTT, i.e. all of them) and GGTT pressure to evict β€” both standard on any real desktop using i915.

Root cause, line by line

Upstream (Linux v4.19, i915_vma.c) revokes user mappings with unmap_mapping_range(drm.anon_inode->i_mapping, offset, vma->size, 1) β€” a pmap-level zap of every user PTE mapping the VMA's GGTT window. DragonFly replaced this with a local drm_vma_node_unmap (sys/dev/drm/i915/i915_vma.c:857-890) whose every step is inert:

  1. devobj = cdev_pager_lookup(obj) β€” returns the OBJT_MGTDEVICE vm_object created at mmap time (sys/dev/drm/drm_gem.c:1099), with a reference taken (sys/vm/vm_pager.c:359-374 vm_pager_object_lookup does vm_object_reference_locked). Fine so far.

  2. vm_object_page_remove(devobj, 0, 0, false) β€” iterates only pages resident in the object's rb-tree. For OBJT_MGTDEVICE, pages are never inserted into the object: sys/dev/drm/i915/i915_gem.c:57 #undef USE_INSERT, the vm_page_insert() path at i915_gem.c:2405-2425 is #if 0 ("NO LONGER USED"), and sys/vm/vm_fault.c:2027-2033 documents the design: "For OBJT_MGTDEVICE the pages are not indexed in the VM object at all but instead directly entered into the pmap" (vm_fault.c:2170-2174 returns the busied fictitious page straight to the caller for pmap_enter). β‡’ no resident pages β†’ nothing removed.

  3. for (i = 0; i < page_count; i++) vm_page_lookup_busy_wait(devobj, i, ...) β€” same object-rb-tree lookup of the same nonexistent pages β‡’ all NULL, continue every iteration. Dead loop. (Even if a page were found, cdev_pager_free_page (sys/vm/device_pager.c:184-194) only removes the page from the object; it does not walk user pmaps β€” although its pmap_page_protect(m, VM_PROT_NONE) call would incidentally do the right thing if the page were ever in the object.)

  4. vm_object_deallocate(devobj) β€” only drops the reference taken by (1).

Net effect: the function removes zero pmap entries, while the caller i915_vma_revoke_mmap proceeds to i915_vma_unset_userfault(vma) and decrements obj->userfault_count (i915_vma.c:915-917) β€” the kernel now believes user mappings were revoked. Grep proof of absence of any other revocation: rg 'pmap_remove|pmap_qremove|pmap_page_protect' sys/dev/drm/ returns zero hits in i915/*, drm core, or linux_vm shims.

Attack chain (requires real Intel GPU)

  1. Unprivileged user opens /dev/dri/renderD128 (no DRM auth needed for render-node GEM), creates object A, I915_MMAP_GTTs it and touches it: i915_gem_fault (i915_gem.c:2229) pins A PIN_MAPPABLE into the GGTT, hands out fictitious aperture pages (i915_gem.c:2418-2428), and marks the VMA USERFAULT (i915_gem.c:2440). CPU PTEs β†’ physical aperture window gmadr + A.node.start.
  2. User creates GGTT pressure (GTT-maps ~mappable_end worth of 1MiB objects). The MM evicts the inactive A: i915_gem_evict_* β†’ i915_vma_unbind (i915_vma.c:1071) β†’ i915_vma_revoke_mmap (i915_vma.c:1146) β†’ no-op unmap (above). A's GTT PTEs cleared, A's drm_mm node freed, but the attacker's CPU PTEs remain valid.
  3. Victim object V (any other client's β€” e.g. root's scanout framebuffer, another user's GTT-mapped buffer) is bound; the drm_mm allocator reuses A's freed range for it (attacker shapes size/alignment to make reuse deterministic).
  4. Attacker reads A's stale mapping β†’ V's contents; writes β†’ V's memory. No fault ever occurs (PTE valid), so no revalidation happens.

Secondary reachable revoke sites with the same no-op: fence-steal i915_gem_fence_reg.c:248 (tiling/fence revocation), GPU-reset i915_gem_revoke_fences i915_gem_fence_reg.c:459, and the object-free twin __i915_gem_object_release_mmap (i915_gem.c:2149-2165, identical DF no-op β†’ stale mappings even survive object free until munmap/exit).

What was and was not verified

  • VERIFIED (static, certain): every step above, cited to lines; full diff against Linux v4.19 i915_vma.c shows this #ifdef __DragonFly__ block is the only functional divergence in the file (135-line diff, mechanical elsewhere).
  • VERIFIED (guest): trigger compiles against DF userland (build.log) and runs to its exact hardware gate (run.log: "no DRI device"; guest PCI shows only vgapci0 chip=0x11111234 virtio-gpu, ls /dev/dri β†’ ENOENT, env.txt).
  • NOT verified (impossible on this guest): the runtime cross-object read/write on real Intel hardware.

Fix

See fix.diff: zap the VMA's aperture window at pmap level using the same fictitious-page + pmap_page_protect() idiom cdev_pager_free_page itself uses (device_pager.c:188). Also applies (analogously, per-GGTT-VMA walk of obj->vma_list) to the twin in i915_gem.c:2149.

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

not_testable: no Intel GPU in the QEMU guest, so neither baseline nor patched kernel can exercise the revocation path; the fix is validated by construction (pmap_page_protect is the same primitive cdev_pager_free_page uses, applied to the exact fictitious pages the fault path entered).

fix.diff; sys/vm/device_pager.c:184-194 (pmap_page_protect idiom); i915_gem.c:2418-2428 (fictitious page source of the same addresses)
↓ fix.diffper-fix-DF-4444

Confirmed kernel references

Detail

Exploit chain

render-node open -> GEM_CREATE(A) -> I915_MMAP_GTT(A) + touch (PTEs->aperture, USERFAULT set) -> GGTT pressure via ~384 GTT-mapped 1MiB objects -> MM evicts inactive A -> i915_vma_unbind -> revoke_mmap no-op (stale PTEs stay valid) -> victim V (other process, e.g. root framebuffer) rebinds into A's freed drm_mm range -> read/write A's stale mapping == read/write V's memory; no fault occurs so no revalidation

Evidence (decisive lines)

VERDICT.md (line-cited proof + attack chain); run.log (trigger runs to the hardware gate on the guest); build.log (compiles clean); env.txt (no /dev/dri, virtio-gpu only); fix.diff (pmap-level revocation)

PoC changes

self-contained trigger written from scratch (inline DRM/i915 uapi structs and ioctl numbers β€” no libdrm on the guest); builds clean on DF cc

Verified recommended fix

Replace the object-walk in the DF drm_vma_node_unmap with a pmap_page_protect(VM_PROT_NONE) sweep over the VMA's fictitious aperture pages [gmadr+node.start, +node.size) β€” see fix.diff

Verdict

Static proof is certain: the DragonFly-only drm_vma_node_unmap() (sys/dev/drm/i915/i915_vma.c:857-890) cannot revoke a single user mapping β€” OBJT_MGTDEVICE aperture pages are entered directly into the pmap and never inserted into the device vm_object (i915_gem.c:57 '#undef USE_INSERT', i915_gem.c:2405-2425 disabled, vm_fault.c:2027-2033/2170-2174), so both vm_object_page_remove() and the vm_page_lookup_busy_wait() loop operate on an empty object and the only remaining statement drops the lookup reference. It is the sole revocation hook in the whole DF drm tree (rg 'pmap_remove|pmap_qremove|pmap_page_protect' over sys/dev/drm returns nothing), yet the caller clears the USERFAULT bookkeeping as if mappings were revoked (i915_vma.c:915-917). Consequence on real Intel-GPU hardware: user CPU PTEs into the physical GGTT aperture survive VMA unbind/evict (i915_vma.c:1146), fence steal (i915_gem_fence_reg.c:248), GPU reset (i915_gem_fence_reg.c:459) and object free (twin no-op at i915_gem.c:2149-2165), and read/write whichever object's pages later occupy the reused aperture range β€” unprivileged cross-object/cross-user GPU memory disclosure+corruption (High). Runtime verification impossible on the audit guest: only QEMU virtio-gpu 0x1234:1111 is present, i915.ko can never attach; the self-contained trigger compiles cleanly on the guest (build.log) and runs to exactly that hardware gate (run.log: 'no DRI device', exit=2, env.txt). fix.diff implements the pmap-level zap (fictitious-page + pmap_page_protect over the VMA's mappable aperture window); fix validation is not_testable for the same hardware reason.