Skip to main content

vitaminc_protected/locked/
mod.rs

1//! `Locked<T>`: storage for secrets that outlive a call.
2//!
3//! See the [`Locked`] docs for what it guarantees and when to use it instead
4//! of [`Protected`](crate::Protected).
5
6use crate::{AsProtectedRef, Choice, OpaqueDebug, ProtectedRef, TimingSafeEq, Zeroed};
7use core::alloc::Layout;
8use core::any::type_name;
9use core::fmt;
10use core::marker::PhantomData;
11use core::mem::{self, ManuallyDrop, MaybeUninit};
12use core::ptr;
13use core::sync::atomic::{AtomicU8, Ordering};
14use std::io;
15use zeroize::{Zeroize, ZeroizeOnDrop};
16
17mod layout;
18#[cfg(kani)]
19mod proofs;
20
21/// The smaps readers and the `mlock` probe the process tests use, shared
22/// with the unit tests here. Declared at file level because a `#[path]`
23/// is resolved through the directories of its enclosing modules, and an
24/// inline test module has none on disk to walk `..` through.
25#[cfg(all(test, target_os = "linux", not(miri)))]
26#[path = "../../tests/common/mod.rs"]
27mod common;
28
29#[cfg(all(unix, not(kani)))]
30mod unix;
31#[cfg(all(unix, not(kani)))]
32use unix::Region;
33
34// The fallback is the backend off Unix and under Kani, which cannot execute
35// system calls. It is also built into every test binary so that its own
36// tests run on the platforms CI has.
37#[cfg(any(test, not(unix), kani))]
38mod fallback;
39#[cfg(any(not(unix), kani))]
40use fallback::Region;
41
42/// Why a [`Locked`] value could not be created, or why its memory is not
43/// locked.
44///
45/// [`Refused`](LockError::Refused), [`Dump`](LockError::Dump),
46/// [`Untracked`](LockError::Untracked), [`Unavailable`](LockError::Unavailable)
47/// and [`Forked`](LockError::Forked) describe a *degraded* value that
48/// exists: under [`LockPolicy::BestEffort`] the first four are reported by
49/// [`Locked::lock_error`] from construction on, and `Forked` from the moment
50/// a forked child looks. [`Locked::require_locked`] turns any of them into
51/// an error and drops the value. The other variants mean no value was
52/// created.
53#[derive(Debug)]
54#[non_exhaustive]
55pub enum LockError {
56    /// The operating system would not map the region at all.
57    Map {
58        /// The size of the mapping requested, guard pages included.
59        bytes: usize,
60        /// The OS error.
61        source: io::Error,
62    },
63    /// The region was mapped but a guard page could not be protected.
64    Guard {
65        /// The OS error.
66        source: io::Error,
67    },
68    /// `mlock` refused the interior, almost always because it would exceed
69    /// `RLIMIT_MEMLOCK` (64 KiB by default on many Linux hosts). Every
70    /// value costs at least one page of that limit whatever its size, so
71    /// the default holds sixteen 4 KiB-page values, or a single one where
72    /// pages are 64 KiB. Raise the limit with `ulimit -l`, `LimitMEMLOCK=`
73    /// in a systemd unit, or the container runtime's ulimit setting.
74    Refused {
75        /// The bytes that would have been locked.
76        bytes: usize,
77        /// The soft `RLIMIT_MEMLOCK` at the time, when it is finite and readable.
78        limit: Option<u64>,
79        /// The OS error.
80        source: io::Error,
81    },
82    /// The region is locked but could not be excluded from core dumps
83    /// (`madvise(MADV_DONTDUMP)` on Linux or Android was refused, which a
84    /// seccomp filter can do). The bytes would appear in a core dump.
85    Dump {
86        /// The OS error.
87        source: io::Error,
88    },
89    /// This platform or build has no memory locking: non-Unix targets, Miri
90    /// (which maps the region but cannot lock it) and Kani. The value is
91    /// wiped on drop as usual.
92    Unavailable,
93    /// The value was created in another process. Memory locks are not
94    /// inherited across `fork`, so in the child the region is unlocked
95    /// until [`Locked::relock`] is called there.
96    Forked,
97    /// This process cannot tell its values when they cross a `fork`: the
98    /// atfork handler that counts forks could not be registered (only want
99    /// of memory makes `pthread_atfork` fail). A forked child would then
100    /// inherit a lock it does not have with nothing to say so, so the
101    /// value is treated as unlocked here.
102    Untracked {
103        /// The OS error.
104        source: io::Error,
105    },
106    /// `T` needs an alignment larger than a page, which the region cannot
107    /// provide.
108    Alignment {
109        /// `T`'s alignment.
110        align: usize,
111        /// The page size, or 0 when the backend has no pages.
112        page: usize,
113    },
114}
115
116impl fmt::Display for LockError {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        match self {
119            Self::Map { bytes, source } => {
120                write!(f, "could not map {bytes} bytes for locked storage: {source}")
121            }
122            Self::Guard { source } => write!(f, "could not protect a guard page: {source}"),
123            Self::Refused {
124                bytes,
125                limit: Some(limit),
126                source,
127            } => write!(
128                f,
129                "locking {bytes} bytes was refused ({source}); the RLIMIT_MEMLOCK soft limit is {limit} bytes"
130            ),
131            Self::Refused {
132                bytes,
133                limit: None,
134                source,
135            } => write!(f, "locking {bytes} bytes was refused ({source})"),
136            Self::Dump { source } => {
137                write!(f, "excluding the region from core dumps was refused ({source})")
138            }
139            Self::Unavailable => f.write_str("memory locking is not available on this platform"),
140            Self::Forked => f.write_str(
141                "the lock belongs to the process that created the value; a forked child inherits the memory but not the lock",
142            ),
143            Self::Untracked { source } => write!(
144                f,
145                "forks cannot be tracked in this process ({source}), so a forked child would misreport the lock"
146            ),
147            Self::Alignment { align, page } => write!(
148                f,
149                "alignment {align} exceeds the page size {page}; locked storage cannot hold this type"
150            ),
151        }
152    }
153}
154
155impl std::error::Error for LockError {
156    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
157        match self {
158            Self::Map { source, .. }
159            | Self::Guard { source }
160            | Self::Refused { source, .. }
161            | Self::Dump { source }
162            | Self::Untracked { source } => Some(source),
163            Self::Unavailable | Self::Forked | Self::Alignment { .. } => None,
164        }
165    }
166}
167
168/// What a [`Locked`] constructor does when the operating system refuses to
169/// lock the memory or, on Linux, to exclude it from core dumps.
170///
171/// The policy is process-wide and set at most once, with [`LockPolicy::set`].
172/// Until it is set, [`BestEffort`](LockPolicy::BestEffort) applies.
173///
174/// # Which to choose
175///
176/// A refused lock loses exactly one property: under memory pressure the
177/// pages holding the secret may be written to swap, which is not wiped when
178/// the process exits. On a host with no swap there is nothing to lose.
179/// Everything else, including the wipe on drop and the exclusion from core
180/// dumps on Linux, still holds. A refused dump exclusion, which is rare and
181/// takes a seccomp filter to provoke, is treated the same way.
182///
183/// `BestEffort` is the default because a strict default fails in development
184/// and CI, where swap exposure is irrelevant, and teaches people to turn it
185/// off. A service that needs the guarantee sets `Strict` once at startup, or
186/// checks [`Locked::locked`] on the values it cares about.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
188pub enum LockPolicy {
189    /// Create the value anyway and record the refusal, readable from
190    /// [`Locked::lock_error`]. When more than one protection is refused,
191    /// the lock refusal is the one recorded.
192    #[default]
193    BestEffort,
194    /// Fail the constructor with the refusal. Where locking is unavailable
195    /// altogether (non-Unix targets, Miri, Kani) that is every constructor:
196    /// [`LockError::Unavailable`] is a refusal too.
197    Strict,
198}
199
200const POLICY_UNSET: u8 = 0;
201const POLICY_BEST_EFFORT: u8 = 1;
202const POLICY_STRICT: u8 = 2;
203static POLICY: AtomicU8 = AtomicU8::new(POLICY_UNSET);
204
205/// The process policy was already set to a different value.
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub struct LockPolicyError {
208    /// The policy in force.
209    pub current: LockPolicy,
210}
211
212impl fmt::Display for LockPolicyError {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        write!(f, "the lock policy is already set to {:?}", self.current)
215    }
216}
217
218impl std::error::Error for LockPolicyError {}
219
220impl LockPolicy {
221    fn encode(self) -> u8 {
222        match self {
223            Self::BestEffort => POLICY_BEST_EFFORT,
224            Self::Strict => POLICY_STRICT,
225        }
226    }
227
228    fn decode(raw: u8) -> Self {
229        match raw {
230            POLICY_STRICT => Self::Strict,
231            _ => Self::BestEffort,
232        }
233    }
234
235    /// Set the process-wide policy. Succeeds at most once, or again with the
236    /// same value; a different value after the first is an error carrying the
237    /// policy in force.
238    ///
239    /// ```
240    /// # mod vitaminc { pub mod protected { pub use vitaminc_protected::*; } }
241    /// use vitaminc::protected::LockPolicy;
242    ///
243    /// // A test binary runs many tests in one process; set once, tolerate repeats.
244    /// let _ = LockPolicy::BestEffort.set();
245    /// assert_eq!(LockPolicy::current(), LockPolicy::BestEffort);
246    /// ```
247    pub fn set(self) -> Result<(), LockPolicyError> {
248        let wanted = self.encode();
249        match POLICY.compare_exchange(POLICY_UNSET, wanted, Ordering::AcqRel, Ordering::Acquire) {
250            Ok(_) => Ok(()),
251            Err(current) if current == wanted => Ok(()),
252            Err(current) => Err(LockPolicyError {
253                current: Self::decode(current),
254            }),
255        }
256    }
257
258    /// The policy in force: the one set with [`set`](Self::set), or
259    /// `BestEffort`.
260    pub fn current() -> Self {
261        Self::decode(POLICY.load(Ordering::Acquire))
262    }
263}
264
265/// A secret stored in memory that is locked against swapping, excluded from
266/// core dumps where the platform allows, fenced by guard pages, and wiped
267/// when dropped.
268///
269/// `Protected<T>` stores its value inline and wipes it on drop, which is the
270/// right shape for a value that lives for one call. It is the wrong shape for
271/// a value that lives for the process: nothing drops a static, a leaked
272/// `Arc`, or anything at all on `SIGTERM`, `SIGKILL` or `process::exit`. The
273/// bytes of a long-lived key then outlive the process in a core dump, in
274/// swap, and at every address a move left them at.
275///
276/// `Locked<T>` closes each of those at allocation time, where the caller
277/// cannot get it wrong, instead of at exit time, where they can. On Unix:
278///
279/// - **The value never moves.** It is written once into a region obtained
280///   from the operating system with `mmap` and only ever reached by
281///   reference. The `Locked<T>` handle itself is four words; moving it
282///   moves no secret bytes.
283/// - **Not swapped.** The region is `mlock`ed, so the kernel keeps it
284///   resident. This can be refused; see [`LockPolicy`].
285/// - **Not dumped.** On Linux and Android the region is marked
286///   `MADV_DONTDUMP`, so a core dump does not contain it.
287/// - **Fenced.** The value sits against a `PROT_NONE` guard page, within
288///   `align_of::<T>()` bytes of it, so a write past its end faults instead
289///   of silently reaching a neighbour. A second guard page precedes the
290///   region and catches an underrun that crosses it.
291/// - **Wiped.** On drop, `T` is zeroized, its destructor runs, then every
292///   byte of the interior is overwritten with volatile stores before it is
293///   unmapped. The
294///   store goes through a pointer the compiler cannot prove dead, so it
295///   cannot be elided, and it is part of releasing the region rather than
296///   of `Locked`'s destructor, so a panic in `T`'s destructor cannot skip it.
297///
298/// # Forking
299///
300/// A forked child inherits the mapping, its guard pages and, on Linux, the
301/// dump exclusion, but the kernel does not carry memory locks across
302/// `fork`. A `Locked` value knows which process locked it, by a fork
303/// generation counted in an atfork handler (so a recycled pid cannot fool
304/// it; a child made by a raw `clone` rather than `fork` is not seen): in a
305/// child,
306/// [`locked`](Self::locked) is `false`, [`lock_error`](Self::lock_error)
307/// is [`LockError::Forked`] and [`require_locked`](Self::require_locked)
308/// fails, until [`relock`](Self::relock) is called there. Where the choice
309/// exists, create keys after forking rather than before.
310///
311/// # Elsewhere
312///
313/// On targets other than Unix, and under Kani, the value lives in an
314/// ordinary heap allocation: wiped on drop exactly as above, but neither
315/// locked nor fenced, and [`lock_error`](Self::lock_error) reports
316/// [`LockError::Unavailable`]. Under Miri the region is mapped for real but
317/// not locked or fenced, since Miri has no model for those calls.
318///
319/// # What it cannot do
320///
321/// `Locked` protects the bytes `T` occupies *inline*. A `T` that owns a heap
322/// allocation (`Vec<u8>`, `String`) has only its header in the region and
323/// is zeroized on drop by its own `Zeroize` impl, but while it lives the
324/// heap buffer is ordinary memory. Use it for inline types: `[u8; N]`, or a
325/// struct of them.
326///
327/// Constructing from an existing value with [`new`](Self::new) copies it
328/// into the region and wipes the parameter slot, but a Rust move is a
329/// bitwise copy the compiler may leave behind, so a value that existed
330/// before the call may survive it in ordinary memory.
331/// [`generate`](Self::generate) builds the value in place and is the
332/// constructor to prefer for a secret that does not yet exist.
333///
334/// # Example
335///
336/// ```
337/// # mod vitaminc { pub mod protected { pub use vitaminc_protected::*; } }
338/// use vitaminc::protected::{Locked, LockError};
339///
340/// # fn fill_from_csprng(buf: &mut [u8; 32]) { buf.copy_from_slice(&[7u8; 32]); }
341/// # fn main() -> Result<(), LockError> {
342/// // Born inside the locked region; never on the stack.
343/// let key: Locked<[u8; 32]> = Locked::generate(fill_from_csprng)?;
344///
345/// key.with(|k| assert_eq!(k[0], 7));
346/// if !key.locked() {
347///     eprintln!("key memory is not locked: {}", key.lock_error().unwrap());
348/// }
349/// # Ok(())
350/// # }
351/// ```
352pub struct Locked<T: Zeroize> {
353    storage: Storage<T>,
354}
355
356// SAFETY: `Locked<T>` owns its region exclusively, exactly as `Box<T>` owns
357// its allocation, so it is `Send` / `Sync` precisely when `T` is.
358unsafe impl<T: Zeroize + Send> Send for Locked<T> {}
359unsafe impl<T: Zeroize + Sync> Sync for Locked<T> {}
360
361/// Storage for a `T`: a region sized for it with the process [`LockPolicy`]
362/// applied. It holds a `T` only once a constructor has written one, and
363/// that constructor then wraps it in a [`Locked`], which is the proof: a
364/// bare `Storage` drops no `T`, so a panic between the allocation and the
365/// write (in `T::zeroed()`, say) releases zero-filled bytes instead of
366/// running `T`'s destructor over them.
367struct Storage<T> {
368    region: Region,
369    /// Boxed so that the common, locked case costs one word.
370    lock: Option<Box<LockError>>,
371    _value: PhantomData<T>,
372}
373
374impl<T> Storage<T> {
375    fn allocate() -> Result<Self, LockError> {
376        let (region, lock) = Region::allocate(Layout::new::<T>())?;
377        if LockPolicy::current() == LockPolicy::Strict {
378            if let Some(err) = lock {
379                // `region` is dropped here, wiped and released.
380                return Err(err);
381            }
382        }
383        Ok(Self {
384            region,
385            lock: lock.map(Box::new),
386            _value: PhantomData,
387        })
388    }
389
390    fn as_ptr(&self) -> *mut T {
391        self.region
392            .value_ptr(Layout::new::<T>())
393            .as_ptr()
394            .cast::<T>()
395    }
396
397    /// # Safety
398    ///
399    /// The interior must hold a live `T`.
400    unsafe fn filled(self) -> Locked<T>
401    where
402        T: Zeroize,
403    {
404        Locked { storage: self }
405    }
406
407    /// Copy the `T` in `slot` into the region, wipe `slot`'s bytes, and
408    /// become a [`Locked`].
409    ///
410    /// # Safety
411    ///
412    /// `slot` must never be read as a `T` again: after this call its bytes
413    /// are zero and no destructor has run on the value they held.
414    unsafe fn move_in(self, slot: &mut ManuallyDrop<T>) -> Locked<T>
415    where
416        T: Zeroize,
417    {
418        // SAFETY (for the caller's contract): the interior is at least
419        // `size_of::<T>()` bytes, aligned to `align_of::<T>()`, and holds no
420        // `T` yet, so a bitwise copy in is a move. The source is then wiped
421        // bitwise, with no semantic effect on `T` since it is never read
422        // again. After the copy the interior holds a live `T`, which
423        // `filled` requires.
424        ptr::copy_nonoverlapping(&**slot as *const T, self.as_ptr(), 1);
425        wipe_bytes(&mut **slot as *mut T);
426        self.filled()
427    }
428}
429
430impl<T: Zeroize> Locked<T> {
431    fn as_ptr(&self) -> *mut T {
432        self.storage.as_ptr()
433    }
434
435    /// Move `value` into locked storage.
436    ///
437    /// The parameter slot is wiped after the copy, without running `T`'s
438    /// destructor, and zeroized if no region could be obtained. That is
439    /// the most this constructor can do: a Rust move is a bitwise copy the
440    /// compiler is free to leave behind, in the caller's frame or in a
441    /// spill, so a value that existed before the call may survive it in
442    /// ordinary memory. [`generate`](Self::generate) is the constructor
443    /// that never has the secret outside the region.
444    ///
445    /// ```
446    /// # mod vitaminc { pub mod protected { pub use vitaminc_protected::*; } }
447    /// use vitaminc::protected::{Locked, LockError};
448    ///
449    /// # fn main() -> Result<(), LockError> {
450    /// let key = Locked::new([0xA5u8; 16])?;
451    /// assert_eq!(key.risky_ref(), &[0xA5u8; 16]);
452    /// # Ok(())
453    /// # }
454    /// ```
455    pub fn new(mut value: T) -> Result<Self, LockError> {
456        let storage = match Storage::allocate() {
457            Ok(storage) => storage,
458            Err(err) => {
459                // `T: Zeroize` does not imply `ZeroizeOnDrop`; the input is
460                // ours now and must not leave here intact.
461                value.zeroize();
462                return Err(err);
463            }
464        };
465        let mut slot = ManuallyDrop::new(value);
466        // SAFETY: `slot` is this function's own parameter and is never read
467        // again after the move.
468        Ok(unsafe { storage.move_in(&mut slot) })
469    }
470
471    /// Build the value in place: the region is filled with `T::zeroed()` and
472    /// `f` is given `&mut T` to fill in. The secret is never on the stack.
473    ///
474    /// Name `T` on the call (`Locked::<[u8; 32]>::generate`) or on the
475    /// closure parameter; a method call inside the closure cannot be
476    /// resolved from the binding's type alone.
477    ///
478    /// ```
479    /// # mod vitaminc { pub mod protected { pub use vitaminc_protected::*; } }
480    /// use vitaminc::protected::{Locked, LockError};
481    ///
482    /// # fn main() -> Result<(), LockError> {
483    /// let key = Locked::<[u8; 8]>::generate(|k| k.copy_from_slice(b"12345678"))?;
484    /// assert_eq!(key.risky_ref(), b"12345678");
485    /// # Ok(())
486    /// # }
487    /// ```
488    pub fn generate<F>(f: F) -> Result<Self, LockError>
489    where
490        T: Zeroed,
491        F: FnOnce(&mut T),
492    {
493        let mut this = Self::zeroed()?;
494        f(this.risky_mut());
495        Ok(this)
496    }
497
498    /// [`generate`](Self::generate) for a filler that can fail. A failure
499    /// drops the half-built value, wiping it.
500    ///
501    /// ```
502    /// # mod vitaminc { pub mod protected { pub use vitaminc_protected::*; } }
503    /// use vitaminc::protected::{Locked, LockError};
504    ///
505    /// #[derive(Debug)]
506    /// enum KeyError { Lock(LockError), Rng }
507    /// impl From<LockError> for KeyError {
508    ///     fn from(e: LockError) -> Self { KeyError::Lock(e) }
509    /// }
510    ///
511    /// let key: Result<Locked<[u8; 32]>, KeyError> = Locked::try_generate(|_k| Err(KeyError::Rng));
512    /// assert!(matches!(key, Err(KeyError::Rng)));
513    /// ```
514    pub fn try_generate<F, E>(f: F) -> Result<Self, E>
515    where
516        T: Zeroed,
517        E: From<LockError>,
518        F: FnOnce(&mut T) -> Result<(), E>,
519    {
520        let mut this = Self::zeroed()?;
521        f(this.risky_mut())?;
522        Ok(this)
523    }
524
525    /// Locked storage holding `T::zeroed()`.
526    pub fn zeroed() -> Result<Self, LockError>
527    where
528        T: Zeroed,
529    {
530        let storage = Storage::allocate()?;
531        // A panic in `T::zeroed()` unwinds through `storage`, which releases
532        // the region without treating its zero-filled bytes as a `T`.
533        let zeroed = T::zeroed();
534        // SAFETY: the interior is sized and aligned for a `T` and holds none
535        // yet; after the write it holds one, which `filled` requires.
536        unsafe {
537            ptr::write(storage.as_ptr(), zeroed);
538            Ok(storage.filled())
539        }
540    }
541
542    /// Whether every protection the platform offers was applied, in this
543    /// process: the memory is locked against swapping and, on Linux,
544    /// excluded from core dumps. [`lock_error`](Self::lock_error) says which
545    /// one was refused, or that the lock was left behind by a `fork`.
546    pub fn locked(&self) -> bool {
547        self.storage.lock.is_none() && self.storage.region.same_process()
548    }
549
550    /// Why the memory is not fully protected, when it is not. A crossed
551    /// `fork` is reported ahead of any refusal recorded before it, as
552    /// [`require_locked`](Self::require_locked) does, since
553    /// [`relock`](Self::relock) is the answer to it.
554    pub fn lock_error(&self) -> Option<&LockError> {
555        static FORKED: LockError = LockError::Forked;
556        if !self.storage.region.same_process() {
557            return Some(&FORKED);
558        }
559        self.storage.lock.as_deref()
560    }
561
562    /// Take the lock again in the current process, and re-apply the dump
563    /// exclusion with it. For a forked child that needs a value its parent
564    /// created; in the same process it repeats what construction did and
565    /// reports the fresh outcome. On success the value is fully protected
566    /// here; on refusal the reason is kept and returned, exactly as after
567    /// construction under [`LockPolicy::BestEffort`].
568    pub fn relock(&mut self) -> Result<(), &LockError> {
569        self.storage.lock = self.storage.region.relock().map(Box::new);
570        match self.storage.lock.as_deref() {
571            None => Ok(()),
572            Some(err) => Err(err),
573        }
574    }
575
576    /// Demand the lock: `Ok(self)` when locked here, otherwise the refusal
577    /// (or [`LockError::Forked`] in a child that has not called
578    /// [`relock`](Self::relock)), with the value dropped and wiped. This is
579    /// the per-value form of [`LockPolicy::Strict`]:
580    ///
581    /// ```
582    /// # mod vitaminc { pub mod protected { pub use vitaminc_protected::*; } }
583    /// use vitaminc::protected::{Locked, LockError};
584    ///
585    /// fn load_key() -> Result<Locked<[u8; 32]>, LockError> {
586    ///     Locked::<[u8; 32]>::generate(|k| k.fill(1))?.require_locked()
587    /// }
588    /// ```
589    pub fn require_locked(mut self) -> Result<Self, LockError> {
590        if !self.storage.region.same_process() {
591            return Err(LockError::Forked);
592        }
593        match self.storage.lock.take() {
594            None => Ok(self),
595            Some(err) => Err(*err),
596        }
597    }
598
599    /// A shared reference to the value. Named like
600    /// [`Controlled::risky_ref`](crate::Controlled::risky_ref): anything you
601    /// copy out of it is yours to wipe.
602    pub fn risky_ref(&self) -> &T {
603        // SAFETY: every constructor writes a `T` into the interior before
604        // returning, and only `drop` invalidates it.
605        unsafe { &*self.as_ptr() }
606    }
607
608    /// An exclusive reference to the value.
609    pub fn risky_mut(&mut self) -> &mut T {
610        // SAFETY: as `risky_ref`, and `&mut self` makes the borrow exclusive.
611        unsafe { &mut *self.as_ptr() }
612    }
613
614    /// Run `f` over the value.
615    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
616        f(self.risky_ref())
617    }
618
619    /// Change the value in place.
620    pub fn update(&mut self, f: impl FnOnce(&mut T)) {
621        f(self.risky_mut())
622    }
623
624    /// A second locked region holding a clone of the value.
625    ///
626    /// There is no `Clone` impl because a clone allocates and can fail. The
627    /// clone is made on the stack and moved in with [`new`](Self::new), so
628    /// it pays that constructor's one copy.
629    pub fn try_clone(&self) -> Result<Self, LockError>
630    where
631        T: Clone,
632    {
633        Self::new(self.risky_ref().clone())
634    }
635
636    /// Run the destructor and the wipe, and hand back the region so a test
637    /// can look at it before it is released.
638    #[cfg(test)]
639    fn into_wiped_region(self) -> Region {
640        let mut this = ManuallyDrop::new(self);
641        // SAFETY: the interior holds a live `T`, zeroized and dropped
642        // exactly once here as `Locked::drop` would; `this` is
643        // `ManuallyDrop`, so `Locked::drop` never runs.
644        unsafe {
645            (*this.as_ptr()).zeroize();
646            ptr::drop_in_place(this.as_ptr());
647        }
648        this.storage.region.wipe();
649        drop(this.storage.lock.take());
650        // SAFETY: `this` is never used again, so the region is moved out once.
651        unsafe { ptr::read(&this.storage.region) }
652    }
653}
654
655/// Overwrite `len` bytes at `ptr` with volatile zero stores and a compiler
656/// fence, so the wipe cannot be elided as a dead store. The bytes are
657/// written as `MaybeUninit<u8>`, never read, so storage that holds padding
658/// or a dropped value is fine.
659///
660/// # Safety
661///
662/// `ptr` must be valid for writes of `len` bytes.
663pub(super) unsafe fn wipe_raw(ptr: *mut u8, len: usize) {
664    let bytes = core::slice::from_raw_parts_mut(ptr.cast::<MaybeUninit<u8>>(), len);
665    for b in bytes {
666        ptr::write_volatile(b, MaybeUninit::new(0));
667    }
668    core::sync::atomic::compiler_fence(Ordering::SeqCst);
669}
670
671/// [`wipe_raw`] over the bytes of a `T`, without any semantic effect on
672/// `T`. For a moved-from slot only.
673///
674/// # Safety
675///
676/// `slot` must point to `size_of::<T>()` writable bytes that will never be
677/// read as a `T` again.
678unsafe fn wipe_bytes<T>(slot: *mut T) {
679    wipe_raw(slot.cast::<u8>(), mem::size_of::<T>());
680}
681
682impl<T: Zeroize> Drop for Locked<T> {
683    fn drop(&mut self) {
684        /// Runs `T`'s destructor when it goes out of scope, so that a panic
685        /// in `T::zeroize` still drops the value.
686        struct DropValue<T>(*mut T);
687        impl<T> Drop for DropValue<T> {
688            fn drop(&mut self) {
689                // SAFETY: the pointee is a live `T`; this is its only drop.
690                unsafe { ptr::drop_in_place(self.0) };
691            }
692        }
693        let value = DropValue(self.as_ptr());
694        // `T: Zeroize` does not imply `T: ZeroizeOnDrop`: whatever `T` owns
695        // outside the region (a heap buffer, say) is wiped by its own
696        // `zeroize` before its destructor frees it. The region's bytes are
697        // wiped when it is released, whatever happens here.
698        // SAFETY: the pointee is a live `T`, and `&mut self` makes the
699        // access exclusive.
700        unsafe { (*value.0).zeroize() };
701        drop(value);
702        // `region` is then dropped by the field destructor, which wipes and
703        // releases it. Field destructors run even when this body unwinds,
704        // so neither a panicking `zeroize` nor a panicking `drop` can skip
705        // the wipe.
706    }
707}
708
709impl<T: Zeroize> Zeroize for Locked<T> {
710    fn zeroize(&mut self) {
711        self.risky_mut().zeroize();
712    }
713}
714
715/// On drop, `T` is zeroized, then dropped, then the whole region is wiped:
716/// what `T` owns elsewhere is covered by its own `zeroize`, and the inline
717/// bytes by the region, whether or not `T::zeroize` reaches them all.
718impl<T: Zeroize> ZeroizeOnDrop for Locked<T> {}
719
720impl<T: Zeroize> OpaqueDebug for Locked<T> {}
721
722impl<T: Zeroize> fmt::Debug for Locked<T> {
723    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
724        write!(
725            f,
726            "Locked<{}>({})",
727            type_name::<T>(),
728            if self.locked() { "locked" } else { "unlocked" }
729        )
730    }
731}
732
733impl<T: Zeroize + TimingSafeEq> TimingSafeEq for Locked<T> {
734    fn ts_eq(&self, other: &Self) -> Choice {
735        self.risky_ref().ts_eq(other.risky_ref())
736    }
737}
738
739impl<'a, T: Zeroize> AsProtectedRef<'a, T> for Locked<T> {
740    fn as_protected_ref(&'a self) -> ProtectedRef<'a, T> {
741        ProtectedRef(self.risky_ref())
742    }
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748
749    #[test]
750    fn a_value_moved_in_reads_back() {
751        let key = Locked::new([0xA5u8; 32]).unwrap();
752        assert_eq!(key.risky_ref(), &[0xA5u8; 32]);
753    }
754
755    #[test]
756    fn a_generated_value_is_built_in_place() {
757        let key = Locked::<[u8; 32]>::generate(|k| k.fill(9)).unwrap();
758        assert_eq!(key.risky_ref(), &[9u8; 32]);
759        assert!(std::ptr::eq(
760            key.risky_ref().as_ptr(),
761            key.storage
762                .region
763                .value_ptr(Layout::new::<[u8; 32]>())
764                .as_ptr()
765        ));
766    }
767
768    #[test]
769    fn a_failed_generation_returns_the_callers_error() {
770        #[derive(Debug, PartialEq)]
771        enum E {
772            Lock,
773            Rng,
774        }
775        impl From<LockError> for E {
776            fn from(_: LockError) -> Self {
777                E::Lock
778            }
779        }
780        let r: Result<Locked<[u8; 16]>, E> = Locked::try_generate(|_| Err(E::Rng));
781        assert_eq!(r.unwrap_err(), E::Rng);
782    }
783
784    #[test]
785    fn update_changes_the_value_in_place() {
786        let mut key = Locked::new([0u8; 8]).unwrap();
787        let before = key.risky_ref().as_ptr();
788        key.update(|k| k[3] = 42);
789        assert_eq!(key.risky_ref()[3], 42);
790        assert_eq!(before, key.risky_ref().as_ptr());
791    }
792
793    #[test]
794    fn the_region_is_all_zero_after_the_wipe() {
795        let key = Locked::new([0xFFu8; 64]).unwrap();
796        let region = key.into_wiped_region();
797        // SAFETY: the region is still mapped; the value's 64 bytes are
798        // initialised, by the value and then by the wipe.
799        let bytes = unsafe {
800            core::slice::from_raw_parts(region.value_ptr(Layout::new::<[u8; 64]>()).as_ptr(), 64)
801        };
802        assert!(bytes.iter().all(|&b| b == 0));
803    }
804
805    #[test]
806    fn the_value_owns_its_own_drop() {
807        use std::sync::atomic::{AtomicUsize, Ordering};
808        static DROPS: AtomicUsize = AtomicUsize::new(0);
809        struct Counted(u8);
810        impl Zeroize for Counted {
811            fn zeroize(&mut self) {
812                self.0 = 0;
813            }
814        }
815        impl Drop for Counted {
816            fn drop(&mut self) {
817                let _ = DROPS.fetch_add(1, Ordering::SeqCst);
818            }
819        }
820        drop(Locked::new(Counted(1)).unwrap());
821        assert_eq!(DROPS.load(Ordering::SeqCst), 1);
822    }
823
824    #[test]
825    fn a_panicking_zeroed_releases_the_region_without_dropping_a_value() {
826        // `Boom` owns a heap allocation, so `drop_in_place` over the zero-
827        // filled interior would free a null pointer. `T::zeroed()` panics
828        // before any `T` exists, and the region must be released as bytes.
829        struct Boom(#[allow(dead_code)] Box<u8>);
830        impl Zeroize for Boom {
831            fn zeroize(&mut self) {
832                *self.0 = 0;
833            }
834        }
835        impl Zeroed for Boom {
836            fn zeroed() -> Self {
837                panic!("no zero value");
838            }
839        }
840        let r = std::panic::catch_unwind(Locked::<Boom>::zeroed);
841        assert!(r.is_err());
842        let r = std::panic::catch_unwind(|| Locked::<Boom>::generate(|_| {}));
843        assert!(r.is_err());
844    }
845
846    #[test]
847    fn what_the_value_owns_elsewhere_is_zeroized_before_it_is_dropped() {
848        use std::sync::atomic::{AtomicBool, Ordering};
849        static ZEROIZED_FIRST: AtomicBool = AtomicBool::new(false);
850        // Owns a heap buffer the region cannot reach.
851        struct Owning(Vec<u8>);
852        impl Zeroize for Owning {
853            fn zeroize(&mut self) {
854                self.0.zeroize();
855            }
856        }
857        impl Drop for Owning {
858            fn drop(&mut self) {
859                // `Vec::zeroize` wipes and clears; a buffer still holding
860                // its bytes here would be freed intact.
861                ZEROIZED_FIRST.store(self.0.is_empty(), Ordering::SeqCst);
862            }
863        }
864        drop(Locked::new(Owning(vec![0xAB; 64])).unwrap());
865        assert!(ZEROIZED_FIRST.load(Ordering::SeqCst));
866    }
867
868    #[test]
869    fn a_panicking_zeroize_still_drops_the_value_and_releases_the_region() {
870        use std::sync::atomic::{AtomicUsize, Ordering};
871        static DROPS: AtomicUsize = AtomicUsize::new(0);
872        struct Stubborn(#[allow(dead_code)] [u8; 8]);
873        impl Zeroize for Stubborn {
874            fn zeroize(&mut self) {
875                panic!("will not be wiped");
876            }
877        }
878        impl Drop for Stubborn {
879            fn drop(&mut self) {
880                let _ = DROPS.fetch_add(1, Ordering::SeqCst);
881            }
882        }
883        let key = Locked::new(Stubborn([1; 8])).unwrap();
884        let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(key)));
885        assert!(r.is_err());
886        assert_eq!(DROPS.load(Ordering::SeqCst), 1);
887    }
888
889    #[test]
890    fn a_panicking_destructor_still_releases_the_region_exactly_once() {
891        // The wipe belongs to the region's release, which the field drop
892        // runs during the unwind out of `Locked::drop`; that a release
893        // always wipes is observed in `fallback::tests`. This pins the
894        // unwind path itself: one drop, no leak, no double release.
895        use std::sync::atomic::{AtomicUsize, Ordering};
896        static DROPS: AtomicUsize = AtomicUsize::new(0);
897        struct Angry([u8; 16]);
898        impl Zeroize for Angry {
899            fn zeroize(&mut self) {
900                self.0.zeroize();
901            }
902        }
903        impl Drop for Angry {
904            fn drop(&mut self) {
905                let _ = DROPS.fetch_add(1, Ordering::SeqCst);
906                panic!("angry");
907            }
908        }
909        let key = Locked::new(Angry([0xEE; 16])).unwrap();
910        let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(key)));
911        assert!(r.is_err());
912        assert_eq!(DROPS.load(Ordering::SeqCst), 1);
913        // No double drop, no leak: the region was released once. That a
914        // release always wipes is checked at the region level, in
915        // `fallback::tests`, where an observing allocator sees the bytes.
916    }
917
918    #[cfg(all(unix, not(miri)))]
919    #[test]
920    fn a_failed_allocation_zeroizes_the_input() {
921        // 128 KiB alignment exceeds every page size this crate is built for,
922        // so the region cannot be mapped and `new` must fail. The input is
923        // ours by then; it must be zeroized, not merely dropped.
924        use std::sync::atomic::{AtomicBool, Ordering};
925        static ZEROIZED: AtomicBool = AtomicBool::new(false);
926        #[repr(align(131072))]
927        struct Wide(u8);
928        impl Zeroize for Wide {
929            fn zeroize(&mut self) {
930                self.0 = 0;
931                ZEROIZED.store(true, Ordering::SeqCst);
932            }
933        }
934        impl Drop for Wide {
935            fn drop(&mut self) {
936                assert_eq!(self.0, 0, "dropped before being zeroized");
937            }
938        }
939        // A 128 KiB value crosses the stack a few times on its way in and
940        // out of `new`; the harness's 2 MiB thread is enough natively but
941        // not with a sanitizer's redzones around every frame.
942        std::thread::Builder::new()
943            .stack_size(16 << 20)
944            .spawn(|| {
945                let r = Locked::new(Wide(0x5A));
946                assert!(matches!(r, Err(LockError::Alignment { .. })), "{r:?}");
947            })
948            .unwrap()
949            .join()
950            .unwrap();
951        assert!(ZEROIZED.load(Ordering::SeqCst));
952    }
953
954    #[test]
955    fn every_error_displays_and_only_os_errors_have_a_source() {
956        use std::error::Error;
957        let os = || io::Error::from_raw_os_error(1);
958        let errors = [
959            LockError::Map {
960                bytes: 3,
961                source: os(),
962            },
963            LockError::Guard { source: os() },
964            LockError::Refused {
965                bytes: 3,
966                limit: Some(0),
967                source: os(),
968            },
969            LockError::Refused {
970                bytes: 3,
971                limit: None,
972                source: os(),
973            },
974            LockError::Dump { source: os() },
975            LockError::Untracked { source: os() },
976            LockError::Unavailable,
977            LockError::Forked,
978            LockError::Alignment { align: 8, page: 4 },
979        ];
980        for err in &errors {
981            assert!(!err.to_string().is_empty());
982            let has_source = !matches!(
983                err,
984                LockError::Unavailable | LockError::Forked | LockError::Alignment { .. }
985            );
986            assert_eq!(err.source().is_some(), has_source, "{err}");
987        }
988        assert!(errors[4].to_string().contains("core dumps"));
989    }
990
991    #[test]
992    fn zeroize_clears_the_value_in_place() {
993        let mut key = Locked::new([0x77u8; 24]).unwrap();
994        let before = key.risky_ref().as_ptr();
995        key.zeroize();
996        assert_eq!(key.risky_ref(), &[0u8; 24]);
997        assert_eq!(before, key.risky_ref().as_ptr());
998    }
999
1000    #[test]
1001    fn the_policy_error_names_the_policy_in_force() {
1002        let err = LockPolicyError {
1003            current: LockPolicy::Strict,
1004        };
1005        assert_eq!(err.to_string(), "the lock policy is already set to Strict");
1006    }
1007
1008    /// A `T` with padding. Its bytes must only ever be handled as bytes: a
1009    /// `u8` slice over them would claim initialisation the padding lacks.
1010    #[derive(Zeroize, Clone, PartialEq, Eq, Debug)]
1011    #[repr(C)]
1012    struct Padded {
1013        a: u8,
1014        b: u32,
1015        c: u16,
1016    }
1017    impl Zeroed for Padded {
1018        fn zeroed() -> Self {
1019            Padded { a: 0, b: 0, c: 0 }
1020        }
1021    }
1022
1023    const PADDED_SIZE: usize = mem::size_of::<Padded>();
1024    const _: () = assert!(
1025        PADDED_SIZE > 1 + 4 + 2,
1026        "the type must actually have padding"
1027    );
1028
1029    #[test]
1030    fn a_padded_value_moves_in_clones_generates_zeroizes_and_wipes() {
1031        const SIZE: usize = PADDED_SIZE;
1032
1033        let key = Locked::new(Padded { a: 1, b: 2, c: 3 }).unwrap();
1034        assert_eq!(*key.risky_ref(), Padded { a: 1, b: 2, c: 3 });
1035        let copy = key.try_clone().unwrap();
1036        assert_eq!(copy.risky_ref(), key.risky_ref());
1037
1038        let mut made = Locked::<Padded>::generate(|p| p.b = 9).unwrap();
1039        assert_eq!(made.risky_ref().b, 9);
1040        made.zeroize();
1041        assert_eq!(*made.risky_ref(), Padded::zeroed());
1042
1043        let region = key.into_wiped_region();
1044        // SAFETY: the region is still mapped, and the wipe initialised every
1045        // byte of the value's storage, padding included.
1046        let bytes = unsafe {
1047            core::slice::from_raw_parts(region.value_ptr(Layout::new::<Padded>()).as_ptr(), SIZE)
1048        };
1049        assert!(bytes.iter().all(|&b| b == 0));
1050    }
1051
1052    #[test]
1053    fn move_in_wipes_the_source_slot() {
1054        let mut slot = ManuallyDrop::new([0xA5u8; 16]);
1055        let storage = Storage::<[u8; 16]>::allocate().unwrap();
1056        // SAFETY: `slot` is read below only as bytes, never as the array.
1057        let key = unsafe { storage.move_in(&mut slot) };
1058        assert_eq!(key.risky_ref(), &[0xA5u8; 16]);
1059        // SAFETY: the wipe initialises every byte of the slot to zero.
1060        let left_behind: [u8; 16] = unsafe { ptr::read((&*slot as *const [u8; 16]).cast()) };
1061        assert_eq!(left_behind, [0u8; 16]);
1062    }
1063
1064    #[test]
1065    fn new_moves_without_running_drop_on_the_source() {
1066        // `new` takes the value by move; the slot it wipes is its own
1067        // parameter, observed directly in `move_in_wipes_the_source_slot`.
1068        // This pins the other half of the contract: no `Drop` runs on the
1069        // source, only on the value in the region, exactly once, and by
1070        // then `zeroize` has run.
1071        use std::sync::atomic::{AtomicUsize, Ordering};
1072        static DROPS: AtomicUsize = AtomicUsize::new(0);
1073        struct Counted([u8; 4]);
1074        impl Zeroize for Counted {
1075            fn zeroize(&mut self) {
1076                self.0.zeroize();
1077            }
1078        }
1079        impl Drop for Counted {
1080            fn drop(&mut self) {
1081                assert_eq!(self.0, [0; 4], "drop runs after zeroize");
1082                let _ = DROPS.fetch_add(1, Ordering::SeqCst);
1083            }
1084        }
1085        let l = Locked::new(Counted([1, 2, 3, 4])).unwrap();
1086        assert_eq!(l.risky_ref().0, [1, 2, 3, 4]);
1087        assert_eq!(
1088            DROPS.load(Ordering::SeqCst),
1089            0,
1090            "the source slot is not dropped"
1091        );
1092        drop(l);
1093        assert_eq!(DROPS.load(Ordering::SeqCst), 1);
1094    }
1095
1096    #[test]
1097    fn try_clone_is_a_second_region() {
1098        let a = Locked::new([3u8; 16]).unwrap();
1099        let b = a.try_clone().unwrap();
1100        assert_eq!(a.risky_ref(), b.risky_ref());
1101        assert_ne!(a.storage.region.ptr(), b.storage.region.ptr());
1102    }
1103
1104    #[test]
1105    fn relock_in_the_same_process_reports_the_known_state() {
1106        let mut key = Locked::new([1u8; 8]).unwrap();
1107        let before = key.locked();
1108        assert_eq!(key.relock().is_ok(), before);
1109        assert_eq!(key.locked(), before);
1110    }
1111
1112    #[test]
1113    fn require_locked_passes_a_locked_value_through() {
1114        let key = Locked::new([1u8; 8]).unwrap();
1115        if key.locked() {
1116            assert!(key.require_locked().is_ok());
1117        } else {
1118            assert!(key.require_locked().is_err());
1119        }
1120    }
1121
1122    #[test]
1123    fn debug_reveals_nothing_but_the_type_and_lock_state() {
1124        let key = Locked::new([0x42u8; 8]).unwrap();
1125        let s = format!("{key:?}");
1126        assert!(s.starts_with("Locked<[u8; 8]>("), "{s}");
1127        assert!(!s.contains("42"));
1128    }
1129
1130    #[test]
1131    fn timing_safe_eq_compares_the_values() {
1132        let a = Locked::new([1u8; 8]).unwrap();
1133        let b = Locked::new([1u8; 8]).unwrap();
1134        let c = Locked::new([2u8; 8]).unwrap();
1135        assert!(bool::from(a.ts_eq(&b)));
1136        assert!(!bool::from(a.ts_eq(&c)));
1137    }
1138
1139    #[test]
1140    fn zero_sized_and_odd_sized_types_are_fine() {
1141        #[derive(Zeroize)]
1142        struct Nothing;
1143        let _ = Locked::new(Nothing).unwrap();
1144        let odd = Locked::new([7u8; 33]).unwrap();
1145        assert_eq!(odd.risky_ref().len(), 33);
1146    }
1147
1148    #[test]
1149    fn the_handle_is_send_and_sync() {
1150        fn assert_send_sync<S: Send + Sync>() {}
1151        assert_send_sync::<Locked<[u8; 32]>>();
1152    }
1153
1154    #[test]
1155    fn the_handle_is_a_few_words_whatever_the_value_size() {
1156        let words = mem::size_of::<Locked<[u8; 1024]>>() / mem::size_of::<usize>();
1157        // Three for the Unix region (base, length, owning pid; the
1158        // fallback's `Layout` costs the same), one for the boxed lock error.
1159        assert!(words <= 4, "{words} words");
1160        assert_eq!(
1161            mem::size_of::<Locked<[u8; 1024]>>(),
1162            mem::size_of::<Locked<u8>>()
1163        );
1164    }
1165
1166    /// Set by the CI job whose host is known to lock memory for real, so
1167    /// that the lock assertions there cannot be skipped by a small limit,
1168    /// a privileged user or a sanitizer's `mlock` interceptor. Without it a
1169    /// regression that never called `mlock` could pass every job.
1170    #[cfg(all(unix, not(miri)))]
1171    fn lock_required() -> bool {
1172        std::env::var_os("LOCKED_TESTS_REQUIRE_LOCK").is_some()
1173    }
1174
1175    #[cfg(all(unix, not(miri)))]
1176    #[test]
1177    fn a_small_value_is_locked_when_the_limit_allows() {
1178        let key = Locked::new([1u8; 32]).unwrap();
1179        // The interior is one page. If the soft limit cannot hold even a
1180        // megabyte the environment is deliberately constrained; the
1181        // process-level tests cover that case.
1182        let constrained = matches!(unix::memlock_limit(), Some(limit) if limit < 1 << 20);
1183        if lock_required() || !constrained {
1184            assert!(key.locked(), "{:?}", key.lock_error());
1185        }
1186    }
1187
1188    #[cfg(all(target_os = "linux", not(miri)))]
1189    #[test]
1190    fn the_kernel_reports_the_region_locked_and_not_dumpable() {
1191        let key = Locked::new([1u8; 32]).unwrap();
1192        // The dump exclusion does not depend on the lock, and nothing on an
1193        // unfiltered host refuses it, so it is never the reported
1194        // degradation here. A refused lock (a small RLIMIT_MEMLOCK) is
1195        // possible and is covered by the process tests; it only skips the
1196        // `Locked:` check below, not the `dd` flag. So does an `mlock` that
1197        // is a no-op in this process.
1198        assert!(
1199            !matches!(key.lock_error(), Some(LockError::Dump { .. })),
1200            "{:?}",
1201            key.lock_error()
1202        );
1203        let expect_locked = lock_required() || (key.locked() && !super::common::mlock_is_a_no_op());
1204        let addr = key.storage.region.ptr().as_ptr() as usize;
1205        if expect_locked {
1206            let locked_kb = super::common::smaps_field(addr, "Locked:");
1207            assert!(locked_kb.unwrap_or(0) > 0, "Locked: {locked_kb:?}");
1208        }
1209        let flags = super::common::vm_flags(addr);
1210        assert!(flags.split(' ').any(|f| f == "dd"), "VmFlags: {flags}");
1211    }
1212}