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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5703 Β· 13 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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/) of strtoq.c vs strtol.c leaves only comments and the cosmetic sign-if shape: 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 its fix.diff) covered only the quad twins strtoq.c/strtouq.c, leaving strtol.c/strtoul.c divergent.
  • sys/conf/files:2233-2236 compiles 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.c cksum 370024243 4183, strtoul.c 3544205135 3234 β€” identical to the audited repo tree (build.log:2-3).
  • run.log / run.2.log / run.3.log: md5 f7431ebd02350460d632303c275f7f4d identical 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):

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] at strtol.c:72 is read only when *s=='x', so it reads at most the byte after 'x', which the caller's terminator covers).
  • The s += 2 at strtol.c:73 can form a pointer one past the NUL for input exactly "0x", but the pre-fetched c == '\0' breaks the digit loop at strtol.c:100-107 before any further *s dereference β€” 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); acc arithmetic 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^63 rows.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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.
↓ fix.diffper-fix-DF-5703

Confirmed kernel references

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.