Skip to content

Commit 5a54606

Browse files
docs: Add examples illustrating how to block and restore signals around mem-isolate calls (#45)
* example: Add example illustrating how to block and restore signals during mem-isolate call * example: Re-work signal blocking example to be more demonstrative of real world signal handling * example: Split blocking-signals example in two * example: Improve example comments * example: Improve inline docs
1 parent 7bd6feb commit 5a54606

File tree

4 files changed

+192
-0
lines changed

4 files changed

+192
-0
lines changed

Cargo.lock

Lines changed: 19 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
@@ -31,6 +31,7 @@ tracing = { version = "0.1.41", optional = true }
3131
[dev-dependencies]
3232
criterion = "0.5.1"
3333
ctor = "0.4.1"
34+
nix = { version = "0.29.0", features = ["signal"] }
3435
rand = "0.9.0"
3536
tempfile = "3.18.0"
3637
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//! This example demonstrates how to block signals in a parent process while
2+
//! executing code in an isolated process.
3+
//!
4+
//! This is useful if:
5+
//!
6+
//! 1. You want to simulate the expected behavior of running your code without
7+
//! `mem_isolate::execute_in_isolated_process()`, which would be guaranteed
8+
//! to treat signals the same both inside and outside of the `callable()`
9+
//! function.
10+
//! 2. You want to prevent either process from being interrupted by signals
11+
//! while your `callable()` is running.
12+
//! 3. You want to ensure that the parent process is not killed while the
13+
//! isolated process is running, leaving an orphaned child process.
14+
//!
15+
//! It is important to remember that `mem_isolate::execute_in_isolated_process()`
16+
//! uses `fork()` under the hood, which creates a child process that will not
17+
//! receive signals sent to the main process as your `callable()` would otherwise.
18+
//!
19+
//! Run this example with `cargo run --example blocking-signals-demonstration`
20+
//!
21+
//! This example is great for illustrating how signal blocking works, but if you
22+
//! want to just copy and paste some code, see `blocking-signals-minimal.rs`
23+
//!
24+
//! NOTE: Because both SIGKILL and SIGSTOP are unblockable, nothing can be done
25+
//! to prevent them from killing the parent process or the child process.
26+
//!
27+
//! WARNING: Do not expect signal handling to work as you might think in a
28+
//! multi-threaded program. It is not recommended to use `mem_isolate` in a
29+
//! multi-threaded program anyway, so that's generally OK.
30+
31+
use mem_isolate::{MemIsolateError, execute_in_isolated_process};
32+
use nix::errno::Errno;
33+
use nix::sys::signal::{SigSet, SigmaskHow, sigprocmask};
34+
use nix::unistd::Pid;
35+
use std::process::{Child, Command, Stdio};
36+
use std::thread;
37+
use std::time::Duration;
38+
39+
fn main() -> Result<(), MemIsolateError> {
40+
let parent_pid = Pid::this();
41+
println!("Parent PID: {}", parent_pid);
42+
let wait_time_for_readability = Duration::from_secs(5);
43+
44+
// Get closures for blocking and restoring signals
45+
let (block_signals, restore_signals) = get_block_and_restore_signal_closures();
46+
47+
// Block all signals before calling `mem_isolate::execute_in_isolated_process()`
48+
// This ensures the main program won't be killed leaving an orphaned child process
49+
println!("Parent: Blocking all signals");
50+
block_signals().expect("Failed to block signals");
51+
52+
// Kick-off a subprocess that will send a SIGTERM to this process
53+
let sigterm_sender_proc = send_sigterm_to_parent(Duration::from_secs(1));
54+
55+
// Run your code in an isolated process. NOTE: The child process created by
56+
// `fork()` inside `execute_in_isolated_process()` will inherit the signal
57+
// mask set by main process just above.
58+
let result = execute_in_isolated_process(move || {
59+
println!(
60+
"\nChild: I've started executing a user-defined callable. I'll wait for {} seconds before exiting...",
61+
wait_time_for_readability.as_secs()
62+
);
63+
thread::sleep(wait_time_for_readability);
64+
println!("Child: I'm all done now, exiting\n");
65+
});
66+
67+
reap_child_process_so_it_doesnt_become_a_zombie(sigterm_sender_proc);
68+
69+
println!(
70+
"Parent: Notice that the SIGTERM is pending and didn't interrupt this process or the child process. Unblocking signals in {} seconds...",
71+
wait_time_for_readability.as_secs()
72+
);
73+
thread::sleep(wait_time_for_readability);
74+
println!("Parent: Restoring signals, expect the parent to now recieve the SIGTERM");
75+
restore_signals().expect("Failed to restore signals");
76+
// WARNING: Don't expect code to ever reach this point, because the pending SIGTERM will kill the parent process
77+
// as soon as we unblock the SIGTERM that has been pending this whole time
78+
println!("Parent: Notice how I never ran");
79+
result
80+
}
81+
82+
fn get_block_and_restore_signal_closures() -> (
83+
impl FnOnce() -> Result<(), Errno>,
84+
impl FnOnce() -> Result<(), Errno>,
85+
) {
86+
let all_signals = SigSet::all();
87+
let mut old_signals = SigSet::empty();
88+
89+
let block_signals = move || {
90+
sigprocmask(
91+
SigmaskHow::SIG_SETMASK,
92+
Some(&all_signals),
93+
Some(&mut old_signals),
94+
)
95+
};
96+
97+
let restore_signals = move || sigprocmask(SigmaskHow::SIG_SETMASK, Some(&old_signals), None);
98+
99+
(block_signals, restore_signals)
100+
}
101+
102+
fn send_sigterm_to_parent(wait_time: Duration) -> Child {
103+
// We do this via a subprocess instead of a thread because the latter will
104+
// break the signal mask that we have set. `mem_isolate::execute_in_isolated_process()`
105+
// also SHOULD NOT be used in a multi-threaded program (see limitations in README)
106+
println!(
107+
"Parent: Sending SIGTERM to self in {} seconds. NOTE: This signal will be sent to the parent while the child is executing the user-defined callable",
108+
wait_time.as_secs()
109+
);
110+
Command::new("sh")
111+
.arg("-c")
112+
.arg(format!(
113+
"sleep {} && kill -s TERM {}",
114+
wait_time.as_secs(),
115+
Pid::this()
116+
))
117+
.stdout(Stdio::null())
118+
.stderr(Stdio::null())
119+
.spawn()
120+
.expect("Failed to spawn delayed kill command")
121+
}
122+
123+
fn reap_child_process_so_it_doesnt_become_a_zombie(mut child: Child) {
124+
let exit_status = child.wait().expect("Failed to wait for child process");
125+
if !exit_status.success() {
126+
panic!("Other process: failed to send SIGTERM to parent process");
127+
}
128+
}

examples/blocking-signals-minimal.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//! See `blocking-signals-demonstration.rs` for a more detailed example of
2+
//! how signal blocking works at runtime, and why you might want to do it.
3+
//!
4+
//! Use this example for copy + paste code snippets.
5+
6+
use mem_isolate::{MemIsolateError, execute_in_isolated_process};
7+
use nix::errno::Errno;
8+
use nix::sys::signal::{SigSet, SigmaskHow, sigprocmask};
9+
10+
fn main() -> Result<(), MemIsolateError> {
11+
// Block all signals right before calling `mem_isolate::execute_in_isolated_process()`
12+
// This ensures the main program won't be killed leaving an orphaned child process
13+
let (block_signals, restore_signals) = get_block_and_restore_signal_closures();
14+
block_signals().expect("Failed to block signals");
15+
16+
// Run your code in an isolated process. NOTE: The child process created by
17+
// `fork()` inside `execute_in_isolated_process()` will inherit the signal
18+
// mask set by main process just above.
19+
let result = execute_in_isolated_process(|| ());
20+
21+
// Restore the signal mask, unblocking all signals
22+
restore_signals().expect("Failed to restore signals");
23+
result
24+
}
25+
26+
fn get_block_and_restore_signal_closures() -> (
27+
impl FnOnce() -> Result<(), Errno>,
28+
impl FnOnce() -> Result<(), Errno>,
29+
) {
30+
let all_signals = SigSet::all();
31+
let mut old_signals = SigSet::empty();
32+
33+
let block_signals = move || {
34+
sigprocmask(
35+
SigmaskHow::SIG_SETMASK,
36+
Some(&all_signals),
37+
Some(&mut old_signals),
38+
)
39+
};
40+
41+
let restore_signals = move || sigprocmask(SigmaskHow::SIG_SETMASK, Some(&old_signals), None);
42+
43+
(block_signals, restore_signals)
44+
}

0 commit comments

Comments
 (0)