# 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.
