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)
PoC verification
Evidence pack
findings/poc/DF-5228 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
- Orphaned PTE: immediately after
dma_free_coherent(p):pmap_kextract(p) = 0x03900000 / 0x03a00000 / 0x18000000across runs β the kernel PTE stays valid after the KVA was handed back to kernel_map. Root cause: the contig region is mapped bypmap_kenter_noinval(vm_contig.c:531) over an object-less kmem_alloc_pageable entry (vm_kern.c:171-175);kmem_freeβvm_map_deleteonly callspmap_removefor object-backed entries (vm_map.c:3325-3370) β never for this one. vm_kern.c:311-313 warns about exactly this misuse. - 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 iscontigfree()(vm_contig.c:588-603). - 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 tovmstats.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. - 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
fixedfix.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'
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).
No comments yet.