# DF-2830 VERDICT — REPRODUCED (leak)

## Bottom line

Classic off-by-one on a NUL terminator: for zone names of length ≥ 13,
`tmpname[len] = ':'` (sys/vm/vm_zone.c:841) writes over the only NUL of the
14-byte stack buffer `tmpname` (the clamp at vm_zone.c:835-836 allows
len == 13 == sizeof(tmpname)-1, and the space-fill/NUL loop terminates at
i == 13). `ksnprintf`'s `%s` (vm_zone.c:854) then reads past the buffer,
copying kernel-stack bytes after `tmpname[13]` into the `vm.zone` sysctl
output — world-readable, confirmed readable by an unprivileged user.

## How it was reproduced

Stock guest (kernel #0). A KLD created two zones with 13- and 16-character
names ("AAAAAAAAAAAAA", "BBBBBBBBBBBBBBBB"). Unprivileged
`sysctl vm.zone | hexdump -C` then showed, per affected row, **over 100
bytes of stack residue** appended to the zone label (run.hexdump.log,
leak_sample.txt):

```
...TS\n\nBBBBBBBBBBBBB:\nBBBBBBBBBBBBB:\n ... \nBBBBBBBBBBBB 000064,...
...AAAAAAAAAAAAA:AAAAAAAAAAAAA:AAAAAAAAAAAAA:...:ASWAPMETA:...
```

Mechanism (verified empirically, matches the code): the bytes immediately
following `tmpname` on the handler's stack frame are the previous
execution's `tmpbuf` (the sysctl handler reuses pooled kernel stacks), so
the over-read *echoes* the previous row and each successive read amplifies
the leak until `ksnprintf`'s 127-byte truncation cap stops it — the output
then reaches a stable fixed point (identical md5 across 3 unprivileged
reads). Side effects of the same bug: rows lose their terminating newline
and merge, and the SIZE/LIMIT/USED/FREE/REQUESTS columns get truncated
(stats corruption).

The echo content is self-referential *because the dominating prior use of
that stack is the sysctl itself*; the first read on any given kernel stack
emits whatever that stack last held — an unprivileged reader can trigger
reads from fresh threads at will. The over-read copies stack memory past a
14-byte boundary into userspace regardless of content (CWE-125/CWE-170).

## Reachability

Requires a zone whose name is ≥ 13 chars: `rfcomm_credit`
(sys/netbt/rfcomm_session.c:162) on `device bluetooth` kernels, or any
KLD-created zone. Stock GENERIC zone names (MAP ENTRY/PV ENTRY/SWAPMETA)
are ≤ 9 chars, so default-config kernels are not affected today — hence
Medium, not High.

## Exploit chain

Read primitive only: unprivileged `sysctl vm.zone` returns up to ~113 bytes
of kernel-stack residue per affected zone row, repeatedly. No write
primitive, no escalation.

## Fix validation

fix.diff clamps the name to 12 chars (`sizeof(tmpname) - 2`) so ':' lands
at tmpname[12] and the NUL at tmpname[13] survives. Applied together with
DF-2829's fix in the guest /usr/src, `make nativekernel
KERNCONF=X86_64_GENERIC`, installed, rebooted 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 with both zones loaded:
`AAAAAAAAAAAA: 000064, 00000000, 000000, 000000, 00000000` — clean padded
label, no echo, no merged rows (fix_hex.log). Baseline reproduced /
patched not reproduced ⇒ fix_status=fixed.

## Kernel references

- sys/vm/vm_zone.c:816 (char tmpname[14])
- sys/vm/vm_zone.c:834-841 (clamp + fill + memcpy + ':' clobber of NUL)
- sys/vm/vm_zone.c:854-858 (ksnprintf %s over-read → SYSCTL_OUT to user)
- sys/netbt/rfcomm_session.c:162 ("rfcomm_credit", 13 chars — in-tree trigger)
