| 1 | use std::{ |
| 2 | env, io, |
| 3 | os::fd::{AsRawFd, FromRawFd, OwnedFd}, |
| 4 | os::unix::process::CommandExt, |
| 5 | process::{Child, Command}, |
| 6 | time::Duration, |
| 7 | }; |
| 8 | |
| 9 | use futures_lite::StreamExt; |
| 10 | use zbus::{ |
| 11 | blocking::{Connection, Proxy}, |
| 12 | proxy::SignalStream, |
| 13 | zvariant::{OwnedObjectPath, Value}, |
| 14 | }; |
| 15 | |
| 16 | use crate::{ |
| 17 | config::{Overrun, RamOverrun}, |
| 18 | units::Bytes, |
| 19 | }; |
| 20 | |
| 21 | const EXEC: &str = "__roundabout_exec"; |
| 22 | const MANAGER: &str = "org.freedesktop.systemd1.Manager"; |
| 23 | const BUS_NAME: &str = "org.freedesktop.systemd1"; |
| 24 | const BUS_PATH: &str = "/org/freedesktop/systemd1"; |
| 25 | |
| 26 | pub struct Scope { |
| 27 | connection: Connection, |
| 28 | name: String, |
| 29 | } |
| 30 | |
| 31 | fn properties( |
| 32 | pid: u32, |
| 33 | cores: u32, |
| 34 | ram: Bytes, |
| 35 | overrun: &Overrun, |
| 36 | ) -> zbus::Result<Vec<(&'static str, Value<'static>)>> { |
| 37 | let max = ram.0 as f64 * overrun.ram_slack.max(1.0); |
| 38 | if !overrun.ram_slack.is_finite() || max >= u64::MAX as f64 { |
| 39 | return Err(zbus::Error::Failure("RAM limit must be finite".into())); |
| 40 | } |
| 41 | let mut properties = vec![ |
| 42 | ("Description", Value::from("roundabout build")), |
| 43 | ("PIDs", Value::from(vec![pid])), |
| 44 | ("CollectMode", Value::from("inactive-or-failed")), |
| 45 | ("CPUAccounting", Value::from(true)), |
| 46 | ("MemoryAccounting", Value::from(true)), |
| 47 | ( |
| 48 | "CPUQuotaPerSecUSec", |
| 49 | Value::from(u64::from(cores.max(1)) * 1_000_000), |
| 50 | ), |
| 51 | ("MemoryMax", Value::from((max as u64).max(1))), |
| 52 | ("OOMPolicy", Value::from("kill")), |
| 53 | ("TimeoutStopUSec", Value::from(10_000_000_u64)), |
| 54 | ]; |
| 55 | if overrun.ram == RamOverrun::Throttle { |
| 56 | properties.push(("MemoryHigh", Value::from(ram.0.max(1)))); |
| 57 | } |
| 58 | Ok(properties) |
| 59 | } |
| 60 | |
| 61 | fn wait_job(mut signals: SignalStream<'_>, job: &OwnedObjectPath) -> zbus::Result<()> { |
| 62 | async_io::block_on(futures_lite::future::race( |
| 63 | async { |
| 64 | while let Some(signal) = signals.next().await { |
| 65 | let (_, path, _, result): (u32, OwnedObjectPath, String, String) = |
| 66 | signal.body().deserialize()?; |
| 67 | if path == *job { |
| 68 | return if result == "done" { |
| 69 | Ok(()) |
| 70 | } else { |
| 71 | Err(zbus::Error::Failure(format!("systemd job {result}"))) |
| 72 | }; |
| 73 | } |
| 74 | } |
| 75 | Err(zbus::Error::Failure( |
| 76 | "systemd disconnected before the job finished".into(), |
| 77 | )) |
| 78 | }, |
| 79 | async { |
| 80 | async_io::Timer::after(Duration::from_secs(15)).await; |
| 81 | Err(zbus::Error::Failure("systemd job timed out".into())) |
| 82 | }, |
| 83 | )) |
| 84 | } |
| 85 | |
| 86 | impl Scope { |
| 87 | fn manager(&self) -> zbus::Result<Proxy<'_>> { |
| 88 | Proxy::new(&self.connection, BUS_NAME, BUS_PATH, MANAGER) |
| 89 | } |
| 90 | |
| 91 | fn start( |
| 92 | connection: Connection, |
| 93 | pid: u32, |
| 94 | lease: u64, |
| 95 | cores: u32, |
| 96 | ram: Bytes, |
| 97 | overrun: &Overrun, |
| 98 | ) -> zbus::Result<Self> { |
| 99 | let scope = Self { |
| 100 | connection, |
| 101 | name: format!("roundabout-{}-{lease}-{pid}.scope", std::process::id()), |
| 102 | }; |
| 103 | let manager = scope.manager()?; |
| 104 | let signals = async_io::block_on( |
| 105 | manager |
| 106 | .inner() |
| 107 | .receive_signal_with_args("JobRemoved", &[(2, &scope.name)]), |
| 108 | )?; |
| 109 | let _: () = manager.call("Subscribe", &())?; |
| 110 | let auxiliary: Vec<(&str, Vec<(&str, Value<'_>)>)> = Vec::new(); |
| 111 | let job: OwnedObjectPath = manager.call( |
| 112 | "StartTransientUnit", |
| 113 | &( |
| 114 | &scope.name, |
| 115 | "fail", |
| 116 | properties(pid, cores, ram, overrun)?, |
| 117 | auxiliary, |
| 118 | ), |
| 119 | )?; |
| 120 | wait_job(signals, &job)?; |
| 121 | drop(manager); |
| 122 | Ok(scope) |
| 123 | } |
| 124 | |
| 125 | pub fn signal(&self, signal: i32) -> zbus::Result<()> { |
| 126 | self.manager()? |
| 127 | .call("KillUnit", &(&self.name, "all", signal)) |
| 128 | } |
| 129 | |
| 130 | fn stop(&self) -> zbus::Result<()> { |
| 131 | let manager = self.manager()?; |
| 132 | let signals = async_io::block_on( |
| 133 | manager |
| 134 | .inner() |
| 135 | .receive_signal_with_args("JobRemoved", &[(2, &self.name)]), |
| 136 | )?; |
| 137 | let job: OwnedObjectPath = manager.call("StopUnit", &(&self.name, "replace"))?; |
| 138 | wait_job(signals, &job) |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | impl Drop for Scope { |
| 143 | fn drop(&mut self) { |
| 144 | let _ = self.stop(); |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | fn gate() -> io::Result<(OwnedFd, OwnedFd)> { |
| 149 | let mut descriptors = [-1; 2]; |
| 150 | if unsafe { libc::pipe2(descriptors.as_mut_ptr(), libc::O_CLOEXEC) } != 0 { |
| 151 | return Err(io::Error::last_os_error()); |
| 152 | } |
| 153 | Ok(unsafe { |
| 154 | ( |
| 155 | OwnedFd::from_raw_fd(descriptors[0]), |
| 156 | OwnedFd::from_raw_fd(descriptors[1]), |
| 157 | ) |
| 158 | }) |
| 159 | } |
| 160 | |
| 161 | pub fn spawn( |
| 162 | command: &Command, |
| 163 | lease: u64, |
| 164 | cores: u32, |
| 165 | ram: Bytes, |
| 166 | overrun: &Overrun, |
| 167 | ) -> Result<(Child, Scope), String> { |
| 168 | let connection = |
| 169 | Connection::session().map_err(|error| format!("can't connect to the user bus, {error}"))?; |
| 170 | let (read, write) = gate().map_err(|error| error.to_string())?; |
| 171 | let descriptor = read.as_raw_fd(); |
| 172 | let mut runner = Command::new(env::current_exe().map_err(|error| error.to_string())?); |
| 173 | runner |
| 174 | .arg(EXEC) |
| 175 | .arg(descriptor.to_string()) |
| 176 | .arg(command.get_program()) |
| 177 | .args(command.get_args()) |
| 178 | .process_group(0); |
| 179 | for (name, value) in command.get_envs() { |
| 180 | if let Some(value) = value { |
| 181 | runner.env(name, value); |
| 182 | } else { |
| 183 | runner.env_remove(name); |
| 184 | } |
| 185 | } |
| 186 | let parent = unsafe { libc::getpid() }; |
| 187 | unsafe { |
| 188 | runner.pre_exec(move || { |
| 189 | if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) != 0 |
| 190 | || libc::fcntl(descriptor, libc::F_SETFD, 0) != 0 |
| 191 | { |
| 192 | return Err(io::Error::last_os_error()); |
| 193 | } |
| 194 | if libc::getppid() != parent { |
| 195 | return Err(io::Error::other("parent exited before launch")); |
| 196 | } |
| 197 | Ok(()) |
| 198 | }); |
| 199 | } |
| 200 | let mut child = runner.spawn().map_err(|error| error.to_string())?; |
| 201 | drop(read); |
| 202 | let result = Scope::start(connection, child.id(), lease, cores, ram, overrun) |
| 203 | .map_err(|error| format!("can't start the transient scope, {error}")) |
| 204 | .and_then(|scope| { |
| 205 | if unsafe { libc::write(write.as_raw_fd(), b"1".as_ptr().cast(), 1) } != 1 { |
| 206 | return Err(io::Error::last_os_error().to_string()); |
| 207 | } |
| 208 | Ok(scope) |
| 209 | }); |
| 210 | drop(write); |
| 211 | match result { |
| 212 | Ok(scope) => Ok((child, scope)), |
| 213 | Err(error) => { |
| 214 | let _ = child.kill(); |
| 215 | let _ = child.wait(); |
| 216 | Err(error) |
| 217 | } |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | pub fn exec() -> Option<i32> { |
| 222 | let mut arguments = env::args_os().skip(1); |
| 223 | if arguments.next()?.as_encoded_bytes() != EXEC.as_bytes() { |
| 224 | return None; |
| 225 | } |
| 226 | let Some(descriptor) = arguments |
| 227 | .next() |
| 228 | .and_then(|arg| arg.to_str()?.parse::<i32>().ok()) |
| 229 | .filter(|fd| *fd >= 3) |
| 230 | else { |
| 231 | return Some(1); |
| 232 | }; |
| 233 | let descriptor = unsafe { OwnedFd::from_raw_fd(descriptor) }; |
| 234 | let mut byte = [0_u8]; |
| 235 | loop { |
| 236 | let count = unsafe { libc::read(descriptor.as_raw_fd(), byte.as_mut_ptr().cast(), 1) }; |
| 237 | if count == 1 && byte[0] == b'1' { |
| 238 | break; |
| 239 | } |
| 240 | if count != -1 || io::Error::last_os_error().kind() != io::ErrorKind::Interrupted { |
| 241 | return Some(1); |
| 242 | } |
| 243 | } |
| 244 | drop(descriptor); |
| 245 | let Some(program) = arguments.next() else { |
| 246 | return Some(1); |
| 247 | }; |
| 248 | let error = Command::new(&program).args(arguments).exec(); |
| 249 | eprintln!( |
| 250 | "roundabout: couldn't start {}: {error}", |
| 251 | program.to_string_lossy() |
| 252 | ); |
| 253 | Some(127) |
| 254 | } |
| 255 | |
| 256 | #[cfg(test)] |
| 257 | mod tests { |
| 258 | use super::*; |
| 259 | |
| 260 | #[test] |
| 261 | fn limits_follow_the_grant_with_memory_headroom() { |
| 262 | let overrun = Overrun::default(); |
| 263 | let properties = properties(123, 8, Bytes::gib(4), &overrun).unwrap(); |
| 264 | assert!(properties.contains(&("PIDs", Value::from(vec![123_u32])))); |
| 265 | assert!(properties.contains(&("CPUQuotaPerSecUSec", Value::from(8_000_000_u64)))); |
| 266 | assert!(properties.contains(&("MemoryMax", Value::from(Bytes::gib(5).0)))); |
| 267 | assert!(!properties.iter().any(|(name, _)| *name == "MemoryHigh")); |
| 268 | } |
| 269 | |
| 270 | #[test] |
| 271 | fn throttle_starts_at_the_granted_memory() { |
| 272 | let overrun = Overrun { |
| 273 | ram: RamOverrun::Throttle, |
| 274 | ..Overrun::default() |
| 275 | }; |
| 276 | let properties = properties(123, 2, Bytes::gib(2), &overrun).unwrap(); |
| 277 | assert!(properties.contains(&("MemoryHigh", Value::from(Bytes::gib(2).0)))); |
| 278 | assert!(properties.contains(&("MemoryMax", Value::from(Bytes::mib(2560).0)))); |
| 279 | } |
| 280 | |
| 281 | #[test] |
| 282 | fn small_grants_never_become_unlimited() { |
| 283 | let overrun = Overrun { |
| 284 | ram_slack: 0.5, |
| 285 | ..Overrun::default() |
| 286 | }; |
| 287 | let properties = properties(123, 0, Bytes::ZERO, &overrun).unwrap(); |
| 288 | assert!(properties.contains(&("CPUQuotaPerSecUSec", Value::from(1_000_000_u64)))); |
| 289 | assert!(properties.contains(&("MemoryMax", Value::from(1_u64)))); |
| 290 | } |
| 291 | |
| 292 | #[test] |
| 293 | fn nonfinite_limits_are_rejected() { |
| 294 | for ram_slack in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN, f64::MAX] { |
| 295 | let overrun = Overrun { |
| 296 | ram_slack, |
| 297 | ..Overrun::default() |
| 298 | }; |
| 299 | assert!(properties(123, 1, Bytes::gib(2), &overrun).is_err()); |
| 300 | } |
| 301 | } |
| 302 | } |