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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5700 Β· 21 files
FileTypeDescriptionSize
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)p2 at bcmp.c:110-111 and bcmp.c:128-129 read up to 8 - (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)p2 at bcmp.c:89 reads up to 8 - shr bytes past the point where 8 compared bytes end;
  • the down-alignment p2 -= shr; va = *(culp)p2 at 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

  1. The file is not compiled into any DragonFlyBSD kernel. - sys/conf/files:2199-2237 (the libkern list) does not include libkern/bcmp.c; - sys/platform/pc64/conf/files:131 builds platform/pc64/x86_64/bcmp.s instead (the mjg-macro MEMCMP β€” verified below); - sys/platform/vkernel64/conf/files has no bcmp/memcmp entry (the vkernel is a userland binary that links host libc).
  2. In-guest proof (logs/kernel_bcmp_disasm.txt): the running X86_64_GENERIC kernel's bcmp at 0xffffffff80bcb800 disassembles 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.
  3. 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.

VERDICT.md
↓ download raw

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:

  1. 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 (the p2 -= shr; va = *(culp)p2 down-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 loop vb load bcmp.c:89). These are the loads the 1994 comment at bcmp.c:53-56 self-admits ("this code will reference the entire longword").
  2. 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 compile libkern/bcmp.c; sys/platform/pc64/conf/files:131 compiles platform/pc64/x86_64/bcmp.s; sys/platform/vkernel64/conf/files has 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's bcmp (0xffffffff80bcb800) is the mjg MEMCMP macro: cmp $0x10,%rdx then first/last loads mov (%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

fixed
baseline reproduced→ patch + rebuild →patched clean

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

Confirmed kernel references

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.