# DF-5801 — vm_page_bits() / vm_page valid-dirty setter family: no bounds validation in release kernels

## What

`vm_page_bits()` (sys/vm/vm_page.c:3905-3923) converts a (base, size) byte
range within a page into the DEV_BSIZE valid/dirty bit mask. Its **only**
bounds guard is `KASSERT(base + size <= PAGE_SIZE, ...)` — and KASSERT is
compiled to a no-op unless `options INVARIANTS` is set
(sys/sys/systm.h:116-119). Every production/release DragonFly kernel ships
without INVARIANTS, so in deployed systems the function computes
`(2 << last_bit) - (1 << first_bit)` on whatever the caller passes, with:

- shift UB for negative or oversized arguments (`1 << first_bit` with
  first_bit < 0, `2 << last_bit` with last_bit >= 30 signed overflow),
- masks with bits >= 8 that, when OR'd into the `uint8_t m->valid` /
  `m->dirty` by the setter family (`vm_page_set_valid`,
  `vm_page_set_validclean`, `vm_page_set_validdirty`,
  `vm_page_clear_dirty`, `vm_page_set_invalid`), silently **over-mark the
  whole page valid** — the stale-kernel-data / info-leak direction, and
- phantom bit-8+ values that corrupt int-sized consumers such as the
  `m->dirty & vm_page_bits(...)` test in kern/vfs_bio.c:4100.

Pass-3 caller sweep (2026-10): **all current in-tree callers are bounded**
(uipc_syscalls.c:1774-1807 sendfile, kern/vfs_helper.c:346-354,
vm/vm_fault.c:3328-3330, kern/vfs_bio.c:377/2517/4149,
vfs/devfs/devfs_vnops.c:2206-2216, vfs/smbfs/smbfs_io.c:494) — so this is
a hardening finding (Info), not a demonstrated leak. The KLD rig proves
the missing guard is real, not theoretical.

## Files

- `pqb_bits_rig.c`   KLD rig: calls the real kernel `vm_page_bits()` /
                     `vm_page_set_valid()` with out-of-page arguments
- `Makefile`         kmod makefile
- `build.sh`         serves sources to the guest (QEMU slirp host=10.0.2.2)
                     and builds the KLD
- `run.sh`           loads the KLD (expect: panic on INVARIANTS kernels)
- `bitsarith.c`      userland twin of the release-build expression,
                     documents the masks a non-INVARIANTS kernel computes
- `fix.diff`         unconditional clamp in vm_page_bits (git-apply clean)

## Reproduce

    sh build.sh          # build KLD on guest
    sh run.sh            # kldload -> INVARIANTS kernel panics with
                         #   'vm_page_bits: illegal base/size 0/8192'
                         #   (see panic.txt; reproduced on 2 boots)
    cc -O2 -o bitsarith bitsarith.c && ./bitsarith   # release semantics

Fix validation (single-fix kernel):

    sh fix_build.sh      # apply fix.diff in guest /usr/src, nativekernel,
                         # installkernel, then reboot the guest
    sh fix_run.sh        # rebuild rig, kldload -> NO panic, clamped masks

## Expected

- Baseline (INVARIANTS guest): panic, args echoed — proving out-of-range
  input reaches the function with zero upstream validation.
- Release kernels: silent masks (0xFFFF / 0x1FF / 0x100) per bitsarith.
- Patched kernel: rig loads cleanly; vm_page_bits(0,8192)->0xff,
  vm_page_bits(4096,1)->0x0, UB cases clamped; setter marks only page bits.
