Skip to main content

catalejo_sys/
ffi.rs

1//! Foreign Function Interface module for the `catalejo-sys` crate.
2
3pub mod binding {
4    #![allow(
5        nonstandard_style,
6        missing_docs,
7        unsafe_op_in_unsafe_fn,
8        improper_ctypes,
9        clippy::missing_safety_doc,
10        reason = "bindgen-generated bindings have non-standard style and generated unsafe helper methods"
11    )]
12    //! Bare automatically-generated bindings to the C-based subsystem.
13
14    // NOTE: Include the `bindgen`-generated bindings for our own crate.
15    include!(concat!(env!("OUT_DIR"), "/catalejo-binding.rs"));
16}
17
18pub mod command {
19    //! Commands for userspace-kernel device ioctls.
20    #![allow(
21        clippy::std_instead_of_core,
22        reason = "imports are false-flagged by clippy where the fix would be nightly-only"
23    )]
24
25    #[cfg(feature = "default-device-path")]
26    use core::ffi::CStr;
27
28    use std::{
29        io,
30        io::ErrorKind,
31        os::fd::{AsRawFd, BorrowedFd, OwnedFd, RawFd},
32        path::Path,
33    };
34
35    #[cfg(feature = "default-device-path")]
36    use std::{path::PathBuf, sync::LazyLock};
37
38    use core::ptr;
39
40    use crate::{
41        ffi::binding::{self, mirilla_map_peephole_initialize_word_t, virtual_address_t},
42        id::{PeepholeId, TargetId},
43    };
44
45    /// The canonical name of the device exposed by the kernel module.
46    ///
47    /// This is used for identifying and interfacing with the appropriate character device.
48    #[cfg(feature = "default-device-path")]
49    pub const MIRILLA_DEVICE_NAME: &str = const {
50        // SAFETY: The `CStr` is obtained from a `bindgen`-generated C string literal, so it always properly nul-delimited.
51        let target_value =
52            unsafe { CStr::from_bytes_with_nul_unchecked(binding::MIRILLA_DEVICE_DEFAULT_NAME) };
53
54        match target_value.to_str() {
55            Ok(target_value) => target_value,
56            Err(..) => unreachable!(),
57        }
58    };
59
60    /// Determine the optionally configured default path to the kernel character device.
61    ///
62    /// A build without the `default-device-path` feature returns [`None`]. Callers must then
63    /// provide a path explicitly or use an already-open device file descriptor.
64    #[inline]
65    #[must_use]
66    pub fn default_device_path() -> Option<&'static Path> {
67        #[cfg(feature = "default-device-path")]
68        {
69            static DEFAULT_DEVICE_PATH: LazyLock<PathBuf> =
70                LazyLock::new(|| PathBuf::from("/dev/").join(self::MIRILLA_DEVICE_NAME));
71
72            Some(DEFAULT_DEVICE_PATH.as_path())
73        }
74
75        #[cfg(not(feature = "default-device-path"))]
76        {
77            None
78        }
79    }
80
81    /// Engage with the target process.
82    ///
83    /// # Failure
84    ///
85    /// This can fail if the:
86    ///
87    /// * Process does not exist.
88    /// * The calling process does not have the required privileges to engage.
89    ///
90    /// # Safety
91    ///
92    /// For soundness purposes, the following must be satisfied:
93    ///
94    /// * The provided file descriptor must be a valid `mirilla`-created one.
95    #[inline]
96    pub unsafe fn engage(fd: BorrowedFd, process_id: binding::pid_t) -> io::Result<TargetId> {
97        let mut target_engagement = None::<TargetId>;
98
99        let target_outcome =
100            // SAFETY:
101            //
102            // * The caller has asserted that the provided file descriptor comes from `mirilla`.
103            // * `mirilla_map_target_id_t` is identical ABI-wise to `Option<TargetId>`.
104            unsafe { binding::catalejo_mirilla_engage(fd.as_raw_fd(), process_id, ptr::from_mut(&mut target_engagement).cast::<binding::mirilla_map_target_id_t>()) };
105
106        match (target_outcome, target_engagement) {
107            (binding::MIRILLA_COMMAND_OK, Some(target_id)) => Ok(target_id),
108            (binding::MIRILLA_COMMAND_OK, ..) => Err(io::Error::from(ErrorKind::InvalidInput)),
109            (
110                target_errno @ binding::mirilla_command_status_t::MIN..binding::MIRILLA_COMMAND_OK,
111                ..,
112            ) => Err(io::Error::from_raw_os_error(target_errno.abs())),
113            // NOTE: This is impossible, hence unreachable.
114            #[cfg(not(feature = "stealth-mode"))]
115            _ => unreachable!(),
116
117            #[cfg(feature = "stealth-mode")]
118            _ => std::process::abort(),
119        }
120    }
121
122    /// Disengage from the target process.
123    ///
124    /// # Failure
125    ///
126    /// This can fail if the:
127    ///
128    /// * Target was not previously engaged.
129    ///
130    /// # Safety
131    ///
132    /// For soundness purposes, the following must be satisfied:
133    ///
134    /// * The provided file descriptor must be a valid `mirilla`-created one.
135    #[inline]
136    pub unsafe fn disengage(fd: OwnedFd, target_id: TargetId) -> io::Result<()> {
137        let target_outcome =
138            // SAFETY: The caller has asserted that the provided file descriptor comes from `mirilla`.
139            unsafe { binding::catalejo_mirilla_disengage(fd.as_raw_fd(), target_id.get()) };
140
141        match target_outcome {
142            binding::MIRILLA_COMMAND_OK => Ok(()),
143            target_errno @ binding::mirilla_command_status_t::MIN..binding::MIRILLA_COMMAND_OK => {
144                Err(io::Error::from_raw_os_error(target_errno.abs()))
145            }
146            // NOTE: This is impossible, hence unreachable.
147            #[cfg(not(feature = "stealth-mode"))]
148            _ => unreachable!(),
149
150            #[cfg(feature = "stealth-mode")]
151            _ => std::process::abort(),
152        }
153    }
154
155    /// For an engaged target process, create a peephole over the specified virtual memory range.
156    ///
157    /// `initialize_word` is a bitset of `MIRILLA_MAP_PEEPHOLE_INITIALIZE_*` preferences the kernel
158    /// applies at creation, so that a caller can request one-shot behavior such as populating the
159    /// mapping without a follow-up command.
160    ///
161    /// # Failure
162    ///
163    /// This can fail if the:
164    ///
165    /// * Target was not previously engaged.
166    /// * Provided memory range is malformed.
167    ///
168    /// # Safety
169    ///
170    /// For soundness purposes, the following must be satisfied:
171    ///
172    /// * The provided file descriptor must be a valid `mirilla`-created one.
173    #[inline]
174    pub unsafe fn peephole(
175        fd: BorrowedFd,
176        target_id: TargetId,
177        start_address: virtual_address_t,
178        end_address: virtual_address_t,
179        initialize_word: mirilla_map_peephole_initialize_word_t,
180    ) -> io::Result<(PeepholeId, OwnedFd)> {
181        let mut peephole_id = None::<PeepholeId>;
182        let mut peephole_fd = None::<OwnedFd>;
183
184        let target_outcome =
185            // SAFETY:
186            //
187            // * The caller has asserted that the provided file descriptor comes from `mirilla`.
188            // * `mirilla_map_peephole_id_t` is identical ABI-wise to `Option<PeepholeId>`.
189            // * `OwnedFd/RawFd` is identical ABI-wise to a host file descriptor.
190            unsafe { binding::catalejo_mirilla_peephole(fd.as_raw_fd(), target_id.get(), start_address, end_address, initialize_word, ptr::from_mut(&mut peephole_id).cast::<binding::mirilla_map_target_id_t>(), ptr::from_mut(&mut peephole_fd).cast::<RawFd>()) };
191
192        match (target_outcome, (peephole_id, peephole_fd)) {
193            (binding::MIRILLA_COMMAND_OK, (Some(target_left), Some(target_right))) => {
194                Ok((target_left, target_right))
195            }
196            (binding::MIRILLA_COMMAND_OK, (..)) => Err(io::Error::from(ErrorKind::InvalidInput)),
197            (
198                target_errno @ binding::mirilla_command_status_t::MIN..binding::MIRILLA_COMMAND_OK,
199                (..),
200            ) => Err(io::Error::from_raw_os_error(target_errno.abs())),
201            // NOTE: This is impossible, hence unreachable.
202            #[cfg(not(feature = "stealth-mode"))]
203            _ => unreachable!(),
204
205            #[cfg(feature = "stealth-mode")]
206            _ => std::process::abort(),
207        }
208    }
209
210    /// For an engaged target process, retrieve the full address space layout, the kernel-resident
211    /// auxiliary vector, and the argument/environment metadata.
212    ///
213    /// The retry mechanism against the racy kernel-resident count
214    /// is handled internally, so the caller receives owned vectors holding every kernel-resident
215    /// entry. If the VMA count or auxiliary vector changes between the sizing pass and the
216    /// population pass, the population is retried with a buffer sized to the new count, a count that
217    /// shrinks yields a truncated prefix, a count that grows triggers another retry. The number of
218    /// retries is bounded to avoid an unbounded loop under adversarial churn.
219    ///
220    /// # Failure
221    ///
222    /// This can fail if the:
223    ///
224    /// * Target was not previously engaged.
225    /// * The kernel reports a count too large to allocate.
226    /// * The population pass cannot converge within the retry bound.
227    ///
228    /// # Safety
229    ///
230    /// For soundness purposes, the following must be satisfied:
231    ///
232    /// * The provided file descriptor must be a valid `mirilla`-created one.
233    #[inline]
234    pub unsafe fn address_space_layout(
235        fd: BorrowedFd,
236        target_id: TargetId,
237    ) -> io::Result<(
238        crate::ffi::lower::AddressSpaceMetadata,
239        Vec<crate::ffi::lower::AddressSpaceLayout>,
240        Vec<crate::ffi::lower::AuxiliaryVectorEntry>,
241    )> {
242        const RETRY_BOUND: u32 = 8;
243
244        let mut retry_count = 0;
245
246        loop {
247            // Sizing pass: ask for no population so the kernel only reports the counts.
248            let mut layout_descriptor = binding::mirilla_outside_list {
249                list_address: 0,
250                list_size: 0,
251                element_size: core::mem::size_of::<crate::ffi::lower::AddressSpaceLayout>() as u32,
252                list_attribute: binding::MIRILLA_OUTSIDE_LIST_ATTRIBUTE_DO_NOT_POPULATE,
253            };
254            let mut auxiliary_vector_descriptor = binding::mirilla_outside_list {
255                list_address: 0,
256                list_size: 0,
257                element_size: core::mem::size_of::<crate::ffi::lower::AuxiliaryVectorEntry>()
258                    as u32,
259                list_attribute: binding::MIRILLA_OUTSIDE_LIST_ATTRIBUTE_DO_NOT_POPULATE,
260            };
261
262            let sizing_outcome =
263                // SAFETY: The caller has asserted that the provided file descriptor comes from `mirilla`.
264                // Both lists carry `DO_NOT_POPULATE`, so no backing buffer is dereferenced.
265                unsafe {
266                    crate::ffi::lower::address_space_layout(
267                        fd,
268                        target_id,
269                        &mut layout_descriptor,
270                        &mut auxiliary_vector_descriptor,
271                    )
272                }?;
273
274            let layout_capacity = sizing_outcome.layout_total_count;
275            let auxiliary_vector_capacity = sizing_outcome.auxiliary_vector_total_count;
276
277            let mut layout_buffer: Vec<crate::ffi::lower::AddressSpaceLayout> =
278                Vec::with_capacity(usize::try_from(layout_capacity).unwrap_or(0));
279            let mut auxiliary_vector_buffer: Vec<crate::ffi::lower::AuxiliaryVectorEntry> =
280                Vec::with_capacity(usize::try_from(auxiliary_vector_capacity).unwrap_or(0));
281
282            // Population pass: supply the buffers and let the kernel fill them.
283            let mut layout_descriptor = binding::mirilla_outside_list {
284                list_address: if layout_capacity != 0 {
285                    layout_buffer.as_mut_ptr().expose_provenance() as u64
286                } else {
287                    0
288                },
289                list_size: layout_capacity,
290                element_size: core::mem::size_of::<crate::ffi::lower::AddressSpaceLayout>() as u32,
291                list_attribute: 0,
292            };
293            let mut auxiliary_vector_descriptor = binding::mirilla_outside_list {
294                list_address: if auxiliary_vector_capacity != 0 {
295                    auxiliary_vector_buffer.as_mut_ptr().expose_provenance() as u64
296                } else {
297                    0
298                },
299                list_size: auxiliary_vector_capacity,
300                element_size: core::mem::size_of::<crate::ffi::lower::AuxiliaryVectorEntry>()
301                    as u32,
302                list_attribute: 0,
303            };
304
305            let population_outcome =
306                // SAFETY:
307                //
308                // * The caller has asserted that the provided file descriptor comes from `mirilla`.
309                // * Each backing buffer is a valid `Vec` allocation of the matching element type
310                //   and capacity, held for the duration of the call.
311                unsafe {
312                    crate::ffi::lower::address_space_layout(
313                        fd,
314                        target_id,
315                        &mut layout_descriptor,
316                        &mut auxiliary_vector_descriptor,
317                    )
318                }?;
319
320            // Convergence: the counts must not have grown beyond the allocated capacity. A shrink
321            // is safe: the populated prefix is valid and the trailing slots are uninitialized. A
322            // grow means the kernel reported more entries than the buffer can hold, so retry with
323            // the new count.
324            if population_outcome.layout_total_count <= layout_capacity
325                && population_outcome.auxiliary_vector_total_count <= auxiliary_vector_capacity
326            {
327                // SAFETY: The kernel populated exactly `population_outcome.*_total_count` entries,
328                // each of the matching element type, into the buffer.
329                unsafe {
330                    layout_buffer.set_len(population_outcome.layout_total_count as usize);
331
332                    auxiliary_vector_buffer
333                        .set_len(population_outcome.auxiliary_vector_total_count as usize);
334                }
335
336                return Ok((
337                    population_outcome.metadata,
338                    layout_buffer,
339                    auxiliary_vector_buffer,
340                ));
341            }
342
343            retry_count += 1;
344            if retry_count >= RETRY_BOUND {
345                #[cfg(feature = "stealth-mode")]
346                return Err(io::Error::from(ErrorKind::ResourceBusy));
347
348                #[cfg(not(feature = "stealth-mode"))]
349                return Err(io::Error::new(
350                    ErrorKind::ResourceBusy,
351                    "address space layout count did not converge within the retry bound",
352                ));
353            }
354        }
355    }
356}
357
358pub mod lower {
359    //! Low-level and plumbing structures and functions towards the Foreign-Function-Interface boundary.
360
361    use std::{io, os::fd::AsRawFd, os::fd::BorrowedFd};
362
363    use crate::{ffi::binding, id::TargetId};
364
365    /// The metadata of an address space, as returned by a layout query.
366    pub type AddressSpaceMetadata = binding::mirilla_map_address_space_metadata;
367
368    /// A single address space layout entry, as returned by a layout query.
369    pub type AddressSpaceLayout = binding::mirilla_map_address_space_layout;
370
371    /// A single auxiliary vector entry, as returned by a layout query.
372    pub type AuxiliaryVectorEntry = binding::mirilla_auxiliary_vector_entry;
373
374    /// The outcome of a layout query: the metadata plus the full kernel-resident
375    /// counts for each outside list.
376    #[derive(Debug)]
377    pub struct AddressSpaceLayoutOutcome {
378        /// Kernel-resident metadata of the address space whose layout was requested.
379        pub metadata: AddressSpaceMetadata,
380
381        /// The full kernel-resident count of address space layout entries.
382        pub layout_total_count: u32,
383
384        /// The full kernel-resident count of auxiliary vector entries.
385        pub auxiliary_vector_total_count: u32,
386    }
387
388    /// Perform a bare-bones address space layout query via the C-implemented shim.
389    ///
390    /// Each `mirilla_outside_list` is an in/out descriptor: the caller supplies the backing
391    /// buffer address, capacity and element size, and the kernel populates up to the capacity
392    /// and reports the full kernel-resident count through the matching outcome. The caller is
393    /// responsible for allocating, sizing and reading back the populated prefix.
394    ///
395    /// # Safety
396    ///
397    /// For soundness purposes, the following must be satisfied:
398    ///
399    /// * The provided file descriptor must be a valid `mirilla`-created one.
400    /// * Each `mirilla_outside_list::list_address` must either be null (only valid with
401    ///   `MIRILLA_OUTSIDE_LIST_ATTRIBUTE_DO_NOT_POPULATE`) or name a writable buffer of at
402    ///   least `list_size * element_size` bytes for the duration of the call.
403    #[inline]
404    pub unsafe fn address_space_layout(
405        fd: BorrowedFd,
406        target_id: TargetId,
407        layout_list: &mut binding::mirilla_outside_list,
408        auxiliary_vector_list: &mut binding::mirilla_outside_list,
409    ) -> io::Result<AddressSpaceLayoutOutcome> {
410        let mut metadata = core::mem::MaybeUninit::<AddressSpaceMetadata>::uninit();
411        let mut layout_outcome =
412            core::mem::MaybeUninit::<binding::mirilla_outside_list_outcome>::uninit();
413        let mut auxiliary_vector_outcome =
414            core::mem::MaybeUninit::<binding::mirilla_outside_list_outcome>::uninit();
415
416        let target_outcome =
417            // SAFETY: The safety concerns of the foreign call have been satisfied by the caller.
418            unsafe {
419                binding::catalejo_mirilla_address_space_layout(
420                    fd.as_raw_fd(),
421                    target_id.get(),
422                    layout_list,
423                    auxiliary_vector_list,
424                    metadata.as_mut_ptr(),
425                    layout_outcome.as_mut_ptr(),
426                    auxiliary_vector_outcome.as_mut_ptr(),
427                )
428            };
429
430        match target_outcome {
431            binding::MIRILLA_COMMAND_OK => Ok(AddressSpaceLayoutOutcome {
432                // SAFETY: The kernel wrote the metadata on success.
433                metadata: unsafe { metadata.assume_init() },
434                // SAFETY: The kernel wrote the address space layout on success.
435                layout_total_count: unsafe { layout_outcome.assume_init() }.total_count,
436                // SAFETY: The kernel wrote the auxiliary vector outcome on success.
437                auxiliary_vector_total_count: unsafe { auxiliary_vector_outcome.assume_init() }
438                    .total_count,
439            }),
440            target_errno @ binding::mirilla_command_status_t::MIN..binding::MIRILLA_COMMAND_OK => {
441                Err(io::Error::from_raw_os_error(target_errno.abs()))
442            }
443            // NOTE: This is impossible, hence unreachable.
444            #[cfg(not(feature = "stealth-mode"))]
445            _ => unreachable!(),
446
447            #[cfg(feature = "stealth-mode")]
448            _ => std::process::abort(),
449        }
450    }
451}