# DF-5709 VERDICT — pass-2 audit of sys/libkern/hexncpy.c

**Status: reproduced** (deterministic, 3/3 identical runs, exit 0)
**Impact: none (latent hardening defect — no live misusing caller today)**
**Confidence: certain** (C operator precedence; demonstrated compile-time and runtime)

## The finding

`sys/sys/libkern.h:67` — `#define HEX_NCPYLEN(s) (s * 3)`. The macro argument
is substituted without parentheses. This is the *documented companion macro*
of `hexncpy()` (comment directly above it: "Number of bytes needed for
hexadecimal representation plus trailing \0"), and in-tree callers use it
both to size buffers and to compute the `outlen` argument. With a compound
argument the buffer size computes by C precedence:

- `HEX_NCPYLEN(n + 1)` → `n + 1 * 3` = `n + 3`   (needed: `3n+3`)
- `HEX_NCPYLEN(a << 1)` → `a << 1 * 3` = `a << 3` (needed: `6a`)
- `HEX_NCPYLEN(x, y)`-style comma args are parenthesized calls in practice — safe.

`hexncpy` with `sep != NULL` writes exactly 3 bytes per input byte while
`outlen >= 3` (sys/libkern/hexncpy.c:56-61), so a caller whose buffer was
sized by the buggy macro but whose `outlen` came out right (or also wrong)
gets a linear overflow with hex-digit+separator content. Demonstrated in
T3: array sized 11 (macro, `NBYTES=8`) vs 27 needed; hexncpy(9 bytes,
outlen=27, ":") smashed 16 guard bytes past the 11-byte buffer and produced
the complete 26-char string `de:ad:be:ef:13:37:c0:0d:42`.

**Why Info/latent:** census of all 13 in-tree call sites (below) shows every
argument is a primary expression or a parenthesized function call — no live
mis-expansion exists in the tree today. The defect is a correctness/hardening
landmine for the next caller, exactly the class Info exists for.

## Evidence

- `run.log` / `run.2.log` — decisive T3 lines:
  `HEX_NCPYLEN(NBYTES+1), NBYTES=8 -> sizeof array = 11 (need 27)`,
  `guard bytes smashed past the 11-byte buffer: 16 (true footprint = 27)`,
  `T3 RESULT: LATENT OVERFLOW DEMONSTRATED`. Deterministic: third run piped
  to md5 = `804494d763fb1715ec30e662a4c4e2a3`, full-run output identical.
- `build.log` — compiles the guest's `/usr/src/sys/libkern/hexncpy.c`
  (md5 `09a38961bf3ecce78beed0079eacd651`, byte-identical to the audited tree)
  unmodified, behind `shim/sys/systm.h` (only `KKASSERT`→`assert`, `NULL`,
  `u_char`).
- `fix.diff` — one-line parenthesization; the harness's `HEX_NCPYLEN_FIXED`
  block is the same edit and sizes/expands correctly (27), i.e. the fix is
  validated at harness level. Kernel rebuild not warranted: the defect has
  no kernel manifestation today (fix_status=not_testable at kernel level).

## Pass-2 negative results (why this is the ONLY new finding)

### (a) Length math on valid inputs — theorem, exhaustively verified

For any `inlen > 0`, `outlen >= 3`, any `sep` (including NULL and ""):
the loop (sys/libkern/hexncpy.c:56-61) runs `k = min(inlen, floor(outlen/3))`
iterations, writes `w*k` bytes (`w=3` with sep, `w=2` without), then the NUL
lands at index `w*k - 1` (sys/libkern/hexncpy.c:62). Max touched index is
`3*floor(outlen/3) - 1 <= outlen - 1`: **never out of bounds**. It also
reads exactly `k <= inlen` bytes of `inb` — no OOB read. T1 verified this
over 60,800 cases (inlen 1..64 × outlen 3..192 × 5 seps) with guard bytes:
**0 violations**.

Differential vs reference (full string, char-granular truncation to
`outlen-1`, NUL): 48,640 cases → 24,384 exact matches; 12,160 deviations are
*all and only* the sep==NULL class (NUL eats the final nibble / early stop =
known DF-2253); 12,096 deviations are sep!=NULL whole-group truncation
(actual is a strict prefix of expected — occurs only when `outlen % 3 != 0`
and the budget ends mid-group; cosmetic granularity, no memory-safety
consequence, no caller depends on partial groups); **0 unexplained
deviations**. Notably, when `outlen` is a multiple of 3 and `sep != NULL`,
hexncpy matches the natural reference *exactly* — the 3-per-byte budget is
self-consistent with HEX_NCPYLEN for the separator case.

### (b) Consumer census (every call site, with lengths)

| Call site | buffer | outlen passed | sep | verdict |
|---|---|---|---|---|
| sys/netinet/if_ether.c:643 (arpintr) | `char hexstr[6]` (if_ether.c:632) | 5 (literal) | NULL | in-bounds; output truncated to 1 char = DF-2253 class |
| sys/netinet/if_ether.c:759,792-795,803,820,828 (arp_update_oncpu) | `char hexstr[2][64]` (if_ether.c:715) | `HEX_NCPYLEN(ifp->if_addrlen)` ≤ 18 (eth=6) | ":" | safe; packet pullup'd ≥ 8+2*addrlen+8 at if_ether.c:897-899 before any of these run |
| sys/netinet/if_ether.c:1054 (in_arpinput) | `char hexstr[64]` (if_ether.c:894) | 18 | ":" | safe |
| sys/net/vlan/if_vlan_ether.c:105 | `hexstr[HEX_NCPYLEN(sizeof(evl_hdr))]` (:55, DEBUG-only) | same | ":" | exact fit, safe |
| sys/net/sppp/if_spppsubr.c:5292 | `char hexstr[len]` **VLA** (5289) | `HEX_NCPYLEN(len)` | "-" | **OVERFLOW — known DF-0269** |
| sys/vfs/nfs/bootp_subr.c:718 | `hexstr[64]` | `HEX_NCPYLEN(sdl_alen)` unbounded | ":" | **known DF-3045** |
| sys/dev/raid/mly/mly.c:1344-1345 | `char hexstr[2][12]` (1283) | 12 | NULL/":" | in-bounds; sep=NULL output truncated = DF-2253 class |
| sys/dev/raid/mly/mly.c:2652 | `hexstr[HEX_NCPYLEN(10)]` (2624) | `HEX_NCPYLEN(ss->cdb_length)` | " " | safe: cdb_length validated ≤ 10 at mly.c:2205-2206 before commands are built |
| sys/dev/raid/mlx/mlx.c:1280-1281 | `char hexstr[2][12]` (1242) | 12 | ":" | exact fit (footprint 12), safe |
| sys/dev/raid/ciss/ciss.c:4181 | `hexstr[HEX_NCPYLEN(16)]=48` (4148) | `HEX_NCPYLEN(cc->cdb.cdb_length)` | " " | safe for kernel CAM paths (cdb_len ≤ 16); only root via mgmt ioctl could exceed |
| sys/dev/raid/aac/aac_debug.c:251,253 | `char hexstr[48]` (168) | 48 literal | " " | exact fit, safe |
| sys/dev/raid/amr/amr_cam.c:623 | `hexstr[HEX_NCPYLEN(16)]=48` (551) | `HEX_NCPYLEN(imin(dxfer_len,16))` ≤ 48 | " " | safe |
| sys/kern/uipc_mbuf.c:2537 (m_print, debug) | `kmalloc(HEX_NCPYLEN(len))` (2535) | `HEX_NCPYLEN(m2->m_len)` | "-" | safe: m2->m_len ≤ remaining len by chain invariant |

No NEW caller-side memory-safety bug beyond DF-0269/DF-3045.

**Cross-ref for the DF-0269 owners (new trigger, same sink, debug-gated):**
`sppp_chap_input` clamps `len` to the attacker-controlled `h->len`
(if_spppsubr.c:3906) *after* the `len < 4` early-out (3902-3905), so a CHAP
challenge with `h->len < 4` reaches `sppp_print_bytes((u_char *)(h+1), len-4)`
with `len-4 = -2` (3927), which wraps through the `u_short len` parameter to
65534 → `char hexstr[65534]` VLA plus hexncpy outlen 196602 — same broken
sink as DF-0269, triggered by u_short wrap instead of plain undersizing.

### (c) Parse side

No `hexnput`/hex-parse counterpart exists in-tree (only `hexncpy` is
declared, sys/sys/libkern.h:143; `hex2ascii_data` in sys/libkern/bcd.c:36 is
unrelated). Nothing to audit.

### (d) Remaining terminator issues beyond DF-2253

- `sep == ""` (empty separator string): `*sep` is `\0`, so NULs are written
  as separators and the caller-visible string truncates at the first pair
  (sys/libkern/hexncpy.c:59-60). Quirk, not memory-unsafe; **no in-tree
  caller passes ""** (census) — not filed.
- `KKASSERT` (sys/libkern/hexncpy.c:52) vanishes in production builds
  (INVARIANTS off): a NULL buffer or `outlen < 3` then walks into the loop /
  the `*--outb` — but that is exactly DF-2252's write-before-buffer
  primitive, same class, no new primitive.
- Integer overflow of `s * 3` in HEX_NCPYLEN needs `s > 715,827,882`; the
  largest live length is a sppp `u_short` (≤ 65535 → 196605) — unreachable.
- `hexdigit[*inb >> 4]` / `hexdigit[*inb & 0xf]` are bounded ≤ 15 against a
  17-byte table (sys/libkern/hexncpy.c:44,57-58) — safe.
- Pure function of its arguments (no globals, no locks) — no concurrency
  or statefulness surface.

## Reproduce

See README.md; `build.sh` + `run.sh` on the DragonFly guest (or any BSD/Linux
with cc — compile `hexncpy.kernel.c`, the packed verbatim copy, via the shim).
Guest artifacts under `/home/maxx/df5709/` (left in place; guest never
panicked, no kernel state changed — `guest_dirty: 0`).

## 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.
