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)
PoC verification
Evidence pack
findings/poc/DF-3567 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 ownbf_skey.cfor 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).
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 beforedata;- the wrap guard
if (d >= end) d = data;after every byte load (bf_skey.c:87 et seq.) resetsdunconditionally, so every one of the 72 key-word byte loads readsdata[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])
- Guest
/usr/src/sys/crypto/blowfish/bf_skey.csha256 == audited tree (a77ce9b53805c4de3298c15a20be0c33205fda6a53f752570da33fc459ff1f01). - Baseline primitive (
guard_bf_len0, guard.log):BF_set_key(&k, 0, guard_page)β SIGSEGV withsi_addr == data(exit 42) β proves thedata[0]dereference. - Table integrity (
bfkat, run.log): 9/9 published Schneier vectors PASS (4EF997456198DD78 β¦ B1B8CC0B250F09A0). - Wrap loop + clamp, full range (
bfdiff, bfdiff.log): schedule and ciphertext byte-identical to LibreSSL's ownbf_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. - Fix validation (fixval.log): with
fix.diffapplied to a source copy,guard_bf_len0prints 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
- sys/crypto/blowfish/bf_skey.c:80-99 (defect: no len floor; data[0] deref)
- sys/crypto/blowfish/blowfish.h:83 (only declaration; no callers)
- sys/conf/files:2257 (compiled under
option crypto) - sys/sys/param.h:264 (opencrypto removal β why the caller disappeared)
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 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)
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.
No comments yet.