β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5709

HEX_NCPYLEN(s) does not parenthesize its argument β€” compound expressions mis-size caller buffers 3x undersize, latent linear overflow inside hexncpy

Summary

The documented buffer-sizing companion macro #define HEX_NCPYLEN(s) (s * 3) substitutes unparenthesized: HEX_NCPYLEN(n+1) expands to n+3 instead of 3(n+1), so a caller sizing a buffer with any compound argument gets a ~3x-undersized buffer while hexncpy writes 3 bytes per input byte - deterministic linear overflow with attacker-influenced hex content. Demonstrated: buf[HEX_NCPYLEN(8+1)]=11 bytes where 27 needed, 16 guard bytes smashed. All 13 in-tree callers pass safe argument shapes today (census) - Info hardening; several callers are one expression-edit away. Footprint theorem proven (60,800 guarded cases 0 violations on valid inputs; 48,640 differential 0 unexplained deviations); DF-2252/2253 reconfirmed not re-reported. parenthesize fix. New DF-0269 debug-gated trigger cross-ref: sppp clamps len AFTER the <4 check so len-4 wraps through u_short to 65534 at sppp_print_bytes.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5709 Β· 13 files
FileTypeDescriptionSize
README.md β€” 3.0 KB ↓ raw
VERDICT.md β€” 8.3 KB ↓ raw
hexdiff.c β€” 7.9 KB view raw
hexncpy.kernel.c β€” 2.6 KB view raw
shim/sys/systm.h β€” 609 B view raw
build.sh β€” 188 B view raw
run.sh β€” 109 B view raw
build.log β€” 106 B view raw
run.log β€” 964 B view raw
run.2.log β€” 312 B view raw
env.txt β€” 234 B view raw
fix.diff β€” 290 B view raw
verdict.json β€” 4.5 KB view raw

DF-5709 β€” HEX_NCPYLEN(s) macro does not parenthesize its argument (latent buffer mis-sizing for hexncpy callers)

What

sys/sys/libkern.h:67:

#define HEX_NCPYLEN(s)  (s * 3)

s is substituted unparenthesized. Any caller that sizes a buffer with a compound expression β€” e.g. char hexstr[HEX_NCPYLEN(cnt + 1)] β€” gets cnt + 1*3 = cnt + 3 bytes instead of (cnt+1)*3, a ~3x undersize. hexncpy (sys/libkern/hexncpy.c:56-62) writes 3 bytes per input byte when sep != NULL, so such a caller gets a linear heap/stack overflow inside hexncpy while believing the buffer was sized by the documented companion macro.

Status today: latent. All 13 in-tree call sites pass primary expressions or parenthesized calls (sizeof(...), ifp->if_addrlen, len, 16, ss->cdb_length, imin(csio->dxfer_len, 16), ifctx->sdl->sdl_alen, m2->m_len, CISS_CDB_BUFFER_SIZE, MLY_CMD_SCSI_SMALL_CDB) β€” verified by census (see VERDICT.md). No live misuse; filed as Info hardening with certain, demonstrated macro semantics.

How to reproduce (userland differential harness β€” the established rig)

