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}