memcchr.c live longword over-read (proven page-contained and result-filtered) + unreachable goto done at :102
Summary
Unlike bcmp.c (dead), memcchr is LIVE in every kernel with the ext2 bitmap scans as sole consumers - and shares the DF-5700 over-read shape: align-down first word reads up to 7B below begin, the word-walk up to 7B past end (2,282,570 below-begin + 61,553 past-end loads measured over 2.6M differential cases). Proven benign by construction: every load is a naturally-aligned granule containing >=1 in-bounds byte (8|4096, cannot straddle a page; 762,160 both-side PROT_NONE guard calls, zero faults) and mismatches in over-read bytes are rejected by the done-filter (10,948/10,948 correct, no disclosure). 2.6M-case differential vs naive reference: 0 failures; DF-5701 truncation shape NOT shared (size_t end-to-end, all 4 consumer lengths bounded at mount). Filed as hardening so future consumers at guard-page boundaries inherit a provably in-bounds primitive; in-bounds-rewrite fix validated in-rig (zero out-of-range loads, differential unchanged). Unreachable goto noted.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5706 Β· 30 filesDF-5706 β sys/libkern/memcchr.c live longword over-read (proven benign) + hardening
What this pack proves (and what it does NOT claim)
sys/libkern/memcchr.c β unlike its sibling bcmp.c (DF-5700, latent) β is
compiled into every DragonFlyBSD kernel (sys/conf/files:2212) and
present in the shipped/running guest kernel (ffffffff809d5bf0 T memcchr,
disassembly matches this C source; see logs/kernel_liveness.txt).
Its word-at-a-time scanner performs a live over-read:
- backward: the first-word align-down load at
memcchr.c:83-85reads up to 7 bytes beforebegin; - forward: the word-walk load at
memcchr.c:90-91runs whilelp < end, so the last granule can extend up to 7 bytes pastbegin + n.
Measured on the pristine source (rig, guest): 2,282,570 below-begin loads,
61,553 past-end loads, max excursion 7 B in both directions, across
2,608,384 differential cases (logs/instr.out).
Why it is benign (the claim of this finding: hardening, no impact)
- Page containment (proof + empirical). Every load is a naturally
aligned
sizeof(long)granule that contains at least one in-bounds byte of[begin, begin+n)(the first granule containsbegin, the last containsend-1). Since 8 | 4096, a granule never straddles a page, so every load stays inside a page that holds valid buffer bytes β it cannot fault and cannot touch an unmapped page. The instrumented rig additionally asserts zero loads outside[align_down(begin), align_up(end))(g_outside_granule == 0), and a 762,160-call guard sweep with PROT_NONE pages flanking the data page on both sides (offsets 0..4095 Γ page-edge-critical lengths Γ 5 content modes) recorded zero faults. - Result filtering. Over-read bytes never escape the function: mismatches
found at
p >= endare rejected by thedone:filter atmemcchr.c:114. The rig observed the filter fire 10,948 times and returnNULLevery time (0 wrong returns across all sweeps). - No disclosure. The loaded words only steer control flow; no byte value
past
endis ever returned or copied out. - Consumers are bounded. All four call sites live in
sys/vfs/ext2fs/ext2_alloc.c(:1358,:1362inode-bitmap scan ofext2_nodealloccg;:1519,:1523block-bitmap scan ofext2_mapsearch). Their lengths are bounded to[1, bsize]by mount-time validation:e2fs_fpg == e2fs_bpg == bsize*8enforced atext2_vfsops.c:558-572,e2fs_ipg β [ipb, bsize*8]at:582-587(also excluding the div-by-zero atext2_alloc.c:1352), andext2_mapsearch'sbprefis normalized to the cg by its only callerext2_alloccg(ext2_alloc.c:1029-1032). Buffers arebsize-byte buffer-cachebreadblocks, so the β€7-byte same-page over-read stays inside the mapped buffer-cache page.
Also documented (same family as DF-5700's dead branch): the goto done; at
memcchr.c:102 is unreachable β entering the body requires *lp != word,
which guarantees one of the 8 TESTBYTEs jumps first. Harmless; kept as belt
and braces.
This finding does NOT share DF-5701's truncation shape: memcchr's
length parameter is size_t end-to-end (memcchr.c:60), and no consumer can
feed a negative or > bsize length given the mount checks above.
How to reproduce
Run on the host (pack copied anywhere) with the QEMU guest up, or directly inside the guest:
# host: ship the pack in (once)
tar czf df5706.tgz -C findings/poc DF-5706
cat df5706.tgz | ssh -F dfbsd-qemu/config dfbsd-maxx \
'rm -rf /tmp/rig5706 && mkdir -p /tmp/rig5706 && tar xzf - -C /tmp/rig5706'
dfbsd-qemu/vm.sh run_user 'cd /tmp/rig5706/DF-5706 && sh ./build.sh && sh ./run.sh'
Expected (see run.log, stable across run.2.log):
guard sweep: 762160 calls (NULL=317387 ptr=444773), wrong returns=0 RESULT: PASS - no load escaped the valid page ... exhaustive differential : 608384 cases, 0 failures randomized differential : 2000000 cases, 0 failures loads below begin : 2282570 (max 7 B before begin) loads past end : 61553 (max 7 B past end) done-filter rejections : 10948 (...) loads outside [align_down(begin), align_up(end)) : 0 (MUST be 0) RESULT: OVERREAD-CONFIRMED (live, <=7B both directions, granule-contained, result-filtered); semantics exact vs reference fixed differential: 2608384 cases, 0 failures fixed loads: below-start=0 past-end=0 in-range=11402250 outside-granule=0 ... RESULT: FIX-PASS - zero loads outside [begin,begin+n), semantics exact RESULT: PASS - no load escaped the valid page ... (guard sweep on FIXED build)
Rig construction
extract.sh+build.shcompile the verbatim kernel body (sys/libkern/memcchr.c:36-117, byte-identical βmemcchr_body.c, re-verified by diff; guest/usr/srcmd5a200de772658559da4a1217004b8469d== repo) withshim/prelude.hreplacing only the four kernel-only includes at:30-34.memcchr_instr.cis the same algorithm with every load site wrapped (LDC/LDB+NOTE); zero differential failures over 2.6M cases proves the instrumentation did not change behavior.guard_harness.cflanks one usable page with PROT_NONE guards on both sides and sweeps every begin-offset Γ page-edge-critical lengths Γ content modes (incl. the killer mode: canaries β c everywhere outside[begin,end)).fix.diff(validated, never applied tosys/): bytewise head alignment, full-word loop only while(lp + 1) <= end, bytewise tail β zero loads outside[begin, begin+n), semantics identical.
Files
build.sh,run.sh,extract.sh,shim/prelude.hβ rigmemcchr_kernel.c(pristine verbatim body, generated),memcchr_body.cmemcchr_instr.c,instr_driver.cβ measurement + differentialguard_harness.cβ containment sweep (used for pristine AND fixed)memcchr_fixed.c(generated: fix.diff applied),memcchr_fixed_instr.c,fixed_driver.cβ fix validationfix.diffβ the fix (git-apply-able against the repo root)build.log,run.log,run.2.log,logs/*.out,journal.txt,logs/kernel_liveness.txt,logs/scratch/(patched + orig kernel file)env.txtβ guest uname, cc, kernel symbol proofVERDICT.md,manifest.json,verdict.json
DF-5706 VERDICT β memcchr.c live, page-contained longword over-read (Info/hardening)
Status: reproduced (the over-read property, measured and bounded). Impact: none.
Context (pass 2, GLM 5.3)
sys/libkern/memcchr.c was re-audited with the primitives-family checklist
that produced DF-5700/5701 on the sibling sys/libkern/bcmp.c:
(a) longword walk / over-read containment, (b) length arithmetic
(int vs size_t truncation), (c) return semantics, (d) consumer census.
(0) Liveness β unlike bcmp.c, memcchr is LIVE
sys/conf/files:2212βlibkern/memcchr.c standard(bcmp.c has no entry).- Shipped guest kernel:
nm /boot/kernel/kernelβffffffff809d5bf0 T memcchr; build objectmemcchr.oβT memcchr. - Disassembly of the running kernel's symbol (
logs/kernel_liveness.txt) shows exactly this C implementation: 8-byte pattern replication (shl $0x8/$0x10/$0x20),n==0early return (test %rdx,%rdx; je), align-down first-word load (and $0xfffffffffffffff8,cmp %rcx,(%r9)). - Only consumer family: ext2 bitmaps β
sys/vfs/ext2fs/ext2_alloc.c:1358,1362(ext2_nodealloccg, inode bitmap) and:1519,1523(ext2_mapsearch, block bitmap); both look for the first non-0xffbyte (a free bit). ext2fs ships as/boot/kernel/ext2fs.ko; with the defaultvfs.usermount=0mounting an image requires root (context for exposure).
(a) The longword walk β over-read exists, is live, and is provably benign
Loads: first-word align-down *lp++ at memcchr.c:83-85; word-walk *lp at
memcchr.c:90-91 with loop guard lp < end (:90), so the last granule may
extend up to sizeof(long)-1 bytes past begin+n when end is unaligned;
the first granule may start up to 7 bytes before begin.
Containment proof: every load is a naturally aligned granule containing
β₯1 in-bounds byte (first granule contains begin; last contains end-1);
8 | 4096 β granules never straddle pages β each load lies fully inside a page
holding valid buffer bytes. It cannot fault; it cannot reach an unmapped page.
Empirical (guest rig, pristine verbatim body, md5-identical to repo):
* instrumented measurement (logs/instr.out): 2,282,570 loads below begin
(max 7 B), 61,553 loads past end (max 7 B), 11,404,517 fully in-range,
0 loads outside [align_down(begin), align_up(end));
* guard sweep (logs/guard.out): 762,160 calls, single usable page flanked
by PROT_NONE guards on both sides, every begin-offset 0..4095 Γ small +
page-edge-critical lengths (end landing at page offset 4096-k, k=0..9) Γ
5 content modes β zero faults.
No disclosure: mismatches located at p >= end are rejected by the
done: filter (memcchr.c:114); the rig observed 10,948 filter
activations, every one correctly returning NULL (0 wrong returns).
Loaded over-read bytes never leave the function. (A β€1-bit timing channel on
whether the β€7 bytes past the buffer equal c is theoretically present β
same as every strlen-style scanner β but for the ext2 consumers those bytes
are adjacent bytes of the same buffer-cache block; sub-threshold.)
Conclusion: DF-5700's over-read shape is shared but LIVE here β still not a vulnerability. Filed as Info (hardening) with the containment proof.
(b) Length arithmetic β DF-5701's shape NOT shared
memcchr(const void *begin, int c, size_t n) (memcchr.c:60) β size_t
end-to-end inside the function; end = begin + n (:84) cannot wrap for
any caller-reachable n. Consumers pass int len but every value is bounded
to [1, bsize] by mount-time validation:
e2fs_bpg == e2fs_fpg, both nonzero, andbpg == bsize*8exactly (ext2_vfsops.c:556-572) βlen = howmany(fpg, NBBY) - start = bsize - start β [1, bsize](ext2_alloc.c:1518);start = dtogd(fs,bpref)/NBBYis bounded becauseext2_alloccg(the only caller of mapsearch) forcesdtog(fs,bpref) == cgelsebpref = 0(ext2_alloc.c:1029-1030) and converts group-relative (:1032).e2fs_ipg β [e2fs_ipb, bsize*8](ext2_vfsops.c:582-587) β inode-bitmaplen = howmany(ipg - ipref, NBBY) β€ bsize(ext2_alloc.c:1357); wrap calls uselen = start + 1 β€ ceil(ipg/8)(:1360-1362,:1521-1523).ipg β₯ ipb β₯ 1also excludes the division by zero atext2_alloc.c:1352(ipref %= fs->e2fs_ipg).
A crafted ext2 image cannot reach memcchr with an out-of-range length: the superblock is validated at mount, before any bitmap scan.
(c) Return semantics β correct
Returns pointer to the first byte β c within [begin, begin+n), or NULL.
The byte loops deliberately test into the granule past end
(memcchr.c:86 bound is lp, not end) but the p < end filter at
:114 guarantees the return domain. Verified by 2,608,384 differential
cases vs a naive reference: 0 disagreements (608,384 exhaustive:
offsets 0..31 Γ lengths 0..96 Γ every mismatch position Γ 4 c-values with
below-begin/past-end canaries; + 2,000,000 randomized).
n == 0 returns before any load (:75-76) β rig-verified.
Also: the goto done; at :102 is unreachable (body entered only when
*lp != word, which forces an earlier TESTBYTE jump) β dead branch, same
family note as DF-5700; harmless.
Fix validation (rig-level, per the established family pattern)
fix.diff replaces the align-down head with bytewise alignment and runs the
word loop only while the whole granule is in range ((lp + 1) <= end), plus
a bytewise tail β zero loads outside [begin, begin+n) in any direction.
Applied via patch -p1 to a scratch copy of the pristine kernel file inside
the build (never to sys/):
- fixed instrumented: below-start=0, past-end=0, outside-granule=0;
differential 2,608,384 cases, 0 failures (
logs/fixed.out); - fixed guard sweep: 762,160 calls, PASS (
logs/guardfix.out).
No kernel rebuild was performed: the finding is Info/hardening with zero
demonstrated impact (nothing to observe in-kernel); the rig compiles the
exact kernel source bytes (memcchr_body.c byte-identical to
sys/libkern/memcchr.c:36-117, re-diffed; guest /usr/src md5 matches the
repo md5 a200de772658559da4a1217004b8469d), which is the same validation
level DF-5700 used for this family.
Negative results (classes hunted and killed)
- OOB write: none β function only reads; returns pointers, never writes.
- Uninitialized
patdone: impossible βdoneis reachable only via TESTBYTE afterpis assigned (:86,:92). - Pointer overflow
begin + n: unreachable n (β€ bsize at every call site). - Consumer OOB from crafted ext2 images: blocked at mount
(
ext2_vfsops.c:556-587); div-by-zero (ext2_alloc.c:1352) impossible. - Info leak: over-read bytes never returned (filter at
:114; measured). ~LONGPTR_MASKint-promotion on 64-bit:(uintptr_t)begin & ~7UL-safe (int β8 converts to the same mask);#if LONG_BITguards 32/64 only.- Refcount/locks/privilege: N/A (pure leaf function over caller buffers).
Guest left clean: rig confined to /tmp/rig5706 (removed after artifact
retrieval); no kernel changes, no panics, guest still up.
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
fixedfix.diff applied cleanly (patch -p1, hunk at :74) to a scratch copy of the pristine kernel file inside the rig and validated at rig level: instrumented fixed build shows below-start=0, past-end=0, outside-granule=0 over 2,608,384 differential cases (0 failures); both-side guard sweep passes unchanged. No kernel rebuild performed: the finding is Info/hardening with zero demonstrated impact, so there is no in-kernel behavior to observe; the rig compiles the exact kernel source bytes (md5-matched), matching the validation level used for this family in DF-5700.
logs/fixed.out (RESULT: FIX-PASS, 0 out-of-range loads), logs/guardfix.out (RESULT: PASS), logs/scratch/memcchr.c.patched, fix.diff, build.log
Confirmed kernel references
Detail
Evidence (decisive lines)
logs/instr.out (over-read CONFIRMED: max 7B below/past, 0 outside-granule loads, 10948 filter rejections, differential 0 failures); logs/guard.out (RESULT: PASS, 762160 guarded calls); logs/fixed.out + logs/guardfix.out (fix validation: 0 out-of-range loads, 0 differential failures); logs/kernel_liveness.txt (nm + objdump of shipped kernel's memcchr); build.log/run.log/run.2.log; memcchr_body.c byte-identical to sys/libkern/memcchr.c:36-117
PoC changes
Seed plan (userland differential + guard pages, established rig pattern) required three rig repairs before the authoritative run: (1) guard_harness.c second mprotect call was missing its length argument; (2) instrumented copies needed an explicit LONGPTR_MASK define (the pristine body defines it at :50 but the hand-written instrumented twins did not); (3) drivers needed
Verified recommended fix
Eliminate the over-read: bytewise head alignment, full-word loop only while (lp+1) <= end, bytewise tail (fix.diff; validated at rig level: zero out-of-range loads, semantics unchanged).
Verdict
The DF-5700-class longword over-read of sys/libkern/memcchr.c is real AND live - unlike bcmp.c this file is compiled into every kernel (sys/conf/files:2212) and the shipped guest kernel's memcchr (0xffffffff809d5bf0, disassembly matches this C source) is this implementation; its only consumers are the ext2 bitmap scans (ext2_alloc.c:1358/1362/1519/1523). Measured on the pristine, byte-identical source: the first-word align-down load (memcchr.c:83-85) reads up to 7 bytes below begin and the word-walk load (memcchr.c:90-91) up to 7 bytes past begin+n (2,282,570 / 61,553 occurrences over 2.6M differential cases). The over-read is provably benign: every load is a naturally-aligned granule containing >=1 in-bounds byte, 8|4096, so no load can straddle a page (instrumented invariant g_outside_granule==0; 762,160-call both-side PROT_NONE guard sweep with page-edge-critical lengths: zero faults), and mismatches found in over-read bytes are rejected by the done-filter at memcchr.c:114 (10,948 observed activations, all correctly returning NULL) so nothing leaks. DF-5701's truncation shape is NOT shared: n is size_t end-to-end (memcchr.c:60) and all consumer lengths are mount-bounded to [1,bsize] (ext2_vfsops.c:556-572 fpg==bpg==bsize8; :582-587 ipg<=bsize8; ext2_mapsearch's bpref normalized by ext2_alloccg:1029-1032). Return semantics exact over 2,608,384 differential cases (0 disagreements). Secondary note: the goto done at :102 is unreachable dead code. Hardening finding only - no exploitable path exists.
No comments yet.