libkern/bcmp.c compares with full longword loads that read up to 7 bytes outside [ptr,ptr+len) β by design, page-contained, and latent-only (file not compiled on any DFly platform)
Summary
The LE body loads full longs regardless of length: tail loads read up to 7B past BOTH buffers, the streaming vb load up to 8-shr past, the down-alignment up to 7B below start (14,788 OOB loads measured over the exhaustive sweep). Provably page-contained (aligned 8-byte loads starting at addresses holding >=1 valid byte never straddle a page; 512,496 PROT_NONE-guard calls incl. page-corner placements, 0 faults; masked out of the result - no disclosure). LATENT ONLY: conf/files omits the file on every DFly platform, pc64 builds bcmp.s instead, and the running kernel's bcmp disassembles to the mjg strictly-in-bounds asm pattern - DF-5676's containment citation described dead code, and the true shipped-kernel argument is stronger. 2.6M differential cases vs memcmp: 0 disagreements. in-bounds-rewrite fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5700 Β· 21 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.5 KB | β raw | |
| VERDICT.md | β | 4.0 KB | β raw | |
| bcmp_kernel.c | β | 4.3 KB | view raw | |
| guard_harness.c | β | 5.2 KB | view raw | |
| bcmp_instr.c | β | 3.3 KB | view raw | |
| instr_driver.c | β | 3.2 KB | view raw | |
| asan_harness.c | β | 2.1 KB | view raw | |
| bcmp_fixed.c | β | 716 B | view raw | |
| bcmp_fixed_instr.c | β | 2.3 KB | view raw | |
| fixed_driver.c | β | 2.8 KB | view raw | |
| build.sh | β | 65 B | view raw | |
| run.sh | β | 89 B | view raw | |
| logs/guard.out | β | 249 B | view raw | |
| logs/instr.out | β | 397 B | view raw | |
| logs/fixed.out | β | 113 B | view raw | |
| logs/guardfix.out | β | 196 B | view raw | |
| logs/kernel_bcmp_disasm.txt | β | 3.2 KB | view raw | |
| logs/env.txt | β | 299 B | view raw | |
| fix.diff | β | 4.0 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 4.8 KB | view raw |
DF-5700 β libkern/bcmp.c by-design longword over-read (dead code on DFly; page-contained)
What the finding is
sys/libkern/bcmp.c compares with full unsigned long loads that are NOT
bounded by the caller's length:
- tail loads
*(culp)p1/*(culp)p2at bcmp.c:110-111 and bcmp.c:128-129 read up to8 - (len % 8)= up to 7 bytes past the end of both buffers (the exact case admitted by the 1994 comment at bcmp.c:53-56); - the misaligned-streaming loop's
vb = *(culp)p2at bcmp.c:89 reads up to8 - shrbytes past the point where 8 compared bytes end; - the down-alignment
p2 -= shr; va = *(culp)p2at bcmp.c:81-85 reads up to 7 bytes below the start of the b2 region.
All over-read bytes are masked/discarded (they never affect the result), so
there is no disclosure β but the loads happen, which matters under object-
granularity sanitizers and for any future caller relying on bcmp staying
inside its stated arguments.
Why it is only Low / impact none here
- The file is not compiled into any DragonFlyBSD kernel.
-
sys/conf/files:2199-2237(the libkern list) does not includelibkern/bcmp.c; -sys/platform/pc64/conf/files:131buildsplatform/pc64/x86_64/bcmp.sinstead (the mjg-macroMEMCMPβ verified below); -sys/platform/vkernel64/conf/fileshas no bcmp/memcmp entry (the vkernel is a userland binary that links host libc). - In-guest proof (
logs/kernel_bcmp_disasm.txt): the running X86_64_GENERIC kernel'sbcmpat0xffffffff80bcb800disassembles to the first/last overlapping-load pattern (cmp $0x10,%rdx;mov (%rdi),%r8;mov -0x8(%rdi,%rdx,1),%r8) β every load strictly inside[base, base+len). The shipped kernel bcmp has zero over-read; the C file's property is latent only. - Page-containment (the property DF-5676 leaned on) holds and is proven both analytically and empirically β see below.
Containment proof (why "bounded OOB read, page-contained" is correct)
Analytic: every longword load in the file is naturally 8-byte aligned, and
8 divides 4096, so an 8-byte load can never straddle a page boundary. Each
such load starts at an address that holds at least one in-bounds byte
(p2 -= shr stays in the first byte's page; loop and tail loads start
strictly below the region end). Hence every out-of-region load lies in the
same page as a valid byte of its buffer: the over-read can read adjacent
slab data in the same page but can never fault and never crosses into a
neighboring page.
Empirical (this rig): 512,496 calls with each buffer placed inside a single
page flanked by PROT_NONE guard pages β including buffers that end on
the last byte of the page and start on the first byte of the page, all
off1/off2 β [0,7] misalignment combinations, lengths 1..4096, and 500k
randomized placements β zero faults (logs/guard.out).
Measurement (that the over-read is real, not theoretical): a load-instrumented
copy of the exact algorithm (bcmp_instr.c, identical control flow, every
load wrapped) counts over the exhaustive alignment/length sweep:
buf1: below-start loads=11554 (max 7 B); past-end loads=2434 (max 7 B)
and buf0: past-end loads=2436 (max 7 B) (logs/instr.out). ASAN would
show the same but the guest has no libasan (asan_harness.c included for
hosts that do).
Build & run (on the DFly guest or any LP64 host with a C compiler)
./build.sh # compiles the pristine bcmp.c + harnesses
./run.sh # containment sweep + instrumented measurement + fix check
Expected decisive output:
RESULT: PASS β no load escaped the buffer's valid pages (12288 + 208 + 500000 cases) buf0: ... past-end loads=2436 (max 7 B ...) buf1: ... below-start loads=11554 (max 7 B) past-end loads=2434 (max 7 B ...) RESULT: OVERREAD-CONFIRMED β longword loads DO read outside [ptr,ptr+len)
Fix validation
fix.diff rewrites the body to provably in-bounds loads (align BOTH
pointers bytewise first; full-longword loop only while β₯8 bytes remain on
both; bytewise tail) β also removing the DF-5701 truncation. Validated on
the same rig: 2,304,192 differential cases 0 failures, instrumented
out-of-region loads drop to 0/0 with max excursion 0 B
(logs/fixed.out), guard sweep still passes (logs/guardfix.out).
Kernel-level rebuild validation is not applicable (the file is not part of
any DFly kernel build); rig-level validation is exact because the kernel
would compile this very source verbatim if a platform ever re-added it.
DF-5700 VERDICT
Status: reproduced (userland rig on the exact kernel source) β impact none in DragonFlyBSD (dead code + page-contained).
Reproduced: what exactly
The over-read is real and was measured, not just argued. On the DFly guest
(DragonFly 6.5-DEVELOPMENT, cc 8.3), the pristine sys/libkern/bcmp.c
compiled verbatim:
- Instrumented measurement (
bcmp_instr.c, every load wrapped, control flow identical, 0 semantic mismatches vs memcmp over the whole sweep β instrumentation validity check): out-of-[ptr,ptr+len)loads occur in 14,788 of ~291k tracked longword loads β up to 7 bytes below the b2 start (thep2 -= shr; va = *(culp)p2down-align at bcmp.c:81-85) and up to 7 bytes past either buffer's end (tail loads bcmp.c:110-111 and bcmp.c:128-129; streaming loopvbload bcmp.c:89). These are the loads the 1994 comment at bcmp.c:53-56 self-admits ("this code will reference the entire longword"). - Guard-page containment (
guard_harness.c): 512,496 calls, each buffer inside a page flanked by PROT_NONE guards, including every page-corner placement β zero faults. Combined with the analytic argument (all longword loads naturally aligned; 8 | 4096 β a load never straddles a page; every load address holds β₯1 in-bounds byte), the over-read is provably confined to the buffer's first/last valid page: it cannot fault, and the extra bytes are masked out of the result (no disclosure).
Why impact is none on DragonFlyBSD
- Build graph:
sys/conf/files:2199-2237(libkern list) does not compilelibkern/bcmp.c;sys/platform/pc64/conf/files:131compilesplatform/pc64/x86_64/bcmp.s;sys/platform/vkernel64/conf/fileshas no bcmp entry (vkernel is a userland process linked against libc). - Running-kernel proof:
logs/kernel_bcmp_disasm.txtβ the booted X86_64_GENERIC kernel'sbcmp(0xffffffff80bcb800) is the mjgMEMCMPmacro:cmp $0x10,%rdxthen first/last loadsmov (%rdi),%r8/mov -0x8(%rdi,%rdx,1),%r8β strictly inside [b1,b1+len) and [b2,b2+len) at every step (sys/cpu/x86_64/include/ asm_mjgmacros.h:49-198). The shipped kernel bcmp adds zero over-read to any caller, including the DF-5676 MATCH() caller in libprop.
Relation to DF-5676 (pass-2 checklist d)
DF-5676 cited bcmp.c:57-110 as the reason the kernel-side MATCH() read is "page-contained". That citation points at dead code β the correct containment argument for the running system is that the shipped bcmp (bcmp.s/memcmp.s via asm_mjgmacros.h) never reads outside the caller's arguments at all, so the only over-read in the DF-5676 chain is the caller's own sentinel-bounded one. Nothing in bcmp.c can worsen DF-5676: not compiled (reachability nil), and even if compiled its over-read is page-contained by the proof above. The one way the DF-5676 class could get worse from this file is the DF-5701 truncation (a β₯2^31 length turns a bounded read into an unbounded forward walk) β equally dead code today.
Exploit chain
None. Dead code; contained OOB read of discarded bytes; no kernel path.
Fix validation (rig-level; kernel rebuild N/A β file not in any kernel)
fix.diff β align-both/longword-loop/bytewise-tail rewrite:
- 2,304,192 differential cases vs memcmp: 0 failures;
- instrumented out-of-region loads: 0 below-start, 0 past-end, max
excursion 0 bytes (was 7/7);
- guard sweep: still 0 faults.
Baseline reproduced=1, patched reproduced=0 (bad behavior gone).
Attempts
3 (ASAN build failed on guest β no libasan; replaced by transparent load instrumentation; one harness self-bug found via the rig itself and fixed).
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 validated at rig level on the guest: instrumented out-of-region loads drop from 14,788 (max 7 B) to 0 (max 0 B); 2,304,192-case differential vs memcmp stays at 0 failures; 512k-case guard sweep still passes. Kernel rebuild validation not applicable: the file is not part of any DFly kernel build, so the rig compiles the exact source the kernel would compile if a platform re-added it.
logs/fixed.out (fixed loads: below-start=0/0 past-end=0/0, RESULT: FIX-PASS), logs/guardfix.out (RESULT: PASS), fix.diff
Confirmed kernel references
- sys/libkern/bcmp.c:53-56
- sys/libkern/bcmp.c:81-91
- sys/libkern/bcmp.c:100-111
- sys/libkern/bcmp.c:114-129
- sys/conf/files:2195-2237
- sys/platform/pc64/conf/files:131
- sys/platform/pc64/x86_64/bcmp.s:31
- sys/cpu/x86_64/include/asm_mjgmacros.h:49-198
- sys/libprop/prop_object.c:739
Detail
Evidence (decisive lines)
logs/instr.out (OVERREAD-CONFIRMED, max 7B below start / 7B past end, semantics 0 mismatches vs memcmp); logs/guard.out (RESULT: PASS - no load escaped the buffer's valid pages, 12288+208+500000 cases); logs/kernel_bcmp_disasm.txt (shipped kernel bcmp = in-bounds asm; bcmp.o from bcmp.s, no libkern bcmp object); logs/fixed.out + logs/guardfix.out (fix validation: 0 out-of-region loads, 0 differential failures, guard sweep pass)
PoC changes
Original PoC plan used ASAN; the guest has no libasan (asan_harness.c retained, unbuildable there). Replaced with a transparent load-instrumented copy of the exact algorithm (bcmp_instr.c: every load site wrapped with address accounting, control flow identical, cross-checked 0 semantic mismatches vs memcmp) plus a PROT_NONE guard-page containment sweep. One harness self-bug (guard_harness memset could itself touch a guard page for large offsets) was checked and excluded by construction (fills stay within the usable page).
Verified recommended fix
Replace the shifted-streaming body with provably in-bounds loads: align BOTH pointers bytewise, full-longword loop only while >= sizeof(long) bytes remain, bytewise tail (fix.diff; also fixes DF-5701 truncation).
Verdict
The by-design longword over-read of sys/libkern/bcmp.c is real and was measured on the pristine source inside the DFly guest: up to 7 bytes below the b2 start (down-align load at bcmp.c:81-85) and up to 7 bytes past either buffer end (streaming vb load at bcmp.c:89, tail loads at bcmp.c:110-111/128-129), exactly as the 1994 comment at bcmp.c:53-56 admits. The over-read is provably page-contained (every load is a naturally-aligned 8-byte load starting at an address holding >=1 in-bounds byte, so it can never straddle a page) β confirmed empirically by 512,496 guard-page-flanked calls with page-corner placements and zero faults β and the extra bytes are masked out of the result, so there is no disclosure and no reachable fault. Impact is 'none' because the file is not compiled into any DragonFlyBSD kernel: sys/conf/files:2195-2237 omits it, sys/platform/pc64/conf/files:131 builds platform/pc64/x86_64/bcmp.s instead, and the running X86_64_GENERIC kernel's bcmp (0xffffffff80bcb800, disasm in pack) is the mjg MEMCMP first/last-load implementation which never reads outside [b1,b1+len)/[b2,b2+len). This also corrects the DF-5676 premise: the shipped bcmp adds zero over-read to the libprop MATCH() chain (its cited bcmp.c:57-110 containment reasoning describes dead code). Latent hardening finding for any future platform that re-adds the file.
No comments yet.