Skip to content

Commit 6c4312e

Browse files
committed
Inline NEON prototype register cleanup
1 parent cfa3246 commit 6c4312e

3 files changed

Lines changed: 95 additions & 88 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
- Added `base64-ng-subtle` as an optional companion crate for projects that
3535
already admit `subtle` and want reviewed `ConstantTimeEq` comparisons for
3636
`base64-ng` buffers.
37+
- Inlined the test-only AArch64 NEON register cleanup macro into the prototype
38+
path so callee-saved `v8..v15` are not restored by a separate helper frame.
3739
- Added a real non-dispatchable AVX2 fixed-block encode prototype for Standard
3840
and URL-safe alphabets. The prototype remains test-only and active runtime
3941
backend selection remains scalar-only.

docs/UNSAFE.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -745,9 +745,9 @@ Safety argument:
745745
- The AArch64 vector path remains test-only and non-dispatchable. Runtime
746746
acceleration is still blocked by the SIMD admission manifest.
747747
- Register-retention note: the AArch64 vector path loads caller bytes into NEON
748-
state and calls `clear_neon_registers_for_test_prototype` before return. This
749-
is retention reduction for the inactive prototype, not a formal
750-
microarchitectural side-channel proof.
748+
state and expands `clear_neon_registers_for_test_prototype!` directly inside
749+
the prototype function before return. This is retention reduction for the
750+
inactive prototype, not a formal microarchitectural side-channel proof.
751751

752752
### `encode_12_bytes_neon_aarch64_standard_family`
753753

@@ -779,8 +779,8 @@ Unsafe operation:
779779
- `encode_standard_family_indices_neon` maps those indices to Standard or
780780
URL-safe alphabet bytes with NEON comparisons and bit selects.
781781
- `vst1q_u8` stores the 16 encoded bytes into the output buffer.
782-
- `clear_neon_registers_for_test_prototype` clears `v0` through `v31` before
783-
return.
782+
- `clear_neon_registers_for_test_prototype!` clears `v0` through `v31` inside
783+
the prototype function before return.
784784
- The local staging array is wiped with the crate cleanup primitive before the
785785
function returns.
786786

@@ -828,11 +828,11 @@ Safety argument:
828828
six-bit Base64 value.
829829
- The helper is private to the test-only prototype path.
830830

831-
### `clear_neon_registers_for_test_prototype`
831+
### `clear_neon_registers_for_test_prototype!`
832832

833833
Location: `src/simd/mod.rs`
834834

835-
Status: private helper for inactive AArch64 NEON test-only prototypes, not
835+
Status: private macro for inactive AArch64 NEON test-only prototypes, not
836836
dispatchable and not reachable from runtime backend selection.
837837

838838
Purpose:
@@ -844,6 +844,9 @@ Preconditions:
844844

845845
- Called only after the prototype has stored its output and no later NEON value
846846
is needed by the function.
847+
- Expanded directly inside the prototype function. It must not be moved to a
848+
separate function because an AArch64 helper can save and restore callee-saved
849+
`v8` through `v15`, undoing register clearing in the helper frame.
847850

848851
Unsafe operation:
849852

@@ -852,14 +855,14 @@ Unsafe operation:
852855

853856
Safety argument:
854857

855-
- The helper does not read or write memory.
856-
- The helper runs at the end of the inactive prototype path.
858+
- The macro does not read or write memory.
859+
- The macro expands at the end of the inactive prototype path.
857860
- Clobbered registers are declared to the compiler with explicit `out("vN")`
858861
operands.
859862
- This is best-effort register-retention reduction for test evidence, not a
860863
guarantee that historical register, stack, cache, or microarchitectural
861864
copies do not exist.
862-
- This helper clears all AArch64 vector registers for the reviewed prototype
865+
- This macro clears all AArch64 vector registers for the reviewed prototype
863866
sequence. It is not an admission claim for arbitrary future NEON code. Before
864867
NEON dispatch can become active, generated assembly must prove which vector
865868
registers carry caller-derived data and whether any callee-saved vector

src/simd/mod.rs

