sanitization 2.0.3

Dependency-free no_std secret memory sanitization with safe defaults and an explicit volatile wipe backend.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
<p align="center">
  <b>Dependency-free, no_std-first secret memory sanitization for Rust.</b><br>
  Redacted secret containers, volatile clearing, data-oblivious helpers, and optional native hardening.
</p>

<div align="center">
  <a href="https://docs.rs/sanitization">Docs.rs</a>
  |
  <a href="https://github.com/valkyoth/sanitization/blob/main/docs/FEATURES.md">Features</a>
  |
  <a href="https://github.com/valkyoth/sanitization/blob/main/docs/ADVANCED_USAGE.md">Advanced Usage</a>
  |
  <a href="https://github.com/valkyoth/sanitization/blob/main/docs/THREAT_MODEL.md">Threat Model</a>
  |
  <a href="https://github.com/valkyoth/sanitization/blob/main/docs/GUARANTEES.md">Guarantees</a>
  |
  <a href="https://github.com/valkyoth/sanitization/blob/main/docs/NON_GUARANTEES.md">Non-Guarantees</a>
  |
  <a href="https://github.com/valkyoth/sanitization/blob/main/SECURITY.md">Security</a>
</div>

<br>

<p align="center">
  <a href="https://github.com/valkyoth/sanitization">
    <img src="https://raw.githubusercontent.com/valkyoth/sanitization/main/.github/images/sanitization.webp" alt="sanitization Rust crate overview">
  </a>
</p>

# sanitization

`sanitization` provides redacted, non-`Copy`, clear-on-drop secret containers
for Rust. The default crate is dependency-free and `no_std`; heap storage,
derive macros, ecosystem interop, memory locking, guard pages, and other
hardening are explicit opt-ins.

Every crate-owned clearing path reaches one audited internal volatile-write
backend. The crate reduces accidental retention and exposure. It does not make
an entire process, operating system, compiler, or hardware platform secret.

