Files
fds-os/rust/fds-cartridged/src/consumers.rs
T
2026-09-22 13:23:34 +08:00

298 lines
9.9 KiB
Rust

//! 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<Child> {
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<Child> {
spawn(
&format!("bay{bay}"),
arguments,
working,
environment,
Some((descriptors, terminal)),
)
}
pub fn start_group(
name: &str,
arguments: &[String],
working: &str,
environment: &[(String, String)],
) -> Result<Child> {
spawn(name, arguments, working, environment, None)
}
fn spawn(
name: &str,
arguments: &[String],
working: &str,
environment: &[(String, String)],
io: Option<([OwnedFd; 3], bool)>,
) -> Result<Child> {
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<usize> {
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<bool> {
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<bool> {
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::<libc::siginfo_t>(),
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(())
}