From 5338a9b2727273150633e27ad51237ad28c0214f Mon Sep 17 00:00:00 2001 From: 007bsd <22483432+007bsd@users.noreply.github.com> Date: Sun, 16 Aug 2026 10:08:49 +0300 Subject: [PATCH] Fix broken AVX2 cmov that skips FO implicit rejection The AVX2 cmov built its blend mask with _mm256_set1_epi64x(b as i64), so for b == 1 every byte's high bit is 0 and _mm256_blendv_epi8 copies nothing. cmov is the Fujisaki-Okamoto implicit-rejection step in decapsulation (len == 32, wholly through the vector loop), so on an invalid ciphertext the rejection value was never applied, turning decapsulation into a chosen-ciphertext plaintext-checking oracle and enabling static key recovery. Fix: negate b once to 0x00/0xFF and broadcast per byte with _mm256_set1_epi8, matching the pq-crystals reference; drop the redundant scalar-tail negation. Identical to the upstream fix in Argyle-Software/kyber#119. --- src/avx2/verify.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/avx2/verify.rs b/src/avx2/verify.rs index af87fa9..0005484 100644 --- a/src/avx2/verify.rs +++ b/src/avx2/verify.rs @@ -23,8 +23,9 @@ pub fn verify(a: &[u8], b: &[u8], mut len: usize) -> u8 { pub fn cmov(r: &mut [u8], x: &[u8], mut len: usize, mut b: u8) { let (mut xvec, mut rvec); + b = b.wrapping_neg(); unsafe { - let bvec = _mm256_set1_epi64x(b as i64); + let bvec = _mm256_set1_epi8(b as i8); for i in 0..(len / 32) { rvec = _mm256_loadu_si256(r[32 * i..].as_ptr() as *const __m256i); xvec = _mm256_loadu_si256(x[32 * i..].as_ptr() as *const __m256i); @@ -34,7 +35,6 @@ pub fn cmov(r: &mut [u8], x: &[u8], mut len: usize, mut b: u8) { } let idx = 32 * (len / 32); len -= idx; - b = b.wrapping_neg(); for i in 0..len { r[idx + i] ^= b & (x[idx + i] ^ r[idx + i]); }