diff options
author | Matthias Schiffer <mschiffer@universe-factory.net> | 2021-01-31 19:18:20 +0100 |
---|---|---|
committer | Matthias Schiffer <mschiffer@universe-factory.net> | 2021-01-31 19:18:20 +0100 |
commit | b9e28bd7990b597d707fb9a81880dc14accc9c41 (patch) | |
tree | fbb54d67bac841728450b62cd192095b9d3993b5 | |
parent | 6eb0851420b358132dd8a72312b25a1f7efd02de (diff) | |
download | rebel-b9e28bd7990b597d707fb9a81880dc14accc9c41.tar rebel-b9e28bd7990b597d707fb9a81880dc14accc9c41.zip |
Unshare/subuid handling
Buildah is too slow for our usecase. Handle userns setup ourselves, so
we can call runc directly.
-rw-r--r-- | Cargo.lock | 58 | ||||
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | src/executor.rs | 2 | ||||
-rw-r--r-- | src/main.rs | 42 | ||||
-rw-r--r-- | src/prepared_command.rs | 96 | ||||
-rw-r--r-- | src/runner.rs | 2 | ||||
-rw-r--r-- | src/runner/buildah.rs | 62 | ||||
-rw-r--r-- | src/runner/runc.rs | 22 | ||||
-rw-r--r-- | src/unshare.rs | 130 | ||||
-rw-r--r-- | src/util.rs | 45 |
10 files changed, 394 insertions, 68 deletions
@@ -1,18 +1,63 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cc" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] name = "dtoa" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e" [[package]] +name = "libc" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cca32fa0182e8c0989459524dc356b8f2b5c10f1b9eb521b7d182c03cf8c5ff" + +[[package]] name = "linked-hash-map" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "nix" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", +] + +[[package]] name = "proc-macro2" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -34,9 +79,12 @@ dependencies = [ name = "rebel" version = "0.1.0" dependencies = [ + "libc", + "nix", "scopeguard", "serde", "serde_yaml", + "users", "walkdir", ] @@ -105,6 +153,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] name = "walkdir" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7,7 +7,10 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +libc = "0.2.84" +nix = "0.19.1" scopeguard = "1.1.0" serde = { version = "1", features = ["derive"] } serde_yaml = "0.8" +users = "0.11.0" walkdir = "2" diff --git a/src/executor.rs b/src/executor.rs index 2523162..814aa79 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -70,7 +70,7 @@ impl<'a> Executor<'a> { } pub fn run(&mut self) -> runner::Result<()> { - let runner = runner::buildah::BuildahRunner::new(self.tasks); + let runner = runner::runc::RuncRunner::new(self.tasks); while !self.tasks_runnable.is_empty() { self.run_one(&runner)?; } diff --git a/src/main.rs b/src/main.rs index 72178be..8d4787d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,47 @@ mod executor; +mod prepared_command; mod recipe; mod resolve; mod runner; mod types; +mod unshare; +mod util; -use std::path::Path; +use nix::{ + mount::{self, MsFlags}, + unistd, +}; +use std::{io::Result, path::Path}; use types::*; +use util::ToIOResult; + +fn mount_buildtmp() -> Result<()> { + mount::mount::<_, _, _, str>( + Some("buildtmp"), + "build/tmp", + Some("tmpfs"), + MsFlags::empty(), + None, + ) + .to_io_result() +} + +fn exec_shell() -> Result<std::convert::Infallible> { + let bin_sh = std::ffi::CString::new("/bin/sh").unwrap(); + unistd::execv(&bin_sh, &[&bin_sh]).to_io_result() +} + +fn execute(mut exc: executor::Executor) -> Result<()> { + unshare::unshare()?; + mount_buildtmp()?; + + exc.run()?; + + exec_shell()?; + + Ok(()) +} fn main() { let recipes = recipe::read_recipes(Path::new("examples")).unwrap(); @@ -29,10 +64,9 @@ fn main() { std::process::exit(1); } let taskset = rsv.to_taskset(); - let mut executor = executor::Executor::new(&tasks, taskset); + let exc = executor::Executor::new(&tasks, taskset); - let result = executor.run(); - if let Err(error) = result { + if let Err(error) = execute(exc) { eprintln!("{}", error); std::process::exit(1); } diff --git a/src/prepared_command.rs b/src/prepared_command.rs new file mode 100644 index 0000000..00d648e --- /dev/null +++ b/src/prepared_command.rs @@ -0,0 +1,96 @@ +use std::{ + ffi::{CString, OsStr}, + io::{Error, ErrorKind, Result}, + iter, + os::unix::{ffi::*, io::RawFd}, + ptr, +}; + +use libc::{c_char, c_void}; +use nix::{fcntl::OFlag, sys::wait, unistd}; + +use crate::util::ToIOResult; + +#[derive(Clone, Debug)] +pub struct PreparedCommandBuilder { + program: CString, + args: Vec<CString>, +} + +#[derive(Debug)] +pub struct PreparedCommand { + child: unistd::Pid, + pipew: RawFd, +} + +impl Drop for PreparedCommand { + fn drop(&mut self) { + let _ = unistd::close(self.pipew); + } +} + +fn os2c<S: AsRef<OsStr>>(s: S) -> CString { + CString::new(s.as_ref().as_bytes()).unwrap() +} + +impl PreparedCommandBuilder { + pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self { + self.args.push(os2c(arg)); + self + } + + pub fn prepare(&mut self) -> Result<PreparedCommand> { + let exe_p = self.program.as_ptr(); + + let argv: Vec<*const c_char> = iter::once(exe_p) + .chain(self.args.iter().map(|arg| arg.as_ptr())) + .chain(iter::once(ptr::null())) + .collect(); + + let argv_p = argv.as_ptr(); + + let (piper, pipew) = unistd::pipe2(OFlag::O_CLOEXEC).to_io_result()?; + + unsafe { + match unistd::fork().to_io_result()? { + unistd::ForkResult::Parent { child } => { + return Ok(PreparedCommand { child, pipew }); + } + unistd::ForkResult::Child => {} + } + + // Child process - only async-signal-safe calls allowed + + libc::close(pipew); + + // Wait for run trigger + let mut buf = [0u8; 1]; + if libc::read(piper, buf.as_mut_ptr() as *mut c_void, buf.len()) != 1 { + // PreparedCommand was dropped, or controlling process exited + libc::_exit(0); + } + + libc::execvp(exe_p, argv_p); + + // exec failed + libc::_exit(127); + } + } +} + +impl PreparedCommand { + pub fn new<S: AsRef<OsStr>>(program: S) -> PreparedCommandBuilder { + PreparedCommandBuilder { + program: os2c(program), + args: Vec::new(), + } + } + + pub fn run(self) -> Result<wait::WaitStatus> { + if unistd::write(self.pipew, &[0]).to_io_result()? != 1 { + return Err(Error::new(ErrorKind::Other, "command trigger write failed")); + } + + wait::waitpid(Some(self.child), None).to_io_result() + } +} diff --git a/src/runner.rs b/src/runner.rs index 7b94573..a290aeb 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,4 +1,4 @@ -pub mod buildah; +pub mod runc; use std::io; diff --git a/src/runner/buildah.rs b/src/runner/buildah.rs deleted file mode 100644 index 72a2e5f..0000000 --- a/src/runner/buildah.rs +++ /dev/null @@ -1,62 +0,0 @@ -use scopeguard::defer; -use std::{ - io::{Error, ErrorKind}, - process::{Command, Stdio}, -}; - -use super::*; -use crate::types::*; - -static IMAGE: &str = "rebel:latest"; - -pub struct BuildahRunner<'a> { - tasks: &'a TaskMap, -} - -impl<'a> BuildahRunner<'a> { - pub fn new(tasks: &'a TaskMap) -> Self { - BuildahRunner { tasks } - } -} - -fn check_status(output: &std::process::Output, message: &str) -> Result<()> { - if output.status.success() { - Ok(()) - } else { - Err(Error::new(ErrorKind::Other, message)) - } -} - -impl<'a> Runner for BuildahRunner<'a> { - fn run(&self, task: &TaskRef) -> Result<()> { - let task_def = self.tasks.get(task).expect("Invalid TaskRef"); - - let buildah_from = Command::new("buildah") - .arg("from") - .arg("--name") - .arg(task) - .arg(IMAGE) - .output()?; - check_status(&buildah_from, "unable to create container")?; - - defer! { - let _ = Command::new("buildah") - .arg("rm") - .arg(task) - .stdout(Stdio::null()) - .output(); - } - - let buildah_run = Command::new("buildah") - .arg("run") - .arg(task) - .arg("sh") - .arg("-c") - .arg(&task_def.run) - .output()?; - - println!("{}:\n\t{:?}\n\t{:?}", task, task_def, buildah_run); - - Ok(()) - } -} diff --git a/src/runner/runc.rs b/src/runner/runc.rs new file mode 100644 index 0000000..b323d5d --- /dev/null +++ b/src/runner/runc.rs @@ -0,0 +1,22 @@ +use super::*; +use crate::types::TaskMap; + +pub struct RuncRunner<'a> { + tasks: &'a TaskMap, +} + +impl<'a> RuncRunner<'a> { + pub fn new(tasks: &'a TaskMap) -> Self { + RuncRunner { tasks } + } +} + +impl<'a> Runner for RuncRunner<'a> { + fn run(&self, task: &TaskRef) -> Result<()> { + let task_def = self.tasks.get(task).expect("Invalid TaskRef"); + + println!("{}:\n\t{:?}", task, task_def); + + Ok(()) + } +} diff --git a/src/unshare.rs b/src/unshare.rs new file mode 100644 index 0000000..2aa2b09 --- /dev/null +++ b/src/unshare.rs @@ -0,0 +1,130 @@ +use std::{ + ffi::{OsStr, OsString}, + fs::File, + io::{self, BufRead, Result}, + os::unix::ffi::*, + path::Path, +}; + +use nix::{ + sched::{self, CloneFlags}, + unistd, +}; + +use crate::prepared_command::PreparedCommand; +use crate::util::{Checkable, ToIOResult}; + +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +struct SubIDRange { + start: libc::uid_t, + count: libc::uid_t, +} + +fn parse_pid(s: &OsStr) -> Option<libc::uid_t> { + s.to_str().and_then(|s| s.parse::<libc::uid_t>().ok()) +} + +fn parse_id_range(line: Vec<u8>, uid: &OsStr, username: Option<&OsStr>) -> Option<SubIDRange> { + let parts: Vec<_> = line.split(|c| *c == b':').map(OsStr::from_bytes).collect(); + if parts.len() != 3 { + return None; + } + if parts[0] != uid && Some(parts[0]) != username { + return None; + } + + if let (Some(start), Some(count)) = (parse_pid(parts[1]), parse_pid(parts[2])) { + Some(SubIDRange { start, count }) + } else { + None + } +} + +fn read_id_ranges(filename: &Path) -> Result<Vec<SubIDRange>> { + let uid = users::get_effective_uid(); + let username = users::get_user_by_uid(uid).map(|user| user.name().to_os_string()); + + let uidstr = OsString::from(uid.to_string()); + let usernamestr = username.as_ref().map(OsString::as_os_str); + + let file = File::open(filename)?; + let lines = io::BufReader::new(file).split(b'\n'); + + let mut ranges = Vec::new(); + + for line in lines { + if let Some(range) = parse_id_range(line?, &uidstr, usernamestr) { + ranges.push(range); + } + } + + Ok(ranges) +} + +#[derive(Debug)] +struct SubIDMap { + lower: libc::uid_t, + upper: libc::uid_t, + count: libc::uid_t, +} + +fn generate_idmap(id: libc::uid_t, mut ranges: Vec<SubIDRange>) -> Vec<SubIDMap> { + let mut map = Vec::new(); + + map.push(SubIDMap { + lower: 0, + upper: id, + count: 1, + }); + + let mut lower = 1; + + ranges.sort(); + for range in &ranges { + map.push(SubIDMap { + lower, + upper: range.start, + count: range.count, + }); + lower += range.count; + } + + map +} + +fn get_uid_map() -> Result<Vec<SubIDMap>> { + let uid = users::get_effective_uid(); + let uid_ranges = read_id_ranges(Path::new("/etc/subuid"))?; + Ok(generate_idmap(uid, uid_ranges)) +} + +fn get_gid_map() -> Result<Vec<SubIDMap>> { + let gid = users::get_effective_gid(); + let gid_ranges = read_id_ranges(Path::new("/etc/subgid"))?; + Ok(generate_idmap(gid, gid_ranges)) +} + +fn prepare_idmap_cmd(cmd: &str, pid: &str, map: &Vec<SubIDMap>) -> Result<PreparedCommand> { + let mut builder = PreparedCommand::new(cmd); + builder.arg(&pid); + for uids in map { + builder.arg(uids.lower.to_string()); + builder.arg(uids.upper.to_string()); + builder.arg(uids.count.to_string()); + } + builder.prepare() +} + +pub fn unshare() -> Result<()> { + let pid = unistd::getpid().to_string(); + + let newuidmap = prepare_idmap_cmd("newuidmap", pid.as_str(), &get_uid_map()?)?; + let newgidmap = prepare_idmap_cmd("newgidmap", pid.as_str(), &get_gid_map()?)?; + + sched::unshare(CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWNS).to_io_result()?; + + newuidmap.run()?.check()?; + newgidmap.run()?.check()?; + + Ok(()) +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..c18a263 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,45 @@ +use std::{ + io::{Error, ErrorKind, Result}, + process::ExitStatus, +}; + +use nix::sys::wait; + +pub trait ToIOResult<T> { + fn to_io_result(self) -> Result<T>; +} + +impl<T> ToIOResult<T> for nix::Result<T> { + fn to_io_result(self) -> Result<T> { + self.map_err(|error| Error::new(ErrorKind::Other, error)) + } +} + +pub trait Checkable { + fn check(&self) -> Result<()>; +} + +impl Checkable for ExitStatus { + fn check(&self) -> Result<()> { + if self.success() { + Ok(()) + } else { + Err(Error::new( + ErrorKind::Other, + format!("process exited with status {}", self), + )) + } + } +} + +impl Checkable for wait::WaitStatus { + fn check(&self) -> Result<()> { + match self { + wait::WaitStatus::Exited(_, 0) => Ok(()), + _ => Err(Error::new( + ErrorKind::Other, + format!("process exited with status {:?}", self), + )), + } + } +} |