twofish_set_key performs no key-length validation, allowing OOB stack writes and ctx corruption if reached with unsupported keylen
Summary
twofish_set_key at twofish.c:433 derives ctx->k_len = key_len_bits/64 with NO validation. For key_len_bits>=320 (k_len>=5): loop at :435 writes me_key[i]/mo_key[i] (size-4 stack arrays at :427) past bounds -> stack OOB; s_key[k_len-i-1] writes past s_key[4] into mk_tab; ((u32*)in_key)[i+i(+1)] reads past caller key buffer. gen_mk_tab switch at :335 falls through for k_len not in {2,3,4} leaving mk_tab uninitialized -> encrypt/decrypt read garbage. Both cryptoapi callers (cryptoapi.c:664,763) pre-validate 128/192/256 (and 256/512 for xts) so unreachable today. Exported as public API in twofish.h:12 with no precondition documented -> future regression risk. Fix: validate key_len_bits at function entry.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0965 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | function-level harness that compiles real twofish.c and calls twofish_set_key with oversized key_len_bits | 4.8 KB | view raw |
| build.sh | build-script | compiles trigger against a copy of /usr/src/sys/crypto/twofish/twofish.c | 467 B | view raw |
| run.sh | run-script | runs the harness as the maxx user | 98 B | view raw |
| build_fixed.sh | fix-validation | applies fix.diff to harness source and re-runs | 802 B | view raw |
| fix.diff | suggested-fix | git-apply-able fix: validate key_len_bits in twofish_set_key | 907 B | view raw |
| build.log | build-log | final successful build, full output | 66 B | view raw |
| run.log | run-log | decisive run, full output (function-level) | 601 B | view raw |
| fix_run.log | fix-log | post-fix run showing oversized keylen now refused | 262 B | view raw |
| env.txt | environment | uname, kern.version, cc version | 348 B | view raw |
| VERDICT.md | verdict | detailed analysis: mechanism, reachability, fix validation | 6.6 KB | β raw |
| README.md | readme | how to reproduce | 2.0 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0965 β twofish_set_key missing key-length validation
Summary
twofish_set_key() at sys/crypto/twofish/twofish.c:425 does not validate
its key_len_bits argument. For key_len_bits >= 320 (k_len >= 5) the
function performs out-of-bounds writes on size-4 stack arrays
(me_key[4]/mo_key[4]), out-of-bounds writes on the struct's s_key[4]
(continuing into mk_tab[4*256]), and out-of-bounds reads of the caller's
key buffer. The gen_mk_tab switch also silently falls through for
unsupported sizes.
Reachability: All in-kernel callers (twofish_cbc_setkey,
twofish_xts_setkey in sys/crypto/cryptoapi/cryptoapi.c) validate the
key length against {128,192,256}/{256,512} before calling, and
cryptoapi_cipher_find() requires probe() to accept the size. So the
bug is NOT reachable through the kernel syscall surface. This is a
defense-in-depth hardening gap.
Reproduce (function-level harness)
./build.sh && ./run.sh
Expected output (function-level proof, runs as maxx):
[control] key_len_bits=256 (k_len=4): status=0x0 exited 0, k_len=4 (function OK) [trigger] key_len_bits=640 (k_len=10): status=0x0 exited 0, k_len=10 (function accepted oversized key) BUG CONFIRMED: ctx->k_len=10 > supported max(4). ...
Fix-validation (function-level)
./build_fixed.sh # applies fix.diff to a copy of twofish.c, rebuilds, runs
Expected output (oversized keylen now refused):
[control] key_len_bits=256 (k_len=4): status=0x0 exited 0, k_len=4 (function OK) [trigger] key_len_bits=640 (k_len=10): status=0x0 exited 0, k_len=0 (no bug observed)
Files
trigger.cβ function-level harness (compiles real twofish.c)build.sh/run.shβ build + run the harnessbuild_fixed.shβ appliesfix.diffto harness source and re-testsfix.diffβ git-apply-able unified diff againstsys/crypto/twofish/twofish.cbuild.log/run.log/fix_run.logβ full untrimmed outputsenv.txtβ guest environmentVERDICT.mdβ detailed analysismanifest.jsonβ artifact catalog
DF-0965 β twofish_set_key missing key-length validation
Verdict
REPRODUCED AT FUNCTION LEVEL β NOT REACHABLE AT RUNTIME via the kernel syscall surface. Defense-in-depth hardening gap (latent). The primitive exists; in-kernel callers protect against it; the fix adds validation in the function itself.
Mechanism
twofish_set_key(ctx, in_key, key_len_bits) at
sys/crypto/twofish/twofish.c:425 derives ctx->k_len = key_len_bits / 64
(line 433) with no validation of the supported values {128,192,256} bits
(k_len β {2,3,4}). When called with key_len_bits β₯ 320 (k_len β₯ 5), the
loop at lines 435β440:
for(i = 0; i < ctx->k_len; ++i) {
a = LE32(((const u_int32_t *)in_key)[i + i]); me_key[i] = a;
b = LE32(((const u_int32_t *)in_key)[i + i + 1]); mo_key[i] = b;
s_key[ctx->k_len - i - 1] = mds_rem(a, b);
}
performs three OOB effects:
- Stack OOB write β
me_key[4]/mo_key[4]are size-4 stack arrays (declared at line 427). For i β₯ 4 the writes clobber adjacent stack locals including thel_key/s_keypointers (line 428). The follow-on loop at lines 442β449 then dereferences these corrupted pointers β SIGBUS/SIGSEGV. - Struct OOB write β
s_key[k_len-i-1]for i small writes pasts_key[4]into themk_tab[4*256]array (immediately afters_keyinstruct twofish_ctx). Attacker-controlledmds_rem(a, b)outputs land in mk_tab. - Out-of-bounds read β
((const u_int32_t *)in_key)[i+i(+1)]for i up to k_len-1 reads past the caller's key buffer (the caller must pass at least8*k_lenbytes; passing fewer yields a stack/heap over-read). - Silent mk_tab fallback β
gen_mk_tab'sswitchat line 335 only handlesk_len β {2,3,4}; for other values it falls through leavingmk_tab[]as the OOB-written garbage. Subsequenttwofish_encrypt/twofish_decryptcalls indexmk_tab[4*extract_byte(x,n)]using this corrupted table.
Reachability analysis (the crucial question)
Every in-kernel caller validates the key length BEFORE calling
twofish_set_key:
| Caller | Location | Validation |
|---|---|---|
twofish_cbc_setkey |
sys/crypto/cryptoapi/cryptoapi.c:664 |
switch (keylen_in_bytes * 8) { case 128: case 192: case 256: β¦ default: return EINVAL; } |
twofish_xts_setkey |
sys/crypto/cryptoapi/cryptoapi.c:763 |
switch (keylen_in_bytes * 8) { case 256: case 512: β¦ default: return EINVAL; } then passes (keylen_in_bytes/2)*8 (always 128 or 256) |
cryptoapi_cipher_find |
sys/crypto/cryptoapi/cryptoapi.c:1003 |
requires cipher->probe(keysize_in_bits) == 0 before the cipher is even returned; twofish_cbc_probe/twofish_xts_probe only accept the supported sizes |
These wrappers are the only callers of twofish_set_key in the entire
kernel tree (verified: grep -rn 'twofish_set_key' sys/). The wrappers are
exposed via cryptoapi_cipher_setkey to dm_target_crypt
(sys/dev/disk/dm/crypt/dm_target_crypt.c:306,555). The dm-crypt target
gets its key from a root-issued dmsetup table line β i.e. it requires
root (or vfs.usermount=1 + a privileged helper) to set the cipher at all,
and even then the key length is parsed as a hex string of fixed length.
Therefore: an unprivileged user CANNOT reach twofish_set_key with an
out-of-range key_len_bits through any syscall on the default GENERIC
kernel. This is a valid hard blocker (dead/unreachable at runtime AND no
syscall harness can exercise it without modifying the validated wrappers).
Function-level proof
The primitive is real and is proven by trigger.c, a userspace harness
that compiles the actual sys/crypto/twofish/twofish.c source and invokes
twofish_set_key with key_len_bits=640 (k_len=10). Observed output:
[control] key_len_bits=256 (k_len=4): status=0x0 exited 0, k_len=4 (function OK)
[trigger] key_len_bits=640 (k_len=10): status=0x0 exited 0, k_len=10 (function accepted oversized key)
BUG CONFIRMED: ctx->k_len=10 > supported max(4). The
function silently took the bad size, ran the OOB loop at
twofish.c:435 (i=0..9), wrote past me_key[4]/mo_key[4]
(stack) and s_key[4] (struct, into mk_tab), then gen_mk_tab
at twofish.c:335 fell through (k_len not in {2,3,4}) leaving
mk_tab as the OOB-written garbage.
The function accepted the oversized keylen without complaint; k_len became
10; the OOB writes happened silently. (Some k_len values also crash via
stack-pointer corruption β see trigger.c history. The current
heap-allocated harness catches the silent case which is the more
exploitable variant if it were ever reachable.)
Exploit chain
Not applicable / blocked by valid hard blocker. The bug is a memory- corruption primitive, but the kernel-side reachability is gated by validated wrappers, so no unprivileged syscall can reach it. There is no privilege boundary to cross; this is a latent / defense-in-depth gap, not an exploitable bug on the default kernel.
If a future commit ever:
- adds a caller of twofish_set_key that does not validate, or
- weakens twofish_cbc_setkey / twofish_xts_setkey to forward arbitrary
keylens (or adds a "raw" setkey path),
the function would become a 4-element stack overflow + struct OOB write
primitive directly triggerable from dmsetup (which can be reached from
userspace via vfs.usermount=1 with a root-created dm-crypt volume).
Fix
The fix validates key_len_bits inside twofish_set_key itself, so any
future caller is also protected. See fix.diff:
- ctx->k_len = key_len_bits / 64; /* 2, 3 or 4 */
+ switch (key_len_bits) {
+ case 128:
+ case 192:
+ case 256:
+ ctx->k_len = key_len_bits / 64; /* 2, 3 or 4 */
+ break;
+ default:
+ ctx->k_len = 0;
+ return;
+ }
Fix validation
Validated at the function level by applying the same patch to the
harness's twofish_src.c and re-running (build_fixed.sh β fix_run.log):
[control] key_len_bits=256 (k_len=4): status=0x0 exited 0, k_len=4 (function OK) [trigger] key_len_bits=640 (k_len=10): status=0x0 exited 0, k_len=0 (no bug observed)
The control (valid 256-bit key) is unchanged, and the trigger (oversized
640-bit) now sets k_len=0 and returns early instead of running the OOB
loop. fix_status: not_testable for kernel runtime (the bug isn't
reachable, so a kernel build is behaviorally a no-op); the function-level
test proves the fix closes the primitive.
PoC changes
The PoC directory was seeded empty; I authored everything here: trigger.c
(function-level harness with control + trigger cases via fork/wait),
build.sh/run.sh repro scripts, build_fixed.sh/df965_patch.py for
fix-validation, and fix.diff.
Fix verification
not_testablecompile validated
kernel build rc=0 -Werror
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source+harness. twofish_set_key no key length validation -> OOB. In-kernel callers validate upstream. Function-level proof + fix.
No comments yet.