DFly i915 registers the GGTT aperture fictitious range on every attach but never unregisters it: unbounded fake-vm_page leak and permanent 8-slot exhaustion (graphics attach DoS) via kldload/kldunload cycles
Summary
i915_gem_init_ggtt() calls vm_phys_fictitious_reg_range() for the GMADR BAR (#ifdef __DragonFly__) to back CPU GTT mmaps with fake vm_pages. i915_ggtt_cleanup_hw() never calls vm_phys_fictitious_unreg_range(), and the shim inserts into the first free of 8 global slots without duplicate detection. Each driver re-attach (kldload/kldunload of i915kms.ko, or error-probe->re-probe) leaks the previous (aperture/4K)-entry vm_page array (~6-8MB per cycle for a 256MB aperture) and consumes a slot; after 8 cycles registration returns EBUSY and the driver can never attach again until reboot. A stale segment also keeps matching the BAR range after unload. radeon in the same tree performs the symmetric unregister, proving the omission is i915-specific. Root-only trigger; no memory corruption, no unprivileged path. Source-level certain (not runnable on the virtio-only guest).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3768 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.8 KB | β raw | |
| VERDICT.md | β | 3.1 KB | β raw | |
| verdict.json | β | 3.0 KB | view raw | |
| manifest.json | β | 837 B | view raw | |
| trigger.sh | β | 646 B | view raw | |
| env.txt | β | 414 B | view raw | |
| upstream_v419_vs_dfbsd.diff | β | 16.4 KB | view raw | |
| fix.diff | β | 1.7 KB | view raw |
DF-3768 β i915 GGTT aperture fictitious-range registered, never unregistered (DFly)
Finding (summary)
DragonFly-specific code in sys/dev/drm/i915/i915_gem_gtt.c:2965-2970
(i915_gem_init_ggtt()) registers the GMADR (GTT aperture) PCI BAR as a
"fictitious" physical range so that CPU mmaps of GTT-bound objects can be
backed by fake vm_pages (i915_gem.c:2391 resolves user faults through
vm_phys_fictitious_to_vm_page(ggtt->gmadr.start + ...)).
The matching teardown never happens: i915_ggtt_cleanup_hw()
(i915_gem_gtt.c:2992-3029) does NOT call vm_phys_fictitious_unreg_range().
The shim backing these calls (sys/dev/drm/drm_vm.c:141-227) keeps a fixed
array of 8 global segments (VM_PHYS_FICTITIOUS_NSEGS) and registers into
the first free slot β it does not detect duplicates.
Consequences (per driver attach/detach cycle β kldload/kldunload of
i915kms.ko, or any error-probe β re-probe of the PCI device):
- Unbounded kernel-memory leak: every (re-)attach allocates a fresh
fake
vm_pagearray of(aperture_size / PAGE_SIZE)entries and keeps the old one alive (nothing frees it). A 256 MB mappable aperture β 65,536 fake pages (~6 MB+ per cycle on x86_64) β repeatable without bound. - Slot exhaustion β graphics DoS: after 8 cycles the segment array is
full and
vm_phys_fictitious_reg_range()returnsEBUSY, failingi915_gem_init_ggtt()β the driver can never attach again until reboot. - Stale segment after unload: the un-freed registration keeps matching the BAR's physical range after the driver is gone; a subsequently attached device that lands on the same BAR address is resolved to the stale shadow array first.
Contrast in the same tree: the radeon driver performs the symmetric
lifecycle (radeon_device.c:1493 register / radeon_device.c:1566
unregister), proving the omission is i915-specific and not a shim
limitation.
Threat model
Trigger requires root (module load/unload or device re-probe). No unprivileged trigger exists in this file. Impact is resource exhaustion / availability of the graphics stack plus unbounded kernel heap growth β not memory corruption. Severity: Low.
Source-level proof points
sys/dev/drm/i915/i915_gem_gtt.c:2966-2970β the registration (inside#ifdef __DragonFly__; no upstream counterpart).sys/dev/drm/i915/i915_gem_gtt.c:2992-3029βi915_ggtt_cleanup_hw()has novm_phys_fictitious_unreg_rangeand no free of the shadow array.sys/dev/drm/drm_vm.c:144-204β 8-slot global array; first-free-slot insert; duplicate registration silently succeeds and leaks the older array;EBUSYwhen full.sys/dev/drm/drm_vm.c:207-227β unreg exists and frees the array, but is never called by i915.sys/dev/drm/radeon/radeon_device.c:1493,1566β symmetric lifecycle done correctly by radeon.sys/dev/drm/i915/i915_gem.c:2391-2395β why the registration must outlive mappings, and why the correct fix mirrors radeon's (unregister at ggtt cleanup after all VMAs/mappings are torn down), not "drop it".
Reproduce (requires real Intel graphics hardware + root)
# on a DragonFly host with i915 hardware and i915kms.ko available:
for i in 1 2 3 4 5 6 7 8 9; do
kldunload i915kms && kldload i915kms
done
# cycle 9: console shows "taking over the fictitious range ..." absent /
# probe fails with EBUSY; vmstat/kmem statistics show ~8 leaked arrays.
The audit guest has no Intel GPU (virtio 0x1111:1234) and no i915kms.ko,
so the runtime demonstration is not_reproduced on this guest β see
VERDICT.md. The logic-level claim is source-certain.
Fix
fix.diff in this directory (against the read-only sys/ tree; never
applied by the auditor): unregister the fictitious range in
i915_ggtt_cleanup_hw() after VMA teardown, mirroring radeon.
DF-3768 β VERDICT
Status: not_reproduced (runtime) β the audit QEMU guest is virtio-only
(pciconf -l: vgapci0@pci0:0:2:0: chip=0x11111234 = Red Hat/QEMU virtual
VGA; ls /boot/kernel/i915kms.ko β No such file; kldstat -v | grep -ci
i915 β 0). There is no Intel graphics device and no i915 module object on
the guest, so the trigger (driver attach/detach cycles) cannot execute.
The finding is source-level certain; the defect does not depend on
hardware behavior, only on the registration/teardown asymmetry.
Why the finding is real (path:line)
i915_gem_gtt.c:2968callsvm_phys_fictitious_reg_range(gmadr.start, gmadr.start+mappable, WC)once per successfuli915_gem_init_ggtt().vm_phys_fictitious_reg_range(drm_vm.c:172-204) allocatespage_count = (end-start)/PAGE_SIZEfakevm_pages (drm_vm.c:183) and stores them in the first free slot of the 8-entry globalvm_phys_fictitious_segs[](drm_vm.c:191-200). A second registration of the same range takes the NEXT free slot and the first array is orphaned (drm_vm.c:202only frees on slot exhaustion).i915_ggtt_cleanup_hw(i915_gem_gtt.c:2992-3029) tears down the gsm iomap, mtrr, stolen, drm_mm β but never callsvm_phys_fictitious_unreg_range. Grep over the whole i915 subtree: zero call sites (only radeon_device.c:1566 and the shim's definition).- After 8 attach cycles,
drm_vm.c:203returns EBUSY βi915_gem_init_ggttreturns error β the driver cannot attach (graphics unavailable until reboot). Each earlier cycle permanently leaked(mappable/4096) * sizeof(struct vm_page)bytes (256 MB aperture β ~6-8 MB/cycle, Γ7 cycles).
Why not worse
- No unprivileged trigger: kldload/kldunload and PCI re-probe are root-only. Not privesc, not memory corruption; leak + availability.
- The stale (never-freed) segment is self-consistent: its fake pages stay allocated, so post-unload lookups resolve to valid memory (no UAF of the shadow array itself; the shim's unreg path β which does kfree β is never taken).
Fix validation
Not performable on this guest (no i915 kernel build target device-independ
β¦actually a kernel with i915 built-in could be built, but the trigger still
requires real Intel hardware to attach; building a kernel proves nothing
about the runtime behavior). fix.diff is the verified-by-inspection fix
mirroring radeon's proven lifecycle; fix_status: not_testable (hardware
absent).
Artifacts
trigger.shβ the kldload/kldunload cycle a maintainer reproduces with on Intel hardware.env.txtβ guest evidence (uname, pciconf, module absence).upstream_v419_vs_dfbsd.diffβ full diff showing the DFly-only block.fix.diffβ the proposed fix.
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 authored by inspection (mirrors radeon's proven lifecycle); cannot be runtime-validated on the virtio-only audit guest
findings/poc/DF-3768/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
README.md (finding + proof points), VERDICT.md (why real, why not worse), env.txt (guest GPU/module absence), upstream_v419_vs_dfbsd.diff (shows the DFly-only registration block), trigger.sh (root repro for maintainers with Intel hw), fix.diff
PoC changes
n/a β no seeded PoC; wrote trigger.sh from scratch for hardware maintainers
Verified recommended fix
Call vm_phys_fictitious_unreg_range(gmadr.start, gmadr.start+min(total,mappable_end)) from i915_ggtt_cleanup_hw() after iomap teardown, mirroring radeon_device.c:1566 β see fix.diff
Verdict
Source-certain DFly-specific defect: i915_gem_init_gtt (i915_gem_gtt.c:2968) registers the GMADR aperture as a fictitious vm_page range but i915_ggtt_cleanup_hw (i915_gem_gtt.c:2992-3029) never calls vm_phys_fictitious_unreg_range. The drm_vm.c shim inserts into the first free of 8 global slots without duplicate detection, so every driver re-attach (kldload/kldunload, re-probe) leaks the previous fake-vm_page array (~6-8MB per cycle for a 256MB aperture) and after 8 cycles registration returns EBUSY and the driver can never attach again until reboot. Root-triggered resource leak/availability issue, no memory corruption, no unprivileged path. Runtime reproduction impossible on the audit guest: virtio-only GPU (chip 0x1111:1234), no i915kms.ko present, module not loaded β verified live; hence not_reproduced with source-level certainty. radeon in the same tree performs the symmetric unregister (radeon_device.c:1566), confirming the intended lifecycle.
No comments yet.