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