From 5b4900277cb90573a803c7a40d64952fc6df49c1 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 12:48:47 -0800 Subject: [PATCH 01/22] cargo: add clap-derive dependency --- Cargo.lock | 236 +++++++++++++++++++++++++---------------------------- Cargo.toml | 1 + 2 files changed, 113 insertions(+), 124 deletions(-) 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..35a7f24 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" From d2baab93814269de670f2ef8118cf4b993eaa4fb Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 12:54:06 -0800 Subject: [PATCH 02/22] styrojail: add skeleton Signed-off-by: Ariadne Conill --- Cargo.toml | 4 ++ bin/styrojail.rs | 109 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 bin/styrojail.rs diff --git a/Cargo.toml b/Cargo.toml index 35a7f24..738599c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,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..f1e2ac7 --- /dev/null +++ b/bin/styrojail.rs @@ -0,0 +1,109 @@ +use std::{env, fs, path::PathBuf, str::FromStr}; + +use anyhow::{anyhow, Result}; +use clap::{Parser}; +use env_logger::Env; +use styrolite::runner::CreateRequestBuilder; + +#[derive(Clone, Debug)] +struct ResourceLimit { + key: String, + value: String, +} + +#[derive(Clone, Debug)] +struct MountSpec { + hostpath: String, + jailpath: String, + read_write: bool, +} + +#[derive(Debug, Parser)] +#[command( + name = "styrojail", + about = "convenient jail-style styrolite frontend", + version, +)] +struct Cli { + #[arg(long)] + no_default_mounts: bool, + + #[arg(long, value_name = "HOSTPATH:JAILPATH", value_parser = parse_mount)] + mount: Vec, + + #[arg(long, value_name = "key:value", value_parser = parse_resource_limit)] + limit: Vec, + + #[arg(value_name = "PROGRAM")] + program: String, + + #[arg(value_name = "ARGS")] + args: Vec, +} + +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(MountSpec { + 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 main() -> Result<()> { + let cli = Cli::parse(); + let createreq = CreateRequestBuilder::new(); + + let mut argv = Vec::with_capacity(1 + cli.args.len()); + argv.push(cli.program.clone()); + argv.extend(cli.args.iter().cloned()); + + eprintln!("no_default_mount = {}", cli.no_default_mounts); + eprintln!("mounts = {:?}", cli.mount); + eprintln!("limits = {:?}", cli.limit); + eprintln!("exec argv = {:?}", argv); + + Ok(()) +} From e73a1f8111def16a37c367afef0b7765ab1d2d30 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 13:02:25 -0800 Subject: [PATCH 03/22] styrojail: build default mount table Signed-off-by: Ariadne Conill --- bin/styrojail.rs | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/bin/styrojail.rs b/bin/styrojail.rs index f1e2ac7..8656b86 100644 --- a/bin/styrojail.rs +++ b/bin/styrojail.rs @@ -12,7 +12,7 @@ struct ResourceLimit { } #[derive(Clone, Debug)] -struct MountSpec { +struct CliMountSpec { hostpath: String, jailpath: String, read_write: bool, @@ -29,7 +29,7 @@ struct Cli { no_default_mounts: bool, #[arg(long, value_name = "HOSTPATH:JAILPATH", value_parser = parse_mount)] - mount: Vec, + mount: Vec, #[arg(long, value_name = "key:value", value_parser = parse_resource_limit)] limit: Vec, @@ -41,7 +41,40 @@ struct Cli { args: Vec, } -fn parse_mount(s: &str) -> Result { +fn build_mounts(cli: &Cli) -> Result> { + let mut mounts = Vec::new(); + + if !cli.no_default_mounts { + // 1) /:/ (read-only) + mounts.push(CliMountSpec { + hostpath: "/".into(), + jailpath: "/".into(), + read_write: false, + }); + + // 2) $CWD:$CWD:rw + 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 @@ -70,7 +103,7 @@ fn parse_mount(s: &str) -> Result { } }; - Ok(MountSpec { + Ok(CliMountSpec { hostpath: hostpath.to_string(), jailpath: jailpath.to_string(), read_write, @@ -100,8 +133,10 @@ fn main() -> Result<()> { argv.push(cli.program.clone()); argv.extend(cli.args.iter().cloned()); + let mounts = build_mounts(&cli)?; + eprintln!("no_default_mount = {}", cli.no_default_mounts); - eprintln!("mounts = {:?}", cli.mount); + eprintln!("mounts = {:?}", mounts); eprintln!("limits = {:?}", cli.limit); eprintln!("exec argv = {:?}", argv); From 805876d59158f01a07718815e05486516ef075d9 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 13:14:08 -0800 Subject: [PATCH 04/22] chore: add read-only mounts Signed-off-by: Ariadne Conill --- src/config.rs | 6 ++++++ src/mount.rs | 4 ++++ src/runner.rs | 5 +++++ src/wrap.rs | 8 ++++++++ 4 files changed, 23 insertions(+) diff --git a/src/config.rs b/src/config.rs index 8026838..441c6ec 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, @@ -240,6 +243,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 { diff --git a/src/mount.rs b/src/mount.rs index 5a60adb..4ea4cb0 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -57,6 +57,10 @@ impl Mountable for MountSpec { flags |= libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC; } + if self.read_only { + flags |= libc::MS_RDONLY; + } + unsafe { let result = libc::mount(source_p, target_p, fstype_p, flags, ptr::null()); diff --git a/src/runner.rs b/src/runner.rs index 61c46e6..bbdee32 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -156,6 +156,11 @@ 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_executable(mut self, executable: &str) -> CreateRequestBuilder { self.config.exec.executable = executable.to_string().into(); self diff --git a/src/wrap.rs b/src/wrap.rs index 216519d..fb6912e 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -307,6 +307,10 @@ impl CreateRequest { .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,6 +323,7 @@ impl CreateRequest { unshare: true, safe: false, create_mountpoint: false, + read_only: false, }; oldroot @@ -335,6 +340,7 @@ impl CreateRequest { unshare: false, safe: false, create_mountpoint: false, + read_only: rootfs_readonly, }; newroot.mount().expect("failed to bind new rootfs"); @@ -349,6 +355,7 @@ impl CreateRequest { unshare: false, safe: true, create_mountpoint: false, + read_only: false, }; procfs.mount().expect("failed to mount /proc"); @@ -365,6 +372,7 @@ impl CreateRequest { unshare: mount.unshare, safe: mount.safe, create_mountpoint: mount.create_mountpoint, + read_only: mount.read_only, }; parented_mount From e962d3a9050c021339f1be2be42190ea45fe571b Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 14:42:11 -0800 Subject: [PATCH 05/22] chore(runner): add Runner::exec Signed-off-by: Ariadne Conill --- src/runner.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/runner.rs b/src/runner.rs index bbdee32..1923fcc 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}; @@ -400,4 +401,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")) + } } From 79c2dd87ee06561ad1db8436fa9a0c1d4b0587ac Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 14:42:35 -0800 Subject: [PATCH 06/22] feat(styrojail): wire up mounts Signed-off-by: Ariadne Conill --- bin/styrojail.rs | 73 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/bin/styrojail.rs b/bin/styrojail.rs index 8656b86..7c97790 100644 --- a/bin/styrojail.rs +++ b/bin/styrojail.rs @@ -3,7 +3,9 @@ use std::{env, fs, path::PathBuf, str::FromStr}; use anyhow::{anyhow, Result}; use clap::{Parser}; use env_logger::Env; -use styrolite::runner::CreateRequestBuilder; +use styrolite::config::{MountSpec as StyroMountSpec}; +use styrolite::runner::{CreateRequestBuilder, Runner}; +use styrolite::namespace::Namespace; #[derive(Clone, Debug)] struct ResourceLimit { @@ -25,18 +27,27 @@ struct CliMountSpec { 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, } @@ -45,14 +56,6 @@ fn build_mounts(cli: &Cli) -> Result> { let mut mounts = Vec::new(); if !cli.no_default_mounts { - // 1) /:/ (read-only) - mounts.push(CliMountSpec { - hostpath: "/".into(), - jailpath: "/".into(), - read_write: false, - }); - - // 2) $CWD:$CWD:rw let cwd: PathBuf = env::current_dir() .map_err(|e| anyhow!("failed to get CWD: {e}"))?; @@ -125,20 +128,50 @@ fn parse_resource_limit(s: &str) -> Result { }) } +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: true, + create_mountpoint: true, + read_only: !m.read_write, + ..Default::default() + } +} + fn main() -> Result<()> { let cli = Cli::parse(); - let createreq = CreateRequestBuilder::new(); - - let mut argv = Vec::with_capacity(1 + cli.args.len()); - argv.push(cli.program.clone()); - argv.extend(cli.args.iter().cloned()); + let mut builder = CreateRequestBuilder::new() + .set_rootfs("/") + .set_rootfs_readonly(true) + .set_executable(&cli.program) + .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)?; - eprintln!("no_default_mount = {}", cli.no_default_mounts); - eprintln!("mounts = {:?}", mounts); - eprintln!("limits = {:?}", cli.limit); - eprintln!("exec argv = {:?}", argv); - - Ok(()) + 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")) } From eec818c4557a87628a05b734c8db284cba9173f4 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 14:46:42 -0800 Subject: [PATCH 07/22] chore(mount): cleanup some mount(2) semantics Signed-off-by: Ariadne Conill --- src/mount.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/mount.rs b/src/mount.rs index 4ea4cb0..0b00b41 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -27,7 +27,7 @@ impl Mountable for MountSpec { 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() @@ -40,6 +40,7 @@ impl Mountable for MountSpec { } let mut flags: c_ulong = libc::MS_SILENT; + let mut need_remount = false; if self.bind { flags |= libc::MS_BIND; @@ -53,19 +54,33 @@ impl Mountable for MountSpec { flags |= libc::MS_REC; } + unsafe { + let result = libc::mount(source_p, target_p, fstype_p, flags, ptr::null()); + + if result < 0 { + bail!("unable to mount"); + } + } + if self.safe { flags |= libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC; + need_remount = true; } if self.read_only { flags |= libc::MS_RDONLY; + need_remount = true; } - unsafe { - let result = libc::mount(source_p, target_p, fstype_p, flags, ptr::null()); + if need_remount { + flags |= libc::MS_REMOUNT; - if result < 0 { - bail!("unable to mount"); + unsafe { + let result = libc::mount(ptr::null(), target_p, ptr::null(), flags, ptr::null()); + + if result < 0 { + bail!("unable to mount"); + } } } From 04e2971d7e9ad48876fe251f6702a6902aca3c4e Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 14:47:07 -0800 Subject: [PATCH 08/22] chore(wrap): if we are not changing our rootfs, do not pivot_root(2) --- src/wrap.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wrap.rs b/src/wrap.rs index fb6912e..43172f8 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -391,7 +391,9 @@ impl CreateRequest { } } - newroot.pivot().expect("failed to pivot to new rootfs"); + if newroot.source.clone().unwrap() != "/" { + newroot.pivot().expect("failed to pivot to new rootfs"); + } Ok(()) } From 8602b65bb364b4e8f87ffb272469f74dfcd5ea06 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 14:52:14 -0800 Subject: [PATCH 09/22] chore(config): add CreateRequest.skip_two_stage_userns Signed-off-by: Ariadne Conill --- src/config.rs | 4 ++++ src/runner.rs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/config.rs b/src/config.rs index 441c6ec..e4d648c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -129,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)] diff --git a/src/runner.rs b/src/runner.rs index 1923fcc..5969adc 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -162,6 +162,11 @@ impl CreateRequestBuilder { 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 From 94ff55e458454f7b91024414d0de9d91349f32dc Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 14:52:44 -0800 Subject: [PATCH 10/22] chore(styrojail): use skip_two_stage_userns so unprivileged userns can be used Signed-off-by: Ariadne Conill --- bin/styrojail.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/styrojail.rs b/bin/styrojail.rs index 7c97790..b89484b 100644 --- a/bin/styrojail.rs +++ b/bin/styrojail.rs @@ -148,6 +148,7 @@ fn main() -> Result<()> { let mut builder = CreateRequestBuilder::new() .set_rootfs("/") .set_rootfs_readonly(true) + .set_skip_two_stage_userns(true) .set_executable(&cli.program) .push_namespace(Namespace::Uts) .push_namespace(Namespace::Time) From 7bda7779dd2f1d1070704e17e9dec53debaebcf4 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 14:58:44 -0800 Subject: [PATCH 11/22] chore(wrap): hook up skip_two_stage_userns Signed-off-by: Ariadne Conill --- src/wrap.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/wrap.rs b/src/wrap.rs index 43172f8..ad68e9a 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -431,11 +431,19 @@ 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)?; @@ -512,7 +520,7 @@ impl Wrappable for CreateRequest { 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])?; } From 9e8307aba6eacb540eaba8243a458fd65f6d31ab Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 15:02:20 -0800 Subject: [PATCH 12/22] chore(styrojail): clean up unused imports --- bin/styrojail.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/styrojail.rs b/bin/styrojail.rs index b89484b..364a126 100644 --- a/bin/styrojail.rs +++ b/bin/styrojail.rs @@ -1,8 +1,7 @@ -use std::{env, fs, path::PathBuf, str::FromStr}; +use std::{env, path::PathBuf}; use anyhow::{anyhow, Result}; use clap::{Parser}; -use env_logger::Env; use styrolite::config::{MountSpec as StyroMountSpec}; use styrolite::runner::{CreateRequestBuilder, Runner}; use styrolite::namespace::Namespace; From e28e61ed773c620fe71b13888535fd6ff8c8543c Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 15:02:46 -0800 Subject: [PATCH 13/22] chore(styrojail): sync host UID/GID Signed-off-by: Ariadne Conill --- bin/styrojail.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bin/styrojail.rs b/bin/styrojail.rs index 364a126..bfc5d62 100644 --- a/bin/styrojail.rs +++ b/bin/styrojail.rs @@ -51,6 +51,10 @@ struct Cli { 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(); @@ -143,12 +147,16 @@ fn to_styrolite_mount(m: &CliMountSpec) -> StyroMountSpec { } 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) .push_namespace(Namespace::Uts) .push_namespace(Namespace::Time) .push_namespace(Namespace::Pid) From 65454bf665da55305a4567cb3769b601e62b027c Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 15:28:01 -0800 Subject: [PATCH 14/22] chore(styrojail): sync UID/GID config Signed-off-by: Ariadne Conill --- bin/styrojail.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/bin/styrojail.rs b/bin/styrojail.rs index bfc5d62..d27a379 100644 --- a/bin/styrojail.rs +++ b/bin/styrojail.rs @@ -2,7 +2,7 @@ use std::{env, path::PathBuf}; use anyhow::{anyhow, Result}; use clap::{Parser}; -use styrolite::config::{MountSpec as StyroMountSpec}; +use styrolite::config::{IdMapping, MountSpec as StyroMountSpec}; use styrolite::runner::{CreateRequestBuilder, Runner}; use styrolite::namespace::Namespace; @@ -139,7 +139,7 @@ fn to_styrolite_mount(m: &CliMountSpec) -> StyroMountSpec { bind: true, recurse: false, unshare: false, - safe: true, + safe: false, create_mountpoint: true, read_only: !m.read_write, ..Default::default() @@ -157,6 +157,18 @@ fn main() -> Result<()> { .set_executable(&cli.program) .set_uid(uid) .set_gid(gid) + .set_setgroups_deny(true) + .set_workload_id("styrojail") + .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) From 0d5f993fa0481cd1494bdc62bb0d3778995db234 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 16:17:55 -0800 Subject: [PATCH 15/22] chore: cargo fmt --- bin/styrojail.rs | 11 +++++------ src/runner.rs | 5 ++++- src/wrap.rs | 8 ++------ 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/bin/styrojail.rs b/bin/styrojail.rs index d27a379..2d322a7 100644 --- a/bin/styrojail.rs +++ b/bin/styrojail.rs @@ -1,10 +1,10 @@ use std::{env, path::PathBuf}; -use anyhow::{anyhow, Result}; -use clap::{Parser}; +use anyhow::{Result, anyhow}; +use clap::Parser; use styrolite::config::{IdMapping, MountSpec as StyroMountSpec}; -use styrolite::runner::{CreateRequestBuilder, Runner}; use styrolite::namespace::Namespace; +use styrolite::runner::{CreateRequestBuilder, Runner}; #[derive(Clone, Debug)] struct ResourceLimit { @@ -23,7 +23,7 @@ struct CliMountSpec { #[command( name = "styrojail", about = "convenient jail-style styrolite frontend", - version, + version )] struct Cli { /// Path to styrolite binary (default: resolved via PATH) @@ -59,8 +59,7 @@ 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: PathBuf = env::current_dir().map_err(|e| anyhow!("failed to get CWD: {e}"))?; let cwd_str = cwd .to_str() diff --git a/src/runner.rs b/src/runner.rs index 5969adc..862e8a5 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -162,7 +162,10 @@ impl CreateRequestBuilder { self } - pub fn set_skip_two_stage_userns(mut self, skip_two_stage_userns: bool) -> CreateRequestBuilder { + 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 } diff --git a/src/wrap.rs b/src/wrap.rs index ad68e9a..f5acfc4 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -307,9 +307,7 @@ impl CreateRequest { .clone() .expect("expected rootfs to be configured"); - let rootfs_readonly = self - .rootfs_readonly - .unwrap_or(false); + 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 @@ -431,9 +429,7 @@ impl Wrappable for CreateRequest { warn!("unable to prepare cgroup: {e}"); } - let skip_two_stage_userns = self - .skip_two_stage_userns - .unwrap_or(false); + let skip_two_stage_userns = self.skip_two_stage_userns.unwrap_or(false); let first_level_ns = if !skip_two_stage_userns { target_ns From 291f18957856fd564c446e547ad475d7ccd8bc3a Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 16:23:25 -0800 Subject: [PATCH 16/22] chore(mount): add wrappers for new-style Linux mount API Signed-off-by: Ariadne Conill --- src/mount.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/mount.rs b/src/mount.rs index 0b00b41..8f7e504 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -1,6 +1,9 @@ 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}; @@ -8,6 +11,76 @@ 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,6 +91,29 @@ 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 mount(&self) -> Result<()> { let source = unpack(self.source.clone()); From 8f0b6e7a54e452c40ff370005459e413a3b6ce95 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 16:31:29 -0800 Subject: [PATCH 17/22] chore(mount): use mount_setattr(2) instead of remounting to change mount flags Signed-off-by: Ariadne Conill --- src/mount.rs | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/mount.rs b/src/mount.rs index 8f7e504..44fe2aa 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -6,7 +6,7 @@ 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}; @@ -122,12 +122,14 @@ impl Mountable for MountSpec { } else { source.as_ptr() }; + let fstype = unpack(self.fstype.clone()); 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(); @@ -136,7 +138,6 @@ impl Mountable for MountSpec { } let mut flags: c_ulong = libc::MS_SILENT; - let mut need_remount = false; if self.bind { flags |= libc::MS_BIND; @@ -151,33 +152,38 @@ impl Mountable for MountSpec { } unsafe { - let result = libc::mount(source_p, target_p, fstype_p, flags, ptr::null()); - - if result < 0 { + 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; - need_remount = true; + set |= libc::MOUNT_ATTR_NOSUID as c_ulong; + set |= libc::MOUNT_ATTR_NODEV as c_ulong; + set |= libc::MOUNT_ATTR_NOEXEC as c_ulong; } if self.read_only { - flags |= libc::MS_RDONLY; - need_remount = true; + set |= libc::MOUNT_ATTR_RDONLY as c_ulong; } - if need_remount { - flags |= libc::MS_REMOUNT; - - unsafe { - let result = libc::mount(ptr::null(), target_p, ptr::null(), flags, ptr::null()); + 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; - if result < 0 { - bail!("unable to mount"); - } + 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(()) From d2448892b54bfb17b934d4074ae15feb06467450 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 18:23:29 -0800 Subject: [PATCH 18/22] chore(mount): add Mountable::seal() Signed-off-by: Ariadne Conill --- src/config.rs | 3 +++ src/mount.rs | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index e4d648c..fc2a6a0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -259,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 44fe2aa..5232fee 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -115,6 +115,20 @@ pub fn mount_setattr_fd( } 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() { @@ -134,7 +148,7 @@ impl Mountable for MountSpec { 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; From b40abeae99d58d8a89b1313445b44cbc54512d6c Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 18:25:04 -0800 Subject: [PATCH 19/22] feature(styrojail): add necessary hacks for / as detached mountpoint --- src/wrap.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/wrap.rs b/src/wrap.rs index f5acfc4..0e8c066 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -302,7 +302,7 @@ 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"); @@ -328,6 +328,46 @@ impl CreateRequest { .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, // nodev/nosuid/noexec on the staging mount is fine + create_mountpoint: true, // ensure stage_base exists + 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()), @@ -338,11 +378,15 @@ impl CreateRequest { unshare: false, safe: false, create_mountpoint: false, - read_only: rootfs_readonly, + 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()), @@ -389,9 +433,7 @@ impl CreateRequest { } } - if newroot.source.clone().unwrap() != "/" { - newroot.pivot().expect("failed to pivot to new rootfs"); - } + newroot.pivot().expect("failed to pivot to new rootfs"); Ok(()) } From 97aca7ce67ffc5ac600076ff7c4553f59d71c2b0 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 18:25:57 -0800 Subject: [PATCH 20/22] chore(wrap): defer mount configuration until UID/GID namespace has been configured Signed-off-by: Ariadne Conill --- src/wrap.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/wrap.rs b/src/wrap.rs index 0e8c066..631ffbe 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -522,12 +522,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. @@ -548,14 +544,6 @@ 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 !skip_two_stage_userns && target_ns.contains(&Namespace::User) { @@ -573,6 +561,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. From cef20163e7992d4eb298f336e666c67700e891c0 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 18:35:08 -0800 Subject: [PATCH 21/22] chore(styrojail): set CWD explicitly Signed-off-by: Ariadne Conill --- bin/styrojail.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/styrojail.rs b/bin/styrojail.rs index 2d322a7..23ebc8a 100644 --- a/bin/styrojail.rs +++ b/bin/styrojail.rs @@ -157,7 +157,8 @@ fn main() -> Result<()> { .set_uid(uid) .set_gid(gid) .set_setgroups_deny(true) - .set_workload_id("styrojail") + .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, From 6b0dd81ac1ad5434edfbb2b2876037d1b2862abf Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Thu, 12 Feb 2026 18:35:15 -0800 Subject: [PATCH 22/22] chore: cargo fmt Signed-off-by: Ariadne Conill --- src/mount.rs | 15 ++++++--------- src/wrap.rs | 10 ++++++---- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/mount.rs b/src/mount.rs index 5232fee..14c9551 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -101,11 +101,7 @@ pub fn move_mount_fd_to(fd: &OwnedFd, target: &str) -> io::Result<()> { ) } -pub fn mount_setattr_fd( - fd: &OwnedFd, - recursive: bool, - attr: &libc::mount_attr, -) -> io::Result<()> { +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; @@ -118,8 +114,10 @@ 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 + 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() }; @@ -196,8 +194,7 @@ impl Mountable for MountSpec { msaflags |= libc::AT_RECURSIVE as c_uint; } - mount_setattr(libc::AT_FDCWD, &self.target, msaflags, &attr) - .map_err(|e| anyhow!(e))?; + mount_setattr(libc::AT_FDCWD, &self.target, msaflags, &attr).map_err(|e| anyhow!(e))?; } Ok(()) diff --git a/src/wrap.rs b/src/wrap.rs index 631ffbe..8f3d6aa 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -332,7 +332,7 @@ impl CreateRequest { // 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()?); + 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. @@ -343,8 +343,8 @@ impl CreateRequest { bind: false, recurse: false, unshare: false, - safe: true, // nodev/nosuid/noexec on the staging mount is fine - create_mountpoint: true, // ensure stage_base exists + safe: true, + create_mountpoint: true, read_only: false, }; stage_tmpfs.mount().expect("failed to mount staging tmpfs"); @@ -363,7 +363,9 @@ impl CreateRequest { create_mountpoint: false, read_only: false, }; - stage_bind.mount().expect("failed to bind / into staging root"); + stage_bind + .mount() + .expect("failed to bind / into staging root"); rootfs = stage_root.to_string(); }