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

BF_set_key dereferences data[0] when len <= 0 (no lower-bound clamp; compiled-in dead code)

Summary

The Blowfish key schedule clamps len from above (len > 72 -> 72) but never from below. With len == 0 (or negative), end = &data[len] sits at/before data, the wrap guard 'if (d >= end) d = data;' never protects the first dereference, and all 72 key-word byte loads read data[0] - a 1-byte out-of-bounds read of a zero-length key buffer and a silently constant-byte key schedule for negative len. Identical defect class to the known rc4_init keylen==0 findings DF-0971/DF-2366, different file - filed so it is not inherited by a future caller. DEMONSTRATED in-guest: BF_set_key(&key, 0, <PROT_NONE page>) -> SIGSEGV with si_addr == data, exit 42. None today: BF_set_key has zero in-tree callers yet sys/conf/files compiles the object into every 'option crypto' kernel - compiled-in dead code carrying a latent 1-byte OOB-read primitive. Correctness proven alongside: 9/9 Schneier KATs, kernel-vs-LibreSSL schedule differential identical for len 1..120.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3567 Β· 18 files
FileTypeDescriptionSize
README.md β€” 2.3 KB ↓ raw
VERDICT.md β€” 3.3 KB ↓ raw
verdict.json β€” 3.5 KB view raw
manifest.json β€” 905 B view raw
guard_bf_len0.c β€” 1.4 KB view raw
bfkat.c β€” 4.7 KB view raw
bfdiff.c β€” 1.6 KB view raw
bf_skey_fixed.c β€” 4.4 KB view raw
shim/sys/systm.h β€” 135 B view raw
build.sh β€” 155 B view raw
run.sh β€” 90 B view raw
build.log β€” 387 B view raw
run.log β€” 2.0 KB view raw
bfdiff.log β€” 88 B view raw
guard.log β€” 435 B view raw
fixval.log β€” 1.3 KB view raw
env.txt β€” 203 B view raw
fix.diff β€” 845 B view raw

DF-3567 β€” BF_set_key dereferences data[0] when len <= 0 (dead code, hardening)

File: sys/crypto/blowfish/bf_skey.c:83-99 Class: CWE-125 out-of-bounds read (1 byte, 72 dereferences) β€” same defect class as the known rc4_init keylen==0 findings DF-0971/DF-2366, but in a different file. Reachability today: NONE β€” BF_set_key has zero in-tree callers (verified by full-tree grep; sys/opencrypto no longer exists, option crypto still compiles the object into every kernel: sys/conf/files:2257). Info/hardening severity precisely because it is compiled-in dead code.

Reproduce (in-guest, same-source userland-object pattern)

Guest sources verified sha256-identical to the audited tree first (bf_skey.c = a77ce9b5...). Harnesses compiled on the guest against /usr/src/sys with a one-line sys/systm.h shim (shim/sys/systm.h).

build:  cc -O2 -DNDEBUG -I shim -I /usr/src/sys -o guard_bf_len0 \
            bf/guard_bf_len0.c /usr/src/sys/crypto/blowfish/bf_skey.c \
            /usr/src/sys/crypto/blowfish/bf_enc.c
run:    ./guard_bf_len0        # zero-length key buffer modeled at a PROT_NONE page

Expected output (baseline)

calling BF_set_key(&key, len=0, data=guard-page 0x800475000)
FAULT-CONFIRMED: sig=11 si_addr=0x800475000 (expected data[0] at guard page)
exit=42

si_addr == data β‡’ the first of the 72 key-schedule byte loads (ri = *(d++), bf_skey.c:86) dereferences data[0] even though a zero-length key has no readable bytes. With a kmalloc'd zero-length block in-kernel the read stays inside the slab (silent), but it is still an out-of-bounds read of the caller's object.

Expected output (with fix.diff applied to a copy of the source)

calling BF_set_key(&key, len=0, data=guard-page 0x800475000)
BUG-NOT-PRESENT: survived len=0
exit=0

Correctness cross-checks (prove the fix and the tables)

  • bfkat: 9/9 published Schneier vectors PASS (bf_pi.h digit-table integrity).
  • bfdiff: kernel schedule+ciphertext byte-identical to LibreSSL's own bf_skey.c for every key length 1..120 with pseudo-random keys (wrap loop + >72 clamp).
  • Both still PASS after the fix (no behavior change for len >= 1).

See run.log (KAT + openssl cross-match), bfdiff.log, guard.log (baseline fault), fixval.log (patched: no fault, KATs still green).

VERDICT.md
↓ download raw

DF-3567 VERDICT

Status: reproduced (same-source userland-object harness, in-guest) β€” impact: none (no in-kernel call path). Fix: validated at harness level (fixed).

What was audited

Pass-2 full audit of sys/crypto/blowfish/bf_skey.c (118 LOC): the Eric Young / KAME Blowfish key schedule. Prior batch verified bf_enc.c clean (213/213 cross-match); this file had no prior findings.

