Skip to main content

secure_types/
array.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#[cfg(not(feature = "use_os"))]
7use alloc::vec::Vec;
8
9use super::{Error, SecureVec, alloc};
10use core::{marker::PhantomData, mem, ptr::NonNull};
11use zeroize::Zeroize;
12
13#[cfg(feature = "use_os")]
14use super::free;
15#[cfg(feature = "use_os")]
16use memsec::Prot;
17
18/// Unlocks the array's memory on construction and re-locks it on drop —
19/// including when the drop happens because the fn closure panicked.
20struct UnlockGuard<'a, T: Zeroize, const LENGTH: usize> {
21   array: &'a SecureArray<T, LENGTH>,
22}
23
24impl<'a, T: Zeroize, const LENGTH: usize> UnlockGuard<'a, T, LENGTH> {
25   fn new(array: &'a SecureArray<T, LENGTH>) -> Self {
26      let ok = array.unlock_memory();
27      debug_assert!(ok, "UnlockGuard::new: unlock_memory failed");
28      UnlockGuard { array }
29   }
30}
31
32impl<'a, T: Zeroize, const LENGTH: usize> Drop for UnlockGuard<'a, T, LENGTH> {
33   fn drop(&mut self) {
34      let ok = self.array.lock_memory();
35      // Failing to re-lock means the protection is silently gone while the value is
36      // still alive, so this is a hard error in every profile.
37      assert!(ok, "UnlockGuard::drop: lock_memory failed");
38   }
39}
40
41/// A fixed-size array allocated in a secure memory region.
42///
43/// ## Security Model
44///
45/// When compiled with the `use_os` feature (the default), it provides several layers of protection:
46/// - **Zeroization on Drop**: The memory is zeroized when the array is dropped.
47/// - **Memory Locking**: The underlying memory pages are locked using `mlock` & `madvise` for (Unix) or
48///   `VirtualLock` & `VirtualProtect` for (Windows) to prevent the OS from memory-dump/swap to disk or other processes accessing the memory.
49///
50/// In a `no_std` environment, it falls back to providing only the **zeroization-on-drop** guarantee.
51///
52/// # Security Note
53///
54/// We intentionally do **not** implement `Index` or `IndexMut`.
55/// `array[0]` is a compile error.
56///
57/// Always use `.unlock()` / `.unlock_mut()` (or the slice variants) to access data.
58///
59/// # Thread Safety
60///
61/// `SecureArray` is `Send` (it can be moved to another thread) but not `Sync`.
62/// `unlock` / `unlock_mut` change the allocation's page protection, so two threads
63/// unlocking the same instance would race (one can relock while the other still
64/// holds a live slice). Share it as `Arc<Mutex<SecureArray<...>>>`.
65///
66/// # Notes
67///
68/// If you return a new allocated `[T; LENGTH]` from one of the unlock methods you are responsible for zeroizing the memory.
69///
70/// # Example
71///
72/// ```
73/// use secure_types::{SecureArray, Zeroize};
74///
75/// let exposed_key: &mut [u8; 32] = &mut [1u8; 32];
76/// let secure_key: SecureArray<u8, 32> = SecureArray::from_slice_mut(exposed_key).unwrap();
77///
78/// secure_key.unlock(|unlocked_slice| {
79///     assert_eq!(unlocked_slice.len(), 32);
80///     assert_eq!(unlocked_slice[0], 1);
81/// });
82///
83/// // Not recommended but if you allocate a new [u8; LENGTH] make sure to zeroize it
84/// let mut exposed = secure_key.unlock(|unlocked_slice| {
85///     [unlocked_slice[0], unlocked_slice[1], unlocked_slice[2]]
86/// });
87///
88/// // Do what you need to to do with the new array
89/// // When you are done with it, zeroize it
90/// exposed.zeroize();
91/// ```
92pub struct SecureArray<T, const LENGTH: usize>
93where
94   T: Zeroize,
95{
96   ptr: NonNull<T>,
97   /// Number of elements that have been initialized (written) so far.
98   ///
99   /// A freshly allocated array starts at `0` and the allocator's poison bytes
100   /// remain in the slots that follow. `drop` and `erase` only zeroize this many
101   /// elements, so they never interpret uninitialized memory as a `T`.
102   initialized: usize,
103   _marker: PhantomData<T>,
104}
105
106unsafe impl<T: Zeroize + Send, const LENGTH: usize> Send for SecureArray<T, LENGTH> {}
107
108impl<T, const LENGTH: usize> SecureArray<T, LENGTH>
109where
110   T: Zeroize,
111{
112   /// Creates an empty (but allocated) SecureArray.
113   ///
114   /// The memory is allocated but not initialized, and it's the caller's responsibility to fill it.
115   ///
116   /// Only elements that are actually written are tracked as initialized: `drop`
117   /// and `erase` zeroize just those, so dropping an array that was never filled is
118   /// sound. The remaining slots still hold the allocator's poison bytes and must
119   /// never be read as a `T`. Initialize the whole array (for example through
120   /// [`unlock_mut`](Self::unlock_mut)) before accessing it.
121   pub fn empty() -> Result<Self, Error> {
122      let size = LENGTH
123         .checked_mul(mem::size_of::<T>())
124         .ok_or(Error::AllocationFailed)?;
125      if size == 0 {
126         // Cannot create a zero-sized secure array
127         return Err(Error::LengthCannotBeZero);
128      }
129
130      // SAFETY: `alloc` is `unsafe` only as a raw-allocation marker — it has no
131      // preconditions beyond rejecting a zero `size`, and returns a pointer
132      // aligned for `T`.
133      let ptr = unsafe { alloc::<T>(size)? };
134
135      let secure_array = SecureArray {
136         ptr,
137         initialized: 0,
138         _marker: PhantomData,
139      };
140
141      let _locked = secure_array.lock_memory();
142
143      #[cfg(feature = "use_os")]
144      if !_locked {
145         return Err(Error::LockFailed);
146      }
147
148      Ok(secure_array)
149   }
150
151   /// Creates a new SecureArray from a `&mut [T; LENGTH]`.
152   ///
153   /// The passed slice is zeroized afterwards
154   pub fn from_slice_mut(content: &mut [T; LENGTH]) -> Result<Self, Error>
155   where
156      T: Clone,
157   {
158      let mut secure_array = match Self::empty() {
159         Ok(secure_array) => secure_array,
160         Err(e) => {
161            content.zeroize();
162            return Err(e);
163         }
164      };
165
166      {
167         let _guard = UnlockGuard::new(&secure_array);
168
169         // SAFETY: the fresh allocation holds `LENGTH` uninitialised slots and
170         // the guard unprotects it. `content` has exactly `LENGTH` elements, so
171         // every `dst.add(i)` is in bounds, and `ptr::write` never reads the
172         // uninitialised destination. `initialized` is committed only after the
173         // loop, so a panic here still leaves the array sound.
174         unsafe {
175            let dst = secure_array.ptr.as_ptr();
176            for (i, item) in content.iter().enumerate() {
177               core::ptr::write(dst.add(i), item.clone());
178            }
179         }
180      }
181      secure_array.initialized = LENGTH;
182
183      content.zeroize();
184
185      Ok(secure_array)
186   }
187
188   /// Creates a new SecureArray from a `&[T; LENGTH]`.
189   ///
190   /// The array is not zeroized, you are responsible for zeroizing it
191   pub fn from_slice(content: &[T; LENGTH]) -> Result<Self, Error>
192   where
193      T: Clone,
194   {
195      let mut secure_array = Self::empty()?;
196
197      {
198         let _guard = UnlockGuard::new(&secure_array);
199
200         // SAFETY: as in `from_slice_mut` — `LENGTH` uninitialised slots in a
201         // fresh allocation, `content` has exactly `LENGTH` elements, `ptr::write`
202         // never reads the destination, and `initialized` is committed afterwards.
203         unsafe {
204            let dst = secure_array.ptr.as_ptr();
205            for (i, item) in content.iter().enumerate() {
206               core::ptr::write(dst.add(i), item.clone());
207            }
208         }
209      }
210      secure_array.initialized = LENGTH;
211
212      Ok(secure_array)
213   }
214
215   pub fn len(&self) -> usize {
216      LENGTH
217   }
218
219   pub fn is_empty(&self) -> bool {
220      self.len() == 0
221   }
222
223   /// Returns the pointer to the locked memory region
224   ///
225   /// # DANGER
226   ///
227   /// This is a low-level API, which should be used only for
228   /// testing purposes. If you need to access the locked memory
229   /// region, use [`unlock`](Self::unlock) or [`unlock_mut`](Self::unlock_mut).
230   #[cfg(feature = "expose-ptr")]
231   #[deprecated(
232      since = "0.3.0",
233      note = "This method is intended only for testing/crash reproduction. Use unlock() or unlock_mut() instead."
234   )]
235   pub fn ptr(&self) -> NonNull<T> {
236      self.ptr
237   }
238
239   pub(crate) fn lock_memory(&self) -> bool {
240      #[cfg(feature = "use_os")]
241      {
242         #[cfg(windows)]
243         {
244            super::mprotect(self.ptr, Prot::NoAccess)
245         }
246         #[cfg(unix)]
247         {
248            super::mprotect(self.ptr, Prot::NoAccess)
249         }
250      }
251      #[cfg(not(feature = "use_os"))]
252      {
253         true // No-op: always "succeeds"
254      }
255   }
256
257   pub(crate) fn unlock_memory(&self) -> bool {
258      #[cfg(feature = "use_os")]
259      {
260         #[cfg(windows)]
261         {
262            super::mprotect(self.ptr, Prot::ReadWrite)
263         }
264         #[cfg(unix)]
265         {
266            super::mprotect(self.ptr, Prot::ReadWrite)
267         }
268      }
269
270      #[cfg(not(feature = "use_os"))]
271      {
272         true // No-op: always "succeeds"
273      }
274   }
275
276   /// Immutable access to the array's data as a `&[T]`
277   ///
278   /// The slice covers exactly the elements that have been initialized: `LENGTH`
279   /// for any array built through a constructor or `unlock_mut`, and empty for
280   /// an [`empty`](Self::empty) array that was never filled (see its contract).
281   pub fn unlock<F, R>(&self, f: F) -> R
282   where
283      F: FnOnce(&[T]) -> R,
284   {
285      let _guard = UnlockGuard::new(self);
286      // SAFETY: the guard unprotects the live allocation. Only the `initialized`
287      // written slots are exposed, so no uninitialised memory is read as a `T`.
288      let slice = unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), self.initialized) };
289      f(slice)
290   }
291
292   /// Mutable access to the array's data as a `&mut [T]`
293   ///
294   /// Exposing the whole array as a `&mut [T]` treats every slot as initialized
295   /// storage, so a later `drop` / `erase` zeroizes all `LENGTH` elements.
296   pub fn unlock_mut<F, R>(&mut self, f: F) -> R
297   where
298      F: FnOnce(&mut [T]) -> R,
299   {
300      self.initialized = LENGTH;
301
302      let _guard = UnlockGuard::new(self);
303      // SAFETY: the guard unprotects the live allocation. Exposing all `LENGTH`
304      // slots as `&mut [T]` is `unlock_mut`'s documented contract — it treats
305      // every slot as initialized storage, which is why `initialized` is set to
306      // `LENGTH` above.
307      let slice = unsafe { core::slice::from_raw_parts_mut(self.ptr.as_ptr(), LENGTH) };
308      f(slice)
309   }
310
311   /// Securely erases the contents of the array by zeroizing the initialized elements.
312   pub fn erase(&mut self) {
313      let _guard = UnlockGuard::new(self);
314
315      // SAFETY: the guard unprotects the live allocation; only the `initialized`
316      // written slots are exposed as `&mut [T]`, so uninitialised memory is never
317      // interpreted as a `T`.
318      unsafe {
319         let slice = core::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.initialized);
320         for element in slice.iter_mut() {
321            element.zeroize();
322         }
323      }
324   }
325
326   /// Same as `SecureVec::init_from_clone`, for the fixed-size buffer.
327   /// `src.len()` must equal `LENGTH`.
328   pub(crate) fn init_from_clone(&mut self, src: &[T])
329   where
330      T: Clone,
331   {
332      debug_assert_eq!(src.len(), LENGTH);
333
334      {
335         let _guard = UnlockGuard::new(self);
336
337         // SAFETY: `src.len() == LENGTH` is the caller's contract, so every
338         // `dst.add(i)` is in bounds of this fixed-size allocation, the guard
339         // unprotects it, and `ptr::write` never reads the unwritten destination.
340         unsafe {
341            let dst = self.ptr.as_ptr();
342            for (i, item) in src.iter().enumerate() {
343               core::ptr::write(dst.add(i), item.clone());
344            }
345         }
346      }
347      // Commit only after every write succeeded, so a panic from `T::clone`
348      // leaves the array with just the elements that were actually written.
349      self.initialized = src.len();
350   }
351}
352
353impl<T: Zeroize, const LENGTH: usize> Drop for SecureArray<T, LENGTH> {
354   fn drop(&mut self) {
355      let ok = self.unlock_memory();
356      debug_assert!(ok, "SecureArray::drop: unlock_memory failed");
357
358      // Only the initialized elements are zeroized. A partially-initialized
359      // array (a panic during `from_slice*` / `init_from_clone`, or an `empty()`
360      // array that was never filled) still holds the allocator's poison bytes in
361      // the remaining slots, and interpreting those as a `T` would dereference
362      // garbage.
363      // SAFETY: the memory was unprotected above; the slice covers only the
364      // `initialized` written slots.
365      let slice = unsafe { core::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.initialized) };
366      for element in slice.iter_mut() {
367         element.zeroize();
368      }
369
370      let size = LENGTH.checked_mul(mem::size_of::<T>()).unwrap_or(0);
371      if size == 0 {
372         return;
373      }
374
375      #[cfg(feature = "use_os")]
376      free(self.ptr);
377
378      #[cfg(not(feature = "use_os"))]
379      // SAFETY: `size` is the full allocation size (`LENGTH * size_of::<T>()`),
380      // the region was unprotected above and is still owned here, and the
381      // `Layout` below matches the one `alloc` used.
382      unsafe {
383         let bytes = core::slice::from_raw_parts_mut(self.ptr.as_ptr() as *mut u8, size);
384         bytes.zeroize();
385
386         let layout = Layout::from_size_align_unchecked(size, mem::align_of::<T>());
387         alloc::alloc::dealloc(self.ptr.as_ptr() as *mut u8, layout);
388      }
389   }
390}
391
392impl<T: Clone + Zeroize, const LENGTH: usize> Clone for SecureArray<T, LENGTH> {
393   /// # Panics
394   ///
395   /// Panics if the clone's secure allocation cannot be made or locked.
396   fn clone(&self) -> Self {
397      let mut new_array = Self::empty().unwrap();
398      self.unlock(|src_slice| {
399         new_array.init_from_clone(src_slice);
400      });
401      new_array
402   }
403}
404
405impl<T: Clone + Zeroize, const LENGTH: usize> TryFrom<SecureVec<T>> for SecureArray<T, LENGTH> {
406   type Error = Error;
407
408   /// Tries to convert a `SecureVec<T>` into a `SecureArray<T, LENGTH>`.
409   ///
410   /// This operation will only succeed if `vec.len() == LENGTH`.
411   /// `LENGTH` is a compile-time constant on the destination type, it cannot
412   /// be taken from the vector's runtime length.
413   ///
414   /// The `SecureVec` is consumed.
415   fn try_from(vec: SecureVec<T>) -> Result<Self, Self::Error> {
416      if vec.len() != LENGTH {
417         return Err(Error::LengthMismatch);
418      }
419
420      let mut new_array = Self::empty()?;
421
422      vec.unlock_slice(|vec_slice| {
423         new_array.init_from_clone(vec_slice);
424      });
425
426      Ok(new_array)
427   }
428}
429
430impl<T: Clone + Zeroize, const LENGTH: usize> TryFrom<Vec<T>> for SecureArray<T, LENGTH> {
431   type Error = Error;
432
433   /// Tries to convert a `Vec<T>` into a `SecureArray<T, LENGTH>`.
434   ///
435   /// This operation will only succeed if `vec.len() == LENGTH`.
436   /// `LENGTH` is a compile-time constant on the destination type, it cannot
437   /// be taken from the vector's runtime length.
438   ///
439   /// The `Vec` is consumed and zeroized.
440   fn try_from(mut vec: Vec<T>) -> Result<Self, Self::Error> {
441      if vec.len() != LENGTH {
442         vec.zeroize();
443         return Err(Error::LengthMismatch);
444      }
445
446      let mut new_array = match Self::empty() {
447         Ok(new_array) => new_array,
448         Err(e) => {
449            vec.zeroize();
450            return Err(e);
451         }
452      };
453
454      new_array.init_from_clone(&vec);
455      vec.zeroize();
456
457      Ok(new_array)
458   }
459}
460
461/// Serializes as a byte buffer, matching the `deserialize_bytes` request of the
462/// `Deserialize` impl below. Formats that support byte buffers get the contents in one
463/// piece rather than element by element; `serde_json` renders either form as an array of
464/// numbers, so its output is unchanged.
465#[cfg(feature = "serde")]
466impl<const LENGTH: usize> serde::Serialize for SecureArray<u8, LENGTH> {
467   fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
468   where
469      S: serde::Serializer,
470   {
471      self.unlock(|slice| serializer.serialize_bytes(slice))
472   }
473}
474
475#[cfg(feature = "serde")]
476impl<'de, const LENGTH: usize> serde::Deserialize<'de> for SecureArray<u8, LENGTH> {
477   fn deserialize<D>(deserializer: D) -> Result<SecureArray<u8, LENGTH>, D::Error>
478   where
479      D: serde::Deserializer<'de>,
480   {
481      struct SecureArrayVisitor<const L: usize>;
482
483      impl<'de, const L: usize> serde::de::Visitor<'de> for SecureArrayVisitor<L> {
484         type Value = SecureArray<u8, L>;
485
486         fn expecting(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
487            write!(formatter, "a byte array of length {}", L)
488         }
489
490         fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
491         where
492            A: serde::de::SeqAccess<'de>,
493         {
494            // Pre-sized to the exact length, and rejected as soon as it overflows, so a
495            // malformed (over-long) input cannot grow the locked buffer.
496            let mut data: SecureVec<u8> =
497               SecureVec::new_with_capacity(L).map_err(serde::de::Error::custom)?;
498
499            while let Some(byte) = seq.next_element::<u8>()? {
500               if data.len() == L {
501                  return Err(serde::de::Error::invalid_length(
502                     data.len() + 1,
503                     &self,
504                  ));
505               }
506
507               data.push(byte);
508            }
509
510            // Check that the deserialized data has the exact length required.
511            if data.len() != L {
512               return Err(serde::de::Error::invalid_length(
513                  data.len(),
514                  &self,
515               ));
516            }
517
518            SecureArray::try_from(data).map_err(serde::de::Error::custom)
519         }
520
521         /// `deserialize_bytes` also accepts a raw byte buffer, so a format can hand the
522         /// array over directly instead of as a sequence of `u8`s.
523         fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
524         where
525            E: serde::de::Error,
526         {
527            let bytes: &[u8; L] = v
528               .try_into()
529               .map_err(|_| serde::de::Error::invalid_length(v.len(), &self))?;
530
531            SecureArray::from_slice(bytes).map_err(serde::de::Error::custom)
532         }
533
534         /// Mirrors `SecureString`'s `visit_string`: wipe the owned buffer the format
535         /// handed over, instead of letting it drop with the plaintext inside.
536         fn visit_byte_buf<E>(self, mut v: Vec<u8>) -> Result<Self::Value, E>
537         where
538            E: serde::de::Error,
539         {
540            let array = self.visit_bytes(&v);
541            v.zeroize();
542            array
543         }
544      }
545
546      deserializer.deserialize_bytes(SecureArrayVisitor::<LENGTH>)
547   }
548}
549
550/// Serializes a `SecureArray<T, LENGTH>` of [`SeqElement`](crate::vec::SeqElement)s as a
551/// tuple of `T` values, matching serde's own `[T; N]` convention.
552///
553/// `SecureArray<u8, LENGTH>` takes the byte-buffer impl above instead; the bound here is
554/// what keeps the two disjoint.
555#[cfg(feature = "serde")]
556impl<const LENGTH: usize, T> serde::Serialize for SecureArray<T, LENGTH>
557where
558   T: crate::vec::SeqElement + serde::Serialize,
559{
560   fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
561   where
562      S: serde::Serializer,
563   {
564      use serde::ser::SerializeTuple;
565
566      let mut tuple = serializer.serialize_tuple(LENGTH)?;
567
568      let elements: Result<(), S::Error> = self.unlock(|slice| {
569         for item in slice {
570            tuple.serialize_element(item)?;
571         }
572
573         Ok(())
574      });
575      elements?;
576
577      tuple.end()
578   }
579}
580
581/// Deserializes a `SecureArray<T, LENGTH>` of [`SeqElement`](crate::vec::SeqElement)s from a
582/// tuple of `T` values.
583#[cfg(feature = "serde")]
584impl<'de, const LENGTH: usize, T> serde::Deserialize<'de> for SecureArray<T, LENGTH>
585where
586   T: crate::vec::SeqElement + Clone + serde::Deserialize<'de>,
587{
588   fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
589   where
590      D: serde::Deserializer<'de>,
591   {
592      struct SecureArraySeqVisitor<const L: usize, T>(::core::marker::PhantomData<T>);
593
594      impl<'de, const L: usize, T> serde::de::Visitor<'de> for SecureArraySeqVisitor<L, T>
595      where
596         T: crate::vec::SeqElement + Clone + serde::Deserialize<'de>,
597      {
598         type Value = SecureArray<T, L>;
599
600         fn expecting(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
601            write!(formatter, "a secure array of length {}", L)
602         }
603
604         fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
605         where
606            A: serde::de::SeqAccess<'de>,
607         {
608            // Pre-sized to the exact length, and rejected as soon as it overflows, so a
609            // malformed (over-long) input cannot grow the locked buffer.
610            let mut data: SecureVec<T> =
611               SecureVec::new_with_capacity(L).map_err(serde::de::Error::custom)?;
612
613            while let Some(element) = seq.next_element::<T>()? {
614               if data.len() == L {
615                  return Err(serde::de::Error::invalid_length(
616                     data.len() + 1,
617                     &self,
618                  ));
619               }
620
621               data.push(element);
622            }
623
624            if data.len() != L {
625               return Err(serde::de::Error::invalid_length(
626                  data.len(),
627                  &self,
628               ));
629            }
630
631            SecureArray::try_from(data).map_err(serde::de::Error::custom)
632         }
633      }
634
635      deserializer.deserialize_tuple(
636         LENGTH,
637         SecureArraySeqVisitor::<LENGTH, T>(::core::marker::PhantomData),
638      )
639   }
640}
641
642#[cfg(all(test, feature = "use_os"))]
643mod tests {
644   use super::*;
645   use std::process::{Command, Stdio};
646
647   #[test]
648   fn lock_unlock() {
649      let exposed: &mut [u8; 3] = &mut [1, 2, 3];
650      let secure: SecureArray<u8, 3> = SecureArray::from_slice_mut(exposed).unwrap();
651
652      let unlocked = secure.unlock_memory();
653      assert!(unlocked);
654
655      let locked = secure.lock_memory();
656      assert!(locked);
657   }
658
659   /// Pins the invariant that only written elements are considered initialized.
660   #[test]
661   fn test_initialized_count_tracking() {
662      let mut array: SecureArray<u8, 3> = SecureArray::empty().unwrap();
663      assert_eq!(array.initialized, 0);
664
665      array.unlock_mut(|slice| {
666         slice[0] = 1;
667         slice[1] = 2;
668         slice[2] = 3;
669      });
670      assert_eq!(array.initialized, 3);
671
672      let from_slice: SecureArray<u8, 3> = SecureArray::from_slice(&[1, 2, 3]).unwrap();
673      assert_eq!(from_slice.initialized, 3);
674   }
675
676   #[test]
677   fn test_index_should_fail_when_locked() {
678      let arg = "CRASH_TEST_ARRAY_LOCKED";
679
680      if std::env::args().any(|a| a == arg) {
681         let exposed: &mut [u8; 3] = &mut [1, 2, 3];
682         let array: SecureArray<u8, 3> = SecureArray::from_slice_mut(exposed).unwrap();
683         // SAFETY (test-only): this deliberately dereferences a locked,
684         // `PROT_NONE` page to prove the access faults — the child process is
685         // expected to die with SIGSEGV, so the read never completes.
686         let _value = unsafe { core::hint::black_box(*array.ptr.as_ptr()) };
687
688         std::process::exit(1);
689      }
690
691      let child = Command::new(std::env::current_exe().unwrap())
692         .arg("array::tests::test_index_should_fail_when_locked")
693         .arg(arg)
694         .arg("--nocapture")
695         .stdout(Stdio::piped())
696         .stderr(Stdio::piped())
697         .spawn()
698         .expect("Failed to spawn child process");
699
700      let output = child.wait_with_output().expect("Failed to wait on child");
701      let status = output.status;
702
703      assert!(
704         !status.success(),
705         "Process exited successfully with code {:?}, but it should have crashed.",
706         status.code()
707      );
708
709      #[cfg(unix)]
710      {
711         use std::os::unix::process::ExitStatusExt;
712         let signal = status
713            .signal()
714            .expect("Process was not terminated by a signal on Unix.");
715         assert!(
716            signal == libc::SIGSEGV || signal == libc::SIGBUS,
717            "Process terminated with unexpected signal: {}",
718            signal
719         );
720         println!(
721            "Test passed: Process correctly terminated with signal {}.",
722            signal
723         );
724      }
725
726      #[cfg(windows)]
727      {
728         const STATUS_ACCESS_VIOLATION: i32 = 0xC0000005_u32 as i32;
729         assert_eq!(
730            status.code(),
731            Some(STATUS_ACCESS_VIOLATION),
732            "Process exited with unexpected code: {:x?}. Expected STATUS_ACCESS_VIOLATION.",
733            status.code()
734         );
735         eprintln!("Test passed: Process correctly terminated with STATUS_ACCESS_VIOLATION.");
736      }
737   }
738}