Skip to main content

secure_types/
vec.rs

1// No_std: we only need `Layout` for computing allocation sizes.
2// We call `alloc::alloc::dealloc` via fully-qualified path to avoid
3// shadowing the crate-level `alloc::<T>()` helper.
4#[cfg(not(feature = "use_os"))]
5use alloc::alloc::Layout;
6
7#[cfg(feature = "use_os")]
8use std::vec::Vec;
9
10// In a `no_std` build `Vec` is only needed by the serde visitor below.
11#[cfg(all(feature = "serde", not(feature = "use_os")))]
12use alloc::vec::Vec;
13
14use super::{Error, SecureArray, alloc};
15use core::{
16   marker::PhantomData,
17   mem,
18   ops::{Bound, RangeBounds},
19   ptr::{self, NonNull},
20};
21use zeroize::{DefaultIsZeroes, Zeroize};
22
23#[cfg(feature = "use_os")]
24use super::free;
25#[cfg(feature = "use_os")]
26use memsec::Prot;
27
28pub type SecureBytes = SecureVec<u8>;
29
30/// Unlocks the vector's memory on construction and re-locks it on drop —
31/// including when the drop happens because the fn closure panicked.
32pub(crate) struct UnlockGuard<'a, T: Zeroize> {
33   vec: &'a SecureVec<T>,
34}
35
36impl<'a, T: Zeroize> UnlockGuard<'a, T> {
37   pub(crate) fn new(vec: &'a SecureVec<T>) -> Self {
38      let ok = vec.unlock_memory();
39      debug_assert!(ok, "UnlockGuard::new: unlock_memory failed");
40
41      UnlockGuard { vec }
42   }
43}
44
45impl<'a, T: Zeroize> Drop for UnlockGuard<'a, T> {
46   fn drop(&mut self) {
47      let ok = self.vec.lock_memory();
48      // Failing to re-lock means the protection is silently gone while the value is
49      // still alive, so this is a hard error in every profile.
50      assert!(ok, "UnlockGuard::drop: lock_memory failed");
51   }
52}
53
54/// A securely allocated, growable vector, just like `std::vec::Vec`.
55///
56/// ## Security Model
57///
58/// When compiled with the `use_os` feature (the default), it provides several layers of protection:
59/// - **Zeroization on Drop**: The memory is zeroized when the vector is dropped.
60/// - **Memory Locking**: The underlying memory pages are locked using `mlock` & `madvise` for (Unix) or
61///   `VirtualLock` & `VirtualProtect` for (Windows) to prevent the OS from memory-dump/swap to disk or other processes accessing the memory.
62///
63/// In a `no_std` environment, it falls back to providing only the **zeroization-on-drop** guarantee.
64///
65/// ## Security Note on Direct Access
66///
67/// We intentionally do **not** implement `Index` / `IndexMut`.
68/// Using `secure_vec[0]` is a compile error.
69///
70/// This is by design: direct indexing would allow bypassing the explicit
71/// unlock mechanism. Always use `unlock_slice()` / `unlock_slice_mut()` (or
72/// the `unlock*` family of methods) to access the contents.
73///
74/// # Thread Safety
75///
76/// `SecureVec` is `Send` (it can be moved to another thread) but not `Sync`.
77/// `unlock*` changes the allocation's page protection, so two threads unlocking
78/// the same instance would race (one can relock while the other still holds a
79/// live slice). Share it as `Arc<Mutex<SecureVec<T>>>`.
80///
81/// # Notes
82///
83/// If you return a new allocated `Vec` from one of the unlock methods you are responsible for zeroizing the memory.
84///
85/// # Example
86///
87/// Using `SecureBytes` (a type alias for `SecureVec<u8>`) to handle a secret key.
88///
89/// ```
90/// use secure_types::{SecureBytes, Zeroize};
91///
92/// // Create a new, empty secure vector.
93/// let mut secret_key = SecureBytes::new().unwrap();
94///
95/// // Push some sensitive data into it.
96/// secret_key.push(0xAB);
97/// secret_key.push(0xCD);
98/// secret_key.push(0xEF);
99///
100/// // The memory is locked here.
101///
102/// // Use a scope to safely access the contents as a slice.
103/// secret_key.unlock_slice(|unlocked_slice| {
104///     assert_eq!(unlocked_slice, &[0xAB, 0xCD, 0xEF]);
105/// });
106///
107/// // Not recommended but if you allocate a new Vec make sure to zeroize it
108/// let mut exposed = secret_key.unlock_slice(|unlocked_slice| {
109///     Vec::from(unlocked_slice)
110/// });
111///
112/// // Do what you need to to do with the new vector
113/// // When you are done with it, zeroize it
114/// exposed.zeroize();
115///
116/// // The memory is automatically locked again when the scope ends.
117///
118/// // When `secret_key` is dropped, its memory is securely zeroized.
119/// ```
120pub struct SecureVec<T>
121where
122   T: Zeroize,
123{
124   ptr: NonNull<T>,
125   pub(crate) len: usize,
126   pub(crate) capacity: usize,
127   _marker: PhantomData<T>,
128}
129
130unsafe impl<T: Zeroize + Send> Send for SecureVec<T> {}
131
132impl<T: Zeroize> SecureVec<T> {
133   /// Create a new `SecureVec` with a capacity of 1
134   pub fn new() -> Result<Self, Error> {
135      let capacity = 1;
136      let size = capacity * mem::size_of::<T>();
137      // SAFETY: `alloc` is `unsafe` only as a raw-allocation marker — it has no
138      // preconditions beyond rejecting a zero `size`, and returns a pointer
139      // aligned for `T`.
140      let ptr = unsafe { alloc::<T>(size)? };
141
142      let secure = SecureVec {
143         ptr,
144         len: 0,
145         capacity,
146         _marker: PhantomData,
147      };
148
149      let _locked = secure.lock_memory();
150
151      #[cfg(feature = "use_os")]
152      if !_locked {
153         return Err(Error::LockFailed);
154      }
155
156      Ok(secure)
157   }
158
159   /// Create a new `SecureVec` with the given capacity
160   pub fn new_with_capacity(mut capacity: usize) -> Result<Self, Error> {
161      if capacity == 0 {
162         capacity = 1;
163      }
164
165      let size = capacity
166         .checked_mul(size_of::<T>())
167         .ok_or(Error::AllocationFailed)?;
168
169      // SAFETY: as in `new` — `alloc` has no preconditions beyond a non-zero
170      // `size`, and `size` here is `capacity * size_of::<T>()` for `capacity >= 1`.
171      let ptr = unsafe { alloc::<T>(size)? };
172
173      let secure = SecureVec {
174         ptr,
175         len: 0,
176         capacity,
177         _marker: PhantomData,
178      };
179
180      let _locked = secure.lock_memory();
181
182      #[cfg(feature = "use_os")]
183      if !_locked {
184         return Err(Error::LockFailed);
185      }
186
187      Ok(secure)
188   }
189
190   #[cfg(feature = "use_os")]
191   /// Create a new `SecureVec` from a `Vec`
192   ///
193   /// The `Vec` is zeroized afterwards
194   pub fn from_vec(mut vec: Vec<T>) -> Result<Self, Error> {
195      if vec.capacity() == 0 {
196         vec.reserve(1);
197      }
198
199      let capacity = vec.capacity();
200      let len = vec.len();
201
202      let size = match capacity.checked_mul(size_of::<T>()) {
203         Some(s) => s,
204         None => {
205            vec.zeroize();
206            return Err(Error::AllocationFailed);
207         }
208      };
209
210      // SAFETY: `alloc` is `unsafe` only as a raw-allocation marker — it has no
211      // preconditions beyond rejecting a zero `size`. `size` is
212      // `capacity * size_of::<T>()` for `capacity >= 1`.
213      let ptr = match unsafe { alloc::<T>(size) } {
214         Ok(ptr) => ptr,
215         Err(_) => {
216            vec.zeroize();
217            return Err(Error::AllocationFailed);
218         }
219      };
220
221      // Move data from the old vec into the secure allocation using ptr::read / ptr::write
222      // This correctly transfers ownership for non-Copy types (e.g. structs containing String).
223      // We then zero the *bytes* of the source buffer (after moving values out) to avoid
224      // leaving sensitive data, and prevent double-drop by clearing the vec length.
225      //
226      // SAFETY: `len <= capacity` elements are initialized in `vec`, and `dst`
227      // points at a fresh allocation of at least `capacity` elements. Each slot is
228      // moved (read + write), never duplicated, and `vec`'s length is zeroed right
229      // after so it cannot drop them again.
230      unsafe {
231         let src = vec.as_ptr();
232         let dst = ptr.as_ptr();
233         for i in 0..len {
234            let value = core::ptr::read(src.add(i));
235            core::ptr::write(dst.add(i), value);
236         }
237      }
238
239      // Prevent the Vec from dropping the now-moved-from elements (would be UB)
240      // and securely erase whatever representation bytes remain in its buffer.
241      //
242      // We use set_len(0) + zeroize on a &mut [u8] view of the allocation
243      // (instead of calling vec.zeroize()) because the Ts have been moved out
244      // via ptr::read. The normal Vec::zeroize impl would zeroize+drop the
245      // moved-from elements, which is UB (and often SIGABRT for a non-copy type).
246      let old_byte_size = capacity * mem::size_of::<T>();
247      // SAFETY: every element in `0..len` was moved out above, so `len` must be
248      // zero before `vec` is dropped; the buffer stays owned by `vec`.
249      unsafe {
250         vec.set_len(0);
251      }
252      if old_byte_size > 0 {
253         // SAFETY: after set_len(0) the allocation bytes are still valid,
254         // we own them exclusively, and no Ts will be dropped by the Vec.
255         let bytes =
256            unsafe { core::slice::from_raw_parts_mut(vec.as_mut_ptr() as *mut u8, old_byte_size) };
257         bytes.zeroize();
258      }
259
260      let secure = SecureVec {
261         ptr,
262         len,
263         capacity,
264         _marker: PhantomData,
265      };
266
267      let locked = secure.lock_memory();
268
269      if !locked {
270         return Err(Error::LockFailed);
271      }
272
273      Ok(secure)
274   }
275
276   /// Create a new `SecureVec` from a mutable slice.
277   ///
278   /// The slice is zeroized afterwards
279   pub fn from_slice_mut(slice: &mut [T]) -> Result<Self, Error>
280   where
281      T: Clone + DefaultIsZeroes,
282   {
283      let mut secure_vec = match SecureVec::new_with_capacity(slice.len()) {
284         Ok(secure_vec) => secure_vec,
285         Err(e) => {
286            slice.zeroize();
287            return Err(e);
288         }
289      };
290
291      secure_vec.init_from_clone(slice);
292      slice.zeroize();
293
294      Ok(secure_vec)
295   }
296
297   /// Create a new `SecureVec` from a slice.
298   ///
299   /// The slice is not zeroized, you are responsible for zeroizing it
300   pub fn from_slice(slice: &[T]) -> Result<Self, Error>
301   where
302      T: Clone,
303   {
304      let mut secure_vec = SecureVec::new_with_capacity(slice.len())?;
305      secure_vec.init_from_clone(slice);
306      Ok(secure_vec)
307   }
308
309   pub fn len(&self) -> usize {
310      self.len
311   }
312
313   /// The number of elements the locked allocation can hold before it grows.
314   ///
315   /// The allocation is re-`mprotect`ed on every growth, so this is also the number of
316   /// elements that can be pushed before another unlock/lock cycle.
317   pub fn capacity(&self) -> usize {
318      self.capacity
319   }
320
321   pub fn is_empty(&self) -> bool {
322      self.len() == 0
323   }
324
325   /// Returns the pointer to the locked memory region
326   ///
327   /// # DANGER
328   ///
329   /// This is a low-level API, which should be used only for
330   /// testing purposes. If you need to access the locked memory
331   /// region, use one of the unlock methods.
332   #[cfg(feature = "expose-ptr")]
333   #[deprecated(
334      since = "0.3.0",
335      note = "This method is intended only for testing/crash reproduction. Use one of the unlock methods instead."
336   )]
337   pub fn ptr(&self) -> NonNull<T> {
338      self.ptr
339   }
340
341   /// Returns the total number of bytes currently allocated for this vector.
342   #[cfg(not(feature = "use_os"))]
343   pub(crate) fn allocated_byte_size(&self) -> usize {
344      self.capacity * mem::size_of::<T>()
345   }
346
347   pub(crate) fn as_mut_ptr(&mut self) -> *mut u8 {
348      self.ptr.as_ptr() as *mut u8
349   }
350
351   pub(crate) fn lock_memory(&self) -> bool {
352      #[cfg(feature = "use_os")]
353      {
354         #[cfg(windows)]
355         {
356            super::mprotect(self.ptr, Prot::NoAccess)
357         }
358         #[cfg(unix)]
359         {
360            super::mprotect(self.ptr, Prot::NoAccess)
361         }
362      }
363      #[cfg(not(feature = "use_os"))]
364      {
365         true // No-op: always "succeeds"
366      }
367   }
368
369   pub(crate) fn unlock_memory(&self) -> bool {
370      #[cfg(feature = "use_os")]
371      {
372         #[cfg(windows)]
373         {
374            super::mprotect(self.ptr, Prot::ReadWrite)
375         }
376         #[cfg(unix)]
377         {
378            super::mprotect(self.ptr, Prot::ReadWrite)
379         }
380      }
381
382      #[cfg(not(feature = "use_os"))]
383      {
384         true // No-op: always "succeeds"
385      }
386   }
387
388   /// Immutable access to the `SecureVec`
389   ///
390   /// # Re-entrancy
391   ///
392   /// The closure must not call another `unlock*` method on this vector: the
393   /// pages are unprotected once and re-protected when this call returns, so a
394   /// nested unlock would re-lock the memory while the inner scope is still
395   /// reading it. The same holds for every `unlock*` method.
396   pub fn unlock<F, R>(&self, f: F) -> R
397   where
398      F: FnOnce(&SecureVec<T>) -> R,
399   {
400      let _guard = UnlockGuard::new(self);
401      f(self)
402   }
403
404   /// Immutable access to the `SecureVec` as `&[T]`
405   pub fn unlock_slice<F, R>(&self, f: F) -> R
406   where
407      F: FnOnce(&[T]) -> R,
408   {
409      let _guard = UnlockGuard::new(self);
410      // SAFETY: the guard unprotects the live allocation, `len` counts only
411      // initialized elements, so the slice is in bounds and never reads an
412      // uninitialised slot; `&self` rules out a concurrent `&mut`.
413      let slice = unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), self.len) };
414      f(slice)
415   }
416
417   /// Mutable access to the `SecureVec` as `&mut [T]`
418   pub fn unlock_slice_mut<F, R>(&mut self, f: F) -> R
419   where
420      F: FnOnce(&mut [T]) -> R,
421   {
422      // SAFETY: `&mut self` guarantees exclusive access, the guard unprotects
423      // the live allocation, and `len` counts only initialized elements.
424      unsafe {
425         let _guard = UnlockGuard::new(self);
426         let slice = core::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len);
427         f(slice)
428      }
429   }
430
431   /// Immutable access to the `SecureVec` as `Iter<T>`
432   pub fn unlock_iter<F, R>(&self, f: F) -> R
433   where
434      F: FnOnce(core::slice::Iter<T>) -> R,
435   {
436      // SAFETY: as in `unlock_slice` — the guard unprotects the live allocation
437      // and `len` counts only initialized elements.
438      unsafe {
439         let _guard = UnlockGuard::new(self);
440         let slice = core::slice::from_raw_parts(self.ptr.as_ptr(), self.len);
441         let iter = slice.iter();
442         f(iter)
443      }
444   }
445
446   /// Mutable access to the `SecureVec` as `IterMut<T>`
447   pub fn unlock_iter_mut<F, R>(&mut self, f: F) -> R
448   where
449      F: FnOnce(core::slice::IterMut<T>) -> R,
450   {
451      // SAFETY: `&mut self` gives exclusive access; the guard unprotects the
452      // live allocation and `len` counts only initialized elements.
453      unsafe {
454         let _guard = UnlockGuard::new(self);
455         let slice = core::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len);
456         let iter = slice.iter_mut();
457         f(iter)
458      }
459   }
460
461   /// Erase the underlying data and clears the vector
462   ///
463   /// The memory is locked again and the capacity is preserved for reuse
464   pub fn erase(&mut self) {
465      {
466         let _guard = UnlockGuard::new(self);
467
468         // SAFETY: the guard unprotects the live allocation; only the `len`
469         // initialized elements are exposed, so uninitialised capacity is never
470         // read as a `T`.
471         unsafe {
472            let slice = core::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len);
473            for elem in slice.iter_mut() {
474               elem.zeroize();
475            }
476         }
477      }
478
479      self.clear();
480   }
481
482   /// Clear the vector
483   ///
484   /// This just sets the vector's len to zero it does not erase the underlying data
485   pub fn clear(&mut self) {
486      self.len = 0;
487   }
488
489   pub fn push(&mut self, value: T) {
490      self.reserve(1);
491
492      let dst = self.ptr.as_ptr();
493      let write_at = self.len;
494
495      {
496         let _guard = UnlockGuard::new(self);
497
498         // SAFETY: `write_at == self.len` and `reserve(1)` above guaranteed
499         // `len < capacity`, so the slot is in bounds; it is uninitialised, and
500         // `ptr::write` does not read it. `value` is moved in exactly once.
501         unsafe {
502            core::ptr::write(dst.add(write_at), value);
503         }
504      }
505
506      self.len = write_at + 1;
507   }
508
509   /// Appends every element of `src` using a single unlock/lock cycle.
510   ///
511   /// A loop of [`push`](Self::push) costs an `mprotect` pair per element, so bulk
512   /// copies (the serde writer feeding this vector, and the binary codec's encoder)
513   /// need this instead. The length is committed only after every write succeeded,
514   /// so a panic from `T::clone` leaves the vector at its previous length.
515   ///
516   /// Gated on `use_os` or `codec`: those are the two features that call it, and
517   /// compiling it for neither would only produce a `dead_code` warning.
518   #[cfg(any(feature = "use_os", feature = "codec"))]
519   pub(crate) fn extend_from_slice(&mut self, src: &[T]) -> Result<(), Error>
520   where
521      T: Clone,
522   {
523      if src.is_empty() {
524         return Ok(());
525      }
526
527      self.try_reserve(src.len())?;
528
529      let write_at = self.len;
530      let dst = self.ptr.as_ptr();
531
532      {
533         let _guard = UnlockGuard::new(self);
534
535         // SAFETY: `try_reserve` above made room for `src.len()` more elements,
536         // so every `dst.add(write_at + i)` is an uninitialised in-bounds slot;
537         // `ptr::write` never reads it. `len` is committed only after the loop.
538         unsafe {
539            for (i, item) in src.iter().enumerate() {
540               core::ptr::write(dst.add(write_at + i), item.clone());
541            }
542         }
543      }
544
545      self.len = write_at + src.len();
546      Ok(())
547   }
548
549   /// Ensures that the vector has enough capacity for at least `additional` more elements.
550   ///
551   /// If more capacity is needed, it will reallocate. This may cause the buffer location to change.
552   ///
553   /// # Panics
554   ///
555   /// Panics if the new capacity overflows `usize` or if the allocation fails.
556   pub fn reserve(&mut self, additional: usize) {
557      self.try_reserve(additional).unwrap_or_else(|error| {
558         panic!(
559            "secure-types: SecureVec::reserve overflow or allocation failed ({error}); SecureVec left unchanged"
560         )
561      });
562   }
563
564   /// Fallible [`reserve`](Self::reserve). The codec uses this so growth is an
565   /// [`Error`] rather than a panic, matching [`EncodeError::Secure`].
566   pub(crate) fn try_reserve(&mut self, additional: usize) -> Result<(), Error> {
567      let required_capacity = self
568         .len
569         .checked_add(additional)
570         .ok_or(Error::AllocationFailed)?;
571
572      if required_capacity <= self.capacity {
573         return Ok(());
574      }
575
576      // Use an amortized growth strategy to avoid reallocating on every push.
577      // If doubling would overflow, fall back to the exact requirement and let
578      // the allocation below report the failure.
579      let new_capacity = self
580         .capacity
581         .max(1)
582         .checked_mul(2)
583         .unwrap_or(required_capacity)
584         .max(required_capacity);
585
586      let new_size = new_capacity
587         .checked_mul(mem::size_of::<T>())
588         .ok_or(Error::AllocationFailed)?;
589
590      // SAFETY: `alloc` has no preconditions beyond a non-zero `size`; `new_size`
591      // is `new_capacity * size_of::<T>()` and `new_capacity >= required > capacity`.
592      let new_ptr = unsafe { alloc::<T>(new_size)? };
593
594      // Copy data to new pointer
595      // SAFETY: `new_ptr` is a fresh allocation of `new_capacity >= self.len`
596      // elements. Each initialized element is moved (read + write) into it, so
597      // ownership transfers exactly once; the old buffer's bytes are wiped and
598      // then freed with the layout `alloc` used. `self.ptr`/`capacity` are
599      // updated only after this block.
600      unsafe {
601         let ok = self.unlock_memory();
602         debug_assert!(ok, "SecureVec::try_reserve: unlock_memory failed");
603
604         // Move (not copy) elements to new buffer to support non-Copy T correctly.
605         // Using read+write transfers ownership of e.g. Strings.
606         let len = self.len();
607         for i in 0..len {
608            let val = core::ptr::read(self.ptr.as_ptr().add(i));
609            core::ptr::write(new_ptr.as_ptr().add(i), val);
610         }
611
612         // Erase old buffer bytes (after move-out)
613         if self.capacity > 0 {
614            let old_bytes = self.capacity * mem::size_of::<T>();
615            let bytes = core::slice::from_raw_parts_mut(self.ptr.as_ptr() as *mut u8, old_bytes);
616            bytes.zeroize();
617         }
618
619         #[cfg(feature = "use_os")]
620         free(self.ptr);
621
622         #[cfg(not(feature = "use_os"))]
623         {
624            let old_size = self.capacity * mem::size_of::<T>();
625            let old_layout = Layout::from_size_align_unchecked(old_size, mem::align_of::<T>());
626            alloc::alloc::dealloc(self.ptr.as_ptr() as *mut u8, old_layout);
627         }
628      }
629
630      // Update pointer and capacity, then re-lock the new memory region
631      self.ptr = new_ptr;
632      self.capacity = new_capacity;
633      let ok = self.lock_memory();
634      assert!(ok, "SecureVec::try_reserve: lock_memory failed");
635      Ok(())
636   }
637
638   /// Creates a draining iterator that removes the specified range from the vector
639   /// and yields the removed items.
640   ///
641   /// Note: the memory is only unlocked while an item is read and while the iterator
642   /// is dropped, so it is left locked once the iterator is gone even if the
643   /// iterator is leaked with `mem::forget`.
644   ///
645   /// # Panics
646   /// Panics if the starting point is greater than the end point or if the end point
647   /// is greater than the length of the vector.
648   pub fn drain<R>(&mut self, range: R) -> Drain<'_, T>
649   where
650      R: RangeBounds<usize>,
651   {
652      let original_len = self.len;
653
654      let (drain_start_idx, drain_end_idx) = resolve_range_indices(range, original_len);
655
656      let tail_len = original_len - drain_end_idx;
657
658      self.len = drain_start_idx;
659
660      Drain {
661         vec_ref: self,
662         drain_start_index: drain_start_idx,
663         current_drain_iter_index: drain_start_idx,
664         drain_end_index: drain_end_idx,
665         original_vec_len: original_len,
666         tail_len,
667         _marker: PhantomData,
668      }
669   }
670
671   /// Initializes a freshly-allocated (uninitialized) buffer by cloning `src`
672   /// into it. Uses `ptr::write` so the uninitialized destination slots are
673   /// never read, never dropped, and no `&mut [T]` is ever formed over them.
674   ///
675   /// `len` is set only after every write succeeds, so a panic from
676   /// `T::clone` leaves the vector at its previous length (0 for a fresh one).
677   pub(crate) fn init_from_clone(&mut self, src: &[T])
678   where
679      T: Clone,
680   {
681      debug_assert!(src.len() <= self.capacity);
682
683      {
684         let _guard = UnlockGuard::new(self);
685
686         // SAFETY: `src.len() <= self.capacity` (asserted above), so every
687         // `dst.add(i)` is an uninitialised in-bounds slot; the guard unprotects
688         // the allocation and `ptr::write` never reads the destination. `len` is
689         // committed only after the loop.
690         unsafe {
691            let dst = self.ptr.as_ptr();
692            for (i, item) in src.iter().enumerate() {
693               core::ptr::write(dst.add(i), item.clone());
694            }
695         }
696      }
697
698      self.len = src.len();
699   }
700}
701
702impl SecureVec<u8> {
703   /// Overwrites `src` at `offset` without changing the length or the capacity.
704   ///
705   /// Used by the binary codec to back-fill the `u32` length placeholder that
706   /// precedes a struct field's framed body, once that body has been written.
707   /// The frame is what lets a reader skip a field it does not know about, which
708   /// is what makes adding a field a compatible change.
709   ///
710   /// Unlike the `unlock*` family this returns nothing: it exposes no slice, so
711   /// the borrowed window is not left up to the caller.
712   ///
713   /// # Panics
714   ///
715   /// Panics if `offset + src.len()` exceeds the current length, or if the
716   /// memory cannot be re-locked afterwards. A patch never grows the vector —
717   /// use [`extend_from_slice`](Self::extend_from_slice) for that.
718   #[cfg(feature = "codec")]
719   pub(crate) fn patch_at(&mut self, offset: usize, src: &[u8]) {
720      let end = offset
721         .checked_add(src.len())
722         .expect("SecureVec::patch_at: offset overflow");
723      assert!(
724         end <= self.len,
725         "SecureVec::patch_at: range {offset}..{end} exceeds length {}",
726         self.len
727      );
728
729      // SAFETY: `end <= self.len`, so `offset..end` lies inside the initialized
730      // region of the allocation. `src` is a distinct live slice that cannot
731      // overlap it, so the copy is non-overlapping. The length is untouched, so
732      // no element is created, duplicated, or dropped here.
733      {
734         let _guard = UnlockGuard::new(self);
735
736         unsafe {
737            core::ptr::copy_nonoverlapping(
738               src.as_ptr(),
739               self.ptr.as_ptr().add(offset),
740               src.len(),
741            );
742         }
743      }
744   }
745}
746
747impl<T: Clone + Zeroize> Clone for SecureVec<T> {
748   /// # Panics
749   ///
750   /// Panics if the clone's secure allocation cannot be made or locked.
751   fn clone(&self) -> Self {
752      let mut new_vec = SecureVec::new_with_capacity(self.capacity).unwrap();
753      self.unlock_slice(|src_slice| {
754         new_vec.init_from_clone(src_slice);
755      });
756      new_vec
757   }
758}
759
760impl<T: Clone + Zeroize, const LENGTH: usize> From<SecureArray<T, LENGTH>> for SecureVec<T> {
761   /// # Panics
762   ///
763   /// Panics if the new secure allocation cannot be made or locked.
764   fn from(array: SecureArray<T, LENGTH>) -> Self {
765      let mut new_vec = SecureVec::new_with_capacity(LENGTH)
766         .expect("Failed to allocate SecureVec during conversion");
767      array.unlock(|array_slice| {
768         new_vec.init_from_clone(array_slice);
769      });
770      new_vec
771   }
772}
773
774impl<T: Zeroize> Drop for SecureVec<T> {
775   fn drop(&mut self) {
776      // SAFETY: `drop` has exclusive ownership; `unlock_memory` restores access.
777      // Only the `len` initialized elements are touched — zeroizing the
778      // uninitialised capacity would interpret poison bytes as a `T`.
779      unsafe {
780         let ok = self.unlock_memory();
781         debug_assert!(ok, "SecureVec::drop: unlock_memory failed");
782
783         let slice = core::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len);
784         for elem in slice.iter_mut() {
785            elem.zeroize();
786         }
787      }
788
789      #[cfg(feature = "use_os")]
790      free(self.ptr);
791
792      #[cfg(not(feature = "use_os"))]
793      // SAFETY: `allocated_byte_size()` is the full allocation size, still owned
794      // here and unprotected above; the `Layout` matches the one `alloc` used.
795      // Byte-wiping it also removes anything a `clear()` left behind, which
796      // `use_os` gets from `memsec::free` instead.
797      unsafe {
798         let byte_size = self.allocated_byte_size();
799         let bytes = core::slice::from_raw_parts_mut(self.ptr.as_ptr() as *mut u8, byte_size);
800         bytes.zeroize();
801
802         let layout = Layout::from_size_align_unchecked(byte_size, mem::align_of::<T>());
803         alloc::alloc::dealloc(self.ptr.as_ptr() as *mut u8, layout);
804      }
805   }
806}
807
808// Note: We intentionally do **not** implement Index / IndexMut.
809// Direct indexing (`vec[0]`) would bypass the unlock mechanism and
810// access locked memory, causing a segfault. This is by design.
811// Always use unlock_slice() / unlock_slice_mut().
812
813/// Upper bound on how much memory a `Deserialize` impl will reserve up front from a
814/// format-supplied `size_hint`.
815///
816/// The hint is advisory and comes from the format, so trusting a huge one would mean
817/// locking that much memory before a single element has been read. The vector still grows
818/// to whatever the real length turns out to be, so a low cap costs nothing but
819/// reallocation.
820#[cfg(feature = "serde")]
821const MAX_PREALLOCATION_FROM_SIZE_HINT: usize = 4096;
822
823/// Serializes as a byte buffer, matching the `deserialize_bytes` request of the
824/// `Deserialize` impl below. Formats that support byte buffers get the contents in one
825/// piece rather than element by element; `serde_json` renders either form as an array of
826/// numbers, so its output is unchanged.
827#[cfg(feature = "serde")]
828impl serde::Serialize for SecureVec<u8> {
829   fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
830   where
831      S: serde::Serializer,
832   {
833      self.unlock_slice(|slice| serializer.serialize_bytes(slice))
834   }
835}
836
837#[cfg(feature = "serde")]
838impl<'de> serde::Deserialize<'de> for SecureVec<u8> {
839   fn deserialize<D>(deserializer: D) -> Result<SecureVec<u8>, D::Error>
840   where
841      D: serde::Deserializer<'de>,
842   {
843      struct SecureVecVisitor;
844      impl<'de> serde::de::Visitor<'de> for SecureVecVisitor {
845         type Value = SecureVec<u8>;
846
847         fn expecting(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
848            write!(formatter, "a sequence or a byte buffer")
849         }
850
851         fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
852         where
853            A: serde::de::SeqAccess<'de>,
854         {
855            // Reserve what the format advertises, so the locked buffer is not grown
856            // (re-allocated and re-`mprotect`ed) once per element — but cap it. The hint
857            // comes from the format, and a huge one would otherwise have us lock that
858            // much memory before reading a single byte.
859            let capacity = seq
860               .size_hint()
861               .unwrap_or(0)
862               .min(MAX_PREALLOCATION_FROM_SIZE_HINT);
863            let mut vec =
864               SecureVec::new_with_capacity(capacity).map_err(serde::de::Error::custom)?;
865
866            while let Some(byte) = seq.next_element::<u8>()? {
867               vec.push(byte);
868            }
869
870            Ok(vec)
871         }
872
873         /// A format that hands over raw bytes instead of a sequence of `u8`s gets a
874         /// single bulk copy straight into locked memory.
875         fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
876         where
877            E: serde::de::Error,
878         {
879            SecureVec::from_slice(v).map_err(serde::de::Error::custom)
880         }
881
882         /// Mirrors `SecureString`'s `visit_string`: wipe the owned buffer the format
883         /// handed over, instead of letting it drop with the plaintext inside.
884         fn visit_byte_buf<E>(self, mut v: Vec<u8>) -> Result<Self::Value, E>
885         where
886            E: serde::de::Error,
887         {
888            let vec = self.visit_bytes(&v);
889            v.zeroize();
890            vec
891         }
892      }
893
894      deserializer.deserialize_bytes(SecureVecVisitor)
895   }
896}
897
898/// Elements that a [`SecureVec`] or [`SecureArray`] encodes as a *sequence of values*
899/// rather than as one byte buffer.
900///
901/// [`u8`] is deliberately absent. A `u8` container is a byte string, so it encodes as a
902/// single bulk buffer — the compact form, and one unlock/lock cycle instead of one per
903/// element. A blanket impl that covered `u8` too would overlap with the byte-buffer impls
904/// above, and Rust has no specialization, so each element type opts in here instead.
905///
906/// Implemented for the core scalar types. Implement it for your own type to make
907/// `SecureVec<T>` and `SecureArray<T, N>` serializable. It is a safe trait: implementing it
908/// only selects an encoding.
909#[cfg(feature = "serde")]
910pub trait SeqElement: Zeroize {}
911
912#[cfg(feature = "serde")]
913impl SeqElement for bool {}
914
915#[cfg(feature = "serde")]
916impl SeqElement for char {}
917
918#[cfg(feature = "serde")]
919impl SeqElement for f32 {}
920
921#[cfg(feature = "serde")]
922impl SeqElement for f64 {}
923
924#[cfg(feature = "serde")]
925impl SeqElement for i8 {}
926
927#[cfg(feature = "serde")]
928impl SeqElement for i16 {}
929
930#[cfg(feature = "serde")]
931impl SeqElement for i32 {}
932
933#[cfg(feature = "serde")]
934impl SeqElement for i64 {}
935
936#[cfg(feature = "serde")]
937impl SeqElement for i128 {}
938
939#[cfg(feature = "serde")]
940impl SeqElement for u16 {}
941
942#[cfg(feature = "serde")]
943impl SeqElement for u32 {}
944
945#[cfg(feature = "serde")]
946impl SeqElement for u64 {}
947
948#[cfg(feature = "serde")]
949impl SeqElement for u128 {}
950
951/// Serializes a `SecureVec<T>` of [`SeqElement`]s as a sequence of `T` values.
952///
953/// `SecureVec<u8>` takes the byte-buffer impl above instead; the bound here is what keeps
954/// the two disjoint.
955#[cfg(feature = "serde")]
956impl<T> serde::Serialize for SecureVec<T>
957where
958   T: SeqElement + serde::Serialize,
959{
960   fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
961   where
962      S: serde::Serializer,
963   {
964      use serde::ser::SerializeSeq;
965
966      let mut seq = serializer.serialize_seq(Some(self.len()))?;
967
968      // One unlock for the whole run. Element writes are per element, which for these
969      // types is unavoidable: the container cannot hand a `&[T]` to a format that asked
970      // for a sequence.
971      let elements: Result<(), S::Error> = self.unlock_slice(|slice| {
972         for item in slice {
973            seq.serialize_element(item)?;
974         }
975
976         Ok(())
977      });
978      elements?;
979
980      seq.end()
981   }
982}
983
984/// Deserializes a `SecureVec<T>` of [`SeqElement`]s from a sequence of `T` values.
985#[cfg(feature = "serde")]
986impl<'de, T> serde::Deserialize<'de> for SecureVec<T>
987where
988   T: SeqElement + serde::Deserialize<'de>,
989{
990   fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
991   where
992      D: serde::Deserializer<'de>,
993   {
994      struct SecureSeqVisitor<T>(PhantomData<T>);
995
996      impl<'de, T> serde::de::Visitor<'de> for SecureSeqVisitor<T>
997      where
998         T: SeqElement + serde::Deserialize<'de>,
999      {
1000         type Value = SecureVec<T>;
1001
1002         fn expecting(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
1003            write!(formatter, "a sequence of secure elements")
1004         }
1005
1006         fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
1007         where
1008            A: serde::de::SeqAccess<'de>,
1009         {
1010            // The same capped reservation as the byte-buffer visitor: the hint comes from
1011            // the format and is only advisory.
1012            let capacity = seq
1013               .size_hint()
1014               .unwrap_or(0)
1015               .min(MAX_PREALLOCATION_FROM_SIZE_HINT);
1016            let mut vec =
1017               SecureVec::new_with_capacity(capacity).map_err(serde::de::Error::custom)?;
1018
1019            while let Some(item) = seq.next_element::<T>()? {
1020               vec.push(item);
1021            }
1022
1023            Ok(vec)
1024         }
1025      }
1026
1027      deserializer.deserialize_seq(SecureSeqVisitor::<T>(PhantomData))
1028   }
1029}
1030
1031/// A draining iterator for `SecureVec<T>`.
1032///
1033/// This struct is created by the `drain` method on `SecureVec`.
1034///
1035/// # Notes
1036///
1037/// The memory is unlocked only while an item is read and while `Drop` compacts the
1038/// vector, so a leaked iterator (`mem::forget`) leaves the vector locked rather than
1039/// exposed. Leaking it still skips the drops of the elements left in the drained
1040/// range and leaves the length at the drain start.
1041pub struct Drain<'a, T: Zeroize + 'a> {
1042   vec_ref: &'a mut SecureVec<T>,
1043   drain_start_index: usize,
1044   current_drain_iter_index: usize,
1045   drain_end_index: usize,
1046
1047   original_vec_len: usize, // Original length of vec_ref before drain
1048   tail_len: usize,         // Number of elements after the drain range in the original vec
1049
1050   _marker: PhantomData<&'a T>,
1051}
1052
1053impl<'a, T: Zeroize> Iterator for Drain<'a, T> {
1054   type Item = T;
1055
1056   fn next(&mut self) -> Option<T> {
1057      if self.current_drain_iter_index >= self.drain_end_index {
1058         return None;
1059      }
1060
1061      // Raw pointer taken before the guard borrows the vector: raw pointers do not
1062      // keep the borrow alive, and the guard must stay alive while we read through it.
1063      let base = self.vec_ref.ptr.as_ptr();
1064
1065      // Unlock for this single read only, so the memory is locked again as soon as
1066      // this returns — and stays locked if the iterator is forgotten.
1067      let _guard = UnlockGuard::new(&*self.vec_ref);
1068
1069      // SAFETY: `current_drain_iter_index < drain_end_index <= original len`, so
1070      // this is an initialized element of the live allocation, unprotected by the
1071      // guard. It is moved out (never duplicated): the index advances so the slot
1072      // is not read again, and `compact` treats its old bits as moved-from.
1073      let item = unsafe { ptr::read(base.add(self.current_drain_iter_index)) };
1074      self.current_drain_iter_index += 1;
1075
1076      Some(item)
1077   }
1078
1079   fn size_hint(&self) -> (usize, Option<usize>) {
1080      let remaining = self.drain_end_index - self.current_drain_iter_index;
1081      (remaining, Some(remaining))
1082   }
1083}
1084
1085impl<'a, T: Zeroize> ExactSizeIterator for Drain<'a, T> {}
1086
1087impl<'a, T: Zeroize> Drain<'a, T> {
1088   /// Unlocks the vector, compacts it, and re-locks it again.
1089   ///
1090   /// Returns the vector's new length. The `UnlockGuard` re-locks the memory even
1091   /// if the compaction panics, so a leaked or panicking iterator never leaves the
1092   /// vector exposed.
1093   fn compact(&self) -> usize {
1094      // Raw pointer taken before the guard borrows the vector: raw pointers do not
1095      // keep the borrow alive, and the guard must stay alive while we compact.
1096      let base = self.vec_ref.ptr.as_ptr();
1097
1098      let _guard = UnlockGuard::new(&*self.vec_ref);
1099
1100      // SAFETY: the guard unprotects the live vector. Every index is within the
1101      // original length; unyielded drain-range elements are dropped exactly once,
1102      // the tail is moved (not copied) into the hole, and the leftover slots hold
1103      // only moved-from / duplicate bit patterns — wiped as bytes, never
1104      // reinterpreted as a `T`.
1105      unsafe {
1106         // Drop drain-range elements that were never yielded. `next` already
1107         // `ptr::read` them out to the caller; dropping those again would
1108         // double-free.
1109         if mem::needs_drop::<T>() {
1110            let mut current_ptr = base.add(self.current_drain_iter_index);
1111            let end_ptr = base.add(self.drain_end_index);
1112            while current_ptr < end_ptr {
1113               ptr::drop_in_place(current_ptr);
1114               current_ptr = current_ptr.add(1);
1115            }
1116         }
1117
1118         let hole_dst_ptr = base.add(self.drain_start_index);
1119         let tail_src_ptr = base.add(self.drain_end_index);
1120
1121         if self.tail_len > 0 {
1122            ptr::copy(tail_src_ptr, hole_dst_ptr, self.tail_len);
1123         }
1124
1125         let new_len = self.drain_start_index + self.tail_len;
1126
1127         // Leftover slots are not valid `T`: they are either dropped unyielded
1128         // items, moved-from yielded items, or the bitwise source of the tail
1129         // copy. `T::zeroize` / `drop_in_place` here aliases the caller's
1130         // values (and the kept tail). Wipe as bytes.
1131         let leftover_elems = self.original_vec_len.saturating_sub(new_len);
1132         let leftover_bytes = leftover_elems.saturating_mul(mem::size_of::<T>());
1133         if leftover_bytes > 0 {
1134            let bytes =
1135               core::slice::from_raw_parts_mut(base.add(new_len) as *mut u8, leftover_bytes);
1136            bytes.zeroize();
1137         }
1138
1139         new_len
1140      }
1141   }
1142}
1143
1144impl<'a, T: Zeroize> Drop for Drain<'a, T> {
1145   fn drop(&mut self) {
1146      let new_len = self.compact();
1147
1148      // `compact` re-locked the memory before returning.
1149      self.vec_ref.len = new_len;
1150   }
1151}
1152
1153// Helper function to resolve RangeBounds to (start, end) indices
1154fn resolve_range_indices<R: RangeBounds<usize>>(range: R, len: usize) -> (usize, usize) {
1155   let start_bound = range.start_bound();
1156   let end_bound = range.end_bound();
1157
1158   let start = match start_bound {
1159      Bound::Included(&s) => s,
1160      Bound::Excluded(&s) => s
1161         .checked_add(1)
1162         .unwrap_or_else(|| panic!("attempted to start drain at Excluded(usize::MAX)")),
1163      Bound::Unbounded => 0,
1164   };
1165
1166   let end = match end_bound {
1167      Bound::Included(&e) => e
1168         .checked_add(1)
1169         .unwrap_or_else(|| panic!("attempted to end drain at Included(usize::MAX)")),
1170      Bound::Excluded(&e) => e,
1171      Bound::Unbounded => len,
1172   };
1173
1174   if start > end {
1175      panic!(
1176         "drain range start ({}) must be less than or equal to end ({})",
1177         start, end
1178      );
1179   }
1180   if end > len {
1181      panic!(
1182         "drain range end ({}) out of bounds for slice of length {}",
1183         end, len
1184      );
1185   }
1186
1187   (start, end)
1188}
1189
1190#[cfg(test)]
1191mod tests {
1192   // Every test in this module is gated on `use_os` or `codec`, so the glob is only
1193   // reachable when one of them is enabled. Importing it unconditionally makes a
1194   // `--no-default-features` build warn about an unused import.
1195   #[cfg(any(feature = "use_os", feature = "codec"))]
1196   use super::*;
1197
1198   #[cfg(feature = "use_os")]
1199   use std::process::{Command, Stdio};
1200
1201   #[cfg(feature = "use_os")]
1202   #[test]
1203   fn lock_unlock_works() {
1204      let secure: SecureVec<u8> = SecureVec::new().unwrap();
1205
1206      let unlocked = secure.unlock_memory();
1207      assert!(unlocked);
1208
1209      let locked = secure.lock_memory();
1210      assert!(locked);
1211   }
1212
1213   #[cfg(feature = "codec")]
1214   #[test]
1215   fn test_patch_at_overwrites_in_place() {
1216      let mut secure = SecureBytes::from_slice(b"abcdefgh").unwrap();
1217
1218      secure.patch_at(2, b"XY");
1219
1220      secure.unlock_slice(|bytes| {
1221         assert_eq!(bytes, b"abXYefgh");
1222         assert_eq!(bytes.len(), 8);
1223      });
1224   }
1225
1226   #[cfg(feature = "codec")]
1227   #[test]
1228   fn test_patch_at_last_bytes_and_whole_buffer() {
1229      let mut secure = SecureBytes::from_slice(b"abcdefgh").unwrap();
1230
1231      secure.patch_at(6, b"XY");
1232      secure.unlock_slice(|bytes| assert_eq!(bytes, b"abcdefXY"));
1233
1234      secure.patch_at(0, b"12345678");
1235      secure.unlock_slice(|bytes| assert_eq!(bytes, b"12345678"));
1236   }
1237
1238   #[cfg(feature = "codec")]
1239   #[test]
1240   fn test_patch_at_empty_source_is_a_noop() {
1241      let mut secure = SecureBytes::from_slice(b"abc").unwrap();
1242
1243      // A zero-length patch is valid inside the buffer and at its very end.
1244      secure.patch_at(0, b"");
1245      secure.patch_at(3, b"");
1246
1247      secure.unlock_slice(|bytes| assert_eq!(bytes, b"abc"));
1248   }
1249
1250   #[cfg(feature = "use_os")]
1251   #[test]
1252   fn test_erase_zeroizes_initialized_slots() {
1253      let mut secure = SecureVec::from_slice(&[1u8, 2, 3]).unwrap();
1254      let capacity = secure.capacity;
1255      secure.erase();
1256      assert_eq!(secure.len, 0);
1257      assert_eq!(secure.capacity, capacity);
1258
1259      let ok = secure.unlock_memory();
1260      assert!(ok);
1261      // SAFETY: test-only — the memory was just unlocked above, and the three
1262      // slots were erased, so reading them is valid and must show zeros.
1263      unsafe {
1264         let slice = core::slice::from_raw_parts(secure.ptr.as_ptr(), 3);
1265         assert_eq!(slice, &[0, 0, 0]);
1266      }
1267      let ok = secure.lock_memory();
1268      assert!(ok);
1269   }
1270
1271   #[cfg(feature = "codec")]
1272   #[test]
1273   fn test_patch_at_leaves_length_and_capacity_alone() {
1274      let mut secure = SecureBytes::new_with_capacity(16).unwrap();
1275      secure.extend_from_slice(b"abc").unwrap();
1276      let capacity_before = secure.unlock(|vec| vec.capacity);
1277
1278      secure.patch_at(0, b"ZY");
1279
1280      secure.unlock(|vec| {
1281         assert_eq!(vec.len, 3);
1282         assert_eq!(vec.capacity, capacity_before);
1283      });
1284      secure.unlock_slice(|bytes| assert_eq!(bytes, b"ZYc"));
1285   }
1286
1287   #[cfg(feature = "codec")]
1288   #[test]
1289   fn test_patch_at_survives_reallocation() {
1290      // Growth moves the buffer to a new locked allocation; the patch must land
1291      // in the live one rather than a stale pointer.
1292      let mut secure = SecureBytes::new().unwrap();
1293      secure.extend_from_slice(b"first").unwrap();
1294      secure.reserve(4096);
1295      secure.extend_from_slice(b"second").unwrap();
1296
1297      secure.patch_at(0, b"FIRST");
1298
1299      secure.unlock_slice(|bytes| assert_eq!(bytes, b"FIRSTsecond"));
1300   }
1301
1302   #[cfg(feature = "codec")]
1303   #[test]
1304   #[should_panic(expected = "exceeds length")]
1305   fn test_patch_at_straddling_the_end_panics() {
1306      let mut secure = SecureBytes::from_slice(b"abc").unwrap();
1307
1308      secure.patch_at(2, b"XY");
1309   }
1310
1311   #[cfg(feature = "codec")]
1312   #[test]
1313   #[should_panic(expected = "exceeds length")]
1314   fn test_patch_at_past_the_end_panics() {
1315      let mut secure = SecureBytes::from_slice(b"abc").unwrap();
1316
1317      secure.patch_at(4, b"");
1318   }
1319
1320   #[cfg(feature = "use_os")]
1321   #[test]
1322   fn test_forgotten_drain_keeps_memory_locked() {
1323      let arg = "CRASH_TEST_DRAIN_FORGET_LOCKED";
1324
1325      if std::env::args().any(|a| a == arg) {
1326         let vec: Vec<u8> = vec![1, 2, 3, 4, 5];
1327         let mut secure = SecureVec::from_vec(vec).unwrap();
1328         let drain = secure.drain(..3);
1329         core::mem::forget(drain);
1330
1331         // SAFETY (test-only): a leaked `Drain` must not leave the vector
1332         // exposed, so this deliberately reads a locked, `PROT_NONE` page and is
1333         // expected to fault (the child process dies with SIGSEGV).
1334         let _value = unsafe { core::hint::black_box(*secure.ptr.as_ptr()) };
1335
1336         std::process::exit(1);
1337      }
1338
1339      let child = Command::new(std::env::current_exe().unwrap())
1340         .arg("vec::tests::test_forgotten_drain_keeps_memory_locked")
1341         .arg(arg)
1342         .arg("--nocapture")
1343         .stdout(Stdio::piped())
1344         .stderr(Stdio::piped())
1345         .spawn()
1346         .expect("Failed to spawn child process");
1347
1348      let output = child.wait_with_output().expect("Failed to wait on child");
1349      let status = output.status;
1350
1351      assert!(
1352         !status.success(),
1353         "Process exited successfully with code {:?}, but it should have crashed.",
1354         status.code()
1355      );
1356
1357      #[cfg(unix)]
1358      {
1359         use std::os::unix::process::ExitStatusExt;
1360         let signal = status
1361            .signal()
1362            .expect("Process was not terminated by a signal on Unix.");
1363         assert!(
1364            signal == libc::SIGSEGV || signal == libc::SIGBUS,
1365            "Process terminated with unexpected signal: {}",
1366            signal
1367         );
1368         println!(
1369            "Test passed: Process correctly terminated with signal {}.",
1370            signal
1371         );
1372      }
1373
1374      #[cfg(windows)]
1375      {
1376         const STATUS_ACCESS_VIOLATION: i32 = 0xC0000005_u32 as i32;
1377         assert_eq!(
1378            status.code(),
1379            Some(STATUS_ACCESS_VIOLATION),
1380            "Process exited with unexpected code: {:x?}.",
1381            status.code()
1382         );
1383      }
1384   }
1385
1386   #[cfg(feature = "use_os")]
1387   #[test]
1388   fn test_index_should_fail_when_locked() {
1389      let arg = "CRASH_TEST_SECUREVEC_LOCKED";
1390
1391      if std::env::args().any(|a| a == arg) {
1392         let vec: Vec<u8> = vec![1, 2, 3];
1393         let secure = SecureVec::from_vec(vec).unwrap();
1394         // SAFETY (test-only): deliberately dereferences the locked pointer to
1395         // prove the security model (mlock + `PROT_NONE`) works — the child process
1396         // is expected to die with SIGSEGV.
1397         let _value = unsafe { core::hint::black_box(*secure.ptr.as_ptr()) };
1398
1399         std::process::exit(1);
1400      }
1401
1402      let child = Command::new(std::env::current_exe().unwrap())
1403         .arg("vec::tests::test_index_should_fail_when_locked")
1404         .arg(arg)
1405         .arg("--nocapture")
1406         .stdout(Stdio::piped())
1407         .stderr(Stdio::piped())
1408         .spawn()
1409         .expect("Failed to spawn child process");
1410
1411      let output = child.wait_with_output().expect("Failed to wait on child");
1412      let status = output.status;
1413
1414      assert!(
1415         !status.success(),
1416         "Process exited successfully with code {:?}, but it should have crashed.",
1417         status.code()
1418      );
1419
1420      #[cfg(unix)]
1421      {
1422         use std::os::unix::process::ExitStatusExt;
1423         let signal = status
1424            .signal()
1425            .expect("Process was not terminated by a signal on Unix.");
1426         assert!(
1427            signal == libc::SIGSEGV || signal == libc::SIGBUS,
1428            "Process terminated with unexpected signal: {}",
1429            signal
1430         );
1431         println!(
1432            "Test passed: Process correctly terminated with signal {}.",
1433            signal
1434         );
1435      }
1436
1437      #[cfg(windows)]
1438      {
1439         const STATUS_ACCESS_VIOLATION: i32 = 0xC0000005_u32 as i32;
1440         assert_eq!(
1441            status.code(),
1442            Some(STATUS_ACCESS_VIOLATION),
1443            "Process exited with unexpected code: {:x?}. Expected STATUS_ACCESS_VIOLATION.",
1444            status.code()
1445         );
1446         eprintln!("Test passed: Process correctly terminated with STATUS_ACCESS_VIOLATION.");
1447      }
1448   }
1449}