# DF-2829 VERDICT — REPRODUCED (leak)

## Bottom line

`zinit()` kmallocs `struct vm_zone` (16640 bytes) without `M_ZERO`
(sys/vm/vm_zone.c:453) and `zinitna()` never initializes `z->znalloc`
(sys/vm/vm_zone.c:347-436 — every other stats field is set; `znalloc` is
only ever incremented at vm_zone.c:773/786). The world-readable sysctl
`vm.zone` prints that field (vm_zone.c:848,854-858) to **any local user**
(verified: identical output as root and via unprivileged `run_user`).

## How it was reproduced

Stock guest kernel (#0, X86_64_GENERIC, INVARIANTS):

1. KLD harness groomed the heap: 16 allocations of `sizeof(struct vm_zone)`
   memset to `0x41`, freed (same size class as `zinit`'s kmalloc).
2. `zinit("leakmark_zone", 64, 1, ZONE_DESTROYABLE)` — zone struct reused a
   marker block (zone=0xfffff80118628000, kprintf in run.log).
3. Unprivileged `sysctl vm.zone` printed:

   `leakmark_zon 000064, 00000000, 000000, 000000, 4702111234474983745`

   `4702111234474983745 == 0x4141414141414141` — the groomed heap marker,
   verbatim, in the REQUESTS column. USED/FREE/LIMIT are 0, so REQUESTS is
   *pure* uninitialized field content. Marker crossed to userspace on the
   first attempt (attempts=1) and identically for root and nobody.

## Reachability on a stock system (no KLD)

`swap_zone` is created with `zinit()` on every boot (sys/vm/swap_pager.c:441),
and the three netbt pools on `device bluetooth` kernels
(sys/netbt/bt_proto.c:139-149). On the fresh stock guest the SWAPMETA
REQUESTS column read `00000001` — i.e. the stale qword happened to be 0 on
that boot (early-boot heap largely fresh pages), which is why the harness
groom is used for a deterministic demonstration. The disclosure window is
*structural*: whatever occupies that 16 KB heap block before `zinit()`
(boot-time kernel allocations, or — for any future runtime `zinit()` caller,
e.g. a KLD — groomable content) is printed to unprivileged userspace.

Secondary window (same finding/fix): `LIST_INSERT_HEAD(&zlist, z)` at
vm_zone.c:359 happens *before* `bzero(z->zpcpu)` at vm_zone.c:362, so a
racing `sysctl vm.zone` reader can sum uninitialized per-CPU
zfreecnt/znalloc slots in the init window (same unprivileged reader surface
as DF-0951).

## Exploit chain

Not a corruption chain — a read primitive: unprivileged user reads
`sysctl vm.zone`; REQUESTS column of the first row discloses 8 bytes of
stale kernel heap per `zinit()`-created zone (decimal-encoded). Value is
bounded by heap residue at zone-creation time; on default kernels zone
creation is boot-time only, so the realistic yield is early-boot heap
residue (may contain kernel pointers → KASLR-relevant on hardened systems;
guest has no KASLR). Impact ceiling: limited kernel heap info leak
(CWE-457). No write primitive, no escalation path.

## Fix validation

Authored fix.diff (znalloc init in zinitna + `M_NOWAIT|M_ZERO` in zinit +
bzero-before-insert reorder). Applied to the guest's /usr/src copy,
`make nativekernel KERNCONF=X86_64_GENERIC` (vm_zone.o rebuilt 20:55,
kernel.stripped relinked), `make installkernel`, reboot into kernel #1
(`DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 20:57:06 UTC 2026`).

Re-ran the exact PoC on the patched kernel (same groom, same module):
`leakmark_zon: ... REQUESTS 00000000` — the 0x4141414141414141 marker is
GONE (fix_validation.log). Baseline reproduced / patched not reproduced ⇒
fix_status=fixed.

## Kernel references

- sys/vm/vm_zone.c:453 (kmalloc without M_ZERO)
- sys/vm/vm_zone.c:347-363 (zinitna init block — no znalloc; bzero after insert)
- sys/vm/vm_zone.c:773,786 (only-ever-increment)
- sys/vm/vm_zone.c:848,854-858 (unprivileged read + print)
- sys/vm/swap_pager.c:441, sys/netbt/bt_proto.c:139-149 (stock zinit callers)
