All skills
Skillintermediate

Rust Zeroization Patterns Reference

This reference documents vulnerability pattern detected by the zeroize-audit tooling for Rust code. Each entry includes: what the flaw is, which tool detects it, severity, category, a minimal Rust snippet showing the bug, and a recommended fix.

Claude Code Knowledge Pack7/10/2026

Overview

Rust Zeroization Patterns Reference

This reference documents vulnerability pattern detected by the zeroize-audit tooling for Rust code. Each entry includes: what the flaw is, which tool detects it, severity, category, a minimal Rust snippet showing the bug, and a recommended fix.


Section A — Semantic Patterns (semantic_audit.py, rustdoc JSON-based)

These patterns are detectable from rustdoc JSON without executing the compiler. semantic_audit.py processes trait impls, derives, and field types from the rustdoc index.


A1 — #[derive(Copy)] on Sensitive Type

Category: SECRET_COPY | Severity: critical

Why it's dangerous: Copy types are bitwise-duplicated on every assignment, function call, and return. No Drop ever runs — the type cannot implement Drop. Every copy is a silent, untracked duplicate that will never be zeroed.

// BAD: every assignment silently duplicates the secret
#[derive(Copy, Clone)]
pub struct CopySecret {
    data: [u8; 32],
}

fn use_key(key: CopySecret) {  // <-- full copy here
    // original still on stack, unzeroed
}

Fix: Remove Copy. Use Clone explicitly where needed and ensure all clones are tracked and zeroed.


A2 — No Zeroize, ZeroizeOnDrop, or Drop

Category: MISSING_SOURCE_ZEROIZE | Severity: high

Why it's dangerous: When the type goes out of scope, Rust calls drop_in_place which simply frees the memory without zeroing it. The secret bytes remain in the freed heap or on the stack until overwritten by future allocations.

// BAD: no cleanup whatsoever
pub struct UnprotectedKey {
    bytes: Vec<u8>,
}

fn example() {
    let key = UnprotectedKey { bytes: vec![0x42; 32] };
    // key drops here — heap bytes never zeroed
}

Fix: Add #[derive(ZeroizeOnDrop)] (with zeroize crate) or implement Drop calling .zeroize() on all fields.


A3 — Zeroize Impl Without Auto-Trigger

Category: MISSING_SOURCE_ZEROIZE | Severity: high

Why it's dangerous: The Zeroize trait provides a .zeroize() method, but it requires explicit invocation. If no Drop or ZeroizeOnDrop calls it, the zeroing never happens automatically when the value goes out of scope.

use zeroize::Zeroize;

// BAD: Zeroize is implemented but never called on drop
pub struct ManualZeroizeToken {
    bytes: Vec<u8>,
}

impl Zeroize for ManualZeroizeToken {
    fn zeroize(&mut self) {
        self.bytes.zeroize();
    }
}

fn example() {
    let token = ManualZeroizeToken { bytes: vec![0x42; 32] };
    // token drops here — zeroize() is NEVER called
}

Fix: Add #[derive(ZeroizeOnDrop)] alongside Zeroize, or add an explicit Drop impl that calls self.zeroize().


A4 — Drop Impl Missing Secret Fields

Category: PARTIAL_WIPE | Severity: high

Why it's dangerous: The struct has multiple sensitive fields, but the Drop impl only zeroes some of them. The unzeroed fields remain in memory after the struct is freed.

// BAD: Drop impl zeroes `secret` but forgets `token`
pub struct ApiSecret {
    secret: Vec<u8>,
    token: Vec<u8>,  // <-- never zeroed
}

impl Drop for ApiSecret {
    fn drop(&mut self) {
        self.secret.zeroize();
        // self.token is NOT zeroed
    }
}

Fix: Ensure Drop calls .zeroize() on every sensitive field, or use #[derive(ZeroizeOnDrop)] to zero all fields automatically.


