---
title: "Rust Zeroization Patterns Reference"
description: "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."
type: skill
canonical_url: https://claudary.paisolsolutions.com/skills/rust-zeroization-patterns
source: "Claudary"
difficulty: intermediate
author: "Claude Code Knowledge Pack"
date: 2026-07-10T11:46:21.480Z
license: CC-BY-4.0
attribution: "Rust Zeroization Patterns Reference — Claudary (https://claudary.paisolsolutions.com/skills/rust-zeroization-patterns)"
---

# 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.

## 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.

```rust
// 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.

```rust
// 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.

```rust
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.

```rust
// 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<T>` zeroes only `len` bytes, not the full allocated `capacity`. Excess capacity bytes remain readable until the allocator reclaims them.

```rust
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<T>` Struct Field

**Category**: `MISSING_SOURCE_ZEROIZE` | **Severity**: critical

**Why it's dangerous**: `ManuallyDrop<T>` inhibits automatic drop for the wrapped value. Rust will never call `Drop` on a `ManuallyDrop<T>` 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.

```rust
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.

```rust
// 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<T>` / `Into<T>` 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.

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

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

// BAD: From<RawBytes> converts secret into a plain Vec with no zeroing
impl From<RawBytes> 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<T>`.

---

### 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.

```rust
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.

```rust
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.

```rust
// 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.

```rust
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.

```rust
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.

```rust
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.

```rust
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<SecretKey>` 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.

```rust
use std::mem;

struct SecretKey([u8; 32]);

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

**Fix**: Use `MaybeUninit<T>::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.

```rust
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.

```rust
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.

```rust
use std::mem;

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

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

---

Source: [Claudary](https://claudary.paisolsolutions.com/skills/rust-zeroization-patterns) · https://claudary.paisolsolutions.com
