# DF-2252 — VERDICT

**Status: reproduced** — impact: `panic` on this INVARIANTS guest through the
real `m_print()` caller; the underlying **deterministic 1-byte NUL underflow
write** is proven at function level on the kernel-compiled real `hexncpy()`.
Fix validated A/B on a rebuilt kernel (`fix.diff`, BUILD2_RC=0). **No
unprivileged-reachable caller exists in the tree** — see Reachability.

## Root cause (path:line)

- `sys/libkern/hexncpy.c:56` — loop `for (; inlen > 0 && outlen >=
  MIN_OUTBLEN; --inlen, outlen -= 3)`: `inlen` is never validated `> 0`
  on entry (the :52 KKASSERT checks only `outb`/`inb`/`outlen`).
- `sys/libkern/hexncpy.c:62` — unconditional `*--outb = 0`: when the loop
  body never ran (`inlen <= 0`), `outb == addr`, so the terminator writes
  NUL at `addr[-1]` — one byte BEFORE the caller's buffer.

## Reproduction (kernel-compiled harness around the REAL function)

Runtime `kldload` GPFs this guest, so `harness_hexncpy.c` was appended to
the guest's `/usr/src/sys/libkern/hexncpy.c` (same TU as the production
function), built with `make nativekernel KERNCONF=X86_64_GENERIC`
(INVARIANTS config, `sys/config/X86_64_GENERIC:56`), installed, booted
(kernel #1, Sat Oct 17 16:14:33 UTC 2026). Trigger: `sysctl
kern.df_hexncpy=1`.

Destination = `&region[16]` of a 32-byte kmalloc'd region prefilled 0xAA;
`outlen=12` so the :52 KKASSERT passes (it ignores `inlen`):

| case | inlen | stock kernel result | verdict |
|------|-------|---------------------|---------|
| C1 control, sep="-" | 4 | `out=[de-ad-be-ef]`, canary 0xaa->0xaa | no underflow when inlen>=1 |
| C2 | 0 | canary 0xaa **-> 0x00**, buf[0] stays 0xaa | **underflow reproduced** |
| C3 | -1 | canary 0xaa **-> 0x00** | **underflow reproduced** |

`underflow_canary_hits=2` (C2+C3 only). `run.log`.

## The cited caller, end to end (m_print)

`sys/kern/uipc_mbuf.c:2527-2544` `m_print()` walks the chain calling
`hexncpy(m2->m_data, m2->m_len, hexstr, HEX_NCPYLEN(m2->m_len), "-")`
(:2537) with no zero guard — and `m_len == 0` mbufs are legal. Crucially
`HEX_NCPYLEN(s) = (s * 3)` (`sys/sys/libkern.h:67`), so `m_len==0` passes
`outlen == 0`:

- **INVARIANTS kernels (this guest):** the :52 KKASSERT fires before the
  underflow — proven by harness mode 2 (`m_gethdr` chain with a legal
  zero-length leading mbuf, `m_print()` called for real):
  `panic: assertion "(outb != NULL && inb != NULL && outlen >=
  MIN_OUTBLEN)" failed in hexncpy at /usr/src/sys/libkern/hexncpy.c:52`,
  backtrace `hexncpy() <- m_print() <- sysctl_df_hexncpy()`, guest wedged
  at `db>` (`panic.txt`, serial console). ssh died, `vm.sh status` = down.
- **Production kernels:** `KKASSERT` compiles out (`sys/sys/systm.h:116-119`),
  the same call falls through to `*--outb = 0` — i.e. writes NUL immediately
  before the `kmalloc(HEX_NCPYLEN(len), M_TEMP, M_ZERO|M_WAITOK)` buffer
  (`uipc_mbuf.c:2533`). The C2/C3 canary result is exactly that write: a
  1-byte heap NUL underflow primitive, value fixed (0x00), offset fixed
  (last byte of the preceding slab object / allocator metadata region).

## Reachability — the honest scoping (TEAM DIRECTIVE)

Full caller sweep of `hexncpy()` in the tree:

