lean_ctx/shell/containment/
gate.rs1use std::io::Read as _;
15use std::os::fd::{AsRawFd as _, RawFd};
16use std::os::unix::process::CommandExt as _;
17use std::process::{Child, Command};
18use std::thread::ScopedJoinHandle;
19
20use super::CallGuard;
21
22pub enum Refusal {
24 NotStarted(std::io::Error),
26 NotLeader,
28 NoGuard(String),
30}
31
32pub fn start_behind_guard(command: &mut Command) -> Result<(Child, CallGuard), Refusal> {
39 let (gate, open) = std::io::pipe().map_err(Refusal::NotStarted)?;
40 let (mut announced, announce) = std::io::pipe().map_err(Refusal::NotStarted)?;
41 let (gate_fd, open_fd, announce_fd) =
42 (gate.as_raw_fd(), open.as_raw_fd(), announce.as_raw_fd());
43 unsafe {
46 command.pre_exec(move || wait_at_gate(gate_fd, open_fd, announce_fd));
47 }
48 std::thread::scope(|scope| {
53 let spawning = scope.spawn(move || {
54 let spawned = command.spawn();
55 drop((gate, announce));
56 spawned
57 });
58 let mut pid = [0_u8; size_of::<libc::pid_t>()];
59 if announced.read_exact(&mut pid).is_err() {
60 drop(open);
62 return Err(Refusal::NotStarted(match finish(spawning) {
63 Err(error) => error,
64 Ok(_) => std::io::Error::other("the process did not wait for its guard"),
65 }));
66 }
67 let pid = libc::pid_t::from_ne_bytes(pid);
68 #[cfg(any(test, feature = "test-support"))]
69 pause_before_guard(pid);
70 let leads = unsafe { libc::getpgid(pid) } == pid;
72 let guard = if leads {
73 attach_guard(pid).map_err(Refusal::NoGuard)
74 } else {
75 Err(Refusal::NotLeader)
76 };
77 let guard = match guard {
78 Ok(guard) => guard,
79 Err(refusal) => {
80 drop(open);
82 let _ = finish(spawning);
83 return Err(refusal);
84 }
85 };
86 let opened = super::write_fd(open.as_raw_fd(), &[1]);
87 drop(open);
88 match finish(spawning) {
89 Ok(child) if opened => Ok((child, guard)),
90 Ok(_) => Err(Refusal::NotStarted(std::io::Error::other(
92 "the gate could not be opened",
93 ))),
94 Err(error) => Err(Refusal::NotStarted(error)),
95 }
96 })
97}
98
99fn finish(spawning: ScopedJoinHandle<'_, std::io::Result<Child>>) -> std::io::Result<Child> {
100 spawning
101 .join()
102 .unwrap_or_else(|_| Err(std::io::Error::other("the start did not finish")))
103}
104
105#[cfg(not(any(test, feature = "test-support")))]
107fn attach_guard(process: libc::pid_t) -> Result<CallGuard, String> {
108 CallGuard::attach_to(process, process)
109}
110
111#[cfg(any(test, feature = "test-support"))]
113fn attach_guard(process: libc::pid_t) -> Result<CallGuard, String> {
114 let nowhere = super::tests::NO_SUCH_GROUP;
115 match GUARD_FAULT.with(std::cell::Cell::get) {
116 None => CallGuard::attach_to(process, process),
117 Some(GuardFault::CannotJoin) => CallGuard::attach_to(nowhere, nowhere),
118 Some(GuardFault::WrongGroup) => CallGuard::attach_to(process, nowhere),
119 }
120}
121
122#[cfg(any(test, feature = "test-support"))]
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum GuardFault {
126 CannotJoin,
128 WrongGroup,
131}
132
133#[cfg(any(test, feature = "test-support"))]
134thread_local! {
135 static GUARD_FAULT: std::cell::Cell<Option<GuardFault>> = const { std::cell::Cell::new(None) };
136 static LAST_GATED: std::cell::Cell<libc::pid_t> = const { std::cell::Cell::new(0) };
137}
138
139#[cfg(any(test, feature = "test-support"))]
142pub fn set_guard_fault_for_test(fault: Option<GuardFault>) {
143 GUARD_FAULT.with(|current| current.set(fault));
144}
145
146#[cfg(any(test, feature = "test-support"))]
148pub(crate) fn last_gated_for_test() -> libc::pid_t {
149 LAST_GATED.with(std::cell::Cell::get)
150}
151
152#[cfg(any(test, feature = "test-support"))]
157fn pause_before_guard(process: libc::pid_t) {
158 LAST_GATED.with(|last| last.set(process));
159 if std::env::var_os(super::tests::PAUSE_ENV).is_some() {
160 let _ = super::write_fd(
161 libc::STDOUT_FILENO,
162 format!("FORKED {process}\n").as_bytes(),
163 );
164 std::thread::sleep(std::time::Duration::from_mins(1));
165 }
166}
167
168fn wait_at_gate(gate: RawFd, open: RawFd, announce: RawFd) -> std::io::Result<()> {
171 unsafe {
175 libc::close(open);
176 let pid = libc::getpid().to_ne_bytes();
177 let told = libc::write(announce, pid.as_ptr().cast(), pid.len());
178 libc::close(announce);
179 if told != pid.len() as isize {
180 return Err(std::io::Error::from_raw_os_error(libc::EPIPE));
181 }
182 let mut byte = 0_u8;
183 loop {
184 match libc::read(gate, (&raw mut byte).cast(), 1) {
185 1 => return Ok(()),
186 0 => return Err(std::io::Error::from_raw_os_error(libc::ECANCELED)),
187 _ => {
188 let error = std::io::Error::last_os_error();
189 if error.raw_os_error() != Some(libc::EINTR) {
190 return Err(error);
191 }
192 }
193 }
194 }
195 }
196}