Skip to content

Commit fbaf978

Browse files
committed
Split cleanup module
1 parent 4c0f12a commit fbaf978

9 files changed

Lines changed: 176 additions & 156 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
exports.
1616
- Split `Profile` and the named MIME/PEM/bcrypt/crypt profile constants into
1717
`src/profiles.rs` while preserving all public root exports.
18+
- Split best-effort cleanup and wipe helpers into `src/cleanup.rs`, preserving
19+
internal call paths and updating the unsafe-boundary release gate.
1820

1921
## 1.0.2 - 2026-05-29
2022

docs/PLAN.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -591,9 +591,9 @@ Recommended `1.0.x` source-layout sequence:
591591
- `1.0.3`: split runtime backend reporting and backend-policy types into
592592
`src/runtime.rs`; split alphabets, custom alphabet validation, the alphabet
593593
macro, and alphabet errors into `src/alphabet.rs`; split profile wrappers
594-
into `src/profiles.rs`.
595-
- `1.0.4`: split stack/owned buffer wrappers and best-effort cleanup helpers
596-
into `src/buffers.rs` and `src/cleanup.rs`.
594+
into `src/profiles.rs`; split best-effort cleanup helpers into
595+
`src/cleanup.rs`.
596+
- `1.0.4`: split stack/owned buffer wrappers into `src/buffers.rs`.
597597
- `1.0.5`: split constant-time-oriented decode, validation, masks, and CT
598598
barriers into `src/ct.rs`. This comes later because it is the most
599599
security-sensitive move.

docs/RELEASE_EVIDENCE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ The release gate runs:
7373
`define_alphabet!` macro
7474
- moved-code review for the `src/profiles.rs` extraction, preserving root
7575
public exports for `Profile` and the named MIME/PEM/bcrypt/crypt profiles
76+
- moved-code review for the `src/cleanup.rs` extraction, preserving internal
77+
cleanup call paths and updating the unsafe-boundary gate for the new audited
78+
unsafe location
7679
- all-features and no-default-features doctests plus documentation builds
7780
- `cargo deny check`
7881
- `cargo audit`

docs/SIMD.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
`base64-ng` is intentionally scalar-only in the `1.0.3` release. Future SIMD
44
dispatch remains gated unless a complete SIMD admission evidence package lands
5-
in a later release series. The crate uses `#![deny(unsafe_code)]` and permits reviewed
6-
`allow(unsafe_code)` exceptions only for audited cleanup, CT barrier, validated
7-
secret conversion helpers in `src/lib.rs`, and the private `src/simd.rs`
8-
boundary. The `simd` feature remains reserved until architecture-specific code
9-
has enough evidence to justify enabling it.
5+
in a later release series. The crate uses `#![deny(unsafe_code)]` and permits
6+
reviewed `allow(unsafe_code)` exceptions only for audited cleanup in
7+
`src/cleanup.rs`, CT barrier and validated secret conversion helpers in
8+
`src/lib.rs`, and the private `src/simd.rs` boundary. The `simd` feature
9+
remains reserved until architecture-specific code has enough evidence to
10+
justify enabling it.
1011

1112
This is a security decision, not a rejection of hardware acceleration. SIMD
1213
must be added only when it can be isolated, tested, and reviewed without

docs/TRUST.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ stable release.
1111
| Runtime dependencies | Zero external crates | `scripts/validate-dependencies.sh` |
1212
| Default dev dependencies | Zero external crates | `Cargo.toml` |
1313
| Optional runtime features | `alloc`, `std`, `stream`; `allow-wasm32-best-effort-wipe` explicit wasm cleanup-limit acceptance; `allow-compiler-fence-only-wipe` explicit unsupported-native cleanup-limit acceptance; reserved `simd`, `tokio`, `kani`, `fuzzing` | `Cargo.toml`, `scripts/check_reserved_features.sh`, `scripts/check_wasm_wipe_policy.sh` |
14-
| Unsafe policy | Scalar encode/decode remains safe Rust; audited unsafe is limited to volatile wiping, constant-time comparison and result-gate barriers, validated secret UTF-8 conversion, and test-only SIMD prototypes; runtime unsafe-boundary reports are conservative and mark SIMD-enabled builds as not high-assurance-boundary-enforced | `src/lib.rs`, `src/simd.rs`, `docs/UNSAFE.md` |
14+
| Unsafe policy | Scalar encode/decode remains safe Rust; audited unsafe is limited to volatile wiping, constant-time comparison and result-gate barriers, validated secret UTF-8 conversion, and test-only SIMD prototypes; runtime unsafe-boundary reports are conservative and mark SIMD-enabled builds as not high-assurance-boundary-enforced | `src/cleanup.rs`, `src/lib.rs`, `src/simd.rs`, `docs/UNSAFE.md` |
1515
| Active backend | Scalar only | `runtime::backend_report()` tests |
1616
| SIMD status | Reserved prototypes only; no accelerated backend admitted | `docs/SIMD.md` |
1717
| Strict decoding | Default behavior rejects whitespace, mixed alphabets, malformed padding, and non-canonical trailing bits | integration tests |

