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

dma_free_coherent() permanently leaks every physical page (wired, unreclaimable) and orphans the kernel PTE

Summary

Bare kmem_free instead of contigfree: the coherent region is wired object-less pages pmap_kenter'd over a pageable entry, so vm_map_delete never pmap_removes or unwires/frees them. Guest-verified: PTE still valid after free (pmap_kextract!=0), wire_count=1, loss-free 60-120x2MB cycle burned 119-124MB permanently (reserve drained first, then free pages); contigmalloc control deficit 0; patched drm.ko deficit 2 pages, PTE gone. Live paths: radeon_gart_fini, amdgpu_ih_ring_fini, TTM dma pools - per-unload/failed-probe permanent wired-RAM burn + DMA-reserve exhaustion DoS. contigfree fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5228 Β· 12 files
FileTypeDescriptionSize
poc5228.c β€” 5.0 KB view raw
Makefile β€” 53 B ↓ download
build.sh β€” 86 B view raw
run.sh β€” 135 B view raw
build.log β€” 1.0 KB view raw
run.log β€” 2.9 KB view raw
run.fixed.log β€” 512 B view raw
env.txt β€” 198 B view raw
fix.diff β€” 1.9 KB view raw
VERDICT.md β€” 4.1 KB ↓ raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 4.6 KB view raw
VERDICT.md
↓ download raw

DF-5228 VERDICT β€” dma_free_coherent() leaks every physical page

Status: REPRODUCED (impact: dos β€” permanent wired-memory exhaustion; also an orphaned-PTE hazard) on the stock /boot/kernel/drm.ko of the DragonFly 6.5-DEVELOPMENT guest.

Evidence chain (all from run.log, stock drm.ko)

  1. Orphaned PTE: immediately after dma_free_coherent(p): pmap_kextract(p) = 0x03900000 / 0x03a00000 / 0x18000000 across runs β€” the kernel PTE stays valid after the KVA was handed back to kernel_map. Root cause: the contig region is mapped by pmap_kenter_noinval (vm_contig.c:531) over an object-less kmem_alloc_pageable entry (vm_kern.c:171-175); kmem_free β†’ vm_map_delete only calls pmap_remove for object-backed entries (vm_map.c:3325-3370) β€” never for this one. vm_kern.c:311-313 warns about exactly this misuse.
  2. Pages never released: PHYS_TO_VM_PAGE(pa)->wire_count == 1, queue == 0 β€” page STILL WIRED after the free (probe across both the reserve path, PG_FICTITIOUS=1, and the real-page path, PG_FICTITIOUS=0). vm_page_free_contig (vm_page.c:2858) is never called; the correct free is contigfree() (vm_contig.c:588-603).
  3. Sustainability test: 60–120 cycles of dma_alloc_coherent(2MB) + dma_free_coherent(2MB) β€” with a correct free this must be loss-free forever. Observed on stock drm.ko: - v2 run: plateau for the first ~60 cycles (the 128MB DMA reserve being silently drained β€” fictitious reserve pages are invisible to vmstats.v_free_count), then linear drain once the reserve is exhausted; 240MB cycled β†’ 124MB permanent deficit, wire surplus 32200; - v3 run (reserve already exhausted): linear from cycle 1; 120MB cycled β†’ 119MB permanent deficit, wire surplus 31199, with zero live allocations at the end.
  4. Control: identical loop with contigmalloc/contigfree β†’ deficit 0–77 pages (sampling noise; 400:1 signal-to-noise).

Impact

Every dma_free_coherent() call permanently burns its pages (wired β†’ never reclaimed, even under full memory pressure) plus reserve capacity shared with all other contig consumers (network rings, sound, USB). Live triggers: radeon_gart_fini (per driver unload / failed probe β€” the GART table is 64KB–MBs), amdgpu_ih_ring_fini Γ—1–3 rings per amdgpu teardown (kldunload / re-probe), TTM DMA pools. Root/hardware lifecycle triggered β†’ Medium.

The orphaned PTE (KVA returned to kernel_map while a valid PTE to a wired page persists) additionally leaves the map/pmap out of sync β€” vm_kern.c:311 warns resident_count is mistabulated in this situation.

No uid=0 chain (resource-exhaustion class). Panic was never needed; the primitive is directly observable.

Fix validation (same run environment)

fix.diff (shared with DF-5227): dma_free_coherent = contigfree(cpu_addr, size, M_DEVBUF). Guest reset (vm.sh reset with-src), fix applied, drm.ko rebuilt and loaded on the fresh boot; identical PoC re-run (run.fixed.log):

  • pmap_kextract(va) = 0 β†’ kernel PTE properly removed;
  • 120MB of alloc+free cycles β†’ deficit 2 pages (noise), wire surplus 2 (vs 30691/31199 on stock);
  • allocations still succeed β†’ no functional regression.

