tc_zeroize 0.1.0

Explicit memory erasure with volatile writes and opt-in scope guards.
Documentation
  • Coverage
  • 100%
    5 out of 5 items documented4 out of 4 items with examples
  • Size
  • Source code size: 45.11 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 235.33 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 2s Average build duration of successful builds.
  • all releases: 2s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • TomiCheng/tc_core
    0 0 2
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • TomiCheng

tc_zeroize

crates.io docs.rs CI license rustc

Explicit memory erasure with volatile writes and opt-in scope guards. The crate is no_std and has no dependencies. A single, default-off alloc feature adds heap-backed containers through the sysroot alloc crate. Its primitive implementations use small, documented unsafe blocks for volatile writes.

Requires Rust 1.85 or later (edition 2024).

API

Item Contract
Zeroize::zeroize(&mut self) Explicitly erases the contents of the current value
ZeroizeOnDrop Marks a type whose own destructor performs erasure; the marker generates no behavior
Zeroizing::new(value) Owns a value and calls its zeroize before dropping it
Deref / DerefMut on Zeroizing<T> Borrows the guarded value for ordinary operations
Types Result of zeroize
u8, u16, u32, u64, u128, usize Zero
i8, i16, i32, i64, i128, isize Zero
bool false
char '\0'
[T; N], [T] where T: Zeroize Every element is cleared; length is preserved
Option<T> where T: Zeroize A present payload is cleared, then dropped, and the option becomes None
MaybeUninit<T> for any T A typed volatile zero store clears storage, excluding any padding guarantee; the slot remains logically uninitialized
Vec<T> where T: Zeroize (alloc) Live elements are cleared and dropped, then the current allocation including spare capacity is cleared; length becomes zero and capacity is retained, subject to the padding limitation
Box<T> where T: Zeroize + ?Sized (alloc) The contents are cleared in place; the box and allocation are retained, including the length of a boxed slice

Empty arrays and slices are supported. Slice lengths are public. Array and slice implementations visit every element and inherit the erasure behavior of T. An option branches on whether a value is present. This is not a constant-time API.

Features

Features Support
None (default) Core-only no_std: primitives, arrays, slices, options, MaybeUninit, and scope guards
alloc All core-only support plus Vec<T> and Box<T: ?Sized>; still no_std, with no external dependencies

String, VecDeque, BTreeMap, and Cow are not supported.

Capability and policy

Zeroize provides a capability, not a mandatory lifetime policy. General-purpose storage types do not know whether their contents are secret; they should offer explicit erasure without automatically imposing cleanup on all uses.

Types that know they hold secrets, such as private-key containers and private RSA engines, can choose the ZeroizeOnDrop policy. Implementors must write their own Drop implementation and call Zeroize::zeroize there. The marker alone does not enforce or implement this behavior.

Zeroizing<T> applies that policy to a local value. The guard implements neither Clone nor Copy. Dereferencing it can still allow a caller to copy or clone the inner value; those separate values are not guarded. A big-integer type can implement and re-export the erasure API; a private-key container can call Zeroize::zeroize from its own destructor and guard local blinding values with Zeroizing.

Usage

Add the crate to your Cargo.toml:

[dependencies]
tc_zeroize = "0.1.0"

Heap-backed containers need the default-off alloc feature:

[dependencies]
tc_zeroize = { version = "0.1.0", features = ["alloc"] }
use tc_zeroize::{Zeroize, Zeroizing};

let mut scratch = [1_u8, 2, 3, 4];
scratch.zeroize();
assert_eq!(scratch, [0; 4]);

let mut optional = Some([7_u32, 9]);
optional.zeroize();
assert_eq!(optional, None);

{
    let mut secret = Zeroizing::new([42_u8; 32]);
    secret[0] = 7;
    assert_eq!(secret[0], 7);
} // The guard clears its current array before dropping it.

API documentation also has executable examples for implementing both traits.

Mechanism and limitations

Primitive erasure uses core::ptr::write_volatile to prevent deletion of the stores and ends with compiler_fence(Ordering::SeqCst) to constrain compiler reordering. Composite implementations delegate to their contents. Every unsafe block documents pointer validity and the validity of the replacement value.

These operations do not flush caches or supply a hardware memory barrier. They do not erase copies left elsewhere by the compiler or operating system, such as registers, stack spills, swap, or core dumps. Padding bytes are not covered.

Copy values permit implicit copies on by-value use. Clearing one binding does not clear other copies; a Copy fixed-width integer type is one example. A Copy type cannot implement Drop, so it cannot perform its own scope-exit cleanup. Even moves of non-Copy types can leave bytes at an old location. Zeroizing clears only its current contents, not those old copies.

With alloc, Vec<T>::zeroize clears live elements before their destructors run, then clears the entire current allocation, including spare capacity. Typed volatile stores do not guarantee erasure of padding in T; padding-free bytes and integer limbs are fully covered. The empty vector retains its capacity. This covers the current allocation, not every buffer used during its lifetime: growth, shrink_to_fit, and into_boxed_slice can leave inaccessible old buffers. Reserve enough capacity before storing secrets to avoid this gap. A Box<[T]> is a good fit for fixed-size secret storage because its allocation has no spare capacity and does not grow; converting an existing vector can still reallocate.

Drop-based cleanup requires the destructor to run. Forgetting or leaking the guard, or aborting the process, bypasses cleanup. A panicking custom zeroize implementation can also leave a composite value partially cleared.

Validation

Integration tests import the public API and cover every supported primitive, empty and nested containers, non-Copy elements, slice boundaries, clearing before a payload's destructor, and guard cleanup on scope exit and unwinding. With alloc, tests also check spare capacity in still-live vector allocations, element destruction order, boxed slices, and nested vectors. Crate and type documentation contains executable examples. Missing public docs and unsafe operations without explicit unsafe blocks in unsafe functions are rejected by crate-level lints.

Functional tests establish resulting values and cleanup order. They do not prove that a compiler preserves the wipe; generated code must be inspected for the target compiler and hardware when validating that property.

Run these commands from the workspace root:

cargo test -p tc_zeroize --locked
cargo test -p tc_zeroize --locked --features alloc
cargo clippy -p tc_zeroize --all-targets --all-features --locked -- -D warnings
cargo fmt -p tc_zeroize --check
cargo doc -p tc_zeroize --no-deps --all-features --locked

Before a release, check the archive contents and run publication validation from a committed checkout:

cargo package -p tc_zeroize --list --locked
cargo publish -p tc_zeroize --dry-run --locked

The archive includes both license texts, this README, the changelog, the source, and integration tests. It must not include target/ or other build artifacts. The publication dry run packages and verifies the crate without uploading it.

License

Licensed under either the MIT license or the Apache License, Version 2.0, at your option.