Skip to main content

catalejo_sys/
exception.rs

1//! Exception contexts, slabs, actions, and the process fault backend.
2//!
3//! A context owns the kernel exception file descriptor. Each mapped slab is edited while writable
4//! and published by changing the whole mapping to read only. Publication installs an immutable
5//! kernel snapshot. Returning the mapping to writable removes that snapshot before editing resumes.
6
7use core::{
8    num::NonZero,
9    ptr::NonNull,
10    slice,
11    sync::atomic::{AtomicUsize, Ordering},
12};
13use std::{
14    io,
15    os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd},
16};
17
18use fack::prelude::Error;
19
20use crate::ffi::binding;
21
22pub mod action;
23pub mod backend;
24
25/// The reason an exception slab size is invalid.
26#[derive(Debug, Clone, Copy, Error, PartialEq, Eq)]
27pub enum InvalidSlabSize {
28    /// Zero cannot describe a mapping.
29    #[error("exception slab size cannot be zero")]
30    Zero,
31
32    /// The size is not aligned to both a base page and the record stride.
33    #[error("exception slab size is not aligned")]
34    Misaligned,
35
36    /// The size exceeds the kernel ABI ceiling.
37    #[error("exception slab size exceeds the kernel limit")]
38    TooLarge,
39}
40
41/// A validated fixed exception slab size in bytes.
42///
43/// NOTE(invariant): The private value is nonzero, aligned to the x86 Linux base-page size and the
44/// 48-byte record stride, and no larger than the kernel slab-size ceiling.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub struct SlabSize(
47    /// The validated slab size in bytes.
48    NonZero<usize>,
49);
50
51impl SlabSize {
52    /// The built-in backend slab size.
53    pub const DEFAULT: Self = Self(
54        NonZero::new(binding::MIRILLA_EXCEPT_DEFAULT_SLAB_SIZE as usize)
55            .expect("the default slab size is nonzero"),
56    );
57
58    /// Validate a slab size.
59    #[inline]
60    pub const fn new(value: usize) -> Result<Self, InvalidSlabSize> {
61        match NonZero::new(value) {
62            None => Err(InvalidSlabSize::Zero),
63            Some(target_size) => {
64                let size_value = target_size.get();
65                let page_aligned = size_value.is_multiple_of(4096);
66                let record_aligned = size_value
67                    .is_multiple_of(core::mem::size_of::<binding::mirilla_except_record>());
68                let within_limit = size_value <= binding::MIRILLA_EXCEPT_SLAB_SIZE_LIMIT as usize;
69
70                match (page_aligned, record_aligned, within_limit) {
71                    (true, true, true) => Ok(Self(target_size)),
72                    (_, _, false) => Err(InvalidSlabSize::TooLarge),
73                    _ => Err(InvalidSlabSize::Misaligned),
74                }
75            }
76        }
77    }
78
79    /// Return the byte size.
80    #[inline]
81    pub const fn get(self) -> usize {
82        let Self(value) = self;
83
84        value.get()
85    }
86
87    /// Return the number of fixed-stride records in one slab.
88    #[inline]
89    pub const fn record_capacity(self) -> usize {
90        let Self(target_size) = self;
91
92        target_size.get() / core::mem::size_of::<binding::mirilla_except_record>()
93    }
94}
95
96/// The reason a userspace soft slab limit is invalid.
97#[derive(Debug, Clone, Copy, Error, PartialEq, Eq)]
98pub enum InvalidSoftSlabLimit {
99    /// Zero would prohibit every allocation.
100    #[error("exception slab limit cannot be zero")]
101    Zero,
102
103    /// The requested limit exceeds the kernel hard limit.
104    #[error("exception slab limit exceeds the kernel limit")]
105    AboveKernelLimit,
106}
107
108/// A userspace allocation limit bounded by the kernel hard limit.
109///
110/// NOTE(invariant): The private value is in the inclusive range from one through the kernel slab
111/// limit.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
113pub struct SoftSlabLimit(
114    /// The validated userspace allocation count.
115    NonZero<usize>,
116);
117
118impl SoftSlabLimit {
119    /// The default policy permits one slab.
120    pub const DEFAULT: Self = Self(NonZero::<usize>::MIN);
121
122    /// Validate a userspace soft limit.
123    #[inline]
124    pub const fn new(value: usize) -> Result<Self, InvalidSoftSlabLimit> {
125        match NonZero::new(value) {
126            None => Err(InvalidSoftSlabLimit::Zero),
127            Some(value) => match value.get() <= binding::MIRILLA_EXCEPT_SLAB_LIMIT as usize {
128                true => Ok(Self(value)),
129                false => Err(InvalidSoftSlabLimit::AboveKernelLimit),
130            },
131        }
132    }
133
134    /// Return the allocation count.
135    #[inline]
136    pub const fn get(self) -> usize {
137        let Self(value) = self;
138
139        value.get()
140    }
141}
142
143/// A nonzero kernel exception-context identifier.
144///
145/// NOTE(invariant): Only a successful CREATE result can construct this private nonzero value.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
147pub struct ExceptionId(
148    /// The nonzero identifier returned by Mirilla CREATE.
149    NonZero<binding::mirilla_except_id_t>,
150);
151
152impl ExceptionId {
153    /// Lift a successful kernel result.
154    #[inline]
155    const fn from_raw(target_id: binding::mirilla_except_id_t) -> Option<Self> {
156        match NonZero::new(target_id) {
157            Some(target_id) => Some(Self(target_id)),
158            None => None,
159        }
160    }
161
162    /// Return the kernel identifier.
163    #[inline]
164    pub const fn get(self) -> binding::mirilla_except_id_t {
165        let Self(value) = self;
166
167        value.get()
168    }
169}
170
171/// A failure while allocating an exception slab.
172#[derive(Debug, Error)]
173pub enum SlabAllocationError {
174    /// The context already owns its configured number of userspace slabs.
175    #[error("exception slab allocation limit reached")]
176    SoftLimitReached,
177
178    /// The operating system rejected or malformed the slab mapping.
179    #[error("exception slab mapping failed with {0}")]
180    #[error(source(0))]
181    System(
182        /// The underlying mapping error.
183        io::Error,
184    ),
185}
186
187/// An fd-owned exception context bound to its creating address space.
188///
189// NOTE(invariant): The descriptor owns the kernel exception context. The identifier and slab size
190// come from the same successful create operation. The allocation count never exceeds the soft
191// limit through safe Rust allocation paths.
192#[derive(Debug)]
193pub struct Context(
194    /// The anonymous exception file descriptor capability.
195    OwnedFd,
196    /// The kernel identifier for the exception context.
197    ExceptionId,
198    /// The exact byte size required by every slab mapping.
199    SlabSize,
200    /// The userspace admission control limit for live slabs.
201    SoftSlabLimit,
202    /// The number of live slab mappings owned through this context.
203    AtomicUsize,
204);
205
206impl Context {
207    /// Create the one exception context allowed for the current address space.
208    ///
209    /// # Safety
210    ///
211    /// `device` must be a descriptor created by Mirilla.
212    ///
213    /// # Errors
214    ///
215    /// This returns a kernel error or an invalid successful result from the foreign interface.
216    #[inline]
217    pub unsafe fn create(
218        device: BorrowedFd<'_>,
219        slab_size: SlabSize,
220        soft_limit: SoftSlabLimit,
221    ) -> io::Result<Self> {
222        let mut target_id = 0 as binding::mirilla_except_id_t;
223        let mut target_fd = -1 as RawFd;
224
225        // SAFETY: The caller supplies the Mirilla descriptor contract. Both output pointers name
226        // live local storage for the duration of the foreign call.
227        let target_status = unsafe {
228            binding::catalejo_mirilla_except_create(
229                device.as_raw_fd(),
230                slab_size.get() as binding::virtual_size_t,
231                &raw mut target_id,
232                &raw mut target_fd,
233            )
234        };
235
236        status(target_status)?;
237
238        let target_id = ExceptionId::from_raw(target_id);
239        let target_fd = match target_fd {
240            0.. => {
241                // SAFETY: A successful create transfers ownership of one nonnegative descriptor.
242                Some(unsafe { OwnedFd::from_raw_fd(target_fd) })
243            }
244            _ => None,
245        };
246
247        match (target_id, target_fd) {
248            (Some(target_id), Some(target_fd)) => {
249                let allocated_count = AtomicUsize::new(0);
250
251                Ok(Self(
252                    target_fd,
253                    target_id,
254                    slab_size,
255                    soft_limit,
256                    allocated_count,
257                ))
258            }
259            (_, Some(target_fd)) => {
260                drop(target_fd);
261
262                Err(io::Error::from(io::ErrorKind::InvalidData))
263            }
264            _ => Err(io::Error::from(io::ErrorKind::InvalidData)),
265        }
266    }
267
268    /// Return the kernel identifier.
269    #[inline]
270    pub const fn id(&self) -> ExceptionId {
271        let &Self(_, target_id, ..) = self;
272
273        target_id
274    }
275
276    /// Return the configured slab size.
277    #[inline]
278    pub const fn slab_size(&self) -> SlabSize {
279        let &Self(_, _, slab_size, ..) = self;
280
281        slab_size
282    }
283
284    /// Return the configured userspace slab limit.
285    #[inline]
286    pub const fn soft_limit(&self) -> SoftSlabLimit {
287        let &Self(_, _, _, soft_limit, ..) = self;
288
289        soft_limit
290    }
291
292    /// Return the number of currently mapped slabs.
293    #[inline]
294    pub fn allocated(&self) -> usize {
295        let Self(_, _, _, _, allocated_count) = self;
296
297        allocated_count.load(Ordering::Acquire)
298    }
299
300    /// Map one editable exception slab.
301    ///
302    /// # Errors
303    ///
304    /// This fails when the userspace soft limit is reached or the operating system rejects the
305    /// mapping.
306    #[inline]
307    pub fn map(&self) -> Result<Slab<'_>, SlabAllocationError> {
308        Self::reserve_slab(self)?;
309
310        let Self(target_fd, _, slab_size, ..) = self;
311        let mut record_list = core::ptr::null_mut();
312
313        // SAFETY: The context descriptor is a live exception descriptor. The output pointer names
314        // local storage and the C helper maps exactly one configured slab on success.
315        let target_status = unsafe {
316            binding::catalejo_except_slab_map(
317                target_fd.as_raw_fd(),
318                slab_size.get() as binding::virtual_size_t,
319                &raw mut record_list,
320            )
321        };
322
323        let map_result = status(target_status)
324            .map_err(SlabAllocationError::System)
325            .and_then(|()| {
326                NonNull::new(record_list).ok_or_else(|| {
327                    SlabAllocationError::System(io::Error::from(io::ErrorKind::InvalidData))
328                })
329            });
330
331        match map_result {
332            Ok(record_list) => Ok(Slab(self, record_list, SlabState::Editable)),
333            Err(target_error) => {
334                Self::release_slab(self);
335
336                Err(target_error)
337            }
338        }
339    }
340
341    /// Reserve one userspace slab allocation slot.
342    fn reserve_slab(&self) -> Result<(), SlabAllocationError> {
343        let Self(_, _, _, soft_limit, allocated_count) = self;
344        let limit_count = soft_limit.get();
345        let update_result =
346            allocated_count.fetch_update(Ordering::AcqRel, Ordering::Acquire, |allocated_count| {
347                match allocated_count < limit_count {
348                    true => Some(allocated_count + 1),
349                    false => None,
350                }
351            });
352
353        match update_result {
354            Ok(_) => Ok(()),
355            Err(_) => Err(SlabAllocationError::SoftLimitReached),
356        }
357    }
358
359    /// Return one userspace slab allocation slot.
360    fn release_slab(&self) {
361        let Self(_, _, _, _, allocated_count) = self;
362        let previous_count = allocated_count.fetch_sub(1, Ordering::AcqRel);
363
364        debug_assert!(previous_count != 0);
365    }
366}
367
368/// The userspace protection state tracked for one slab.
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370enum SlabState {
371    /// The slab is writable and contributes no immutable kernel table.
372    Editable,
373    /// The slab is read only and contributes its immutable kernel snapshot.
374    Published,
375}
376
377/// One complete exception slab mapping owned by a [`Context`].
378///
379// NOTE(invariant): The pointer names one exact mapping from the borrowed context. The tracked state
380// changes only after successful whole-VMA protection transitions. Drop unmaps the mapping before
381// returning the context allocation slot.
382#[derive(Debug)]
383pub struct Slab<'context>(
384    /// The context that owns the file descriptor used to create this slab.
385    &'context Context,
386    /// The first exception record in the complete slab mapping.
387    NonNull<binding::mirilla_except_record>,
388    /// The last protection state established through this safe wrapper.
389    SlabState,
390);
391
392impl Slab<'_> {
393    /// Return whether the slab currently contributes a published kernel snapshot.
394    #[inline]
395    pub const fn is_published(&self) -> bool {
396        let Self(_, _, slab_state) = self;
397
398        matches!(slab_state, SlabState::Published)
399    }
400
401    /// Return read-only access to the complete record list.
402    #[inline]
403    pub const fn record_list(&self) -> &[binding::mirilla_except_record] {
404        let Self(target_context, record_list, _) = self;
405        let record_count = target_context.slab_size().record_capacity();
406
407        // SAFETY: The slab invariant owns the complete live mapping for this lifetime. The mapping
408        // always contains exactly record_count fixed-size records.
409        unsafe { slice::from_raw_parts(record_list.as_ptr(), record_count) }
410    }
411
412    /// Return mutable access to the complete record list while the slab is editable.
413    #[inline]
414    pub const fn record_list_mut(&mut self) -> Option<&mut [binding::mirilla_except_record]> {
415        let Self(target_context, record_list, slab_state) = self;
416        let record_count = target_context.slab_size().record_capacity();
417
418        match slab_state {
419            SlabState::Editable => {
420                // SAFETY: Editable state proves the VMA is writable. Exclusive access to the slab
421                // prevents a second Rust reference to the returned record list.
422                Some(unsafe { slice::from_raw_parts_mut(record_list.as_ptr(), record_count) })
423            }
424            SlabState::Published => None,
425        }
426    }
427
428    /// Publish the complete record list as an immutable kernel snapshot.
429    ///
430    /// A successful call leaves the slab read only. A failed call leaves the slab editable and
431    /// owned by the caller.
432    ///
433    /// # Errors
434    ///
435    /// This returns the operating system or kernel validation error from the protection change.
436    #[inline]
437    pub fn publish(&mut self) -> io::Result<()> {
438        let Self(target_context, record_list, slab_state) = self;
439
440        match slab_state {
441            SlabState::Published => Ok(()),
442            SlabState::Editable => {
443                // SAFETY: The slab owns this exact complete mapping. No mutable record borrow can
444                // coexist with this exclusive slab borrow.
445                let target_status = unsafe {
446                    binding::catalejo_except_slab_publish(
447                        record_list.as_ptr(),
448                        target_context.slab_size().get() as binding::virtual_size_t,
449                    )
450                };
451
452                status(target_status)?;
453                *slab_state = SlabState::Published;
454
455                Ok(())
456            }
457        }
458    }
459
460    /// Remove the active kernel snapshot and return the slab to editable memory.
461    ///
462    /// A successful call leaves the slab writable. A failed call preserves the published state.
463    ///
464    /// # Errors
465    ///
466    /// This returns the operating system error from the protection change.
467    #[inline]
468    pub fn edit(&mut self) -> io::Result<()> {
469        let Self(target_context, record_list, slab_state) = self;
470
471        match slab_state {
472            SlabState::Editable => Ok(()),
473            SlabState::Published => {
474                // SAFETY: The slab owns this exact complete mapping and the kernel accepts only the
475                // supported whole-VMA read-only to read-write transition.
476                let target_status = unsafe {
477                    binding::catalejo_except_slab_edit(
478                        record_list.as_ptr(),
479                        target_context.slab_size().get() as binding::virtual_size_t,
480                    )
481                };
482
483                status(target_status)?;
484                *slab_state = SlabState::Editable;
485
486                Ok(())
487            }
488        }
489    }
490}
491
492impl Drop for Slab<'_> {
493    #[inline]
494    fn drop(&mut self) {
495        let &mut Self(target_context, record_list, _) = self;
496
497        // SAFETY: The slab invariant owns this exact complete mapping and Drop is its final Rust
498        // owner. Unmapping also detaches any published kernel snapshot.
499        let unmap_status = unsafe {
500            binding::catalejo_except_slab_unmap(
501                record_list.as_ptr(),
502                target_context.slab_size().get() as binding::virtual_size_t,
503            )
504        };
505
506        // NOTE(invariant): A failed unmap leaves the VMA and any publication active. Keep its soft
507        // allocation slot charged because Rust can no longer prove that the kernel slab vanished.
508        if unmap_status == 0 {
509            Context::release_slab(target_context);
510        }
511    }
512}
513
514/// Convert a C negative-errno status into an I/O result.
515fn status(target_status: core::ffi::c_int) -> io::Result<()> {
516    match target_status {
517        0 => Ok(()),
518        ..=-1 => Err(io::Error::from_raw_os_error(target_status.saturating_abs())),
519        _ => Err(io::Error::from(io::ErrorKind::InvalidData)),
520    }
521}
522
523const _: () = {
524    assert!(core::mem::size_of::<binding::mirilla_except_boundary>() == 16);
525    assert!(core::mem::size_of::<binding::mirilla_except_predicate>() == 16);
526    assert!(core::mem::size_of::<binding::mirilla_except_action>() == 16);
527    assert!(core::mem::size_of::<binding::mirilla_except_record>() == 48);
528    assert!(core::mem::align_of::<binding::mirilla_except_record>() == 16);
529};