vm_page_free_contig() can leak PG_FICTITIOUS DMA-reserve pages when vm_low_phys_reserved has been lowered below the freed block
| Field | Value |
|---|---|
| ID | DF-2678 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-401 Missing Release of Memory |
| File | sys/vm/vm_page.c |
| Lines | 2868 (mutable watermark branch), 3184 (fictitious early-return) |
| Area | vm |
| Confidence | speculative |
| Discovered | 2026-08-29 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
vm_page_free_contig() decides between "return to alist" and "normal
free" using pa < vm_low_phys_reserved (vm_page.c:2868), but
vm_low_phys_reserved is a mutable watermark lowered by
vm_page_startup_finish() (:826) as boot-time reserve pages are released.
A contig block still inside the alist but above the final watermark
takes the else-branch on a PG_FICTITIOUS page: vm_page_unwire is a
no-op for fictitious pages and vm_page_free_toq early-returns at
:3184-3188 β the page is permanently leaked from both the alist and the
page queues, and v_wire_count accounting drifts.
Threat model & preconditions
In-kernel contigmalloc/contigfree users (drivers, privileged) on boot geometries where alist blocks survive above the final watermark; repeated alloc/free cycles shrink the low-DMA reserve until contigmalloc failures. No memory corruption; resource-exhaustion DoS of the DMA reserve only. Not reproduced (requires specific boot memory geometry; speculative).
Recommended fix
--- a/sys/vm/vm_page.c
+++ b/sys/vm/vm_page.c
@@ -2865,7 +2865,7 @@ vm_page_free_contig(vm_page_t m, unsigned long size)
vm_pindex_t start = pa >> PAGE_SHIFT;
vm_pindex_t pages = (size + PAGE_MASK) >> PAGE_SHIFT;
- if (pa < vm_low_phys_reserved) {
+ if (m->flags & PG_FICTITIOUS) {
/*
* Just assert check the first page for convenience.
*/
Timeline
- 2026-08-29 Discovered during pass-2 audit of vm_page.c (GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2678 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| v26harness.c | β | 14.3 KB | view raw | |
| run.log | β | 555 B | view raw | |
| fix.diff | β | 950 B | view raw |
Fix verification
fixedPatched kernel: vm_page_free_contig now returns fictitious pages to the DMA alist regardless of the watermark; the harness exhaustion loop ran to its 8192-iteration cap (baseline died at 2046 = full 141MB reserve lost) proving contigmalloc below the watermark never fails again.
cmd=4 on patched kernel: exhaustion loop: 8192 x 64KB (cap reached, no failure); free/wire counters stable
Confirmed kernel references
Detail
Exploit chain
kernel harness (root kldload) -> contigmalloc below watermark -> force watermark 0 -> vm_page_free_contig -> fictitious early-return leak -> repeat 2046x -> DMA reserve exhausted
Evidence (decisive lines)
v26harness.c t4 (cmd=4); df2678_mod.log: LEAK lines + exhaustion loop; kernel refs vm_page.c:2868,3184-3188,826
PoC changes
none (first-version harness)
Verified recommended fix
--- sys/vm/vm_page.c 2026-06-29 12:51:19.000000000 +0000
Verdict
Mechanically reproduced with a kernel-module harness that injects the exact precondition the finding describes: a PG_FICTITIOUS page allocated from the DMA alist whose physical address sits above vm_low_phys_reserved (the geometry vm_page_startup_finish produces when it lowers the watermark below blocks still tracked by the alist). vm_page_free_contig (sys/vm/vm_page.c:2868) branched on pa < vm_low_phys_reserved, took the normal-free path, and vm_page_free hit the PG_FICTITIOUS early-return at vm_page.c:3184-3188: the page was returned to neither the alist nor the free queues. A loop of alloc/free cycles drained 2046 x 64KB (128MB) from the guests 141MB DMA reserve before contigmalloc below the watermark failed permanently. Guest boot geometry showed the real watermark at 0xb063000 with 36163 reserved dma pages, so the scenario is boot/driver-geometry dependent exactly as the finding states, but the branch defect is proven deterministically. No unprivileged trigger: contigmalloc/free_contig callers are in-kernel.
No comments yet.