Skip to content

Commit acdf182

Browse files
committed
Harden decode prototype parity checks
1 parent 14735a8 commit acdf182

3 files changed

Lines changed: 76 additions & 34 deletions

File tree

docs/UNSAFE.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -720,9 +720,10 @@ Safety argument:
720720
unaligned intrinsics, so no alignment precondition is required.
721721
- The SSSE3/SSE4.1 target-feature contract enables every intrinsic used by
722722
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.
723+
- The prototype copies only the validated decoded length to caller output after
724+
an unconditional release-mode equality check proves the vector-packed prefix
725+
matches the scalar-validation prefix. It wipes local value, packed, and
726+
scalar-validation staging buffers before returning.
726727
- Runtime decode dispatch does not call this function. Future admission must
727728
update this inventory, generated-code evidence, fuzzing, backend reporting,
728729
benchmarks, and release notes in the same commit that makes any decode SIMD
@@ -784,9 +785,10 @@ Safety argument:
784785
prototype.
785786
- VBMI compaction indices are constants in `0..=59`, so the permute reads only
786787
bytes produced by the lane-local decode shuffle.
787-
- The prototype copies only the validated decoded length to caller output and
788-
wipes local value, packed, and scalar-validation staging buffers before
789-
returning.
788+
- The prototype copies only the validated decoded length to caller output after
789+
an unconditional release-mode equality check proves the vector-packed prefix
790+
matches the scalar-validation prefix. It wipes local value, packed, and
791+
scalar-validation staging buffers before returning.
790792
- Runtime decode dispatch does not call this function. Future admission must
791793
update this inventory, generated-code evidence, fuzzing, backend reporting,
792794
benchmarks, and release notes in the same commit that makes any decode SIMD
@@ -846,9 +848,10 @@ Safety argument:
846848
- AVX2 byte shuffle is lane-local, so the prototype explicitly compacts the
847849
second 12-byte decoded lane from offsets `16..28` to `12..24` inside local
848850
staging before copying to caller output.
849-
- The prototype copies only the validated decoded length to caller output and
850-
wipes local value, packed, and scalar-validation staging buffers before
851-
returning.
851+
- The prototype copies only the validated decoded length to caller output after
852+
an unconditional release-mode equality check proves the vector-packed prefix
853+
matches the scalar-validation prefix. It wipes local value, packed, and
854+
scalar-validation staging buffers before returning.
852855
- Runtime decode dispatch does not call this function. Future admission must
853856
update this inventory, generated-code evidence, fuzzing, backend reporting,
854857
benchmarks, and release notes in the same commit that makes any decode SIMD
@@ -977,7 +980,9 @@ Safety argument:
977980
prototype copies bytes to caller-visible output.
978981
- The input and output array types provide fixed readable and writable bounds.
979982
- The vector store writes only to a local 16-byte packed buffer; the caller
980-
output receives at most the scalar-validated `written` prefix.
983+
output receives at most the scalar-validated `written` prefix, and only after
984+
an unconditional release-mode equality check proves the vector-packed prefix
985+
matches the scalar-validation prefix.
981986
- The compaction mask contains only valid source indices or zero lanes.
982987
- Staging, packed, and scalar-output buffers are wiped before successful return
983988
or along the error path.

src/simd/neon.rs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -287,11 +287,8 @@ where
287287
clear_neon_registers_after_vector_block!();
288288
}
289289

290-
debug_assert_eq!(&packed[..written], &scalar_output[..written]);
291-
output[..written].copy_from_slice(&packed[..written]);
292290
crate::wipe_bytes(&mut values);
293-
crate::wipe_bytes(&mut packed);
294-
crate::wipe_bytes(&mut scalar_output);
291+
copy_verified_decode_output(&mut packed, &mut scalar_output, output, written)?;
295292
Ok(written)
296293
}
297294

@@ -422,17 +419,42 @@ where
422419
vbslq_u8(slash, vdupq_n_u8(slash_char), encoded)
423420
}
424421