Root cause

BF_set_key(key, len, data) clamps len from above (len > 72 β†’ 72) at bf_skey.c:80-81 but never from below. With len <= 0:

  • end = &data[len] (bf_skey.c:84) lands at or before data;
  • the wrap guard if (d >= end) d = data; after every byte load (bf_skey.c:87 et seq.) resets d unconditionally, so every one of the 72 key-word byte loads reads data[0] β€” an out-of-bounds read of a zero-length key buffer, and a silently wrong (constant-byte-key) schedule for any negative len.

Same defect class as the known rc4_init keylen==0 findings (DF-0971/DF-2366, sys/crypto/rc4/rc4.c:71-75) β€” NOT re-reported here; this is the Blowfish instance.

Reachability (why Info)

Full-tree grep: BF_set_key has zero in-tree callers (declaration sys/crypto/blowfish/blowfish.h:83 only). The old opencrypto/cryptosoft consumer was removed from DragonFly (sys/sys/param.h:264 notes the crypto(9) removal); sys/conf/files:2257 still compiles bf_skey.c into every option crypto kernel (X86_64_GENERIC has it), so it is compiled-in dead code. No attacker-controlled path exists today; the finding is filed so the primitive is not inherited by a future caller.

Verification (guest: DragonFly 6.5-DEVELOPMENT #0, gcc 8.3 [DragonFly])

  1. Guest /usr/src/sys/crypto/blowfish/bf_skey.c sha256 == audited tree (a77ce9b53805c4de3298c15a20be0c33205fda6a53f752570da33fc459ff1f01).
  2. Baseline primitive (guard_bf_len0, guard.log): BF_set_key(&k, 0, guard_page) β†’ SIGSEGV with si_addr == data (exit 42) β€” proves the data[0] dereference.
  3. Table integrity (bfkat, run.log): 9/9 published Schneier vectors PASS (4EF997456198DD78 … B1B8CC0B250F09A0).
  4. Wrap loop + clamp, full range (bfdiff, bfdiff.log): schedule and ciphertext byte-identical to LibreSSL's own bf_skey.c/bf_enc.c (guest /usr/src/crypto/libressl) for every len 1..120, pseudo-random keys β€” the >72 clamp (len 100 == len 72) included.
  5. Fix validation (fixval.log): with fix.diff applied to a source copy, guard_bf_len0 prints BUG-NOT-PRESENT (exit 0); bfkat 9/9 and bfdiff len 1..120 still PASS β€” no behavioral change for len >= 1.

Exploit chain

None β€” no caller, dead code, 1-byte read ceiling. Hardening only.

kernel_refs

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 applied to a source copy (never sys/): guard harness prints BUG-NOT-PRESENT (exit 0) instead of the data[0] fault; bfkat 9/9 and bfdiff len-1..120 vs LibreSSL still byte-identical - no behavioral change for valid lengths. Harness-level validation only (no kernel build): the function has no in-kernel callers, so no kernel-path regression is possible.

fixval.log: 'BUG-NOT-PRESENT: survived len=0', 'RESULT-PASS: 0 failures' x2 (bfkat_fixed, bfdiff_fixed)
↓ fix.diffper-fix-DF-3567

Confirmed kernel references

Detail

Evidence (decisive lines)

guard.log: 'FAULT-CONFIRMED: sig=11 si_addr=0x800475000' (baseline len=0); fixval.log: 'BUG-NOT-PRESENT: survived len=0' + RESULT-PASS 0 failures for bfkat_fixed/bfdiff_fixed; run.log: 9/9 Schneier vectors; bfdiff.log: kernel-vs-LibreSSL len 1..120 identical

PoC changes

No seed PoC existed; authored guard-page harness (guard_bf_len0.c) plus bfkat/bfdiff correctness harnesses. One compile fix on the way: DragonFly siginfo_t field is si_addr (not si_addr member via si->addr) and _exit needs unistd.h.

Verified recommended fix

bf_skey.c: skip the P[] key-mixing loop reads when len < 1 (see fix.diff), so no key byte is dereferenced for a degenerate length.

Verdict

BF_set_key clamps len from above (bf_skey.c:80-81) but never from below, so len<=0 makes all 72 key-schedule byte loads dereference data[0] (bf_skey.c:83-99) - a 1-byte out-of-bounds read of a zero-length key buffer, same class as the known rc4 DF-0971/DF-2366. Reproduced in-guest with the same-source userland-object pattern: guard-page run faults with si_addr==data (exit 42). Impact is none because BF_set_key has zero in-tree callers (compiled-in dead code under 'option crypto', sys/conf/files:2257); filed as hardening so a future caller cannot inherit the primitive. Fix validated at harness level: with fix.diff (skip P[] key mixing when len<1) the fault is gone and both the 9/9 Schneier KATs and the len-1..120 LibreSSL byte-identity differential still pass.