A5 — ZeroizeOnDrop on Struct with Heap Fields

Category: PARTIAL_WIPE | Severity: medium

Why it's dangerous: ZeroizeOnDrop zeros all fields via the Zeroize implementation, but Vec zeroes only len bytes, not the full allocated capacity. Excess capacity bytes remain readable until the allocator reclaims them.

use zeroize::ZeroizeOnDrop;

// BAD: ZeroizeOnDrop zeros len bytes but capacity tail is untouched
#[derive(ZeroizeOnDrop)]
pub struct SessionKey {
    data: Vec<u8>,
}

fn example() {
    let mut key = SessionKey { data: Vec::with_capacity(64) };
    key.data.extend_from_slice(&[0x42; 32]);
    // capacity[32..64] bytes never zeroed
}

Fix: Use Zeroizing<Vec<u8>> which uses zeroize_and_drop for the full buffer, or manually self.data.zeroize(); self.data.shrink_to_fit() in Drop.


A6 — ManuallyDrop Struct Field

Category: MISSING_SOURCE_ZEROIZE | Severity: critical

Why it's dangerous: ManuallyDrop inhibits automatic drop for the wrapped value. Rust will never call Drop on a ManuallyDrop field unless ManuallyDrop::drop() is called explicitly. If the containing struct's Drop impl does not explicitly drop and zero the field, the secret bytes are never wiped.

use std::mem::ManuallyDrop;

// BAD: Drop is never called on `key` field automatically
pub struct SecretHolder {
    key: ManuallyDrop<Vec<u8>>,
}

// When SecretHolder drops, `key` is NOT zeroed — bytes stay in heap

Fix: Implement Drop for SecretHolder that explicitly calls self.key.zeroize() (if Vec<u8> implements Zeroize) and then unsafe { ManuallyDrop::drop(&mut self.key) }.


A7 — #[derive(Clone)] on Zeroizing Type

Category: SECRET_COPY | Severity: medium

Why it's dangerous: Each clone() call creates an independent heap allocation containing the same secret bytes. The clone must be independently zeroed. If callers pass clones to functions that don't zero them on return, the secret escapes the zeroing lifecycle.

// BAD: clone() creates an untracked duplicate that may not be zeroed
#[derive(Clone)]
pub struct CloneableKey {
    bytes: Vec<u8>,
}

impl Drop for CloneableKey {
    fn drop(&mut self) { self.bytes.zeroize(); }
}

fn bad_caller(key: &CloneableKey) {
    let copy = key.clone(); // a new heap allocation
    do_something_with(copy); // copy may not be zeroed on return from do_something_with
}

Fix: Remove Clone if not needed. If cloning is required, document that all clones must implement the same zeroization lifecycle.


A8 — From / Into to Non-Zeroizing Type

Category: SECRET_COPY | Severity: medium

Why it's dangerous: A From/Into conversion transfers the secret bytes into a type that does not implement ZeroizeOnDrop or Drop. The original may be zeroed but the converted value escapes without zeroization guarantees.

type RawBytes = Vec<u8>;  // type alias — does NOT implement ZeroizeOnDrop

pub struct ApiSecret {
    secret: Vec<u8>,
    token: RawBytes,
}

// BAD: From converts secret into a plain Vec with no zeroing
impl From for ApiSecret {
    fn from(token: RawBytes) -> Self {
        ApiSecret { secret: vec![], token }
    }
}
// The returned ApiSecret has no Drop/Zeroize impl

Fix: Ensure the target type of From/Into also implements ZeroizeOnDrop, or wrap in Zeroizing.


A9 — ptr::write_bytes Without compiler_fence

Category: OPTIMIZED_AWAY_ZEROIZE | Severity: medium

Why it's dangerous: ptr::write_bytes is a non-volatile memory write. If the compiler determines the memory is never read afterwards (classic dead-store elimination), it may remove the write entirely. Unlike volatile_set_memory, there is no compiler barrier to prevent this.