Lines changed: 80 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,85 @@ where
264264
(encode[62] == b'+' && encode[63] == b'/') || (encode[62] == b'-' && encode[63] == b'_')
265265
}
266266

267+
#[cfg(all(test, target_arch = "aarch64"))]
268+
macro_rules! clear_neon_registers_for_test_prototype {
269+
() => {{
270+
// SAFETY: This test-only cleanup is expanded directly inside the
271+
// prototype function after it stores its output. There is no separate
272+
// helper frame whose ABI save/restore can undo `v8..v15` clearing. The
273+
// explicit outputs tell the compiler every AArch64 vector register is
274+
// clobbered while the assembly clears it. This is retention reduction
275+
// for prototype evidence, not a formal microarchitectural proof.
276+
core::arch::asm!(
277+
"eor v0.16b, v0.16b, v0.16b",
278+
"eor v1.16b, v1.16b, v1.16b",
279+
"eor v2.16b, v2.16b, v2.16b",
280+
"eor v3.16b, v3.16b, v3.16b",
281+
"eor v4.16b, v4.16b, v4.16b",
282+
"eor v5.16b, v5.16b, v5.16b",
283+
"eor v6.16b, v6.16b, v6.16b",
284+
"eor v7.16b, v7.16b, v7.16b",
285+
"eor v8.16b, v8.16b, v8.16b",
286+
"eor v9.16b, v9.16b, v9.16b",
287+
"eor v10.16b, v10.16b, v10.16b",
288+
"eor v11.16b, v11.16b, v11.16b",
289+
"eor v12.16b, v12.16b, v12.16b",
290+
"eor v13.16b, v13.16b, v13.16b",
291+
"eor v14.16b, v14.16b, v14.16b",
292+
"eor v15.16b, v15.16b, v15.16b",
293+
"eor v16.16b, v16.16b, v16.16b",
294+
"eor v17.16b, v17.16b, v17.16b",
295+
"eor v18.16b, v18.16b, v18.16b",
296+
"eor v19.16b, v19.16b, v19.16b",
297+
"eor v20.16b, v20.16b, v20.16b",
298+
"eor v21.16b, v21.16b, v21.16b",
299+
"eor v22.16b, v22.16b, v22.16b",
300+
"eor v23.16b, v23.16b, v23.16b",
301+
"eor v24.16b, v24.16b, v24.16b",
302+
"eor v25.16b, v25.16b, v25.16b",
303+
"eor v26.16b, v26.16b, v26.16b",
304+
"eor v27.16b, v27.16b, v27.16b",
305+
"eor v28.16b, v28.16b, v28.16b",
306+
"eor v29.16b, v29.16b, v29.16b",
307+
"eor v30.16b, v30.16b, v30.16b",
308+
"eor v31.16b, v31.16b, v31.16b",
309+
out("v0") _,
310+
out("v1") _,
311+
out("v2") _,
312+
out("v3") _,
313+
out("v4") _,
314+
out("v5") _,
315+
out("v6") _,
316+
out("v7") _,
317+
out("v8") _,
318+
out("v9") _,
319+
out("v10") _,
320+
out("v11") _,
321+
out("v12") _,
322+
out("v13") _,
323+
out("v14") _,
324+
out("v15") _,
325+
out("v16") _,
326+
out("v17") _,
327+
out("v18") _,
328+
out("v19") _,
329+
out("v20") _,
330+
out("v21") _,
331+
out("v22") _,
332+
out("v23") _,
333+
out("v24") _,
334+
out("v25") _,
335+
out("v26") _,
336+
out("v27") _,
337+
out("v28") _,
338+
out("v29") _,
339+
out("v30") _,
340+
out("v31") _,
341+
options(nostack, preserves_flags)
342+
);
343+
}};
344+
}
345+
267346
#[cfg(all(test, target_arch = "aarch64"))]
268347
#[target_feature(enable = "neon")]
269348
unsafe fn encode_12_bytes_neon_aarch64_standard_family<A>(input: &[u8; 12], output: &mut [u8; 16])
@@ -296,7 +375,7 @@ where
296375

