Skip to main content

secure_types/
lib.rs

1#![doc = include_str!("../readme.md")]
2// no_std is implied whenever `use_os` is not active.
3// `use_os` is the default feature — see Cargo.toml.
4#![cfg_attr(not(feature = "use_os"), no_std)]
5
6#[cfg(not(feature = "use_os"))]
7extern crate alloc;
8
9pub mod array;
10#[cfg(feature = "codec")]
11pub mod codec;
12pub mod string;
13pub mod vec;
14#[cfg(feature = "use_os")]
15pub mod writer;
16
17pub use array::SecureArray;
18pub use string::SecureString;
19pub use vec::{SecureBytes, SecureVec};
20#[cfg(feature = "use_os")]
21pub use writer::SecureBytesWriter;
22
23#[cfg(feature = "serde")]
24pub use vec::SeqElement;
25
26#[cfg(feature = "codec")]
27pub use codec::{
28   DecodeError, EncodeError, FORMAT_VERSION, decode, decode_slice, encode, encode_into_vec, encode_to_vec,
29   encode_to_vec_with_capacity, encode_with_capacity, encoded_len,
30};
31
32use core::ptr::NonNull;
33pub use zeroize::Zeroize;
34
35#[cfg(feature = "use_os")]
36pub use memsec;
37#[cfg(feature = "use_os")]
38use memsec::Prot;
39
40#[derive(Debug)]
41#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
42pub enum Error {
43   AllocationFailed,
44   LengthCannotBeZero,
45   SizeCannotBeZero,
46   NullAllocation,
47   LockFailed,
48   UnlockFailed,
49   LengthMismatch,
50   InvalidUtf8,
51   AlignmentFailed,
52}
53
54impl core::fmt::Display for Error {
55   fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56      match self {
57         Self::AllocationFailed => write!(f, "Failed to allocate memory"),
58         Self::LengthCannotBeZero => write!(f, "Length cannot be zero"),
59         Self::SizeCannotBeZero => write!(f, "Size cannot be zero"),
60         Self::NullAllocation => write!(f, "Allocated Ptr is null"),
61         Self::LockFailed => write!(f, "Failed to lock memory"),
62         Self::UnlockFailed => write!(f, "Failed to unlock memory"),
63         Self::LengthMismatch => {
64            write!(
65               f,
66               "Source length does not match the fixed size of the destination array"
67            )
68         }
69         Self::InvalidUtf8 => write!(f, "Bytes are not valid UTF-8"),
70         Self::AlignmentFailed => write!(f, "Failed to satisfy allocation alignment"),
71      }
72   }
73}
74
75impl core::error::Error for Error {}
76
77#[cfg(all(feature = "use_os", unix))]
78const ALLOC_TAG_MALLOC: usize = 0xDEAD_BEEF;
79// `memfd_secret` is a Linux-only allocator, so the tag that selects it — and the
80// availability probe that caches whether it exists — are only meaningful there.
81#[cfg(all(feature = "use_os", target_os = "linux"))]
82const ALLOC_TAG_MEMFD: usize = 0x5EC0_0000;
83
84#[cfg(all(feature = "use_os", target_os = "linux"))]
85use core::sync::atomic::{AtomicU8, Ordering};
86
87#[cfg(all(feature = "use_os", target_os = "linux"))]
88static MEMFD_SECRET_SUPPORT: AtomicU8 = AtomicU8::new(MEMFD_UNKNOWN);
89#[cfg(all(feature = "use_os", target_os = "linux"))]
90const MEMFD_UNKNOWN: u8 = 0;
91#[cfg(all(feature = "use_os", target_os = "linux"))]
92const MEMFD_NO: u8 = 1;
93#[cfg(all(feature = "use_os", target_os = "linux"))]
94const MEMFD_YES: u8 = 2;
95
96/// Calculates the offset needed to store a usize header while maintaining
97/// the alignment requirements of T.
98#[cfg(all(feature = "use_os", unix))]
99const fn get_header_offset<T>() -> usize {
100   let header_size = core::mem::size_of::<usize>();
101   let align = core::mem::align_of::<T>();
102
103   // If T needs stronger alignment than usize, we must pad more.
104   // Otherwise, sizeof(usize) is sufficient.
105   if align > header_size {
106      align
107   } else {
108      header_size
109   }
110}
111
112/// Reports whether the kernel supports `memfd_secret`-backed allocations.
113///
114/// `memfd_secret` is Linux-only, so this is always `false` on every other
115/// target (including non-Linux Unix like macOS and FreeBSD), where the crate
116/// falls back to [`memsec::malloc_sized`].
117#[cfg(all(feature = "use_os", unix))]
118pub fn supports_memfd_secret() -> bool {
119   #[cfg(target_os = "linux")]
120   {
121      match MEMFD_SECRET_SUPPORT.load(Ordering::Relaxed) {
122         MEMFD_YES => true,
123         MEMFD_NO => false,
124         _ => {
125            // SAFETY: probes `memfd_secret` with no flags and no pointers; any
126            // returned fd is closed immediately.
127            let supported = unsafe {
128               use libc::{SYS_memfd_secret, close, syscall};
129               let res = syscall(SYS_memfd_secret as _, 0isize);
130               if res >= 0 {
131                  close(res as libc::c_int);
132                  true
133               } else {
134                  false
135               }
136            };
137            MEMFD_SECRET_SUPPORT.store(
138               if supported { MEMFD_YES } else { MEMFD_NO },
139               Ordering::Relaxed,
140            );
141            supported
142         }
143      }
144   }
145
146   #[cfg(not(target_os = "linux"))]
147   {
148      false
149   }
150}
151
152/// Allocate memory
153///
154/// For `Windows` it always uses [memsec::malloc_sized]
155///
156/// For `Linux` it uses [memsec::memfd_secret_sized] if `memfd_secret` is supported
157///
158/// For every other `Unix` (macOS, FreeBSD, …) it uses [memsec::malloc_sized]:
159/// `memfd_secret` is Linux-only.
160///
161/// If the allocation fails it fallbacks to [memsec::malloc_sized]
162pub(crate) unsafe fn alloc<T>(size: usize) -> Result<NonNull<T>, Error> {
163   #[cfg(feature = "use_os")]
164   {
165      if size == 0 {
166         return Err(Error::SizeCannotBeZero);
167      }
168
169      #[cfg(windows)]
170      // SAFETY: `size != 0` was checked above; `malloc_sized` returns either a
171      // valid pointer to `size` bytes or `None`, and the pointer is re-checked.
172      unsafe {
173         let allocated_ptr = memsec::malloc_sized(size);
174         let non_null = allocated_ptr.ok_or(Error::AllocationFailed)?;
175         let ptr = non_null.as_ptr() as *mut T;
176         NonNull::new(ptr).ok_or(Error::NullAllocation)
177      }
178
179      #[cfg(unix)]
180      {
181         let header_offset = get_header_offset::<T>();
182
183         // Calculate alignment requirement
184         let align_req = core::mem::align_of::<usize>().max(core::mem::align_of::<T>());
185
186         // Calculate raw size (Header + Data)
187         let raw_size = size
188            .checked_add(header_offset)
189            .ok_or(Error::AllocationFailed)?;
190
191         // Calculate padded size to satisfy alignment
192         let remainder = raw_size % align_req;
193         let alloc_size = if remainder == 0 {
194            raw_size
195         } else {
196            raw_size
197               .checked_add(align_req - remainder)
198               .ok_or(Error::AllocationFailed)?
199         };
200
201         // `memfd_secret` is Linux-only; everywhere else the malloc fallback
202         // below is the sole allocator, so the probe and the tag are compiled out.
203         #[cfg(target_os = "linux")]
204         {
205            let ptr_opt = if supports_memfd_secret() {
206               // SAFETY: `memsec` allocation of the byte count computed above;
207               // its result is checked for null before use.
208               unsafe { memsec::memfd_secret_sized(alloc_size) }
209            } else {
210               None
211            };
212
213            if let Some(raw_ptr_nonnull) = ptr_opt {
214               let raw_ptr = raw_ptr_nonnull.as_ptr() as *mut u8;
215
216               debug_assert!(
217                  (raw_ptr as usize).is_multiple_of(core::mem::align_of::<usize>()),
218                  "allocator returned a pointer not aligned for the usize header tag"
219               );
220
221               // SAFETY: `raw_ptr` is `memsec`'s user pointer for a live
222               // `alloc_size`-byte allocation, aligned for a `usize` (asserted
223               // above). The tag goes at offset 0 and the user region starts at
224               // `header_offset`, which `alloc_size` reserves — both in bounds.
225               unsafe { *(raw_ptr as *mut usize) = ALLOC_TAG_MEMFD };
226
227               // SAFETY: `header_offset <= alloc_size`, so the offset pointer
228               // stays inside the allocation.
229               let user_ptr = unsafe { raw_ptr.add(header_offset) as *mut T };
230               return NonNull::new(user_ptr).ok_or(Error::NullAllocation);
231            }
232         }
233
234         // SAFETY: as in the memfd branch — `memsec`'s user pointer for an
235         // `alloc_size`-byte allocation, aligned for the `usize` tag, with the tag
236         // at offset 0 and the user region at `header_offset`.
237         unsafe {
238            let allocated_ptr = memsec::malloc_sized(alloc_size);
239            let non_null = allocated_ptr.ok_or(Error::AllocationFailed)?;
240
241            let raw_ptr = non_null.as_ptr() as *mut u8;
242
243            debug_assert!(
244               (raw_ptr as usize).is_multiple_of(core::mem::align_of::<usize>()),
245               "allocator returned a pointer not aligned for the usize header tag"
246            );
247
248            // Write the MALLOC tag
249            *(raw_ptr as *mut usize) = ALLOC_TAG_MALLOC;
250
251            let user_ptr = raw_ptr.add(header_offset) as *mut T;
252            NonNull::new(user_ptr).ok_or(Error::NullAllocation)
253         }
254      }
255   }
256
257   #[cfg(not(feature = "use_os"))]
258   {
259      // `alloc::alloc::alloc` requires a non-zero-size layout, so a zero-sized
260      // `T` (a ZST, where `capacity * size_of::<T>() == 0`) must be rejected
261      // rather than handed to it. `use_os` refuses the same input.
262      if size == 0 {
263         return Err(Error::SizeCannotBeZero);
264      }
265
266      let layout = core::alloc::Layout::from_size_align(size, core::mem::align_of::<T>())
267         .map_err(|_| Error::AlignmentFailed)?;
268      // SAFETY: `size != 0` was checked just above, so the `Layout` is valid for
269      // `alloc`, which returns either an aligned pointer or null.
270      let ptr = unsafe { alloc::alloc::alloc(layout) as *mut T };
271      if ptr.is_null() {
272         return Err(Error::NullAllocation);
273      }
274      // SAFETY: the null case returned above, so `ptr` is non-null.
275      unsafe { Ok(NonNull::new_unchecked(ptr)) }
276   }
277}
278
279#[cfg(feature = "use_os")]
280pub(crate) fn free<T>(ptr: NonNull<T>) {
281   #[cfg(windows)]
282   // SAFETY: `ptr` was returned by `memsec::malloc_sized` in `alloc` and is freed
283   // exactly once here, so `memsec::free` receives the pointer it handed out.
284   unsafe {
285      memsec::free(ptr);
286   }
287
288   #[cfg(unix)]
289   {
290      let header_offset = get_header_offset::<T>();
291
292      // SAFETY: `ptr` is this allocation's user pointer, so `ptr - header_offset`
293      // is exactly the pointer `memsec` returned; `alloc` wrote the tag there, so
294      // reading it back and dispatching to the matching deallocator is sound. The
295      // allocation is freed exactly once (this consumes the `NonNull`).
296      unsafe {
297         let user_ptr = ptr.as_ptr() as *mut u8;
298         let raw_ptr = user_ptr.sub(header_offset);
299
300         // Reconstruct the NonNull pointer to the START of the allocation (header)
301         let non_null_raw = NonNull::new_unchecked(raw_ptr);
302
303         // Read the tag
304         let tag = *(raw_ptr as *const usize);
305
306         match tag {
307            #[cfg(target_os = "linux")]
308            ALLOC_TAG_MEMFD => {
309               memsec::free_memfd_secret(non_null_raw);
310            }
311            ALLOC_TAG_MALLOC => {
312               memsec::free(non_null_raw);
313            }
314            _ => {
315               // Tag mismatch: double free or a corrupted header. Freeing through
316               // the wrong allocator would be worse, and silently doing nothing
317               // leaks the allocation, so fail loudly in every profile — memsec
318               // itself aborts on a canary mismatch.
319               panic!(
320                  "SecureAllocator: Corrupt header tag found: {:x}",
321                  tag
322               );
323            }
324         }
325      }
326   }
327}
328
329#[cfg(feature = "use_os")]
330pub(crate) fn mprotect<T>(ptr: NonNull<T>, prot: Prot::Ty) -> bool {
331   #[cfg(unix)]
332   {
333      // We need to protect the whole block, including the header.
334      let header_offset = get_header_offset::<T>();
335      // SAFETY: `ptr - header_offset` is the pointer `memsec` returned (see
336      // `alloc`), which is what `memsec::mprotect` expects, and `prot` is a valid
337      // `Prot` value. The block stays allocated while `ptr` is live.
338      unsafe {
339         let raw_ptr = (ptr.as_ptr() as *mut u8).sub(header_offset);
340         let raw_non_null = NonNull::new_unchecked(raw_ptr as *mut T);
341
342         memsec::mprotect(raw_non_null, prot)
343      }
344   }
345   #[cfg(windows)]
346   {
347      // SAFETY: `ptr` is a live `memsec` allocation and `prot` a valid `Prot`.
348      unsafe { memsec::mprotect(ptr, prot) }
349   }
350}