ttm_page_alloc: TTM page pools have no memory-pressure reclamation β the vm_lowmem eventhandler is never invoked anywhere in the kernel and register_shrinker does not exist
Summary
Both shrinker wirings are dead: register_shrinker has no definition in sys/ (only dead call sites), and the EVENTHANDLER_REGISTER(vm_lowmem,...) registers into an event no EVENTHANDLER_INVOKE site in the entire kernel ever fires (the list is created lazily so it compiles silently). On any drm-attached system the live WC/UC pools are trimmed only by the put-path over-limit check and are never drained under VM pressure - an unpriv user churning GPU buffers converts gigabytes of reclaimable memory into avoidable OOM kills. Secondary hazard: the registration is signature-confused (handler typed void*,int deref'd as shrink_control*), so any future naive wiring becomes a near-NULL kernel write. Static proof decisive; runtime hardware-gated. Typed-adapter + fire-from-vm_pageout fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4507 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.9 KB | β raw | |
| VERDICT.md | β | 6.4 KB | β raw | |
| static_proof.sh | β | 2.0 KB | view raw | |
| static_proof.log | β | 6.2 KB | view raw | |
| env.txt | β | 1.2 KB | view raw | |
| fix.diff | β | 2.7 KB | view raw | |
| verdict.json | β | 4.6 KB | view raw |
DF-4507 β TTM page pools have no memory-pressure reclamation on DragonFly
File: sys/dev/drm/ttm/ttm_page_alloc.c (live, unguarded β compiled into drm.ko)
Severity: Medium (local memory-pressure DoS amplification on drm systems)
Confidence: certain (statically proven dead reclamation path)
Verification status: untested at runtime β the QEMU audit guest has no GPU
(see VERDICT.md); the defect itself is proven by static_proof.sh, which is
fully reproducible offline.
What is broken
ttm_pool_mm_shrink_init() (ttm_page_alloc.c:442-449) tries to give the TTM
page pools a memory-pressure hook two ways, and both are dead:
- It fills in
manager->mm_shrink.{count_objects,scan_objects,seeks}β but DragonFly has noregister_shrinker()definition anywhere insys/(the Linux shimsys/dev/drm/include/linux/shrinker.hdefines the struct types only). The callbacks are never called. (For comparison: i915'sregister_shrinker()call is#if 0'd out, andttm_page_alloc_dma.cis entirely preprocessed out.) - It registers
ttm_pool_shrink_scanviaEVENTHANDLER_REGISTER(vm_lowmem, ...)β but no code in the entire kernel executesEVENTHANDLER_INVOKE(vm_lowmem, ...). The event list is created by the registration itself (slow-handler lists are created lazily) and then never fired. FreeBSD firesvm_lowmemfrom its VM subsystem; this DragonFly port never acquired the invoke site.
Net effect: the WC/UC page pools are never drained under VM pressure.
The only live trimming is the put-path over-limit trim
(ttm_page_alloc.c:709-721) at options.max_size, which defaults to half the
kernel zone in pages (sys/dev/drm/ttm/ttm_memory.c:353). On a drm desktop
(any system where radeon/amdgpu/i915 attach), an unprivileged client churning
GPU buffers can leave up to max_size pages per pool permanently pinned; when
the system later hits real memory pressure, the page daemon cannot reclaim
them, converting reclaimable memory into avoidable OOM kills.
Secondary hazard: ttm_pool_shrink_scan was registered with the wrong
signature. vm_lowmem handlers are invoked as func(void *arg, int flags);
the scan callback dereferences its second argument as
struct shrink_control * (sc->gfp_mask = M_WAITOK at line 398,
sc->nr_to_scan at line 418). Anyone who naively wires the event up in the
future turns this into a near-NULL pointer write (kernel panic). The proposed
fix adds a typed adapter. (This deadness is also why DF-1663's fini-vs-shrinker
race has no practical trigger on this tree.)
How to reproduce (static proof β no guest required)
./static_proof.sh > static_proof.log
Decisive lines in the output:
- [1] vm_lowmem is referenced only by ttm_page_alloc.c:447,453.
- [2] the complete list of EVENTHANDLER_INVOKE sites in sys/ β
vm_lowmem is not among them (COUNT=0).
- [3] register_shrinker has no definition; the only calls sit in #if 0
(i915_gem_shrinker.c:523-531) or in preprocessed-out code
(ttm_page_alloc_dma.c:36).
- [4] ttm_page_alloc.c has no preprocessing guard β it is live code.
- [5] the only live reclamation: put-path trim vs options.max_size
(ttm_memory.c:353).
- [6] the signature landmine at lines 398/418.
Why no runtime PoC
The audit QEMU guest (DragonFly 6.5-DEVELOPMENT X86_64_GENERIC) has a QEMU
std-VGA device (vendor 0x1234 device 0x1111), no /dev/dri, and no
radeon/amdgpu/i915 attachment β the TTM pools are never instantiated there,
so pool-growth-under-pressure cannot be demonstrated on it. See env.txt.
Fix
See fix.diff (never applied to sys/): declare the vm_lowmem event type
in sys/sys/eventhandler.h, invoke it from the page daemon's catastrophic
shortage block (sys/vm/vm_pageout.c), and register a correctly-typed
adapter instead of casting the scan callback. Fix validation is
not_testable on this guest for the same hardware reason.
DF-4507 VERDICT β untested at runtime; defect certain by static proof
Bottom line
The finding is certain: the memory-pressure reclamation path of the TTM
page pool (sys/dev/drm/ttm/ttm_page_alloc.c) is dead code on DragonFly, by
two independent and independently-verifiable facts:
ttm_pool_mm_shrink_init()assignsmanager->mm_shrink.count_objects / scan_objects / seeks(ttm_page_alloc.c:444-446) but nothing can ever call them:register_shrinker()has no definition anywhere insys/(rg -n "register_shrinker" sys/returns only the two call sites in dead code β i915_gem_shrinker.c:525 inside#if 0, and ttm_page_alloc_dma.c:1149 inside the whole-file#if defined(CONFIG_SWIOTLB) || defined(CONFIG_INTEL_IOMMU)guard at ttm_page_alloc_dma.c:36, which is never defined on DragonFly). The shim headersys/dev/drm/include/linux/shrinker.hdefines the struct types only β there is no registration API at all.- The fallback registration
EVENTHANDLER_REGISTER(vm_lowmem, ttm_pool_shrink_scan, manager, EVENTHANDLER_PRI_ANY)(ttm_page_alloc.c:447-448) creates a slow-handler list named "vm_lowmem" (eventhandler lists are created lazily at registration β sys/sys/eventhandler.h:128-129), but no code in the kernel ever executesEVENTHANDLER_INVOKE(vm_lowmem, ...)(rg -n "EVENTHANDLER_INVOKE" sys/enumerates every invoke site in the tree: usb, acpi, shutdown, power, network β vm_lowmem is not among them). The event therefore never fires, ever.
Everything in the file was read end-to-end; both claims above are reproduced
mechanically by static_proof.sh (output in static_proof.log).
Consequence chain (assessed)
- The four live pools (
wc,uc,wc dma32,uc dma32) are trimmed only by the put path whenpool->npages > _manager->options.max_size(ttm_page_alloc.c:709-721).max_sizecomes fromttm_page_alloc_init(glob, glob->zone_kernel->max_mem/(2*PAGE_SIZE))(sys/dev/drm/ttm/ttm_memory.c:353) β i.e. half the kernel memory zone, in pages, per pool. - ttm_page_alloc.c itself is live (no preprocessing guard;
sys/dev/drm/builds drm.ko from it; radeon/amdgpu sit in the same SUBDIR tree). - On any system where a drm GPU driver attaches, unprivileged GPU clients
(open
/dev/dri/*) drivettm_pool_populateβttm_get_pagesβ pool fills, andttm_pool_unpopulateβttm_put_pagesβ pool growth. Freed WC/UC pages accumulate up tomax_sizeper pool and are unreclaimable under VM pressure because both pressure hooks are dead. - Under a genuine memory squeeze the page daemon cannot drain these pools, so the system takes OOM kills (or stalls) while potentially gigabytes are sitting in the pools. On Linux this exact situation is handled by the mm shrinker this port dropped.
Severity was set to Medium rather than High: the impact is availability-only,
requires a drm-attached system plus a sustained unprivileged GPU workload and
coincident memory pressure, and the pinned amount is bounded by
4 Γ max_size pages.
Interaction with the known pass-1 finding DF-1663
DF-1663 (fini races vm_lowmem shrinker on static_buf and _manager
lifetime) is not re-reported here. Note however that its racing counterpart
is precisely this dead shrinker: because the event never fires, DF-1663's
race has no practical trigger on this tree either. Both resolve together by
either wiring the event correctly (see fix.diff) or deleting the dead
registration outright.
Why the status is untested (not not_reproduced)
The PoC would require instantiating the TTM pools, i.e. a GPU whose drm driver attaches. The audit QEMU guest has:
DragonFly dfbsd 6.5-DEVELOPMENT #0 X86_64_GENERIC vgapci0@pci0:0:2.0: chip=0x11111234 (QEMU std VGA β no drm driver) /dev/dri: absent drm.ko: present at /boot/kernel/drm.ko, not loaded at boot
so the finding is neither reproducible nor refutable there; the static proof is the decisive evidence (and it is certain). A runtime validation would need bare-metal or passthrough GPU hardware.
Side observation from the guest session (out of scope for this file, recorded
for the maintainers): kldload drm on the 6.5-DEVELOPMENT guest wedged the
kernel linker lock (kldstat afterwards blocks forever in state D on kldlk;
three such processes captured). drm_core_init() itself is #if 0-gutted,
and MODULE_DEPEND(drm, agp, ...) (sys/dev/drm/drm_drv.c:1143) forces the
agp.ko dependency load β the wedge lives in that module-load path, not in
ttm_page_alloc.c (whose init runs later, per ttm device global init). The
guest was reset clean afterwards (vm.sh reset with-src, verified up).
Exploit chain
None (availability class). Sketch of the DoS chain that a GPU-equipped system exposes: unprivileged user churns WC/UC GEM buffers β pools saturate at max_size pages each β user (or any other load) drives the system into the page daemon's shortage path β pools are not drained (dead hooks) β OOM kills of unrelated processes. Chain not executable on this guest (no GPU).
Fix validation
not_testable on this guest (baseline cannot be reproduced without a GPU).
fix.diff proposes the minimal correct wiring:
sys/sys/eventhandler.h: declare the event with a real type (typedef void (*vm_lowmem_fn)(void *, int); EVENTHANDLER_DECLARE(...)), which is also what makes futureEVENTHANDLER_INVOKEsites type-safe.sys/vm/vm_pageout.c: fireEVENTHANDLER_INVOKE(vm_lowmem, 0)in the catastrophic-shortage block ofvm_pageout_scan(before the OOM-kill path), where FreeBSD-equivalent semantics belong.sys/dev/drm/ttm/ttm_page_alloc.c: register a typed adapter (ttm_pool_lowmem(void *, int)) that builds a localstruct shrink_controland callsttm_pool_shrink_scan; drop the deadmm_shrinkassignments. Registering the rawttm_pool_shrink_scan(current code) would, once the event is wired, cast the integerflagsargument tostruct shrink_control *and write through it (ttm_page_alloc.c:398) β a near-NULL write and instant panic.
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
not_testablefix.diff (a35e872c...) proposes the correct wiring (event typedef+declare, pageout invoke before OOM kills, typed adapter replacing the signature-mismatched registration). Validation requires reproducing the baseline first, which needs a GPU-attached system this guest lacks; additionally the adapter removes the latent near-NULL sc->gfp_mask write that naive wiring would introduce
findings/poc/DF-4507/fix.diff hunks 1-3; landmine lines ttm_page_alloc.c:398,418; invoke-site placement sys/vm/vm_pageout.c:1794ff catastrophic block
Confirmed kernel references
- sys/dev/drm/ttm/ttm_page_alloc.c:385
- sys/dev/drm/ttm/ttm_page_alloc.c:397
- sys/dev/drm/ttm/ttm_page_alloc.c:418
- sys/dev/drm/ttm/ttm_page_alloc.c:442
- sys/dev/drm/ttm/ttm_page_alloc.c:447
- sys/dev/drm/ttm/ttm_page_alloc.c:709
- sys/dev/drm/include/linux/shrinker.h:32
- sys/dev/drm/i915/i915_gem_shrinker.c:525
- sys/dev/drm/ttm/ttm_page_alloc_dma.c:36
- sys/dev/drm/ttm/ttm_memory.c:353
- sys/sys/eventhandler.h:114
- sys/sys/eventhandler.h:128
Detail
Evidence (decisive lines)
static_proof.log sections [1]-[6]: vm_lowmem referenced only by ttm_page_alloc.c:447,453; full EVENTHANDLER_INVOKE enumeration shows COUNT=0 for vm_lowmem; register_shrinker absent (i915 call #if 0 at i915_gem_shrinker.c:523-531, dma variant preprocessed out at ttm_page_alloc_dma.c:36); ttm_page_alloc.c unguarded/live; only live trim at ttm_page_alloc.c:709-721 vs ttm_memory.c:353; env.txt documents guest lacking GPU (chip=0x11111234, no /dev/dri) and the kldload-drm linker wedge that forced a guest reset
PoC changes
n/a β no runtime PoC was seeded; a static proof script (static_proof.sh) was authored instead and its full output captured in static_proof.log
Verified recommended fix
Declare vm_lowmem with a real handler type in sys/sys/eventhandler.h, invoke it from the page daemon's catastrophic-shortage block in sys/vm/vm_pageout.c, and register a typed adapter (ttm_pool_lowmem(void*,int) building a local struct shrink_control) instead of casting ttm_pool_shrink_scan; drop the dead mm_shrink assignments
Verdict
Runtime-untestable on the audit guest (QEMU std-VGA, no drm driver attaches, so the TTM pools are never instantiated), but the defect is certain from static proof reproduced by static_proof.sh: the pool shrinker callbacks are assigned to manager->mm_shrink yet register_shrinker() has no definition anywhere in sys/ (its only call sites are #if 0 or whole-file-preprocessed-out), and the fallback EVENTHANDLER_REGISTER(vm_lowmem, ttm_pool_shrink_scan, ...) creates a slow-handler list that no EVENTHANDLER_INVOKE(vm_lowmem) site in the kernel ever fires. Consequence: on drm-attached systems the WC/UC page pools are never drained under memory pressure and are bounded only by the put-path trim at options.max_size (half the kernel zone, per pool β ttm_memory.c:353), turning reclaimable memory into avoidable OOM kills. Assessed impact class: local memory-pressure DoS amplification (dos), Medium. The registered callback also has a mismatched signature (vm_lowmem handlers are func(void,int) while ttm_pool_shrink_scan dereferences its second arg as struct shrink_control at lines 398/418), a panic landmine if the event is ever wired naively; fix.diff adds the typed adapter plus the event declaration and pageout invoke site.
No comments yet.