# DF-2252 — Off-by-one NUL write before caller buffer when inlen <= 0

`sys/libkern/hexncpy.c` lines 52-62 (Medium, memcorrupt). VERIFY run
2026-10-17. Evidence pack layout follows the house standard.

## What was verified and how

Runtime `kldload` GPFs this guest, so the harness is compiled INTO the
kernel: `harness_hexncpy.c` is appended to the guest's
`/usr/src/sys/libkern/hexncpy.c` (same translation unit as the REAL
production `hexncpy()`), built with `make nativekernel KERNCONF=X86_64_GENERIC`
(stock X86_64_GENERIC has `options INVARIANTS`, config line 56), installed
and booted. The harness is driven by `sysctl kern.df_hexncpy=<mode>`:

* mode 1 — canary/output matrix around `hexncpy()` itself
* mode 2 — `m_print()` with a legal `m_len == 0` mbuf leading the chain

## Baseline result (stock `hexncpy.c`, kernel #1 Oct 17 16:14:33 UTC 2026)

* **C2/C3 (the finding):** destination handed to `hexncpy()` is
  `&region[16]` of a 32-byte `kmalloc` region filled with 0xAA. With
  `inlen=0` and `inlen=-1` (`outlen=12`, so the line-52 KKASSERT passes),
  the byte before the buffer (`region[15]`) goes **0xaa -> 0x00** while
  `region[16]` stays 0xaa: `*--outb = 0` (hexncpy.c:62) wrote one byte
  BEFORE the caller buffer because the loop never advanced `outb`.
  `underflow_canary_hits=2`.
* **C1 control:** `inlen=4, outlen=12, sep="-"` -> `out=[de-ad-be-ef]`,
  canary intact — no underflow when `inlen >= 1`.
* **m_print path (mode 2):** `m_print()` (sys/kern/uipc_mbuf.c:2527-2544)
  with `m_len=0` leading mbuf passes `outlen=HEX_NCPYLEN(0)=0`
  (sys/sys/libkern.h:67 `#define HEX_NCPYLEN(s) (s * 3)`), so on this
  INVARIANTS kernel the line-52 KKASSERT fires FIRST:
  `panic: assertion "(outb != NULL && inb != NULL && outlen >= MIN_OUTBLEN)"
  failed in hexncpy at hexncpy.c:52` with backtrace `m_print() -> hexncpy()`
  (panic.txt). On production kernels KKASSERT compiles out
  (sys/sys/systm.h:116-119) and the same call instead falls through to the
  `*--outb = 0` underflow demonstrated by C2/C3 — a 1-byte NUL write before
  the `kmalloc(HEX_NCPYLEN(len), M_TEMP, ...)` buffer (uipc_mbuf.c:2533).

## Reachability (the honest scoping)

* `m_print()` has exactly one in-tree caller: `sys/dev/misc/musycc/musycc.c:754`
  (rx-error debug print on an ancient T1/E1 PCI card; no such hardware on the
  guest or in any common system). ddb has no `m_print` command in this tree,
  `netstat -m` reads sysctl counters only (no kernel m_print call), and no
  syscall/ioctl path reaches it. ddb `call` can invoke it manually, but ddb
  access is already root-equivalent.
* Every other in-tree `hexncpy()` caller that passes a variable `inlen`
  (uipc_mbuf.c:2537, ciss.c:4181, mly.c:2652, amr_cam.c:623, bootp_subr.c:718,
  if_ether.c:759+ if_addrlen calls) pairs it with `HEX_NCPYLEN(inlen)`, so
  `inlen<=0` implies `outlen==0` — KKASSERT panic on INVARIANTS kernels,
  underflow on production kernels. sppp guards with `if (len)`
  (if_spppsubr.c:5289-5293).
* **No unprivileged-reachable caller exists.** The write primitive is real
  and deterministic, but the only reachable behaviors today are
  INVARIANTS-panic / production-underflow from diagnostics paths that an
  unprivileged user cannot invoke. Not a privesc candidate as the tree
  stands; it is a landmine for any future zero-length-tolerating caller.

## Fix validation (A/B on the same guest)

`fix.diff` (also carries the DF-2253 accounting change — both live in the
same loop; plus a companion `m_print()` skip-zero-m_len hunk):
re-applied to the guest tree, kernel rebuilt (incremental nativekernel),
rebooted into kernel #2, identical harness re-run:

* C2/C3: canary stays 0xaa, `buf0=0x00` (terminator lands INSIDE the
  buffer at `addr[0]`) — underflow gone.
* C1: `out=[de-ad-be-ef]` — bit-identical to stock sep!="" behavior.
* C4: `deadbeef1234`, C5: `1234`, C6: `deadbeef` — sep==NULL outputs now
  full and correctly terminated (that part is DF-2253).
* mode 2: `m_print()` skips the empty mbuf, prints the 8-byte mbuf, returns
  — **no panic**, guest stays up.

See `run.log` (baseline matrix), `panic.txt` (baseline m_print panic),
`fix_run.log` (patched A/B), `fix_build.log`, `build.log` (baseline build).

## Reproduce

```
# guest must be up with /usr/src (vm.sh reset with-src)
sh build.sh      # backups, harness append, nativekernel, installkernel, reboot
sh run.sh        # sysctl kern.df_hexncpy=1 ; sysctl kern.df_hexncpy=2 (panics)
```
