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!(®ion.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!(®ion.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 ®ion.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!(®ion.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}