Skip to content

Commit 8a77fae

Browse files
committed
Add SSSE3 decode prototype evidence
1 parent 0107188 commit 8a77fae

9 files changed

Lines changed: 261 additions & 7 deletions

File tree

docs/RELEASE_EVIDENCE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ The release gate runs:
119119
Rust targets are installed
120120
- backend evidence capture for runtime backend reporting, admitted AVX-512
121121
VBMI, AVX2, SSSE3/SSE4.1, or NEON encode dispatch when supported, and
122-
inactive SIMD prototype scalar-equivalence output for remaining candidates
122+
inactive SIMD prototype scalar-equivalence output for remaining candidates,
123+
including the SSSE3/SSE4.1 fixed-block decode prototype
123124
- SIMD admission policy for the current release series, with AVX-512 VBMI,
124125
AVX2, SSSE3/SSE4.1, and NEON encode admitted only for std x86/x86_64 or std
125126
aarch64 Standard and URL-safe alphabets, no decode acceleration, and no SIMD

docs/SIMD.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ may show admitted encode acceleration as active. On 32-bit ARM, NEON remains
253253
scaffold evidence. The script also writes
254254
`target/release-evidence/backend/MANIFEST.txt` with toolchain metadata,
255255
commands, status values, artifact checksums, and explicit
256-
`prototype_state=real-non-dispatchable` labels for prototype-only backends and
256+
`prototype_state=real-non-dispatchable` labels for prototype-only backends,
257+
including the non-dispatchable SSSE3/SSE4.1 fixed-block decode prototype, and
257258
`active_backend_admitted=avx512-vbmi-or-avx2-or-ssse3-sse4.1-or-neon-encode` for admitted encode
258259
backends.
259260

docs/SIMD_ADMISSION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ State labels are intentionally strict:
8888
| --- | --- | --- | --- |
8989
| AVX-512 VBMI | admitted backend | `avx512f`, `avx512bw`, `avx512vl`, `avx512vbmi` | std x86/x86_64 runtime-dispatched encode for Standard and URL-safe alphabet families; fixed 48-byte blocks use vector code only when at least one full block is present; shorter inputs, tails, unsupported alphabets, in-place encode, line-ending insertion, `no_std`, and decode use scalar fallback |
9090
| AVX2 | admitted backend | `avx2` | std x86/x86_64 runtime-dispatched encode for Standard and URL-safe alphabet families; fixed 24-byte blocks use vector code only when at least one full block is present; shorter inputs, tails, unsupported alphabets, in-place encode, line-ending insertion, `no_std`, and decode use scalar fallback |
91-
| SSSE3/SSE4.1 | admitted backend | `ssse3`, `sse4.1` | std x86/x86_64 runtime-dispatched encode for Standard and URL-safe alphabet families; fixed 12-byte blocks use vector code only when at least one full block is present; shorter inputs, tails, unsupported alphabets, in-place encode, line-ending insertion, `no_std`, and decode use scalar fallback |
91+
| SSSE3/SSE4.1 | admitted backend | `ssse3`, `sse4.1` | std x86/x86_64 runtime-dispatched encode for Standard and URL-safe alphabet families; fixed 12-byte encode blocks use vector code only when at least one full block is present; a fixed 16-byte decode block prototype exists for tests and evidence only; public decode, shorter inputs, tails, unsupported alphabets, in-place encode, line-ending insertion, and `no_std` use scalar fallback |
9292
| NEON | admitted backend | `neon` | std aarch64 runtime-dispatched encode for Standard and URL-safe alphabet families; fixed 12-byte blocks use vector code only when at least one full block is present; shorter inputs, tails, unsupported alphabets, 32-bit ARM, in-place encode, line-ending insertion, `no_std`, and decode use scalar fallback |
9393
| wasm `simd128` | real non-dispatchable prototype | `simd128` | real fixed-block encode prototype for Standard and URL-safe alphabets; test-binary compile evidence only; non-dispatchable |
9494

docs/UNSAFE.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,61 @@ Safety argument:
673673
six-bit Base64 value.
674674
- The helper is private to the Standard-family SSSE3/SSE4.1 encode path.
675675

