//! Managed jobs enter a root-owned cgroup before losing privileges or executing. //! Descendants inherit membership; they cannot escape by double-forking. use crate::media::{c, checked}; use fds_common::{Bay, Error, Result, read_text}; use std::{ fs::{self, File, OpenOptions}, io::{self, Read, Seek, SeekFrom}, os::{ fd::{AsRawFd, FromRawFd, OwnedFd}, unix::{ fs::{OpenOptionsExt, PermissionsExt}, process::CommandExt, }, }, path::{Path, PathBuf}, process::{Child, Command, Stdio}, time::{Duration, Instant}, }; const ROOT: &str = "/sys/fs/cgroup/fds"; pub fn prepare() -> Result<()> { let mut stat: libc::statfs = unsafe { std::mem::zeroed() }; checked( unsafe { libc::statfs(c("/sys/fs/cgroup")?.as_ptr(), &mut stat) }, "inspect cgroup filesystem", )?; if stat.f_type as u64 == 0x6265_6572 { checked( unsafe { libc::mount( c("none")?.as_ptr(), c("/sys/fs/cgroup")?.as_ptr(), c("cgroup2")?.as_ptr(), libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC, std::ptr::null(), ) }, "mount managed process hierarchy", )?; } else if stat.f_type as u64 != 0x6367_7270 { return Err(Error( "Managed programs require the cgroup v2 filesystem".into(), )); } fs::create_dir_all(ROOT)?; fs::set_permissions(ROOT, fs::Permissions::from_mode(0o755))?; fs::write("/sys/fs/cgroup/cgroup.subtree_control", b"+pids")?; fs::write(Path::new(ROOT).join("cgroup.subtree_control"), b"+pids")?; Ok(()) } fn directory(name: &str) -> PathBuf { Path::new(ROOT).join(name) } pub fn enter_service(name: &str) -> Result<()> { if unsafe { libc::geteuid() } != 0 || !fds_common::manifest::identifier(name) { return Err(Error("Invalid privileged service group".into())); } let path = directory(name); fs::create_dir_all(&path)?; fs::write(path.join("pids.max"), b"256")?; fs::write(path.join("cgroup.procs"), b"0")?; Ok(()) } pub fn start( bay: Bay, arguments: &[String], working: &str, environment: &[(String, String)], ) -> Result { start_group(&format!("bay{bay}"), arguments, working, environment) } pub fn foreground( bay: Bay, arguments: &[String], working: &str, environment: &[(String, String)], descriptors: [OwnedFd; 3], terminal: bool, ) -> Result { spawn( &format!("bay{bay}"), arguments, working, environment, Some((descriptors, terminal)), ) } pub fn start_group( name: &str, arguments: &[String], working: &str, environment: &[(String, String)], ) -> Result { spawn(name, arguments, working, environment, None) } fn spawn( name: &str, arguments: &[String], working: &str, environment: &[(String, String)], io: Option<([OwnedFd; 3], bool)>, ) -> Result { if !fds_common::manifest::identifier(name) { return Err(Error("Invalid process group".into())); } if arguments.is_empty() || !arguments[0].starts_with('/') || arguments.len() > 128 || arguments.iter().any(|a| a.contains('\0')) { return Err(Error( "Managed run requires an absolute executable path and at most 128 arguments".into(), )); } let path = directory(name); fs::create_dir_all(&path)?; fs::write(path.join("pids.max"), b"256")?; let group = OpenOptions::new() .write(true) .custom_flags(libc::O_CLOEXEC) .open(path.join("cgroup.procs"))?; let mut command = Command::new(&arguments[0]); let working = c(working)?; command .args(&arguments[1..]) .env_clear() .env("PATH", "/usr/bin:/bin:/run/fds/bin") .env("HOME", "/home/fds") .env("USER", "fds") .env("LOGNAME", "fds") .env("LANG", "en_US.UTF-8") .env("SHELL", "/bin/bash") .stdin(Stdio::null()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()); command.envs(environment.iter().cloned()); let mut terminal = false; if let Some(([input, output, errors], tty)) = io { command .stdin(Stdio::from(input)) .stdout(Stdio::from(output)) .stderr(Stdio::from(errors)); terminal = tty; } // Only async-signal-safe syscalls are used in the forked child. Writing 0 // moves the child itself, avoiding PID reuse and parent/child migration races. unsafe { command.pre_exec(move || { if libc::write(group.as_raw_fd(), b"0".as_ptr().cast(), 1) != 1 { return Err(io::Error::last_os_error()); } if libc::setsid() < 0 { return Err(io::Error::last_os_error()); } if terminal && libc::ioctl(0, libc::TIOCSCTTY, 0) < 0 { return Err(io::Error::last_os_error()); } if libc::setgroups(0, std::ptr::null()) < 0 || libc::setgid(1000) < 0 || libc::setuid(1000) < 0 { return Err(io::Error::last_os_error()); } if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0 { return Err(io::Error::last_os_error()); } // Resolve client-selected working directories only as the user. if libc::chdir(working.as_ptr()) < 0 { return Err(io::Error::last_os_error()); } let mut mask: libc::sigset_t = std::mem::zeroed(); libc::sigemptyset(&mut mask); if libc::sigprocmask(libc::SIG_SETMASK, &mask, std::ptr::null_mut()) < 0 { return Err(io::Error::last_os_error()); } Ok(()) }); } command.spawn().map_err(Into::into) } pub fn count(bay: Bay) -> Result { let path = directory(&format!("bay{bay}")).join("cgroup.procs"); if !path.exists() { return Ok(0); } Ok(read_text(&path, 1024 * 1024)?.lines().count()) } fn populated(events: &mut File) -> Result { events.seek(SeekFrom::Start(0))?; let mut text = String::new(); events.take(4096).read_to_string(&mut text)?; if text.lines().any(|s| s == "populated 0") { Ok(false) } else if text.lines().any(|s| s == "populated 1") { Ok(true) } else { Err(Error("Invalid cgroup event state".into())) } } fn wait_empty(events: &mut File, limit: Duration) -> Result { let deadline = Instant::now() + limit; loop { if !populated(events)? { return Ok(true); } let remaining = deadline.saturating_duration_since(Instant::now()); if remaining.is_zero() { return Ok(false); } let mut descriptor = libc::pollfd { fd: events.as_raw_fd(), events: libc::POLLPRI | libc::POLLERR, revents: 0, }; let result = unsafe { libc::poll( &mut descriptor, 1, remaining.as_millis().max(1).min(i32::MAX as u128) as i32, ) }; if result < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted { continue; } checked(result, "wait for cartridge consumers")?; } } pub fn stop(bay: Bay) -> Result<()> { stop_group(&format!("bay{bay}")) } pub fn stop_group(name: &str) -> Result<()> { if !fds_common::manifest::identifier(name) { return Err(Error("Invalid process group".into())); } let path = directory(name); if !path.exists() { return Ok(()); } let mut events = File::open(path.join("cgroup.events"))?; if !populated(&mut events)? { return Ok(()); } let membership = format!("0::/fds/{name}"); for value in read_text(&path.join("cgroup.procs"), 1024 * 1024)?.lines() { let pid: i32 = value .parse() .map_err(|_| Error("Invalid consumer process ID".into()))?; let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) } as libc::c_int; if fd < 0 { if io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) { continue; } checked(fd, "open consumer process handle")?; } let fd = unsafe { OwnedFd::from_raw_fd(fd) }; // Verify membership after opening the stable handle. A recycled PID in // another cgroup must never receive a signal intended for a consumer. let current = match read_text( &Path::new("/proc").join(pid.to_string()).join("cgroup"), 16384, ) { Ok(s) => s, Err(_) => continue, }; if current.lines().any(|s| s == membership) { let sent = unsafe { libc::syscall( libc::SYS_pidfd_send_signal, fd.as_raw_fd(), libc::SIGTERM, std::ptr::null::(), 0, ) } as libc::c_int; if sent < 0 && io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH) { checked(sent, "stop cartridge consumer")?; } } } if !wait_empty(&mut events, Duration::from_secs(1))? { // The kernel kills the entire cgroup atomically, including forks made // after the TERM snapshot. This is an exit deadline, not a fixed wait. fs::write(path.join("cgroup.kill"), b"1")?; if !wait_empty(&mut events, Duration::from_secs(2))? { return Err(Error( "Cartridge consumers did not exit; media remains mounted".into(), )); } } Ok(()) } pub fn stop_all() -> Result<()> { for n in 1..=12 { stop(Bay::try_from(n)?)?; } Ok(()) }