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)
PoC verification
Evidence pack
findings/poc/DF-5709 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
#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).
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(md509a38961bf3ecce78beed0079eacd651, byte-identical to the audited tree) unmodified, behindshim/sys/systm.h(onlyKKASSERTβassert,NULL,u_char).fix.diffβ one-line parenthesization; the harness'sHEX_NCPYLEN_FIXEDblock 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):*sepis\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 oroutlen < 3then 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 * 3in HEX_NCPYLEN needss > 715,827,882; the largest live length is a spppu_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_testableKernel 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.
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
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.
No comments yet.