676+
### `decode_16_bytes_ssse3_sse41`
677+
678+
Location: `src/simd/x86/mod.rs`
679+
680+
Status: non-dispatchable std x86/x86_64 SSSE3/SSE4.1 decode prototype for
681+
Standard and URL-safe alphabet families. It is reachable only from SIMD tests
682+
and backend evidence capture. Runtime decode dispatch remains scalar.
683+
684+
Purpose:
685+
686+
- Establish the first fixed-block SIMD decode admission boundary without
687+
changing public decode behavior.
688+
- Exercise SSSE3/SSE4.1 6-bit-value packing for a 16-byte encoded block that
689+
decodes to at most 12 bytes.
690+
- Verify error agreement, padding behavior, canonical trailing-bit rejection,
691+
and rejected-input output retention before any later dispatch admission.
692+
693+
Preconditions:
694+
695+
- Caller must prove SSSE3 and SSE4.1 are available on the current CPU.
696+
- Input is exactly 16 encoded bytes.
697+
- Output is exactly 12 bytes.
698+
- The vectorized packing path is used only for Standard-family alphabets
699+
(`A-Z`, `a-z`, `0-9`, and either `+/` or `-_`). Other alphabets use the
700+
staged scalar fallback inside the prototype.
701+
702+
Unsafe operation:
703+
704+
- `_mm_loadu_si128` loads sixteen 6-bit values from a local staging array.
705+
- `_mm_maddubs_epi16` and `_mm_madd_epi16` pack groups of four 6-bit values
706+
into 24-bit decoded quanta.
707+
- `_mm_shuffle_epi8` compacts the packed quanta into byte order.
708+
- `_mm_storeu_si128` stores the packed prototype output into a local 16-byte
709+
staging array.
710+
- `clear_xmm_registers_after_encode_block` clears XMM registers before return
711+
to reduce register retention in the vector decode prototype.
712+
713+
Safety argument:
714+
715+
- Scalar strict decode validation runs into a private 12-byte staging buffer
716+
before any byte is copied to the caller output. If validation fails, the
717+
caller output is left untouched and the staging buffer is wiped.
718+
- The input and output array types provide fixed readable and writable bounds.
719+
- The SIMD load and store operate only on local 16-byte arrays and use
720+
unaligned intrinsics, so no alignment precondition is required.
721+
- The SSSE3/SSE4.1 target-feature contract enables every intrinsic used by
722+
the prototype.
723+
- The prototype copies only the validated decoded length to caller output and
724+
wipes local value, packed, and scalar-validation staging buffers before
725+
returning.
726+
- Runtime decode dispatch does not call this function. Future admission must
727+
update this inventory, generated-code evidence, fuzzing, backend reporting,
728+
benchmarks, and release notes in the same commit that makes any decode SIMD
729+
path reachable.
730+
676731
### `clear_xmm_registers_after_encode_block`
677732

678733
Location: `src/simd/x86/cleanup.rs`

scripts/check_backend_evidence.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ cat "$runtime_output"
1515

1616
echo "backend evidence: SIMD prototype scalar-equivalence tests"
1717
prototype_status=0
18-
cargo test --all-features simd::tests:: -- --nocapture >"$prototype_output" 2>&1 || prototype_status="$?"
18+
cargo test --all-features simd:: -- --nocapture >"$prototype_output" 2>&1 || prototype_status="$?"
1919
cat "$prototype_output"
2020

