DF-5701 / verdict.json
{ "finding_id": "DF-5701", "status": "reproduced", "reproduced": true, "impact": "none", "confidence": "certain", "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.", "exploit_chain": "", "evidence": "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)", "kernel_refs": [ "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" ], "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.", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 180, "guest_dirty": 0, "build_cmd": "cd findings/poc/DF-5701 && ./build.sh", "run_cmd": "cd findings/poc/DF-5701 && ./run.sh", "code_hash": "b936ffa477d986cb1603f2fefc55519dd8ae7b0cc626c3277f15c91078302695", "notes": "This is the only way the DF-5676 class could get worse from bcmp.c: a >=2^31 length converts a page-contained over-read into an unbounded forward scan - equally unreachable today (dead file). The BE fallbacks `return(length)` at bcmp.c:142 truncates size_t to int too; subsumed here since the whole length pipeline is replaced by fix.diff. Guest left running, untouched beyond /tmp scratch (cleaned).", "recommended_fix": "Make the counter size_t with explicit `len >= sizeof(long)` loop conditions and a bytewise tail (fix.diff, shared with DF-5700).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "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.", "fix_evidence": "logs/truncfix.out (RESULT: FIX-PASS (no false equal)), logs/fixed.out in the DF-5700 pack (differential 0 failures post-fix), fix.diff" } |