bcmp.c:58 `int len = length` size_t-to-int truncation: bcmp(a,b,2^32)==0 false-equal with zero bytes compared; prefix-only compare at 2^32+k; signed-overflow unbounded forward walk at 2^31
Summary
The working counter is int while length is size_t: 2^32 truncates to 0 and returns 'equal' having compared NOTHING (false-equal = authentication-bypass class); 2^32+k compares only k bytes; [2^31,2^32) makes len negative and the signed-overflow decrement wraps it positive, walking forward past both buffers until it faults (panic-class DoS). All three reproduced on the pristine source at -O2 and -O0 with guard pages. Dead code on every DFly platform (the shipped asm bcmp takes the full 64-bit length) - latent defect inherited from 32-bit-size_t FreeBSD code. size_t-counter fix (shared with DF-5700) validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5701 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 3.1 KB | β raw | |
| bcmp_kernel.c | β | 4.3 KB | view raw | |
| trunc_harness.c | β | 3.4 KB | view raw | |
| trunc_fixed.c | β | 1.9 KB | view raw | |
| bcmp_fixed.c | β | 716 B | view raw | |
| build.sh | β | 65 B | view raw | |
| run.sh | β | 89 B | view raw | |
| logs/trunc2.out | β | 472 B | view raw | |
| logs/trunc0.out | β | 472 B | view raw | |
| logs/truncfix.out | β | 210 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.2 KB | view raw | |
| verdict.json | β | 4.0 KB | view raw |
DF-5701 β bcmp.c:58 int len = length size_tβint truncation: false "equal" / unbounded walk
What the finding is
sys/libkern/bcmp.c:58 stores the size_t length parameter in an int:
int shl, shr, len = length; /* bcmp.c:58 */
...
if (len == 0) return (0); /* bcmp.c:62-63 */
For lengths β₯ 2^31 (LP64 size_t) this breaks three ways, all reproduced on
the DFly guest at -O2 and -O0 (logs/trunc2.out, logs/trunc0.out):
- (A) false equal at exact multiples of 2^32:
bcmp(a, b, 2^32)returns 0 ("equal") without comparing a single byte β fatal for any authentication/equality use; - (B) prefix-only comparison:
bcmp(a, b, 2^32 + k)truncates tok, compares k bytes and reports "equal" for buffers differing beyond k; - (C) signed overflow β unbounded forward walk:
len -= sizeof(long)at bcmp.c:88/:114 overflows signed int for lengths in [2^31, 2^32); the wrap makes the loop counter positive and bcmp walks FORWARD past the buffers comparing garbage until it faults (SIGSEGV in the guarded rig; in a kernel this would be a wild scan ending in a page-fault panic).
Why it is only Low / impact none here
The file is not compiled into any DragonFlyBSD kernel β see the DF-5700
pack (sys/conf/files:2199-2237 excludes it; sys/platform/pc64/conf/files:131
uses platform/pc64/x86_64/bcmp.s; logs/kernel_bcmp_disasm.txt shows the
running kernel's bcmp is the in-bounds assembly version, which takes the
full 64-bit length in %rdx and has no truncation). The bug is latent:
it would become live the moment any platform configuration re-added
libkern/bcmp.c (it is inherited FreeBSD code designed for 32-bit
size_t).
Rig
trunc_harness.c places both buffers in pages flanked by PROT_NONE
guards and forks for the crash cases:
./build.sh
./run.sh # runs trunc_harness at -O2 and -O0
Expected decisive output:
== (A) length = 2^32 ... df_bcmp(b1,b2,2^32) = 0 -> FALSE EQUAL (bug) == (B) length = 2^32+8 ... df_bcmp(b1,b2,2^32+8) = 0 -> FALSE EQUAL (bug) == (C) length = 2^31 ... child outcome: SIGSEGV in df_bcmp (unbounded walk proven) RESULT: false-equal@2^32 false-equal@2^32+8 crash@2^31 (A=1 B=1 C=1)
Fix validation
fix.diff (shared with DF-5700) uses size_t len with explicit
len >= sizeof(long) loop conditions and a bytewise tail. Post-fix,
trunc_fixed.c shows huge lengths no longer yield a false equal: bcmp
genuinely attempts the full comparison and faults on the guard page (as
memcmp would) β logs/truncfix.out:
fixed bcmp(2^32) on guarded pages: outcome=SIGSEGV while genuinely comparing (correct) fixed bcmp(2^32+8) on guarded pages: outcome=SIGSEGV while genuinely comparing (correct) RESULT: FIX-PASS (no false equal)
Kernel-level rebuild validation is not applicable (dead file; see DF-5700).
DF-5701 VERDICT
Status: reproduced (userland rig on the exact kernel source, -O2 and -O0) β impact none in DragonFlyBSD (dead code).
Reproduced: what exactly
On the DFly guest (DragonFly 6.5-DEVELOPMENT, cc 8.3), pristine
sys/libkern/bcmp.c compiled verbatim, buffers in guarded pages:
- (A)
bcmp(a, b, 0x100000000)returns 0 β "equal" β comparing zero bytes (int len = lengthat bcmp.c:58 truncates 2^32 to 0; thelen == 0early-out at bcmp.c:62-63 fires). Any equality/security use of bcmp with a length that is an exact multiple of 2^32 becomes a trivial bypass. - (B)
bcmp(a, b, 0x100000008)returns 0 while the buffers differ at byte 8: the truncation keeps only the low 8, so bcmp compares 8 bytes of a 4 GiB+8 request. - (C)
bcmp(a, b, 0x80000000)SIGSEGVs:len= INT_MIN, andlen -= sizeof(long)at bcmp.c:88 (shifted path) / bcmp.c:114 (aligned path) overflows signed int (UB); with the wrap the loop counter turns positive and the function walks FORWARD comparing longwords far past the buffers until the guard page faults. In a kernel this is an unbounded read scan ending in a page-fault panic.
All three at -O2 and -O0 (logs/trunc2.out, logs/trunc0.out); (A) and
(B) are plain defined-behavior truncation β optimization-independent. (C)
is UB whose exact manifestation varies by compiler (it is still a false
result at worst, a wild scan at best-case-for-the-attacker).
Why impact is none on DragonFlyBSD
Same structural proof as DF-5700: the file is in no kernel build
(sys/conf/files:2199-2237 excludes it; sys/platform/pc64/conf/files:131
uses platform/pc64/x86_64/bcmp.s; vkernel links libc). The shipped
assembly bcmp takes the full 64-bit length in %rdx
(logs/kernel_bcmp_disasm.txt) and has no truncation. The bug is latent
FreeBSD-heritage code designed when size_t was 32-bit (where int len
could hold every valid length).
Exploit chain
None on DFly (dead code). If ever revived: (A)/(B) are an auth/equality bypass primitive; (C) is a kernel-panic DoS from any attacker-influenced β₯2^31 length.
Fix validation (rig-level; kernel rebuild N/A β file not in any kernel)
fix.diff (shared with DF-5700) makes len a size_t with explicit
len >= sizeof(long) loop conditions and a bytewise tail. Post-fix
(trunc_fixed.c, logs/truncfix.out): lengths 2^32 and 2^32+8 no longer
return a false equal β bcmp genuinely attempts the full comparison (SIGSEGV
on the guard page, exactly as memcmp would on a 3-page arena), and the
2.3M-case differential stays at 0 failures.
Baseline reproduced=1, patched reproduced=0 (false-equal gone).
Attempts
2 (initial harness had a self-bug β its own memset crossed a guard page; found via the rig, fixed; then clean reproduction at both opt levels).
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: post-fix, lengths 2^32 and 2^32+8 no longer return a false equal - bcmp genuinely attempts the full comparison (SIGSEGV on the guard page of the 3-page arena, exactly as memcmp would), and the 2.3M-case differential stays at 0 failures. Kernel rebuild validation not applicable: the file is in no DFly kernel build.
logs/truncfix.out (RESULT: FIX-PASS (no false equal)), logs/fixed.out in the DF-5700 pack (differential 0 failures post-fix), fix.diff
Confirmed kernel references
- sys/libkern/bcmp.c:58
- sys/libkern/bcmp.c:62-63
- sys/libkern/bcmp.c:88
- sys/libkern/bcmp.c:110-111
- sys/libkern/bcmp.c:114
- sys/libkern/bcmp.c:128-129
- sys/libkern/bcmp.c:142
- sys/conf/files:2195-2237
- sys/platform/pc64/conf/files:131
Detail
Evidence (decisive lines)
logs/trunc2.out (-O2: FALSE EQUAL at 2^32, FALSE EQUAL at 2^32+8, SIGSEGV unbounded walk at 2^31; RESULT A=1 B=1 C=1); logs/trunc0.out (-O0: identical); logs/truncfix.out (post-fix: FIX-PASS, no false equal - huge lengths genuinely compare and fault on the guard as memcmp would); logs/kernel_bcmp_disasm.txt (shipped kernel bcmp = asm version, no truncation)
PoC changes
Seed sketch had no compiler on the host; moved to the DFly guest (cc 8.3). First harness version crashed in its own memset (it filled a 4096-byte page starting 64 bytes in) - a rig bug found by the rig itself, fixed by giving each buffer its own guarded arena. Case (C) initially 'survived' because case (B) had flipped a byte the walk then mismatched on; restored the byte so (C) demonstrates the true unbounded walk. Added the -O0 build to pin the truncation behavior across optimization levels.
Verified recommended fix
Make the counter size_t with explicit len >= sizeof(long) loop conditions and a bytewise tail (fix.diff, shared with DF-5700).
Verdict
bcmp.c:58 stores the size_t length in an int. Reproduced on the pristine source in the DFly guest at both -O2 and -O0: (A) bcmp(a,b,2^32) returns 0 ('equal') having compared zero bytes - the truncation yields len==0 and the early-out at bcmp.c:62-63 fires; (B) bcmp(a,b,2^32+8) returns 0 ('equal') comparing only the first 8 bytes while the buffers differ at byte 8; (C) bcmp(a,b,2^31) takes len=INT_MIN into len -= sizeof(long) at bcmp.c:88/114, the signed overflow wraps the counter positive, and the function walks forward past both buffers comparing garbage until it faults (SIGSEGV on a guard page in the rig; a kernel would take an unmapped-page panic). In any equality/security use, (A)/(B) are a false-equal bypass primitive and (C) an unbounded read scan. Impact is 'none' on DragonFlyBSD because the file is dead code: sys/conf/files:2195-2237 omits it, pc64/conf/files:131 builds x86_64/bcmp.s (full 64-bit %rdx length, disasm in pack), vkernel links libc. The bug is FreeBSD-heritage code designed for 32-bit size_t and goes latent-lethal the moment any platform config re-adds the file.
No comments yet.