Race-induced kernel panic in vm_contig_pg_alloc when first iteration of alloc loop fails (vm_contig_pg_free called with size=0)
Summary
vm_contig_pg_alloc alloc loop at :401 unconditionally calls vm_contig_pg_free(start, (i-start)*PAGE_SIZE) on failure. On first iteration i==start so size=0. vm_contig_pg_free at :494 round_page(0)=0 -> panic(size must not be 0) at :496. Verify loop (:377) and verify->alloc transition (:398) hold NO lock -> concurrent allocator/fault/pageout touching candidate page between scan and alloc triggers. Same defect at :420-426 (PQ_FREE/hold_count check) and :408-419 (PQ_CACHE retry). Reachable via /dev/cpuctl CPUCTL_UPDATE (SYSCAP_NOCPUCTL_UPDATE, root) with 4MB size, or DRM/netmap paths. Widened by large allocation size (verify loop scales linearly). Fix: if (i > start) before vm_contig_pg_free.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0955 · 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| vm_contig_stress.c | trigger-source | 4 threads x 64MB mmap/munmap churn — tries to drive indirect contigmalloc race | 2.8 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -pthread | 143 B | view raw |
| run.sh | run-script | timeout 70 ./vm_contig_stress | 178 B | view raw |
| run.log | run-log | 60s stress, no panic; direct trigger devices absent on guest | 297 B | view raw |
| env.txt | environment | uname, cc version, /dev/cpuctl and /dev/netmap absent | 336 B | view raw |
| VERDICT.md | verdict | source trace + why no trigger path on this guest | 4.0 KB | ↓ raw |
| README.md | readme | build/run/expected | 722 B | ↓ raw |
| fix.diff | suggested-fix | if (i > start) before both vm_contig_pg_free calls — one-line obvious correctness fix | 955 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-0955 — PoC
vm_contig_pg_alloc calls vm_contig_pg_free(start, (i-start)*PAGE_SIZE)
unconditionally on alloc-loop failure. On the first iteration i==start
so size=0, which vm_contig_pg_free panics on.
Build
./build.sh
Run
./run.sh
Expected
The PoC hammers memory pressure hoping to drive an indirect
kernel-internal contigmalloc into the verify→alloc race. On this
guest, no /dev/cpuctl, /dev/netmap, or active DRM, so the direct
trigger paths are absent and the stress test does not panic.
See VERDICT.md for the source trace.
The fix (fix.diff) is a one-line obvious correctness fix: add
if (i > start) before both vm_contig_pg_free calls in the alloc
loop.
DF-0955 — vm_contig_pg_alloc size=0 panic (vm_contig.c:401-426)
Verdict: NOT REPRODUCED FROM USERSPACE (real bug by source inspection; needs concurrent allocator races not reachable from this guest)
Mechanism (confirmed by source inspection)
vm_contig_pg_alloc (sys/vm/vm_contig.c:254) has a verify→alloc
sequence with no lock held between them (the in-source comment at
:375 says "(still in critical section)" but it's a critical_section
in the cpu scheduler sense, not a vm_page spinlock; the page state can
change between verify and alloc).
The verify loop (:377):
for (i = start + 1; i < (start + size / PAGE_SIZE); i++) {
m = &pga[i];
pqtype = m->queue - m->pc;
if (... pqtype != PQ_FREE && pqtype != PQ_CACHE ... ||
m->wire_count || m->hold_count ||
(m->busy_count & (PBUSY_LOCKED | PBUSY_MASK)) ||
(m->flags & PG_NEED_COMMIT))
{
start++; goto again;
}
}
The alloc loop (:398):
for (i = start; i < (start + size / PAGE_SIZE); i++) {
m = &pga[i];
if (vm_page_busy_try(m, TRUE)) {
vm_contig_pg_free(start, (i - start) * PAGE_SIZE); // <-- panic if i==start
start++; goto again;
}
pqtype = m->queue - m->pc;
if (pqtype == PQ_CACHE && ...) {
...
}
if (pqtype != PQ_FREE || m->hold_count) {
vm_page_wakeup(m);
vm_contig_pg_free(start, (i - start) * PAGE_SIZE); // <-- panic if i==start
start++; goto again;
}
...
}
vm_contig_pg_free (vm_contig.c:489):
size = round_page(size);
if (size == 0)
panic("vm_contig_pg_free: size must not be 0");
On the first iteration of the alloc loop, i == start and
(i - start) * PAGE_SIZE == 0. If anything other threads did between
verify and alloc makes the FIRST page fail the alloc check (busy_try
fails, OR pqtype changed away from PQ_FREE/PQ_CACHE, OR hold_count
became non-zero, OR wire_count became non-zero), we hit the failure
branch and call vm_contig_pg_free(start, 0) → panic.
The race window is the time between the verify loop completing and the
alloc loop's first vm_page_busy_try. The window is sub-microsecond
but real (no lock is held).
Why it cannot be triggered from this guest
The bug requires concurrent vm_contig_pg_alloc activity (or any other allocator that touches the candidate page state) hitting the specific page in the verify→alloc window. From userspace, the only reachable triggers are:
/dev/cpuctl CPUCTL_UPDATEwith a ≥4MB size — not present on this guest (ls /dev/cpuctl*→ no such file,hw.cpuctl_verifyis an unknown sysctl). RequiresSYSCAP_NOCPUCTL_UPDATE(root).- DRM/ioctl paths — guest has no DRM activity.
- netmap (
/dev/netmap) — not present on this guest.
Without those, the only path into vm_contig_pg_alloc from userspace
is indirect (kernel-internal contigmalloc calls from drivers under
load). The stress test vm_contig_stress.c hammers 4 threads x 64MB
mmap/munmap churn for 60 seconds trying to drive the pageout daemon /
allocator into the race. run.log shows the test completed without
panic — the indirect path's race window is too narrow to hit from
generic memory pressure.
Conclusion
Real concurrency bug by source inspection (the i==start guard is
clearly missing in both failure branches). Recording
not_reproduced with confidence=likely — the bug exists, the
trigger paths (/dev/cpuctl, /dev/netmap, DRM) are absent or
inactive on this guest. Marking fix_status: not_testable — the
fix.diff is a one-line obvious correctness fix and compiles cleanly;
runtime validation would require either the missing /dev/cpuctl
device or a kernel-internal concurrency harness.
Suggested fix
fix.diff adds if (i > start) guards before both
vm_contig_pg_free calls in the alloc loop. If the failure occurs on
the first iteration, there is nothing previously allocated to free,
so we skip the call entirely. The fix matches the recommendation in
the finding's ## Recommended fix section ("if (i > start) before
vm_contig_pg_free").
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
—
Detail
Exploit chain
none
Evidence (decisive lines)
—
Verdict
Source-confirmed. vm_contig_pg_alloc i==start -> vm_contig_pg_free(0) -> panic. Needs /dev/cpuctl or kernel race. Fix: if(i>start) guard.
No comments yet.