β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5701

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5701 Β· 16 files
FileTypeDescriptionSize
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 to k, 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).

VERDICT.md
↓ download raw

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 = length at bcmp.c:58 truncates 2^32 to 0; the len == 0 early-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, and len -= 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5701

Confirmed kernel references

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.