shrub_core/sys/unix/
commands.rs1use std::ffi::{CStr, CString};
4pub use std::os::unix::process::{CommandExt, ExitStatusExt};
5use std::sync::Arc;
6
7use command_fds::{CommandFdExt, FdMapping};
8
9use crate::options::PidsLimit;
10use crate::{ShellFd, error, openfiles};
11
12const PIDS_CURRENT_BUFFER: usize = 32;
16
17pub trait CommandFdInjectionExt {
19 fn inject_fds(
32 &mut self,
33 open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
34 ) -> Result<(), error::Error>;
35}
36
37impl CommandFdInjectionExt for std::process::Command {
38 fn inject_fds(
39 &mut self,
40 open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
41 ) -> Result<(), error::Error> {
42 let fd_mappings: Vec<FdMapping> = open_files
43 .map(|(child_fd, open_file)| -> Result<FdMapping, error::Error> {
44 let parent_fd = open_file.try_clone_to_owned()?;
45 Ok(FdMapping {
46 child_fd,
47 parent_fd,
48 })
49 })
50 .collect::<Result<Vec<_>, _>>()?;
51
52 self.fd_mappings(fd_mappings)
53 .map_err(|_e| error::ErrorKind::ChildCreationFailure)?;
54
55 Ok(())
56 }
57}
58
59pub trait CommandFgControlExt {
61 fn take_foreground(&mut self);
63 fn lead_session(&mut self);
65}
66
67impl CommandFgControlExt for std::process::Command {
68 fn take_foreground(&mut self) {
69 unsafe {
74 self.pre_exec(pre_exec_take_foreground);
75 }
76 }
77
78 fn lead_session(&mut self) {
79 unsafe {
84 self.pre_exec(pre_exec_lead_session);
85 }
86 }
87}
88
89fn pre_exec_take_foreground() -> Result<(), std::io::Error> {
90 use crate::sys;
91
92 sys::terminal::move_self_to_foreground()?;
93 Ok(())
94}
95
96fn pre_exec_lead_session() -> Result<(), std::io::Error> {
97 if let Err(e) = nix::unistd::setsid() {
98 return Err(std::io::Error::other(format!(
99 "failed to become session leader: {e}"
100 )));
101 }
102
103 #[cfg(not(target_os = "macos"))]
104 let control = libc::TIOCSCTTY;
105 #[cfg(target_os = "macos")]
106 let control: u64 = libc::TIOCSCTTY.into();
107
108 let result = unsafe { libc::ioctl(0, control, 0) };
111 if result != 0 {
112 return Err(std::io::Error::other("failed to set controlling terminal"));
113 }
114
115 Ok(())
116}
117
118pub trait CommandCgroupExt {
120 fn join_cgroup(&mut self, cgroup_procs: Arc<CString>) -> Result<(), error::Error>;
128}
129
130impl CommandCgroupExt for std::process::Command {
131 fn join_cgroup(&mut self, cgroup_procs: Arc<CString>) -> Result<(), error::Error> {
132 unsafe {
138 self.pre_exec(move || pre_exec_join_cgroup(&cgroup_procs));
139 }
140 Ok(())
141 }
142}
143
144fn pre_exec_join_cgroup(cgroup_procs: &CStr) -> Result<(), std::io::Error> {
145 use nix::fcntl::{OFlag, open};
146 use nix::sys::stat::Mode;
147
148 let file = open(
149 cgroup_procs,
150 OFlag::O_WRONLY | OFlag::O_CLOEXEC,
151 Mode::empty(),
152 )?;
153 nix::unistd::write(&file, b"0")?;
154 Ok(())
155}
156
157pub trait CommandOpenFileLimitExt {
159 fn set_open_file_limit(&mut self, limit: u64) -> Result<(), error::Error>;
167}
168
169impl CommandOpenFileLimitExt for std::process::Command {
170 fn set_open_file_limit(&mut self, limit: u64) -> Result<(), error::Error> {
171 unsafe {
177 self.pre_exec(move || pre_exec_set_open_file_limit(limit));
178 }
179 Ok(())
180 }
181}
182
183#[cfg(target_os = "macos")]
187const MACOS_OPEN_MAX: libc::rlim_t = 10240;
188
189fn pre_exec_set_open_file_limit(limit: u64) -> Result<(), std::io::Error> {
190 use nix::sys::resource::{Resource, getrlimit, setrlimit};
191
192 let (_, hard) = getrlimit(Resource::RLIMIT_NOFILE)?;
193 #[allow(
196 clippy::unnecessary_fallible_conversions,
197 reason = "rlim_t is not u64 on every Unix"
198 )]
199 let soft = libc::rlim_t::try_from(limit).unwrap_or(hard).min(hard);
200 let result = setrlimit(Resource::RLIMIT_NOFILE, soft, hard);
201 #[cfg(target_os = "macos")]
202 let result = match result {
203 Err(nix::errno::Errno::EINVAL) => {
204 setrlimit(Resource::RLIMIT_NOFILE, soft.min(MACOS_OPEN_MAX), hard)
205 }
206 other => other,
207 };
208 Ok(result?)
209}
210
211pub trait CommandPidsLimitsExt {
213 fn check_pids_limits(&mut self, limits: Arc<[PidsLimit]>) -> Result<(), error::Error>;
228}
229
230impl CommandPidsLimitsExt for std::process::Command {
231 fn check_pids_limits(&mut self, limits: Arc<[PidsLimit]>) -> Result<(), error::Error> {
232 let shell_pid = nix::unistd::getpid();
235 unsafe {
243 self.pre_exec(move || pre_exec_check_pids_limits(&limits, shell_pid));
244 }
245 Ok(())
246 }
247}
248
249fn pre_exec_check_pids_limits(
250 limits: &[PidsLimit],
251 shell_pid: nix::unistd::Pid,
252) -> Result<(), std::io::Error> {
253 if nix::unistd::getpid() == shell_pid {
254 return Err(std::io::Error::from_raw_os_error(libc::ENOTSUP));
255 }
256 limits.iter().try_for_each(check_pids_limit)
257}
258
259fn check_pids_limit(limit: &PidsLimit) -> Result<(), std::io::Error> {
263 use std::os::unix::fs::FileExt as _;
264
265 let mut buffer = [0u8; PIDS_CURRENT_BUFFER];
266 let count = match limit.current.read_at(&mut buffer, 0) {
267 Ok(len) if len < buffer.len() => buffer.get(..len).and_then(parse_count),
268 _ => None,
269 };
270 match count {
271 Some(count) if count <= limit.max => Ok(()),
272 Some(_) => Err(std::io::Error::from_raw_os_error(libc::EAGAIN)),
273 None => Err(std::io::Error::from_raw_os_error(libc::EIO)),
274 }
275}
276
277fn parse_count(bytes: &[u8]) -> Option<u64> {
280 let digits = bytes.strip_suffix(b"\n").unwrap_or(bytes);
281 if digits.is_empty() {
282 return None;
283 }
284 digits.iter().try_fold(0u64, |count, byte| {
285 let digit = byte.checked_sub(b'0').filter(|digit| *digit <= 9)?;
286 count.checked_mul(10)?.checked_add(u64::from(digit))
287 })
288}
289
290pub(crate) fn is_missing(path: &CStr) -> bool {
293 use std::os::unix::ffi::OsStrExt as _;
294
295 !std::path::Path::new(std::ffi::OsStr::from_bytes(path.to_bytes())).exists()
296}
297
298#[cfg(test)]
299mod tests {
300 use proptest::prelude::*;
301
302 use super::parse_count;
303
304 proptest! {
305 #[test]
306 fn a_count_parses_with_or_without_its_newline(count: u64) {
307 prop_assert_eq!(parse_count(count.to_string().as_bytes()), Some(count));
308 prop_assert_eq!(parse_count(format!("{count}\n").as_bytes()), Some(count));
309 }
310
311 #[test]
312 fn anything_parsed_is_the_decimal_it_reads(bytes: Vec<u8>) {
313 if let Some(count) = parse_count(&bytes) {
314 let digits = bytes.strip_suffix(b"\n").unwrap_or(&bytes);
315 prop_assert!(digits.iter().all(u8::is_ascii_digit));
316 let text = std::str::from_utf8(digits).map_err(|e| TestCaseError::fail(e.to_string()))?;
317 prop_assert_eq!(text.parse::<u64>().ok(), Some(count));
318 }
319 }
320
321 #[test]
322 fn a_count_with_anything_else_does_not_parse(
323 count: u64,
324 junk in "[^0-9]{1,4}",
325 ) {
326 prop_assume!(junk != "\n");
327 let text = format!("{count}{junk}");
328 prop_assert_eq!(parse_count(text.as_bytes()), None);
329 }
330 }
331
332 #[test]
333 fn empty_and_overflowing_counts_do_not_parse() {
334 assert_eq!(parse_count(b""), None);
335 assert_eq!(parse_count(b"\n"), None);
336 assert_eq!(parse_count(b"18446744073709551616"), None);
337 assert_eq!(parse_count(b"max\n"), None);
338 }
339}