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)
PoC verification
Evidence pack
findings/poc/DF-3861 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 = 8segments, does NOT deduplicate ranges, and leaks the wholepage_count * sizeof(struct vm_page)allocation when a segment is abandoned: reg_range (drm_vm.c:173-204) kmallocsvisible_vram_size / PAGE_SIZEfake 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).
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 amdgpuloads 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)
- Registration: amdgpu_device.c:2648-2658 (
vm_phys_fictitious_reg_range, flag set true at 2658). - Teardown: amdgpu_device.c:2751-2753 sets
adev->fictitious_range_registered = false;β the only write to that flag after init β and there is novm_phys_fictitious_unreg_rangecall anywhere in sys/dev/drm/amd/ (verified:rg vm_phys_fictitious sys/dev/drm/amd/matches only amdgpu_device.c:2391,2648,2658,2752). - The flag therefore exists for no reason; the radeon driver demonstrates the intended protocol (radeon_device.c:1493-1503 register, 1565-1567 unregister).
- Shim semantics (drm_vm.c:141-227): 8 static slots;
reg_rangeallocatespage_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), whichamdgpu_device_inittreats 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_testablefix.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
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.
No comments yet.