The harness compiles the real kernel source unmodified (md5-matched against the guest's /usr/src/sys/libkern/hexncpy.c: 09a38961bf3ecce78beed0079eacd651) behind a one-header shim.

On the DragonFly guest (unprivileged user maxx):

mkdir -p ~/df5709 && cd ~/df5709
# copy: hexdiff.c hexncpy.kernel.c shim/sys/systm.h build.sh run.sh
HEXNCPY_SRC=/usr/src/sys/libkern/hexncpy.c sh build.sh   # -> ./hexdiff
sh run.sh

Expected output (decisive lines)

T1 guard theorem : cases=60800 violations=0 (expected 0)
T2 differential  : cases=48640 match=24384 dev_sepnull(DF-2253 class)=12160 dev_trunc_granularity=12096 dev_other=0 (expected 0)
T3 ...
   HEX_NCPYLEN(NBYTES+1), NBYTES=8 -> sizeof array = 11 (need 27)
   hexncpy(9 bytes, outlen=27, ":") into the 11-byte macro-sized buffer:
      guard bytes smashed past the 11-byte buffer: 16 (true footprint = 27 bytes)
T3 RESULT: LATENT OVERFLOW DEMONSTRATED (macro sizes 27->11, hexncpy needs 27 bytes at sep!=NULL)
T4 DF-2252 confirmed: inlen=0 wrote NUL at outb[-1] (byte before buffer)
T4 DF-2253 confirmed: sep=NULL, outlen=HEX_NCPYLEN(4)=12 -> '0102030' (final nibble of 04 destroyed)
  • T1/T2 are the pass-2 negative results: no OOB write on any valid input, and zero unexplained deviations from a reference formatter (remaining deviations are exactly the known DF-2253 class plus cosmetic whole-group truncation granularity).
  • T3 is the DF-5709 demonstration (macro mis-expansion + the overflow a future caller would get).
  • T4 reconfirms the two known findings behave as documented.

Exit code 0 iff T1 violations == 0, T2 dev_other == 0, and both T4 confirmations hit. Deterministic across runs (identical output md5).

Fix

One line, sys/sys/libkern.h:67 β€” see fix.diff: #define HEX_NCPYLEN(s) ((s) * 3). Validated inside the harness (HEX_NCPYLEN_FIXED sizes/expands correctly).

VERDICT.md
↓ download raw

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.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

Kernel rebuild not warranted for an Info latent macro defect with no kernel manifestation; the one-line fix is validated inside the harness itself (HEX_NCPYLEN_FIXED variant sizes arrays/expansions correctly: 27, and its guard section stays intact). fix.diff authored against the read-only sys/ tree, never applied.

run.log T3 section: 'HEX_NCPYLEN_FIXED(NBYTES+1) -> sizeof array = 27' vs macro's 11; fix.diff = the same parenthesization for sys/sys/libkern.h:67.
↓ fix.diffper-fix-DF-5709

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5709/run.log lines: 'T3 ... sizeof array = 11 (need 27)', 'guard bytes smashed past the 11-byte buffer: 16 (true footprint = 27 bytes)', 'T3 RESULT: LATENT OVERFLOW DEMONSTRATED'; 'T1 guard theorem : cases=60800 violations=0'; 'T2 differential ... dev_other=0'; 'T4 DF-2252 confirmed'; 'T4 DF-2253 confirmed'. build.log compiles the guest's /usr/src/sys/libkern/hexncpy.c (md5 09a38961bf3ecce78beed0079eacd651, byte-identical to the audited tree) unmodified via shim/sys/systm.h. Third run output md5 804494d763fb1715ec30e662a4c4e2a3 (deterministic).

PoC changes

No PoC seed existed (pass-2 new finding); authored the rig from scratch: hexdiff.c differential harness + shim/sys/systm.h (KKASSERT->assert, NULL, u_char) so sys/libkern/hexncpy.c compiles unmodified. First guest build failed on NULL-in-shim; fixed by adding to the shim. Harness embeds HEX_NCPYLEN verbatim from sys/sys/libkern.h:67 plus the parenthesized HEX_NCPYLEN_FIXED fix variant.

Verified recommended fix

Parenthesize the macro argument: #define HEX_NCPYLEN(s) ((s) * 3) (sys/sys/libkern.h:67)

Verdict

HEX_NCPYLEN(s) at sys/sys/libkern.h:67 expands its argument unparenthesized as (s * 3); a compound argument mis-sizes the caller's buffer by C precedence (HEX_NCPYLEN(n+1) -> n+3 instead of 3n+3) while hexncpy writes 3 bytes per input byte at sep!=NULL (sys/libkern/hexncpy.c:56-61), so the next caller to size a buffer with a compound expression gets a linear heap/stack overflow. Demonstrated deterministically by a userland differential harness compiling the real kernel source: macro-sized array = 11 bytes vs 27 needed, hexncpy smashed 16 guard bytes past it and produced the full 26-char string. Latent today: all 13 in-tree call sites pass primary/parenthesized arguments (census in VERDICT.md), hence Info hardening with certain macro semantics. Same harness proves the pass-2 negative results: 60,800 guard-byte cases with 0 OOB writes on valid inputs, 48,640 differential cases with 0 unexplained output deviations (remaining deviations are exactly the known DF-2253 class plus cosmetic whole-group truncation), and reconfirms DF-2252 (outb[-1] NUL at inlen=0) and DF-2253 (sep==NULL eats final nibble) behave as documented.