The 2.0 line has intentional breaking changes. Existing 1.x users should read
the [2.0 migration guide](https://github.com/valkyoth/sanitization/blob/main/docs/MIGRATION_2.0.md).

## Start Here

Choose the narrowest type that matches the storage requirement:

| Need | Start with |
| --- | --- |
| Fixed-size key, nonce, or token | `SecretBytes<N>` |
| Runtime-length bytes with fixed capacity | `SecretBoxBytes` with `alloc` |
| Growable secret bytes | `SecretVec` with `alloc` |
| Secret UTF-8 text | `SecretString` with `alloc` |
| Untrusted input with a public maximum | `BoundedSecretVec<MAX>` or `BoundedSecretString<MAX>` |
| Fallible generated dynamic input | `try_from_fn_bounded` or `try_from_chars_bounded` |
| Custom value needing current-value clearing only | `Secret<T>` where `T: SecureSanitize` |
| Generic shared exposure with reviewed stable storage | `Secret<T>` where `T: StableSharedSecretStorage` |
| Generic mutable exposure with reviewed stable storage | `Secret<T>` where `T: StableMutableSecretStorage` |
| Closed production storage allow-list | `AllowlistedSecret<T, PrivatePolicy>` |
| Fixed key that should avoid swap/pagefiles | `LockedSecretBytes<N>` with `memory-lock` |
| Dynamic locked bytes or text | `LockedSecretVec` or `LockedSecretString` |
| Permanently bounded locked bytes or text | `BoundedLockedSecretVec<MAX>` or `BoundedLockedSecretString<MAX>` |
| Many same-size locked keys | `SecretPool<N, SLOTS>` |
| Guarded dynamic mapping | `GuardedSecretVec` or `GuardedSecretString` |
| Permanently bounded guarded bytes or text | `BoundedGuardedSecretVec<MAX>` or `BoundedGuardedSecretString<MAX>` |
| Fixed mapping sealed between scoped accesses | `SealedSecretBytes<N>` with `page-seal` |
| Secret-derived control flow | `ct::Choice`, fixed CT helpers, and explicit declassification |
| One successful scoped access | `ConsumeOnceSecret<T>` |
| Existing ordinary byte storage | `sanitization::wipe` |
| Existing ecosystem trait bounds | `zeroize-interop`, `subtle-interop`, or a companion crate |

Use the API in three levels:

1. **Essentials:** ordinary owned containers and direct wiping.
2. **Protected operations:** data-oblivious comparison and recommended locked storage.
3. **Advanced hardening:** custom protection policy, guard pages, sealing,
   classified CT ownership, cache/register controls, and specialized backends.

Most applications should begin at level 1 and add only the level 2 controls
required by their threat model.

## Install

Fixed-size `no_std` secrets need no feature flags:

```toml
[dependencies]
sanitization = "2.0.3"
```

Heap-backed byte and text containers:

```toml
[dependencies]
sanitization = { version = "2.0.3", features = ["alloc"] }
```

Recommended native hardening profile:

```toml
[dependencies]
sanitization = { version = "2.0.3", features = ["profile-hardened-native"] }
```

This profile includes OS-random canaries and `strict-canary-check`. Enabling
`canary-check` alone uses deterministic address-derived words intended to catch
accidental corruption; it is not an attacker-resistant integrity control.

Optional derives:

```toml
[dependencies]
sanitization = { version = "2.0.3", features = ["derive"] }
```

See the complete [feature reference](https://github.com/valkyoth/sanitization/blob/main/docs/FEATURES.md) before combining
platform features.

## Level 1: Essential Secret Ownership

### Fixed-Size Secrets

Use `SecretBytes<N>` when the size is known at compile time. Generate directly
into the container where possible instead of first creating an ordinary secret
buffer.

```rust
use sanitization::SecretBytes;

let mut key = SecretBytes::<32>::from_fn(|index| index as u8);

let first = key.expose_secret(|bytes| bytes[0]);
assert_eq!(first, 0);
assert!(key.constant_time_eq(&[
    0, 1, 2, 3, 4, 5, 6, 7,
    8, 9, 10, 11, 12, 13, 14, 15,
    16, 17, 18, 19, 20, 21, 22, 23,
    24, 25, 26, 27, 28, 29, 30, 31,
]));

key.replace_from_fn(|index| 31 - index as u8);
key.secure_clear();
assert!(key.constant_time_eq(&[0; 32]));
```

`SecretBytes<N>` does not implement `Clone`, `Copy`, `Deref`, `AsRef<[u8]>`,
or secret-printing `Debug`. `expose_secret` directly borrows owned storage. Use
the reason-bearing `export_secret_copy` only when an external API requires an
independent temporary copy.

```rust
use sanitization::SecretBytes;

let key = SecretBytes::<32>::from_array([7; 32]);
let public_identifier = key.export_secret_copy(
    "protocol callback returns public key identifier byte",
    |bytes| bytes[0],
);
assert_eq!(public_identifier, 7);
```

The crate clears that temporary on normal return and unwinding, but cannot
clear copies made by the callback or copies surviving process abort.

### Heap Bytes And Text

Enable `alloc` for runtime-length secrets:

```rust
use sanitization::{SecretBoxBytes, SecretString, SecretVec};

let fixed = SecretBoxBytes::from_slice(b"fixed-token");
assert!(fixed.constant_time_eq(b"fixed-token"));

let mut bytes = SecretVec::from_slice(b"session-key");
bytes.extend_from_slice(b"-v2");
assert!(bytes.constant_time_eq(b"session-key-v2"));

let mut text = SecretString::from_secret_str("bearer-token");
text.push_str("-v2");
assert!(text.constant_time_eq("bearer-token-v2"));

let generated = SecretVec::try_from_fn_bounded(32, 4096, |index| {
    Ok::<u8, &'static str>(index as u8)
})?;
assert_eq!(generated.len(), 32);
# Ok::<(), sanitization::SecretGenerateError<&'static str>>(())
```

Choose deliberately:

- `SecretBoxBytes` has one runtime-length allocation whose capacity cannot grow.
- `SecretVec` and `SecretString` use managed growth that clears replaced
  allocations before releasing them.
- bounded variants reject public lengths above `MAX`, including during serde
  ingestion.
- `try_with_capacity` returns `SecretAllocationError`; generated constructors
  return `SecretGenerateError<E>` so allocation and generator failures remain
  distinct.
- `try_from_slice_bounded` and `try_from_secret_str_bounded` enforce public byte
  limits before allocation. `try_from_fn_bounded` does the same for byte count;
  `try_from_chars_bounded` checks worst-case UTF-8 byte capacity with checked
  arithmetic before allocation or callback execution.
- infallible `with_capacity`, `from_fn`, and `from_chars` are for trusted,
  already-bounded public sizes and retain ordinary allocation panic/abort
  behavior.
- ownership-taking constructors such as `from_vec` and `from_string` avoid a
  second heap allocation but make the supplied allocation secret storage.

### Direct Wiping

Use the sealed `wipe` module only when a secret already exists in ordinary
supported storage:

```rust
use sanitization::wipe;

let mut bytes = [0xA5; 32];
wipe::bytes(&mut bytes);
assert_eq!(bytes, [0; 32]);
```

Use `wipe::maybe_uninit` for non-live `MaybeUninit<T>` storage such as an
allocator or fixed-capacity container's spare slots. It performs volatile
writes without constructing references to uninitialized byte values.

With `alloc`, `wipe::vec` and `wipe::string` clear the reachable allocation.
`WipeOnDrop<T>` is available only for audited built-in byte/text types. Custom
structured types should implement `SecureSanitize` and use `Secret<T>` or the
optional derives.

### Custom Structs

The `derive` feature generates field-wise struct sanitization:

```rust
use sanitization::{SecretBytes, SecureSanitize, SecureSanitizeOnDrop};

#[derive(SecureSanitize, SecureSanitizeOnDrop)]
struct Credentials {
    key: SecretBytes<32>,
    nonce: SecretBytes<12>,
}
```

`SecureSanitizeOnDrop` requires `DropSafeSanitize + Unpin` and invokes the
complete sanitizer, preserving aggregate cleanup such as external storage and
ordering. `#[derive(SecureSanitize)]` supplies the drop-safe contract for its
generated field-wise sanitizer. A reviewed manual aggregate sanitizer must
implement `DropSafeSanitize` explicitly. For generic drop structs, declare
`T: SecureSanitize + Unpin` on the struct itself. Pinned secret owners require
a reviewed pin-aware manual design.

Enum derives are rejected. Safe generated code cannot reach inactive enum
representation bytes after variant transitions. Model secret storage with a
stable struct layout and keep public state in a separate tag.

## Level 2: Protected Operations

### Data-Oblivious Final Decisions

The native `ct` module aims to avoid secret-dependent control flow and memory
access under documented target and compiler conditions. It does not promise
identical wall-clock timing on every machine.

For a final fixed-size authentication decision, use the reason-bearing helper:

```rust
use sanitization::ct::declassified_eq_fixed;

let expected = [7u8; 32];
let received = [7u8; 32];

let accepted = declassified_eq_fixed(
    &expected,
    &received,
    "authentication comparison result is public",
);
assert!(accepted);
```

Use `eq_fixed`, `cmp_fixed`, and `Choice` when the result must remain inside
the data-oblivious domain for further composition. `eq_public_len` and
`declassified_eq_public_len` treat length as public and may return immediately
on mismatch.

Declassification reasons are review labels, not authorization. This repository
runs `scripts/lint-declassification-reasons.py` to reject dynamic, placeholder,
and generic reasons. High-assurance downstream projects can run the same lint.

### Locked Native Storage

`memory-lock` provides private native mappings backed by `mlock`/`VirtualLock`
or platform equivalents. Generate or decode directly into locked storage when
possible:

```rust,no_run
# #[cfg(feature = "memory-lock")]
# {
use sanitization::LockedSecretBytes;

let mut key = LockedSecretBytes::<32>::try_from_fill(|output| {
    output.fill(7);
    Ok::<(), std::io::Error>(())
})?;

assert_eq!(key.try_constant_time_eq(&[7; 32]), Ok(true));
key.try_replace_from_fallible_fill(|output| {
    output.fill(9);
    Ok::<(), std::io::Error>(())
})?;
# Ok::<(), Box<dyn std::error::Error>>(())
# }
```

When a custom protection request is needed, initialize the resulting mapping
without an intermediate array:

```rust,no_run
# #[cfg(feature = "memory-lock")]
# {
use sanitization::{LockedSecretBytes, ProtectionRequest};

let request = ProtectionRequest::profile_hardened_native();
let key = LockedSecretBytes::<32>::zeroed_with_protection(request)?
    .try_init_with(|output| {
        // Decode, derive, or ask an RNG to write directly into `output`.
        output.fill(7);
        Ok::<(), std::io::Error>(())
    })?;

assert_eq!(key.try_constant_time_eq(&[7; 32]), Ok(true));
# Ok::<(), Box<dyn std::error::Error>>(())
# }
```

Runtime-length decoders should use the policy-aware dynamic constructor. The
mapping and every `Required` control are established before the callback runs;
decoder failure, excessive output length, invalid integrity canaries, and the
unreported tail are cleared before an error or value is returned. The callback
receives exactly the requested capacity, initialized to zero and without live
canary material:

```rust,no_run
# #[cfg(feature = "memory-lock")]
# {
use sanitization::{
    BoundedLockedSecretVec, ForkProtectionRequest, ProtectionRequest, Requirement,
};

const MAX_DECODED_SECRET_BYTES: usize = 4096;
let decoder_capacity = 4096; // Validate the protocol length before this point.
let request = ProtectionRequest {
    memory_lock: Requirement::Required,
    dump_exclusion: Requirement::Required,
    fork: ForkProtectionRequest::exclude(Requirement::Required),
    guard_pages: Requirement::NotRequested,
    canary: Requirement::Required,
    cache_policy: Requirement::NotRequested,
};
let decoded = BoundedLockedSecretVec::<MAX_DECODED_SECRET_BYTES>::try_from_capacity_with_protection(
    decoder_capacity,
    request,
    |output| {
        output[..5].copy_from_slice(b"token");
        Ok::<usize, std::io::Error>(5)
    },
)?;

assert_eq!(decoded.try_constant_time_eq(b"token"), Ok(true));
# Ok::<(), Box<dyn std::error::Error>>(())
# }
```

`BoundedGuardedSecretVec<MAX>` provides the same lifetime invariant with guard
pages. `BoundedLockedSecretString<MAX>` and
`BoundedGuardedSecretString<MAX>` enforce the same permanent maximum in UTF-8
bytes and clear the mapping when the initialized prefix is not valid UTF-8.

The one-shot `*_bounded_with_protection` constructors on the growable mapped
types enforce admission only; the returned `LockedSecretVec`,
`GuardedSecretVec`, or string wrapper can still grow later. Use the const-generic
bounded mapped types when the maximum must remain enforced for the value's
entire lifetime. They keep the growable owner private and check construction,
append, and replacement before allocation or generator execution. Unbounded
constructors accept only trusted, already-limited capacities.

`profile_hardened_native()` permits explicit degraded success for dump and
fork exclusion; use a custom request like the one above when plaintext must not
be materialized without them.

Prefer direct final-storage generation through `try_from_fn`, `try_from_fill`,
or `try_init_with`. Avoid `Clone`, `to_vec`, formatting, and temporary arrays
for secret material. Keep unavoidable cryptographic scratch buffers in
clear-on-drop owners, and keep mutable exposure closures as short as possible.
These practices reduce avoidable copies; they cannot prove that compiler moves
or register spills never create historical copies.

For the reviewed native bundle, enable `profile-hardened-native` and use its
type-associated constructor:

```rust,no_run
# #[cfg(feature = "profile-hardened-native")]
# {
use sanitization::LockedSecretBytes;

let key = LockedSecretBytes::<32>::zeroed_hardened_native()?;
let request = key.protection_request();

if !key.protection_report().satisfies(request) {
    return Err("preferred runtime protections were unavailable".into());
}
# Ok::<(), Box<dyn std::error::Error>>(())
# }
```

Cargo features compile capabilities; they do not prove that runtime controls
succeeded. Required failures return `ProtectionError`. Preferred failures are
visible in `ProtectionReport`. Validate the report once at startup according
to deployment policy.

Locked memory does not by itself control hibernation images, every crash-dump
path, privileged reads, DMA, firmware, or external copies. Platform coverage
and fork/dump behavior are documented in
[`TARGETS.md`](https://github.com/valkyoth/sanitization/blob/main/docs/TARGETS.md) and
[`PROTECTION_REPORT.md`](https://github.com/valkyoth/sanitization/blob/main/docs/PROTECTION_REPORT.md).

## Level 3: Advanced Hardening

Advanced facilities are intentionally separate from the normal path:

| Requirement | Facility | Read first |
| --- | --- | --- |
| Custom required/preferred runtime controls | `ProtectionRequest` and `ProtectionReport` | [Protection reports]https://github.com/valkyoth/sanitization/blob/main/docs/PROTECTION_REPORT.md |
| Guard pages around dynamic storage | `GuardedSecretVec`, `GuardedSecretString` | [Advanced usage]https://github.com/valkyoth/sanitization/blob/main/docs/ADVANCED_USAGE.md |
| Inaccessible pages between accesses | `SealedSecretBytes<N>` | [Safety]https://github.com/valkyoth/sanitization/blob/main/docs/SAFETY.md |
| Secret-bearing optional/result CT state | `SecretValue`, `SecretCtOption`, `SecretCtResult` | [Guarantees]https://github.com/valkyoth/sanitization/blob/main/docs/GUARANTEES.md |
| Cache-line eviction after clearing | `cache-flush` | [Barrier strategy]https://github.com/valkyoth/sanitization/blob/main/docs/BARRIERS.md |
| Best-effort vector-register clearing | `register-scrub` | [Barrier strategy]https://github.com/valkyoth/sanitization/blob/main/docs/BARRIERS.md |
| Many fixed secrets under one lock quota | `SecretPool<N, SLOTS>` | [Advanced usage]https://github.com/valkyoth/sanitization/blob/main/docs/ADVANCED_USAGE.md |
| N-of-N fixed split storage | `SplitSecretBytes<N, SHARES>` | [Threat model]https://github.com/valkyoth/sanitization/blob/main/docs/THREAT_MODEL.md |
| HSM, TEE, enclave, or keystore adapters | `hardware-secrets` traits | [Advanced usage]https://github.com/valkyoth/sanitization/blob/main/docs/ADVANCED_USAGE.md |

Do not enable advanced features merely because they sound stronger. Each one
has separate target assumptions, failure modes, and residual risks. The
[advanced usage guide](https://github.com/valkyoth/sanitization/blob/main/docs/ADVANCED_USAGE.md) provides short recipes and links
to the normative documents.

Page-sealed callers that must observe final mapping cleanup should call
`SealedSecretBytes::try_close()` before drop. It reports page normalization,
unlock, and unmap failures without exposing bytes, addresses, or canary values;
cleanup makes each page writable, erases it, and reseals it independently.
If any page transition fails, successfully processed pages are already erased
and resealed, while the uncertain page, mapping, and any established lock are
retained without attempting unmap. `Drop` follows the same security-first
retention policy and remains the final best-effort fallback.

High-assurance applications should use `AllowlistedSecret<T, P>` as their
internal production alias, keep `P` private or `pub(crate)`, and run
`scripts/lint-storage-policies.py` over sensitive modules. The lint rejects
direct `Secret<T>`, unapproved storage-marker implementations, and public
policy types. See [`STORAGE_CONTRACTS.md`](https://github.com/valkyoth/sanitization/blob/main/docs/STORAGE_CONTRACTS.md) and the
compile-checked `high_assurance_policy` example.

## Feature And Platform Reference

The dependency-free, `no_std` default enables `asm-compare`, selecting the
reviewed equal-length assembly backend on x86_64 and AArch64 and falling back
portably elsewhere. `default-features = false` disables that default backend.
The other major opt-in groups are:

- allocation and integration: `alloc`, `std`, `derive`, `serde`,
  `zeroize-interop`, `subtle-interop`;
- mapped storage: `memory-lock`, `guard-pages`, `page-seal`, `canary-check`,
  `random-canary`;
- CT and post-use controls: `asm-compare`, `strict-compare`, `cache-flush`,
  `register-scrub`;
- named profiles: `profile-hardened-native`, `profile-guarded-native`, and
  `profile-hardened-linux`.

See [FEATURES.md](https://github.com/valkyoth/sanitization/blob/main/docs/FEATURES.md) for every feature, implication, companion
crate, and profile policy. Native profiles fail to compile on WASM rather than
silently degrading.

WASM compatibility is explicit and reduced-guarantee. Pair `memory-lock` with
`wasm-compat` only when API compatibility is required; WASM linear memory has
no host `mlock`, `mprotect`, fork policy, or native volatile guarantee across a
JIT boundary. See [FEATURE_PROFILES.md](https://github.com/valkyoth/sanitization/blob/main/docs/FEATURE_PROFILES.md).

## Trust Dashboard

| Area | Status |
| --- | --- |
| License | `MIT OR Apache-2.0` |
| MSRV | Rust `1.90.0` |
| Pinned toolchain | Rust `1.97.1` |
| Default target | `no_std` |
| Default external runtime dependencies | zero |
| Unsafe policy | denied at crate root and isolated in documented modules |
| Clear primitive | volatile writes by default |
| Proc macros | optional `derive` feature |
| Formal evidence | bounded Kani harnesses for selected properties |
| Main guarantee | narrow ownership, redaction, and clear-on-drop hygiene |

High-assurance users should read these documents in order:

1. [Threat model]https://github.com/valkyoth/sanitization/blob/main/docs/THREAT_MODEL.md
2. [Guarantees]https://github.com/valkyoth/sanitization/blob/main/docs/GUARANTEES.md
3. [Non-guarantees]https://github.com/valkyoth/sanitization/blob/main/docs/NON_GUARANTEES.md
4. [Safety and unsafe boundaries]https://github.com/valkyoth/sanitization/blob/main/docs/SAFETY.md
5. [Target tiers]https://github.com/valkyoth/sanitization/blob/main/docs/TARGETS.md
6. [Evidence]https://github.com/valkyoth/sanitization/blob/main/docs/EVIDENCE.md
7. [Error handling]https://github.com/valkyoth/sanitization/blob/main/docs/ERROR_HANDLING.md
8. [Deployment hardening]https://github.com/valkyoth/sanitization/blob/main/docs/DEPLOYMENT_HARDENING.md

## Rust Version Support

The MSRV is Rust `1.90.0`. Release development is pinned to Rust `1.97.1`, and
the release gate checks compatibility from `1.90.0` through the pinned stable
toolchain. The online release preflight also verifies that the pin is the
current stable patch release without changing the MSRV.

## Ecosystem Integration

The core crate remains dependency-free by default. Opt-in integration is split
by ownership boundary:

| Integration | Use |
| --- | --- |
| `sanitization-derive` | Struct derives for sanitization and conservative CT traits |
| `zeroize-interop` | Existing APIs requiring `zeroize` trait bounds |
| `subtle-interop` | Existing APIs requiring `subtle::ConstantTimeEq` |
| `sanitization-arrayvec` | `ArrayVec` storage |
| `sanitization-bytes` | Fixed-capacity `BytesMut` storage |
| `sanitization-crypto-interop` | SHA-2/BLAKE3 cleanup wrappers and HMAC-SHA2 helpers |

The optional derive dependency is exact-pinned to the runtime's version because
generated code may reference runtime traits introduced by that same release.
The release script publishes and waits for the derive crate before the core.

`zeroize` remains broader for retrofitting existing ecosystem types.
`sanitization` focuses on ownership, lifecycle, explicit exposure, and optional
platform protection. Interop features bridge trait bounds without replacing the
core clearing backend.

## Verification And Release Checks

Run the local gate before release-sensitive changes:

```bash
scripts/checks.sh
```

It covers formatting, feature matrices, docs, examples, linting, negative
fixtures, downstream migration, codegen inspection, leakage smoke checks,
Loom, lifecycle probes, API snapshots, package archives, and optional Kani/Miri
when installed. Native target evidence and timing runs are documented in
[`EVIDENCE.md`](https://github.com/valkyoth/sanitization/blob/main/docs/EVIDENCE.md) and
[`LEAKAGE_TESTS.md`](https://github.com/valkyoth/sanitization/blob/main/docs/LEAKAGE_TESTS.md).

Release publication is staged through:

```bash
scripts/release_crates.py --version 2.0.3 --prepare-only
scripts/release_crates.py --require-tag
```

The script publishes the five crates in dependency order and pauses while
crates.io indexes dependencies.

## Limits

Important limits include:

- safe Rust cannot reliably scrub historical stack frames or compiler-created
  copies;
- destructors do not run after process abort;
- exposure closures and external libraries can copy, log, export, or retain
  secret data;
- memory locking does not solve every swap, hibernation, dump, debugger,
  privileged-read, DMA, firmware, or VM snapshot threat;
- data-oblivious source structure is not a universal hardware timing guarantee;
- cache flush and register scrub helpers cover only their documented target
  subsets;
- Core Miri unit tests model locked-container lifecycle and verify
  clear-before-release, but do not prove native OS protection. Downstream Miri
  uses portable comparison code, while mapped constructors remain unsupported;
  Kani does not prove real concurrency.

See [NON_GUARANTEES.md](https://github.com/valkyoth/sanitization/blob/main/docs/NON_GUARANTEES.md),
[THREAT_MODEL.md](https://github.com/valkyoth/sanitization/blob/main/docs/THREAT_MODEL.md), and [SECURITY.md](https://github.com/valkyoth/sanitization/blob/main/SECURITY.md) before
using this crate for high-assurance secret handling.