use std::ptr;

pub struct WriteBytesSecret {
    data: [u8; 32],
}

fn wipe_insecure(s: &mut WriteBytesSecret) {
    // BAD: compiler may eliminate this as a dead store
    unsafe {
        ptr::write_bytes(s as *mut WriteBytesSecret, 0, 1);
    }
}
// No compiler_fence — wipe is DSE-vulnerable

Fix: Add std::sync::atomic::compiler_fence(std::sync::atomic::Ordering::SeqCst) after the write, or use zeroize::Zeroize which is DSE-resistant by design.


A10 — #[cfg(feature)] Wrapping Drop or Zeroize Impl

Category: NOT_ON_ALL_PATHS | Severity: medium

Why it's dangerous: When the controlling feature flag is disabled, the cleanup impl is compiled out entirely. Code built without the feature silently loses all zeroization, with no compile error or warning.

pub struct CfgGuardedKey {
    secret: Vec<u8>,
}

// BAD: when feature "zeroize" is off, this impl does not exist
#[cfg(feature = "zeroize")]
impl Drop for CfgGuardedKey {
    fn drop(&mut self) {
        self.secret.zeroize();
    }
}

Fix: Make zeroization unconditional. If the zeroize crate is optional, gate the crate import but always zero memory manually in Drop using a volatile write loop as the fallback.


A11 — #[derive(Debug)] on Sensitive Type

Category: SECRET_COPY | Severity: low

Why it's dangerous: The Debug trait formats all fields into a string. Any logging framework, panic handler, or dbg!() call will print the secret bytes in plaintext. This is a common source of credential leaks in logs.

// BAD: {key:?} or panic prints the raw bytes
#[derive(Debug)]
pub struct DebugSecret {
    secret: Vec<u8>,
}

Fix: Remove #[derive(Debug)]. Implement Debug manually to show a redacted placeholder: write!(f, "DebugSecret([REDACTED])").


A12 — #[derive(Serialize)] on Sensitive Type

Category: SECRET_COPY | Severity: low

Why it's dangerous: Serialization creates a representation of the secret in the serialization output (JSON, msgpack, etc.). If the output buffer is not itself zeroed after use, the secret bytes leak into the serialized payload.

use serde::Serialize;

// BAD: serde may write secret bytes to an uncontrolled buffer
#[derive(Serialize)]
pub struct SerializableSecret {
    secret: Vec<u8>,
}

Fix: Remove Serialize. If serialization is required, implement it manually to skip or encrypt sensitive fields, and ensure the output buffer is zeroed after use.


Section B — Dangerous API Patterns (find_dangerous_apis.py, source grep-based)

These patterns are detected by scanning Rust source files for calls to APIs that prevent or bypass zeroization. Detection confidence is "likely" when the call appears within ±15 lines of a sensitive name, "needs_review" otherwise.


B1 — mem::forget(secret)

Category: MISSING_SOURCE_ZEROIZE | Severity: critical

Why it's dangerous: mem::forget leaks the value without running its destructor. If the type has a Drop impl that calls zeroize, mem::forget bypasses it entirely. The heap allocation is leaked and never zeroed.

use std::mem;

struct SecretKey(Vec<u8>);
impl Drop for SecretKey { fn drop(&mut self) { self.0.zeroize(); } }

fn bad(key: SecretKey) {
    // BAD: Drop is never called — bytes leak forever
    mem::forget(key);
}

Fix: Never call mem::forget on values containing secrets. Use explicit zeroing before consuming the value if early release is needed.


B2 — ManuallyDrop::new(secret) Call

Category: MISSING_SOURCE_ZEROIZE | Severity: critical

Why it's dangerous: Wrapping a value in ManuallyDrop suppresses its destructor. The secret bytes will not be zeroed when the ManuallyDrop wrapper is dropped unless ManuallyDrop::drop() is called explicitly.

