#![warn(missing_docs)]
#![warn(clippy::pedantic, clippy::unwrap_used)]
#![warn(missing_debug_implementations)]
#[cfg(not(any(target_family = "unix")))]
compile_error!(
"Because of its heavy use of POSIX system calls, this crate only supports Unix-like operating systems (e.g. Linux, macOS, BSD)"
);
mod macros;
use macros::{debug, error};
#[cfg(feature = "tracing")]
use tracing::{Level, instrument, span};
use libc::{EINTR, c_int};
use std::fmt::Debug;
use std::fs::File;
use std::io::{Read, Write};
use std::os::unix::io::FromRawFd;
#[cfg(test)]
mod tests;
mod c;
use c::{
ForkReturn, PipeFds, SystemFunctions, WaitpidStatus, child_process_exited_on_its_own,
child_process_killed_by_signal,
};
pub mod errors;
pub use errors::MemIsolateError;
use errors::{
CallableDidNotExecuteError::{ChildPipeCloseFailed, ForkFailed, PipeCreationFailed},
CallableExecutedError::{ChildPipeWriteFailed, DeserializationFailed, SerializationFailed},
CallableStatusUnknownError::{
CallableProcessDiedDuringExecution, ChildProcessKilledBySignal, ParentPipeCloseFailed,
ParentPipeReadFailed, UnexpectedChildExitStatus, UnexpectedWaitpidReturnValue, WaitFailed,
},
};
use MemIsolateError::{CallableDidNotExecute, CallableExecuted, CallableStatusUnknown};
pub use serde::{Serialize, de::DeserializeOwned};
const CHILD_EXIT_HAPPY: i32 = 0;
const CHILD_EXIT_IF_READ_CLOSE_FAILED: i32 = 3;
const CHILD_EXIT_IF_WRITE_FAILED: i32 = 4;
#[cfg(feature = "tracing")]
const HIGHEST_LEVEL: Level = Level::ERROR;
#[cfg_attr(feature = "tracing", instrument(skip(callable)))]
pub fn execute_in_isolated_process<F, T>(callable: F) -> Result<T, MemIsolateError>
where
F: FnOnce() -> T,
T: Serialize + DeserializeOwned,
{
#[cfg(feature = "tracing")]
let parent_span = span!(HIGHEST_LEVEL, "parent").entered();
let sys = get_system_functions();
let PipeFds { read_fd, write_fd } = create_pipe(&sys)?;
match fork(&sys)? {
ForkReturn::Child => {
#[cfg(feature = "tracing")]
let _child_span = {
std::mem::drop(parent_span);
span!(HIGHEST_LEVEL, "child").entered()
};
#[cfg(test)]
c::mock::disable_mocking();
let mut writer = unsafe { File::from_raw_fd(write_fd) };
close_read_end_of_pipe_in_child_or_exit(&sys, &mut writer, read_fd);
let result = execute_callable(callable);
let encoded = serialize_result_or_error_value(result);
write_and_flush_or_exit(&sys, &mut writer, &encoded);
exit_happy(&sys)
}
ForkReturn::Parent(child_pid) => {
close_write_end_of_pipe_in_parent(&sys, write_fd)?;
let pipe_result = read_all_of_child_result_pipe(read_fd);
let waitpid_bespoke_status = wait_for_child(&sys, child_pid)?;
error_if_child_unhappy(waitpid_bespoke_status)?;
let buffer = pipe_result?;
error_if_buffer_is_empty(&buffer)?;
deserialize_result(&buffer)
}
}
}
#[must_use]
#[cfg_attr(feature = "tracing", instrument)]
fn get_system_functions() -> impl SystemFunctions {
#[cfg(not(test))]
let sys = c::RealSystemFunctions;
#[cfg(test)]
let sys = if c::mock::is_mocking_enabled() {
c::mock::get_current_mock()
} else {
c::mock::MockableSystemFunctions::with_fallback()
};
debug!("using {:?}", sys);
sys
}
#[cfg_attr(feature = "tracing", instrument)]
fn create_pipe<S: SystemFunctions>(sys: &S) -> Result<PipeFds, MemIsolateError> {
let pipe_fds = match sys.pipe() {
Ok(pipe_fds) => pipe_fds,
Err(err) => {
let err = CallableDidNotExecute(PipeCreationFailed(err));
error!("error creating pipe, propagating {:?}", err);
return Err(err);
}
};
debug!("pipe created: {:?}", pipe_fds);
Ok(pipe_fds)
}
#[cfg_attr(feature = "tracing", instrument)]
fn fork<S: SystemFunctions>(sys: &S) -> Result<ForkReturn, MemIsolateError> {
match sys.fork() {
Ok(result) => Ok(result),
Err(err) => {
let err = CallableDidNotExecute(ForkFailed(err));
error!("error forking, propagating {:?}", err);
Err(err)
}
}
}
#[cfg_attr(feature = "tracing", instrument(skip(callable)))]
fn execute_callable<F, T>(callable: F) -> T
where
F: FnOnce() -> T,
{
debug!("starting execution of user-supplied callable");
#[allow(clippy::let_and_return)]
let result = {
#[cfg(feature = "tracing")]
let _span = span!(HIGHEST_LEVEL, "inside_callable").entered();
callable()
};
debug!("finished execution of user-supplied callable");
result
}
#[cfg_attr(feature = "tracing", instrument)]
fn wait_for_child<S: SystemFunctions>(
sys: &S,
child_pid: c_int,
) -> Result<WaitpidStatus, MemIsolateError> {
debug!("waiting for child process");
let waitpid_bespoke_status = loop {
match sys.waitpid(child_pid) {
Ok(status) => break status,
Err(wait_err) => {
if wait_err.raw_os_error() == Some(EINTR) {
debug!("waitpid interrupted with EINTR, retrying");
continue;
}
let err = CallableStatusUnknown(WaitFailed(wait_err));
error!("error waiting for child process, propagating {:?}", err);
return Err(err);
}
}
};
debug!(
"wait completed, received status: {:?}",
waitpid_bespoke_status
);
Ok(waitpid_bespoke_status)
}
#[cfg_attr(feature = "tracing", instrument)]
fn error_if_child_unhappy(waitpid_bespoke_status: WaitpidStatus) -> Result<(), MemIsolateError> {
let result = if let Some(exit_status) = child_process_exited_on_its_own(waitpid_bespoke_status)
{
debug!("child process exited with status: {}", exit_status);
match exit_status {
CHILD_EXIT_HAPPY => Ok(()),
CHILD_EXIT_IF_READ_CLOSE_FAILED => {
Err(CallableDidNotExecute(ChildPipeCloseFailed(None)))
}
CHILD_EXIT_IF_WRITE_FAILED => Err(CallableExecuted(ChildPipeWriteFailed(None))),
unhandled_status => Err(CallableStatusUnknown(UnexpectedChildExitStatus(
unhandled_status,
))),
}
} else if let Some(signal) = child_process_killed_by_signal(waitpid_bespoke_status) {
Err(CallableStatusUnknown(ChildProcessKilledBySignal(signal)))
} else {
Err(CallableStatusUnknown(UnexpectedWaitpidReturnValue(
waitpid_bespoke_status,
)))
};
if let Ok(()) = result {
debug!("child process exited happily on its own");
} else {
error!("child process signaled an error, propagating {:?}", result);
}
result
}
#[cfg_attr(feature = "tracing", instrument)]
fn deserialize_result<T: DeserializeOwned>(buffer: &[u8]) -> Result<T, MemIsolateError> {
match bincode::deserialize::<Result<T, MemIsolateError>>(buffer) {
Ok(Ok(result)) => {
debug!("successfully deserialized happy result");
Ok(result)
}
Ok(Err(err)) => {
debug!("successfully deserialized error result: {:?}", err);
Err(err)
}
Err(err) => {
let err = CallableExecuted(DeserializationFailed(err.to_string()));
error!("failed to deserialize result, propagating {:?}", err);
Err(err)
}
}
}
#[cfg_attr(feature = "tracing", instrument(skip(result)))]
fn serialize_result_or_error_value<T: Serialize>(result: T) -> Vec<u8> {
match bincode::serialize(&Ok::<T, MemIsolateError>(result)) {
Ok(encoded) => {
debug!(
"serialization successful, resulted in {} bytes",
encoded.len()
);
encoded
}
Err(err) => {
let err = CallableExecuted(SerializationFailed(err.to_string()));
error!(
"serialization failed, now attempting to serialize error: {:?}",
err
);
#[allow(clippy::let_and_return)]
let encoded = bincode::serialize(&Err::<T, MemIsolateError>(err))
.expect("failed to serialize error");
debug!(
"serialization of error successful, resulting in {} bytes",
encoded.len()
);
encoded
}
}
}
#[cfg_attr(feature = "tracing", instrument)]
fn write_and_flush_or_exit<S, W>(sys: &S, writer: &mut W, buffer: &[u8])
where
S: SystemFunctions,
W: Write + Debug,
{
let result = writer.write_all(buffer).and_then(|()| writer.flush());
#[allow(unused_variables)]
if let Err(err) = result {
error!("error writing to pipe: {:?}", err);
let exit_code = CHILD_EXIT_IF_WRITE_FAILED;
debug!("exiting child process with exit code: {}", exit_code);
#[allow(clippy::used_underscore_items)]
sys._exit(exit_code);
} else {
debug!("wrote and flushed to pipe successfully");
}
}
fn exit_happy<S: SystemFunctions>(sys: &S) -> ! {
#[cfg(feature = "tracing")]
let _span = {
const FN_NAME: &str = stringify!(exit_happy);
span!(HIGHEST_LEVEL, FN_NAME).entered()
};
let exit_code = CHILD_EXIT_HAPPY;
debug!("exiting child process with exit code: {}", exit_code);
#[allow(clippy::used_underscore_items)]
sys._exit(exit_code);
}
#[cfg_attr(feature = "tracing", instrument)]
fn read_all_of_child_result_pipe(read_fd: c_int) -> Result<Vec<u8>, MemIsolateError> {
let mut buffer = Vec::new();
{
let mut reader = unsafe { File::from_raw_fd(read_fd) };
if let Err(err) = reader.read_to_end(&mut buffer) {
let err = CallableStatusUnknown(ParentPipeReadFailed(err));
error!("error reading from pipe, propagating {:?}", err);
return Err(err);
}
} debug!("successfully read {} bytes from pipe", buffer.len());
Ok(buffer)
}
#[cfg_attr(feature = "tracing", instrument)]
fn error_if_buffer_is_empty(buffer: &[u8]) -> Result<(), MemIsolateError> {
if buffer.is_empty() {
let err = CallableStatusUnknown(CallableProcessDiedDuringExecution);
error!("buffer unexpectedly empty, propagating {:?}", err);
return Err(err);
}
Ok(())
}
#[cfg_attr(feature = "tracing", instrument)]
fn close_write_end_of_pipe_in_parent<S: SystemFunctions>(
sys: &S,
write_fd: c_int,
) -> Result<(), MemIsolateError> {
if let Err(err) = sys.close(write_fd) {
let err = CallableStatusUnknown(ParentPipeCloseFailed(err));
error!("error closing write end of pipe, propagating {:?}", err);
return Err(err);
}
debug!("write end of pipe closed successfully");
Ok(())
}
#[cfg_attr(feature = "tracing", instrument)]
fn close_read_end_of_pipe_in_child_or_exit<S: SystemFunctions>(
sys: &S,
writer: &mut (impl Write + Debug),
read_fd: c_int,
) {
if let Err(close_err) = sys.close(read_fd) {
let err = CallableDidNotExecute(ChildPipeCloseFailed(Some(close_err)));
error!(
"error closing read end of pipe, now attempting to serialize error: {:?}",
err
);
let encoded = bincode::serialize(&err).expect("failed to serialize error");
writer
.write_all(&encoded)
.expect("failed to write error to pipe");
writer.flush().expect("failed to flush error to pipe");
let exit_code = CHILD_EXIT_IF_READ_CLOSE_FAILED;
error!("exiting child process with exit code: {}", exit_code);
#[allow(clippy::used_underscore_items)]
sys._exit(exit_code);
} else {
debug!("read end of pipe closed successfully");
}
}