2121
{
@@ -36,7 +36,7 @@ cat "$prototype_output"
3636
echo
3737
echo "commands:"
3838
echo "cargo test --test rfc4648 --all-features runtime_backend_report_matches_admission_state -- --nocapture"
39-
echo "cargo test --all-features simd::tests:: -- --nocapture"
39+
echo "cargo test --all-features simd:: -- --nocapture"
4040
echo
4141
echo "status:"
4242
echo "runtime_backend_report=$runtime_status"
@@ -47,6 +47,7 @@ cat "$prototype_output"
4747
echo "prototype_avx512_vbmi=admitted-encode-backend"
4848
echo "prototype_avx2=admitted-encode-backend"
4949
echo "prototype_ssse3_sse41=admitted-encode-backend"
50+
echo "prototype_ssse3_sse41_decode=real-non-dispatchable"
5051
echo "prototype_neon=admitted-encode-backend-aarch64-standard-family"
5152
echo "prototype_wasm_simd128=real-non-dispatchable-compile-evidence-only"
5253
echo "wasm_simd128_evidence=compile-test-binary-only"
@@ -62,6 +63,7 @@ cat "$prototype_output"
6263
echo
6364
echo "interpretation:"
6465
echo "This evidence records runtime backend reporting, admitted AVX-512 VBMI, AVX2, SSSE3/SSE4.1, or NEON encode dispatch when supported, and remaining inactive SIMD prototype scalar-equivalence results for this machine."
66+
echo "The SSSE3/SSE4.1 decode prototype is fixed-block, non-dispatchable evidence only; public decode APIs still report scalar execution."
6567
echo "Wasm results are prototype evidence; they are not active backend admission evidence."
6668
echo "The admitted x86 AVX-512 VBMI, AVX2, and SSSE3/SSE4.1 paths exercise real fixed-block vector encode logic for Standard and URL-safe alphabets when the required CPU feature bundle is available."
6769
echo "On AArch64 hosts, the admitted NEON path exercises real fixed-block vector encode logic for Standard and URL-safe alphabets; 32-bit ARM remains scaffold evidence."

scripts/validate-panic-policy.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ test -s docs/PANIC_POLICY.md
8181

8282
find src -name '*.rs' | sort | while IFS= read -r source_file; do
8383
case "$source_file" in
84-
src/kani_proofs.rs|src/tests.rs|src/simd/tests.rs|src/simd/wasm.rs)
84+
src/kani_proofs.rs|src/tests.rs|src/simd/tests.rs|src/simd/wasm.rs|src/simd/x86_decode_tests.rs)
8585
continue
8686
;;
8787
esac

src/simd/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,18 @@ pub(crate) use neon::neon_supports_alphabet;
4343
))]
4444
mod x86;
4545
#[cfg(all(test, any(target_arch = "x86", target_arch = "x86_64")))]
46-
pub(super) use x86::{encode_12_bytes_ssse3_sse41, encode_24_bytes_avx2, encode_48_bytes_avx512};
46+
pub(super) use x86::{
47+
decode_16_bytes_ssse3_sse41, encode_12_bytes_ssse3_sse41, encode_24_bytes_avx2,
48+
encode_48_bytes_avx512,
49+
};
4750
#[cfg(all(test, target_arch = "wasm32"))]
4851
mod wasm;
52+
#[cfg(all(
53+
feature = "std",
54+
test,
55+
any(target_arch = "x86", target_arch = "x86_64")
56+
))]
57+
mod x86_decode_tests;
4958

5059
/// Backend currently allowed to execute.
5160
#[derive(Clone, Copy, Debug, Eq, PartialEq)]

src/simd/x86/mod.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
mod cleanup;
44

5+
#[cfg(test)]
6+
use crate::DecodeError;
57
use crate::{Alphabet, EncodeError, checked_encoded_len, encode_base64_value, scalar};
68

