Kernel strtol/strtoul take the 0x/0X prefix without a following hex digit, violating the C99 strtol endptr contract and diverging from DragonFly's own userland libc on the same input (int-width twins of DF-5702; DF-5702's fix covered only the quad twins)
Summary
The int-width twins share the DF-5702 defect (a normalized diff proves the same Torek body modulo type width): the 0x prefix test lacks the isxdigit(s[1]) guard the userland template has, so '0x'+non-xdigit inputs report NO conversion where C99 and DFly libc parse 0 and advance endptr. Verified with the reused rig: 28 endptr divergences exactly the class, 0 value divergences across all int/long boundary rows, md5-identical x3; the one-line guard eliminates all 28 with 0 regressions. Both files compiled into the kernel; all base-0/16 consumers parse root/boot-trusted strings (the only user-reachable sites are base 10) - fail-closed kernel/userland parser desync, no memory-safety impact (the s+=2 past-NUL pointer is never dereferenced). isxdigit-guard fix for both int twins.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5703 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 6.6 KB | β raw | |
| diffharness.c | β | 6.6 KB | view raw | |
| kernel_strtol_shim.h | β | 771 B | view raw | |
| build.sh | β | 205 B | view raw | |
| run.sh | β | 127 B | view raw | |
| fix.diff | β | 903 B | view raw | |
| build.log | β | 1.7 KB | view raw | |
| run.log | β | 12.7 KB | view raw | |
| run.2.log | β | 12.7 KB | view raw | |
| run.3.log | β | 12.7 KB | view raw | |
| env.txt | β | 254 B | view raw | |
| verdict.json | β | 6.4 KB | view raw |
DF-5703 β kernel strtol/strtoul 0x-prefix endptr divergence (int-width twins of DF-5702)
What this proves
sys/libkern/strtol.c:70-75 (and its duplicated-body unsigned twin
sys/libkern/strtoul.c:68-73) accept the 0x/0X prefix without checking
that a hex digit follows, so inputs whose C99 subject sequence is exactly
"0" ("0x", "0X", "0xg", "0x 1", "0xz", β¦) are reported as
no conversion (*endptr = nptr, return 0) where C99 7.20.1.4p7 and
DragonFly's own userland libc (lib/libc/stdlib/_strtol.h:86 /
_strtoul.h:86, both gate the prefix on isxdigit_l(s[1])) parse 0 and
advance endptr past the '0'.
Same Chris Torek body as the quad twins of DF-5702 (strtoq.c/strtouq.c)
β a normalized diff shows the parser cores are identical modulo type width
and comments; DF-5702's fix covered the quad twins only, leaving these
int-width twins divergent.
Build / run (on the DragonFly guest)
scp -F dfbsd-qemu/config kernel_strtol_shim.h diffharness.c build.sh run.sh dfbsd-maxx:/tmp/df5703/ vm.sh run_user 'cd /tmp/df5703 && sh ./build.sh' vm.sh run_user 'cd /tmp/df5703 && sh ./run.sh'
build.sh sed-generates userland-compilable copies of the UNTOUCHED guest
/usr/src/sys/libkern/{strtol,strtoul}.c (cksum-verified against the audited
repo: 370024243 4183, 3544205135 3234) plus fix-only variants carrying
exactly the isxdigit((unsigned char)s[1]) guard, and links them against
guest libc.
Expected output
SUMMARY block (deterministic β md5-identical across 3 runs):
kernel-vs-libc VALUE divergences : 0 (must be 0) kernel-vs-libc ENDPTR divergences : 28 (DF-5703: 0x-prefix-no-xdigit class, valid bases) invalid-base semantic diffs : 306 (legacy BSD behavior; no kernel caller passes these) saturation w/o ERANGE (DF-2230 class) : 65 (known, not re-reported) patched-kernel residual ENDPTR diffs : 0 (fix validation; must be 0) patched-kernel VALUE regressions : 0 (must be 0) harness exit: 0
Each divergence row looks like:
ENDPTR-DIFF s="0x" b=0 kernel=0 (off 0) libc=0 (off 1) libc_ERANGE=0
value is always 0 on both sides β the kernel is strictly fail-closed (never accepts or advances beyond libc) β so this is a parser-desync / interop defect (Low), not memory corruption.
Corpus
DF-5702's 43 inputs plus the 32-bit-long boundary rows
(2147483647/2147483648/-2147483648/-2147483649/4294967295/4294967296,
hex forms) and the live 64-bit-long boundaries, x 9 bases x {signed,
unsigned} β the cutoff/cutlim idiom at int width is value-exact against
libc on every boundary row.
DF-5703 VERDICT β REPRODUCED (deterministic, 3/3 runs; Low severity, no memory-safety impact)
Finding
sys/libkern/strtol.c:70-75 β the 0x-prefix test:
if ((base == 0 || base == 16) &&
c == '0' && (*s == 'x' || *s == 'X')) {
c = s[1];
s += 2;
base = 16;
}
lacks the isxdigit((unsigned char)s[1]) guard that DragonFly's own userland
template carries (lib/libc/stdlib/_strtol.h:86,
lib/libc/stdlib/_strtoul.h:86 β isxdigit_l((unsigned char)s[1], locale)).
Consequence: when 0x/0X is followed by a non-hex-digit (including the
terminator), the kernel body skips the prefix, finds no digits, and reports
no conversion (any == 0 β *endptr = nptr, strtol.c:122-123,
return 0), where C99 7.20.1.4 (the subject sequence is the longest valid
prefix, i.e. "0") and DragonFly libc parse 0 and advance *endptr past
the '0'. The unsigned twin sys/libkern/strtoul.c:68-73 is
byte-identical in this block and has the same divergence.
Twin verification (the "share or differ" question)
- A type-normalized diff (
s/quad_t/long/; s/u_quad_t/unsigned long/; s/qbase/base/) ofstrtoq.cvsstrtol.cleaves only comments and the cosmetic sign-ifshape: the parser core β including the unguarded 0x block β is the same Chris Torek body. The int-width twins SHARE the DF-5702 defect; DF-5702's fix (and itsfix.diff) covered only the quad twinsstrtoq.c/strtouq.c, leavingstrtol.c/strtoul.cdivergent. sys/conf/files:2233-2236compiles all four into the kernel (standard), unlike e.g.bcmp.c(DF-5700's not-compiled note).
Proof (Phase V, on the DragonFly guest)
Rig reused from DF-5702 (userland differential harness; kernel sources compiled unmodified against guest libc):
- Guest
/usr/src/sys/libkern/strtol.ccksum370024243 4183,strtoul.c3544205135 3234β identical to the audited repo tree (build.log:2-3). run.log/run.2.log/run.3.log: md5f7431ebd02350460d632303c275f7f4didentical across 3 runs; harness exit 0.- 28 ENDPTR-DIFF rows, exactly the 0x-no-xdigit class: inputs
{"0x","0X","0xg","0XG","0x 1","0xz1","0xz"}x bases {0,16} x {strtol,strtoul}. Representative row:ENDPTR-DIFF s="0x" b=0 kernel=0 (off 0) libc=0 (off 1). - 0 VALUE divergences on the full corpus (53 inputs x 6 valid bases x
2 families), including every 32-bit (
2147483647/2147483648/ -2147483648/-2147483649/4294967295/4294967296, hex forms) and 64-bit (Β±(2^63-1), Β±2^63, 2^64-1, 2^64, hex forms) long boundary row β the cutoff/cutlim idiom at int width (strtol.c:96-98) is value-exact against libc, both saturation directions. - 65 ERANGE-ONLY rows = identical saturation value, libc sets ERANGE, kernel cannot β DF-2230's class, counted, NOT re-reported.
- 306 invalid-base rows (base 1/37/-1): legacy BSD no-validation vs libc EINVAL β no in-tree kernel caller passes an invalid base (census: all call sites pass 0/8/10/16), documented negative note, not filed.
Fix validation
fix.diff (git-apply-checked against the repo) adds only
isxdigit((unsigned char)s[1]) to the prefix test in both files. The
fix-only variants compiled through the identical rig eliminate all 28
divergences with 0 value regressions (run.log SUMMARY: residual 0,
regressions 0; build.log carries the baseline-vs-fix unified diff showing
the guard as the only change). Pure-function semantic fix, non-memory-
corruption class β no kernel rebuild required for validation.
Threat model / consumer census (int variants)
Kernel strtol/strtoul callers (rg census, contrib excluded):
sys/netgraph/netgraph/ng_parse.c:339,432,526,1115andsys/netgraph7/netgraph/ng_parse.c:341,439,539,541,1038,1274β the ng_parse ASCII-integer surface, base 0 with endptr-validated consumption; the shared parser surface where kernel/ng EINVAL vs libc-validated accept desync matters. Netgraph configuration is root-only (ng sockets/sockopts), so no unpriv escalation.sys/kern/subr_bus.c:2311,2320(kenvhint.*at boot),sys/kern/subr_blist.c:1057,sys/kern/subr_alist.c:1005β boot-time, loader/root-trusted.sys/dev/bus/u4b/quirk/usb_quirk.c:978(kenv quirks, base 0), drmkstrtol/kstrtoulwrapperssys/dev/drm/include/linux/kernel.h:157,220(module params),sys/dev/sound/...hdaa.c(many, kenv/device hints),sys/dev/acpica/*(ACPIAML/kenv),sys/dev/raid/mpr,sys/dev/netif/*β all root/boot-trusted configuration strings.- The only unprivileged-user-reachable call sites parse base 10 with NULL
or length-checked endptr and are unaffected by the 0x class:
sys/vfs/hammer/hammer_vnops.c:1822(PFS symlink@@Pfs%05d,strtoul(buf, NULL, 10)) andsys/vfs/hammer/hammer_subs.c:1073(:%05dpath form,strtoul(str+1,&ptr,10)+ptr-str==6check). sys/libprop/prop_data.c:572(strtoul(tagattrval,&cp,0)) β kernel plist/XML parser; no in-tree kernel-side prop recv consumer feeds unprivileged input (census: noprop_dictionary_recv*callers outside libprop itself).
Impact ceiling: kernel/userland parser desync on the shared
ASCII-integer surface β fail-closed (kernel consumes strictly less than
libc; identical values), so mis-parsed values are impossible; the
defect is endptr-contract divergence on "0xβ¦" inputs to base 0/16
consumers. Low severity / hardening / interop, mirroring DF-5702.
Memory-safety negative results (why this is NOT a memory bug)
- All reads are NUL-terminated-contract (
s[1]atstrtol.c:72is read only when*s=='x', so it reads at most the byte after 'x', which the caller's terminator covers). - The
s += 2atstrtol.c:73can form a pointer one past the NUL for input exactly"0x", but the pre-fetchedc == '\0'breaks the digit loop atstrtol.c:100-107before any further*sdereference β no OOB read executes; the fix removes the stale pointer arithmetic as a side effect. - Sole write is
*endptr(strtol.c:122-123), gated non-NULL, storing a pointer into the caller's own buffer. cutlim(int) cannot truncate for valid bases (base <= 36 β cutlim <= 35);accarithmetic is unsigned-long throughout; the negative- range cutoff-(unsigned long)LONG_MIN(strtol.c:96) is the exact 2^63 wrap on two's-complement, verified value-exact at-2^63rows.
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
fixedfix.diff (git-apply-checked against the repo) adds only the isxdigit((unsigned char)s[1]) guard to both files; recompiled through the identical rig, all 28 ENDPTR divergences vanish and no value changes on any valid base (SUMMARY: patched residual 0, regressions 0). Pure-function semantic fix - the compiled fix variant is byte-for-byte the kernel body except the guard - so no kernel rebuild was required for validation (non-memory-corruption class).
findings/poc/DF-5703/run.log SUMMARY lines: 'patched-kernel residual ENDPTR diffs : 0 (fix validation; must be 0)' and 'patched-kernel VALUE regressions : 0 (must be 0)'; build.log contains the unified diff of k_strtol.c vs k_strtol_fix.c / k_strtoul.c vs k_strtoul_fix.c showing the guard as the only change.
Confirmed kernel references
- sys/libkern/strtol.c:70-75
- sys/libkern/strtol.c:96-98
- sys/libkern/strtol.c:99-117
- sys/libkern/strtol.c:122-123
- sys/libkern/strtoul.c:68-73
- lib/libc/stdlib/_strtol.h:86
- lib/libc/stdlib/_strtoul.h:86
- sys/conf/files:2233-2236
- sys/netgraph/netgraph/ng_parse.c:339
- sys/netgraph7/netgraph/ng_parse.c:341
- sys/kern/subr_bus.c:2311
- sys/vfs/hammer/hammer_vnops.c:1822
- sys/vfs/hammer/hammer_subs.c:1073
Detail
Evidence (decisive lines)
findings/poc/DF-5703/run.log (and run.2.log/run.3.log, all md5 f7431ebd02350460d632303c275f7f4d): rows 'ENDPTR-DIFF s="0x" b=0 kernel=0 (off 0) libc=0 (off 1)' x28; SUMMARY block: VALUE 0 / ENDPTR 28 / invalid-base 306 / ERANGE-ONLY 65 (DF-2230 class, not re-reported) / patched residual 0 / patched regressions 0. build.log shows cksum-identical guest /usr/src kernel sources (370024243 4183 strtol.c, 3544205135 3234 strtoul.c) and the sed-generated k_*.c copies with the fix variants differing by only the isxdigit guard. fix.diff git-apply-checked against the repo tree.
PoC changes
No seed PoC existed. Rig adapted from DF-5702's (directly reusable as flagged by the orchestrator): kernel_strtol_shim.h swaps the four kernel includes for userland equivalents (LONG_/ULONG_ already provided by userland limits.h); build.sh sed-generates userland-compilable k_strtol.c/k_strtoul.c plus fix-only k_*_fix.c variants from the untouched guest /usr/src copies (cksum-verified against the repo); diffharness.c drives DF-5702's 43-input corpus PLUS the int-width boundary rows (32-bit long 2147483647/2147483648/-2147483648/-2147483649/4294967295/4294967296 + hex forms, and the live 64-bit long boundaries) x 9 bases x {signed,unsigned} comparing kernel vs libc VALUE / endptr offset / ERANGE, with invalid-base rows classified separately so the DF-5703 signal is unambiguous.
Verified recommended fix
Add the same isxdigit((unsigned char)s[1]) guard the userland template has (_strtol.h:86 / _strtoul.h:86) to the 0x-prefix test in sys/libkern/strtol.c:70-75 and sys/libkern/strtoul.c:68-73 (see fix.diff).
Verdict
Kernel strtol/strtoul consume the 0x/0X prefix without verifying a hex digit follows (strtol.c:70-75, strtoul.c:68-73), so inputs whose C99 subject sequence is exactly "0" (e.g. "0x", "0xg", "0xz") are reported as NO conversion (endptr=nptr, return 0) where C99 7.20.1.4p7 and DragonFly's own userland libc (strtol via _strtol.h:86, strtoul via _strtoul.h:86, both gated on isxdigit_l(s[1])) parse 0 and advance endptr past the '0'. A type-normalized diff proves strtol.c is the same Chris Torek body as strtoq.c modulo type width: the int-width twins SHARE the DF-5702 defect, which DF-5702's fix left unfixed in these two files. Proven on the DragonFly guest with a differential harness compiling the untouched guest kernel sources (cksum-identical to the repo) against guest libc: 28 ENDPTR divergences, exactly and only this class (bases 0/16, both families), md5-identical across 3 runs. VALUE divergences are 0 across the whole corpus including every 32-bit and 64-bit long boundary row - the int-width cutoff idiom is value-exact, the kernel is strictly fail-closed (never accepts or advances beyond libc), and there is no memory-safety impact (all reads NUL-terminated; the s+=2 past-NUL pointer formed on input exactly "0x" is never dereferenced because the pre-fetched c=='\0' breaks the loop first; sole write is endptr). Net effect: kernel/userland parser desync on the shared ASCII-integer surface (ng_parse ng4/ng7 base-0 int32/hex parse EINVAL vs libc-validated accept; boot kenv consumers; hammer PFS name parsing is base 10 and unaffected) - Low severity hardening/interop defect, machine-proven, twin of DF-5702.
No comments yet.