[PATCH] crypto: lskcipher - preserve state across unaligned chunks
From: Karl Mehltretter
Date: Sat Aug 29 2026 - 15:48:24 EST
crypto_lskcipher_crypt_unaligned() splits a request into page-sized
chunks but never sets CRYPTO_LSKCIPHER_FLAG_CONT, so a stateful algorithm
restarts from its keyed state at every page boundary. It also marks every
chunk CRYPTO_LSKCIPHER_FLAG_FINAL, which is wrong but harmless: chunks are
trimmed to a multiple of the chunk size, so the trailing partial block that
FINAL guards against is caught after the loop instead.
ARC4 is the only lskcipher with internal state, and cbc, the other
in-tree user of the direct API, never passes more than one block, so
this went unnoticed. bpf_crypto_crypt() however hands
__bpf_dynptr_data() to crypto_lskcipher_encrypt() unchecked, leaving
both alignment and length to the BPF program. An 8192-byte ARC4 request
offset by one byte comes back with its second page identical to its
first: the keystream is reused.
Set FINAL only on the last chunk and CONT after the first, mirroring the
progression used by crypto_lskcipher_crypt_sg().
Fixes: 0ae4dcc1ebf6 ("crypto: skcipher - Add internal state support")
Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehltretter@xxxxxxxxx>
---
Reproduced on a Raspberry Pi 400 (Cortex-A72) with a BPF program that
creates a "skcipher"/"arc4" context, takes a dynptr over an 8192-byte map
value, offsets it by one with bpf_dynptr_adjust() and calls
bpf_crypto_encrypt(). Unpatched, the second page of the output equals the
first; patched, it matches an aligned run exactly, final state included.
Rebuilding that needs two programs: bpf_crypto_ctx_create() is KF_SLEEPABLE
and only available to BPF_PROG_TYPE_SYSCALL, bpf_crypto_encrypt() only to
SCHED_CLS/SCHED_ACT/XDP, so the context passes between them as a kptr.
With CONFIG_CRYPTO_ARC4=m, load arc4 first: arc4.ko advertises only the
legacy "ecb(arc4)" alias, so a cold create with algo "arc4" fails with
-EOPNOTSUPP before reaching this path. CONFIG_CRYPTO_ARC4=y also works.
ecb(aes), cbc(aes) and cbc(camellia) were checked the same way before and
after at several lengths and misalignments and are unchanged; with generic
ciphers they have alignmask 0 and never enter the helper.
The helper is not dead code, in case removing it looks tempting: ecb() and
cbc() inherit the wrapped cipher's alignmask (crypto/ecb.c), and geode-aes,
padlock-aes and sparc64 camellia still declare one, so those instances use
it today. They are all stateless, hence unaffected by the flag handling.
Not an unprivileged surface: the crypto kfuncs need CAP_BPF, and
bpf_crypto_encrypt() additionally CAP_NET_ADMIN.
crypto/lskcipher.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/crypto/lskcipher.c b/crypto/lskcipher.c
index a8b07594005db..6c30436a11354 100644
--- a/crypto/lskcipher.c
+++ b/crypto/lskcipher.c
@@ -75,6 +75,7 @@ static int crypto_lskcipher_crypt_unaligned(
unsigned ivsize = crypto_lskcipher_ivsize(tfm);
unsigned bs = crypto_lskcipher_blocksize(tfm);
unsigned cs = crypto_lskcipher_chunksize(tfm);
+ u32 flags = 0;
int err;
u8 *tiv;
u8 *p;
@@ -98,13 +99,16 @@ static int crypto_lskcipher_crypt_unaligned(
if (chunk > cs)
chunk &= ~(cs - 1);
+ if (chunk == len)
+ flags |= CRYPTO_LSKCIPHER_FLAG_FINAL;
memcpy(p, src, chunk);
- err = crypt(tfm, p, p, chunk, tiv, CRYPTO_LSKCIPHER_FLAG_FINAL);
+ err = crypt(tfm, p, p, chunk, tiv, flags);
if (err)
goto out;
memcpy(dst, p, chunk);
+ flags |= CRYPTO_LSKCIPHER_FLAG_CONT;
src += chunk;
dst += chunk;
len -= chunk;
base-commit: cf72cbb39da84b6f02f90c07f33b102fc10b16f0
--
2.53.0