Notes

  • v1 of the PoC mis-measured (vmstats deltas are invisible while the reserve absorbs the leak) β€” v2/v3 instruments (page-state probe + sustainability loop + control) were written after observing the reserve behavior; all three baseline runs are kept in run.log.
  • kldunload drm (stock, no device attached) hung the guest's linker subsystem once; pre-existing drm teardown defect family (DF-4532/DF-5064), unrelated to this finding (linux_dma.c code paths are not involved in module teardown). The fix-validation run avoided unload (fresh boot).
  • Guest reset clean after evidence collection.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src/sys/dev/drm/linux_dma.c, drm.ko rebuilt from source, loaded on a fresh boot (vm.sh reset with-src): identical PoC now shows pmap_kextract(va)=0 (PTE removed) and a 120MB alloc+free loop deficit of 2 pages (noise) vs 30691 on stock - the leak is gone; allocations still succeed.

run.fixed.log: 'DF5228: after free: pmap_kextract(va)=0000000000000000 -> kernel PTE gone'; 'RESULT: ... deficit=2 pages (0MB), v_wire surplus=2'; compare run.log baseline 'deficit=30691 pages (119MB), v_wire surplus=31199'
↓ fix.diffper-fix-DF-5228

Confirmed kernel references

Detail

Exploit chain

any radeon/amdgpu driver unload or failed GPU probe cycles dma_alloc_coherent/dma_free_coherent (GART table, IH rings) -> every cycle permanently burns KBs-MBs of wired RAM and reserve capacity -> repeatable until the machine is out of memory with all of it unreclaimable (hard DoS); the shared 128MB DMA reserve starvation also degrades unrelated devices' contig allocations.

Evidence (decisive lines)

run.log: 'pmap_kextract(va)=0000000018000000 -> kernel PTE STILL VALID (ORPHANED)'; 'phys page state: wire_count=1 ... -> page STILL WIRED (LEAKED)'; 'RESULT: 120 MB alloc+freed ... deficit=30691 pages (119MB), v_wire surplus=31199' with 'control ... deficit=0'; run.fixed.log: 'kernel PTE gone', 'deficit=2 pages (0MB)'

PoC changes

Authored fresh (reserved-ID note: DF-5226 on disk holds the twin-batch's unrelated refuted-candidate negative record, left untouched; this finding used the next reserved ID). v1 measured only vmstats deltas and under-reported while the DMA reserve absorbed the leak; v2/v3 added the PTE/page-state probes, the reserve-exhausting sustainability loop and the contigmalloc/contigfree control - all baseline runs kept in run.log.

Verified recommended fix

Implement dma_free_coherent with contigfree(cpu_addr, size, M_DEVBUF) instead of bare kmem_free (and zero the allocation via contigmalloc_map(M_NOWAIT|M_ZERO) - DF-5227); fix.diff in this pack.

Verdict

REPRODUCED on stock drm.ko (DragonFly 6.5-DEVELOPMENT guest): dma_free_coherent (sys/dev/drm/linux_dma.c:81) is a bare kmem_free(kernel_map, addr, size), but the matching free for kmem_alloc_contig/contigmalloc is contigfree() (vm_contig.c:588-603: pmap_qremove + kmem_free + vm_page_free_contig). The contig region's pages are wired object-less pages pmap_kenter'd over an object-less kmem_alloc_pageable entry, so vm_map_delete (vm_map.c:3325-3370) never pmap_removes the range and never unwires/frees the pages. Demonstrated per-free: kernel PTE still valid after free (pmap_kextract != 0) and the phys page still wire_count=1 (probe on both reserve [PG_FICTITIOUS] and real pages); demonstrated at scale: an alloc+free loop that must be loss-free burned 119-124MB permanently per 120-240MB cycled (after silently draining the 128MB shared DMA reserve), wire surplus 31199-32200 pages, control contigmalloc/contigfree deficit 0. Live in-tree leak paths: radeon_gart_fini (radeon_gart.c:110), amdgpu_ih_ring_fini (amdgpu_ih.c:111), TTM dma pools. Impact: permanent wired-memory exhaustion (unreclaimable under pressure) + DMA-reserve starvation for other drivers + orphaned-PTE/pmap accounting desync; triggers are driver lifecycle (unload/re-probe) -> Medium. Fix validated in the same run environment: contigfree-based dma_free_coherent removes the PTE (pmap_kextract=0) and the loop deficit collapses to 2 pages (noise).