diff --git a/compact_str/Cargo.toml b/compact_str/Cargo.toml index c4c1fbc7..956b90f6 100644 --- a/compact_str/Cargo.toml +++ b/compact_str/Cargo.toml @@ -29,6 +29,7 @@ sqlx = ["dep:sqlx", "std"] sqlx-mysql = ["sqlx", "sqlx/mysql"] sqlx-postgres = ["sqlx", "sqlx/postgres"] sqlx-sqlite = ["sqlx", "sqlx/sqlite"] +zeroize = ["dep:zeroize"] [dependencies] arbitrary = { version = "1", optional = true, default-features = false } @@ -42,6 +43,7 @@ rkyv = { version = "0.8", optional = true, default-features = false } serde = { version = "1", optional = true, default-features = false, features = ["derive", "alloc"] } smallvec = { version = "1", optional = true, features = ["union"] } sqlx = { version = "0.8", optional = true, default-features = false } +zeroize = { version = "1", optional = true, default-features = false } castaway = { version = "0.2.3", default-features = false, features = ["alloc"] } cfg-if = "1" diff --git a/compact_str/src/features/mod.rs b/compact_str/src/features/mod.rs index a2ac7c05..1953d2fe 100644 --- a/compact_str/src/features/mod.rs +++ b/compact_str/src/features/mod.rs @@ -22,3 +22,5 @@ mod serde; mod smallvec; #[cfg(feature = "sqlx")] mod sqlx; +#[cfg(feature = "zeroize")] +mod zeroize; diff --git a/compact_str/src/features/zeroize.rs b/compact_str/src/features/zeroize.rs new file mode 100644 index 00000000..b2219ff1 --- /dev/null +++ b/compact_str/src/features/zeroize.rs @@ -0,0 +1,44 @@ +//! Implements the [`zeroize::Zeroize`] trait for [`CompactString`] + +use crate::CompactString; +use zeroize::Zeroize; + +#[cfg_attr(docsrs, doc(cfg(feature = "zeroize")))] +impl Zeroize for CompactString { + fn zeroize(&mut self) { + self.0.zeroize(); + } +} + +#[cfg(test)] +mod tests { + use alloc::string::String; + use test_strategy::proptest; + + use super::*; + use crate::tests::rand_unicode; + + #[test] + fn smoketest_zeroize() { + let mut short = CompactString::from("hello"); + short.zeroize(); + assert_eq!(short, "\0\0\0\0\0"); + + let mut long = CompactString::from("I am a long string that will be on the heap"); + long.zeroize(); + assert_eq!(long, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); + assert!(long.is_heap_allocated()); + } + + #[proptest] + #[cfg_attr(miri, ignore)] + fn proptest_zeroize(#[strategy(rand_unicode())] s: String) { + let mut compact = CompactString::new(s.clone()); + let mut control = s.clone(); + + compact.zeroize(); + control.zeroize(); + + assert_eq!(compact, control); + } +} diff --git a/compact_str/src/repr/mod.rs b/compact_str/src/repr/mod.rs index 88498c50..2e22749b 100644 --- a/compact_str/src/repr/mod.rs +++ b/compact_str/src/repr/mod.rs @@ -533,6 +533,64 @@ impl Repr { } } + /// Zero out the memory backing this [`Repr`]. + #[cfg(feature = "zeroize")] + pub(crate) fn zeroize(&mut self) { + // We can't zero out static memory so we just replace ourselves with + // the EMPTY variant. + if self.is_static_str() { + *self = EMPTY; + return; + } + + /// Performs a volatile `memset` operation which fills a slice with a value. + /// + /// # SAFETY: + /// + /// * The memory pointed to by `dst` must be valid for `count` contiguous bytes. + /// * `count` must not be larger than an isize + /// * `dst` + `count` must not wrap around the address space. + /// + /// Derived from: . + /// + /// TODO(parkmycar): use `volatile_set_memory` when stabilized + #[inline(always)] + unsafe fn volatile_zero(dst: *mut u8, count: usize) { + for i in 0..count { + let dst = dst.add(i); + ptr::write_volatile(dst, 0); + } + } + + /// Uses fences to prevent the compiler from re-ordering memory accesses. + #[inline(always)] + fn atomic_fence() { + use core::sync::atomic; + atomic::compiler_fence(atomic::Ordering::SeqCst); + } + + // The last byte stores our discriminant and stack length. + let last_byte = self.last_byte(); + + let (ptr, cap) = if last_byte == HEAP_MASK { + // SAFETY: We just checked the discriminant to make sure we're heap allocated + let heap_buffer = unsafe { self.as_mut_heap() }; + let ptr = heap_buffer.ptr.as_ptr(); + let cap = heap_buffer.capacity(); + (ptr, cap) + } else { + let ptr = self as *mut Self as *mut u8; + let cap = MAX_SIZE - 1; + (ptr, cap) + }; + + // SAFTEY: We know our pointer is valid for `cap` bytes because the capacity came + // from an already existing CompactString. Also we don't allow allocations larger + // then an isize. + unsafe { volatile_zero(ptr, cap) }; + atomic_fence() + } + /// Returns the last byte that's on the stack. /// /// The last byte stores the discriminant that indicates whether the string is on the stack or diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 89ed746c..f8a4fbc0 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -11,9 +11,10 @@ cargo-fuzz = true [dependencies] arbitrary = { version = "1", features = ["derive"] } bytes = "1" -compact_str = { path = "../compact_str", features = ["bytes", "smallvec"] } +compact_str = { path = "../compact_str", features = ["bytes", "smallvec", "zeroize"] } rand = { version = "0.8", features = ["small_rng"] } rand_distr = "0.4" +zeroize = "1" # Fuzz with both AFL++ and libFuzzer afl = { version = "0.14.2", optional = true } diff --git a/fuzz/src/actions.rs b/fuzz/src/actions.rs index 477870ce..dd0dae7c 100644 --- a/fuzz/src/actions.rs +++ b/fuzz/src/actions.rs @@ -46,8 +46,10 @@ pub enum Action<'a> { CloneAndDrop, /// Calls into_bytes, validates equality, and converts back into strings RoundTripIntoBytes, - // Repeat the string to form a new string. + /// Repeat the string to form a new string. Repeat(usize), + /// Zero out the data backing the string. + Zeroize, } impl Action<'_> { @@ -405,6 +407,11 @@ impl Action<'_> { *compact = new_compact; *control = new_control; } + Zeroize => { + use zeroize::Zeroize; + control.zeroize(); + compact.zeroize(); + } } } }