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

```c
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):

- `sys/netgraph/netgraph/ng_parse.c:339,432,526,1115` and
  `sys/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` (kenv `hint.*` 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),
  drm `kstrtol/kstrtoul` wrappers `sys/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)`) and `sys/vfs/hammer/hammer_subs.c:1073`
  (`:%05d` path form, `strtoul(str+1,&ptr,10)` + `ptr-str==6` check).
- `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: no `prop_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]` 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.
