1#![warn(missing_docs)]
57#![warn(clippy::pedantic, clippy::unwrap_used)]
58#![warn(missing_debug_implementations)]
59
60#[cfg(not(any(target_family = "unix")))]
61compile_error!(
62 "Because of its heavy use of POSIX system calls, this crate only supports Unix-like operating systems (e.g. Linux, macOS, BSD)"
63);
64
65mod macros;
66use macros::{debug, error};
67#[cfg(feature = "tracing")]
68use tracing::{Level, instrument, span};
71
72use libc::{EINTR, c_int};
73use std::fmt::Debug;
74use std::fs::File;
75use std::io::{Read, Write};
76use std::os::unix::io::FromRawFd;
77
78#[cfg(test)]
79mod tests;
80
81mod c;
82use c::{
83 ForkReturn, PipeFds, SystemFunctions, WaitpidStatus, child_process_exited_on_its_own,
84 child_process_killed_by_signal,
85};
86
87pub mod errors;
88pub use errors::MemIsolateError;
89use errors::{
90 CallableDidNotExecuteError::{ChildPipeCloseFailed, ForkFailed, PipeCreationFailed},
91 CallableExecutedError::{ChildPipeWriteFailed, DeserializationFailed, SerializationFailed},
92 CallableStatusUnknownError::{
93 CallableProcessDiedDuringExecution, ChildProcessKilledBySignal, ParentPipeCloseFailed,
94 ParentPipeReadFailed, UnexpectedChildExitStatus, UnexpectedWaitpidReturnValue, WaitFailed,
95 },
96};
97
98use MemIsolateError::{CallableDidNotExecute, CallableExecuted, CallableStatusUnknown};
99
100pub use serde::{Serialize, de::DeserializeOwned};
102
103const CHILD_EXIT_HAPPY: i32 = 0;
105const CHILD_EXIT_IF_READ_CLOSE_FAILED: i32 = 3;
106const CHILD_EXIT_IF_WRITE_FAILED: i32 = 4;
107
108#[cfg(feature = "tracing")]
109const HIGHEST_LEVEL: Level = Level::ERROR;
110
111#[cfg_attr(feature = "tracing", instrument(skip(callable)))]
196pub fn execute_in_isolated_process<F, T>(callable: F) -> Result<T, MemIsolateError>
197where
198 F: FnOnce() -> T,
199 T: Serialize + DeserializeOwned,
200{
201 #[cfg(feature = "tracing")]
202 let parent_span = span!(HIGHEST_LEVEL, "parent").entered();
203
204 let sys = get_system_functions();
205 let PipeFds { read_fd, write_fd } = create_pipe(&sys)?;
206
207 match fork(&sys)? {
208 ForkReturn::Child => {
209 #[cfg(feature = "tracing")]
210 let _child_span = {
211 std::mem::drop(parent_span);
212 span!(HIGHEST_LEVEL, "child").entered()
213 };
214 let mut writer = unsafe { File::from_raw_fd(write_fd) };
222 close_read_end_of_pipe_in_child_or_exit(&sys, &mut writer, read_fd);
223
224 let result = execute_callable(callable);
225 let encoded = serialize_result_or_error_value(result);
226 write_and_flush_or_exit(&sys, &mut writer, &encoded);
227 exit_happy(&sys)
228 }
229 ForkReturn::Parent(child_pid) => {
230 close_write_end_of_pipe_in_parent(&sys, write_fd)?;
231
232 let waitpid_bespoke_status = wait_for_child(&sys, child_pid)?;
233 error_if_child_unhappy(waitpid_bespoke_status)?;
234
235 let buffer: Vec<u8> = read_all_of_child_result_pipe(read_fd)?;
236 deserialize_result(&buffer)
237 }
238 }
239}
240
241#[must_use]
242#[cfg_attr(feature = "tracing", instrument)]
243fn get_system_functions() -> impl SystemFunctions {
244 #[cfg(not(test))]
246 let sys = c::RealSystemFunctions;
247
248 #[cfg(test)]
249 let sys = if c::mock::is_mocking_enabled() {
250 c::mock::get_current_mock()
252 } else {
253 c::mock::MockableSystemFunctions::with_fallback()
255 };
256
257 debug!("using {:?}", sys);
258 sys
259}
260
261#[cfg_attr(feature = "tracing", instrument)]
262fn create_pipe<S: SystemFunctions>(sys: &S) -> Result<PipeFds, MemIsolateError> {
263 let pipe_fds = match sys.pipe() {
264 Ok(pipe_fds) => pipe_fds,
265 Err(err) => {
266 let err = CallableDidNotExecute(PipeCreationFailed(err));
267 error!("error creating pipe, propagating {:?}", err);
268 return Err(err);
269 }
270 };
271 debug!("pipe created: {:?}", pipe_fds);
272 Ok(pipe_fds)
273}
274
275#[cfg_attr(feature = "tracing", instrument)]
276fn fork<S: SystemFunctions>(sys: &S) -> Result<ForkReturn, MemIsolateError> {
277 match sys.fork() {
278 Ok(result) => Ok(result),
279 Err(err) => {
280 let err = CallableDidNotExecute(ForkFailed(err));
281 error!("error forking, propagating {:?}", err);
282 Err(err)
283 }
284 }
285}
286
287#[cfg_attr(feature = "tracing", instrument(skip(callable)))]
288fn execute_callable<F, T>(callable: F) -> T
289where
290 F: FnOnce() -> T,
291{
292 debug!("starting execution of user-supplied callable");
293 #[allow(clippy::let_and_return)]
294 let result = {
295 #[cfg(feature = "tracing")]
296 let _span = span!(HIGHEST_LEVEL, "inside_callable").entered();
297 callable()
298 };
299 debug!("finished execution of user-supplied callable");
300 result
301}
302
303#[cfg_attr(feature = "tracing", instrument)]
304fn wait_for_child<S: SystemFunctions>(
305 sys: &S,
306 child_pid: c_int,
307) -> Result<WaitpidStatus, MemIsolateError> {
308 debug!("waiting for child process");
309 let waitpid_bespoke_status = loop {
310 match sys.waitpid(child_pid) {
311 Ok(status) => break status,
312 Err(wait_err) => {
313 if wait_err.raw_os_error() == Some(EINTR) {
314 debug!("waitpid interrupted with EINTR, retrying");
315 continue;
316 }
317 let err = CallableStatusUnknown(WaitFailed(wait_err));
318 error!("error waiting for child process, propagating {:?}", err);
319 return Err(err);
320 }
321 }
322 };
323
324 debug!(
325 "wait completed, received status: {:?}",
326 waitpid_bespoke_status
327 );
328 Ok(waitpid_bespoke_status)
329}
330
331#[cfg_attr(feature = "tracing", instrument)]
332fn error_if_child_unhappy(waitpid_bespoke_status: WaitpidStatus) -> Result<(), MemIsolateError> {
333 let result = if let Some(exit_status) = child_process_exited_on_its_own(waitpid_bespoke_status)
334 {
335 match exit_status {
336 CHILD_EXIT_HAPPY => Ok(()),
337 CHILD_EXIT_IF_READ_CLOSE_FAILED => {
338 Err(CallableDidNotExecute(ChildPipeCloseFailed(None)))
339 }
340 CHILD_EXIT_IF_WRITE_FAILED => Err(CallableExecuted(ChildPipeWriteFailed(None))),
341 unhandled_status => Err(CallableStatusUnknown(UnexpectedChildExitStatus(
342 unhandled_status,
343 ))),
344 }
345 } else if let Some(signal) = child_process_killed_by_signal(waitpid_bespoke_status) {
346 Err(CallableStatusUnknown(ChildProcessKilledBySignal(signal)))
347 } else {
348 Err(CallableStatusUnknown(UnexpectedWaitpidReturnValue(
349 waitpid_bespoke_status,
350 )))
351 };
352
353 if let Ok(()) = result {
354 debug!("child process exited happily on its own");
355 } else {
356 error!("child process signaled an error, propagating {:?}", result);
357 }
358
359 result
360}
361
362#[cfg_attr(feature = "tracing", instrument)]
363fn deserialize_result<T: DeserializeOwned>(buffer: &[u8]) -> Result<T, MemIsolateError> {
364 match bincode::deserialize::<Result<T, MemIsolateError>>(buffer) {
365 Ok(Ok(result)) => {
366 debug!("successfully deserialized happy result");
367 Ok(result)
368 }
369 Ok(Err(err)) => {
370 debug!("successfully deserialized error result: {:?}", err);
371 Err(err)
372 }
373 Err(err) => {
374 let err = CallableExecuted(DeserializationFailed(err.to_string()));
375 error!("failed to deserialize result, propagating {:?}", err);
376 Err(err)
377 }
378 }
379}
380
381#[cfg_attr(feature = "tracing", instrument(skip(result)))]
387fn serialize_result_or_error_value<T: Serialize>(result: T) -> Vec<u8> {
388 match bincode::serialize(&Ok::<T, MemIsolateError>(result)) {
389 Ok(encoded) => {
390 debug!(
391 "serialization successful, resulted in {} bytes",
392 encoded.len()
393 );
394 encoded
395 }
396 Err(err) => {
397 let err = CallableExecuted(SerializationFailed(err.to_string()));
398 error!(
399 "serialization failed, now attempting to serialize error: {:?}",
400 err
401 );
402 #[allow(clippy::let_and_return)]
403 let encoded = bincode::serialize(&Err::<T, MemIsolateError>(err))
404 .expect("failed to serialize error");
405 debug!(
406 "serialization of error successful, resulting in {} bytes",
407 encoded.len()
408 );
409 encoded
410 }
411 }
412}
413
414#[cfg_attr(feature = "tracing", instrument)]
415fn write_and_flush_or_exit<S, W>(sys: &S, writer: &mut W, buffer: &[u8])
416where
417 S: SystemFunctions,
418 W: Write + Debug,
419{
420 let result = writer.write_all(buffer).and_then(|()| writer.flush());
421 #[allow(unused_variables)]
422 if let Err(err) = result {
423 error!("error writing to pipe: {:?}", err);
424 let exit_code = CHILD_EXIT_IF_WRITE_FAILED;
427 debug!("exiting child process with exit code: {}", exit_code);
428 #[allow(clippy::used_underscore_items)]
429 sys._exit(exit_code);
430 } else {
431 debug!("wrote and flushed to pipe successfully");
432 }
433}
434
435fn exit_happy<S: SystemFunctions>(sys: &S) -> ! {
436 #[cfg(feature = "tracing")]
440 let _span = {
441 const FN_NAME: &str = stringify!(exit_happy);
442 span!(HIGHEST_LEVEL, FN_NAME).entered()
443 };
444
445 let exit_code = CHILD_EXIT_HAPPY;
446 debug!("exiting child process with exit code: {}", exit_code);
447
448 #[allow(clippy::used_underscore_items)]
449 sys._exit(exit_code);
450}
451
452#[cfg_attr(feature = "tracing", instrument)]
453fn read_all_of_child_result_pipe(read_fd: c_int) -> Result<Vec<u8>, MemIsolateError> {
454 let mut buffer = Vec::new();
456 {
457 let mut reader = unsafe { File::from_raw_fd(read_fd) };
458 if let Err(err) = reader.read_to_end(&mut buffer) {
459 let err = CallableStatusUnknown(ParentPipeReadFailed(err));
460 error!("error reading from pipe, propagating {:?}", err);
461 return Err(err);
462 }
463 } if buffer.is_empty() {
466 let err = CallableStatusUnknown(CallableProcessDiedDuringExecution);
468 error!("buffer unexpectedly empty, propagating {:?}", err);
469 return Err(err);
470 }
471
472 debug!("successfully read {} bytes from pipe", buffer.len());
473 Ok(buffer)
474}
475
476#[cfg_attr(feature = "tracing", instrument)]
477fn close_write_end_of_pipe_in_parent<S: SystemFunctions>(
478 sys: &S,
479 write_fd: c_int,
480) -> Result<(), MemIsolateError> {
481 if let Err(err) = sys.close(write_fd) {
482 let err = CallableStatusUnknown(ParentPipeCloseFailed(err));
483 error!("error closing write end of pipe, propagating {:?}", err);
484 return Err(err);
485 }
486 debug!("write end of pipe closed successfully");
487 Ok(())
488}
489
490#[cfg_attr(feature = "tracing", instrument)]
491fn close_read_end_of_pipe_in_child_or_exit<S: SystemFunctions>(
492 sys: &S,
493 writer: &mut (impl Write + Debug),
494 read_fd: c_int,
495) {
496 if let Err(close_err) = sys.close(read_fd) {
497 let err = CallableDidNotExecute(ChildPipeCloseFailed(Some(close_err)));
498 error!(
499 "error closing read end of pipe, now attempting to serialize error: {:?}",
500 err
501 );
502
503 let encoded = bincode::serialize(&err).expect("failed to serialize error");
504 writer
505 .write_all(&encoded)
506 .expect("failed to write error to pipe");
507 writer.flush().expect("failed to flush error to pipe");
508
509 let exit_code = CHILD_EXIT_IF_READ_CLOSE_FAILED;
510 error!("exiting child process with exit code: {}", exit_code);
511 #[allow(clippy::used_underscore_items)]
512 sys._exit(exit_code);
513 } else {
514 debug!("read end of pipe closed successfully");
515 }
516}