- `m_print()` itself has exactly ONE in-tree caller:
  `sys/dev/misc/musycc/musycc.c:754` — RX-error debug print of an ancient
  T1/E1 PCI telephony card (and it clamps `m_len` to 16 but does not guard
  0, `musycc.c:750-754`). No musycc hardware exists on the guest (or in
  any common modern system); the driver is not even probed.
- ddb: no `m_print` command exists in `sys/ddb/` in this tree; `netstat -m`
  reads sysctl counters only (no kernel `m_print` call); no syscall/ioctl/
  netgraph path reaches it. ddb `call` can invoke it manually, but ddb
  console access is already root-equivalent.
- All other variable-`inlen` callers (`ciss.c:4181`, `mly.c:2652`,
  `amr_cam.c:623`, `bootp_subr.c:718`, the `if_ether.c` `if_addrlen` log
  sites) pair `outlen = HEX_NCPYLEN(inlen)`, so a zero length yields
  `outlen == 0` — the same KKASSERT/underflow dichotomy, from device-
  controlled or boot-time lengths only. `sppp_print_bytes` shows the
  correct pattern: `if (len)` guard (`sys/net/sppp/if_spppsubr.c:5289-5293`).
- Fixed-length callers (`if_ether.c:643` inlen=2, `mly.c:1344` inlen=4,
  `mlx.c:1280`, `aac_debug.c:251`) never pass `inlen <= 0`.

**Conclusion: the write primitive is real and 100% deterministic, but no
unprivileged-reachable caller passes `inlen <= 0` today. privesc_candidate =
false.** The m_print path is the strongest reachable instance and it is a
diagnostics routine behind root-only/debug/hardware walls. The bug remains
a serious landmine: any future caller that tolerates zero-length input
(e.g. a length straight off a packet header or a device field) instantly
turns this into a silent 1-byte heap NUL corruption on production kernels
(and a DoS panic on INVARIANTS ones).

## Fix validation (A/B, same guest image)

`fix.diff` = (a) `hexncpy.c`: drain `sep ? 3 : 2` per iteration, terminate
via `if (sep != NULL && outb != addr) *--outb = 0; else *outb = 0;` —
nothing-written (`inlen <= 0`) now terminates at `addr[0]` instead of
`addr[-1]`; (b) companion `uipc_mbuf.c` hunk: `m_print()` skips empty
mbufs (the cited caller's own zero guard). Applied to the guest tree
(patch -p1, hunks at hexncpy.c:53 and uipc_mbuf.c:2534), incremental
`make nativekernel` (BUILD2_RC=0, hexncpy.c + uipc_mbuf.c recompiled with
-Werror), rebooted into kernel #2 (Sat Oct 17 16:28:39 UTC 2026).

Identical harness, identical inputs (`fix_run.log`):

| case | baseline (stock) | patched |
|------|------------------|---------|
| C1 sep="-" inlen=4 | `de-ad-be-ef`, canary ok | `de-ad-be-ef`, canary ok — **bit-identical** |
| C2 inlen=0 | canary 0xaa->**0x00** | canary **0xaa->0xaa**, buf0=0x00 — underflow GONE |
| C3 inlen=-1 | canary 0xaa->**0x00** | canary **0xaa->0xaa**, buf0=0x00 — underflow GONE |
| m_print m_len=0 (mode 2) | KKASSERT panic, db>, guest down | prints the 8-byte mbuf, **returns**, guest up |
| underflow_canary_hits | 2 | 0 |

Bad behavior gone on the patched kernel; sep!="" output byte-for-byte
unchanged; guest stayed up through all re-runs.

## Artifacts

`run.log` (baseline matrix), `panic.txt` (baseline m_print panic + backtrace),
`fix_run.log` (patched A/B), `build.log` / `fix_build.log` (nativekernel
tails + RC lines; full 6 MB logs preserved on the guest at /root/build1.log,
/root/build2.log), `harness_hexncpy.c` (sha256
27dccb374f1ca3665e01527d75dd6de76e28209674e811845f9ffdea24778a75),
`fix.diff` (sha256 745fdae526ade23b4c34f118af06eeec2be0f312dd087f8859fce09270bfacd3,
git-apply clean against sys/), `env.txt`, `build.sh`, `run.sh`.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
