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}