docs/UNSAFE.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
`base64-ng` keeps scalar encode/decode in safe Rust. The crate root uses
44
`#![deny(unsafe_code)]`, and reviewed `allow(unsafe_code)` exceptions are
5-
limited to volatile wipe helpers, the constant-time comparison accumulator
6-
barrier, the validated secret UTF-8 conversion helper, the constant-time error
7-
gate barrier in `src/lib.rs`, and the SIMD boundary in `src/simd.rs`.
5+
limited to volatile wipe helpers in `src/cleanup.rs`, the constant-time
6+
comparison accumulator barrier, the validated secret UTF-8 conversion helper,
7+
the constant-time error gate barrier in `src/lib.rs`, and the SIMD boundary in
8+
`src/simd.rs`.
89

910
This inventory is intentionally small and release-gate enforced. Any new unsafe
1011
block must be added here before an accelerated backend can be admitted.
@@ -17,19 +18,20 @@ block must be added here before an accelerated backend can be admitted.
1718
- Optional SIMD prototypes live only in `src/simd.rs` and are compiled only
1819
for tests until a real backend is admitted.
1920
- `scripts/validate-unsafe-boundary.sh` fails if `allow(unsafe_code)` appears
20-
outside the volatile wipe helpers, the constant-time comparison accumulator
21-
barrier, the validated secret UTF-8 conversion helper, the constant-time
22-
error gate barrier, or `src/simd.rs`.
21+
outside `src/cleanup.rs`, the constant-time comparison accumulator barrier,
22+
the validated secret UTF-8 conversion helper, the constant-time error gate
23+
barrier, or `src/simd.rs`.
2324
- `scripts/validate-unsafe-boundary.sh` fails if architecture intrinsics, CPU
24-
feature detection, or `target_feature` gates appear outside `src/simd.rs`.
25+
feature detection, or `target_feature` gates appear outside the reviewed
26+
cleanup, constant-time gate, and SIMD boundaries.
2527
- Every unsafe function and unsafe block must have a local safety explanation.
2628
- Prototype functions are not eligible for runtime dispatch.
2729

2830
## Current Unsafe Sites
2931

3032
### `wipe_bytes`
3133

32-
Location: `src/lib.rs`
34+
Location: `src/cleanup.rs`
3335

3436
Status: active cleanup primitive.
3537

@@ -93,7 +95,7 @@ Limitations:
9395

9496
### `wipe_barrier`
9597

96-
Location: `src/lib.rs`
98+
Location: `src/cleanup.rs`
9799

98100
Status: active cleanup-boundary hardening primitive.
99101

@@ -307,7 +309,7 @@ Limitations:
307309

308310
### `wipe_vec_spare_capacity`
309311

310-
Location: `src/lib.rs`
312+
Location: `src/cleanup.rs`
311313

312314
Status: active cleanup primitive when `alloc` is enabled.
313315

scripts/validate-unsafe-boundary.sh

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,52 +8,59 @@ fi
88

99
simd_allowed='src/simd.rs'
1010
root_allowed='src/lib.rs'
11+
cleanup_allowed='src/cleanup.rs'
1112
matches="$(grep -RIl 'allow(unsafe_code)' src | sort || true)"
12-
allowed="$(printf '%s\n%s' "$root_allowed" "$simd_allowed" | sort)"
13+
allowed="$(printf '%s\n%s\n%s' "$root_allowed" "$cleanup_allowed" "$simd_allowed" | sort)"
1314

1415
if [ "$matches" != "$allowed" ]; then
15-
echo "unsafe boundary: allow(unsafe_code) may appear only in $root_allowed and $simd_allowed"
16+
echo "unsafe boundary: allow(unsafe_code) may appear only in $root_allowed, $cleanup_allowed, and $simd_allowed"
1617
if [ -n "$matches" ]; then
1718
echo "$matches"
1819
fi
1920
exit 1
2021
fi
2122

