FDS/OS 1.0
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
//! 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 start_group(
|
||||
name: &str,
|
||||
arguments: &[String],
|
||||
working: &str,
|
||||
environment: &[(String, String)],
|
||||
) -> 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]);
|
||||
command
|
||||
.args(&arguments[1..])
|
||||
.current_dir(working)
|
||||
.env_clear()
|
||||
.env("PATH", "/usr/bin:/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());
|
||||
// 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 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());
|
||||
}
|
||||
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(())
|
||||
}
|
||||
Reference in New Issue
Block a user