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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3768 Β· 8 files
FileTypeDescriptionSize
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):

  1. Unbounded kernel-memory leak: every (re-)attach allocates a fresh fake vm_page array 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.
  2. Slot exhaustion β†’ graphics DoS: after 8 cycles the segment array is full and vm_phys_fictitious_reg_range() returns EBUSY, failing i915_gem_init_ggtt() β†’ the driver can never attach again until reboot.
  3. 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

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.

VERDICT.md
↓ download raw

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)

  1. i915_gem_gtt.c:2968 calls vm_phys_fictitious_reg_range(gmadr.start, gmadr.start+mappable, WC) once per successful i915_gem_init_ggtt().
  2. vm_phys_fictitious_reg_range (drm_vm.c:172-204) allocates page_count = (end-start)/PAGE_SIZE fake vm_pages (drm_vm.c:183) and stores them in the first free slot of the 8-entry global vm_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:202 only frees on slot exhaustion).
  3. i915_ggtt_cleanup_hw (i915_gem_gtt.c:2992-3029) tears down the gsm iomap, mtrr, stolen, drm_mm β€” but never calls vm_phys_fictitious_unreg_range. Grep over the whole i915 subtree: zero call sites (only radeon_device.c:1566 and the shim's definition).
  4. After 8 attach cycles, drm_vm.c:203 returns EBUSY β†’ i915_gem_init_ggtt returns 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_testable
baseline no→ patch + rebuild →patched clean

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

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.