2223
root_allow_count="$(grep -c '^#\[allow(unsafe_code)\]$' "$root_allowed" || true)"
23-
if [ "$root_allow_count" -ne 7 ]; then
24-
echo "unsafe boundary: src/lib.rs must have exactly seven reviewed allow(unsafe_code) helpers"
24+
if [ "$root_allow_count" -ne 4 ]; then
25+
echo "unsafe boundary: src/lib.rs must have exactly four reviewed allow(unsafe_code) helpers"
26+
exit 1
27+
fi
28+
29+
cleanup_allow_count="$(grep -c '^#\[allow(unsafe_code)\]$' "$cleanup_allowed" || true)"
30+
if [ "$cleanup_allow_count" -ne 3 ]; then
31+
echo "unsafe boundary: src/cleanup.rs must have exactly three reviewed allow(unsafe_code) helpers"
2532
exit 1
2633
fi
2734

2835
if ! awk '
2936
/^#\[allow\(unsafe_code\)\]$/ {
3037
allow_line = NR
3138
}
32-
/^fn wipe_bytes\(/ || /^fn wipe_barrier\(/ || /^fn wipe_vec_spare_capacity\(/ || /^fn ct_error_gate_barrier\(/ || /^fn constant_time_eq_same_len\(/ || /^fn string_from_validated_secret_bytes\(/ || /^fn ct_decode_alphabet_byte/ {
39+
/^(pub\(crate\) )?fn wipe_bytes\(/ || /^fn wipe_barrier\(/ || /^(pub\(crate\) )?fn wipe_vec_spare_capacity\(/ || /^fn ct_error_gate_barrier\(/ || /^fn constant_time_eq_same_len\(/ || /^fn string_from_validated_secret_bytes\(/ || /^fn ct_decode_alphabet_byte/ {
3340
if (allow_line != NR - 1) {
3441
failed = 1
3542
}
3643
seen += 1
3744
}
3845
END { exit failed || seen != 7 }
39-
' "$root_allowed"; then
40-
echo "unsafe boundary: src/lib.rs allow(unsafe_code) must apply only to reviewed cleanup, secret-conversion, comparison, CT scan, and CT gate helpers"
46+
' "$root_allowed" "$cleanup_allowed"; then
47+
echo "unsafe boundary: allow(unsafe_code) must apply only to reviewed cleanup, secret-conversion, comparison, CT scan, and CT gate helpers"
4148
exit 1
4249
fi
4350

4451
arch_matches="$(grep -RIl -e 'core::arch' -e 'std::arch' -e 'is_x86_feature_detected!' -e 'target_feature' src | sort || true)"
45-
arch_allowed="$(printf '%s\n%s' "$root_allowed" "$simd_allowed" | sort)"
52+
arch_allowed="$(printf '%s\n%s\n%s' "$root_allowed" "$cleanup_allowed" "$simd_allowed" | sort)"
4653

4754
if [ "$arch_matches" != "$arch_allowed" ]; then
48-
echo "unsafe boundary: architecture intrinsics may appear only in $root_allowed cleanup barriers and $simd_allowed"
55+
echo "unsafe boundary: architecture intrinsics may appear only in $root_allowed CT barriers, $cleanup_allowed cleanup barriers, and $simd_allowed"
4956
if [ -n "$arch_matches" ]; then
5057
echo "$arch_matches"
5158
fi
5259
exit 1
5360
fi
5461

55-
if ! grep -q 'core::arch::asm!' "$root_allowed"; then
56-
echo "unsafe boundary: src/lib.rs cleanup barrier must use the reviewed inline assembly barrier"
62+
if ! grep -q 'core::arch::asm!' "$cleanup_allowed"; then
63+
echo "unsafe boundary: src/cleanup.rs cleanup barrier must use the reviewed inline assembly barrier"
5764
exit 1
5865
fi
5966

@@ -90,7 +97,7 @@ if ! awk '
9097
prev1 = $0
9198
}
9299
END { exit failed }
93-
' "$root_allowed" "$simd_allowed"; then
100+
' "$root_allowed" "$cleanup_allowed" "$simd_allowed"; then
94101
echo "unsafe boundary: every unsafe block must have a nearby SAFETY explanation"
95102
exit 1
96103
fi

src/cleanup.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//! Best-effort dependency-free memory cleanup helpers.
2+
3+
#[inline(never)]
4+
#[allow(unsafe_code)]
5+
pub(crate) fn wipe_bytes(bytes: &mut [u8]) {
6+
for byte in bytes.iter_mut() {
7+
// SAFETY: `byte` comes from a unique mutable slice iterator, so the
8+
// pointer is non-null, aligned, valid for one `u8` write, and does not
9+
// alias another live mutable reference during this iteration.
10+
unsafe {
11+
core::ptr::write_volatile(byte, 0);
12+
}
13+
}
14+
wipe_barrier(bytes.as_mut_ptr(), bytes.len());
15+
}
16+
17+
#[inline(never)]
18+
#[allow(unsafe_code)]
19+
fn wipe_barrier(ptr: *mut u8, len: usize) {
20+
let _ = (ptr, len);
21+
22+
#[cfg(all(not(miri), any(target_arch = "x86", target_arch = "x86_64")))]
23+
{
24+
// `mfence` orders prior stores before later memory operations on
25+
// x86/x86_64, while the pointer and length are opaque optimizer inputs.
26+
// SAFETY: the assembly block does not read or write through the pointer.
27+
unsafe {
28+
core::arch::asm!(
29+
"mfence",
30+
"/* {0} {1} */",
31+
in(reg) ptr,
32+
in(reg) len,
33+
options(nostack, preserves_flags)
34+
);
35+
}
36+
}
37+
38+
#[cfg(all(not(miri), target_arch = "aarch64"))]
39+
{
40+
// `dsb sy` completes prior explicit memory accesses before later
41+
// instructions, and `isb sy` flushes subsequent instruction context.
42+
// SAFETY: the assembly block does not read or write through the pointer.
43+
unsafe {
44+
core::arch::asm!(
45+
"dsb sy",
46+
"isb sy",
47+
"hint #20",
48+
"/* {0} {1} */",
49+
in(reg) ptr,
50+
in(reg) len,
51+
options(nostack, preserves_flags)
52+
);
53+
}
54+
}
55+
56+
#[cfg(all(not(miri), target_arch = "arm"))]
57+
{
58+
// `dsb sy` completes prior explicit memory accesses before later
59+
// instructions, and `isb sy` flushes subsequent instruction context.
60+
// SAFETY: the assembly block does not read or write through the pointer.
61+
unsafe {
62+
core::arch::asm!(
63+
"dsb sy",
64+
"isb sy",
65+
"/* {0} {1} */",
66+
in(reg) ptr,
67+
in(reg) len,
68+
options(nostack, preserves_flags)
69+
);
70+
}
71+
}
72+
73+
#[cfg(all(not(miri), any(target_arch = "riscv32", target_arch = "riscv64")))]
74+
{
75+
// `fence rw, rw` orders prior reads/writes before later reads/writes.
76+
// SAFETY: the assembly block does not read or write through the pointer.
77+
unsafe {
78+
core::arch::asm!(
79+
"fence rw, rw",
80+
"/* {0} {1} */",
81+
in(reg) ptr,
82+
in(reg) len,
83+
options(nostack, preserves_flags)
84+
);
85+
}
86+
}
87+
88+
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
89+
}
90+
91+
pub(crate) fn wipe_tail(bytes: &mut [u8], start: usize) {
92+
wipe_bytes(&mut bytes[start..]);
93+
}
94+
95+
#[cfg(feature = "alloc")]
96+
#[allow(unsafe_code)]
97+
pub(crate) fn wipe_vec_spare_capacity(bytes: &mut alloc::vec::Vec<u8>) {
98+
let ptr = bytes.as_mut_ptr();
99+
let len = bytes.len();
100+
let capacity = bytes.capacity();
101+
let spare = capacity - len;
102+
if spare == 0 {
103+
return;
104+
}
105+
106+
let mut offset = len;
107+
while offset < capacity {
108+
// SAFETY: `offset` is within the vector allocation's spare capacity, so
109+
// the pointer is valid, aligned, and writable for one `u8`. This writes
110+
// a zero byte without reading the prior uninitialized value.
111+
unsafe {
112+
core::ptr::write_volatile(ptr.add(offset), 0);
113+
}
114+
offset += 1;
115+
}
116+
// SAFETY: `spare > 0`, so `len < capacity` and `ptr.add(len)` points
117+
// inside the vector allocation at the first spare-capacity byte.
118+
let spare_ptr = unsafe { ptr.add(len) };
119+
wipe_barrier(spare_ptr, spare);
120+
}
121+
122+
#[cfg(feature = "alloc")]
123+
pub(crate) fn wipe_vec_all(bytes: &mut alloc::vec::Vec<u8>) {
124+
wipe_bytes(bytes);
125+
wipe_vec_spare_capacity(bytes);
126+
}

0 commit comments

Comments
 (0)