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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4507 Β· 7 files
FileTypeDescriptionSize
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:

  1. It fills in manager->mm_shrink.{count_objects,scan_objects,seeks} β€” but DragonFly has no register_shrinker() definition anywhere in sys/ (the Linux shim sys/dev/drm/include/linux/shrinker.h defines the struct types only). The callbacks are never called. (For comparison: i915's register_shrinker() call is #if 0'd out, and ttm_page_alloc_dma.c is entirely preprocessed out.)
  2. It registers ttm_pool_shrink_scan via EVENTHANDLER_REGISTER(vm_lowmem, ...) β€” but no code in the entire kernel executes EVENTHANDLER_INVOKE(vm_lowmem, ...). The event list is created by the registration itself (slow-handler lists are created lazily) and then never fired. FreeBSD fires vm_lowmem from 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.

VERDICT.md
↓ download raw

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:

  1. ttm_pool_mm_shrink_init() assigns manager->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 in sys/ (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 header sys/dev/drm/include/linux/shrinker.h defines the struct types only β€” there is no registration API at all.
  2. 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 executes EVENTHANDLER_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 when pool->npages > _manager->options.max_size (ttm_page_alloc.c:709-721). max_size comes from ttm_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/*) drive ttm_pool_populate β†’ ttm_get_pages β†’ pool fills, and ttm_pool_unpopulate β†’ ttm_put_pages β†’ pool growth. Freed WC/UC pages accumulate up to max_size per 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:

  1. 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 future EVENTHANDLER_INVOKE sites type-safe.
  2. sys/vm/vm_pageout.c: fire EVENTHANDLER_INVOKE(vm_lowmem, 0) in the catastrophic-shortage block of vm_pageout_scan (before the OOM-kill path), where FreeBSD-equivalent semantics belong.
  3. sys/dev/drm/ttm/ttm_page_alloc.c: register a typed adapter (ttm_pool_lowmem(void *, int)) that builds a local struct shrink_control and calls ttm_pool_shrink_scan; drop the dead mm_shrink assignments. Registering the raw ttm_pool_shrink_scan (current code) would, once the event is wired, cast the integer flags argument to struct 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_testable
baseline no→ patch + rebuild →patched clean

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

Confirmed kernel references

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.