79
use cleanup::{
@@ -170,6 +172,8 @@ use core::arch::x86::{
170172
_mm512_loadu_si512, _mm512_or_si512, _mm512_permutexvar_epi8, _mm512_set1_epi32,
171173
_mm512_shuffle_epi8, _mm512_slli_epi32, _mm512_srli_epi32, _mm512_storeu_si512,
172174
};
175+
#[cfg(all(test, target_arch = "x86"))]
176+
use core::arch::x86::{_mm_madd_epi16, _mm_maddubs_epi16};
173177
#[cfg(target_arch = "x86_64")]
174178
use core::arch::x86_64::{
175179
__m128i, __m256i, __m512i, _mm_add_epi8, _mm_and_si128, _mm_blendv_epi8, _mm_cmpeq_epi8,
@@ -181,6 +185,8 @@ use core::arch::x86_64::{
181185
_mm512_loadu_si512, _mm512_or_si512, _mm512_permutexvar_epi8, _mm512_set1_epi32,
182186
_mm512_shuffle_epi8, _mm512_slli_epi32, _mm512_srli_epi32, _mm512_storeu_si512,
183187
};
188+
#[cfg(all(test, target_arch = "x86_64"))]
189+
use core::arch::x86_64::{_mm_madd_epi16, _mm_maddubs_epi16};
184190

185191
#[expect(
186192
clippy::cast_ptr_alignment,
@@ -326,6 +332,78 @@ where
326332
crate::wipe_bytes(&mut staged);
327333
}
328334

335+
#[expect(
336+
clippy::cast_ptr_alignment,
337+
reason = "_mm_loadu_si128 and _mm_storeu_si128 accept unaligned pointers"
338+
)]
339+
#[cfg(test)]
340+
#[target_feature(enable = "ssse3,sse4.1")]
341+
pub(crate) unsafe fn decode_16_bytes_ssse3_sse41<A, const PAD: bool>(
342+
input: &[u8; 16],
343+
output: &mut [u8; 12],
344+
) -> Result<usize, DecodeError>
345+
where
346+
A: Alphabet,
347+
{
348+
let mut scalar_output = [0; 12];
349+
let written = match scalar::decode_slice::<A, PAD>(input, &mut scalar_output) {
350+
Ok(written) => written,
351+
Err(error) => {
352+
crate::wipe_bytes(&mut scalar_output);
353+
return Err(error);
354+
}
355+
};
356+
357+
if !is_standard_or_url_safe_family::<A>() {
358+
output[..written].copy_from_slice(&scalar_output[..written]);
359+
crate::wipe_bytes(&mut scalar_output);
360+
return Ok(written);
361+
}
362+
363+
let mut values = [0; 16];
364+
fill_decode_values::<A>(input, &mut values);
365+
let mut packed = [0; 16];
366+
367+
// SAFETY: Fixed arrays back the unaligned loads and stores, the
368+
// target-feature contract enables SSSE3/SSE4.1, and scalar validation
369+
// above proves the 16-byte block is canonical for the selected padding
370+
// policy before any bytes are copied to the caller output.
371+
unsafe {
372+
let values_vec = _mm_loadu_si128(values.as_ptr().cast::<__m128i>());
373+
let merged_pairs = _mm_maddubs_epi16(values_vec, _mm_set1_epi32(0x0140_0140));
374+
let merged_quads = _mm_madd_epi16(merged_pairs, _mm_set1_epi32(0x0001_1000));
375+
let shuffle = _mm_setr_epi8(
376+
2, 1, 0, 6, 5, 4, 10, 9, 8, 14, 13, 12, -128, -128, -128, -128,
377+
);
378+
let decoded = _mm_shuffle_epi8(merged_quads, shuffle);
379+
_mm_storeu_si128(packed.as_mut_ptr().cast::<__m128i>(), decoded);
380+
clear_xmm_registers_after_encode_block();
381+
}
382+
383+
debug_assert_eq!(&packed[..written], &scalar_output[..written]);
384+
output[..written].copy_from_slice(&packed[..written]);
385+
crate::wipe_bytes(&mut values);
386+
crate::wipe_bytes(&mut packed);
387+
crate::wipe_bytes(&mut scalar_output);
388+
Ok(written)
389+
}
390+
391+
#[cfg(test)]
392+
fn fill_decode_values<A>(input: &[u8; 16], values: &mut [u8; 16])
393+
where
394+
A: Alphabet,
395+
{
396+
let mut index = 0;
397+
while index < input.len() {
398+
values[index] = if input[index] == b'=' {
399+
0
400+
} else {
401+
A::decode(input[index]).unwrap_or(0)
402+
};
403+
index += 1;
404+
}
405+
}
406+
329407
fn is_standard_or_url_safe_family<A>() -> bool
330408
where
331409
A: Alphabet,

