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)
PoC verification
Evidence pack
findings/poc/DF-4444 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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()insys/dev/drm/i915/i915_vma.c:857-890cannot revoke any user CPU mapping: theOBJT_MGTDEVICEvm_object never contains pages (fault path enters fictitious aperture pages directly into the pmap), so bothvm_object_page_remove()and thevm_page_lookup_busy_wait()loop are no-ops. After VMA unbind/evict (or object free β twin ati915_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 verdictinconclusive.
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=2env.txtβ guest uname, drm sysctls, no/dev/driVERDICT.mdβ full line-cited static proof + attack chainfix.diffβ pmap-level revocation of the VMA's aperture windowmanifest.json,verdict.json
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:
-
devobj = cdev_pager_lookup(obj)β returns theOBJT_MGTDEVICEvm_object created at mmap time (sys/dev/drm/drm_gem.c:1099), with a reference taken (sys/vm/vm_pager.c:359-374vm_pager_object_lookupdoesvm_object_reference_locked). Fine so far. -
vm_object_page_remove(devobj, 0, 0, false)β iterates only pages resident in the object's rb-tree. ForOBJT_MGTDEVICE, pages are never inserted into the object:sys/dev/drm/i915/i915_gem.c:57#undef USE_INSERT, thevm_page_insert()path ati915_gem.c:2405-2425is#if 0("NO LONGER USED"), andsys/vm/vm_fault.c:2027-2033documents 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-2174returns the busied fictitious page straight to the caller forpmap_enter). β no resident pages β nothing removed. -
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,continueevery 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 itspmap_page_protect(m, VM_PROT_NONE)call would incidentally do the right thing if the page were ever in the object.) -
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)
- 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 APIN_MAPPABLEinto the GGTT, hands out fictitious aperture pages (i915_gem.c:2418-2428), and marks the VMAUSERFAULT(i915_gem.c:2440). CPU PTEs β physical aperture windowgmadr + A.node.start. - 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. - 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).
- 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.cshows 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 onlyvgapci0 chip=0x11111234virtio-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_testablenot_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)
Confirmed kernel references
- sys/dev/drm/i915/i915_vma.c:857-890
- sys/dev/drm/i915/i915_vma.c:892-918
- sys/dev/drm/i915/i915_vma.c:1130-1150
- sys/dev/drm/i915/i915_gem.c:57
- sys/dev/drm/i915/i915_gem.c:2193-2229
- sys/dev/drm/i915/i915_gem.c:2405-2428
- sys/dev/drm/i915/i915_gem.c:2440
- sys/dev/drm/i915/i915_gem.c:2149-2165
- sys/dev/drm/i915/i915_gem_fence_reg.c:248
- sys/dev/drm/i915/i915_gem_fence_reg.c:459
- sys/dev/drm/drm_gem.c:1099
- sys/vm/device_pager.c:97-107
- sys/vm/device_pager.c:184-194
- sys/vm/vm_pager.c:359-374
- sys/vm/vm_fault.c:2027-2033
- sys/vm/vm_fault.c:2162-2174
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.
No comments yet.