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)
PoC verification
Evidence pack
findings/poc/DF-1727 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
sys/dev/drm/ttm/ttm_tt.c:67β forttm_bo_type_device,page_flags |= TTM_PAGE_FLAG_ZERO_ALLOC.sys/dev/drm/ttm/ttm_page_alloc.c:738-739βif (flags & TTM_PAGE_FLAG_ZERO_ALLOC) gfp_flags |= __GFP_ZERO;sys/dev/drm/ttm/ttm_page_alloc.c:749βp = alloc_page(gfp_flags);sys/dev/drm/include/linux/gfp.h:45β#define __GFP_ZERO M_ZERO.sys/dev/drm/include/linux/gfp.h:64-72βalloc_page(int flags) { ... return vm_page_alloczwq(0, VM_ALLOC_NORMAL|VM_ALLOC_SYSTEM|VM_ALLOC_INTERRUPT); }β theflagsargument is never used (onlyflags & GFP_DMA32is consulted earlier); the fixed flag set passed tovm_page_alloczwqdoes not includeVM_ALLOC_ZERO.sys/dev/drm/include/linux/gfp.h:85-95βalloc_pages()callsvm_page_alloc_contig(...)with no zero flag at all.- Cold-pool path (lines 742-757 in
ttm_page_alloc.c) β fresh pages not zeroed. sys/dev/drm/ttm/ttm_bo_vm.c:690,714βm = ttm->pages[OFF_TO_IDX(offset)]; *mres = m;maps the page to userspace.
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
fixedVALIDATED 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.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- n
- c
- l
- u
- d
- e
- /
- l
- i
- n
- u
- x
- /
- g
- f
- p
- .
- h
- :
- 4
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- n
- c
- l
- u
- d
- e
- /
- l
- i
- n
- u
- x
- /
- g
- f
- p
- .
- h
- :
- 6
- 4
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- t
- t
- m
- /
- t
- t
- m
- _
- t
- t
- .
- c
- :
- 6
- 7
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- t
- t
- m
- /
- t
- t
- m
- _
- p
- a
- g
- e
- _
- a
- l
- l
- o
- c
- .
- c
- :
- 7
- 3
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- t
- t
- m
- /
- t
- t
- m
- _
- p
- a
- g
- e
- _
- a
- l
- l
- o
- c
- .
- c
- :
- 7
- 4
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- t
- t
- m
- /
- t
- t
- m
- _
- b
- o
- _
- v
- m
- .
- c
- :
- 6
- 9
- 0
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.
No comments yet.