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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5227 Β· 12 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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:

  1. 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.
  2. Fills the block: every page starts with the ASCII marker DF5227-STALE-KERNEL-MEMORY-MARKER!, body 0xA5.
  3. contigfree() β€” pages return to the DMA-reserve alist, contents preserved (alist_free, no zeroing anywhere: sys/vm/vm_page.c:2868-2877).
  4. dma_alloc_coherent(NULL, 2MB) β€” per the Linux DMA API contract this must return zeroed memory.
  5. 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:612 contigmalloc_map(size, M_DEVBUF, M_NOWAIT, ...) β€” no M_ZERO; vm_contig.c:533-534 zeroes only if (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

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 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'
↓ fix.diffper-fix-DF-5227

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.