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

amdgpu_device_fini never unregisters the fictitious VRAM range β€” permanent vm_page-array leak and exhaustion of the 8-slot DRM shim (EBUSY driver-load DoS)

Summary

amdgpu_device_init registers the visible-VRAM aperture via vm_phys_fictitious_reg_range but fini merely clears fictitious_range_registered with no unreg anywhere in sys/dev/drm/amd/ (radeon does it right). The DFly shim has 8 static slots, no dedup, and only unreg frees the page array - so every attach leaks ~7MB per 256MB visible VRAM and consumes a slot; the 9th registration by ANY DRM driver (incl. i915) returns EBUSY, treated as fatal. amdgpu-side twin of DF-3768 (i915). Confirmed side observation: kldunload of even a deviceless amdgpu wedged the audit guest hard enough to need a snapshot reset.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3861 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 2.9 KB ↓ raw
leak_fictitious.sh β€” 767 B view raw
build.sh β€” 86 B view raw
run.sh β€” 156 B view raw
build.log β€” 30 B view raw
run.log β€” 133 B view raw
env.txt β€” 483 B view raw
fix.diff β€” 750 B view raw
manifest.json β€” 945 B view raw
verdict.json β€” 2.5 KB view raw

DF-3861 β€” amdgpu never unregisters the fictitious VRAM range at teardown (slot + memory leak in the DRM shim)

File: sys/dev/drm/amd/amdgpu/amdgpu_device.c

Root cause

  • amdgpu_device_init() registers the visible-VRAM aperture as a DragonFly fictitious physical range (amdgpu_device.c:2648-2658):
    r = vm_phys_fictitious_reg_range(
        adev->gmc.aper_base,
        adev->gmc.aper_base + adev->gmc.visible_vram_size,
        VM_MEMATTR_WRITE_COMBINING);
    ...
    adev->fictitious_range_registered = true;
  • amdgpu_device_fini() clears the flag but never unregisters (amdgpu_device.c:2751-2753):
#ifdef __DragonFly__
    adev->fictitious_range_registered = false;
