Skip to main content

pager_lang/
region.rs

1//! The owned virtual-memory region and the host page-size query.
2
3use core::ptr::NonNull;
4use core::sync::atomic::{AtomicUsize, Ordering};
5
6use crate::error::PagerError;
7use crate::protection::Protection;
8use crate::sys;
9
10/// The size of a memory page on the host, in bytes.
11///
12/// Every region is a whole number of these, and protection changes apply at page
13/// granularity, so this is the unit to reason in when sizing regions. The value is queried
14/// from the operating system once and cached, so repeated calls are a single relaxed atomic
15/// load. It is always a power of two — typically 4096, though some systems use 16384.
16///
17/// # Examples
18///
19/// ```
20/// let page = pager_lang::page_size();
21/// assert!(page.is_power_of_two());
22/// assert!(page >= 4096);
23/// ```
24#[must_use]
25pub fn page_size() -> usize {
26    static CACHE: AtomicUsize = AtomicUsize::new(0);
27
28    let cached = CACHE.load(Ordering::Relaxed);
29    if cached != 0 {
30        return cached;
31    }
32    // Querying twice from two threads is harmless: both observe the same value and store it.
33    let queried = sys::page_size();
34    CACHE.store(queried, Ordering::Relaxed);
35    queried
36}
37
38/// Rounds `len` up to a whole number of pages, rejecting zero and reporting overflow.
39fn pages_for(len: usize) -> Result<usize, PagerError> {
40    if len == 0 {
41        return Err(PagerError::ZeroSize);
42    }
43    let page = page_size();
44    // `page` is a power of two, so masking off the low bits rounds down; adding `page - 1`
45    // first rounds up. The add is the only step that can overflow.
46    let bumped = len.checked_add(page - 1).ok_or(PagerError::SizeOverflow)?;
47    Ok(bumped & !(page - 1))
48}
49
50/// An owned region of page-aligned virtual memory, freed when it is dropped.
51///
52/// A region is the unit a JIT or runtime works in: a block of memory whose access
53/// permissions can be changed independently of the rest of the address space. It is created
54/// [`ReadWrite`](Protection::ReadWrite) so code or data can be written in, then moved to
55/// another protection — usually [`ReadExecute`](Protection::ReadExecute) — with
56/// [`protect`](Self::protect). Dropping the region returns its pages to the operating system.
57///
58/// The requested length is rounded up to a whole number of [`page_size`] bytes, so
59/// [`len`](Self::len) is the real, page-aligned size and is at least what was asked for.
60/// Optionally a region can be flanked by inaccessible *guard pages* with
61/// [`with_guard_pages`](Self::with_guard_pages), so a run off either end faults at once
62/// instead of silently corrupting a neighbour.
63///
64/// `Region` is [`Send`] and [`Sync`]: it owns its mapping the way `Box<[u8]>` owns its
65/// allocation, and every method that touches the bytes takes `&self` or `&mut self`, so the
66/// borrow checker rules out data races on the safe surface.
67///
68/// # Executing code
69///
70/// Writing machine code into a region and running it is inherently unsafe — the bytes must
71/// be valid code for the target and match the calling convention you transmute to. The safe
72/// surface here gets the memory into the right state; the final transmute-and-call is the
73/// caller's `unsafe` responsibility. On architectures with separate instruction and data
74/// caches (for example AArch64), freshly written code also needs the instruction cache
75/// synchronized before it is run; on x86 and x86-64 this is automatic.
76///
77/// # Examples
78///
79/// Write a byte pattern, read it back, then flip the region to executable (the W^X pattern,
80/// minus the architecture-specific call):
81///
82/// ```
83/// use pager_lang::{Protection, Region};
84///
85/// let mut region = Region::new(64)?;
86/// assert!(region.len() >= 64);
87/// assert_eq!(region.protection(), Protection::ReadWrite);
88///
89/// // Fill the first bytes and read them straight back.
90/// region.write(0, &[0x90, 0x90, 0xC3])?;
91/// assert_eq!(&region.as_slice().unwrap()[..3], &[0x90, 0x90, 0xC3]);
92///
93/// // Make it read+execute: it is no longer writable.
94/// region.protect(Protection::ReadExecute)?;
95/// assert!(region.as_mut_slice().is_none());
96/// # Ok::<(), pager_lang::PagerError>(())
97/// ```
98pub struct Region {
99    /// Base of the whole mapping. Equal to `data` for a plain region; one page lower (the
100    /// first guard page) for a guarded one. This is what `Drop` frees.
101    base: NonNull<u8>,
102    /// Start of the usable bytes the public methods expose.
103    data: NonNull<u8>,
104    /// Length of the usable region, in bytes — a whole number of pages.
105    len: usize,
106    /// Length of the whole mapping including any guard pages, in bytes.
107    total: usize,
108    /// Current protection of the usable region.
109    prot: Protection,
110    /// Whether guard pages flank the usable region.
111    guarded: bool,
112}
113
114// SAFETY: a `Region` owns a unique virtual-memory mapping and frees it exactly once on drop.
115// The raw pointers it holds are an owned resource, not aliases shared with anything else, so
116// moving a `Region` to another thread simply moves ownership of that mapping — the same as
117// `Box<[u8]>`. Every method that reads or writes the bytes takes `&self` or `&mut self`, so
118// Rust's borrow rules prevent a shared `&Region` from being used to mutate, leaving only
119// race-free shared reads.
120unsafe impl Send for Region {}
121// SAFETY: see the `Send` impl above. Sharing `&Region` across threads exposes only the
122// read-only and pointer-returning methods; mutation requires `&mut Region`, which cannot be
123// aliased, so concurrent access through shared references is data-race free.
124unsafe impl Sync for Region {}
125
126impl Region {
127    /// Maps a new read/write region of at least `len` bytes.
128    ///
129    /// The length is rounded up to a whole number of [`page_size`] bytes; the region starts
130    /// out [`ReadWrite`](Protection::ReadWrite). The bytes are zero-initialized by the
131    /// operating system.
132    ///
133    /// # Errors
134    ///
135    /// - [`PagerError::ZeroSize`] if `len` is zero.
136    /// - [`PagerError::SizeOverflow`] if rounding `len` up to whole pages overflows `usize`.
137    /// - [`PagerError::Map`] if the operating system cannot map the region.
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// use pager_lang::Region;
143    ///
144    /// let region = Region::new(1)?;
145    /// // One byte was asked for; a whole page is what you get.
146    /// assert_eq!(region.len(), pager_lang::page_size());
147    /// # Ok::<(), pager_lang::PagerError>(())
148    /// ```
149    pub fn new(len: usize) -> Result<Self, PagerError> {
150        let data_len = pages_for(len)?;
151        let base = sys::map(data_len).map_err(PagerError::Map)?;
152        Ok(Region {
153            base,
154            data: base,
155            len: data_len,
156            total: data_len,
157            prot: Protection::ReadWrite,
158            guarded: false,
159        })
160    }
161
162    /// Maps a new read/write region of at least `len` bytes, flanked on both sides by an
163    /// inaccessible guard page.
164    ///
165    /// The usable region behaves exactly like one from [`new`](Self::new): rounded up to
166    /// whole pages and starting [`ReadWrite`](Protection::ReadWrite). The difference is the
167    /// guard page immediately before and after it — both [`None`](Protection::None) — so a
168    /// read or write that runs off either end faults immediately rather than corrupting
169    /// whatever happened to be mapped next. [`has_guard_pages`](Self::has_guard_pages)
170    /// reports `true`. [`len`](Self::len), [`as_ptr`](Self::as_ptr), and the slice accessors
171    /// all describe the usable region only; the guards are never exposed.
172    ///
173    /// # Errors
174    ///
175    /// - [`PagerError::ZeroSize`] if `len` is zero.
176    /// - [`PagerError::SizeOverflow`] if the total size including guard pages overflows `usize`.
177    /// - [`PagerError::Map`] if the operating system cannot map the region.
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use pager_lang::Region;
183    ///
184    /// let region = Region::with_guard_pages(128)?;
185    /// assert!(region.has_guard_pages());
186    /// assert!(region.len() >= 128);
187    /// # Ok::<(), pager_lang::PagerError>(())
188    /// ```
189    pub fn with_guard_pages(len: usize) -> Result<Self, PagerError> {
190        let page = page_size();
191        let data_len = pages_for(len)?;
192        let two_guards = page.checked_mul(2).ok_or(PagerError::SizeOverflow)?;
193        let total = data_len
194            .checked_add(two_guards)
195            .ok_or(PagerError::SizeOverflow)?;
196        let base = sys::map_guarded(total, page, data_len).map_err(PagerError::Map)?;
197        // SAFETY: the usable region starts `page` bytes into a mapping of `total` bytes, and
198        // `page < total`, so the offset pointer stays inside the same mapping and is non-null.
199        let data = unsafe { NonNull::new_unchecked(base.as_ptr().add(page)) };
200        Ok(Region {
201            base,
202            data,
203            len: data_len,
204            total,
205            prot: Protection::ReadWrite,
206            guarded: true,
207        })
208    }
209
210    /// The length of the usable region in bytes — a whole number of pages, at least the size
211    /// requested.
212    #[must_use]
213    pub fn len(&self) -> usize {
214        self.len
215    }
216
217    /// Whether the region is empty. Always `false`: a region spans at least one page. Present
218    /// so the type reads naturally alongside [`len`](Self::len).
219    #[must_use]
220    pub fn is_empty(&self) -> bool {
221        self.len == 0
222    }
223
224    /// The region's current protection.
225    #[must_use]
226    pub fn protection(&self) -> Protection {
227        self.prot
228    }
229
230    /// Whether the region is flanked by guard pages (created with
231    /// [`with_guard_pages`](Self::with_guard_pages)).
232    #[must_use]
233    pub fn has_guard_pages(&self) -> bool {
234        self.guarded
235    }
236
237    /// A raw const pointer to the start of the usable region.
238    ///
239    /// Always valid to obtain; dereferencing or transmuting it (for example to a function
240    /// pointer to run code in the region) is `unsafe` and the caller's responsibility.
241    #[must_use]
242    pub fn as_ptr(&self) -> *const u8 {
243        self.data.as_ptr()
244    }
245
246    /// A raw mutable pointer to the start of the usable region.
247    ///
248    /// Always valid to obtain; dereferencing it is `unsafe` and only sound while the region's
249    /// protection permits the access.
250    #[must_use]
251    pub fn as_mut_ptr(&mut self) -> *mut u8 {
252        self.data.as_ptr()
253    }
254
255    /// Borrows the region as a byte slice, or `None` if it is not currently readable.
256    ///
257    /// Returns `Some` exactly when [`protection`](Self::protection) is readable (everything
258    /// but [`Protection::None`]). The `None` case is what keeps this safe: a region flipped
259    /// to a no-access protection cannot be read through a slice.
260    ///
261    /// # Examples
262    ///
263    /// ```
264    /// use pager_lang::{Protection, Region};
265    ///
266    /// let mut region = Region::new(32)?;
267    /// assert!(region.as_slice().is_some());
268    ///
269    /// region.protect(Protection::None)?;
270    /// assert!(region.as_slice().is_none());
271    /// # Ok::<(), pager_lang::PagerError>(())
272    /// ```
273    #[must_use]
274    pub fn as_slice(&self) -> Option<&[u8]> {
275        if !self.prot.is_readable() {
276            return None;
277        }
278        // SAFETY: `data` addresses `len` bytes of a live, readable mapping; `len` is a whole
279        // number of pages and so well under `isize::MAX`. The shared borrow of `self` bounds
280        // the slice's lifetime, and a readable protection cannot change without `&mut self`.
281        Some(unsafe { core::slice::from_raw_parts(self.data.as_ptr(), self.len) })
282    }
283
284    /// Borrows the region as a mutable byte slice, or `None` if it is not currently writable.
285    ///
286    /// Returns `Some` exactly when [`protection`](Self::protection) is writable
287    /// ([`ReadWrite`](Protection::ReadWrite) or
288    /// [`ReadWriteExecute`](Protection::ReadWriteExecute)). Use this to fill a region in bulk;
289    /// for a single placed write, [`write`](Self::write) does the bounds checking for you.
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// use pager_lang::{Protection, Region};
295    ///
296    /// let mut region = Region::new(8)?;
297    /// region.as_mut_slice().unwrap().fill(0xAB);
298    /// assert!(region.as_slice().unwrap().iter().all(|&b| b == 0xAB));
299    ///
300    /// region.protect(Protection::ReadExecute)?;
301    /// assert!(region.as_mut_slice().is_none());
302    /// # Ok::<(), pager_lang::PagerError>(())
303    /// ```
304    #[must_use]
305    pub fn as_mut_slice(&mut self) -> Option<&mut [u8]> {
306        if !self.prot.is_writable() {
307            return None;
308        }
309        // SAFETY: `data` addresses `len` writable bytes of a live mapping; `len` is well
310        // under `isize::MAX`. The exclusive borrow of `self` guarantees no other reference to
311        // these bytes exists for the slice's lifetime.
312        Some(unsafe { core::slice::from_raw_parts_mut(self.data.as_ptr(), self.len) })
313    }
314
315    /// Copies `bytes` into the region starting at `offset`.
316    ///
317    /// The common way to load machine code or data: it checks that the region is writable and
318    /// that the write fits, then copies. Writing an empty slice is a no-op.
319    ///
320    /// # Errors
321    ///
322    /// - [`PagerError::NotWritable`] if the region's protection does not permit writing.
323    /// - [`PagerError::OutOfBounds`] if `offset + bytes.len()` exceeds [`len`](Self::len).
324    ///
325    /// # Examples
326    ///
327    /// ```
328    /// use pager_lang::{PagerError, Region};
329    ///
330    /// let mut region = Region::new(16)?;
331    /// region.write(4, &[1, 2, 3, 4])?;
332    /// assert_eq!(&region.as_slice().unwrap()[4..8], &[1, 2, 3, 4]);
333    ///
334    /// // A write past the end is rejected, not truncated.
335    /// let len = region.len();
336    /// assert!(matches!(
337    ///     region.write(len, &[0]),
338    ///     Err(PagerError::OutOfBounds { .. })
339    /// ));
340    /// # Ok::<(), pager_lang::PagerError>(())
341    /// ```
342    pub fn write(&mut self, offset: usize, bytes: &[u8]) -> Result<(), PagerError> {
343        if !self.prot.is_writable() {
344            return Err(PagerError::NotWritable);
345        }
346        let out_of_bounds = || PagerError::OutOfBounds {
347            offset,
348            len: bytes.len(),
349            region_len: self.len,
350        };
351        let end = offset.checked_add(bytes.len()).ok_or_else(out_of_bounds)?;
352        if end > self.len {
353            return Err(out_of_bounds());
354        }
355        if bytes.is_empty() {
356            return Ok(());
357        }
358        // SAFETY: the destination range `[offset, offset + bytes.len())` lies within the
359        // writable region (bounds checked just above). The source is the caller's slice and
360        // the destination is our mapping, so the two never overlap; both are valid for the
361        // length of the copy.
362        unsafe {
363            core::ptr::copy_nonoverlapping(
364                bytes.as_ptr(),
365                self.data.as_ptr().add(offset),
366                bytes.len(),
367            );
368        }
369        Ok(())
370    }
371
372    /// Changes the region's protection.
373    ///
374    /// Applies to the usable region only; any guard pages are left untouched. Setting the
375    /// protection it already has is a no-op that returns `Ok`. This is the W^X flip: write
376    /// code under [`ReadWrite`](Protection::ReadWrite), then `protect` to
377    /// [`ReadExecute`](Protection::ReadExecute) before running it.
378    ///
379    /// # Errors
380    ///
381    /// [`PagerError::Protect`] if the operating system refuses the change — most often a
382    /// platform that forbids writable-and-executable pages rejecting
383    /// [`Protection::ReadWriteExecute`].
384    ///
385    /// # Examples
386    ///
387    /// ```
388    /// use pager_lang::{Protection, Region};
389    ///
390    /// let mut region = Region::new(64)?;
391    /// region.protect(Protection::ReadExecute)?;
392    /// assert_eq!(region.protection(), Protection::ReadExecute);
393    /// # Ok::<(), pager_lang::PagerError>(())
394    /// ```
395    pub fn protect(&mut self, protection: Protection) -> Result<(), PagerError> {
396        if protection == self.prot {
397            return Ok(());
398        }
399        // SAFETY: `data`/`len` name the committed usable region of a live mapping this
400        // `Region` owns, with `len` a whole number of pages — exactly what `sys::protect`
401        // requires.
402        unsafe { sys::protect(self.data.as_ptr(), self.len, protection) }
403            .map_err(PagerError::Protect)?;
404        self.prot = protection;
405        Ok(())
406    }
407}
408
409impl core::fmt::Debug for Region {
410    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
411        f.debug_struct("Region")
412            .field("addr", &self.data.as_ptr())
413            .field("len", &self.len)
414            .field("protection", &self.prot)
415            .field("guard_pages", &self.guarded)
416            .finish()
417    }
418}
419
420impl Drop for Region {
421    fn drop(&mut self) {
422        // SAFETY: `base`/`total` are exactly the base and length of the mapping this `Region`
423        // created and uniquely owns; `Drop` runs once, so the mapping has not been freed.
424        // The result is deliberately discarded: `Drop` cannot return an error and there is no
425        // meaningful recovery from a failed unmap.
426        let _ = unsafe { sys::unmap(self.base.as_ptr(), self.total) };
427    }
428}
429
430#[cfg(test)]
431#[allow(
432    clippy::unwrap_used,
433    clippy::expect_used,
434    clippy::panic,
435    reason = "tests assert on specific outcomes; a wrong outcome should fail the test loudly"
436)]
437mod tests {
438    use super::{Region, page_size};
439    use crate::{PagerError, Protection};
440
441    #[test]
442    fn test_page_size_is_a_sane_power_of_two() {
443        let page = page_size();
444        assert!(page.is_power_of_two());
445        assert!(page >= 4096);
446        // Cached: a second call returns the same value.
447        assert_eq!(page, page_size());
448    }
449
450    #[test]
451    fn test_new_rounds_up_to_a_whole_page() {
452        let region = Region::new(1).unwrap();
453        assert_eq!(region.len(), page_size());
454        assert!(!region.is_empty());
455        assert_eq!(region.protection(), Protection::ReadWrite);
456        assert!(!region.has_guard_pages());
457    }
458
459    #[test]
460    fn test_new_zero_is_rejected() {
461        assert!(matches!(Region::new(0), Err(PagerError::ZeroSize)));
462    }
463
464    #[test]
465    fn test_new_overflow_is_rejected() {
466        assert!(matches!(
467            Region::new(usize::MAX),
468            Err(PagerError::SizeOverflow)
469        ));
470    }
471
472    #[test]
473    fn test_write_then_read_round_trips() {
474        let mut region = Region::new(64).unwrap();
475        region.write(8, &[0xDE, 0xAD, 0xBE, 0xEF]).unwrap();
476        assert_eq!(
477            &region.as_slice().unwrap()[8..12],
478            &[0xDE, 0xAD, 0xBE, 0xEF]
479        );
480    }
481
482    #[test]
483    fn test_write_out_of_bounds_is_rejected() {
484        let mut region = Region::new(16).unwrap();
485        let len = region.len();
486        assert!(matches!(
487            region.write(len, &[1]),
488            Err(PagerError::OutOfBounds { .. })
489        ));
490        // An offset that overflows when the length is added is also rejected, not wrapped.
491        assert!(matches!(
492            region.write(usize::MAX, &[1, 2]),
493            Err(PagerError::OutOfBounds { .. })
494        ));
495    }
496
497    #[test]
498    fn test_write_empty_slice_is_ok_even_at_the_end() {
499        let mut region = Region::new(16).unwrap();
500        let len = region.len();
501        assert_eq!(region.write(len, &[]), Ok(()));
502    }
503
504    #[test]
505    fn test_protect_changes_access_and_gates_the_slices() {
506        let mut region = Region::new(32).unwrap();
507        assert!(region.as_mut_slice().is_some());
508
509        region.protect(Protection::ReadExecute).unwrap();
510        assert_eq!(region.protection(), Protection::ReadExecute);
511        assert!(region.as_mut_slice().is_none());
512        assert!(region.as_slice().is_some());
513        assert_eq!(region.write(0, &[1]), Err(PagerError::NotWritable));
514
515        region.protect(Protection::None).unwrap();
516        assert!(region.as_slice().is_none());
517    }
518
519    #[test]
520    fn test_protect_to_same_protection_is_a_noop() {
521        let mut region = Region::new(8).unwrap();
522        assert_eq!(region.protect(Protection::ReadWrite), Ok(()));
523        assert_eq!(region.protection(), Protection::ReadWrite);
524    }
525
526    #[test]
527    fn test_guarded_region_is_usable_like_any_other() {
528        let mut region = Region::with_guard_pages(100).unwrap();
529        assert!(region.has_guard_pages());
530        assert!(region.len() >= 100);
531        region.write(0, &[7; 16]).unwrap();
532        assert_eq!(&region.as_slice().unwrap()[..16], &[7; 16]);
533    }
534
535    #[test]
536    fn test_many_regions_coexist() {
537        let regions: Vec<Region> = (1..=32).map(|n| Region::new(n * 64).unwrap()).collect();
538        for (i, region) in regions.iter().enumerate() {
539            assert!(region.len() >= (i + 1) * 64);
540        }
541        // All freed here without crashing.
542    }
543}