422+
#[cfg(all(target_arch = "aarch64", test))]
423+
fn copy_verified_decode_output<const PACKED: usize, const SCALAR: usize>(
424+
packed: &mut [u8; PACKED],
425+
scalar_output: &mut [u8; SCALAR],
426+
output: &mut [u8],
427+
written: usize,
428+
) -> Result<(), crate::DecodeError> {
429+
if packed[..written] != scalar_output[..written] {
430+
crate::wipe_bytes(packed);
431+
crate::wipe_bytes(scalar_output);
432+
return Err(crate::DecodeError::InvalidInput);
433+
}
434+
435+
output[..written].copy_from_slice(&packed[..written]);
436+
crate::wipe_bytes(packed);
437+
crate::wipe_bytes(scalar_output);
438+
Ok(())
439+
}
440+
425441
#[cfg(all(target_arch = "aarch64", test))]
426442
fn fill_decode_values<A, const N: usize>(input: &[u8; N], values: &mut [u8; N])
427443
where
428444
A: Alphabet,
429445
{
430446
let mut index = 0;
431447
while index < input.len() {
432-
values[index] = if input[index] == b'=' {
433-
0
434-
} else {
435-
A::decode(input[index]).unwrap_or(0)
448+
values[index] = match input[index] {
449+
b'=' => 0,
450+
byte => {
451+
if let Some(value) = A::decode(byte) {
452+
value
453+
} else {
454+
debug_assert!(false, "fill_decode_values called on unvalidated input");
455+
0
456+
}
457+
}
436458
};
437459
index += 1;
438460
}

src/simd/x86/decode.rs

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,8 @@ where
6666
super::cleanup::clear_xmm_registers_after_encode_block();
6767
}
6868

69-
debug_assert_eq!(&packed[..written], &scalar_output[..written]);
70-
output[..written].copy_from_slice(&packed[..written]);
7169
crate::wipe_bytes(&mut values);
72-
crate::wipe_bytes(&mut packed);
73-
crate::wipe_bytes(&mut scalar_output);
70+
copy_verified_decode_output(&mut packed, &mut scalar_output, output, written)?;
7471
Ok(written)
7572
}
7673

@@ -127,11 +124,8 @@ where
127124
];
128125
packed[12..24].copy_from_slice(&upper_lane);
129126

130-
debug_assert_eq!(&packed[..written], &scalar_output[..written]);
131-
output[..written].copy_from_slice(&packed[..written]);
132127
crate::wipe_bytes(&mut values);
133-
crate::wipe_bytes(&mut packed);
134-
crate::wipe_bytes(&mut scalar_output);
128+
copy_verified_decode_output(&mut packed, &mut scalar_output, output, written)?;
135129
Ok(written)
136130
}
137131

@@ -192,24 +186,45 @@ where
192186
super::cleanup::clear_zmm_registers_after_encode_block();
193187
}
194188

195-
debug_assert_eq!(&packed[..written], &scalar_output[..written]);
196-
output[..written].copy_from_slice(&packed[..written]);
197189
crate::wipe_bytes(&mut values);
198-
crate::wipe_bytes(&mut packed);
199-
crate::wipe_bytes(&mut scalar_output);
190+
copy_verified_decode_output(&mut packed, &mut scalar_output, output, written)?;
200191
Ok(written)
201192
}
202193

194+
fn copy_verified_decode_output<const PACKED: usize, const SCALAR: usize>(
195+
packed: &mut [u8; PACKED],
196+
scalar_output: &mut [u8; SCALAR],
197+
output: &mut [u8],
198+
written: usize,
199+
) -> Result<(), DecodeError> {
200+
if packed[..written] != scalar_output[..written] {
201+
crate::wipe_bytes(packed);
202+
crate::wipe_bytes(scalar_output);
203+
return Err(DecodeError::InvalidInput);
204+
}
205+
206+
output[..written].copy_from_slice(&packed[..written]);
207+
crate::wipe_bytes(packed);
208+
crate::wipe_bytes(scalar_output);
209+
Ok(())
210+
}
211+
203212
fn fill_decode_values<A, const N: usize>(input: &[u8; N], values: &mut [u8; N])
204213
where
205214
A: Alphabet,
206215
{
207216
let mut index = 0;
208217
while index < input.len() {
209-
values[index] = if input[index] == b'=' {
210-
0
211-
} else {
212-
A::decode(input[index]).unwrap_or(0)
218+
values[index] = match input[index] {
219+
b'=' => 0,
220+
byte => {
221+
if let Some(value) = A::decode(byte) {
222+
value
223+
} else {
224+
debug_assert!(false, "fill_decode_values called on unvalidated input");
225+
0
226+
}
227+
}
213228
};
214229
index += 1;
215230
}

0 commit comments

Comments
 (0)