Skip to main content

dryoc/
protected.rs

1//! # Memory protection utilities
2//!
3//! Provides access to the memory locking system calls, such as `mlock()` and
4//! `mprotect()` on UNIX-like systems, `VirtualLock()` and `VirtualProtect()` on
5//! Windows. Similar to libsodium's `sodium_mlock` and `sodium_mprotect_*`
6//! functions.
7//!
8//! On Linux, sets `MADV_DONTDUMP` with `madvise()` on locked regions.
9//!
10//! The protected memory features are available on Unix and Windows targets with
11//! the `protected` feature flag enabled. This feature is enabled by default.
12//!
13//! ## Bottom line
14//!
15//! - Use protected memory for long-lived secrets such as private keys,
16//!   key-encryption keys, password-hash inputs, and session keys.
17//! - Locked memory asks the OS to keep those pages resident in RAM, reducing
18//!   the chance that secret bytes are written to swap.
19//! - On Linux, locked memory is also marked with `MADV_DONTDUMP`, reducing the
20//!   chance that secret bytes appear in ordinary core dumps.
21//! - Protected allocations are surrounded by no-access guard pages, which can
22//!   turn some out-of-bounds reads or writes into immediate process faults.
23//! - Read-only and no-access modes change OS page permissions, so invalid reads
24//!   or writes can fault instead of silently exposing or corrupting data.
25//! - Explicit zeroization preserves the value's lock and page-protection state.
26//! - Dropping a protected value zeroizes its allocation and, if it is locked,
27//!   unlocks it exactly once before releasing it.
28//! - If cleanup cannot make memory writable, restore its protection, or unlock
29//!   it, the process aborts rather than continuing with uncertain secret-memory
30//!   state.
31//! - It does not make bytes invisible to the current process, privileged OS
32//!   tooling, debuggers, other processes with permission to inspect this
33//!   process's address space, or copies made before data enters protected
34//!   memory.
35//! - It is heavier than ordinary allocation: each protected allocation uses
36//!   page-aligned storage with guard pages, and protection changes require
37//!   fallible system calls.
38//! - Platform behavior differs: Linux gets best-effort dump exclusion with
39//!   `MADV_DONTDUMP`; macOS and other Unix-like targets use `mlock()` and
40//!   `mprotect()` without that dump flag; Windows uses `VirtualLock()` and
41//!   `VirtualProtect()`.
42//!
43//! ## When to use protected memory
44//!
45//! Protected memory is most useful for secrets that remain in memory after an
46//! operation returns. It gives the operating system more information about how
47//! those bytes should be handled and makes accidental misuse easier to catch.
48//!
49//! The tradeoff is cost and complexity: small values can consume multiple pages
50//! of virtual memory, protection changes require system calls, and those system
51//! calls can fail because of platform limits or permissions. For short-lived
52//! buffers that are created, used, and dropped immediately, zeroizing ordinary
53//! stack or heap storage may be simpler and faster.
54//!
55//! ## What protection means in practice
56//!
57//! These APIs reduce exposure, but they do not make secret bytes invisible to
58//! the process that owns them. Code with a valid reference can still read
59//! read-write memory, and copies made before a value enters protected memory
60//! are outside this module's control. For example,
61//! [`NewLockedFromSlice::from_slice_into_locked`] copies the source slice into
62//! a protected allocation; callers remain responsible for the lifetime and
63//! cleanup policy of the original slice.
64//!
65//! Protected memory also is not a cross-process isolation mechanism. Another
66//! process's ability to inspect these bytes is determined by the operating
67//! system's process-memory access controls, such as debugger permissions,
68//! sandbox policy, user identity, and privileges.
69//!
70//! In practice, a protected value is an owned heap allocation whose state is
71//! tracked in the type: locked or unlocked, and read-write, read-only, or
72//! no-access. Accessor methods are only available for states where that access
73//! is valid, and direct memory access that bypasses the type system can still
74//! fault if it violates the active OS page protections.
75//!
76//! ## Platform notes
77//!
78//! On Linux, locking a region also makes a best-effort `madvise()` call with
79//! `MADV_DONTDUMP`, and unlocking reverses that with `MADV_DODUMP`. This keeps
80//! the locked pages out of ordinary core dumps when the kernel accepts the
81//! advice, but it is not a general crash-reporting or privileged-debugger
82//! boundary.
83//!
84//! On macOS and other Unix-like targets, this module uses `mlock()`,
85//! `munlock()`, and `mprotect()`, but it does not set a dump-exclusion flag.
86//! Locking is still subject to the process memory-locking limit, which can be
87//! low by default. If that limit is exceeded, protected allocation or locking
88//! returns an error.
89//!
90//! On Windows, this module uses `VirtualLock()`, `VirtualUnlock()`, and
91//! `VirtualProtect()`. `VirtualLock()` pins pages in the process working set
92//! and can fail when the process exceeds the working-set limits enforced by the
93//! OS. There is no `MADV_DONTDUMP` equivalent in this module.
94//!
95//! If the `serde` feature is enabled, the
96//! [`serde::Deserialize`](https://docs.rs/serde/latest/serde/trait.Deserialize.html) and
97//! [`serde::Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) traits will be
98//! implemented for [`HeapBytes`] and [`HeapByteArray`].
99//!
100//! ## Example
101//!
102//! ```
103//! use dryoc::protected::*;
104//!
105//! // Create a read-only, locked region of memory
106//! let readonly_locked = HeapBytes::from_slice_into_readonly_locked(b"some locked bytes")
107//!     .expect("failed to get locked bytes");
108//!
109//! // ... now do stuff with `readonly_locked` ...
110//! println!("{:?}", readonly_locked.as_slice());
111//! ```
112//!
113//! ## Protection features
114//!
115//! The type safe API uses traits to guard against misuse of protected memory.
116//! For example, memory that is set as read-only can be accessed with immutable
117//! accessors (such as `.as_slice()` or `.as_array()`), but not with mutable
118//! accessors like `.as_mut_slice()` or `.as_mut_array()`.
119//!
120//! ```compile_fail
121//! use dryoc::protected::*;
122//!
123//! // Create a read-only, locked region of memory
124//! let readonly_locked = HeapBytes::from_slice_into_readonly_locked(b"some locked bytes")
125//!     .expect("failed to get locked bytes");
126//!
127//! // Try to access the memory mutably
128//! println!("{:?}", readonly_locked.as_mut_slice()); // fails to compile, cannot access mutably
129//! ```
130//!
131//! Memory that has been protected as read-only or no-access will cause the
132//! process to crash if you attempt to access the memory improperly. To test
133//! this, try the following code (which requires an `unsafe` block):
134//!
135//! ```should_panic
136//! use dryoc::protected::*;
137//!
138//! // Create a read-only, locked region of memory
139//! let readonly_locked = HeapBytes::from_slice_into_readonly_locked(b"some locked bytes")
140//!     .expect("failed to get locked bytes");
141//!
142//! // Write to a protected region of memory, causing a crash.
143//! unsafe {
144//!     std::ptr::write(readonly_locked.as_slice().as_ptr() as *mut u8, 0) // <- crash happens here
145//! };
146//! ```
147//!
148//! Running the code above produces as `signal: 10, SIGBUS: access to undefined
149//! memory` panic.
150#[cfg(feature = "nightly")]
151use std::alloc::{AllocError, Allocator};
152use std::fmt;
153use std::marker::PhantomData;
154use std::ptr::{self, NonNull};
155use std::sync::LazyLock;
156
157use subtle::ConstantTimeEq;
158use zeroize::{Zeroize, ZeroizeOnDrop};
159
160use crate::error;
161use crate::rng::copy_randombytes;
162pub use crate::types::*;
163
164mod int {
165    #[derive(Clone, Debug, PartialEq, Eq)]
166    pub(super) enum LockMode {
167        Locked,
168        Unlocked,
169    }
170
171    #[derive(Clone, Debug, PartialEq, Eq)]
172    pub(super) enum ProtectMode {
173        ReadOnly,
174        ReadWrite,
175        NoAccess,
176    }
177
178    #[derive(Clone)]
179    pub(super) struct InternalData<A> {
180        pub(super) a: A,
181        pub(super) lm: LockMode,
182        pub(super) pm: ProtectMode,
183    }
184}
185
186#[doc(hidden)] // Edit this PR to remove doc(hidden) or add a doc comment.
187pub mod traits {
188    pub trait ProtectMode {}
189    pub struct ReadOnly {}
190    pub struct ReadWrite {}
191    pub struct NoAccess {}
192
193    impl ProtectMode for ReadOnly {}
194    impl ProtectMode for ReadWrite {}
195    impl ProtectMode for NoAccess {}
196
197    pub trait LockMode {}
198    pub struct Locked {}
199    pub struct Unlocked {}
200    impl LockMode for Locked {}
201    impl LockMode for Unlocked {}
202}
203
204/// A region of memory that can be locked, but is not yet protected. In order to
205/// lock the memory, it may require making a copy.
206pub trait Lockable<A: Zeroize + Bytes> {
207    /// Consumes `self`, creates a new protected region of memory, and returns
208    /// the result in a heap-allocated, page-aligned region of memory. The
209    /// memory is locked with `mlock()` on UNIX, or `VirtualLock()` on
210    /// Windows. By default, the protect mode is set to ReadWrite (i.e., no
211    /// exec) using `mprotect()` on UNIX, or `VirtualProtect()` on Windows.
212    /// On Linux, it will also set `MADV_DONTDUMP` using `madvise()`.
213    ///
214    /// # Errors
215    ///
216    /// Returns [`error::Error::Io`] if the pages cannot be locked. A common
217    /// cause is exceeding the process's locked-memory limit.
218    fn mlock(self) -> Result<Protected<A, traits::ReadWrite, traits::Locked>, error::Error>;
219}
220
221/// Protected region of memory that can be locked.
222pub trait Lock<A: Zeroize + Bytes, PM: traits::ProtectMode> {
223    /// Locks a region of memory, using `mlock()` on UNIX, or `VirtualLock()` on
224    /// Windows. By default, the protect mode is set to ReadWrite (i.e., no
225    /// exec) using `mprotect()` on UNIX, or `VirtualProtect()` on Windows.
226    /// On Linux, it will also set `MADV_DONTDUMP` using `madvise()`.
227    ///
228    /// # Errors
229    ///
230    /// Returns [`error::Error::Io`] if the pages cannot be locked, for example
231    /// because the process has reached its locked-memory limit.
232    fn mlock(self) -> Result<Protected<A, PM, traits::Locked>, error::Error>;
233}
234
235/// Protected region of memory that is already locked and can be unlocked.
236pub trait Unlock<A: Zeroize + Bytes, PM: traits::ProtectMode> {
237    /// Unlocks a region of memory, using `munlock()` on UNIX, or
238    /// `VirtualUnlock()` on Windows.
239    ///
240    /// # Errors
241    ///
242    /// Returns [`error::Error::Io`] if the pages cannot be unlocked.
243    fn munlock(self) -> Result<Protected<A, PM, traits::Unlocked>, error::Error>;
244}
245
246/// Protected region of memory that can be set as read-only.
247pub trait ProtectReadOnly<A: Zeroize + Bytes, PM: traits::ProtectMode, LM: traits::LockMode> {
248    /// Protects a region of memory as read-only (and no exec), using
249    /// `mprotect()` on UNIX, or `VirtualProtect()` on Windows.
250    ///
251    /// # Errors
252    ///
253    /// Returns [`error::Error::Io`] if the page permissions cannot be changed.
254    fn mprotect_readonly(self) -> Result<Protected<A, traits::ReadOnly, LM>, error::Error>;
255}
256
257/// Protected region of memory that can be set as read-write.
258pub trait ProtectReadWrite<A: Zeroize + Bytes, PM: traits::ProtectMode, LM: traits::LockMode> {
259    /// Protects a region of memory as read-write (and no exec), using
260    /// `mprotect()` on UNIX, or `VirtualProtect()` on Windows.
261    ///
262    /// # Errors
263    ///
264    /// Returns [`error::Error::Io`] if the page permissions cannot be changed.
265    fn mprotect_readwrite(self) -> Result<Protected<A, traits::ReadWrite, LM>, error::Error>;
266}
267
268/// Protected region of memory that can be set as no-access. Must be unlocked.
269pub trait ProtectNoAccess<A: Zeroize + Bytes, PM: traits::ProtectMode> {
270    /// Protects an unlocked region of memory as no-access (and no exec), using
271    /// `mprotect()` on UNIX, or `VirtualProtect()` on Windows.
272    ///
273    /// # Errors
274    ///
275    /// Returns [`error::Error::Io`] if the page permissions cannot be changed.
276    fn mprotect_noaccess(
277        self,
278    ) -> Result<Protected<A, traits::NoAccess, traits::Unlocked>, error::Error>;
279}
280
281/// Bytes which can be allocated and protected.
282pub trait NewLocked<A: Zeroize + NewBytes + Lockable<A>> {
283    /// Returns a new locked byte array.
284    ///
285    /// # Errors
286    ///
287    /// Returns [`error::Error::Io`] if the allocation cannot be locked,
288    /// commonly because the process has reached its locked-memory limit.
289    fn new_locked() -> Result<Protected<A, traits::ReadWrite, traits::Locked>, error::Error>;
290    /// Returns a new locked byte array.
291    ///
292    /// # Errors
293    ///
294    /// Returns [`error::Error::Io`] if the allocation cannot be locked or its
295    /// page permissions cannot be changed to read-only.
296    fn new_readonly_locked() -> Result<Protected<A, traits::ReadOnly, traits::Locked>, error::Error>;
297    /// Returns a new locked byte array, filled with random data.
298    ///
299    /// # Errors
300    ///
301    /// Returns [`error::Error::Io`] if the allocation cannot be locked.
302    fn generate_locked() -> Result<Protected<A, traits::ReadWrite, traits::Locked>, error::Error>;
303    /// Returns a new read-only, locked byte array, filled with random data.
304    ///
305    /// # Errors
306    ///
307    /// Returns [`error::Error::Io`] if the allocation cannot be locked or its
308    /// page permissions cannot be changed to read-only.
309    fn generate_readonly_locked()
310    -> Result<Protected<A, traits::ReadOnly, traits::Locked>, error::Error>;
311    /// Returns a new locked byte array, filled with random data.
312    ///
313    /// Prefer [`generate_locked`](Self::generate_locked). This method is
314    /// retained for compatibility.
315    ///
316    /// # Errors
317    ///
318    /// Returns the same errors as [`generate_locked`](Self::generate_locked).
319    #[deprecated(note = "use generate_locked() instead")]
320    fn gen_locked() -> Result<Protected<A, traits::ReadWrite, traits::Locked>, error::Error> {
321        Self::generate_locked()
322    }
323    /// Returns a new read-only, locked byte array, filled with random data.
324    ///
325    /// Prefer [`generate_readonly_locked`](Self::generate_readonly_locked).
326    /// This method is retained for compatibility.
327    ///
328    /// # Errors
329    ///
330    /// Returns the same errors as
331    /// [`generate_readonly_locked`](Self::generate_readonly_locked).
332    #[deprecated(note = "use generate_readonly_locked() instead")]
333    fn gen_readonly_locked() -> Result<Protected<A, traits::ReadOnly, traits::Locked>, error::Error>
334    {
335        Self::generate_readonly_locked()
336    }
337}
338
339/// Create a new region of protected memory from a slice.
340pub trait NewLockedFromSlice<A: Zeroize + NewBytes + Lockable<A>> {
341    /// Returns a new locked region of memory from `src`.
342    ///
343    /// # Errors
344    ///
345    /// Returns an error if `src` has the wrong length for a fixed-size output
346    /// or the pages cannot be locked.
347    ///
348    /// # Panics
349    ///
350    /// May panic if allocating or resizing the protected storage fails,
351    /// including when the requested size cannot be rounded to whole pages.
352    fn from_slice_into_locked(
353        src: &[u8],
354    ) -> Result<Protected<A, traits::ReadWrite, traits::Locked>, crate::error::Error>;
355    /// Returns a new read-only locked region of memory from `src`.
356    ///
357    /// # Errors
358    ///
359    /// Returns an error if `src` has the wrong length for a fixed-size output,
360    /// its pages cannot be locked, or its page permissions cannot be changed
361    /// to read-only.
362    ///
363    /// # Panics
364    ///
365    /// May panic if allocating or resizing the protected storage fails,
366    /// including when the requested size cannot be rounded to whole pages.
367    fn from_slice_into_readonly_locked(
368        src: &[u8],
369    ) -> Result<Protected<A, traits::ReadOnly, traits::Locked>, crate::error::Error>;
370}
371
372/// Holds a protected region of memory. Does not implement [`Copy`] or
373/// [`Debug`](std::fmt::Debug). Accessible states implement [`Clone`] when the
374/// backing storage supports it; each clone has a distinct allocation.
375pub struct Protected<A: Zeroize + Bytes, PM: traits::ProtectMode, LM: traits::LockMode> {
376    i: Option<int::InternalData<A>>,
377    p: PhantomData<PM>,
378    l: PhantomData<LM>,
379}
380
381/// Short-hand type aliases for protected types.
382pub mod ptypes {
383    /// Locked, read-write, page-aligned memory region type alias
384    pub type Locked<T> = super::Protected<T, super::traits::ReadWrite, super::traits::Locked>;
385    /// Locked, read-only, page-aligned memory region type alias
386    pub type LockedRO<T> = super::Protected<T, super::traits::ReadOnly, super::traits::Locked>;
387    /// Unlocked, no-access, page-aligned memory region type alias
388    pub type NoAccess<T> = super::Protected<T, super::traits::NoAccess, super::traits::Unlocked>;
389    /// Unlocked, read-write, page-aligned memory region type alias
390    pub type Unlocked<T> = super::Protected<T, super::traits::ReadWrite, super::traits::Unlocked>;
391    /// Unlocked, read-only, page-aligned memory region type alias
392    pub type UnlockedRO<T> = super::Protected<T, super::traits::ReadOnly, super::traits::Unlocked>;
393    /// Locked, read-write, page-aligned bytes type alias
394    pub type LockedBytes = Locked<super::HeapBytes>;
395}
396
397impl<T: Zeroize + NewBytes + ResizableBytes + Lockable<T> + NewLocked<T>> Clone for Locked<T> {
398    fn clone(&self) -> Self {
399        let mut cloned = T::new_locked().expect("unable to create new locked instance");
400        cloned.resize(self.len(), 0);
401        cloned.as_mut_slice().copy_from_slice(self.as_slice());
402        cloned
403    }
404}
405
406impl<T: Zeroize + NewBytes + ResizableBytes + Lockable<T> + NewLocked<T>> Clone for LockedRO<T> {
407    fn clone(&self) -> Self {
408        let mut cloned = T::new_locked().expect("unable to create new locked instance");
409        cloned.resize(self.len(), 0);
410        cloned.as_mut_slice().copy_from_slice(self.as_slice());
411        cloned
412            .mprotect_readonly()
413            .expect("unable to protect readonly")
414    }
415}
416
417impl<T: Zeroize + Bytes + Clone> Clone for Unlocked<T> {
418    fn clone(&self) -> Self {
419        Self::new_with(self.i.as_ref().unwrap().a.clone())
420    }
421}
422
423impl<T: Zeroize + NewBytes + Clone> Clone for UnlockedRO<T> {
424    fn clone(&self) -> Self {
425        Unlocked::<T>::new_with(self.i.as_ref().unwrap().a.clone())
426            .mprotect_readonly()
427            .expect("unable to create new readonly instance")
428    }
429}
430
431pub use ptypes::*;
432
433fn dryoc_mlock(data: &[u8]) -> Result<(), std::io::Error> {
434    if data.is_empty() {
435        // no-op
436        return Ok(());
437    }
438    #[cfg(unix)]
439    {
440        #[cfg(target_os = "linux")]
441        {
442            // tell the kernel not to include this memory in a core dump
443            use libc::{MADV_DONTDUMP, madvise};
444            // SAFETY: `data` is a valid, non-empty byte slice. `madvise` may
445            // accept any address range and reports errors through its return
446            // value; this advisory call does not change Rust aliasing rules.
447            unsafe {
448                madvise(data.as_ptr() as *mut c_void, data.len(), MADV_DONTDUMP);
449            }
450        }
451
452        use libc::{c_void, mlock as c_mlock};
453        // SAFETY: `data` is a valid, non-empty byte slice. The OS only pins the
454        // mapped pages for this address range and reports failure via `ret`.
455        let ret = unsafe { c_mlock(data.as_ptr() as *const c_void, data.len()) };
456        match ret {
457            0 => Ok(()),
458            _ => Err(std::io::Error::last_os_error()),
459        }
460    }
461    #[cfg(windows)]
462    {
463        use winapi::shared::minwindef::LPVOID;
464        use winapi::um::memoryapi::VirtualLock;
465
466        // SAFETY: `data` is a valid, non-empty byte slice. `VirtualLock` pins
467        // the corresponding pages and reports failure through its return value.
468        let res = unsafe { VirtualLock(data.as_ptr() as LPVOID, data.len()) };
469        if res != 0 {
470            Ok(())
471        } else {
472            Err(std::io::Error::last_os_error())
473        }
474    }
475}
476
477fn dryoc_munlock(data: &[u8]) -> Result<(), std::io::Error> {
478    if data.is_empty() {
479        // no-op
480        return Ok(());
481    }
482    #[cfg(unix)]
483    {
484        #[cfg(target_os = "linux")]
485        {
486            // undo MADV_DONTDUMP
487            use libc::{MADV_DODUMP, madvise};
488            // SAFETY: `data` is a valid, non-empty byte slice. This reverses
489            // the advisory dump flag for the same address range.
490            unsafe {
491                madvise(data.as_ptr() as *mut c_void, data.len(), MADV_DODUMP);
492            }
493        }
494
495        use libc::{c_void, munlock as c_munlock};
496        // SAFETY: `data` is a valid, non-empty byte slice. The OS unpins the
497        // mapped pages for this address range and reports failure via `ret`.
498        let ret = unsafe { c_munlock(data.as_ptr() as *const c_void, data.len()) };
499        match ret {
500            0 => Ok(()),
501            _ => Err(std::io::Error::last_os_error()),
502        }
503    }
504    #[cfg(windows)]
505    {
506        use winapi::shared::minwindef::LPVOID;
507        use winapi::um::memoryapi::VirtualUnlock;
508
509        // SAFETY: `data` is a valid, non-empty byte slice. `VirtualUnlock`
510        // unpins the corresponding pages and reports failure via `res`.
511        let res = unsafe { VirtualUnlock(data.as_ptr() as LPVOID, data.len()) };
512        if res != 0 {
513            Ok(())
514        } else {
515            Err(std::io::Error::last_os_error())
516        }
517    }
518}
519
520fn dryoc_mprotect_readonly(data: &[u8]) -> Result<(), std::io::Error> {
521    dryoc_mprotect_ptr(
522        data.as_ptr() as *mut u8,
523        data.len(),
524        PageProtectMode::ReadOnly,
525    )
526}
527
528fn dryoc_mprotect_readwrite(data: &[u8]) -> Result<(), std::io::Error> {
529    dryoc_mprotect_ptr(
530        data.as_ptr() as *mut u8,
531        data.len(),
532        PageProtectMode::ReadWrite,
533    )
534}
535
536fn dryoc_mprotect_readwrite_ptr(data: *mut u8, len: usize) -> Result<(), std::io::Error> {
537    dryoc_mprotect_ptr(data, len, PageProtectMode::ReadWrite)
538}
539
540fn dryoc_mprotect_noaccess(data: &[u8]) -> Result<(), std::io::Error> {
541    dryoc_mprotect_ptr(
542        data.as_ptr() as *mut u8,
543        data.len(),
544        PageProtectMode::NoAccess,
545    )
546}
547
548fn dryoc_mprotect_mode(data: &[u8], mode: &int::ProtectMode) -> Result<(), std::io::Error> {
549    match mode {
550        int::ProtectMode::ReadOnly => dryoc_mprotect_readonly(data),
551        int::ProtectMode::ReadWrite => dryoc_mprotect_readwrite(data),
552        int::ProtectMode::NoAccess => dryoc_mprotect_noaccess(data),
553    }
554}
555
556fn dryoc_mprotect_noaccess_ptr(data: *mut u8, len: usize) -> Result<(), std::io::Error> {
557    dryoc_mprotect_ptr(data, len, PageProtectMode::NoAccess)
558}
559
560#[derive(Clone, Copy)]
561enum PageProtectMode {
562    ReadOnly,
563    ReadWrite,
564    NoAccess,
565}
566
567fn dryoc_mprotect_ptr(
568    data: *mut u8,
569    len: usize,
570    mode: PageProtectMode,
571) -> Result<(), std::io::Error> {
572    if len == 0 {
573        // no-op
574        return Ok(());
575    }
576    #[cfg(unix)]
577    {
578        use libc::{PROT_NONE, PROT_READ, PROT_WRITE, c_void, mprotect as c_mprotect};
579        let prot = match mode {
580            PageProtectMode::ReadOnly => PROT_READ,
581            PageProtectMode::ReadWrite => PROT_READ | PROT_WRITE,
582            PageProtectMode::NoAccess => PROT_NONE,
583        };
584        // SAFETY: Callers pass page-aligned ranges from protected allocations.
585        // `mprotect` changes page permissions and reports errors via `ret`.
586        let ret = unsafe { c_mprotect(data as *mut c_void, len, prot) };
587        match ret {
588            0 => Ok(()),
589            _ => Err(std::io::Error::last_os_error()),
590        }
591    }
592    #[cfg(windows)]
593    {
594        use winapi::shared::minwindef::{DWORD, LPVOID};
595        use winapi::um::memoryapi::VirtualProtect;
596        use winapi::um::winnt::{PAGE_NOACCESS, PAGE_READONLY, PAGE_READWRITE};
597
598        let protect = match mode {
599            PageProtectMode::ReadOnly => PAGE_READONLY,
600            PageProtectMode::ReadWrite => PAGE_READWRITE,
601            PageProtectMode::NoAccess => PAGE_NOACCESS,
602        };
603        let mut old: DWORD = 0;
604
605        // SAFETY: Callers pass committed ranges from `VirtualAlloc`.
606        // `VirtualProtect` changes page permissions and reports errors via
607        // `res`.
608        let res = unsafe { VirtualProtect(data as LPVOID, len, protect, &mut old) };
609        if res != 0 {
610            Ok(())
611        } else {
612            Err(std::io::Error::last_os_error())
613        }
614    }
615}
616
617impl<A: Zeroize + Bytes, PM: traits::ProtectMode, LM: traits::LockMode> Protected<A, PM, LM> {
618    fn new() -> Self {
619        Self {
620            i: None,
621            p: PhantomData,
622            l: PhantomData,
623        }
624    }
625
626    fn new_with(a: A) -> Self {
627        Self {
628            i: Some(int::InternalData {
629                a,
630                lm: int::LockMode::Unlocked,
631                pm: int::ProtectMode::ReadWrite,
632            }),
633            p: PhantomData,
634            l: PhantomData,
635        }
636    }
637
638    fn swap_some_or_err<F, OPM: traits::ProtectMode, OLM: traits::LockMode>(
639        &mut self,
640        f: F,
641    ) -> Result<Protected<A, OPM, OLM>, error::Error>
642    where
643        F: Fn(&mut int::InternalData<A>) -> Result<Protected<A, OPM, OLM>, error::Error>,
644    {
645        match &mut self.i {
646            Some(d) => {
647                let mut new = f(d)?;
648                // swap into new struct
649                std::mem::swap(&mut new.i, &mut self.i);
650                Ok(new)
651            }
652            _ => Err(error::Error::invalid_state(
653                crate::ErrorContext::ProtectedMemory,
654            )),
655        }
656    }
657}
658
659impl<A: Zeroize + Bytes, PM: traits::ProtectMode> Unlock<A, PM>
660    for Protected<A, PM, traits::Locked>
661{
662    fn munlock(mut self) -> Result<Protected<A, PM, traits::Unlocked>, error::Error> {
663        self.swap_some_or_err(|old| {
664            dryoc_munlock(old.a.as_slice())?;
665            // update internal state
666            old.lm = int::LockMode::Unlocked;
667            Ok(Protected::<A, PM, traits::Unlocked>::new())
668        })
669    }
670}
671
672impl<A: Zeroize + Bytes + Default, PM: traits::ProtectMode> Lock<A, PM>
673    for Protected<A, PM, traits::Unlocked>
674{
675    fn mlock(mut self) -> Result<Protected<A, PM, traits::Locked>, error::Error> {
676        self.swap_some_or_err(|old| {
677            dryoc_mlock(old.a.as_slice())?;
678            // update internal state
679            old.lm = int::LockMode::Locked;
680            Ok(Protected::<A, PM, traits::Locked>::new())
681        })
682    }
683}
684
685impl<A: Zeroize + Bytes, PM: traits::ProtectMode, LM: traits::LockMode> ProtectReadOnly<A, PM, LM>
686    for Protected<A, PM, LM>
687{
688    fn mprotect_readonly(mut self) -> Result<Protected<A, traits::ReadOnly, LM>, error::Error> {
689        self.swap_some_or_err(|old| {
690            dryoc_mprotect_readonly(old.a.as_slice())?;
691            // update internal state
692            old.pm = int::ProtectMode::ReadOnly;
693            Ok(Protected::<A, traits::ReadOnly, LM>::new())
694        })
695    }
696}
697
698impl<A: Zeroize + Bytes, PM: traits::ProtectMode, LM: traits::LockMode> ProtectReadWrite<A, PM, LM>
699    for Protected<A, PM, LM>
700{
701    fn mprotect_readwrite(mut self) -> Result<Protected<A, traits::ReadWrite, LM>, error::Error> {
702        self.swap_some_or_err(|old| {
703            dryoc_mprotect_readwrite(old.a.as_slice())?;
704            // update internal state
705            old.pm = int::ProtectMode::ReadWrite;
706            Ok(Protected::<A, traits::ReadWrite, LM>::new())
707        })
708    }
709}
710
711impl<A: Zeroize + Bytes, PM: traits::ProtectMode> ProtectNoAccess<A, PM>
712    for Protected<A, PM, traits::Unlocked>
713{
714    fn mprotect_noaccess(
715        mut self,
716    ) -> Result<Protected<A, traits::NoAccess, traits::Unlocked>, error::Error> {
717        self.swap_some_or_err(|old| {
718            dryoc_mprotect_noaccess(old.a.as_slice())?;
719            // update internal state
720            old.pm = int::ProtectMode::NoAccess;
721            Ok(Protected::<A, traits::NoAccess, traits::Unlocked>::new())
722        })
723    }
724}
725
726impl<A: Zeroize + Bytes + AsRef<[u8]>, LM: traits::LockMode> AsRef<[u8]>
727    for Protected<A, traits::ReadOnly, LM>
728{
729    fn as_ref(&self) -> &[u8] {
730        self.i.as_ref().unwrap().a.as_ref()
731    }
732}
733
734impl<A: Zeroize + Bytes + AsRef<[u8]>, LM: traits::LockMode> AsRef<[u8]>
735    for Protected<A, traits::ReadWrite, LM>
736{
737    fn as_ref(&self) -> &[u8] {
738        self.i.as_ref().unwrap().a.as_ref()
739    }
740}
741
742impl<A: Zeroize + MutBytes + AsMut<[u8]>, LM: traits::LockMode> AsMut<[u8]>
743    for Protected<A, traits::ReadWrite, LM>
744{
745    fn as_mut(&mut self) -> &mut [u8] {
746        self.i.as_mut().unwrap().a.as_mut()
747    }
748}
749
750impl<A: Zeroize + Bytes, LM: traits::LockMode> Bytes for Protected<A, traits::ReadOnly, LM> {
751    #[inline]
752    fn as_slice(&self) -> &[u8] {
753        self.i.as_ref().unwrap().a.as_slice()
754    }
755
756    #[inline]
757    fn len(&self) -> usize {
758        self.i.as_ref().unwrap().a.len()
759    }
760
761    #[inline]
762    fn is_empty(&self) -> bool {
763        self.i.as_ref().unwrap().a.is_empty()
764    }
765}
766
767impl<A: Zeroize + Bytes, LM: traits::LockMode> Bytes for Protected<A, traits::ReadWrite, LM> {
768    #[inline]
769    fn as_slice(&self) -> &[u8] {
770        self.i.as_ref().unwrap().a.as_slice()
771    }
772
773    #[inline]
774    fn len(&self) -> usize {
775        self.i.as_ref().unwrap().a.len()
776    }
777
778    #[inline]
779    fn is_empty(&self) -> bool {
780        self.i.as_ref().unwrap().a.is_empty()
781    }
782}
783
784impl<const LENGTH: usize> From<StackByteArray<LENGTH>> for HeapByteArray<LENGTH> {
785    fn from(other: StackByteArray<LENGTH>) -> Self {
786        let mut r = HeapByteArray::<LENGTH>::new_byte_array();
787        let mut s = other;
788        r.copy_from_slice(s.as_slice());
789        s.zeroize();
790        r
791    }
792}
793
794impl<const LENGTH: usize> StackByteArray<LENGTH> {
795    /// Locks a [StackByteArray], consuming it, and returning a [Protected]
796    /// wrapper.
797    ///
798    /// # Errors
799    ///
800    /// Returns [`error::Error::Io`] if the pages cannot be locked.
801    ///
802    /// # Panics
803    ///
804    /// Panics if the page-aligned allocation cannot be created or its size
805    /// cannot be represented after page rounding and adding guard pages.
806    pub fn mlock(
807        self,
808    ) -> Result<Protected<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Locked>, error::Error>
809    {
810        Protected::<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Unlocked>::new_with(
811            self.into(),
812        )
813        .mlock()
814    }
815}
816
817impl<const LENGTH: usize> StackByteArray<LENGTH> {
818    /// Returns a readonly protected [StackByteArray].
819    ///
820    /// # Errors
821    ///
822    /// Returns [`error::Error::Io`] if the page permissions cannot be changed
823    /// to read-only.
824    ///
825    /// # Panics
826    ///
827    /// Panics if the page-aligned allocation cannot be created or its size
828    /// cannot be represented after page rounding and adding guard pages.
829    pub fn mprotect_readonly(
830        self,
831    ) -> Result<Protected<HeapByteArray<LENGTH>, traits::ReadOnly, traits::Unlocked>, error::Error>
832    {
833        Protected::<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Unlocked>::new_with(
834            self.into(),
835        )
836        .mprotect_readonly()
837    }
838}
839
840impl<const LENGTH: usize> Lockable<HeapByteArray<LENGTH>> for HeapByteArray<LENGTH> {
841    /// Locks a [HeapByteArray], and returns a [Protected] wrapper.
842    fn mlock(
843        self,
844    ) -> Result<Protected<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Locked>, error::Error>
845    {
846        Protected::<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Unlocked>::new_with(self)
847            .mlock()
848    }
849}
850
851impl Lockable<HeapBytes> for HeapBytes {
852    /// Locks a [HeapBytes], and returns a [Protected] wrapper.
853    fn mlock(
854        self,
855    ) -> Result<Protected<HeapBytes, traits::ReadWrite, traits::Locked>, error::Error> {
856        Protected::<HeapBytes, traits::ReadWrite, traits::Unlocked>::new_with(self).mlock()
857    }
858}
859
860#[derive(Clone)]
861/// Custom page-aligned allocator implementation. Creates blocks of page-aligned
862/// heap-allocated memory regions, with no-access pages before and after the
863/// allocated region of memory. Allocations whose requested alignment does not
864/// divide the host page size are rejected.
865pub struct PageAlignedAllocator;
866
867#[cfg(unix)]
868const DEFAULT_PAGESIZE: usize = 4096;
869
870#[cfg(unix)]
871fn page_size_from_sysconf(page_size: libc::c_long) -> usize {
872    if page_size > 0 {
873        page_size as usize
874    } else {
875        DEFAULT_PAGESIZE
876    }
877}
878
879static PAGESIZE: LazyLock<usize> = LazyLock::new(|| {
880    #[cfg(unix)]
881    {
882        use libc::{_SC_PAGE_SIZE, sysconf};
883        // SAFETY: `sysconf(_SC_PAGE_SIZE)` has no pointer arguments and returns
884        // the host page size or an error sentinel.
885        let page_size = unsafe { sysconf(_SC_PAGE_SIZE) };
886        page_size_from_sysconf(page_size)
887    }
888    #[cfg(windows)]
889    {
890        use winapi::um::sysinfoapi::{GetSystemInfo, SYSTEM_INFO};
891        let mut si = SYSTEM_INFO::default();
892        // SAFETY: `si` is a valid writable `SYSTEM_INFO` out-parameter for the
893        // duration of the call.
894        unsafe { GetSystemInfo(&mut si) };
895        si.dwPageSize as usize
896    }
897});
898
899fn _page_round(size: usize, pagesize: usize) -> Option<usize> {
900    let rem = size % pagesize;
901    if rem == 0 {
902        Some(size)
903    } else {
904        size.checked_add(pagesize - rem)
905    }
906}
907
908fn protected_alloc_error() -> std::io::Error {
909    std::io::Error::other("protected memory allocation failed")
910}
911
912#[derive(Clone, Copy)]
913struct RawRegionLayout {
914    rounded_size: usize,
915    total_size: usize,
916}
917
918fn checked_raw_region_layout(
919    user_size: usize,
920    pagesize: usize,
921) -> Result<RawRegionLayout, std::io::Error> {
922    let rounded_size = _page_round(user_size, pagesize).ok_or_else(protected_alloc_error)?;
923    let guard_size = pagesize.checked_mul(2).ok_or_else(protected_alloc_error)?;
924    let total_size = rounded_size
925        .checked_add(guard_size)
926        .ok_or_else(protected_alloc_error)?;
927    Ok(RawRegionLayout {
928        rounded_size,
929        total_size,
930    })
931}
932
933#[derive(Clone, Copy)]
934struct RawProtectedAllocation {
935    base: NonNull<u8>,
936    data: NonNull<u8>,
937    rounded_size: usize,
938    total_size: usize,
939}
940
941fn platform_alloc(total_size: usize, pagesize: usize) -> Result<NonNull<u8>, std::io::Error> {
942    #[cfg(unix)]
943    {
944        use libc::posix_memalign;
945        let mut out = ptr::null_mut();
946
947        // SAFETY: `out` is a valid out-parameter. `pagesize` is the host page
948        // size and therefore a power-of-two alignment; `total_size` was checked
949        // by `checked_raw_region_layout`.
950        let ret = unsafe { posix_memalign(&mut out, pagesize, total_size) };
951        if ret != 0 {
952            return Err(std::io::Error::from_raw_os_error(ret));
953        }
954
955        NonNull::new(out as *mut u8).ok_or_else(protected_alloc_error)
956    }
957    #[cfg(windows)]
958    {
959        let _ = pagesize;
960        use winapi::um::memoryapi::VirtualAlloc;
961        use winapi::um::winnt::{MEM_COMMIT, MEM_RESERVE, PAGE_READWRITE};
962
963        // SAFETY: `total_size` was checked by `checked_raw_region_layout`. Null
964        // address lets the OS choose the base, and failure is handled by
965        // checking for null.
966        let out = unsafe {
967            VirtualAlloc(
968                ptr::null_mut(),
969                total_size,
970                MEM_COMMIT | MEM_RESERVE,
971                PAGE_READWRITE,
972            )
973        };
974
975        NonNull::new(out as *mut u8).ok_or_else(std::io::Error::last_os_error)
976    }
977}
978
979fn platform_free(base: NonNull<u8>, total_size: usize) {
980    #[cfg(unix)]
981    {
982        let _ = total_size;
983        // SAFETY: `base` is the original allocation base returned by
984        // `posix_memalign`.
985        unsafe { libc::free(base.as_ptr() as *mut libc::c_void) };
986    }
987    #[cfg(windows)]
988    {
989        let _ = total_size;
990        use winapi::shared::minwindef::LPVOID;
991        use winapi::um::memoryapi::VirtualFree;
992        use winapi::um::winnt::MEM_RELEASE;
993        // SAFETY: `base` is the original allocation base returned by
994        // `VirtualAlloc`; size 0 with `MEM_RELEASE` releases the whole region.
995        unsafe { VirtualFree(base.as_ptr() as LPVOID, 0, MEM_RELEASE) };
996    }
997}
998
999fn allocate_raw_region(user_size: usize) -> Result<RawProtectedAllocation, std::io::Error> {
1000    let pagesize = *PAGESIZE;
1001    let layout = checked_raw_region_layout(user_size, pagesize)?;
1002    let base = platform_alloc(layout.total_size, pagesize)?;
1003    let base_ptr = base.as_ptr();
1004
1005    if let Err(err) = dryoc_mprotect_noaccess_ptr(base_ptr, pagesize) {
1006        platform_free(base, layout.total_size);
1007        return Err(err);
1008    }
1009
1010    let aft_guard_offset = pagesize
1011        .checked_add(layout.rounded_size)
1012        .ok_or_else(protected_alloc_error)?;
1013    // SAFETY: `aft_guard_offset` was bounds-checked as part of the raw region
1014    // layout and leaves one full guard page in the allocation.
1015    let aft_guard = unsafe { base_ptr.add(aft_guard_offset) };
1016    if let Err(err) = dryoc_mprotect_noaccess_ptr(aft_guard, pagesize) {
1017        let _ = dryoc_mprotect_readwrite_ptr(base_ptr, pagesize);
1018        platform_free(base, layout.total_size);
1019        return Err(err);
1020    }
1021
1022    // SAFETY: `base` points to the full raw allocation and `pagesize` skips the
1023    // front guard page to the start of the user region.
1024    let data_ptr = unsafe { base_ptr.add(pagesize) };
1025    let data = NonNull::new(data_ptr).ok_or_else(protected_alloc_error)?;
1026
1027    Ok(RawProtectedAllocation {
1028        base,
1029        data,
1030        rounded_size: layout.rounded_size,
1031        total_size: layout.total_size,
1032    })
1033}
1034
1035fn deallocate_raw_region(raw: RawProtectedAllocation) {
1036    let pagesize = *PAGESIZE;
1037    let base_ptr = raw.base.as_ptr();
1038    let _ = dryoc_mprotect_readwrite_ptr(base_ptr, pagesize);
1039
1040    if let Some(aft_guard_offset) = pagesize.checked_add(raw.rounded_size) {
1041        // SAFETY: `aft_guard_offset` mirrors `allocate_raw_region` and points
1042        // at the aft guard page inside this allocation.
1043        let aft_guard = unsafe { base_ptr.add(aft_guard_offset) };
1044        let _ = dryoc_mprotect_readwrite_ptr(aft_guard, pagesize);
1045    }
1046
1047    platform_free(raw.base, raw.total_size);
1048}
1049
1050struct ProtectedBuffer {
1051    base: Option<NonNull<u8>>,
1052    data: NonNull<u8>,
1053    len: usize,
1054    capacity: usize,
1055    rounded_size: usize,
1056    total_size: usize,
1057}
1058
1059// SAFETY: `ProtectedBuffer` uniquely owns its allocation. Moving it to another
1060// thread does not invalidate the allocation, and access to mutable bytes still
1061// requires `&mut self`.
1062unsafe impl Send for ProtectedBuffer {}
1063
1064// SAFETY: Shared references expose only immutable byte slices and metadata; the
1065// type has no interior mutability.
1066unsafe impl Sync for ProtectedBuffer {}
1067
1068impl ProtectedBuffer {
1069    fn new_filled(len: usize, value: u8) -> Result<Self, std::io::Error> {
1070        if len == 0 {
1071            return Ok(Self::default());
1072        }
1073
1074        let raw = allocate_raw_region(len)?;
1075        let mut buffer = Self {
1076            base: Some(raw.base),
1077            data: raw.data,
1078            len,
1079            capacity: len,
1080            rounded_size: raw.rounded_size,
1081            total_size: raw.total_size,
1082        };
1083        buffer.as_mut_slice().fill(value);
1084        Ok(buffer)
1085    }
1086
1087    fn from_slice(src: &[u8]) -> Result<Self, std::io::Error> {
1088        let mut buffer = Self::new_filled(src.len(), 0)?;
1089        buffer.as_mut_slice().copy_from_slice(src);
1090        Ok(buffer)
1091    }
1092
1093    fn as_ptr(&self) -> *const u8 {
1094        self.data.as_ptr()
1095    }
1096
1097    fn as_mut_ptr(&mut self) -> *mut u8 {
1098        self.data.as_ptr()
1099    }
1100
1101    fn as_slice(&self) -> &[u8] {
1102        debug_assert!(self.len <= self.capacity);
1103        // SAFETY: `data` is either a valid allocation for `len` initialized
1104        // bytes or a dangling non-null pointer with `len == 0`.
1105        unsafe { std::slice::from_raw_parts(self.data.as_ptr(), self.len) }
1106    }
1107
1108    fn as_mut_slice(&mut self) -> &mut [u8] {
1109        debug_assert!(self.len <= self.capacity);
1110        // SAFETY: `data` is either a valid uniquely owned allocation for `len`
1111        // initialized bytes or a dangling non-null pointer with `len == 0`.
1112        unsafe { std::slice::from_raw_parts_mut(self.data.as_ptr(), self.len) }
1113    }
1114
1115    fn len(&self) -> usize {
1116        self.len
1117    }
1118
1119    fn is_empty(&self) -> bool {
1120        self.len == 0
1121    }
1122
1123    fn resize(&mut self, new_len: usize, value: u8) {
1124        if new_len == self.len {
1125            return;
1126        }
1127
1128        let mut resized = Self::new_filled(new_len, value).expect("protected resize failed");
1129        let len_to_copy = std::cmp::min(self.len, new_len);
1130        resized.as_mut_slice()[..len_to_copy].copy_from_slice(&self.as_slice()[..len_to_copy]);
1131        std::mem::swap(self, &mut resized);
1132    }
1133
1134    fn copy_from_slice(&mut self, other: &[u8]) {
1135        self.as_mut_slice().copy_from_slice(other);
1136    }
1137}
1138
1139impl Default for ProtectedBuffer {
1140    fn default() -> Self {
1141        Self {
1142            base: None,
1143            data: NonNull::dangling(),
1144            len: 0,
1145            capacity: 0,
1146            rounded_size: 0,
1147            total_size: 0,
1148        }
1149    }
1150}
1151
1152impl Clone for ProtectedBuffer {
1153    fn clone(&self) -> Self {
1154        Self::from_slice(self.as_slice()).expect("protected clone failed")
1155    }
1156}
1157
1158impl fmt::Debug for ProtectedBuffer {
1159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1160        f.debug_struct("ProtectedBuffer")
1161            .field("len", &self.len())
1162            .field("contents", &"[REDACTED]")
1163            .finish()
1164    }
1165}
1166
1167impl PartialEq for ProtectedBuffer {
1168    fn eq(&self, other: &Self) -> bool {
1169        self.as_slice().ct_eq(other.as_slice()).into()
1170    }
1171}
1172
1173impl Eq for ProtectedBuffer {}
1174
1175impl Zeroize for ProtectedBuffer {
1176    fn zeroize(&mut self) {
1177        self.as_mut_slice().zeroize();
1178    }
1179}
1180
1181impl Drop for ProtectedBuffer {
1182    fn drop(&mut self) {
1183        if let Some(base) = self.base.take() {
1184            if self.rounded_size != 0 {
1185                let _ = dryoc_mprotect_readwrite_ptr(self.data.as_ptr(), self.rounded_size);
1186            }
1187            self.as_mut_slice().zeroize();
1188            deallocate_raw_region(RawProtectedAllocation {
1189                base,
1190                data: self.data,
1191                rounded_size: self.rounded_size,
1192                total_size: self.total_size,
1193            });
1194        }
1195    }
1196}
1197
1198impl AsRef<[u8]> for ProtectedBuffer {
1199    fn as_ref(&self) -> &[u8] {
1200        self.as_slice()
1201    }
1202}
1203
1204impl AsMut<[u8]> for ProtectedBuffer {
1205    fn as_mut(&mut self) -> &mut [u8] {
1206        self.as_mut_slice()
1207    }
1208}
1209
1210impl std::ops::Deref for ProtectedBuffer {
1211    type Target = [u8];
1212
1213    fn deref(&self) -> &Self::Target {
1214        self.as_slice()
1215    }
1216}
1217
1218impl std::ops::DerefMut for ProtectedBuffer {
1219    fn deref_mut(&mut self) -> &mut Self::Target {
1220        self.as_mut_slice()
1221    }
1222}
1223
1224impl std::ops::Index<usize> for ProtectedBuffer {
1225    type Output = u8;
1226
1227    #[inline]
1228    fn index(&self, index: usize) -> &Self::Output {
1229        &self.as_slice()[index]
1230    }
1231}
1232
1233impl std::ops::IndexMut<usize> for ProtectedBuffer {
1234    #[inline]
1235    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
1236        &mut self.as_mut_slice()[index]
1237    }
1238}
1239
1240macro_rules! impl_index_protected_buffer {
1241    ($range:ty) => {
1242        impl std::ops::Index<$range> for ProtectedBuffer {
1243            type Output = [u8];
1244
1245            #[inline]
1246            fn index(&self, index: $range) -> &Self::Output {
1247                &self.as_slice()[index]
1248            }
1249        }
1250        impl std::ops::IndexMut<$range> for ProtectedBuffer {
1251            #[inline]
1252            fn index_mut(&mut self, index: $range) -> &mut Self::Output {
1253                &mut self.as_mut_slice()[index]
1254            }
1255        }
1256    };
1257}
1258
1259impl_index_protected_buffer!(std::ops::Range<usize>);
1260impl_index_protected_buffer!(std::ops::RangeFull);
1261impl_index_protected_buffer!(std::ops::RangeFrom<usize>);
1262impl_index_protected_buffer!(std::ops::RangeInclusive<usize>);
1263impl_index_protected_buffer!(std::ops::RangeTo<usize>);
1264impl_index_protected_buffer!(std::ops::RangeToInclusive<usize>);
1265
1266#[cfg(feature = "nightly")]
1267// SAFETY: `allocate` returns the user slice inside an owned allocation preceded
1268// by one guard page. `deallocate` subtracts that same guard-page offset,
1269// restores guard-page permissions, and releases the original allocation with
1270// the matching platform allocator.
1271unsafe impl Allocator for PageAlignedAllocator {
1272    #[inline]
1273    fn allocate(&self, layout: std::alloc::Layout) -> Result<NonNull<[u8]>, AllocError> {
1274        let pagesize = *PAGESIZE;
1275        if !pagesize.is_multiple_of(layout.align()) {
1276            return Err(AllocError);
1277        }
1278
1279        let raw = allocate_raw_region(layout.size()).map_err(|_| AllocError)?;
1280        // SAFETY: `raw.data` points to the unique user-visible allocation
1281        // region returned by `allocate_raw_region`.
1282        unsafe {
1283            Ok(NonNull::new_unchecked(ptr::slice_from_raw_parts_mut(
1284                raw.data.as_ptr(),
1285                layout.size(),
1286            )))
1287        }
1288    }
1289
1290    /// # Safety
1291    ///
1292    /// `ptr` must be a user-region pointer previously returned by this
1293    /// allocator's `allocate` method with the same `layout`.
1294    #[inline]
1295    // SAFETY: The caller contract above is the `Allocator::deallocate` safety
1296    // contract for this implementation.
1297    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: std::alloc::Layout) {
1298        let pagesize = *PAGESIZE;
1299
1300        // SAFETY: `ptr` points to the user region returned by `allocate`, which
1301        // starts exactly one guard page after the original allocation base.
1302        let base_ptr = unsafe { ptr.as_ptr().sub(pagesize) };
1303        let Some(base) = NonNull::new(base_ptr) else {
1304            return;
1305        };
1306        let Ok(raw_layout) = checked_raw_region_layout(layout.size(), pagesize) else {
1307            return;
1308        };
1309        deallocate_raw_region(RawProtectedAllocation {
1310            base,
1311            data: ptr,
1312            rounded_size: raw_layout.rounded_size,
1313            total_size: raw_layout.total_size,
1314        });
1315    }
1316}
1317
1318/// Provides a heap-allocated, fixed-length, page-aligned memory region.
1319///
1320/// This struct provides a heap-allocated fixed-length byte array. Required for
1321/// working with protected memory regions.
1322#[derive(Zeroize, ZeroizeOnDrop, Debug, PartialEq, Eq, Clone)]
1323pub struct HeapByteArray<const LENGTH: usize>(ProtectedBuffer);
1324
1325/// Provides a heap-allocated, resizable memory region.
1326///
1327/// This struct provides heap-allocated resizable byte array. Required for
1328/// working with protected memory regions.
1329#[derive(Zeroize, ZeroizeOnDrop, Debug, PartialEq, Eq, Clone, Default)]
1330pub struct HeapBytes(ProtectedBuffer);
1331
1332impl<A: Zeroize + NewBytes + Lockable<A>> NewLocked<A> for A {
1333    fn new_locked() -> Result<Protected<Self, traits::ReadWrite, traits::Locked>, error::Error> {
1334        Self::new_bytes().mlock()
1335    }
1336
1337    fn new_readonly_locked()
1338    -> Result<Protected<Self, traits::ReadOnly, traits::Locked>, error::Error> {
1339        Self::new_bytes()
1340            .mlock()
1341            .and_then(|p| p.mprotect_readonly())
1342    }
1343
1344    fn generate_locked() -> Result<Protected<Self, traits::ReadWrite, traits::Locked>, error::Error>
1345    {
1346        let mut res = Self::new_bytes().mlock()?;
1347        copy_randombytes(res.as_mut_slice());
1348        Ok(res)
1349    }
1350
1351    fn generate_readonly_locked()
1352    -> Result<Protected<Self, traits::ReadOnly, traits::Locked>, error::Error> {
1353        Self::generate_locked().and_then(|s| s.mprotect_readonly())
1354    }
1355}
1356
1357impl<A: Zeroize + NewBytes + ResizableBytes + Lockable<A>> NewLockedFromSlice<A> for A {
1358    /// Copies `src` into a new locked byte buffer.
1359    fn from_slice_into_locked(
1360        src: &[u8],
1361    ) -> Result<Protected<Self, traits::ReadWrite, traits::Locked>, crate::error::Error> {
1362        let mut res = Self::new_bytes().mlock()?;
1363        res.resize(src.len(), 0);
1364        res.as_mut_slice().copy_from_slice(src);
1365        Ok(res)
1366    }
1367
1368    /// Copies `src` into a new read-only, locked byte buffer.
1369    fn from_slice_into_readonly_locked(
1370        src: &[u8],
1371    ) -> Result<Protected<Self, traits::ReadOnly, traits::Locked>, crate::error::Error> {
1372        Self::from_slice_into_locked(src).and_then(|s| s.mprotect_readonly())
1373    }
1374}
1375
1376impl<const LENGTH: usize> NewLockedFromSlice<HeapByteArray<LENGTH>> for HeapByteArray<LENGTH> {
1377    /// Copies `other` into a new fixed-size locked byte array.
1378    fn from_slice_into_locked(
1379        other: &[u8],
1380    ) -> Result<Protected<Self, traits::ReadWrite, traits::Locked>, crate::error::Error> {
1381        if other.len() != LENGTH {
1382            return Err(length_error!(crate::ErrorContext::Slice, other.len(), exact LENGTH));
1383        }
1384        let mut res = Self::new_bytes().mlock()?;
1385        res.as_mut_slice().copy_from_slice(other);
1386        Ok(res)
1387    }
1388
1389    fn from_slice_into_readonly_locked(
1390        other: &[u8],
1391    ) -> Result<Protected<Self, traits::ReadOnly, traits::Locked>, crate::error::Error> {
1392        Self::from_slice_into_locked(other).and_then(|s| s.mprotect_readonly())
1393    }
1394}
1395
1396impl<const LENGTH: usize> Bytes for HeapByteArray<LENGTH> {
1397    #[inline]
1398    fn as_slice(&self) -> &[u8] {
1399        &self.0
1400    }
1401
1402    #[inline]
1403    fn len(&self) -> usize {
1404        self.0.len()
1405    }
1406
1407    #[inline]
1408    fn is_empty(&self) -> bool {
1409        self.0.is_empty()
1410    }
1411}
1412
1413impl Bytes for HeapBytes {
1414    #[inline]
1415    fn as_slice(&self) -> &[u8] {
1416        &self.0
1417    }
1418
1419    #[inline]
1420    fn len(&self) -> usize {
1421        self.0.len()
1422    }
1423
1424    #[inline]
1425    fn is_empty(&self) -> bool {
1426        self.0.is_empty()
1427    }
1428}
1429
1430impl<const LENGTH: usize> MutBytes for HeapByteArray<LENGTH> {
1431    #[inline]
1432    fn as_mut_slice(&mut self) -> &mut [u8] {
1433        self.0.as_mut_slice()
1434    }
1435
1436    fn copy_from_slice(&mut self, other: &[u8]) {
1437        self.0.copy_from_slice(other)
1438    }
1439}
1440
1441impl NewBytes for HeapBytes {
1442    fn new_bytes() -> Self {
1443        Self::default()
1444    }
1445}
1446
1447impl MutBytes for HeapBytes {
1448    #[inline]
1449    fn as_mut_slice(&mut self) -> &mut [u8] {
1450        self.0.as_mut_slice()
1451    }
1452
1453    fn copy_from_slice(&mut self, other: &[u8]) {
1454        self.0.copy_from_slice(other)
1455    }
1456}
1457
1458impl ResizableBytes for HeapBytes {
1459    fn resize(&mut self, new_len: usize, value: u8) {
1460        self.0.resize(new_len, value);
1461    }
1462}
1463
1464impl<A: Zeroize + NewBytes + ResizableBytes + Lockable<A>> ResizableBytes
1465    for Protected<A, traits::ReadWrite, traits::Locked>
1466{
1467    fn resize(&mut self, new_len: usize, value: u8) {
1468        match &mut self.i {
1469            Some(d) => {
1470                // because it's locked, we'll do a swaparoo here instead of a plain resize
1471                let mut new = A::new_bytes();
1472                // resize the new array
1473                new.resize(new_len, value);
1474                // need to actually lock the memory now, because it was previously locked
1475                let mut locked = new.mlock().expect("unable to lock on resize");
1476                let len_to_copy = std::cmp::min(new_len, d.a.as_slice().len());
1477                locked.i.as_mut().unwrap().a.as_mut_slice()[..len_to_copy]
1478                    .copy_from_slice(&d.a.as_slice()[..len_to_copy]);
1479                std::mem::swap(&mut locked.i, &mut self.i);
1480                // when dropped, the old region will unlock automatically in
1481                // Drop
1482            }
1483            None => panic!("invalid array"),
1484        }
1485    }
1486}
1487
1488impl<A: Zeroize + NewBytes + ResizableBytes + Lockable<A>> ResizableBytes
1489    for Protected<A, traits::ReadWrite, traits::Unlocked>
1490{
1491    fn resize(&mut self, new_len: usize, value: u8) {
1492        match &mut self.i {
1493            Some(d) => d.a.resize(new_len, value),
1494            None => panic!("invalid array"),
1495        }
1496    }
1497}
1498
1499impl<A: Zeroize + MutBytes, LM: traits::LockMode> MutBytes for Protected<A, traits::ReadWrite, LM> {
1500    #[inline]
1501    fn as_mut_slice(&mut self) -> &mut [u8] {
1502        match &mut self.i {
1503            Some(d) => d.a.as_mut_slice(),
1504            None => panic!("invalid array"),
1505        }
1506    }
1507
1508    fn copy_from_slice(&mut self, other: &[u8]) {
1509        match &mut self.i {
1510            Some(d) => d.a.copy_from_slice(other),
1511            None => panic!("invalid array"),
1512        }
1513    }
1514}
1515
1516impl<const LENGTH: usize> std::convert::AsRef<[u8; LENGTH]> for HeapByteArray<LENGTH> {
1517    fn as_ref(&self) -> &[u8; LENGTH] {
1518        let arr = self.0.as_ptr() as *const [u8; LENGTH];
1519        // SAFETY: `HeapByteArray<LENGTH>` always allocates exactly `LENGTH`
1520        // initialized bytes, and `[u8; LENGTH]` has alignment 1.
1521        unsafe { &*arr }
1522    }
1523}
1524
1525impl<const LENGTH: usize> std::convert::AsMut<[u8; LENGTH]> for HeapByteArray<LENGTH> {
1526    fn as_mut(&mut self) -> &mut [u8; LENGTH] {
1527        let arr = self.0.as_mut_ptr() as *mut [u8; LENGTH];
1528        // SAFETY: `HeapByteArray<LENGTH>` always allocates exactly `LENGTH`
1529        // initialized bytes. `&mut self` provides exclusive access to them.
1530        unsafe { &mut *arr }
1531    }
1532}
1533
1534impl<const LENGTH: usize> std::convert::AsRef<[u8]> for HeapByteArray<LENGTH> {
1535    fn as_ref(&self) -> &[u8] {
1536        self.0.as_ref()
1537    }
1538}
1539
1540impl std::convert::AsRef<[u8]> for HeapBytes {
1541    fn as_ref(&self) -> &[u8] {
1542        self.0.as_ref()
1543    }
1544}
1545
1546impl<const LENGTH: usize> std::convert::AsMut<[u8]> for HeapByteArray<LENGTH> {
1547    fn as_mut(&mut self) -> &mut [u8] {
1548        self.0.as_mut()
1549    }
1550}
1551
1552impl std::convert::AsMut<[u8]> for HeapBytes {
1553    fn as_mut(&mut self) -> &mut [u8] {
1554        self.0.as_mut()
1555    }
1556}
1557
1558impl<const LENGTH: usize> std::ops::Deref for HeapByteArray<LENGTH> {
1559    type Target = [u8];
1560
1561    fn deref(&self) -> &Self::Target {
1562        &self.0
1563    }
1564}
1565
1566impl<const LENGTH: usize> std::ops::DerefMut for HeapByteArray<LENGTH> {
1567    fn deref_mut(&mut self) -> &mut Self::Target {
1568        &mut self.0
1569    }
1570}
1571
1572impl std::ops::Deref for HeapBytes {
1573    type Target = [u8];
1574
1575    fn deref(&self) -> &Self::Target {
1576        &self.0
1577    }
1578}
1579
1580impl std::ops::DerefMut for HeapBytes {
1581    fn deref_mut(&mut self) -> &mut Self::Target {
1582        &mut self.0
1583    }
1584}
1585
1586impl<A: Bytes + Zeroize, LM: traits::LockMode> std::ops::Deref
1587    for Protected<A, traits::ReadOnly, LM>
1588{
1589    type Target = [u8];
1590
1591    fn deref(&self) -> &Self::Target {
1592        self.i.as_ref().unwrap().a.as_slice()
1593    }
1594}
1595
1596impl<A: Bytes + Zeroize, LM: traits::LockMode> std::ops::Deref
1597    for Protected<A, traits::ReadWrite, LM>
1598{
1599    type Target = [u8];
1600
1601    fn deref(&self) -> &Self::Target {
1602        self.i.as_ref().unwrap().a.as_slice()
1603    }
1604}
1605
1606impl<A: MutBytes + Zeroize, LM: traits::LockMode> std::ops::DerefMut
1607    for Protected<A, traits::ReadWrite, LM>
1608{
1609    fn deref_mut(&mut self) -> &mut Self::Target {
1610        self.i.as_mut().unwrap().a.as_mut_slice()
1611    }
1612}
1613
1614impl<const LENGTH: usize> std::ops::Index<usize> for HeapByteArray<LENGTH> {
1615    type Output = u8;
1616
1617    #[inline]
1618    fn index(&self, index: usize) -> &Self::Output {
1619        &self.0[index]
1620    }
1621}
1622impl<const LENGTH: usize> std::ops::IndexMut<usize> for HeapByteArray<LENGTH> {
1623    #[inline]
1624    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
1625        &mut self.0[index]
1626    }
1627}
1628
1629macro_rules! impl_index_heapbytearray {
1630    ($range:ty) => {
1631        impl<const LENGTH: usize> std::ops::Index<$range> for HeapByteArray<LENGTH> {
1632            type Output = [u8];
1633
1634            #[inline]
1635            fn index(&self, index: $range) -> &Self::Output {
1636                &self.0[index]
1637            }
1638        }
1639        impl<const LENGTH: usize> std::ops::IndexMut<$range> for HeapByteArray<LENGTH> {
1640            #[inline]
1641            fn index_mut(&mut self, index: $range) -> &mut Self::Output {
1642                &mut self.0[index]
1643            }
1644        }
1645    };
1646}
1647
1648impl_index_heapbytearray!(std::ops::Range<usize>);
1649impl_index_heapbytearray!(std::ops::RangeFull);
1650impl_index_heapbytearray!(std::ops::RangeFrom<usize>);
1651impl_index_heapbytearray!(std::ops::RangeInclusive<usize>);
1652impl_index_heapbytearray!(std::ops::RangeTo<usize>);
1653impl_index_heapbytearray!(std::ops::RangeToInclusive<usize>);
1654
1655impl<const LENGTH: usize> Default for HeapByteArray<LENGTH> {
1656    fn default() -> Self {
1657        Self(ProtectedBuffer::new_filled(LENGTH, 0).expect("protected allocation failed"))
1658    }
1659}
1660
1661impl<A: Zeroize + NewBytes + Lockable<A> + NewLocked<A>> Default
1662    for Protected<A, traits::ReadWrite, traits::Locked>
1663{
1664    fn default() -> Self {
1665        A::new_locked().expect("mlock failed")
1666    }
1667}
1668
1669impl std::ops::Index<usize> for HeapBytes {
1670    type Output = u8;
1671
1672    #[inline]
1673    fn index(&self, index: usize) -> &Self::Output {
1674        &self.0[index]
1675    }
1676}
1677impl std::ops::IndexMut<usize> for HeapBytes {
1678    #[inline]
1679    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
1680        &mut self.0[index]
1681    }
1682}
1683
1684macro_rules! impl_index_heapbytes {
1685    ($range:ty) => {
1686        impl std::ops::Index<$range> for HeapBytes {
1687            type Output = [u8];
1688
1689            #[inline]
1690            fn index(&self, index: $range) -> &Self::Output {
1691                &self.0[index]
1692            }
1693        }
1694        impl std::ops::IndexMut<$range> for HeapBytes {
1695            #[inline]
1696            fn index_mut(&mut self, index: $range) -> &mut Self::Output {
1697                &mut self.0[index]
1698            }
1699        }
1700    };
1701}
1702
1703impl_index_heapbytes!(std::ops::Range<usize>);
1704impl_index_heapbytes!(std::ops::RangeFull);
1705impl_index_heapbytes!(std::ops::RangeFrom<usize>);
1706impl_index_heapbytes!(std::ops::RangeInclusive<usize>);
1707impl_index_heapbytes!(std::ops::RangeTo<usize>);
1708impl_index_heapbytes!(std::ops::RangeToInclusive<usize>);
1709
1710impl<const LENGTH: usize> From<&[u8; LENGTH]> for HeapByteArray<LENGTH> {
1711    fn from(src: &[u8; LENGTH]) -> Self {
1712        let mut arr = Self::default();
1713        arr.0.copy_from_slice(src);
1714        arr
1715    }
1716}
1717
1718impl<const LENGTH: usize> From<[u8; LENGTH]> for HeapByteArray<LENGTH> {
1719    fn from(mut src: [u8; LENGTH]) -> Self {
1720        let ret = Self::from(&src);
1721        // need to zeroize this input
1722        src.zeroize();
1723        ret
1724    }
1725}
1726
1727impl<const LENGTH: usize> TryFrom<&[u8]> for HeapByteArray<LENGTH> {
1728    type Error = error::Error;
1729
1730    fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
1731        if src.len() != LENGTH {
1732            Err(length_error!(crate::ErrorContext::Slice, src.len(), exact LENGTH))
1733        } else {
1734            let mut arr = Self::default();
1735            arr.0.copy_from_slice(src);
1736            Ok(arr)
1737        }
1738    }
1739}
1740
1741impl From<&[u8]> for HeapBytes {
1742    fn from(src: &[u8]) -> Self {
1743        Self(ProtectedBuffer::from_slice(src).expect("protected allocation failed"))
1744    }
1745}
1746
1747impl<const LENGTH: usize> ByteArray<LENGTH> for HeapByteArray<LENGTH> {
1748    #[inline]
1749    fn as_array(&self) -> &[u8; LENGTH] {
1750        let ptr = self.0.as_ptr() as *const [u8; LENGTH];
1751        // SAFETY: `HeapByteArray<LENGTH>` always allocates exactly `LENGTH`
1752        // initialized bytes, and `[u8; LENGTH]` has alignment 1.
1753        unsafe { &*ptr }
1754    }
1755}
1756
1757impl<const LENGTH: usize> NewBytes for HeapByteArray<LENGTH> {
1758    fn new_bytes() -> Self {
1759        Self::default()
1760    }
1761}
1762
1763impl NewBytes for Protected<HeapBytes, traits::ReadWrite, traits::Locked> {
1764    fn new_bytes() -> Self {
1765        match HeapBytes::new_locked() {
1766            Ok(r) => r,
1767            Err(err) => panic!("Error creating locked bytes: {:?}", err),
1768        }
1769    }
1770}
1771
1772impl<const LENGTH: usize> NewBytes
1773    for Protected<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Locked>
1774{
1775    fn new_bytes() -> Self {
1776        match HeapByteArray::<LENGTH>::new_locked() {
1777            Ok(r) => r,
1778            Err(err) => panic!("Error creating locked bytes: {:?}", err),
1779        }
1780    }
1781}
1782
1783impl<const LENGTH: usize> NewByteArray<LENGTH>
1784    for Protected<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Locked>
1785{
1786    fn new_byte_array() -> Self {
1787        match HeapByteArray::<LENGTH>::new_locked() {
1788            Ok(r) => r,
1789            Err(err) => panic!("Error creating locked bytes: {:?}", err),
1790        }
1791    }
1792
1793    fn r#gen() -> Self {
1794        match HeapByteArray::<LENGTH>::new_locked() {
1795            Ok(mut r) => {
1796                copy_randombytes(r.as_mut_slice());
1797                r
1798            }
1799            Err(err) => panic!("Error creating locked bytes: {:?}", err),
1800        }
1801    }
1802}
1803
1804impl<const LENGTH: usize> NewByteArray<LENGTH> for HeapByteArray<LENGTH> {
1805    fn new_byte_array() -> Self {
1806        Self::default()
1807    }
1808
1809    /// Returns a new byte array filled with random data.
1810    fn r#gen() -> Self {
1811        let mut res = Self::default();
1812        copy_randombytes(res.as_mut_slice());
1813        res
1814    }
1815}
1816
1817impl<const LENGTH: usize> MutByteArray<LENGTH> for HeapByteArray<LENGTH> {
1818    fn as_mut_array(&mut self) -> &mut [u8; LENGTH] {
1819        let ptr = self.0.as_mut_ptr() as *mut [u8; LENGTH];
1820        // SAFETY: `HeapByteArray<LENGTH>` always allocates exactly `LENGTH`
1821        // initialized bytes. `&mut self` provides exclusive access to them.
1822        unsafe { &mut *ptr }
1823    }
1824}
1825
1826impl<const LENGTH: usize> ByteArray<LENGTH>
1827    for Protected<HeapByteArray<LENGTH>, traits::ReadOnly, traits::Unlocked>
1828{
1829    #[inline]
1830    fn as_array(&self) -> &[u8; LENGTH] {
1831        match &self.i {
1832            Some(d) => d.a.as_array(),
1833            None => panic!("invalid array"),
1834        }
1835    }
1836}
1837
1838impl<const LENGTH: usize> ByteArray<LENGTH>
1839    for Protected<HeapByteArray<LENGTH>, traits::ReadOnly, traits::Locked>
1840{
1841    #[inline]
1842    fn as_array(&self) -> &[u8; LENGTH] {
1843        match &self.i {
1844            Some(d) => d.a.as_array(),
1845            None => panic!("invalid array"),
1846        }
1847    }
1848}
1849
1850impl<const LENGTH: usize> ByteArray<LENGTH>
1851    for Protected<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Unlocked>
1852{
1853    #[inline]
1854    fn as_array(&self) -> &[u8; LENGTH] {
1855        match &self.i {
1856            Some(d) => d.a.as_array(),
1857            None => panic!("invalid array"),
1858        }
1859    }
1860}
1861
1862impl<const LENGTH: usize> ByteArray<LENGTH>
1863    for Protected<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Locked>
1864{
1865    #[inline]
1866    fn as_array(&self) -> &[u8; LENGTH] {
1867        match &self.i {
1868            Some(d) => d.a.as_array(),
1869            None => panic!("invalid array"),
1870        }
1871    }
1872}
1873
1874impl<const LENGTH: usize> MutByteArray<LENGTH>
1875    for Protected<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Locked>
1876{
1877    #[inline]
1878    fn as_mut_array(&mut self) -> &mut [u8; LENGTH] {
1879        match &mut self.i {
1880            Some(d) => d.a.as_mut_array(),
1881            None => panic!("invalid array"),
1882        }
1883    }
1884}
1885
1886impl<const LENGTH: usize> MutByteArray<LENGTH>
1887    for Protected<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Unlocked>
1888{
1889    #[inline]
1890    fn as_mut_array(&mut self) -> &mut [u8; LENGTH] {
1891        match &mut self.i {
1892            Some(d) => d.a.as_mut_array(),
1893            None => panic!("invalid array"),
1894        }
1895    }
1896}
1897
1898impl<const LENGTH: usize> AsMut<[u8; LENGTH]>
1899    for Protected<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Locked>
1900{
1901    fn as_mut(&mut self) -> &mut [u8; LENGTH] {
1902        match &mut self.i {
1903            Some(d) => d.a.as_mut(),
1904            None => panic!("invalid array"),
1905        }
1906    }
1907}
1908
1909impl<const LENGTH: usize> AsMut<[u8; LENGTH]>
1910    for Protected<HeapByteArray<LENGTH>, traits::ReadWrite, traits::Unlocked>
1911{
1912    fn as_mut(&mut self) -> &mut [u8; LENGTH] {
1913        match &mut self.i {
1914            Some(d) => d.a.as_mut(),
1915            None => panic!("invalid array"),
1916        }
1917    }
1918}
1919
1920impl<A: Zeroize + Bytes, PM: traits::ProtectMode, LM: traits::LockMode> Drop
1921    for Protected<A, PM, LM>
1922{
1923    fn drop(&mut self) {
1924        let Some(mut data) = self.i.take() else {
1925            return;
1926        };
1927
1928        let writable = data.a.as_slice().is_empty()
1929            || data.pm == int::ProtectMode::ReadWrite
1930            || match dryoc_mprotect_readwrite(data.a.as_slice()) {
1931                Ok(()) => true,
1932                Err(err) => abort_protected_memory_failure("making memory writable for drop", err),
1933            };
1934
1935        if writable {
1936            data.a.zeroize();
1937        }
1938
1939        if data.lm == int::LockMode::Locked {
1940            match dryoc_munlock(data.a.as_slice()) {
1941                Ok(()) => data.lm = int::LockMode::Unlocked,
1942                Err(err) => abort_protected_memory_failure("unlocking memory for drop", err),
1943            }
1944        }
1945    }
1946}
1947
1948impl<A: Zeroize + Bytes, PM: traits::ProtectMode, LM: traits::LockMode> ZeroizeOnDrop
1949    for Protected<A, PM, LM>
1950{
1951}
1952
1953impl<A: Zeroize + Bytes, PM: traits::ProtectMode, LM: traits::LockMode> Zeroize
1954    for Protected<A, PM, LM>
1955{
1956    fn zeroize(&mut self) {
1957        let Some(data) = &mut self.i else {
1958            return;
1959        };
1960        if data.a.as_slice().is_empty() {
1961            return;
1962        }
1963
1964        let previous_mode = data.pm.clone();
1965        if previous_mode != int::ProtectMode::ReadWrite
1966            && let Err(error) = dryoc_mprotect_readwrite(data.a.as_slice())
1967        {
1968            abort_protected_memory_failure("making memory writable for zeroization", error);
1969        }
1970
1971        data.a.zeroize();
1972
1973        if previous_mode != int::ProtectMode::ReadWrite
1974            && let Err(error) = dryoc_mprotect_mode(data.a.as_slice(), &previous_mode)
1975        {
1976            abort_protected_memory_failure("restoring memory protection after zeroization", error);
1977        }
1978    }
1979}
1980
1981fn abort_protected_memory_failure(_operation: &str, _error: std::io::Error) -> ! {
1982    std::process::abort()
1983}
1984
1985#[cfg(test)]
1986mod tests {
1987    use proptest::prelude::*;
1988
1989    use super::*;
1990
1991    #[test]
1992    fn protected_byte_array_debug_redacts_contents() {
1993        let bytes = HeapByteArray::from(StackByteArray::from([0xabu8; 4]));
1994        let debug = format!("{bytes:?}");
1995
1996        assert!(debug.contains("[REDACTED]"));
1997        assert!(!debug.contains("171"));
1998    }
1999
2000    fn interesting_lengths() -> impl Strategy<Value = usize> {
2001        let pagesize = *PAGESIZE;
2002        let max = pagesize.saturating_mul(2).saturating_add(8);
2003
2004        prop_oneof![
2005            Just(0usize),
2006            Just(1),
2007            0usize..=128,
2008            pagesize.saturating_sub(8)..=pagesize.saturating_add(8),
2009            pagesize.saturating_mul(2).saturating_sub(8)..=max,
2010        ]
2011        .boxed()
2012    }
2013
2014    fn small_lengths() -> impl Strategy<Value = usize> {
2015        prop_oneof![Just(0usize), Just(1), 0usize..=256].boxed()
2016    }
2017
2018    fn interesting_bytes() -> impl Strategy<Value = Vec<u8>> {
2019        interesting_lengths()
2020            .prop_flat_map(|len| prop::collection::vec(any::<u8>(), len))
2021            .boxed()
2022    }
2023
2024    fn small_bytes() -> impl Strategy<Value = Vec<u8>> {
2025        small_lengths()
2026            .prop_flat_map(|len| prop::collection::vec(any::<u8>(), len))
2027            .boxed()
2028    }
2029
2030    #[cfg_attr(
2031        tarpaulin,
2032        ignore = "tarpaulin can segfault while tracing mlock/mprotect tests"
2033    )]
2034    #[test]
2035    fn test_lock_unlock() {
2036        use crate::dryocstream::Key;
2037
2038        let key = Key::generate();
2039        let key_clone = key.clone();
2040
2041        let locked_key = key.mlock().expect("lock failed");
2042
2043        let unlocked_key = locked_key.munlock().expect("unlock failed");
2044
2045        assert_eq!(unlocked_key.as_slice(), key_clone.as_slice());
2046    }
2047
2048    #[cfg_attr(
2049        tarpaulin,
2050        ignore = "tarpaulin can segfault while tracing mlock/mprotect tests"
2051    )]
2052    #[test]
2053    fn explicit_zeroize_preserves_locked_readwrite_state() {
2054        let mut locked =
2055            HeapBytes::from_slice_into_locked(b"sensitive").expect("locked allocation failed");
2056
2057        locked.zeroize();
2058
2059        assert_eq!(locked.as_slice(), &[0; 9]);
2060        let state = locked.i.as_ref().expect("protected state missing");
2061        assert_eq!(state.lm, int::LockMode::Locked);
2062        assert_eq!(state.pm, int::ProtectMode::ReadWrite);
2063
2064        let unlocked = locked.munlock().expect("unlock after zeroize failed");
2065        assert_eq!(unlocked.as_slice(), &[0; 9]);
2066    }
2067
2068    #[cfg(unix)]
2069    #[cfg_attr(
2070        tarpaulin,
2071        ignore = "tarpaulin can segfault while tracing mlock/mprotect tests"
2072    )]
2073    #[test]
2074    fn explicit_zeroize_restores_readonly_protection() {
2075        let mut readonly = HeapBytes::from_slice_into_readonly_locked(b"sensitive")
2076            .expect("read-only locked allocation failed");
2077
2078        readonly.zeroize();
2079
2080        assert_eq!(readonly.as_slice(), &[0; 9]);
2081        let state = readonly.i.as_ref().expect("protected state missing");
2082        assert_eq!(state.lm, int::LockMode::Locked);
2083        assert_eq!(state.pm, int::ProtectMode::ReadOnly);
2084
2085        // Verify the operating-system permissions, not just the typestate.
2086        let child = unsafe { libc::fork() };
2087        assert!(child >= 0, "fork failed");
2088        if child == 0 {
2089            let data = readonly.as_slice().as_ptr() as *mut u8;
2090            // SAFETY: The child intentionally probes the read-only page. A
2091            // correct implementation terminates it with SIGSEGV or SIGBUS.
2092            unsafe {
2093                std::ptr::write_volatile(data, 1);
2094                libc::_exit(0);
2095            }
2096        }
2097
2098        let mut status = 0;
2099        // SAFETY: `child` is the positive PID returned by `fork`, and `status`
2100        // points to writable storage for the wait status.
2101        let wait_ret = unsafe { libc::waitpid(child, &mut status, 0) };
2102        assert_eq!(wait_ret, child);
2103        assert!(
2104            libc::WIFSIGNALED(status),
2105            "child unexpectedly wrote to explicitly zeroized read-only memory"
2106        );
2107
2108        let readwrite = readonly
2109            .mprotect_readwrite()
2110            .expect("read-write transition failed");
2111        let unlocked = readwrite.munlock().expect("unlock failed");
2112        assert_eq!(unlocked.as_slice(), &[0; 9]);
2113    }
2114
2115    #[cfg_attr(
2116        tarpaulin,
2117        ignore = "tarpaulin can segfault while tracing mlock/mprotect tests"
2118    )]
2119    #[test]
2120    fn test_protect_unprotect() {
2121        use crate::dryocstream::Key;
2122
2123        let key = Key::generate();
2124        let key_clone = key.clone();
2125
2126        let readonly_key = key.mprotect_readonly().expect("mprotect failed");
2127        assert_eq!(readonly_key.as_slice(), key_clone.as_slice());
2128
2129        let mut readwrite_key = readonly_key.mprotect_readwrite().expect("mprotect failed");
2130        assert_eq!(readwrite_key.as_slice(), key_clone.as_slice());
2131
2132        // should be able to write now without blowing up
2133        readwrite_key.as_mut_slice()[0] = 0;
2134    }
2135
2136    #[cfg(feature = "nightly")]
2137    #[test]
2138    fn test_allocator() {
2139        let mut vec: Vec<i32, _> = Vec::new_in(PageAlignedAllocator);
2140
2141        vec.push(1);
2142        vec.push(2);
2143        vec.push(3);
2144
2145        for i in 0..5000 {
2146            vec.push(i);
2147        }
2148
2149        vec.resize(5, 0);
2150
2151        assert_eq!([1, 2, 3, 0, 1], vec.as_slice());
2152    }
2153
2154    #[cfg(feature = "nightly")]
2155    #[test]
2156    fn test_allocator_honors_supported_alignment() {
2157        let allocator = PageAlignedAllocator;
2158        let layout = std::alloc::Layout::from_size_align(1, *PAGESIZE).unwrap();
2159        let allocation = allocator.allocate(layout).unwrap();
2160        let data = allocation.as_ptr() as *mut u8;
2161
2162        assert_eq!(data.addr() % layout.align(), 0);
2163
2164        // SAFETY: `data` was returned by `allocator` for this exact `layout`.
2165        unsafe { allocator.deallocate(NonNull::new_unchecked(data), layout) };
2166    }
2167
2168    #[cfg(feature = "nightly")]
2169    #[test]
2170    fn test_allocator_rejects_unsupported_alignment() {
2171        let unsupported_alignment = PAGESIZE.checked_mul(2).unwrap();
2172        let layout = std::alloc::Layout::from_size_align(1, unsupported_alignment).unwrap();
2173
2174        assert!(PageAlignedAllocator.allocate(layout).is_err());
2175    }
2176
2177    #[cfg(feature = "nightly")]
2178    #[test]
2179    fn test_allocator_handles_zero_sized_layout() {
2180        let allocator = PageAlignedAllocator;
2181        let layout = std::alloc::Layout::from_size_align(0, 1).unwrap();
2182        let allocation = allocator.allocate(layout).unwrap();
2183        let data = allocation.as_ptr() as *mut u8;
2184
2185        assert_eq!(allocation.len(), 0);
2186        assert_eq!(data.addr() % layout.align(), 0);
2187
2188        // SAFETY: `data` was returned by `allocator` for this exact `layout`.
2189        unsafe { allocator.deallocate(NonNull::new_unchecked(data), layout) };
2190    }
2191
2192    #[test]
2193    fn test_page_rounding() {
2194        let pagesize = *PAGESIZE;
2195
2196        assert_eq!(_page_round(0, pagesize), Some(0));
2197        assert_eq!(_page_round(1, pagesize), Some(pagesize));
2198        assert_eq!(_page_round(pagesize, pagesize), Some(pagesize));
2199        assert_eq!(_page_round(pagesize + 1, pagesize), Some(pagesize * 2));
2200        assert_eq!(_page_round(usize::MAX, pagesize), None);
2201    }
2202
2203    #[cfg(unix)]
2204    #[test]
2205    fn test_page_size_from_sysconf_handles_error_sentinel() {
2206        assert_eq!(page_size_from_sysconf(-1), DEFAULT_PAGESIZE);
2207        assert_eq!(page_size_from_sysconf(0), DEFAULT_PAGESIZE);
2208        assert_eq!(page_size_from_sysconf(8192), 8192);
2209    }
2210
2211    #[test]
2212    fn test_empty_heapbytes_and_locking() {
2213        let empty = HeapBytes::default();
2214        assert!(empty.is_empty());
2215        assert_eq!(empty.as_slice().len(), 0);
2216
2217        let locked: LockedBytes = HeapBytes::new_locked().expect("empty mlock failed");
2218        assert!(locked.is_empty());
2219
2220        let unlocked = locked.munlock().expect("empty munlock failed");
2221        assert!(unlocked.is_empty());
2222    }
2223
2224    #[test]
2225    fn test_heapbytes_resize_grow_shrink_and_fill() {
2226        let mut bytes = HeapBytes::default();
2227        bytes.resize(3, 0x7a);
2228        assert_eq!(bytes.as_slice(), &[0x7a, 0x7a, 0x7a]);
2229
2230        bytes.as_mut_slice()[1] = 0x11;
2231        bytes.resize(5, 0x5a);
2232        assert_eq!(bytes.as_slice(), &[0x7a, 0x11, 0x7a, 0x5a, 0x5a]);
2233
2234        bytes.resize(2, 0);
2235        assert_eq!(bytes.as_slice(), &[0x7a, 0x11]);
2236
2237        bytes.resize(0, 0);
2238        assert!(bytes.is_empty());
2239    }
2240
2241    proptest! {
2242        #![proptest_config(ProptestConfig::with_cases(64))]
2243
2244        #[test]
2245        fn proptest_heapbytes_roundtrip_clone_and_mutation(data in interesting_bytes()) {
2246            let bytes = HeapBytes::from(data.as_slice());
2247            prop_assert_eq!(bytes.len(), data.len());
2248            prop_assert_eq!(bytes.as_slice(), data.as_slice());
2249            prop_assert_eq!(bytes.as_ref(), data.as_slice());
2250
2251            let mut cloned = bytes.clone();
2252            prop_assert_eq!(&cloned, &bytes);
2253            prop_assert_eq!(cloned.as_slice(), data.as_slice());
2254
2255            if !data.is_empty() {
2256                prop_assert_eq!(cloned[0], data[0]);
2257
2258                let last = data.len() - 1;
2259                prop_assert_eq!(cloned[last], data[last]);
2260
2261                cloned[0] = cloned[0].wrapping_add(1);
2262                prop_assert_ne!(cloned[0], data[0]);
2263                prop_assert_eq!(&cloned[1..], &data[1..]);
2264            }
2265        }
2266
2267        #[test]
2268        fn proptest_heapbytes_resize_matches_vec_model(
2269            initial in interesting_bytes(),
2270            ops in prop::collection::vec((interesting_lengths(), any::<u8>()), 0..12),
2271        ) {
2272            let mut bytes = HeapBytes::from(initial.as_slice());
2273            let mut model = initial;
2274
2275            for (new_len, value) in ops {
2276                bytes.resize(new_len, value);
2277                model.resize(new_len, value);
2278                prop_assert_eq!(bytes.as_slice(), model.as_slice());
2279            }
2280        }
2281
2282        #[test]
2283        fn proptest_protection_transitions_preserve_bytes(data in interesting_bytes()) {
2284            let protected =
2285                Protected::<HeapBytes, traits::ReadWrite, traits::Unlocked>::new_with(
2286                    HeapBytes::from(data.as_slice()),
2287                );
2288
2289            let readonly = protected
2290                .mprotect_readonly()
2291                .expect("readonly mprotect failed");
2292            prop_assert_eq!(readonly.as_slice(), data.as_slice());
2293
2294            let readwrite = readonly
2295                .mprotect_readwrite()
2296                .expect("readwrite mprotect failed");
2297            prop_assert_eq!(readwrite.as_slice(), data.as_slice());
2298
2299            let noaccess = readwrite
2300                .mprotect_noaccess()
2301                .expect("noaccess mprotect failed");
2302            let readwrite = noaccess
2303                .mprotect_readwrite()
2304                .expect("readwrite mprotect failed");
2305            prop_assert_eq!(readwrite.as_slice(), data.as_slice());
2306        }
2307    }
2308
2309    proptest! {
2310        #![proptest_config(ProptestConfig::with_cases(32))]
2311
2312        #[test]
2313        fn proptest_locked_heapbytes_resize_matches_vec_model(
2314            initial in small_bytes(),
2315            ops in prop::collection::vec((small_lengths(), any::<u8>()), 0..8),
2316        ) {
2317            let mut locked = HeapBytes::from_slice_into_locked(initial.as_slice())
2318                .expect("locked allocation failed");
2319            let mut model = initial;
2320
2321            for (new_len, value) in ops {
2322                locked.resize(new_len, value);
2323                model.resize(new_len, value);
2324                prop_assert_eq!(locked.as_slice(), model.as_slice());
2325            }
2326
2327            let unlocked = locked.munlock().expect("munlock failed");
2328            prop_assert_eq!(unlocked.as_slice(), model.as_slice());
2329        }
2330
2331        #[test]
2332        fn proptest_heapbytearray_exact_size_views(data in any::<[u8; 32]>()) {
2333            let mut bytes = HeapByteArray::<32>::from(&data);
2334
2335            prop_assert_eq!(bytes.as_array(), &data);
2336            prop_assert_eq!(AsRef::<[u8; 32]>::as_ref(&bytes), &data);
2337            prop_assert_eq!(bytes.as_slice(), &data);
2338
2339            let mut expected = data;
2340            bytes.as_mut_array()[7] ^= 0xa5;
2341            expected[7] ^= 0xa5;
2342            prop_assert_eq!(bytes.as_array(), &expected);
2343
2344            AsMut::<[u8; 32]>::as_mut(&mut bytes)[24] = 0x5a;
2345            expected[24] = 0x5a;
2346            prop_assert_eq!(bytes.as_slice(), &expected);
2347        }
2348    }
2349
2350    #[test]
2351    fn test_heapbytearray_exact_size_views() {
2352        let mut bytes = HeapByteArray::<4>::default();
2353        bytes.as_mut_array().copy_from_slice(&[1, 2, 3, 4]);
2354
2355        assert_eq!(bytes.as_array(), &[1, 2, 3, 4]);
2356        assert_eq!(AsRef::<[u8; 4]>::as_ref(&bytes), &[1, 2, 3, 4]);
2357
2358        AsMut::<[u8; 4]>::as_mut(&mut bytes)[2] = 9;
2359        assert_eq!(bytes.as_slice(), &[1, 2, 9, 4]);
2360    }
2361
2362    #[cfg_attr(
2363        tarpaulin,
2364        ignore = "tarpaulin can segfault while tracing mlock/mprotect tests"
2365    )]
2366    #[test]
2367    fn test_mprotect_handles_single_byte_slice() {
2368        let mut vec = HeapBytes::from(&[1u8][..]);
2369
2370        dryoc_mprotect_readonly(vec.as_slice()).expect("readonly mprotect failed");
2371        dryoc_mprotect_readwrite(vec.as_slice()).expect("readwrite mprotect failed");
2372        vec[0] = 2;
2373
2374        assert_eq!(vec[0], 2);
2375    }
2376
2377    #[cfg_attr(
2378        tarpaulin,
2379        ignore = "tarpaulin can segfault while tracing mlock/mprotect tests"
2380    )]
2381    #[test]
2382    fn test_mprotect_handles_exact_page_slice() {
2383        let pagesize = *PAGESIZE;
2384        let mut vec = HeapBytes::default();
2385        vec.resize(pagesize, 1);
2386
2387        dryoc_mprotect_readonly(vec.as_slice()).expect("readonly mprotect failed");
2388        dryoc_mprotect_readwrite(vec.as_slice()).expect("readwrite mprotect failed");
2389        vec[0] = 2;
2390        vec[pagesize - 1] = 3;
2391
2392        assert_eq!(vec[0], 2);
2393        assert_eq!(vec[pagesize - 1], 3);
2394    }
2395
2396    #[cfg(unix)]
2397    #[cfg_attr(
2398        tarpaulin,
2399        ignore = "tarpaulin can segfault while tracing mlock/mprotect tests"
2400    )]
2401    #[test]
2402    fn test_mprotect_noaccess_covers_page_boundary_tail() {
2403        let pagesize = *PAGESIZE;
2404        let mut vec = HeapBytes::default();
2405        vec.resize(pagesize + 1, 0);
2406
2407        dryoc_mprotect_noaccess(vec.as_slice()).expect("noaccess mprotect failed");
2408
2409        let child = unsafe { libc::fork() };
2410        assert!(child >= 0, "fork failed");
2411
2412        if child == 0 {
2413            let tail = unsafe { vec.as_slice().as_ptr().add(pagesize) as *mut u8 };
2414            unsafe {
2415                std::ptr::write_volatile(tail, 1);
2416                libc::_exit(0);
2417            }
2418        }
2419
2420        let mut status = 0;
2421        let wait_ret = unsafe { libc::waitpid(child, &mut status, 0) };
2422        dryoc_mprotect_readwrite(vec.as_slice()).expect("readwrite mprotect failed");
2423
2424        assert_eq!(wait_ret, child);
2425        assert!(
2426            libc::WIFSIGNALED(status),
2427            "child unexpectedly wrote to protected tail page"
2428        );
2429    }
2430
2431    // #[test]
2432    // fn test_crash() {
2433    //     use crate::protected::*;
2434
2435    //     // Create a read-only, locked region of memory
2436    //     let readonly_locked =
2437    // HeapBytes::from_slice_into_readonly_locked(b"some locked bytes")
2438    //         .expect("failed to get locked bytes");
2439
2440    //     // Write to a protected region of memory, causing a crash.
2441    //     unsafe {
2442    //         ptr::write(readonly_locked.as_slice().as_ptr() as *mut u8, 0) //
2443    // <- crash happens here     };
2444    // }
2445}