index/rindex omit the ISO C char conversion of the search argument: never match for ch outside [-128,127], diverging from strchr/strrchr (backs every kernel strchr/strrchr via libkern.h)
Summary
Both primitives compare the sign-extended string byte against the UNTRUNCATED int ch, where ISO C converts c to char first: for ch outside [-128,127] the kernel never matches while libc does - and libkern.h inline-wraps strchr/strrchr to these, so this is the behavior of every in-kernel call site. Guest differential (ch in [-300,300] over a corpus embedding 0x80/0xE9/0xFF): 116 divergences (58+58), deterministic 3/3; guard-page-proven no memory unsafety (byte-at-a-time, halts at NUL). No in-tree caller passes a high-bit search byte (census) - latent parser-port hazard (field-separation/auth-confusion shape) + arch-dependent (char signedness). (char)ch cast fix validated (patched mismatches=0). Both twins marked done.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5712 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| diffharness.c | β | 6.5 KB | view raw | |
| build.sh | β | 250 B | view raw | |
| run.sh | β | 118 B | view raw | |
| build.log | β | 903 B | view raw | |
| run.log | β | 1.4 KB | view raw | |
| run.2.log | β | 1.4 KB | view raw | |
| run.3.log | β | 1.4 KB | view raw | |
| env.txt | β | 218 B | view raw | |
| fix.diff | β | 463 B | view raw | |
| VERDICT.md | β | 5.6 KB | β raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 4.3 KB | view raw |
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 bzerod 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.
Fix verification
fixedThe one-character-per-file fix was validated inside the same harness: build.sh sed-applies exactly if (u.p == ch) -> if (u.p == (char)ch) to the verbatim kernel sources (diff shown in build.log) and compiles the patched bodies alongside; the differential vs libc drops from 116 mismatches to 0 (PATCHED_TOTAL_MISMATCHES=0, 3/3 runs). The cast reproduces libc semantics on both signednesses of char. A full kernel rebuild adds nothing for this stateless pure function beyond the demonstrated object-level equivalence, so none was performed; fix.diff is authored against the read-only sys/ tree and was never applied there.
findings/poc/DF-5712/run.log: 'DIFF patched: index 0 mismatches, rindex 0 mismatches' / 'PATCHED_TOTAL_MISMATCHES=0'; build.log: the 44c44/45c45-style diff hunks showing only the cast changed; fix.diff: git-apply-able unified diff for both files.
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-5712/run.log: 'DIFF baseline: index 58 mismatches, rindex 58 mismatches' + 'BASELINE_TOTAL_MISMATCHES=116' + samples 'str="" ch=-256 kernel=-1 libc=0'; 'CH0_SEMANTICS: OK'; 'GUARDPAGE_RESULT: byte-exact, halts at NUL (0 unexpected faults)'; 'PATCHED_TOTAL_MISMATCHES=0'. Identical in run.2.log/run.3.log. build.log proves the guest /usr/src sources are sha256-identical to the audited tree and the compiled bodies are byte-identical (BODY-IDENTICAL lines).
PoC changes
Harness authored fresh (no seed). (1) kernel #include
Verified recommended fix
Cast the search argument to char before comparing in both primitives: if (*u.p == (char)ch) in sys/libkern/index.c and sys/libkern/rindex.c (see fix.diff).
Verdict
The kernel's index()/rindex() (the backend of every in-kernel strchr/strrchr via sys/sys/libkern.h:164-174) compare the sign-extended string byte against the untruncated int search argument at sys/libkern/index.c:46 and sys/libkern/rindex.c:47, where ISO C requires converting the argument to char first. For any ch outside [-128,127] the kernel scans never match while libc truncates and matches the corresponding byte: the userland differential harness (kernel sources compiled verbatim in the DragonFly guest, ch in [-300,300] over a high-bit corpus) shows 116 divergences from libc (58 index + 58 rindex), identical across 3 runs, dropping to 0 with the one-character ((char)ch) fix. No memory unsafety: both scans are byte-at-a-time and halt exactly at the NUL β guard-page-proven, ch==0 tail semantics match libc exactly. No in-tree caller passes a high-bit search byte (census in VERDICT.md), so the defect is latent: a parser ported from userland that searches for a delimiter >= 0x80 would silently treat every occurrence as absent (field-separation confusion shape).
No comments yet.