lean_ctx/shell/containment/
guard.rs1use std::io::Read as _;
19use std::os::unix::process::CommandExt as _;
20use std::process::{Child, ChildStdout, Command, Stdio};
21use std::time::Duration;
22
23use super::{END_BUDGET, EXIT_CONTAINMENT, EXIT_USAGE, GroupEnd};
24
25const READY: &[u8] = b"leanctx-group-guard ready\n";
27const EXIT_UNCONFIRMED: i32 = 2;
29const EXIT_SURVIVORS: i32 = 3;
31const READY_BUDGET: Duration = Duration::from_secs(5);
33const REAP_BUDGET: Duration = Duration::from_secs(2);
35
36pub(super) fn main(mut args: impl Iterator<Item = std::ffi::OsString>) -> i32 {
38 let group = match (args.next(), args.next(), args.next()) {
39 (Some(flag), Some(value), None) if flag == "--group" => value
40 .to_str()
41 .and_then(|value| value.parse::<libc::pid_t>().ok()),
42 _ => None,
43 };
44 group.filter(|group| *group > 1).map_or(EXIT_USAGE, run)
45}
46
47pub(super) fn run(expected: libc::pid_t) -> i32 {
49 if unsafe { libc::getpgrp() } != expected {
51 return EXIT_CONTAINMENT;
52 }
53 super::set_termination_signals(libc::SIG_IGN);
56 let pin = unsafe { libc::fork() };
58 if pin == 0 {
59 unsafe { libc::_exit(0) };
61 }
62 if pin < 0 {
63 return EXIT_CONTAINMENT;
64 }
65 let left = unsafe { libc::setpgid(0, 0) } == 0 && unsafe { libc::getpgrp() } != expected;
68 if !left {
69 reap_pin(pin);
70 return EXIT_CONTAINMENT;
71 }
72 let _ = super::write_fd(libc::STDOUT_FILENO, READY);
74 wait_for_end_of_input();
75 let end = super::end_pinned_group(expected, END_BUDGET);
76 reap_pin(pin);
77 match end {
78 GroupEnd::Empty => 0,
79 GroupEnd::Unconfirmed => EXIT_UNCONFIRMED,
80 GroupEnd::Survivors => EXIT_SURVIVORS,
81 GroupEnd::Refused => EXIT_CONTAINMENT,
82 }
83}
84
85fn wait_for_end_of_input() {
88 let mut buffer = [0_u8; 64];
89 loop {
90 let read =
92 unsafe { libc::read(libc::STDIN_FILENO, buffer.as_mut_ptr().cast(), buffer.len()) };
93 if read == 0 {
94 return;
95 }
96 if read < 0 && std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted {
97 return;
98 }
99 }
100}
101
102fn reap_pin(pin: libc::pid_t) {
103 let mut status: libc::c_int = 0;
104 unsafe { libc::waitpid(pin, &raw mut status, 0) };
106}
107
108fn guard_command(expected: libc::pid_t) -> std::io::Result<Command> {
110 #[cfg(any(test, feature = "test-support"))]
111 if super::harness_helper() {
112 return harness_guard_command(expected);
113 }
114 let mut command = Command::new(std::env::current_exe()?);
115 command.args([super::GUARD_MODE, "--group", &expected.to_string()]);
116 Ok(command)
117}
118
119#[cfg(any(test, feature = "test-support"))]
122fn harness_guard_command(expected: libc::pid_t) -> std::io::Result<Command> {
123 let mut command = Command::new(std::env::current_exe()?);
124 command
125 .args([
126 super::tests::HELPER_TEST,
127 "--exact",
128 "--ignored",
129 "--nocapture",
130 ])
131 .env(super::tests::HELPER_ENV, "group-guard")
132 .env(super::tests::GROUP_ENV, expected.to_string());
133 Ok(command)
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum GuardOutcome {
139 Ended,
141 Unconfirmed,
143 Failed,
145}
146
147pub struct CallGuard {
150 guard: Child,
151 hold: Option<std::io::PipeWriter>,
152}
153
154impl CallGuard {
155 pub(super) fn attach_to(join: libc::pid_t, expected: libc::pid_t) -> Result<Self, String> {
160 let (reader, writer) = std::io::pipe().map_err(|error| error.to_string())?;
161 let mut command = guard_command(expected).map_err(|error| error.to_string())?;
162 command
163 .stdin(reader)
164 .stdout(Stdio::piped())
165 .stderr(Stdio::null())
166 .process_group(join);
167 let mut guard = command
168 .spawn()
169 .map_err(|error| format!("the guard could not join the group: {error}"))?;
170 drop(command);
172 let ready = guard
173 .stdout
174 .take()
175 .is_some_and(|output| ready_within(output, READY_BUDGET));
176 if !ready {
177 drop(writer);
180 let _ = guard.kill();
181 let _ = crate::shell::exec::reap_within(&mut guard, REAP_BUDGET);
182 return Err("the guard did not become ready".to_string());
183 }
184 Ok(Self {
185 guard,
186 hold: Some(writer),
187 })
188 }
189
190 pub fn finish(mut self) -> GuardOutcome {
192 drop(self.hold.take());
193 let outcome = if let Some(status) =
194 crate::shell::exec::reap_within(&mut self.guard, END_BUDGET + REAP_BUDGET)
195 {
196 match status.code() {
197 Some(0) => GuardOutcome::Ended,
198 Some(EXIT_UNCONFIRMED) => GuardOutcome::Unconfirmed,
199 _ => GuardOutcome::Failed,
200 }
201 } else {
202 let _ = self.guard.kill();
203 let _ = crate::shell::exec::reap_within(&mut self.guard, REAP_BUDGET);
204 GuardOutcome::Failed
205 };
206 #[cfg(any(test, feature = "test-support"))]
209 if let Some(forced) = FORCED_OUTCOME.with(std::cell::Cell::get) {
210 return forced;
211 }
212 outcome
213 }
214}
215
216impl Drop for CallGuard {
220 fn drop(&mut self) {
221 if self.hold.take().is_some()
222 && crate::shell::exec::reap_within(&mut self.guard, END_BUDGET + REAP_BUDGET).is_none()
223 {
224 let _ = self.guard.kill();
225 let _ = crate::shell::exec::reap_within(&mut self.guard, REAP_BUDGET);
226 }
227 }
228}
229
230#[cfg(any(test, feature = "test-support"))]
231thread_local! {
232 static FORCED_OUTCOME: std::cell::Cell<Option<GuardOutcome>> =
233 const { std::cell::Cell::new(None) };
234}
235
236#[cfg(any(test, feature = "test-support"))]
239pub fn force_outcome_for_test(outcome: Option<GuardOutcome>) {
240 FORCED_OUTCOME.with(|forced| forced.set(outcome));
241}
242
243fn ready_within(mut output: ChildStdout, budget: Duration) -> bool {
245 const MAX_PREAMBLE: usize = 8192;
246 let (sender, receiver) = std::sync::mpsc::channel();
247 std::thread::spawn(move || {
248 let mut seen = Vec::new();
249 let mut chunk = [0_u8; 256];
250 let found = loop {
251 match output.read(&mut chunk) {
252 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
253 Ok(0) | Err(_) => break false,
254 Ok(read) => {
255 seen.extend_from_slice(&chunk[..read]);
256 if seen.windows(READY.len()).any(|window| window == READY) {
257 break true;
258 }
259 if seen.len() > MAX_PREAMBLE {
260 break false;
261 }
262 }
263 }
264 };
265 let _ = sender.send(found);
266 });
267 receiver.recv_timeout(budget).unwrap_or(false)
268}