# DF-5712 VERDICT — index/rindex (char)ch truncation divergence

**Status: REPRODUCED** (deterministic, 3/3 runs) · **Impact: none** (no memory
unsafety; semantic/conformance divergence, latent) · **Confidence: certain**

## What was audited

Pass-2 twin audit of `sys/libkern/rindex.c` (53 LOC) and `sys/libkern/index.c`
(52 LOC) — the kernel's `strchr`/`strrchr` backend (`sys/sys/libkern.h:164-174`
inline-wraps `strchr`→`index`, `strrchr`→`rindex`; registered standard in
`sys/conf/files:2208,2216`).

## The primitives family checklist

**(a) c==0 tail handling / NUL-walk bounds — CLEAN, harness-proven.**
`index` tests `*u.p == ch` *before* the NUL test (sys/libkern/index.c:46-49), so
`index(p,0)` returns the pointer to the terminating NUL — exact `strchr` semantics.
`rindex` executes the match test on the NUL byte itself (`sys/libkern/rindex.c:47-48`
sets `save = u.p` when `ch==0`), then returns at rindex.c:49-50 — exact `strrchr`
semantics. Harness Part 2: `CH0_SEMANTICS: OK — match libc exactly (0 bad)` across
the whole corpus. The NUL-walk bound is: halt at the first zero byte —
byte-at-a-time, no word-at-a-time shape exists in either file.

**(b) Consumer census.** Direct callers (all NUL-safe):
- `sys/vfs/smbfs/smbfs_vfsops.c:170,173` — `index(strncpy(...),0)`: the entire
  `f_mntfromname[MNAMELEN]` is `bzero`d first (line 167), so the scan for the
  terminator is intrinsically bounded. (Legacy bucket.)
- `sys/vfs/smbfs/smbfs_vnops.c:975-1000` — NUL-terminated name parsing (legacy).
- `sys/netgraph/ksocket/ng_ksocket.c:222,579,586` — operates on a
  `ksnprintf(name, sizeof(name), "%s", ...)` copy (line 577) / the ng_parse
  NUL-terminated text buffer — bounded.
- `sys/ddb/db_input.c:249` — static, always-terminated ddb line buffer (privileged).
- `sys/libkern/fnmatch.c:99,105` — fnmatch contract requires NUL-terminated args.
- `sys/dev/raid/vinum/{vinumio.c:707,vinummemory.c:56,151}` — NUL-terminated
  config strings (privileged ioctl path).
- `sys/dev/disk/nata/ata-disk.c:511-512` — `index(param.model, ' ')` where
  `model[40]` (ata-raid.h:781) is device-supplied identify data; a hostile drive
  could in theory present 40 non-space/non-NUL bytes making the scan continue
  into the surrounding `ata_params` (it stops at the first zero byte of the
  512-byte identify image in the same softc — cosmetic `ad_describe()` string
  split only). Hardware-trust wart, not a primitive defect, not attacker-reachable.
Indirect: **every** kernel `strchr`/`strrchr` call site compiles to these two
functions. A grep for high-bit search arguments across `sys/` found **zero**
callers passing ch outside ASCII — the divergence below is latent.

**(c) Guard-page differential — CLEAN, harness-proven.** With the string's NUL
placed on the last byte before a `PROT_NONE` page, all scans (including `ch==0`,
returning exactly `guard-1`) complete without touching the guard page:
`GUARDPAGE_RESULT: byte-exact, halts at NUL (0 unexpected faults)`. The
deliberate no-NUL demo faults at the guard — that is the documented
NUL-termination contract, reachable in-kernel only via a consumer bug (none found).

**(d) Word-at-a-time overread — NONE.** Both loops are pure byte-at-a-time
(`for (;; ++u.p)` index.c:45 / rindex.c:46). Unlike vectorized libc `strchr`,
these never read past the terminator. Safest possible shape.

## The finding (reproduced)

`if (*u.p == ch)` (index.c:46, rindex.c:47) compares `int`-promoted `char`
against the untruncated `int ch`. ISO C requires the `char` conversion. On
x86_64 (signed char): for `ch ∈ [128,255]` (and any other out-of-range value,
e.g. ±256), the kernel never matches; libc matches `(char)ch`. Concrete samples
from run.log: `str="" ch=-256 kernel=-1 libc=0` (libc finds `(char)(-256)==0`,
the terminator); `str="a" ch=256 kernel=-1 libc=1`. Totals: **58 index + 58
rindex = 116 mismatches**, 3/3 runs identical.

Security framing: fail-to-match, never OOB. A ported parser searching a
high-bit delimiter (≥0x80) would treat delimited fields as one lump — a
field-separation/auth-confusion shape. No current in-tree trigger → Low, latent.

## Exploit chain

None — there is no memory-corruption primitive and no in-tree trigger; this is
a correctness/conformance defect in a pure function. Pushing to uid=0 is not
applicable to this class; the differential harness *is* the demonstration.

## Fix validation (function-level, in-harness)

`fix.diff` casts the comparison: `if (*u.p == (char)ch)` in both files. The
exact patched bodies (sed-applied to the verbatim kernel sources, one char
changed — diff shown in build.log) were compiled into the same harness:
`PATCHED_TOTAL_MISMATCHES=0` (3/3 runs). `(char)ch` is the libc-exact fix on
both signednesses of `char`: on signed-char machines `(char)128 == (char)0x80 ==
-128` matches the sign-extended byte; on unsigned-char machines the same cast
matches the zero-extended byte, mirroring libc byte comparison. A kernel rebuild
is not meaningful for this pure function beyond the object-level equivalence
already demonstrated; the rig did not need to be reset (no kernel state touched,
guest stayed clean, no panic).

## Guest

DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 x86_64,
cc 8.3, page size 4096 (env.txt). Guest `/usr/src/sys/libkern/{index,rindex}.c`
sha256-identical to the audited repo tree (build.log).

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