298 lines
9.9 KiB
Rust
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(())
|
|
}
|