297376
let encoded = encode_standard_family_indices_neon::<A>(indices);
298377
vst1q_u8(output.as_mut_ptr(), encoded);
299-
clear_neon_registers_for_test_prototype();
378+
clear_neon_registers_for_test_prototype!();
300379
}
301380
crate::wipe_bytes(&mut staged);
302381
}
@@ -337,82 +416,5 @@ where
337416
vbslq_u8(slash, vdupq_n_u8(slash_char), encoded)
338417
}
339418

340-
#[cfg(all(test, target_arch = "aarch64"))]
341-
unsafe fn clear_neon_registers_for_test_prototype() {
342-
// SAFETY: This test-only cleanup runs after the prototype stores its
343-
// output. The explicit outputs tell the compiler every AArch64 vector
344-
// register is clobbered while the assembly clears it. This is retention
345-
// reduction for prototype evidence, not a formal microarchitectural proof.
346-
unsafe {
347-
core::arch::asm!(
348-
"eor v0.16b, v0.16b, v0.16b",
349-
"eor v1.16b, v1.16b, v1.16b",
350-
"eor v2.16b, v2.16b, v2.16b",
351-
"eor v3.16b, v3.16b, v3.16b",
352-
"eor v4.16b, v4.16b, v4.16b",
353-
"eor v5.16b, v5.16b, v5.16b",
354-
"eor v6.16b, v6.16b, v6.16b",
355-
"eor v7.16b, v7.16b, v7.16b",
356-
"eor v8.16b, v8.16b, v8.16b",
357-
"eor v9.16b, v9.16b, v9.16b",
358-
"eor v10.16b, v10.16b, v10.16b",
359-
"eor v11.16b, v11.16b, v11.16b",
360-
"eor v12.16b, v12.16b, v12.16b",
361-
"eor v13.16b, v13.16b, v13.16b",
362-
"eor v14.16b, v14.16b, v14.16b",
363-
"eor v15.16b, v15.16b, v15.16b",
364-
"eor v16.16b, v16.16b, v16.16b",
365-
"eor v17.16b, v17.16b, v17.16b",
366-
"eor v18.16b, v18.16b, v18.16b",
367-
"eor v19.16b, v19.16b, v19.16b",
368-
"eor v20.16b, v20.16b, v20.16b",
369-
"eor v21.16b, v21.16b, v21.16b",
370-
"eor v22.16b, v22.16b, v22.16b",
371-
"eor v23.16b, v23.16b, v23.16b",
372-
"eor v24.16b, v24.16b, v24.16b",
373-
"eor v25.16b, v25.16b, v25.16b",
374-
"eor v26.16b, v26.16b, v26.16b",
375-
"eor v27.16b, v27.16b, v27.16b",
376-
"eor v28.16b, v28.16b, v28.16b",
377-
"eor v29.16b, v29.16b, v29.16b",
378-
"eor v30.16b, v30.16b, v30.16b",
379-
"eor v31.16b, v31.16b, v31.16b",
380-
out("v0") _,
381-
out("v1") _,
382-
out("v2") _,
383-
out("v3") _,
384-
out("v4") _,
385-
out("v5") _,
386-
out("v6") _,
387-
out("v7") _,
388-
out("v8") _,
389-
out("v9") _,
390-
out("v10") _,
391-
out("v11") _,
392-
out("v12") _,
393-
out("v13") _,
394-
out("v14") _,
395-
out("v15") _,
396-
out("v16") _,
397-
out("v17") _,
398-
out("v18") _,
399-
out("v19") _,
400-
out("v20") _,
401-
out("v21") _,
402-
out("v22") _,
403-
out("v23") _,
404-
out("v24") _,
405-
out("v25") _,
406-
out("v26") _,
407-
out("v27") _,
408-
out("v28") _,
409-
out("v29") _,
410-
out("v30") _,
411-
out("v31") _,
412-
options(nostack, preserves_flags)
413-
);
414-
}
415-
}
416-
417419
#[cfg(all(test, not(target_arch = "wasm32")))]
418420
mod tests;

0 commit comments

Comments
 (0)