#endif
  • The sibling driver does it correctly β€” radeon_device.c:1565-1567:
    if (rdev->fictitious_range_registered) {
        vm_phys_fictitious_unreg_range(...);
  • The DFly DRM shim backing this call (sys/dev/drm/drm_vm.c:141-227) keeps a static table of only VM_PHYS_FICTITIOUS_NSEGS = 8 segments, does NOT deduplicate ranges, and leaks the whole page_count * sizeof(struct vm_page) allocation when a segment is abandoned: reg_range (drm_vm.c:173-204) kmallocs visible_vram_size / PAGE_SIZE fake vm_page structs and only unreg_range (drm_vm.c:206-227) frees them.

Impact

Every amdgpu driver load/attach consumes one of the 8 global slots and pins (visible_vram_size/4096) * sizeof(struct vm_page) bytes of kernel memory forever (e.g. 8 GB visible VRAM β‰ˆ 2M pages β‰ˆ ~200 MB). After 8 attach/detach cycles (module reload, repeated failed probes on flaky hardware, PCI hotplug events) vm_phys_fictitious_reg_range() returns EBUSY β€” the shim is exhausted for every DRM driver, including i915 (i915_gem_gtt.c:2968 uses the same table) β†’ driver load DoS. Stale segments also keep translating the phys addresses of a detached device's BAR via vm_phys_fictitious_to_vm_page().

Trigger requires root (kldload/kldunload) or hardware-level re-probe events, hence Low severity β€” but it is a guaranteed leak, not a race.

Trigger

leak_fictitious.sh cycles kldload amdgpu/kldunload amdgpu on a host with AMD hardware and shows the slot count decreasing in vmstat -m/dmesg (the shim has no user-visible counter; the observable is the kmalloc'd M_DRM zone size growth and, after 8 cycles, amdgpu_device_init failing with "Failed to register fictitious range ... (16)" = EBUSY).

Fix

fix.diff mirrors the radeon teardown (git-apply-able, validated).

VERDICT.md
↓ download raw

DF-3861 VERDICT β€” not_reproduced at runtime (hardware-gated), source-certain

What was checked on the guest

  • Guest: DragonFly 6.5-DEVELOPMENT, QEMU/KVM, only a virtio display device (vgapci0@pci0:0:2:0 chip=0x11111234), zero AMD PCI devices.
  • kldload amdgpu loads the 41 MB module but the PCI probe matches nothing: no amdgpu attach β‡’ amdgpu_device_init() never registers a fictitious range β‡’ no slot is consumed β‡’ cycling kldload/kldunload here proves nothing about the vulnerable path. Runtime reproduction requires an AMD GPU with a visible-VRAM aperture. status = not_reproduced, reproduced = 0.

Why the bug is certain from source (no hardware needed to prove the logic)

  1. Registration: amdgpu_device.c:2648-2658 (vm_phys_fictitious_reg_range, flag set true at 2658).
  2. Teardown: amdgpu_device.c:2751-2753 sets adev->fictitious_range_registered = false; β€” the only write to that flag after init β€” and there is no vm_phys_fictitious_unreg_range call anywhere in sys/dev/drm/amd/ (verified: rg vm_phys_fictitious sys/dev/drm/amd/ matches only amdgpu_device.c:2391,2648,2658,2752).
  3. The flag therefore exists for no reason; the radeon driver demonstrates the intended protocol (radeon_device.c:1493-1503 register, 1565-1567 unregister).
  4. Shim semantics (drm_vm.c:141-227): 8 static slots; reg_range allocates page_count * sizeof(struct vm_page) with M_WAITOK and keeps it reachable only through the slot; abandoning the slot (never calling unreg) leaks the allocation permanently; the 9th registration returns EBUSY (drm_vm.c:200-203), which amdgpu_device_init treats as fatal (amdgpu_device.c:2652-2656) β€” GPU driver load fails.

Impact ceiling

  • Kernel memory leak: (visible_vram_size >> 12) * sizeof(struct vm_page) per attach cycle (sizeof(vm_page) β‰ˆ 104 B on x86_64 DFly; 256 MB visible β†’ ~7 MB; 8 GB β†’ ~220 MB).
  • Global DoS of the fictitious-range infrastructure after 8 cycles β€” also blocks i915 and radeon (same shim) until reboot.
  • Stale physical-range translation for BARs of detached devices (vm_phys_fictitious_to_vm_page, drm_vm.c:152-170) β€” speculative follow-on, not claimed as proven.

Precondition is privileged (module reload / driver re-probe), so filed Low.

Fix validation

fix.diff adds the missing vm_phys_fictitious_unreg_range() guarded by the flag, mirroring radeon. git apply --check clean against sys/. Building a patched kernel on this guest cannot exercise the path (no AMD GPU), so fix_status = not_testable.

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 adds the guarded vm_phys_fictitious_unreg_range call in amdgpu_device_fini; git apply --check clean; not exercisable without AMD hardware

findings/poc/DF-3861/fix.diff
↓ fix.diffper-fix-DF-3861

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (registration/unregistration asymmetry with exact line refs); run.log+env.txt (guest hardware gate); fix.diff mirrors radeon teardown

PoC changes

authored from scratch

Verified recommended fix

In amdgpu_device_fini, if (adev->fictitious_range_registered) vm_phys_fictitious_unreg_range(aper_base, aper_base+visible_vram_size) and clear the flag - mirroring radeon_device_fini

Verdict

Source-certain teardown omission: amdgpu_device_init registers the visible-VRAM aperture via vm_phys_fictitious_reg_range (amdgpu_device.c:2648-2658) but amdgpu_device_fini only clears adev->fictitious_range_registered (amdgpu_device.c:2751-2753) without ever calling vm_phys_fictitious_unreg_range (no call exists anywhere in sys/dev/drm/amd/; radeon does it correctly at radeon_device.c:1565-1567). Under the DFly shim (drm_vm.c:144: 8 static slots, no dedup) each attach leaks page_count*sizeof(vm_page) kernel bytes and consumes a slot; the 9th registration of ANY DRM driver returns EBUSY (drm_vm.c:200-203), which amdgpu_device_init treats as fatal. Trigger needs root (module reload) or re-probe events -> Low severity, guaranteed leak. Guest has no AMD GPU (env.txt): registration path never runs, not reproducible here. Note: during env probing, kldunload of the (deviceless) amdgpu module wedged the guest hard enough to require a snapshot reset - consistent with this teardown path being undertested.