Skip to main content

oxicrypt_imageread/
lib.rs

1//! Kernel-mediated reads of the module's own loaded image.
2//!
3//! # Why this crate exists at all
4//!
5//! `oxicrypt-integrity` keeps `#![forbid(unsafe_code)]`, because the
6//! failure mode of a raw pointer read, in the crate whose whole job is
7//! integrity, is the one failure mode worth spending effort to avoid.
8//! On Linux and Android it can hold that line completely: the loaded
9//! image is readable through `/proc/self/mem` or through the backing
10//! file, so every acquisition is an ordinary positioned file read and a
11//! wrong offset produces a short read rather than undefined behaviour.
12//!
13//! Darwin and Windows offer no file-shaped route to a process's own
14//! memory. Reading the image there needs a system call, and a system
15//! call needs an `extern` declaration — so the declarations live here,
16//! in a crate that does nothing else, rather than eroding the guarantee
17//! in the crate that performs the test.
18//!
19//! # Why a system call rather than a pointer read
20//!
21//! Both mechanisms below are *kernel-mediated copies*, and that is the
22//! point of choosing them. The addresses this crate is asked to read
23//! come from a range table inside the artifact; a corrupt or hostile
24//! table can name an address that is not mapped. Dereferencing it would
25//! fault and take the process down — a denial of service triggered by
26//! exactly the malformed input the integrity test exists to detect.
27//! `mach_vm_read_overwrite` and `ReadProcessMemory` return a status
28//! instead, so an unreadable range becomes an error return and the
29//! module enters its error state, which is the required outcome.
30//!
31//! The `unsafe` here is therefore confined to *calling* two documented
32//! system interfaces with a buffer this crate owns. It performs no
33//! pointer arithmetic on the addresses it is given, parses no executable
34//! format, and never dereferences them.
35
36/// Why a self-image read did not complete.
37#[derive(Debug)]
38pub enum ReadError {
39    /// This target has no implemented mechanism.
40    NoMechanism,
41    /// The operating system refused the read and reported this status.
42    ///
43    /// `mach_vm_read_overwrite`'s `kern_return_t` on Darwin, the value
44    /// of `GetLastError()` on Windows. Carried rather than collapsed to
45    /// a boolean because "the address is not mapped" and "the process
46    /// lacks the right" are different findings for whoever is holding
47    /// a module that will not start.
48    Os(i64),
49    /// The mechanism succeeded but returned fewer bytes than asked for.
50    ///
51    /// Distinguished from [`ReadError::Os`] because a short read with a
52    /// success status means the request straddled the end of a mapping,
53    /// which is a statement about the range table rather than about
54    /// permissions.
55    Short {
56        /// Bytes requested.
57        wanted: usize,
58        /// Bytes the mechanism actually supplied.
59        got: usize,
60    },
61}
62
63impl core::fmt::Display for ReadError {
64    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
65        match self {
66            Self::NoMechanism => f.write_str("no self-image read mechanism on this target"),
67            Self::Os(status) => write!(f, "the operating system refused the read: status {status}"),
68            Self::Short { wanted, got } => {
69                write!(f, "short self-image read: wanted {wanted} bytes, got {got}")
70            }
71        }
72    }
73}
74
75impl std::error::Error for ReadError {}
76
77/// Whether this target has a mechanism at all.
78///
79/// Exposed so a caller can tell "the test was not performed because this
80/// platform has no mechanism" from "the test was performed and failed",
81/// without provoking a read to find out.
82#[must_use]
83pub const fn available() -> bool {
84    cfg!(any(target_os = "macos", target_os = "ios", windows))
85}
86
87/// Copies `out.len()` bytes beginning at `addr` in this process's own
88/// loaded image into `out`.
89///
90/// # Errors
91///
92/// Returns [`ReadError::NoMechanism`] on a target with no implementation,
93/// [`ReadError::Os`] when the operating system refuses, and
94/// [`ReadError::Short`] when fewer bytes arrive than were asked for.
95///
96/// An empty `out` is a successful no-op: the mechanisms below are not
97/// specified for a zero-length request, and asking one for zero bytes
98/// would make the outcome depend on a platform detail rather than on the
99/// module's state.
100pub fn read_self(addr: usize, out: &mut [u8]) -> Result<(), ReadError> {
101    if out.is_empty() {
102        return Ok(());
103    }
104    imp::read_self(addr, out)
105}
106
107#[cfg(any(target_os = "macos", target_os = "ios"))]
108mod imp {
109    #![allow(unsafe_code)]
110
111    use super::ReadError;
112
113    /// `mach_port_t`.
114    type MachPortT = u32;
115    /// `kern_return_t`.
116    type KernReturnT = i32;
117    /// `mach_vm_address_t` and `mach_vm_size_t` are both 64-bit on every
118    /// Darwin target this module builds for.
119    type MachVmAddressT = u64;
120    /// See [`MachVmAddressT`].
121    type MachVmSizeT = u64;
122
123    /// `KERN_SUCCESS`.
124    const KERN_SUCCESS: KernReturnT = 0;
125
126    unsafe extern "C" {
127        /// `mach_task_self()` is a macro over this global in
128        /// `<mach/mach_init.h>`, not a function — declaring it as a
129        /// function would link against a symbol that does not exist.
130        static mach_task_self_: MachPortT;
131
132        /// Copies memory from `target_task` into a buffer the caller
133        /// already owns, rather than allocating a new one as
134        /// `mach_vm_read` does. Returns `KERN_SUCCESS` or a
135        /// `kern_return_t` describing why not.
136        fn mach_vm_read_overwrite(
137            target_task: MachPortT,
138            address: MachVmAddressT,
139            size: MachVmSizeT,
140            data: MachVmAddressT,
141            out_size: *mut MachVmSizeT,
142        ) -> KernReturnT;
143    }
144
145    pub(super) fn read_self(addr: usize, out: &mut [u8]) -> Result<(), ReadError> {
146        let wanted = out.len();
147        let mut got: MachVmSizeT = 0;
148        // SAFETY: `out` is a live, uniquely borrowed slice of `wanted`
149        // bytes, so the destination the kernel is given is valid for
150        // writes of exactly the size declared. `addr` is not
151        // dereferenced here — it is passed to the kernel, which
152        // validates it and reports `KERN_INVALID_ADDRESS` rather than
153        // faulting if it is not mapped. `out_size` points to a live
154        // local. `mach_task_self_` is the current task port.
155        let status = unsafe {
156            mach_vm_read_overwrite(
157                mach_task_self_,
158                addr as MachVmAddressT,
159                wanted as MachVmSizeT,
160                out.as_mut_ptr() as MachVmAddressT,
161                &mut got,
162            )
163        };
164        if status != KERN_SUCCESS {
165            return Err(ReadError::Os(i64::from(status)));
166        }
167        let got = usize::try_from(got).unwrap_or(0);
168        if got != wanted {
169            return Err(ReadError::Short { wanted, got });
170        }
171        Ok(())
172    }
173}
174
175#[cfg(windows)]
176mod imp {
177    #![allow(unsafe_code)]
178
179    use super::ReadError;
180
181    /// `HANDLE`.
182    type Handle = *mut core::ffi::c_void;
183
184    unsafe extern "system" {
185        /// A pseudo-handle to the current process. It needs no closing.
186        fn GetCurrentProcess() -> Handle;
187
188        /// Copies memory from another process — or, as here, from this
189        /// one. Returns zero on failure, with the reason in
190        /// `GetLastError`.
191        fn ReadProcessMemory(
192            process: Handle,
193            base: *const core::ffi::c_void,
194            buffer: *mut core::ffi::c_void,
195            size: usize,
196            read: *mut usize,
197        ) -> i32;
198
199        /// The calling thread's last error code.
200        fn GetLastError() -> u32;
201    }
202
203    pub(super) fn read_self(addr: usize, out: &mut [u8]) -> Result<(), ReadError> {
204        let wanted = out.len();
205        let mut got: usize = 0;
206        // SAFETY: `out` is a live, uniquely borrowed slice of `wanted`
207        // bytes, so the destination buffer is valid for writes of the
208        // declared size. `addr` is passed to the kernel as an opaque
209        // address and is never dereferenced here; an unmapped address
210        // makes `ReadProcessMemory` return zero rather than fault.
211        // `GetCurrentProcess` yields a pseudo-handle that requires no
212        // release, and `got` points to a live local.
213        let ok = unsafe {
214            ReadProcessMemory(
215                GetCurrentProcess(),
216                addr as *const core::ffi::c_void,
217                out.as_mut_ptr().cast::<core::ffi::c_void>(),
218                wanted,
219                &mut got,
220            )
221        };
222        if ok == 0 {
223            // SAFETY: reads a thread-local error code set by the call
224            // above; it takes no arguments and returns a plain integer.
225            let code = unsafe { GetLastError() };
226            return Err(ReadError::Os(i64::from(code)));
227        }
228        if got != wanted {
229            return Err(ReadError::Short { wanted, got });
230        }
231        Ok(())
232    }
233}
234
235/// Every target with a file-shaped route to its own image, plus every
236/// target this module has not been ported to.
237///
238/// Linux and Android are deliberately here rather than given a
239/// mechanism: they read through `/proc/self/mem` or the backing file,
240/// which needs no `unsafe` at all, so compiling one for them would add
241/// an exception the boundary does not need.
242#[cfg(not(any(target_os = "macos", target_os = "ios", windows)))]
243mod imp {
244    use super::ReadError;
245
246    pub(super) fn read_self(_addr: usize, _out: &mut [u8]) -> Result<(), ReadError> {
247        Err(ReadError::NoMechanism)
248    }
249}