DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0955

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0955 · 10 files
FileTypeDescriptionSize
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
README.md readme build/run/expected
↓ download 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.

VERDICT.md verdict source trace + why no trigger path on this guest
↓ download raw

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_UPDATE with a ≥4MB size — not present on this guest (ls /dev/cpuctl* → no such file, hw.cpuctl_verify is an unknown sysctl). Requires SYSCAP_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_testable

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