Skip to content

Commit a729d1f

Browse files
authored
Implement Zeroize for std::ffi::CString (#759)
This implements Zeroize for std::ffi::CString. As CString is not in allocate but in std, a new optional std feature is added to depend on the std library.
1 parent 86455d5 commit a729d1f

File tree

3 files changed

+64
-6
lines changed

3 files changed

+64
-6
lines changed

.github/workflows/zeroize.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ jobs:
6969
# Isolate this crate from workspace which is otherwise MSRV 1.56 due to 2021 edition crates
7070
- run: rm ../Cargo.toml
7171
- run: cargo test
72-
- run: cargo test --features alloc,derive
72+
- run: cargo test --features alloc,derive,std
7373

7474
# Feature-gated ARM64 SIMD register support (nightly-only)
7575
aarch64:

zeroize/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ default = ["alloc"]
2424
aarch64 = []
2525
alloc = []
2626
derive = ["zeroize_derive"]
27+
std = ["alloc"]
2728

2829
[package.metadata.docs.rs]
2930
all-features = true

zeroize/src/lib.rs

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,13 @@
6161
//! impl'd for `Vec<T>` for the above types as well as `String`, where it provides
6262
//! [`Vec::clear`] / [`String::clear`]-like behavior (truncating to zero-length)
6363
//! but ensures the backing memory is securely zeroed with some caveats.
64-
//! (NOTE: see "Stack/Heap Zeroing Notes" for important `Vec`/`String` details)
64+
//!
65+
//! With the `std` feature enabled (which it is **not** by default), [`Zeroize`]
66+
//! is also implemented for [`CString`]. After calling `zeroize()` on a `CString`,
67+
//! it will its internal buffer will contain exactly one nul byte. The backing
68+
//! memory is zeroed by converting it to a `Vec<u8>` and back into a `CString`.
69+
//! (NOTE: see "Stack/Heap Zeroing Notes" for important `Vec`/`String`/`CString` details)
70+
//!
6571
//!
6672
//! The [`DefaultIsZeroes`] marker trait can be impl'd on types which also
6773
//! impl [`Default`], which implements [`Zeroize`] by overwriting a value with
@@ -191,10 +197,10 @@
191197
//! [`Pin`][`core::pin::Pin`] can be leveraged in conjunction with this crate
192198
//! to ensure data kept on the stack isn't moved.
193199
//!
194-
//! The `Zeroize` impls for `Vec` and `String` zeroize the entire capacity of
195-
//! their backing buffer, but cannot guarantee copies of the data were not
196-
//! previously made by buffer reallocation. It's therefore important when
197-
//! attempting to zeroize such buffers to initialize them to the correct
200+
//! The `Zeroize` impls for `Vec`, `String` and `CString` zeroize the entire
201+
//! capacity of their backing buffer, but cannot guarantee copies of the data
202+
//! were not previously made by buffer reallocation. It's therefore important
203+
//! when attempting to zeroize such buffers to initialize them to the correct
198204
//! capacity, and take care to prevent subsequent reallocation.
199205
//!
200206
//! The `secrecy` crate provides higher-level abstractions for eliminating
@@ -233,6 +239,9 @@
233239
#[cfg_attr(test, macro_use)]
234240
extern crate alloc;
235241

242+
#[cfg(feature = "std")]
243+
extern crate std;
244+
236245
#[cfg(feature = "zeroize_derive")]
237246
#[cfg_attr(docsrs, doc(cfg(feature = "zeroize_derive")))]
238247
pub use zeroize_derive::{Zeroize, ZeroizeOnDrop};
@@ -253,6 +262,9 @@ use core::{ops, ptr, slice::IterMut, sync::atomic};
253262
#[cfg(feature = "alloc")]
254263
use alloc::{boxed::Box, string::String, vec::Vec};
255264

265+
#[cfg(feature = "std")]
266+
use std::ffi::CString;
267+
256268
/// Trait for securely erasing types from memory
257269
pub trait Zeroize {
258270
/// Zero out this object from memory using Rust intrinsics which ensure the
@@ -511,6 +523,27 @@ impl Zeroize for String {
511523
}
512524
}
513525

526+
#[cfg(feature = "std")]
527+
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
528+
impl Zeroize for CString {
529+
fn zeroize(&mut self) {
530+
// mem::take uses replace internally to swap the pointer
531+
// Unfortunately this results in an allocation for a Box::new(&[0]) as CString must
532+
// contain a trailing zero byte
533+
let this = std::mem::take(self);
534+
// - CString::into_bytes calls ::into_vec which takes ownership of the heap pointer
535+
// as a Vec<u8>
536+
// - Calling .zeroize() on the resulting vector clears out the bytes
537+
// From: https://github.com/RustCrypto/utils/pull/759#issuecomment-1087976570
538+
let mut buf = this.into_bytes();
539+
buf.zeroize();
540+
// expect() should never fail, because zeroize() truncates the Vec
541+
let zeroed = CString::new(buf).expect("buf not truncated");
542+
// Replace self by the zeroed CString to maintain the original ptr of the buffer
543+
let _ = std::mem::replace(self, zeroed);
544+
}
545+
}
546+
514547
/// Fallible trait for representing cases where zeroization may or may not be
515548
/// possible.
516549
///
@@ -723,6 +756,9 @@ mod tests {
723756
#[cfg(feature = "alloc")]
724757
use alloc::vec::Vec;
725758

759+
#[cfg(feature = "std")]
760+
use std::ffi::CString;
761+
726762
#[derive(Clone, Debug, PartialEq)]
727763
struct ZeroizedOnDrop(u64);
728764

@@ -881,6 +917,27 @@ mod tests {
881917
assert!(as_vec.iter().all(|byte| *byte == 0));
882918
}
883919

920+
#[cfg(feature = "std")]
921+
#[test]
922+
fn zeroize_c_string() {
923+
let mut cstring = CString::new("Hello, world!").expect("CString::new failed");
924+
let orig_len = cstring.as_bytes().len();
925+
let orig_ptr = cstring.as_bytes().as_ptr();
926+
cstring.zeroize();
927+
// This doesn't quite test that the original memory has been cleared, but only that
928+
// cstring now owns an empty buffer
929+
assert!(cstring.as_bytes().is_empty());
930+
for i in 0..orig_len {
931+
unsafe {
932+
// Using a simple deref, only one iteration of the loop is performed
933+
// presumably because after zeroize, the internal buffer has a length of one/
934+
// `read_volatile` seems to "fix" this
935+
// Note that this is very likely UB
936+
assert_eq!(orig_ptr.add(i).read_volatile(), 0);
937+
}
938+
}
939+
}
940+
884941
#[cfg(feature = "alloc")]
885942
#[test]
886943
fn zeroize_box() {

0 commit comments

Comments
 (0)