use std::mem::ManuallyDrop;

struct SecretKey(Vec<u8>);
impl Drop for SecretKey { fn drop(&mut self) { self.0.zeroize(); } }

fn bad(key: SecretKey) {
    // BAD: Drop never runs for the inner SecretKey
    let _md = ManuallyDrop::new(key);
}

Fix: If ManuallyDrop is required for FFI or unsafe code, explicitly call key.zeroize() before passing into ManuallyDrop::new, or ensure the surrounding code calls ManuallyDrop::drop().


B3 — Box::leak(secret)

Category: MISSING_SOURCE_ZEROIZE | Severity: critical

Why it's dangerous: Box::leak produces a 'static reference by preventing the Box from ever being dropped. The secret allocation persists for the entire program lifetime and is never zeroed.

struct SecretKey(Vec<u8>);

fn bad(key: SecretKey) -> &'static SecretKey {
    // BAD: key is never dropped or zeroed
    Box::leak(Box::new(key))
}

Fix: Avoid Box::leak for secrets. Use Arc with proper Drop if shared ownership is needed, ensuring the last reference is dropped before program exit.


B4 — mem::uninitialized()

Category: MISSING_SOURCE_ZEROIZE | Severity: critical

Why it's dangerous: mem::uninitialized returns memory with undefined contents — which in practice means prior stack or heap bytes are exposed as the return value. It is unsound (deprecated since Rust 1.39) and may expose sensitive data from prior use of that memory region.

use std::mem;

struct SecretKey([u8; 32]);

unsafe fn bad() -> SecretKey {
    // BAD: may return bytes from prior sensitive allocations
    mem::uninitialized()
}

Fix: Use MaybeUninit::zeroed().assume_init() for zero-initialized memory, or MaybeUninit::uninit() only when you will fully initialize before reading.


B5 — Box::into_raw(secret)

Category: MISSING_SOURCE_ZEROIZE | Severity: high

Why it's dangerous: Box::into_raw consumes the Box and returns a raw pointer, preventing the destructor from running. The caller is responsible for zeroing and deallocating, but this is often forgotten.

struct SecretKey(Vec<u8>);
impl Drop for SecretKey { fn drop(&mut self) { self.0.zeroize(); } }

fn bad(key: SecretKey) -> *mut SecretKey {
    // BAD: Drop is suppressed; raw pointer escapes
    Box::into_raw(Box::new(key))
}

Fix: If raw pointer access is required for FFI, zero the value before converting: call key.zeroize() (if applicable), then use Box::into_raw. Document the requirement for the caller to Box::from_raw and drop the value.


B6 — ptr::write_bytes Without Volatile

Category: OPTIMIZED_AWAY_ZEROIZE | Severity: high

Why it's dangerous: ptr::write_bytes is a non-volatile write. The compiler's dead-store elimination pass can and will remove it if the memory is not read afterwards. Use of this function as a zeroization primitive is unreliable at optimization levels O1 and above.

use std::ptr;

struct SecretKey([u8; 32]);

fn wipe(key: &mut SecretKey) {
    // BAD: may be eliminated by DSE at -O1/-O2
    unsafe { ptr::write_bytes(key as *mut SecretKey, 0, 1); }
}

Fix: Use zeroize::Zeroize (which uses volatile writes internally) or add std::sync::atomic::compiler_fence(Ordering::SeqCst) after the write.


B7 — mem::transmute::<SensitiveType, _>

Category: SECRET_COPY | Severity: high

Why it's dangerous: mem::transmute performs a bitwise copy of the value into the target type. If the target type does not implement ZeroizeOnDrop, the transmuted copy is a secret that will never be zeroed.

use std::mem;

struct SecretKey([u8; 32]);
impl Drop for SecretKey { fn drop(&mut self) { /* zeroize */ } }

fn bad(key: SecretKey) -> [u8; 32] {