mem_isolate/
lib.rs

1//! # `mem-isolate`: *Run unsafe code safely*
2//!
3//! It runs your function via a `fork()`, waits for the result, and
4//! returns it.
5//!
6//! This grants your code access to an exact copy of memory and state at the
7//! time just before the call, but guarantees that the function will not affect
8//! the parent process's memory footprint in any way. It forces functions to be
9//! *pure*, even if they aren't.
10//!
11//! ```
12//! use mem_isolate::execute_in_isolated_process;
13//!
14//! // No heap, stack, or program memory out here...
15//! let result = mem_isolate::execute_in_isolated_process(|| {
16//!     // ...Can be affected by anything in here
17//!     Box::leak(Box::new(vec![42; 1024]));
18//! });
19//! ```
20//!
21//! To keep things simple, this crate exposes only two public interfaces:
22//!
23//! * [`execute_in_isolated_process`] - The function that executes your code in
24//!   an isolated process.
25//! * [`MemIsolateError`] - The error type that function returns ☝️
26//!
27//! For more code examples, see [`examples/`](https://github.com/brannondorsey/mem-isolate/tree/main/examples).
28//! [This one](https://github.com/brannondorsey/mem-isolate/blob/main/examples/error-handling-basic.rs)
29//! in particular shows how you should think about error handling.
30//!
31//! For more information, see the [README](https://github.com/brannondorsey/mem-isolate).
32//!
33//! ## Supported platforms
34//!
35//! Because of its heavy use of POSIX system calls, this crate only
36//! supports Unix-like operating systems (e.g. Linux, macOS, BSD).
37//!
38//! Windows and wasm support are not planned at this time.
39#![warn(missing_docs)]
40
41#[cfg(not(any(target_family = "unix")))]
42compile_error!(
43    "Because of its heavy use of POSIX system calls, this crate only supports Unix-like operating systems (e.g. Linux, macOS, BSD)"
44);
45
46use std::fs::File;
47use std::io::{Read, Write};
48use std::os::unix::io::FromRawFd;
49
50#[cfg(test)]
51mod tests;
52
53mod c;
54use c::{
55    ForkReturn, PipeFds, SystemFunctions, child_process_exited_on_its_own,
56    child_process_killed_by_signal,
57};
58
59pub mod errors;
60pub use errors::MemIsolateError;
61use errors::{CallableDidNotExecuteError, CallableExecutedError, CallableStatusUnknownError};
62
63// Re-export the serde traits our public API depends on
64pub use serde::{Serialize, de::DeserializeOwned};
65
66/// Executes a user-supplied `callable` in a forked child process so that any
67/// memory changes during execution do not affect the parent. The child
68/// serializes its result (using bincode) and writes it through a pipe, which
69/// the parent reads and deserializes.
70///
71/// # Example
72///
73/// ```rust
74/// use mem_isolate::execute_in_isolated_process;
75///
76/// let leaky_fn = || {
77///     // Leak 1KiB of memory
78///     let data: Vec<u8> = Vec::with_capacity(1024);
79///     let data = Box::new(data);
80///     Box::leak(data);
81/// };
82///
83/// let _ = execute_in_isolated_process(leaky_fn);
84/// // However, the memory is not leaked in the parent process here
85/// ```
86///
87/// # Error Handling
88///
89/// Error handling is organized into three levels:
90///
91/// 1. The first level describes the effect of the error on the `callable` (e.g.
92///    did your callable function execute or not)
93/// 2. The second level describes what `mem-isolate` operation caused the error
94///    (e.g. did serialization fail)
95/// 3. The third level is the underlying OS error if it is available (e.g. an
96///    `io::Error`)
97///
98/// For most applications, you'll care only about the first level:
99///
100/// ```rust
101/// use mem_isolate::{execute_in_isolated_process, MemIsolateError};
102///
103/// // Function that might cause memory issues
104/// let result = execute_in_isolated_process(|| {
105///     // Some operation
106///     "Success!".to_string()
107/// });
108///
109/// match result {
110///     Ok(value) => println!("Callable succeeded: {}", value),
111///     Err(MemIsolateError::CallableDidNotExecute(_)) => {
112///         // Safe to retry, callable never executed
113///         println!("Callable did not execute, can safely retry");
114///     },
115///     Err(MemIsolateError::CallableExecuted(_)) => {
116///         // Do not retry unless idempotent
117///         println!("Callable executed but result couldn't be returned");
118///     },
119///     Err(MemIsolateError::CallableStatusUnknown(_)) => {
120///         // Retry only if idempotent
121///         println!("Unknown if callable executed, retry only if idempotent");
122///     }
123/// }
124/// ```
125///
126/// For a more detailed look at error handling, see the documentation in the
127/// [`errors`] module.
128///
129/// ## Important Note on Closures
130///
131/// When using closures that capture and mutate variables from their environment,
132/// these mutations **only occur in the isolated child process** and do not affect
133/// the parent process's memory. For example, it may seem surprising that the
134/// following code will leave the parent's `counter` variable unchanged:
135///
136/// ```rust
137/// use mem_isolate::execute_in_isolated_process;
138///
139/// let mut counter = 0;
140/// let result = execute_in_isolated_process(|| {
141///     counter += 1;  // This increment only happens in the child process
142///     counter        // Returns 1
143/// });
144/// assert_eq!(counter, 0);  // Parent's counter remains unchanged
145/// ```
146///
147/// This is the intended behavior as the function's purpose is to isolate all
148/// memory effects of the callable. However, this can be surprising, especially
149/// for [`FnMut`] or [`FnOnce`] closures.
150pub fn execute_in_isolated_process<F, T>(callable: F) -> Result<T, MemIsolateError>
151where
152    F: FnOnce() -> T,
153    T: Serialize + DeserializeOwned,
154{
155    use CallableDidNotExecuteError::*;
156    use CallableExecutedError::*;
157    use CallableStatusUnknownError::*;
158    use MemIsolateError::*;
159
160    // Use the appropriate implementation based on build config
161    #[cfg(not(test))]
162    let sys = c::RealSystemFunctions;
163
164    #[cfg(test)]
165    let sys = if c::mock::is_mocking_enabled() {
166        // Use the mock from thread-local storage
167        c::mock::get_current_mock()
168    } else {
169        // Create a new fallback mock if no mock is active
170        c::mock::MockableSystemFunctions::with_fallback()
171    };
172
173    // Create a pipe.
174    let PipeFds { read_fd, write_fd } = match sys.pipe() {
175        Ok(pipe_fds) => pipe_fds,
176        Err(err) => {
177            return Err(CallableDidNotExecute(PipeCreationFailed(err)));
178        }
179    };
180
181    // Statuses 3-63 are normally fair game
182    const CHILD_EXIT_HAPPY: i32 = 0;
183    const CHILD_EXIT_IF_READ_CLOSE_FAILED: i32 = 3;
184    const CHILD_EXIT_IF_WRITE_FAILED: i32 = 4;
185
186    match sys.fork() {
187        Err(err) => Err(CallableDidNotExecute(ForkFailed(err))),
188        Ok(ForkReturn::Child) => {
189            // NOTE: We chose to panic in the child if we can't communicate an error back to the parent.
190            // The parent can then interpret this an an UnexpectedChildDeath.
191
192            // Droping the writer will close the write_fd, so we take it early and explicitly to
193            // prevent use after free with two unsafe calls scattered around the child code below.
194            let mut writer = unsafe { File::from_raw_fd(write_fd) };
195
196            // Close the read end of the pipe
197            if let Err(close_err) = sys.close(read_fd) {
198                let err = CallableDidNotExecute(ChildPipeCloseFailed(Some(close_err)));
199                let encoded = bincode::serialize(&err).expect("failed to serialize error");
200                writer
201                    .write_all(&encoded)
202                    .expect("failed to write error to pipe");
203                writer.flush().expect("failed to flush error to pipe");
204                sys._exit(CHILD_EXIT_IF_READ_CLOSE_FAILED);
205            }
206
207            // Execute the callable and handle serialization
208            let result = callable();
209            let encoded = match bincode::serialize(&Ok::<T, MemIsolateError>(result)) {
210                Ok(encoded) => encoded,
211                Err(err) => {
212                    let err = CallableExecuted(SerializationFailed(err.to_string()));
213                    bincode::serialize(&Err::<T, MemIsolateError>(err))
214                        .expect("failed to serialize error")
215                }
216            };
217
218            // Write the result to the pipe
219            let write_result = writer.write_all(&encoded).and_then(|_| writer.flush());
220
221            if let Err(_err) = write_result {
222                // If we can't write to the pipe, we can't communicate the error either
223                // Parent will detect this as an UnexpectedChildDeath
224                sys._exit(CHILD_EXIT_IF_WRITE_FAILED);
225            }
226
227            // Exit immediately; use _exit to avoid running atexit()/on_exit() handlers
228            // and flushing stdio buffers, which are exact clones of the parent in the child process.
229            sys._exit(CHILD_EXIT_HAPPY);
230            // The code after _exit is unreachable because _exit never returns
231        }
232        Ok(ForkReturn::Parent(child_pid)) => {
233            // Close the write end of the pipe
234            if let Err(close_err) = sys.close(write_fd) {
235                return Err(CallableStatusUnknown(ParentPipeCloseFailed(close_err)));
236            }
237
238            // Wait for the child process to exit
239            let waitpid_bespoke_status = match sys.waitpid(child_pid) {
240                Ok(status) => status,
241                Err(wait_err) => {
242                    return Err(CallableStatusUnknown(WaitFailed(wait_err)));
243                }
244            };
245
246            if let Some(exit_status) = child_process_exited_on_its_own(waitpid_bespoke_status) {
247                match exit_status {
248                    CHILD_EXIT_HAPPY => {}
249                    CHILD_EXIT_IF_READ_CLOSE_FAILED => {
250                        return Err(CallableDidNotExecute(ChildPipeCloseFailed(None)));
251                    }
252                    CHILD_EXIT_IF_WRITE_FAILED => {
253                        return Err(CallableExecuted(ChildPipeWriteFailed(None)));
254                    }
255                    unhandled_status => {
256                        return Err(CallableStatusUnknown(UnexpectedChildExitStatus(
257                            unhandled_status,
258                        )));
259                    }
260                }
261            } else if let Some(signal) = child_process_killed_by_signal(waitpid_bespoke_status) {
262                return Err(CallableStatusUnknown(ChildProcessKilledBySignal(signal)));
263            } else {
264                return Err(CallableStatusUnknown(UnexpectedWaitpidReturnValue(
265                    waitpid_bespoke_status,
266                )));
267            }
268
269            // Read from the pipe by wrapping the read fd as a File
270            let mut buffer = Vec::new();
271            {
272                let mut reader = unsafe { File::from_raw_fd(read_fd) };
273                if let Err(err) = reader.read_to_end(&mut buffer) {
274                    return Err(CallableStatusUnknown(ParentPipeReadFailed(err)));
275                }
276            } // The read_fd will automatically be closed when the File is dropped
277
278            if buffer.is_empty() {
279                // TODO: How can we more rigorously know this? Maybe we write to a mem map before and after execution?
280                return Err(CallableStatusUnknown(CallableProcessDiedDuringExecution));
281            }
282            // Update the deserialization to handle child errors
283            match bincode::deserialize::<Result<T, MemIsolateError>>(&buffer) {
284                Ok(Ok(result)) => Ok(result),
285                Ok(Err(err)) => Err(err),
286                Err(err) => Err(CallableExecuted(DeserializationFailed(err.to_string()))),
287            }
288        }
289    }
290}