|
| 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 | + |
1 | 41 | #[cfg(not(any(target_family = "unix")))] |
2 | 42 | compile_error!( |
3 | 43 | "Because of its heavy use of POSIX system calls, this crate only supports Unix-like operating systems (e.g. Linux, macOS, BSD)" |
@@ -28,12 +68,87 @@ pub use serde::{Serialize, de::DeserializeOwned}; |
28 | 68 | /// serializes its result (using bincode) and writes it through a pipe, which |
29 | 69 | /// the parent reads and deserializes. |
30 | 70 | /// |
31 | | -/// # Safety |
32 | | -/// This code directly calls glibc functions (via the libc crate) and should |
33 | | -/// only be used in a Unix environment. |
| 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. |
34 | 150 | pub fn execute_in_isolated_process<F, T>(callable: F) -> Result<T, MemIsolateError> |
35 | 151 | where |
36 | | - // TODO: Consider restricting to disallow FnMut() closures |
37 | 152 | F: FnOnce() -> T, |
38 | 153 | T: Serialize + DeserializeOwned, |
39 | 154 | { |
@@ -161,7 +276,7 @@ where |
161 | 276 | } // The read_fd will automatically be closed when the File is dropped |
162 | 277 |
|
163 | 278 | if buffer.is_empty() { |
164 | | - // TODO: How can we more rigerously know this? Maybe we write to a mem map before and after execution? |
| 279 | + // TODO: How can we more rigorously know this? Maybe we write to a mem map before and after execution? |
165 | 280 | return Err(CallableStatusUnknown(CallableProcessDiedDuringExecution)); |
166 | 281 | } |
167 | 282 | // Update the deserialization to handle child errors |
|
0 commit comments