# DF-2253 — sep==NULL misplaces NUL terminator and truncates output

`sys/libkern/hexncpy.c` lines 56-62 (Low, memcorrupt bucket / correctness).
VERIFY run 2026-10-17.

## What was verified and how

Same kernel-compiled harness as DF-2252 (`harness_hexncpy.c` appended to the
guest's `/usr/src/sys/libkern/hexncpy.c`, `make nativekernel`, sysctl-driven):
cases C4/C5/C6 exercise `sep == NULL` through the REAL production function.

* **C5 — the cited reachable instance, exact call shape of
  `sys/netinet/if_ether.c:643`** (`hexncpy((unsigned char *)&ar->ar_hrd, 2,
  hexstr, 5, NULL)` from `arpintr()`): input bytes `12 34` (ar_hrd 0x1234)
  -> stock kernel returns the string **"1"** — the loop wrote '1','2' then
  stopped (outlen drains 3 per iteration: 5 -> 2 < 3), and `*--outb = 0`
  (hexncpy.c:62) backed over the '2'. One hex digit instead of four; the
  low nibble of the first byte is silently dropped.
* **C4 — generic truncation:** `inlen=6, outlen=HEX_NCPYLEN(6)=18,
  sep=NULL` -> **"deadbeef123"** (11 digits, the 12th clobbered): the
  drain-3 accounting converts only ceil-ish 2/3 of the buffer when no
  separator is written.
* **C6 — mly.c:1344 shape:** `inlen=4, outlen=12, sep=NULL` ->
  **"deadbee"** instead of "deadbeef".

## End-to-end on the live stock kernel (kernel #1, INVARIANTS)

`arp_inject.c` writes a crafted ethernet frame (ar_hrd = 0x1234) to
`/dev/tap0`; `tapwrite()` injects it into `ifp->if_input` =
`ether_input()` (sys/net/tap/if_tap.c:981) -> NETISR_ARP -> `arpintr()`
(sys/netinet/if_ether.c:630-648) -> the line-643 `hexncpy` -> console:

```
arp_inject: wrote 42 bytes to /dev/tap0 (ar_hrd=0x1234)
arp: unknown hardware address format (0x1)      <- stock: ONE digit, should be (0x1234)
```

(`arp_e2e.log`.) The injector needs root only because /dev/tap0 is
root-only on the guest — on a real network, *any LAN peer* can send such a
frame unauthenticated; the impact is a wrong one-digit diagnostic in the log
(cosmetic/correctness, no memory unsafety: hexstr is char[6] and the write
stays at hexstr[1]).

## Fix validation (A/B, kernel #2 with fix.diff)

* C5 -> **"1234"**, C4 -> **"deadbeef1234"**, C6 -> **"deadbeef"**:
  sep==NULL now drains 2 per iteration and terminates right after the last
  digit.
* sep!="" control C1 -> `de-ad-be-ef`, **bit-identical to stock**.
* End-to-end re-run: `arp: unknown hardware address format (0x1234)` —
  full four digits (`arp_e2e_fixed.log`).

## Reproduce

```
# guest up with /usr/src (vm.sh reset with-src)
sh build.sh      # harness (+ pseudo-device tap) into kernel, nativekernel, reboot
sh run.sh        # matrix sysctl + tap create + arp_inject + dmesg check
```