src/simd/x86_decode_tests.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use super::*;
2+
use crate::{Alphabet, DecodeError, Engine, Standard, UrlSafe};
3+
4+
fn fill_pattern(output: &mut [u8], seed: usize) {
5+
for (index, byte) in output.iter_mut().enumerate() {
6+
let value = (index * 73 + seed * 19) % 256;
7+
*byte = u8::try_from(value).unwrap();
8+
}
9+
}
10+
11+
#[test]
12+
fn ssse3_sse41_decode_block_matches_scalar_when_available() {
13+
if !ssse3_sse41_available() {
14+
println!("skipped: SSSE3/SSE4.1 decode block test requires ssse3 and sse4.1");
15+
return;
16+
}
17+
18+
let mut raw = [0; 12];
19+
for seed in 0..64 {
20+
fill_pattern(&mut raw, seed);
21+
assert_ssse3_decode_case::<Standard, true>(&encoded_block::<Standard, true>(&raw));
22+
assert_ssse3_decode_case::<UrlSafe, true>(&encoded_block::<UrlSafe, true>(&raw));
23+
assert_ssse3_decode_case::<Standard, false>(&encoded_block::<Standard, false>(&raw));
24+
assert_ssse3_decode_case::<UrlSafe, false>(&encoded_block::<UrlSafe, false>(&raw));
25+
}
26+
27+
for len in [10, 11] {
28+
fill_pattern(&mut raw, len);
29+
assert_ssse3_decode_case::<Standard, true>(&encoded_block::<Standard, true>(&raw[..len]));
30+
assert_ssse3_decode_case::<UrlSafe, true>(&encoded_block::<UrlSafe, true>(&raw[..len]));
31+
}
32+
33+
assert_ssse3_decode_error_matches_scalar::<Standard, true>(mutated_block(
34+
encoded_block::<Standard, true>(&raw),
35+
5,
36+
b'!',
37+
));
38+
assert_ssse3_decode_error_matches_scalar::<Standard, true>(mutated_block(
39+
encoded_block::<Standard, true>(&raw),
40+
3,
41+
b'=',
42+
));
43+
assert_ssse3_decode_error_matches_scalar::<UrlSafe, true>(mutated_block(
44+
encoded_block::<UrlSafe, true>(&raw),
45+
8,
46+
b'/',
47+
));
48+
49+
let non_canonical = *b"AAAAAAAAAAAAAB==";
50+
assert_ssse3_decode_error_matches_scalar::<Standard, true>(non_canonical);
51+
assert_ssse3_decode_error_matches_scalar::<UrlSafe, true>(non_canonical);
52+
}
53+
54+
fn encoded_block<A, const PAD: bool>(input: &[u8]) -> [u8; 16]
55+
where
56+
A: Alphabet,
57+
{
58+
let mut encoded = [0; 16];
59+
let written = Engine::<A, PAD>::new()
60+
.encode_slice(input, &mut encoded)
61+
.unwrap();
62+
assert_eq!(written, encoded.len());
63+
encoded
64+
}
65+
66+
fn mutated_block(mut input: [u8; 16], index: usize, byte: u8) -> [u8; 16] {
67+
input[index] = byte;
68+
input
69+
}
70+
71+
fn assert_ssse3_decode_case<A, const PAD: bool>(input: &[u8; 16])
72+
where
73+
A: Alphabet,
74+
{
75+
let mut ssse3 = [0x55; 12];
76+
let mut scalar = [0xaa; 12];
77+
// SAFETY: The caller checked SSSE3/SSE4.1 availability before invoking
78+
// this helper.
79+
let ssse3_len = unsafe { decode_16_bytes_ssse3_sse41::<A, PAD>(input, &mut ssse3) }.unwrap();
80+
let scalar_len = Engine::<A, PAD>::new()
81+
.decode_slice(input, &mut scalar)
82+
.unwrap();
83+
assert_eq!(ssse3_len, scalar_len);
84+
assert_eq!(&ssse3[..ssse3_len], &scalar[..scalar_len]);
85+
}
86+
87+
fn assert_ssse3_decode_error_matches_scalar<A, const PAD: bool>(input: [u8; 16])
88+
where
89+
A: Alphabet,
90+
{
91+
let mut ssse3 = [0x55; 12];
92+
let mut scalar = [0xaa; 12];
93+
// SAFETY: The caller checked SSSE3/SSE4.1 availability before invoking
94+
// this helper.
95+
let ssse3_error = unsafe { decode_16_bytes_ssse3_sse41::<A, PAD>(&input, &mut ssse3) }
96+
.expect_err("malformed block must be rejected by SSSE3/SSE4.1 prototype");
97+
let scalar_error = Engine::<A, PAD>::new()
98+
.decode_slice(&input, &mut scalar)
99+
.expect_err("malformed block must be rejected by scalar decoder");
100+
assert_eq!(ssse3_error, scalar_error);
101+
assert_eq!(ssse3, [0x55; 12]);
102+
assert!(matches!(
103+
ssse3_error,
104+
DecodeError::InvalidByte { .. }
105+
| DecodeError::InvalidPadding { .. }
106+
| DecodeError::InvalidLength
107+
));
108+
}

0 commit comments

Comments
 (0)