dma_alloc_coherent() returns non-zeroed (stale) kernel memory β Linux DMA API contract violation
Summary
kmem_alloc_contig without M_ZERO: pages come from the DMA-reserve alist/free scan with previous contents intact; the disabled FreeBSD reference in the same function passes BUS_DMA_ZERO. Every live in-tree caller carries a manual memset workaround (amdgpu_ih.c:66, drm_pci.c:130, radeon_gart.c:85) so latent today - future/upstream-synced callers become live kernel info leaks via GPU readback. Guest-verified 512/512 stale pages with planted marker reappearing verbatim in the same physical block; patched drm.ko 0/512. contigmalloc_map+M_ZERO fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5227 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc5227.c | β | 4.6 KB | view raw | |
| Makefile | β | 53 B | β download | |
| build.sh | β | 86 B | view raw | |
| run.sh | β | 135 B | view raw | |
| build.log | β | 1.4 KB | view raw | |
| run.log | β | 909 B | view raw | |
| run.fixed.log | β | 311 B | view raw | |
| env.txt | β | 198 B | view raw | |
| fix.diff | β | 1.9 KB | view raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| manifest.json | β | 1.0 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-5227 VERDICT β dma_alloc_coherent() returns stale kernel memory
Status: REPRODUCED (impact: leak) β on the guest, against the stock
/boot/kernel/drm.ko of DragonFly 6.5-DEVELOPMENT (built Jul 2 2026).
How it was proven
poc5227.c is a kernel module (KLD, -Werror clean) that calls the real
dma_alloc_coherent() exported by drm.ko:
contigmalloc(2MB)β the exact allocator the shim uses underneath (kmem_alloc_contigβcontigmalloc_map, sys/vm/vm_contig.c:608-613). Allocation landed at physical 0x3000000.- Fills the block: every page starts with the ASCII marker
DF5227-STALE-KERNEL-MEMORY-MARKER!, body 0xA5. contigfree()β pages return to the DMA-reserve alist, contents preserved (alist_free, no zeroing anywhere: sys/vm/vm_page.c:2868-2877).dma_alloc_coherent(NULL, 2MB)β per the Linux DMA API contract this must return zeroed memory.- Result (run.log): same physical block (0x3000000), 512/512 pages stale, all 512 markers present verbatim, 0xA5 fill visible in the hexdump.
Why deterministic: both allocations resolve through the low-memory DMA
reserve's first-fit alist_alloc (vm_page.c:2806-2814), so the second
allocation reuses exactly the block freed in step 3.
Why the shim is wrong
- linux_dma.c:47
kmem_alloc_contig(size, 0, high, align)β vm_contig.c:612contigmalloc_map(size, M_DEVBUF, M_NOWAIT, ...)β no M_ZERO; vm_contig.c:533-534 zeroes onlyif (flags & M_ZERO). - The reserve path (vm_contig.c:288-290) never zeroes either.
- The Linux contract ("coherent memory is zeroed") is what the disabled
FreeBSD reference implementation in the same function implements
(linux_dma.c:64-67 passes BUS_DMA_ZERO) β and what all live callers are
forced to hand-roll:
memset()at amdgpu_ih.c:66, drm_pci.c:130, radeon_gart.c:85.
Threat model / ceiling
On real hardware the "device" is the GPU. Any LinuxKPI consumer that trusts the contract and lets the engine (or userspace, via GEM/GTT mappings) read its coherent allocation discloses stale kernel/user memory. Today's in-tree callers mask the bug with memsets, so there is no live unprivileged leak on a stock desktop β hence Medium, not High. The defect is in the shim: every future/upstream-synced caller that drops its memset becomes a live kernel-info-leak (the classic dma-coherent leak class).
No uid=0 chain applies (info-leak class).
Fix validation (same run, same guest)
fix.diff (shared with DF-5228): allocate with
contigmalloc_map(..., M_NOWAIT | M_ZERO, ...).
- Guest reset (
vm.sh reset with-src), fix applied to/usr/src, drm.ko rebuilt from source, loaded fresh. - Re-run of the identical PoC (run.fixed.log):
DF5227 RESULT: marker_hits=0 stale_nonzero_pages=0/512β same physical block reuse, buffer now zeroed. Baseline behavior GONE.
Observations / notes
kldunload drm(stock or patched, no device attached) wedged the guest's kernel-linker subsystem on one occasion; pre-existing drm teardown behavior (see DF-4532/DF-5064 family), not related to this finding β the fix run avoided unload entirely (fresh boot + load).- Guest reset clean after the runs.
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 and loaded on a fresh boot (vm.sh reset with-src): the identical PoC returns marker_hits=0 stale_nonzero_pages=0/512 (run.fixed.log) - the stale-memory primitive is gone; allocation still succeeds on the same physical block, so no functional regression in the PoC path.
run.fixed.log: 'DF5227 RESULT: marker_hits=0 stale_nonzero_pages=0/512'; compare run.log baseline 'marker_hits=512 stale_nonzero_pages=512/512'
Confirmed kernel references
Detail
Exploit chain
spray/stale kernel data in contiguous physical pages (any prior contigmalloc/kmem_alloc_contig user, incl. network driver rings or a previous coherent allocation) -> driver calls dma_alloc_coherent() trusting the zeroing contract -> GPU/engine reads the buffer -> userspace reads GPU-accessible memory: stale kernel memory disclosure. The in-kernel primitive (stale bytes verbatim in the coherent allocation) is demonstrated; the userspace-read leg requires GPU hardware (absent on the audit guest).
Evidence (decisive lines)
run.log: 'SAME PHYSICAL BLOCK', 'marker_hits=512 stale_nonzero_pages=512/512', hexdump lines 'DF5227 LEAK 0000: 44 46 35 32 32 37 ... |DF5227-STALE-Ker|'; run.fixed.log: 'marker_hits=0 stale_nonzero_pages=0/512'
PoC changes
Authored fresh (no usable seed for this finding; the reserved DF-5226 directory holds the twin-batch's unrelated refuted-candidate negative record and was left untouched, so this finding used the next reserved ID). KLD module calls the real drm.ko-exported dma_alloc_coherent; determinism engineered via the DMA-reserve first-fit alist reuse (contigmalloc/memset/contigfree immediately before the coherent alloc).
Verified recommended fix
Implement dma_alloc_coherent with contigmalloc_map(..., M_NOWAIT | M_ZERO, ...) instead of kmem_alloc_contig (and dma_free_coherent with contigfree - see DF-5228); fix.diff in this pack.
Verdict
REPRODUCED on stock drm.ko (DragonFly 6.5-DEVELOPMENT guest): dma_alloc_coherent() (sys/dev/drm/linux_dma.c:47) implements the Linux coherent-allocation API over kmem_alloc_contig() without M_ZERO (vm_contig.c:612 passes M_NOWAIT only; zeroing requires M_ZERO at vm_contig.c:533-534; reserve path vm_contig.c:288 never zeroes), so the returned buffer contains the previous owner's kernel memory verbatim. PoC: contigmalloc 2MB -> marker fill -> contigfree -> dma_alloc_coherent 2MB lands on the SAME physical block (0x3000000) with 512/512 stale pages and the marker readable in the allocation. Contract violation certain; live in-tree callers (amdgpu_ih.c:66, drm_pci.c:130, radeon_gart.c:85) all carry manual memset() workarounds, so no unprivileged leak on stock desktop today -> severity Medium (latent kernel info leak for any caller that trusts the API, e.g. future upstream syncs). Fix validated in the same run: contigmalloc_map with M_NOWAIT|M_ZERO returns a fully zeroed buffer (marker_hits=0/512 stale 0/512) with identical reuse determinism.
No comments yet.