Skip to content

Commit 828d6fe

Browse files
fix: Proper waitpid return value handling (#3)
* fix: Proper handling of bespoke waitpid status * nit: Improve example * test: Add tests covering child pid termination by signal Also prove that suspending and restarting child process results in normal termination by signal afterwords * nit: Update examples/error-handling-complete.rs * chore: Remove leftover lines * chore: Remove completed TODO
1 parent 0b9b650 commit 828d6fe

File tree

7 files changed

+257
-20
lines changed

7 files changed

+257
-20
lines changed

Cargo.lock

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ thiserror = "2.0.11"
1919
[dev-dependencies]
2020
criterion = "0.5.1"
2121
rand = "0.9.0"
22+
tempfile = "3.18.0"
2223

2324
[[bench]]
2425
name = "benchmarks"

examples/error-handling-complete.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
//! This example is meant to show you all of the possible errors that can occur
2+
//! when using `execute_in_isolated_process()`.
3+
//!
4+
//! It doesn't actually do anything meaningful
15
use MemIsolateError::*;
26
use mem_isolate::errors::CallableDidNotExecuteError::*;
37
use mem_isolate::errors::CallableExecutedError::*;
@@ -22,6 +26,8 @@ fn main() {
2226
Err(CallableStatusUnknown(ParentPipeReadFailed(_err))) => {}
2327
Err(CallableStatusUnknown(CallableProcessDiedDuringExecution)) => {}
2428
Err(CallableStatusUnknown(UnexpectedChildExitStatus(_status))) => {}
29+
Err(CallableStatusUnknown(ChildProcessKilledBySignal(_signal))) => {}
30+
Err(CallableStatusUnknown(UnexpectedWaitpidReturnValue(_val))) => {}
2531
};
2632
}
2733

src/c.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,28 @@ impl SystemFunctions for RealSystemFunctions {
8383
}
8484
}
8585

86+
pub type WaitpidStatus = libc::c_int;
87+
pub type ExitStatus = libc::c_int;
88+
pub type Signal = libc::c_int;
89+
90+
#[inline]
91+
pub fn child_process_exited_on_its_own(waitpid_status: WaitpidStatus) -> Option<ExitStatus> {
92+
if libc::WIFEXITED(waitpid_status) {
93+
Some(libc::WEXITSTATUS(waitpid_status))
94+
} else {
95+
None
96+
}
97+
}
98+
99+
#[inline]
100+
pub fn child_process_killed_by_signal(waitpid_status: WaitpidStatus) -> Option<Signal> {
101+
if libc::WIFSIGNALED(waitpid_status) {
102+
Some(libc::WTERMSIG(waitpid_status))
103+
} else {
104+
None
105+
}
106+
}
107+
86108
// For test builds, these functions will be provided by the mock module
87109
#[cfg(test)]
88110
mod tests {

src/errors.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use thiserror::Error;
3636
/// [`MemIsolateError`] is the **primary error type returned by the crate**. The
3737
/// goal is to give the caller context about what happened to their callable if
3838
/// something went wrong.
39+
// TODO: Consider making this Send + Sync
3940
#[derive(Error, Debug, Serialize, Deserialize)]
4041
pub enum MemIsolateError {
4142
/// Indicates something went wrong before the callable was executed. Because
@@ -165,6 +166,15 @@ pub enum CallableStatusUnknownError {
165166
/// indicating the success or failure of the user-supplied callable itself.
166167
#[error("the callable process exited with an unexpected status: {0}")]
167168
UnexpectedChildExitStatus(i32),
169+
170+
/// The child process responsible for executing the user-supplied callable
171+
/// was killed by a signal
172+
#[error("the callable process was killed by a signal: {0}")]
173+
ChildProcessKilledBySignal(i32),
174+
175+
/// Waitpid returned an unexpected value
176+
#[error("waitpid returned an unexpected value: {0}")]
177+
UnexpectedWaitpidReturnValue(i32),
168178
}
169179

170180
fn serialize_os_error<S>(error: &io::Error, serializer: S) -> Result<S::Ok, S::Error>

src/lib.rs

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ use std::os::unix::io::FromRawFd;
1111
mod tests;
1212

1313
mod c;
14-
use c::{ForkReturn, PipeFds, SystemFunctions};
14+
use c::{
15+
ForkReturn, PipeFds, SystemFunctions, child_process_exited_on_its_own,
16+
child_process_killed_by_signal,
17+
};
1518

1619
pub mod errors;
1720
pub use errors::MemIsolateError;
@@ -28,9 +31,6 @@ pub use serde::{Serialize, de::DeserializeOwned};
2831
/// # Safety
2932
/// This code directly calls glibc functions (via the libc crate) and should
3033
/// only be used in a Unix environment.
31-
// TODO: Real error handling with thiserror. Plumbing errors in the child also
32-
// need to be passed back to the parent. TODO: Benchmark nicing the child
33-
// process.
3434
pub fn execute_in_isolated_process<F, T>(callable: F) -> Result<T, MemIsolateError>
3535
where
3636
// TODO: Consider restricting to disallow FnMut() closures
@@ -56,7 +56,6 @@ where
5656
};
5757

5858
// Create a pipe.
59-
// TODO: Should I use dup2 somewhere here?
6059
let PipeFds { read_fd, write_fd } = match sys.pipe() {
6160
Ok(pipe_fds) => pipe_fds,
6261
Err(err) => {
@@ -122,28 +121,34 @@ where
122121
}
123122

124123
// Wait for the child process to exit
125-
// TODO: waitpid doesn't return the exit status you expect it to.
126-
// See the Linux Programming Interface book for more details.
127-
let status = match sys.waitpid(child_pid) {
124+
let waitpid_bespoke_status = match sys.waitpid(child_pid) {
128125
Ok(status) => status,
129126
Err(wait_err) => {
130127
return Err(CallableStatusUnknown(WaitFailed(wait_err)));
131128
}
132129
};
133130

134-
match status {
135-
CHILD_EXIT_HAPPY => {}
136-
CHILD_EXIT_IF_READ_CLOSE_FAILED => {
137-
return Err(CallableDidNotExecute(ChildPipeCloseFailed(None)));
138-
}
139-
CHILD_EXIT_IF_WRITE_FAILED => {
140-
return Err(CallableExecuted(ChildPipeWriteFailed(None)));
141-
}
142-
unhandled_status => {
143-
return Err(CallableStatusUnknown(UnexpectedChildExitStatus(
144-
unhandled_status,
145-
)));
131+
if let Some(exit_status) = child_process_exited_on_its_own(waitpid_bespoke_status) {
132+
match exit_status {
133+
CHILD_EXIT_HAPPY => {}
134+
CHILD_EXIT_IF_READ_CLOSE_FAILED => {
135+
return Err(CallableDidNotExecute(ChildPipeCloseFailed(None)));
136+
}
137+
CHILD_EXIT_IF_WRITE_FAILED => {
138+
return Err(CallableExecuted(ChildPipeWriteFailed(None)));
139+
}
140+
unhandled_status => {
141+
return Err(CallableStatusUnknown(UnexpectedChildExitStatus(
142+
unhandled_status,
143+
)));
144+
}
146145
}
146+
} else if let Some(signal) = child_process_killed_by_signal(waitpid_bespoke_status) {
147+
return Err(CallableStatusUnknown(ChildProcessKilledBySignal(signal)));
148+
} else {
149+
return Err(CallableStatusUnknown(UnexpectedWaitpidReturnValue(
150+
waitpid_bespoke_status,
151+
)));
147152
}
148153

149154
// Read from the pipe by wrapping the read fd as a File

0 commit comments

Comments
 (0)