# 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):
```c
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):
```c
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):
```c
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").
