# DF-5702 — VERDICT

**File:** `sys/libkern/strtoq.c` (129 LOC) with its duplicated-body sibling
`sys/libkern/strtouq.c` (107 LOC) — the kernel quad parser pair.
**Finding:** C99 `strtol`-family endptr/prefix contract violation: the kernel
`strtoq`/`strtouq` consume the `0x`/`0X` prefix **without confirming that a
hex digit follows**, so inputs whose C99 subject sequence is exactly `"0"`
are reported as **no conversion** (`*endptr = nptr`, return 0) instead of
"parsed 0, `*endptr` at the `x`". This diverges from DragonFly's own userland
libc (`strtoq`→`strtoll`/`strtouq`→`strtoull`, built from
`lib/libc/stdlib/_strtol.h:83-91` and `_strtoul.h:86-94`, both of which gate
the prefix on `isxdigit(s[1])`), producing kernel/userland parser desync on
the shared ASCII-integer surface.

## Reproduced? YES (status: reproduced, impact class: none — semantic contract divergence)

Guest: DragonFly 6.5-DEVELOPMENT `#0 Thu Jul 2 06:02:54 UTC 2026`, cc 8.3
[DragonFly]. The guest `/usr/src/sys/libkern/{strtoq,strtouq}.c` are
byte-identical to the audited repo tree (cksum 3699317468 4185 /
3988050194 3159 — see build_full.log).

### Method (the established differential rig)

`build.sh` seds the **untouched guest kernel sources** into userland-compilable
form (only the four kernel `#include`s swapped for `kernel_strtoq_shim.h`,
function renamed `k_strtoq`/`k_strtouq` — no hand edits; full generated copies
in `k_strtoq.c`/`k_strtouq.c`), links them into `diffharness` against guest
libc, and drives a 43-input × 9-base corpus through both families, comparing
VALUE, `endptr-nptr`, and post-call `errno`. A second copy carrying **only**
the proposed `isxdigit((unsigned char)s[1])` guard (`k_strtoq_fix.c`) proves
fix sufficiency in the same run.

### Decisive output (run.log / run2.log, deterministic across 3 runs, identical md5)

```
ENDPTR-DIFF s="0x"  b=0   kernel= 0 (off 0)  libc= 0 (off 1)
ENDPTR-DIFF s="0xg" b=16  kernel= 0 (off 0)  libc= 0 (off 1)
... (28 rows total: {"0x","0X","0xg","0XG","0x 1","0xz1","0xz"} × {base 0,16} × {signed,unsigned})

SUMMARY:
  kernel-vs-libc VALUE divergences      : 0   <- saturation values are bit-identical to libc
  kernel-vs-libc ENDPTR divergences     : 28  <- DF-5702, exactly the 0x-no-xdigit class
  invalid-base semantic diffs           : 258 <- legacy BSD (no base validation); no kernel caller passes base 1/37/-1
  saturation w/o ERANGE (DF-2230 class) : 79  <- KNOWN, root cause filed as DF-2230 (not re-reported)
  patched-kernel residual ENDPTR diffs  : 0   <- fix validation PASSED
  patched-kernel VALUE regressions      : 0   <- fix changes no value on any valid base
```

## Root cause (path:line)

- `sys/libkern/strtoq.c:72-77` and `sys/libkern/strtouq.c:70-75`: prefix taken
  on `(*s == 'x' || *s == 'X')` alone; when no hex digit follows, the digit
  loop (strtoq.c:103-121) breaks with `any == 0`, so strtoq.c:126-127
  (strtouq.c:104-105) stores `nptr` — the legitimate leading `0` digit is
  swallowed by the failed prefix. C99 7.20.1.4p7: "if the subject sequence
  begins with a '0x' or '0X' but is not followed by hex digits, then the
  subject sequence is just '0'" (endptr just past the `0`).
- Userland counterpart guards it: `lib/libc/stdlib/_strtol.h:83-91` and
  `_strtoul.h:86-94` (`&& isxdigit_l((unsigned char)s[1], locale)`) — same
  guard exists in NetBSD/OpenBSD/FreeBSD modern `strtol` implementations.

## Impact assessment (honest ceiling)

- **No memory-safety impact.** All reads terminate at the NUL (the `s[1]`
  peek at strtoq.c:74 is in-bounds because it is gated on `*s == 'x'|'X'`);
  the only write is `*endptr`. Verified end-to-end over the corpus.
