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}