diff --git a/Cargo.lock b/Cargo.lock index 81ddc2b..0798255 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -43,18 +43,18 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", @@ -63,21 +63,21 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -85,6 +85,46 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clap" +version = "4.5.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + [[package]] name = "colorchoice" version = "1.0.4" @@ -93,9 +133,9 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "env_filter" -version = "0.1.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", "regex", @@ -103,9 +143,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ "anstream", "anstyle", @@ -114,36 +154,42 @@ dependencies = [ "log", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" dependencies = [ "proc-macro2", "quote", @@ -152,9 +198,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.180" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "log" @@ -164,9 +210,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mktemp-rs" @@ -191,9 +237,9 @@ dependencies = [ [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "pin-project-lite" @@ -203,42 +249,42 @@ checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -248,9 +294,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -259,9 +305,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "serde" @@ -306,11 +352,18 @@ dependencies = [ "zmij", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "styrolite" version = "0.1.8" dependencies = [ "anyhow", + "clap", "env_logger", "libc", "log", @@ -323,9 +376,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" dependencies = [ "proc-macro2", "quote", @@ -343,9 +396,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "utf8parse" @@ -355,86 +408,21 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 1d6e4d6..738599c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2024" [dependencies] anyhow = "1.0.100" +clap = { version = "4.5.58", features = ["derive"] } env_logger = "0.11.8" libc = "0.2.180" log = "0.4.29" @@ -28,6 +29,10 @@ name = "styrolite" name = "styrolite" path = "bin/styrolite.rs" +[[bin]] +name = "styrojail" +path = "bin/styrojail.rs" + [[example]] name = "styrolite-rundir" path = "examples/rundir.rs" diff --git a/bin/styrojail.rs b/bin/styrojail.rs new file mode 100644 index 0000000..23ebc8a --- /dev/null +++ b/bin/styrojail.rs @@ -0,0 +1,197 @@ +use std::{env, path::PathBuf}; + +use anyhow::{Result, anyhow}; +use clap::Parser; +use styrolite::config::{IdMapping, MountSpec as StyroMountSpec}; +use styrolite::namespace::Namespace; +use styrolite::runner::{CreateRequestBuilder, Runner}; + +#[derive(Clone, Debug)] +struct ResourceLimit { + key: String, + value: String, +} + +#[derive(Clone, Debug)] +struct CliMountSpec { + hostpath: String, + jailpath: String, + read_write: bool, +} + +#[derive(Debug, Parser)] +#[command( + name = "styrojail", + about = "convenient jail-style styrolite frontend", + version +)] +struct Cli { + /// Path to styrolite binary (default: resolved via PATH) + #[arg(long, default_value = "styrolite")] + styrolite_bin: String, + + /// Do not synthesize default mounts (e.g. $CWD:$CWD:rw) + #[arg(long)] + no_default_mounts: bool, + + /// Additional bind-mounts for the jail + #[arg(long, value_name = "HOSTPATH:JAILPATH", value_parser = parse_mount)] + mount: Vec, + + /// Additional cgroup2 resource limits for the jail + #[arg(long, value_name = "key:value", value_parser = parse_resource_limit)] + limit: Vec, + + /// The program being jailed + #[arg(value_name = "PROGRAM")] + program: String, + + /// Arguments to the program being jailed + #[arg(value_name = "ARGS")] + args: Vec, +} + +fn current_ids() -> (libc::uid_t, libc::gid_t) { + unsafe { (libc::geteuid(), libc::getegid()) } +} + +fn build_mounts(cli: &Cli) -> Result> { + let mut mounts = Vec::new(); + + if !cli.no_default_mounts { + let cwd: PathBuf = env::current_dir().map_err(|e| anyhow!("failed to get CWD: {e}"))?; + + let cwd_str = cwd + .to_str() + .ok_or(anyhow!("CWD is not valid UTF-8"))? + .to_string(); + + mounts.push(CliMountSpec { + hostpath: cwd_str.clone(), + jailpath: cwd_str, + read_write: true, + }); + } + + // Then append user-specified mounts + mounts.extend(cli.mount.clone()); + + Ok(mounts) +} + +fn parse_mount(s: &str) -> Result { + let mut parts = s.split(':'); + + let hostpath = parts + .next() + .ok_or(anyhow!("mount must look like /hostpath:/jailpath[:rw]"))?; + + let jailpath = parts + .next() + .ok_or(anyhow!("mount must look like /hostpath:/jailpath[:rw]"))?; + + let mode = parts.next(); + + if parts.next().is_some() { + return Err(anyhow!("mount must look like /hostpath:/jailpath[:rw]")); + } + + if hostpath.is_empty() || jailpath.is_empty() { + return Err(anyhow!("mount must look like /hostpath:/jailpath[:rw]")); + } + + let read_write = match mode { + None => false, // default = read-only + Some("rw") => true, + Some(_) => { + return Err(anyhow!("only ':rw' is supported as a mount modifier")); + } + }; + + Ok(CliMountSpec { + hostpath: hostpath.to_string(), + jailpath: jailpath.to_string(), + read_write, + }) +} + +fn parse_resource_limit(s: &str) -> Result { + let (k, v) = s + .split_once('=') + .ok_or(anyhow!("limit must look like key=value"))?; + + if k.is_empty() { + return Err(anyhow!("limit key cannot be empty")); + } + + Ok(ResourceLimit { + key: k.to_string(), + value: v.to_string(), + }) +} + +fn to_styrolite_mount(m: &CliMountSpec) -> StyroMountSpec { + StyroMountSpec { + source: Some(m.hostpath.clone()), + target: m.jailpath.clone(), + fstype: None, + bind: true, + recurse: false, + unshare: false, + safe: false, + create_mountpoint: true, + read_only: !m.read_write, + ..Default::default() + } +} + +fn main() -> Result<()> { + let (uid, gid) = current_ids(); + + let cli = Cli::parse(); + let mut builder = CreateRequestBuilder::new() + .set_rootfs("/") + .set_rootfs_readonly(true) + .set_skip_two_stage_userns(true) + .set_executable(&cli.program) + .set_uid(uid) + .set_gid(gid) + .set_setgroups_deny(true) + .set_working_directory(std::env::current_dir()?.as_os_str().to_str().unwrap_or("/")) + .set_workload_id(format!("styrojail-{}", std::process::id()).as_str()) + .push_uid_mapping(IdMapping { + base_nsid: uid, + base_hostid: uid, + remap_count: 1, + }) + .push_gid_mapping(IdMapping { + base_nsid: gid, + base_hostid: gid, + remap_count: 1, + }) + .push_namespace(Namespace::Uts) + .push_namespace(Namespace::Time) + .push_namespace(Namespace::Pid) + .push_namespace(Namespace::User) + .push_namespace(Namespace::Ipc) + .push_namespace(Namespace::Mount); + + let args_ref: Vec<&str> = cli.args.iter().map(|s| s.as_str()).collect(); + builder = builder.set_arguments(args_ref); + + let mounts = build_mounts(&cli)?; + + for m in &mounts { + builder = builder.push_mount(to_styrolite_mount(m)); + } + + for lim in &cli.limit { + builder = builder.push_resource_limit(&lim.key, &lim.value); + } + + let req = builder.to_request(); + let runner = Runner::new(&cli.styrolite_bin); + runner.exec(req)?; + + Err(anyhow!("styrolite exec failed")) +} diff --git a/src/config.rs b/src/config.rs index 8026838..fc2a6a0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -88,6 +88,9 @@ pub struct CreateRequest { /// It should be assumed that the rootfs might already have proc, sys, and dev mounts. (This might need to change?) pub rootfs: Option, + /// Whether the rootfs should be mounted readonly. + pub rootfs_readonly: Option, + /// The executable specification for the initial process created in this /// container. pub exec: ExecutableSpec, @@ -126,8 +129,12 @@ pub struct CreateRequest { /// Whether setgroups(2) should be denied in this container. pub setgroups_deny: Option, + /// Capabilities for this container. pub capabilities: Option, + + /// Whether the two-stage userns setup should be skipped. + pub skip_two_stage_userns: Option, } #[derive(Default, Debug, Serialize, Deserialize)] @@ -240,6 +247,9 @@ pub struct MountSpec { /// Whether the target mount point should be created as a directory if it /// does not exist. pub create_mountpoint: bool, + + /// Whether the mount point should be mounted readonly. + pub read_only: bool, } pub trait Mountable { @@ -249,6 +259,9 @@ pub trait Mountable { /// Pivot, making this mount point the new rootfs. /// The old rootfs is unmounted as a side effect. fn pivot(&self) -> Result<()>; + + /// Makes a mountpoint read-only after the fact. + fn seal(&self) -> Result<()>; } pub type ResourceLimits = BTreeMap; diff --git a/src/mount.rs b/src/mount.rs index 5a60adb..14c9551 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -1,13 +1,86 @@ use std::env; use std::ffi::{CString, c_ulong}; use std::fs; +use std::io; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; +use std::os::raw::{c_int, c_uint}; use std::ptr; -use anyhow::{Result, bail}; +use anyhow::{Result, anyhow, bail}; use libc; use crate::config::{MountSpec, Mountable}; +const MOVE_MOUNT_F_EMPTY_PATH: c_uint = 0x4; + +/// open_tree(2) +pub fn open_tree(dfd: c_int, path: &str, flags: c_uint) -> io::Result { + let c_path = CString::new(path)?; + let ret = unsafe { libc::syscall(libc::SYS_open_tree, dfd, c_path.as_ptr(), flags) }; + + if ret < 0 { + return Err(io::Error::last_os_error()); + } + + Ok(unsafe { OwnedFd::from_raw_fd(ret as c_int) }) +} + +/// move_mount(2) +pub fn move_mount( + from_dfd: c_int, + from_path: &str, + to_dfd: c_int, + to_path: &str, + flags: c_uint, +) -> io::Result<()> { + let c_from = CString::new(from_path)?; + let c_to = CString::new(to_path)?; + + let ret = unsafe { + libc::syscall( + libc::SYS_move_mount, + from_dfd, + c_from.as_ptr(), + to_dfd, + c_to.as_ptr(), + flags, + ) + }; + + if ret < 0 { + return Err(io::Error::last_os_error()); + } + + Ok(()) +} + +/// mount_setattr(2) +pub fn mount_setattr( + dfd: c_int, + path: &str, + flags: c_uint, + attr: &libc::mount_attr, +) -> io::Result<()> { + let c_path = CString::new(path)?; + + let ret = unsafe { + libc::syscall( + libc::SYS_mount_setattr, + dfd, + c_path.as_ptr(), + flags, + attr as *const libc::mount_attr, + std::mem::size_of::(), + ) + }; + + if ret < 0 { + return Err(io::Error::last_os_error()); + } + + Ok(()) +} + fn unpack(data: Option) -> CString { if data.is_some() && let Ok(cstr) = CString::new(data.unwrap()) @@ -18,7 +91,42 @@ fn unpack(data: Option) -> CString { CString::new("").expect("") } +pub fn move_mount_fd_to(fd: &OwnedFd, target: &str) -> io::Result<()> { + move_mount( + fd.as_raw_fd(), + "", + libc::AT_FDCWD, + target, + MOVE_MOUNT_F_EMPTY_PATH as c_uint, + ) +} + +pub fn mount_setattr_fd(fd: &OwnedFd, recursive: bool, attr: &libc::mount_attr) -> io::Result<()> { + let mut flags = libc::AT_EMPTY_PATH as c_uint; + if recursive { + flags |= libc::AT_RECURSIVE as c_uint; + } + + mount_setattr(fd.as_raw_fd(), "", flags, attr) +} + impl Mountable for MountSpec { + fn seal(&self) -> Result<()> { + let tree = open_tree( + libc::AT_FDCWD, + self.source + .as_deref() + .ok_or_else(|| anyhow!("source missing"))?, + libc::OPEN_TREE_CLOEXEC as u32, + )?; + + let mut attr: libc::mount_attr = unsafe { std::mem::zeroed() }; + attr.attr_set |= libc::MOUNT_ATTR_RDONLY as u64; + mount_setattr_fd(&tree, false, &attr)?; + + Ok(()) + } + fn mount(&self) -> Result<()> { let source = unpack(self.source.clone()); let source_p = if self.source.is_none() { @@ -26,17 +134,19 @@ impl Mountable for MountSpec { } else { source.as_ptr() }; + let fstype = unpack(self.fstype.clone()); - let fstype_p = if self.fstype.is_none() { + let fstype_p = if self.fstype.is_none() || self.bind { ptr::null() } else { fstype.as_ptr() }; + let target = CString::new(self.target.clone())?; let target_p = target.as_ptr(); if self.create_mountpoint { - fs::create_dir_all(self.target.clone())?; + fs::create_dir_all(&self.target)?; } let mut flags: c_ulong = libc::MS_SILENT; @@ -53,16 +163,38 @@ impl Mountable for MountSpec { flags |= libc::MS_REC; } + unsafe { + let rc = libc::mount(source_p, target_p, fstype_p, flags, ptr::null()); + if rc < 0 { + bail!("unable to mount"); + } + } + + let mut set: c_ulong = 0; + if self.safe { - flags |= libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC; + set |= libc::MOUNT_ATTR_NOSUID as c_ulong; + set |= libc::MOUNT_ATTR_NODEV as c_ulong; + set |= libc::MOUNT_ATTR_NOEXEC as c_ulong; } - unsafe { - let result = libc::mount(source_p, target_p, fstype_p, flags, ptr::null()); + if self.read_only { + set |= libc::MOUNT_ATTR_RDONLY as c_ulong; + } - if result < 0 { - bail!("unable to mount"); + if set != 0 { + let mut attr: libc::mount_attr = unsafe { std::mem::zeroed() }; + attr.attr_set = set as u64; + attr.attr_clr = 0; + attr.propagation = 0; + attr.userns_fd = 0; + + let mut msaflags: c_uint = 0; + if self.recurse { + msaflags |= libc::AT_RECURSIVE as c_uint; } + + mount_setattr(libc::AT_FDCWD, &self.target, msaflags, &attr).map_err(|e| anyhow!(e))?; } Ok(()) diff --git a/src/runner.rs b/src/runner.rs index 61c46e6..862e8a5 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; use std::io::{BufWriter, Write}; +use std::os::unix::process::CommandExt; use std::process::Command; use anyhow::{Result, anyhow}; @@ -156,6 +157,19 @@ impl CreateRequestBuilder { self } + pub fn set_rootfs_readonly(mut self, rootfs_readonly: bool) -> CreateRequestBuilder { + self.config.rootfs_readonly = Some(rootfs_readonly); + self + } + + pub fn set_skip_two_stage_userns( + mut self, + skip_two_stage_userns: bool, + ) -> CreateRequestBuilder { + self.config.skip_two_stage_userns = Some(skip_two_stage_userns); + self + } + pub fn set_executable(mut self, executable: &str) -> CreateRequestBuilder { self.config.exec.executable = executable.to_string().into(); self @@ -395,4 +409,26 @@ impl Runner { Err(anyhow!("failed to launch/monitor child process")) } + + /// Replace the current process with the styrolite runner directly. + #[cfg(unix)] + pub fn exec(&self, config: T) -> Result<()> { + let mut config_file = TempFile::new("styrolite-cfg-", ".json")?; + self.write_config(config, &mut config_file)?; + + // Build the command like before + let mut command = self.create_command(&config_file)?; + + // NOTE: If exec succeeds, this process image is replaced; no destructors run. + // That means config_file won't be dropped, so a drop-based cleanup won't happen. + // If TempFile is delete-on-drop, the file may be left behind. + let err = command.exec(); // only returns on failure + Err(anyhow!(err)) + } + + #[cfg(not(unix))] + pub fn exec(&self, config: T) -> Result<()> { + let _ = config; + Err(anyhow!("Runner::exec is only supported on unix")) + } } diff --git a/src/wrap.rs b/src/wrap.rs index 216519d..8f3d6aa 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -302,11 +302,13 @@ impl CreateRequest { fn pivot_fs(&self) -> Result<()> { debug!("early mount!"); - let rootfs = self + let mut rootfs = self .rootfs .clone() .expect("expected rootfs to be configured"); + let rootfs_readonly = self.rootfs_readonly.unwrap_or(false); + // Unshare rootfs mount so we can later pivot to a new rootfs. // The unshared root mount will be cleaned up once the new rootfs is // in place. @@ -319,12 +321,55 @@ impl CreateRequest { unshare: true, safe: false, create_mountpoint: false, + read_only: false, }; oldroot .mount() .expect("failed to unshare / in new mount namespace"); + // If we want to clone the VFS root, e.g. for styrojail, + // we have to do some special things to cope with that. + let stage_base = format!("/tmp/styrolite-stage-{}", self.identity()?); + let stage_root = format!("/tmp/styrolite-stage-{}/root", self.identity()?); + let stage_old = format!("/tmp/styrolite-stage-{}/old", self.identity()?); + + if rootfs == "/" { + // Mount a tmpfs staging area so we can pivot into a non-"/" mountpoint. + let stage_tmpfs = MountSpec { + source: Some("tmpfs".to_string()), + target: stage_base, + fstype: Some("tmpfs".to_string()), + bind: false, + recurse: false, + unshare: false, + safe: true, + create_mountpoint: true, + read_only: false, + }; + stage_tmpfs.mount().expect("failed to mount staging tmpfs"); + + std::fs::create_dir_all(&stage_root).expect("failed to create staging root dir"); + std::fs::create_dir_all(&stage_old).expect("failed to create staging old dir"); + + let stage_bind = MountSpec { + source: Some("/".to_string()), + target: stage_root.clone(), + fstype: Some("none".to_string()), + bind: true, + recurse: true, + unshare: false, + safe: false, + create_mountpoint: false, + read_only: false, + }; + stage_bind + .mount() + .expect("failed to bind / into staging root"); + + rootfs = stage_root.to_string(); + } + // Now mount the new rootfs. let newroot = MountSpec { source: Some(rootfs.clone()), @@ -335,10 +380,15 @@ impl CreateRequest { unshare: false, safe: false, create_mountpoint: false, + read_only: false, }; newroot.mount().expect("failed to bind new rootfs"); + if rootfs_readonly { + newroot.seal().expect("failed to make new rootfs readonly"); + } + // Mount /proc. let procfs = MountSpec { source: Some("proc".to_string()), @@ -349,6 +399,7 @@ impl CreateRequest { unshare: false, safe: true, create_mountpoint: false, + read_only: false, }; procfs.mount().expect("failed to mount /proc"); @@ -365,6 +416,7 @@ impl CreateRequest { unshare: mount.unshare, safe: mount.safe, create_mountpoint: mount.create_mountpoint, + read_only: mount.read_only, }; parented_mount @@ -421,11 +473,17 @@ impl Wrappable for CreateRequest { warn!("unable to prepare cgroup: {e}"); } - let first_level_ns = target_ns - .iter() - .filter(|ns| **ns != Namespace::User) - .cloned() - .collect::>(); + let skip_two_stage_userns = self.skip_two_stage_userns.unwrap_or(false); + + let first_level_ns = if !skip_two_stage_userns { + target_ns + .iter() + .filter(|ns| **ns != Namespace::User) + .cloned() + .collect::>() + } else { + target_ns.clone() + }; debug!("unsharing namespaces"); unshare(&first_level_ns)?; @@ -466,12 +524,8 @@ impl Wrappable for CreateRequest { pef.read_exact(&mut buf)?; if target_ns.contains(&Namespace::User) { - // We are preparing the userns for PID 1 because we are in the same mount namespace - // as the child, and thus the first process created is always PID 1. We no longer - // have access to the host /proc so we just hardcode PID 1 and hope for the best. - // So far, this seems to work fairly well. debug!("child has dropped into its own userns, configuring from supervisor"); - self.prepare_userns(1)?; + self.prepare_userns(pid)?; } // The supervisor has now configured the user namespace, so let the first process run. @@ -492,17 +546,9 @@ impl Wrappable for CreateRequest { process::exit(1); } - if target_ns.contains(&Namespace::Mount) { - self.pivot_fs()?; - } else { - warn!("mount namespace not present in requested namespaces, trying to work anyway..."); - warn!("this is an insecure configuration!"); - } - - debug!("mount tree finalized, doing final prep"); let mut pef = unsafe { File::from_raw_fd(parent_efd) }; - if target_ns.contains(&Namespace::User) { + if !skip_two_stage_userns && target_ns.contains(&Namespace::User) { debug!("unsharing user namespace"); unshare(&vec![Namespace::User])?; } @@ -517,6 +563,16 @@ impl Wrappable for CreateRequest { let mut buf = [0u8; 8]; cef.read_exact(&mut buf)?; + // We are configured, now do the mount stuff? + if target_ns.contains(&Namespace::Mount) { + self.pivot_fs()?; + } else { + warn!("mount namespace not present in requested namespaces, trying to work anyway..."); + warn!("this is an insecure configuration!"); + } + + debug!("mount tree finalized, doing final prep"); + // We need to toggle SECBIT before we change UID/GID, // or else changing UID/GID may cause us to lose the capabilities // we need to explicitly drop capabilities later on.