- **No value desync**: kernel and libc produce bit-identical values for every
  input/base pair, including all saturation cases (the harness asserts this:
  VALUE divergences = 0). The divergence is strictly in the
  conversion/no-conversion decision, and the kernel is always the
  **stricter** (fail-closed) side: it never accepts or advances past what
  libc accepts.
- Concrete manifestations, all fail-closed:
  - `sys/netgraph/netgraph/ng_parse.c:620-628` and
    `sys/netgraph7/netgraph/ng_parse.c:640-648` (`ng_int64_parse`): a token
    like `"0xz"` that any C99-conforming parser (and thus any userland tool
    that pre-validated the same string with libc `strtoll`) accepts as 0 is
    rejected `EINVAL` by the kernel parser (`eptr == s + *off` guard).
  - `sys/kern/kern_environment.c:432-470` (`kgetenv_quad`), isp/hammer/dm
    callers: same fail-closed reject; inputs are loader/root-controlled.
  - `sys/kern/subr_scanf.c:186-234,391` (%d/%i/%o/%u/%x/%p): insulated —
    scanf pre-scans with its own PFXOK/NDIGITS logic and passes
    `endptr = NULL` to the parser.
  - plist layer (DF-2230's file): both kernel and userland reject
    `<integer>0xZZ</integer>` because `_prop_object_internalize_find_tag`
    (prop_object.c:390) requires the next char to be `<` — no accept/reject
    change there.
- Net: a C-contract violation and kernel/userland behavioral desync with
  fail-closed direction only. Severity Low (hardening/interop), confidence
  certain (machine-proven differential).

## Not re-reported (per pass-2 instructions)

- **DF-2230** (silent saturation of plist integers; root cause at
  strtoq.c:122-123 / strtouq.c:100-101 — kernel variant cannot signal
  ERANGE): the harness counts the 79 ERANGE-ONLY rows and marks them as the
  known class. The kernel-errno aspect is **not** filed here.

## Fix validation

`fix.diff` (git-apply-able against the read-only `sys/` tree; apply-tested in
a scratch repo) adds exactly the `isxdigit((unsigned char)s[1])` guard to
both files, mirroring `_strtol.h:86`/`_strtoul.h:89`. Compiling the patched
sources through the same rig yields **0 residual ENDPTR divergences and 0
value regressions** on all valid bases (run.log SUMMARY lines 5-6). This is
a pure-function semantic fix; no kernel rebuild is required to validate it
(the compiled object is the kernel body unchanged except the guard), and the
bug class is not memory corruption, so the rebuild-reboot fix protocol is
not triggered. Guest left running, no kernel state modified.

## Negative results (weight for the clean parts of this file)

- Memory safety: all string reads terminate at NUL (do-while isspace
  strtoq.c:61-63; `s[1]` peek gated strtoq.c:73-74; digit loop breaks at
  NUL before any further read, strtoq.c:103-111); only write is `*endptr`
  (strtoq.c:126-127). No OOB, no uninit, no truncation (all math is
  u_quad_t).
- Arithmetic: cutoff/cutlim idiom verified correct for the signed pair
  including the exact −2^63 boundary (strtoq.c:99-102: cutoff = 2^63 when
  neg; "−9223372036854775808" accepted, "−9223372036854775809" saturates to
  QUAD_MIN — value-identical to libc) and for the unsigned pair
  (strtouq.c:78-80: UQUAD_MAX boundary exact).
- Divide-by-zero impossible: `base == 0` is always converted to 8/10 at
  strtoq.c:78-79 / strtouq.c:76-77 before `qbase` is used; negative or >36
  bases break the digit loop immediately (`c >= base`, int compare —
  always true for negative base) before any division-dependent path can
  misbehave.
- `strtouq` negative acceptance (checklist d): '-' accepted, value negated
  modulo 2^64 (strtouq.c:62-64,102-103) — C99 7.20.1.4p5-conformant and
  identical to libc `strtoull` (harness: no divergence rows for "-1",
  "-18446744073709551615", etc. beyond the counted ERANGE class).
- ctype parity: kernel macros (sys/sys/ctype.h:43-51) are semantically
  identical to userland C-locale ctype(3) for all 256 bytes.
- No errno surface at all in the kernel variant (systematic difference vs
  libc ERANGE at `_strtol.h:139,149`) — that is DF-2230's claimed root
  cause; not re-filed.

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