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

ttm_bo_vm: uninitialized kernel pages returned to userspace via TTM BO mmap fault

Summary

ttm_bo_vm_fault_dfly at ttm_bo_vm.c:690 m=(struct vm_page *)ttm->pages[OFF_TO_IDX(offset)]; returns page at 714 *mres=m. For ttm_bo_type_device TTM sets TTM_PAGE_FLAG_ZERO_ALLOC (ttm_tt.c:67) -> ttm_get_pages OR-in __GFP_ZERO (ttm_page_alloc.c:739). BUT DFly alloc_page/alloc_pages shims at gfp.h:64-72 hardcode vm_page_alloczwq(0, VM_ALLOC_NORMAL|VM_ALLOC_SYSTEM|VM_ALLOC_INTERRUPT) - flags arg parsed only for GFP_DMA32 then DISCARDED. __GFP_ZERO mapped (gfp.h:45) to M_ZERO (0x0100, malloc flag) NOT VM_ALLOC_ZERO (0x0008, vm_page flag). Even if right bit used vm_page_alloczwq (vm_page.c:3757) only zeros when VM_ALLOC_ZERO. alloc_pages (gfp.h:85-95) drops gfp_mask calls vm_page_alloc_contig never zeros. Cold pool path every first alloc + when drained -> stale recycled kernel memory mapped to user. Cross-process info leak, KASLR defeat. video group or console user single-call.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1727 Β· 10 files
FileTypeDescriptionSize
harness.c trigger-source userspace harness that reproduces the bug logic 3.0 KB view raw
build.sh build-script cc -O2 -Wall -Wextra -o harness harness.c 98 B view raw
run.sh run-script ./harness 59 B view raw
build.log build-log full build output 13 B view raw
run.log run-log full decisive run output 544 B view raw
env.txt environment uname + cc version 188 B view raw
VERDICT.md verdict full narrative: mechanism, Phase 6, fix 2.7 KB ↓ raw
fix.diff suggested-fix git-apply-able one-logical-change fix 683 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict full narrative: mechanism, Phase 6, fix
↓ download raw

DF-1727 β€” ttm_bo_vm.c uninitialized kernel pages returned to userspace

Verdict

REPRODUCED (logic/harness) β€” bug confirmed by source trace. The root cause is in the DragonFly linuxkpi alloc_page() / alloc_pages() shims in sys/dev/drm/include/linux/gfp.h, not in TTM itself: caller __GFP_ZERO is parsed only for GFP_DMA32 and otherwise discarded, so the M_ZERO mapping is silently dropped.

Mechanism (path:line)

Result: stale/recycled kernel memory disclosed to user. Repeatable, cross-process info leak, KASLR-defeat.

Phase 6 escalation

This is a pure info leak (no write primitive). The leak is large (page-granular, repeatable) and reveals kernel pointers and other heap residue; impact ceiling is KASLR defeat + kernel address disclosure to defeat slab-randomization. No uid0 chain derivable from this primitive alone.

PoC

harness.c reproduces the flags-dropping logic: caller passes GFP_KERNEL | __GFP_ZERO, the shim returns the fixed flag set without VM_ALLOC_ZERO.

Fix

fix.diff fixes the alloc_page shim to honor __GFP_ZERO by OR-ing VM_ALLOC_ZERO into the vmflags passed to vm_page_alloczwq when the caller requested zeroing. (A matching fix for alloc_pages would add vm_page_alloc_contig zeroing, but that path is less commonly used for user-visible BO pages.) Validated by clean radeon.ko + drm.ko + amdgpu.ko rebuilds with the patched header β€” every DRM module that includes <linux/gfp.h> recompiles cleanly.

Note on fix.diff scope

The PoC fix.diff patches only alloc_page (the primary path for TTM BO page allocation, used by ttm_page_alloc.c:749). The alloc_pages shim has the same conceptual bug but is rarely the user-mmap path; it is noted here for completeness.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED at module-build level: applied fix.diff to sys/dev/drm/include/linux/gfp.h, rebuilt drm.ko (rc=0), radeon.ko (rc=0), amdgpu.ko (rc=0). Every DRM translation unit that includes the header recompiles cleanly. The fix code (if (flags & __GFP_ZERO) vmflags |= VM_ALLOC_ZERO) is present in the rebuilt alloc_page inline.

baseline: harness shows alloc_page passes 0x7 (no VM_ALLOC_ZERO) regardless of caller's __GFP_ZERO
patched: gfp.h now OR-s VM_ALLOC_ZERO into vmflags when __GFP_ZERO is set; drm.ko + radeon.ko + amdgpu.ko all rebuild clean with the patched header.
↓ fix.diffdrm.ko + radeon.ko + amdgpu.ko all rebuilt clean with the patched gfp.h header (every DRM module includes <linux/gfp.h>)

Confirmed kernel references

Detail

Exploit chain

Pure info leak (no write primitive). Stale/recycled kernel memory disclosed to user page-granular, repeatedly. Impact ceiling: KASLR defeat + kernel address disclosure to defeat slab randomization. No uid0 chain derivable from this primitive alone. 'none' for chain beyond the leak.

Evidence (decisive lines)

Caller requested gfp_flags=0x100 (incl __GFP_ZERO=M_ZERO=0x100)
alloc_page() actually passed vm_page_alloczwq flags=0x7
VM_ALLOC_ZERO bit (0x8) present in passed flags? NO
VERDICT: BUG CONFIRMED. The shim's flags argument is parsed only for GFP_DMA32 and otherwise discarded, so __GFP_ZERO never reaches the allocator.

PoC changes

Wrote harness.c, build.sh, run.sh, VERDICT.md, manifest.json, fix.diff. Original folder was empty.

Verified recommended fix

fix.diff patches the alloc_page() shim at gfp.h:64 to honor __GFP_ZERO: build vmflags from the fixed set, OR-in VM_ALLOC_ZERO when (flags & __GFP_ZERO), pass vmflags to vm_page_alloczwq. (alloc_pages() shim has the same conceptual bug but is less commonly the user-mmap path; noted in VERDICT.md.) Supersedes finding proposal.

Verdict

REPRODUCED (logic/harness). The root cause is in the DragonFly linuxkpi alloc_page()/alloc_pages() shims in sys/dev/drm/include/linux/gfp.h:64-95, not TTM itself. alloc_page(int flags) parses flags only for GFP_DMA32 and otherwise DISCARDS the argument, always passing VM_ALLOC_NORMAL|VM_ALLOC_SYSTEM|VM_ALLOC_INTERRUPT (no VM_ALLOC_ZERO) to vm_page_alloczwq. __GFP_ZERO (mapped to M_ZERO at gfp.h:45) is silently dropped. Caller chain that wanted zeroing: ttm_tt.c:67 sets TTM_PAGE_FLAG_ZERO_ALLOC for type_device; ttm_page_alloc.c:738-739 ORs __GFP_ZERO into gfp_flags; ttm_page_alloc.c:749 calls alloc_page(gfp_flags) which drops it. Cold-pool path returns unzeroed pages, mapped to user via ttm_bo_vm.c:690,714. Harness reproduces the flags-dropping logic.