From 54865484799a14147f7d909bfac2f0ba230d3d55 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Tue, 28 Nov 2023 15:14:56 +0100 Subject: [PATCH 01/10] Add PID namespace support This patch changes the API to require spawning a new process to enable the sandbox. This is necessary because PID namespaces on Linux only take effect for new processes. As a result, the new process will be created as PID 1 without access to any other process through system calls like `kill` or the `procfs` filesystem interface. This fixes a gap in Birdcage's environment variable isolation where it was still possible to read the unsandboxed environment from `/proc/self/environ`. Since the new process takes on the responsibility of PID 1 in the new namespace, it will automatically be made parent for any orphaned process. Currently these processes will remain zombies until the sandboxed process is shut down. --- CHANGELOG.md | 1 + Cargo.toml | 106 +------------ examples/sandbox.rs | 9 +- integration/canonicalize.rs | 18 +++ integration/consistent_id_mappings.rs | 37 +++++ {tests => integration}/env.rs | 13 +- integration/exec.rs | 23 +++ integration/exec_symlinked_dir.rs | 43 +++++ integration/exec_symlinked_dirs_exec.rs | 43 +++++ integration/exec_symlinked_file.rs | 45 ++++++ integration/fs.rs | 47 ++++++ {tests => integration}/fs_broken_symlink.rs | 30 +++- integration/fs_null.rs | 18 +++ integration/fs_readonly.rs | 44 ++++++ integration/fs_restrict_child.rs | 49 ++++++ integration/fs_symlink.rs | 56 +++++++ integration/fs_symlink_dir.rs | 45 ++++++ integration/fs_symlink_dir_separate_perms.rs | 49 ++++++ integration/fs_write_also_read.rs | 42 +++++ {tests => integration}/full_env.rs | 13 +- {tests => integration}/full_sandbox.rs | 32 +++- integration/harness.rs | 157 +++++++++++++++++++ {tests => integration}/missing_exception.rs | 13 +- integration/net.rs | 18 +++ {tests => integration}/seccomp.rs | 15 +- src/lib.rs | 22 +-- src/linux/mod.rs | 96 ++++++++++-- src/linux/namespaces.rs | 62 ++++---- src/macos.rs | 5 +- tests/canonicalize.rs | 13 -- tests/consistent_id_mappings.rs | 21 --- tests/exec.rs | 26 --- tests/exec_symlinked_dir.rs | 31 ---- tests/exec_symlinked_dirs_exec.rs | 31 ---- tests/exec_symlinked_file.rs | 33 ---- tests/fs.rs | 27 ---- tests/fs_null.rs | 13 -- tests/fs_readonly.rs | 25 --- tests/fs_restrict_child.rs | 29 ---- tests/fs_symlink.rs | 37 ----- tests/fs_symlink_dir.rs | 27 ---- tests/fs_symlink_dir_separate_perms.rs | 30 ---- tests/fs_write_also_read.rs | 23 --- tests/net.rs | 12 -- 44 files changed, 954 insertions(+), 575 deletions(-) create mode 100644 integration/canonicalize.rs create mode 100644 integration/consistent_id_mappings.rs rename {tests => integration}/env.rs (62%) create mode 100644 integration/exec.rs create mode 100644 integration/exec_symlinked_dir.rs create mode 100644 integration/exec_symlinked_dirs_exec.rs create mode 100644 integration/exec_symlinked_file.rs create mode 100644 integration/fs.rs rename {tests => integration}/fs_broken_symlink.rs (53%) create mode 100644 integration/fs_null.rs create mode 100644 integration/fs_readonly.rs create mode 100644 integration/fs_restrict_child.rs create mode 100644 integration/fs_symlink.rs create mode 100644 integration/fs_symlink_dir.rs create mode 100644 integration/fs_symlink_dir_separate_perms.rs create mode 100644 integration/fs_write_also_read.rs rename {tests => integration}/full_env.rs (54%) rename {tests => integration}/full_sandbox.rs (66%) create mode 100644 integration/harness.rs rename {tests => integration}/missing_exception.rs (62%) create mode 100644 integration/net.rs rename {tests => integration}/seccomp.rs (75%) delete mode 100644 tests/canonicalize.rs delete mode 100644 tests/consistent_id_mappings.rs delete mode 100644 tests/exec.rs delete mode 100644 tests/exec_symlinked_dir.rs delete mode 100644 tests/exec_symlinked_dirs_exec.rs delete mode 100644 tests/exec_symlinked_file.rs delete mode 100644 tests/fs.rs delete mode 100644 tests/fs_null.rs delete mode 100644 tests/fs_readonly.rs delete mode 100644 tests/fs_restrict_child.rs delete mode 100644 tests/fs_symlink.rs delete mode 100644 tests/fs_symlink_dir.rs delete mode 100644 tests/fs_symlink_dir_separate_perms.rs delete mode 100644 tests/fs_write_also_read.rs delete mode 100644 tests/net.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b859a0b..827e723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - (Linux) Sandbox exceptions for symbolic links - (macOS) Modifying exceptions for paths affected by existing exceptions - (Linux) Symlink/Canonical path's exceptions overriding each other +- (Linux) PID namespace support ## [v0.5.0] - 2023-10-13 diff --git a/Cargo.toml b/Cargo.toml index 4715eb4..582ea31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,108 +10,8 @@ license = "GPL-3.0-or-later" edition = "2021" [[test]] -name = "canonicalize" -path = "tests/canonicalize.rs" -harness = false - -[[test]] -name = "env" -path = "tests/env.rs" -harness = false - -[[test]] -name = "exec" -path = "tests/exec.rs" -harness = false - -[[test]] -name = "exec_symlinked_dir" -path = "tests/exec_symlinked_dir.rs" -harness = false - -[[test]] -name = "exec_symlinked_file" -path = "tests/exec_symlinked_file.rs" -harness = false - -[[test]] -name = "exec_symlinked_dirs_exec" -path = "tests/exec_symlinked_dirs_exec.rs" -harness = false - -[[test]] -name = "fs" -path = "tests/fs.rs" -harness = false - -[[test]] -name = "fs_readonly" -path = "tests/fs_readonly.rs" -harness = false - -[[test]] -name = "fs_restrict_child" -path = "tests/fs_restrict_child.rs" -harness = false - -[[test]] -name = "fs_write_also_read" -path = "tests/fs_write_also_read.rs" -harness = false - -[[test]] -name = "fs_symlink" -path = "tests/fs_symlink.rs" -harness = false - -[[test]] -name = "fs_symlink_dir" -path = "tests/fs_symlink_dir.rs" -harness = false - -[[test]] -name = "fs_broken_symlink" -path = "tests/fs_broken_symlink.rs" -harness = false - -[[test]] -name = "fs_symlink_dir_separate_perms" -path = "tests/fs_symlink_dir_separate_perms.rs" -harness = false - -[[test]] -name = "fs_null" -path = "tests/fs_null.rs" -harness = false - -[[test]] -name = "full_env" -path = "tests/full_env.rs" -harness = false - -[[test]] -name = "full_sandbox" -path = "tests/full_sandbox.rs" -harness = false - -[[test]] -name = "net" -path = "tests/net.rs" -harness = false - -[[test]] -name = "consistent_id_mappings" -path = "tests/consistent_id_mappings.rs" -harness = false - -[[test]] -name = "seccomp" -path = "tests/seccomp.rs" -harness = false - -[[test]] -name = "missing_exception" -path = "tests/missing_exception.rs" +name = "harness" +path = "integration/harness.rs" harness = false [target.'cfg(target_os = "linux")'.dependencies] @@ -120,6 +20,8 @@ libc = "0.2.132" [dev-dependencies] clap = { version = "3.2.17", features = ["derive"] } +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" tempfile = "3.3.0" [dependencies] diff --git a/examples/sandbox.rs b/examples/sandbox.rs index e8fbcb1..e1f3544 100644 --- a/examples/sandbox.rs +++ b/examples/sandbox.rs @@ -65,11 +65,12 @@ fn main() -> Result<(), Box> { } // Activate sandbox. - birdcage.lock().unwrap(); + let mut command = Command::new(cli.cmd); + command.args(&cli.args); + let mut child = birdcage.spawn(command)?; - // Run the command. - let status = Command::new(cli.cmd).args(&cli.args).spawn()?.wait()?; - let exit_code = status.code().unwrap_or(111); + // Wait for sandboxee to exit. + let exit_code = child.wait()?.code().unwrap_or(111); process::exit(exit_code); } diff --git a/integration/canonicalize.rs b/integration/canonicalize.rs new file mode 100644 index 0000000..8c9bb26 --- /dev/null +++ b/integration/canonicalize.rs @@ -0,0 +1,18 @@ +use std::fs; + +use birdcage::{Birdcage, Exception, Sandbox}; + +use crate::TestSetup; + +pub fn setup() -> TestSetup { + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::Read("./".into())).unwrap(); + + TestSetup { sandbox, data: String::new() } +} + +pub fn validate(_data: String) { + // Check for success on reading the `Cargo.toml` file. + let file = fs::read_to_string("./Cargo.toml").unwrap(); + assert!(file.contains("birdcage")); +} diff --git a/integration/consistent_id_mappings.rs b/integration/consistent_id_mappings.rs new file mode 100644 index 0000000..5b77326 --- /dev/null +++ b/integration/consistent_id_mappings.rs @@ -0,0 +1,37 @@ +use birdcage::{Birdcage, Sandbox}; +use serde::{Deserialize, Serialize}; + +use crate::TestSetup; + +#[derive(Serialize, Deserialize)] +struct TestData { + uid: u32, + gid: u32, + euid: u32, + egid: u32, +} + +pub fn setup() -> TestSetup { + let uid = unsafe { libc::getuid() }; + let gid = unsafe { libc::getgid() }; + let euid = unsafe { libc::geteuid() }; + let egid = unsafe { libc::getegid() }; + + let sandbox = Birdcage::new(); + + // Serialize test data. + let data = TestData { uid, gid, euid, egid }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); + + assert_eq!(data.uid, unsafe { libc::getuid() }); + assert_eq!(data.gid, unsafe { libc::getgid() }); + assert_eq!(data.euid, unsafe { libc::geteuid() }); + assert_eq!(data.egid, unsafe { libc::getegid() }); +} diff --git a/tests/env.rs b/integration/env.rs similarity index 62% rename from tests/env.rs rename to integration/env.rs index 8ef1439..259b346 100644 --- a/tests/env.rs +++ b/integration/env.rs @@ -2,16 +2,21 @@ use std::env; use birdcage::{Birdcage, Exception, Sandbox}; -fn main() { +use crate::TestSetup; + +pub fn setup() -> TestSetup { // Setup our environment variables env::set_var("PUBLIC", "GOOD"); env::set_var("PRIVATE", "BAD"); // Activate our sandbox. - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::Environment("PUBLIC".into())).unwrap(); - birdcage.lock().unwrap(); + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::Environment("PUBLIC".into())).unwrap(); + + TestSetup { sandbox, data: String::new() } +} +pub fn validate(_data: String) { // The `PUBLIC` environment variable can be accessed. assert_eq!(env::var("PUBLIC"), Ok("GOOD".into())); diff --git a/integration/exec.rs b/integration/exec.rs new file mode 100644 index 0000000..1557360 --- /dev/null +++ b/integration/exec.rs @@ -0,0 +1,23 @@ +use std::fs; +use std::process::Command; + +use birdcage::{Birdcage, Exception, Sandbox}; + +use crate::TestSetup; + +pub fn setup() -> TestSetup { + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::ExecuteAndRead("/usr/bin/true".into())).unwrap(); + + TestSetup { sandbox, data: String::new() } +} + +pub fn validate(_data: String) { + // Check for success when executing `true`. + let cmd = Command::new("/usr/bin/true").status().unwrap(); + assert!(cmd.success()); + + // Check for success on reading the `true` file. + let cmd_file = fs::read("/usr/bin/true"); + assert!(cmd_file.is_ok()); +} diff --git a/integration/exec_symlinked_dir.rs b/integration/exec_symlinked_dir.rs new file mode 100644 index 0000000..2f141b3 --- /dev/null +++ b/integration/exec_symlinked_dir.rs @@ -0,0 +1,43 @@ +use std::os::unix::fs as unixfs; +use std::path::PathBuf; +use std::process::Command; + +use birdcage::{Birdcage, Exception, Sandbox}; +use serde::{Deserialize, Serialize}; + +use crate::TestSetup; + +#[derive(Serialize, Deserialize)] +struct TestData { + symlink_dir: PathBuf, +} + +pub fn setup() -> TestSetup { + // Create symlinked executable dir. + let tempdir = tempfile::tempdir().unwrap().into_path(); + let symlink_dir = tempdir.join("bin"); + unixfs::symlink("/usr/bin", &symlink_dir).unwrap(); + + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::ExecuteAndRead(symlink_dir.clone())).unwrap(); + + // Serialize test data. + let data = TestData { symlink_dir }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); + + // Ensure symlinked dir's executable works. + let symlink_dir_exec = data.symlink_dir.join("true"); + let cmd = Command::new(symlink_dir_exec).status().unwrap(); + assert!(cmd.success()); + + // Ensure original dir's executable works. + let cmd = Command::new("/usr/bin/true").status().unwrap(); + assert!(cmd.success()); +} diff --git a/integration/exec_symlinked_dirs_exec.rs b/integration/exec_symlinked_dirs_exec.rs new file mode 100644 index 0000000..d4e9c6b --- /dev/null +++ b/integration/exec_symlinked_dirs_exec.rs @@ -0,0 +1,43 @@ +use std::os::unix::fs as unixfs; +use std::path::PathBuf; +use std::process::Command; + +use birdcage::{Birdcage, Exception, Sandbox}; +use serde::{Deserialize, Serialize}; + +use crate::TestSetup; + +#[derive(Serialize, Deserialize)] +struct TestData { + symlink_dir_exec: PathBuf, +} + +pub fn setup() -> TestSetup { + // Create symlinked executable dir. + let tempdir = tempfile::tempdir().unwrap().into_path(); + let symlink_dir = tempdir.join("bin"); + let symlink_dir_exec = symlink_dir.join("true"); + unixfs::symlink("/usr/bin", &symlink_dir).unwrap(); + + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::ExecuteAndRead(symlink_dir_exec.clone())).unwrap(); + + // Serialize test data. + let data = TestData { symlink_dir_exec }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); + + // Ensure symlinked dir's executable works. + let cmd = Command::new(data.symlink_dir_exec).status().unwrap(); + assert!(cmd.success()); + + // Ensure original dir's executable works. + let cmd = Command::new("/usr/bin/true").status().unwrap(); + assert!(cmd.success()); +} diff --git a/integration/exec_symlinked_file.rs b/integration/exec_symlinked_file.rs new file mode 100644 index 0000000..75960a0 --- /dev/null +++ b/integration/exec_symlinked_file.rs @@ -0,0 +1,45 @@ +use std::fs; +use std::os::unix::fs as unixfs; +use std::path::PathBuf; +use std::process::Command; + +use birdcage::{Birdcage, Exception, Sandbox}; +use serde::{Deserialize, Serialize}; + +use crate::TestSetup; + +#[derive(Serialize, Deserialize)] +struct TestData { + symlink_exec: PathBuf, +} + +pub fn setup() -> TestSetup { + // Create symlinked executable. + let tempdir = tempfile::tempdir().unwrap().into_path(); + let exec_dir = tempdir.join("bin"); + fs::create_dir(&exec_dir).unwrap(); + let symlink_exec = exec_dir.join("true"); + unixfs::symlink("/usr/bin/true", &symlink_exec).unwrap(); + + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::ExecuteAndRead(symlink_exec.clone())).unwrap(); + + // Serialize test data. + let data = TestData { symlink_exec }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); + + // Ensure symlinked executable works. + let cmd = Command::new(data.symlink_exec).status().unwrap(); + assert!(cmd.success()); + + // Ensure original executable works. + let cmd = Command::new("/usr/bin/true").status().unwrap(); + assert!(cmd.success()); +} diff --git a/integration/fs.rs b/integration/fs.rs new file mode 100644 index 0000000..70e90ac --- /dev/null +++ b/integration/fs.rs @@ -0,0 +1,47 @@ +use std::fs; +use std::path::PathBuf; + +use birdcage::{Birdcage, Exception, Sandbox}; +use serde::{Deserialize, Serialize}; +use tempfile::NamedTempFile; + +use crate::TestSetup; + +const FILE_CONTENT: &str = "expected content"; + +#[derive(Serialize, Deserialize)] +struct TestData { + public_path: PathBuf, + private_path: PathBuf, +} + +pub fn setup() -> TestSetup { + // Setup our test files. + let private_path = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap(); + fs::write(&private_path, FILE_CONTENT.as_bytes()).unwrap(); + let public_path = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap(); + fs::write(&public_path, FILE_CONTENT.as_bytes()).unwrap(); + + // Setup sandbox exceptions. + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::Read(public_path.clone())).unwrap(); + + // Serialize test data. + let data = TestData { public_path, private_path }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); + + // Access to the public file is allowed. + let content = fs::read_to_string(data.public_path).unwrap(); + assert_eq!(content, FILE_CONTENT); + + // Access to the private file is prohibited. + let result = fs::read_to_string(data.private_path); + assert!(result.is_err()); +} diff --git a/tests/fs_broken_symlink.rs b/integration/fs_broken_symlink.rs similarity index 53% rename from tests/fs_broken_symlink.rs rename to integration/fs_broken_symlink.rs index e8d8bce..7981dba 100644 --- a/tests/fs_broken_symlink.rs +++ b/integration/fs_broken_symlink.rs @@ -4,9 +4,17 @@ use std::path::PathBuf; use birdcage::error::Error; use birdcage::{Birdcage, Exception, Sandbox}; +use serde::{Deserialize, Serialize}; use tempfile::NamedTempFile; -fn main() { +use crate::TestSetup; + +#[derive(Serialize, Deserialize)] +struct TestData { + symlink: PathBuf, +} + +pub fn setup() -> TestSetup { // Setup a symlink without target. let tempfile = NamedTempFile::new().unwrap(); let tempfile_path = tempfile.path().to_path_buf(); @@ -17,14 +25,24 @@ fn main() { assert!(!tempfile_path.exists()); // Sandbox exception fails with invalid path error. - let mut birdcage = Birdcage::new(); - let result = birdcage.add_exception(Exception::Read(symlink.clone())); + let mut sandbox = Birdcage::new(); + let result = sandbox.add_exception(Exception::Read(symlink.clone())); assert!(matches!(result, Err(Error::InvalidPath(_)))); - birdcage.lock().unwrap(); + + // Serialize test data. + let data = TestData { symlink }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); // Read/Write results in error. - let result = fs::read_to_string(&symlink); + let result = fs::read_to_string(&data.symlink); assert!(result.is_err()); - let result = fs::write(&symlink, "bob"); + let result = fs::write(&data.symlink, "bob"); assert!(result.is_err()); } diff --git a/integration/fs_null.rs b/integration/fs_null.rs new file mode 100644 index 0000000..07a7db4 --- /dev/null +++ b/integration/fs_null.rs @@ -0,0 +1,18 @@ +use std::fs; + +use birdcage::{Birdcage, Exception, Sandbox}; + +use crate::TestSetup; + +pub fn setup() -> TestSetup { + // Activate our sandbox. + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::WriteAndRead("/dev/null".into())).unwrap(); + + TestSetup { sandbox, data: String::new() } +} + +pub fn validate(_data: String) { + // Writing to `/dev/null` is allowed. + fs::write("/dev/null", "blub").unwrap(); +} diff --git a/integration/fs_readonly.rs b/integration/fs_readonly.rs new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/integration/fs_readonly.rs @@ -0,0 +1,44 @@ +use std::fs; +use std::path::PathBuf; + +use birdcage::{Birdcage, Exception, Sandbox}; +use serde::{Deserialize, Serialize}; +use tempfile::NamedTempFile; + +use crate::TestSetup; + +const FILE_CONTENT: &str = "expected content"; + +#[derive(Serialize, Deserialize)] +struct TestData { + file: PathBuf, +} + +pub fn setup() -> TestSetup { + // Setup the test file. + let file = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap(); + fs::write(&file, FILE_CONTENT.as_bytes()).unwrap(); + + // Activate our sandbox. + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::Read(file.clone())).unwrap(); + + // Serialize test data. + let data = TestData { file }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); + + // Reading from the file is allowed. + let content = fs::read_to_string(&data.file).unwrap(); + assert_eq!(content, FILE_CONTENT); + + // Writing to the file is prohibited. + let result = fs::write(&data.file, FILE_CONTENT.as_bytes()); + assert!(result.is_err()); +} diff --git a/integration/fs_restrict_child.rs b/integration/fs_restrict_child.rs new file mode 100644 index 0000000..d5811f2 --- /dev/null +++ b/integration/fs_restrict_child.rs @@ -0,0 +1,49 @@ +use std::fs; +use std::path::PathBuf; + +use birdcage::{Birdcage, Exception, Sandbox}; +use serde::{Deserialize, Serialize}; + +use crate::TestSetup; + +const FILE_CONTENT: &str = "expected content"; + +#[derive(Serialize, Deserialize)] +struct TestData { + tempfile: PathBuf, + tempdir: PathBuf, +} + +pub fn setup() -> TestSetup { + // Setup our test tree. + let tempdir = tempfile::tempdir().unwrap().into_path(); + let tempfile = tempdir.join("target-file"); + fs::write(&tempfile, FILE_CONTENT.as_bytes()).unwrap(); + + // Setup sandbox, allowing read/write to dir, but only read for the file. + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::WriteAndRead(tempdir.clone())).unwrap(); + sandbox.add_exception(Exception::Read(tempfile.clone())).unwrap(); + + // Serialize test data. + let data = TestData { tempfile, tempdir }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); + + // Write access to directory works. + fs::create_dir(data.tempdir.join("boop")).unwrap(); + + // Read access to file works. + let content = fs::read_to_string(&data.tempfile).unwrap(); + assert_eq!(content, FILE_CONTENT); + + // Write access to file is denied. + let result = fs::write(&data.tempfile, "no"); + assert!(result.is_err()); +} diff --git a/integration/fs_symlink.rs b/integration/fs_symlink.rs new file mode 100644 index 0000000..034fb68 --- /dev/null +++ b/integration/fs_symlink.rs @@ -0,0 +1,56 @@ +use std::fs; +use std::os::unix::fs as unixfs; +use std::path::PathBuf; + +use birdcage::{Birdcage, Exception, Sandbox}; +use serde::{Deserialize, Serialize}; +use tempfile::NamedTempFile; + +use crate::TestSetup; + +const FILE_CONTENT: &str = "expected content"; + +#[derive(Serialize, Deserialize)] +struct TestData { + private: PathBuf, + public: PathBuf, +} + +pub fn setup() -> TestSetup { + // Setup our test files. + let private_path = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap(); + fs::write(&private_path, FILE_CONTENT.as_bytes()).unwrap(); + let public_path = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap(); + fs::write(&public_path, FILE_CONTENT.as_bytes()).unwrap(); + + // Create symlinks for the files. + let private_str = private_path.to_string_lossy() + "_tmpfile"; + let private = PathBuf::from(private_str.as_ref()); + let public_str = public_path.to_string_lossy() + "_tmpfile"; + let public = PathBuf::from(public_str.as_ref()); + unixfs::symlink(&private_path, &private).unwrap(); + unixfs::symlink(&public_path, &public).unwrap(); + + // Activate our sandbox. + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::Read(public.clone())).unwrap(); + + // Serialize test data. + let data = TestData { private, public }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); + + // Access to the public file is allowed. + let content = fs::read_to_string(&data.public).unwrap(); + assert_eq!(content, FILE_CONTENT); + + // Access to the private file is prohibited. + let result = fs::read_to_string(&data.private); + assert!(result.is_err()); +} diff --git a/integration/fs_symlink_dir.rs b/integration/fs_symlink_dir.rs new file mode 100644 index 0000000..5ec06f4 --- /dev/null +++ b/integration/fs_symlink_dir.rs @@ -0,0 +1,45 @@ +use std::fs; +use std::os::unix::fs as unixfs; +use std::path::PathBuf; + +use birdcage::{Birdcage, Exception, Sandbox}; +use serde::{Deserialize, Serialize}; +use tempfile::TempDir; + +use crate::TestSetup; + +const FILE_CONTENT: &str = "expected content"; + +#[derive(Serialize, Deserialize)] +struct TestData { + symlink_path: PathBuf, +} + +pub fn setup() -> TestSetup { + // Setup our test directory. + let tempdir = TempDir::new().unwrap().into_path(); + let symlink_str = tempdir.to_string_lossy() + "_tmpfile"; + let symlink_path = PathBuf::from(symlink_str.as_ref()); + unixfs::symlink(&tempdir, &symlink_path).unwrap(); + + // Activate our sandbox. + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::WriteAndRead(symlink_path.clone())).unwrap(); + + // Serialize test data. + let data = TestData { symlink_path }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); + + // Try to create a file in the symlinked directory. + let path = data.symlink_path.join("tmpfile"); + fs::write(&path, FILE_CONTENT.as_bytes()).unwrap(); + let content = fs::read_to_string(&path).unwrap(); + assert_eq!(content, FILE_CONTENT); +} diff --git a/integration/fs_symlink_dir_separate_perms.rs b/integration/fs_symlink_dir_separate_perms.rs new file mode 100644 index 0000000..b33eb5f --- /dev/null +++ b/integration/fs_symlink_dir_separate_perms.rs @@ -0,0 +1,49 @@ +use std::fs; +use std::os::unix::fs as unixfs; +use std::path::PathBuf; + +use birdcage::{Birdcage, Exception, Sandbox}; +use serde::{Deserialize, Serialize}; +use tempfile::TempDir; + +use crate::TestSetup; + +const FILE_CONTENT: &str = "expected content"; + +#[derive(Serialize, Deserialize)] +struct TestData { + symlink_src: PathBuf, +} + +pub fn setup() -> TestSetup { + // Setup our test directories. + let tempdir = TempDir::new().unwrap().into_path(); + let symlink_src = tempdir.join("src"); + fs::create_dir(&symlink_src).unwrap(); + let symlink_dst = tempdir.join("dst"); + unixfs::symlink(&symlink_src, &symlink_dst).unwrap(); + + // Add read+write for src, but also add readonly for dst. + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::WriteAndRead(symlink_src.clone())).unwrap(); + sandbox.add_exception(Exception::Read(symlink_dst.clone())).unwrap(); + + // Serialize test data. + let data = TestData { symlink_src }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); + + // Ensure writing works. + let testfile = data.symlink_src.join("file"); + fs::write(&testfile, FILE_CONTENT).unwrap(); + + // Ensure reading works. + let content = fs::read_to_string(&testfile).unwrap(); + assert_eq!(content, FILE_CONTENT); +} diff --git a/integration/fs_write_also_read.rs b/integration/fs_write_also_read.rs new file mode 100644 index 0000000..00e46ad --- /dev/null +++ b/integration/fs_write_also_read.rs @@ -0,0 +1,42 @@ +use std::fs; +use std::path::PathBuf; + +use birdcage::{Birdcage, Exception, Sandbox}; +use serde::{Deserialize, Serialize}; +use tempfile::NamedTempFile; + +use crate::TestSetup; + +const FILE_CONTENT: &str = "expected content"; + +#[derive(Serialize, Deserialize)] +struct TestData { + file: PathBuf, +} + +pub fn setup() -> TestSetup { + // Setup our test files. + let file = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap(); + + // Activate our sandbox. + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::WriteAndRead(file.clone())).unwrap(); + + // Serialize test data. + let data = TestData { file }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); + + // Write access is allowed. + fs::write(&data.file, FILE_CONTENT.as_bytes()).unwrap(); + + // Read access is allowed. + let content = fs::read_to_string(data.file).unwrap(); + assert_eq!(content, FILE_CONTENT); +} diff --git a/tests/full_env.rs b/integration/full_env.rs similarity index 54% rename from tests/full_env.rs rename to integration/full_env.rs index ffb9c8d..3347895 100644 --- a/tests/full_env.rs +++ b/integration/full_env.rs @@ -2,15 +2,20 @@ use std::env; use birdcage::{Birdcage, Exception, Sandbox}; -fn main() { +use crate::TestSetup; + +pub fn setup() -> TestSetup { // Setup our environment variables env::set_var("PUBLIC", "GOOD"); // Activate our sandbox. - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::FullEnvironment).unwrap(); - birdcage.lock().unwrap(); + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::FullEnvironment).unwrap(); + + TestSetup { sandbox, data: String::new() } +} +pub fn validate(_data: String) { // The `PUBLIC` environment variable can be accessed. assert_eq!(env::var("PUBLIC"), Ok("GOOD".into())); } diff --git a/tests/full_sandbox.rs b/integration/full_sandbox.rs similarity index 66% rename from tests/full_sandbox.rs rename to integration/full_sandbox.rs index e2a384d..286b19d 100644 --- a/tests/full_sandbox.rs +++ b/integration/full_sandbox.rs @@ -1,15 +1,24 @@ use std::net::TcpStream; +use std::path::PathBuf; use std::process::Command; use std::{env, fs}; use birdcage::{Birdcage, Sandbox}; +use serde::{Deserialize, Serialize}; use tempfile::NamedTempFile; -fn main() { +use crate::TestSetup; + +#[derive(Serialize, Deserialize)] +struct TestData { + path: PathBuf, +} + +pub fn setup() -> TestSetup { const FILE_CONTENT: &str = "expected content"; // Create testfile. - let path = NamedTempFile::new().unwrap(); + let path = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap(); // Ensure non-sandboxed write works. fs::write(&path, FILE_CONTENT.as_bytes()).unwrap(); @@ -31,15 +40,26 @@ fn main() { env::set_var("TEST", "value"); assert_eq!(env::var("TEST"), Ok("value".into())); - // Activate our sandbox. - Birdcage::new().lock().unwrap(); + // Setup birdcage sandbox. + let sandbox = Birdcage::new(); + + // Serialize test data. + let data = TestData { path }; + let data = serde_json::to_string(&data).unwrap(); + + TestSetup { sandbox, data } +} + +pub fn validate(data: String) { + // Deserialize test data. + let data: TestData = serde_json::from_str(&data).unwrap(); // Ensure sandboxed write is blocked. - let result = fs::write(&path, b"x"); + let result = fs::write(&data.path, b"x"); assert!(result.is_err()); // Ensure sandboxed read is blocked. - let result = fs::read_to_string(path); + let result = fs::read_to_string(data.path); assert!(result.is_err()); // Ensure sandboxed socket connect is blocked. diff --git a/integration/harness.rs b/integration/harness.rs new file mode 100644 index 0000000..970c8a3 --- /dev/null +++ b/integration/harness.rs @@ -0,0 +1,157 @@ +use std::process::{self, Command, Stdio}; + +use birdcage::{Birdcage, Exception, Sandbox}; + +test_mods! { + mod canonicalize; + #[cfg(target_os = "linux")] + mod consistent_id_mappings; + mod env; + mod exec; + mod exec_symlinked_dir; + mod exec_symlinked_dirs_exec; + mod exec_symlinked_file; + mod fs; + mod fs_broken_symlink; + mod fs_null; + mod fs_readonly; + mod fs_restrict_child; + mod fs_symlink; + mod fs_symlink_dir; + mod fs_symlink_dir_separate_perms; + mod fs_write_also_read; + mod full_env; + mod full_sandbox; + mod missing_exception; + mod net; + #[cfg(target_os = "linux")] + mod seccomp; +} + +/// Integration test directory. +const TEST_DIR: &str = "integration"; + +/// Test setup state. +pub struct TestSetup { + pub sandbox: Birdcage, + pub data: String, +} + +fn main() { + let mut args = std::env::args().skip(1); + + // Get test name or spawn all the tests. + let test_name = match args.next() { + Some(test_name) => test_name, + None => { + spawn_tests(); + return; + }, + }; + + // Find test matching the name. + let test = match TESTS.iter().find(|(cmd, ..)| cmd == &test_name) { + Some(test) => test, + None => unreachable!("invalid test module name: {test_name:?}"), + }; + + // Run setup or test validation. + match args.next() { + Some(test_data) => test.2(test_data), + None => run_setup(&test_name, &test.1), + } +} + +/// Reexecute binary to launch tests as separate processes. +/// +/// Returns `true` on success. +fn spawn_tests() { + eprintln!("\nrunning {} tests", TESTS.len()); + + // Spawn child processes for all tests. + let current_exe = std::env::current_exe().unwrap(); + let children: Vec<_> = TESTS + .iter() + .map(|(cmd, ..)| { + let child = + Command::new(¤t_exe).args([cmd]).stderr(Stdio::piped()).spawn().unwrap(); + (cmd, child) + }) + .collect(); + + // Check results for each test. + let mut passed = 0; + for (name, child) in children { + let output = match child.wait_with_output() { + Ok(output) => output, + Err(err) => { + eprintln!("test {TEST_DIR}/{name}.rs ... \x1b[31mHARNESS FAILURE\x1b[0m: {err}"); + continue; + }, + }; + + // Report individual test results. + if !output.status.success() { + eprintln!("test {TEST_DIR}/{name}.rs ... \x1b[31mFAILED\x1b[0m"); + + // Print stderr on failure if there is some. + let stderr = String::from_utf8_lossy(&output.stderr); + if !stderr.is_empty() { + eprintln!("\n---- {TEST_DIR}/{name}.rs stderr ----\n{}\n", stderr.trim()); + } + } else { + eprintln!("test {TEST_DIR}/{name}.rs ... \x1b[32mok\x1b[0m"); + passed += 1; + } + } + + // Print total results. + let failed = TESTS.len() - passed; + if failed > 0 { + eprintln!("\ntest result: \x1b[31mFAILED\x1b[0m. {} passed; {} failed", passed, failed); + } else { + eprintln!("\ntest result: \x1b[32mok\x1b[0m. {} passed; {} failed", passed, failed); + } + + eprintln!(); +} + +/// Run test's setup step and spawn validation child. +fn run_setup(test_name: &str, setup: &fn() -> TestSetup) { + // Run test setup. + let mut test_setup = setup(); + + // Add exceptions to allow self-execution. + let current_exe = std::env::current_exe().unwrap(); + for path in [current_exe.clone(), "/usr/lib".into(), "/lib64".into(), "/lib".into()] { + if path.exists() { + test_setup.sandbox.add_exception(Exception::ExecuteAndRead(path)).unwrap(); + } + } + + // Reexecute test with sandbox enabled. + let mut command = Command::new(current_exe); + command.args([test_name, &test_setup.data.as_str()]); + let child = test_setup.sandbox.spawn(command).unwrap(); + + // Validate test results. + let output = child.wait_with_output().unwrap(); + if !output.status.success() { + process::exit(output.status.code().unwrap_or(1)); + } +} + +#[macro_export] +macro_rules! test_mods { + ($($(#[$cfg:meta])? mod $mod:ident);*;) => { + $( + $( #[$cfg] )? + mod $mod; + )* + + const TESTS: &[(&str, fn() -> $crate::TestSetup, fn(String))] = &[$( + $( #[$cfg] )? + (stringify!($mod), $mod :: setup, $mod :: validate), + )*]; + }; +} diff --git a/tests/missing_exception.rs b/integration/missing_exception.rs similarity index 62% rename from tests/missing_exception.rs rename to integration/missing_exception.rs index e28c2e8..a0ce814 100644 --- a/tests/missing_exception.rs +++ b/integration/missing_exception.rs @@ -3,11 +3,13 @@ use std::path::PathBuf; use birdcage::error::Error; use birdcage::{Birdcage, Exception, Sandbox}; -fn main() { - let mut birdcage = Birdcage::new(); +use crate::TestSetup; + +pub fn setup() -> TestSetup { + let mut sandbox = Birdcage::new(); // Add a path that doesn't exist. - let result = birdcage.add_exception(Exception::Read("/does/not/exist".into())); + let result = sandbox.add_exception(Exception::Read("/does/not/exist".into())); // Ensure it is appropriately reported that exception was NOT added. match result { @@ -15,6 +17,9 @@ fn main() { _ => panic!("expected path error"), } + TestSetup { sandbox, data: String::new() } +} + +pub fn validate(_data: String) { // Ensure locking is always successful. - birdcage.lock().unwrap(); } diff --git a/integration/net.rs b/integration/net.rs new file mode 100644 index 0000000..19516e7 --- /dev/null +++ b/integration/net.rs @@ -0,0 +1,18 @@ +use std::net::TcpStream; + +use birdcage::{Birdcage, Exception, Sandbox}; + +use crate::TestSetup; + +pub fn setup() -> TestSetup { + // Setup sandbox exceptions. + let mut sandbox = Birdcage::new(); + sandbox.add_exception(Exception::Networking).unwrap(); + + TestSetup { sandbox, data: String::new() } +} + +pub fn validate(_data: String) { + let result = TcpStream::connect("8.8.8.8:443"); + assert!(result.is_ok()); +} diff --git a/tests/seccomp.rs b/integration/seccomp.rs similarity index 75% rename from tests/seccomp.rs rename to integration/seccomp.rs index 70232da..32d209e 100644 --- a/tests/seccomp.rs +++ b/integration/seccomp.rs @@ -1,14 +1,14 @@ -#[cfg(target_os = "linux")] use std::ffi::CString; -#[cfg(target_os = "linux")] use birdcage::{Birdcage, Sandbox}; -#[cfg(target_os = "linux")] -fn main() { - // Activate our sandbox. - Birdcage::new().lock().unwrap(); +use crate::TestSetup; +pub fn setup() -> TestSetup { + TestSetup { sandbox: Birdcage::new(), data: String::new() } +} + +pub fn validate(_data: String) { // Ensure `chdir` is allowed. let root_path = CString::new("/").unwrap(); let result = unsafe { libc::chdir(root_path.as_ptr()) }; @@ -24,6 +24,3 @@ fn main() { let result = unsafe { libc::syscall(libc::SYS_clone, flags, stack) }; assert_eq!(result, -1); } - -#[cfg(not(target_os = "linux"))] -fn main() {} diff --git a/src/lib.rs b/src/lib.rs index e2a0bcd..6fb646b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ //! //! # Example //! -//! ```rust +//! ```rust,ignore //! use std::fs; //! //! use birdcage::{Birdcage, Sandbox}; @@ -18,7 +18,7 @@ //! fs::read_to_string(file.path()).unwrap(); //! //! // Initialize the sandbox; by default everything is prohibited. -//! Birdcage::new().lock().unwrap(); +//! Birdcage::new().spawn().unwrap(); //! //! // Reads with sandbox should fail. //! let result = fs::read_to_string(file.path()); @@ -27,6 +27,7 @@ use std::env; use std::path::PathBuf; +use std::process::{Child, Command}; use crate::error::Result; #[cfg(target_os = "linux")] @@ -64,18 +65,21 @@ pub trait Sandbox: Sized { /// symlink's target. fn add_exception(&mut self, exception: Exception) -> Result<&mut Self>; - /// Apply the sandbox restrictions to the current process. + /// Setup sandbox and spawn a new process. + /// + /// This will setup the sandbox in the **CURRENT** process, before launching + /// the sandboxee. Since most of the restrictions will also be applied to + /// the calling process, it is recommended to create a separate process + /// before calling this method. The calling process is **NOT** fully + /// sandboxed. /// /// # Errors /// /// Sandboxing will fail if the calling process is not single-threaded. /// - /// Since sandboxing layers are applied in multiple steps, it is possible - /// that after a failure some restrictions are still applied. While this - /// never allows the process to do things it wasn't capable of doing - /// before, it is still recommended to abort the sandboxing process if - /// you want to continue operations without a sandbox in place. - fn lock(self) -> Result<()>; + /// After failure, the calling process might still be affected by partial + /// sandboxing restrictions. + fn spawn(self, sandboxee: Command) -> Result; } /// Sandboxing exception rule. diff --git a/src/linux/mod.rs b/src/linux/mod.rs index 8e8e05f..4021f22 100644 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -1,13 +1,16 @@ //! Linux sandboxing. use std::collections::HashMap; +use std::ffi::CString; use std::io::Error as IoError; use std::os::unix::ffi::OsStrExt; +use std::os::unix::process::CommandExt; use std::path::{Component, Path, PathBuf}; -use std::{env, io}; +use std::process::{Child, Command}; +use std::{env, fs, io}; use crate::error::{Error, Result}; -use crate::linux::namespaces::MountAttrFlags; +use crate::linux::namespaces::{MountAttrFlags, Namespaces}; use crate::linux::seccomp::SyscallFilter; use crate::{Exception, Sandbox}; @@ -41,28 +44,72 @@ impl Sandbox for LinuxSandbox { Ok(self) } - fn lock(self) -> Result<()> { + fn spawn(self, mut sandboxee: Command) -> Result { + // Ensure calling process is not multi-threaded. + assert!( + thread_count().unwrap_or(0) == 1, + "`Sandbox::spawn` must be called from a single-threaded process" + ); + // Remove environment variables. if !self.full_env { crate::restrict_env_variables(&self.env_exceptions); } - // Setup namespaces. - namespaces::create_namespaces(self.allow_networking, self.path_exceptions)?; + // Get EUID/EGID outside of the namespaces. + let uid = unsafe { libc::geteuid() }; + let gid = unsafe { libc::getegid() }; + + // Isolate networking using a network namespace. + if !self.allow_networking { + namespaces::create_user_namespace(0, 0, Namespaces::NETWORK)?; + } - // Setup system call filters. - SyscallFilter::apply()?; + // Isolate filesystem using a mount namespace. + namespaces::create_mount_namespace(self.path_exceptions)?; - // Block suid/sgid. + // Setup PID namespace. // - // This is also blocked by our bind mount's MS_NOSUID flag, so we're just - // doubling-down here. - no_new_privs()?; + // Create a new PID namespace before spawning the child to make it PID 1. The + // mount namespace is required to create `/proc` after process creation. + namespaces::create_user_namespace(0, 0, Namespaces::PID | Namespaces::MOUNT)?; - Ok(()) + // Spawn the sandboxee. + // + // We make use of `pre_exec` to create the remaining resource restrictions which + // must be setup in the sandboxee's process context. + let child = unsafe { sandboxee.pre_exec(move || post_fork(uid, gid)).spawn()? }; + + Ok(child) } } +// NOTE: Since this new process is PID 1, it will be responsible for reaping all +// orphans. We currently do not create a reaper for these and instead leak +// zombies, relying on the spawned process being short-lived and not spawning a +// lot of children. +// +/// Sandboxing steps executed in the new process' context. +fn post_fork(uid: u32, gid: u32) -> io::Result<()> { + // Create new procfs directory. + let new_proc_c = CString::new("/proc").unwrap(); + namespaces::mount_proc(&new_proc_c)?; + + // Drop root user mapping and ensure abstract namespace is cleared. + namespaces::create_user_namespace(uid, gid, Namespaces::empty())?; + + // Setup system call filters. + SyscallFilter::apply().map_err(io::Error::other)?; + + // Block suid/sgid. + // + // This is also blocked by our bind mount's MS_NOSUID flag, so we're just + // doubling-down here. + no_new_privs()?; + + Ok(()) +} + /// Path permissions required for the sandbox. #[derive(Default)] pub(crate) struct PathExceptions { @@ -119,12 +166,12 @@ impl PathExceptions { } /// Prevent suid/sgid. -fn no_new_privs() -> Result<()> { +fn no_new_privs() -> io::Result<()> { let result = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) }; match result { 0 => Ok(()), - _ => Err(IoError::last_os_error().into()), + _ => Err(IoError::last_os_error()), } } @@ -199,3 +246,24 @@ fn normalize_path(path: &Path) -> PathBuf { fn path_has_symlinks(path: &Path) -> bool { path.ancestors().any(|path| path.read_link().is_ok()) } + +/// Get the number of threads used by the current process. +fn thread_count() -> io::Result { + // Read process status from procfs. + let status = fs::read_to_string("/proc/self/status")?; + + // Parse procfs output. + let (_, threads_start) = status.split_once("Threads:").ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "/proc/self/status missing \"Threads:\"") + })?; + let thread_count = threads_start.split_whitespace().next().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "/proc/self/status output malformed") + })?; + + // Convert to number. + let thread_count = thread_count + .parse::() + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; + + Ok(thread_count) +} diff --git a/src/linux/namespaces.rs b/src/linux/namespaces.rs index 2d62ac3..bdbf0f4 100644 --- a/src/linux/namespaces.rs +++ b/src/linux/namespaces.rs @@ -7,7 +7,7 @@ use std::io::Error as IoError; use std::os::unix::ffi::OsStrExt; use std::os::unix::fs as unixfs; use std::path::{Component, Path, PathBuf}; -use std::{env, mem, ptr}; +use std::{env, io, mem, ptr}; use bitflags::bitflags; @@ -17,36 +17,11 @@ use crate::linux::PathExceptions; /// Path for mount namespace's new root. const NEW_ROOT: &str = "/tmp/birdcage-root"; -/// Isolate process using Linux namespaces. -/// -/// If successful, this will always clear the abstract namespace. -/// -/// Additionally it will isolate network access if `allow_networking` is -/// `false`. -pub(crate) fn create_namespaces(allow_networking: bool, exceptions: PathExceptions) -> Result<()> { - // Get EUID/EGID outside of the namespace. - let uid = unsafe { libc::geteuid() }; - let gid = unsafe { libc::getegid() }; - - // Setup the network namespace. - if !allow_networking { - create_user_namespace(0, 0, Namespaces::NETWORK)?; - } - - // Isolate filesystem and procfs. - create_mount_namespace(exceptions)?; - - // Drop root user mapping and ensure abstract namespace is cleared. - create_user_namespace(uid, gid, Namespaces::empty())?; - - Ok(()) -} - /// Create a mount namespace to isolate filesystem access. /// /// This will deny access to any path which isn't part of `bind_mounts`. Allowed /// paths are mounted according to their bind mount flags. -fn create_mount_namespace(exceptions: PathExceptions) -> Result<()> { +pub fn create_mount_namespace(exceptions: PathExceptions) -> Result<()> { // Create mount namespace to allow creation of new mounts. create_user_namespace(0, 0, Namespaces::MOUNT)?; @@ -101,7 +76,7 @@ fn create_mount_namespace(exceptions: PathExceptions) -> Result<()> { let new_proc = new_root.join("proc"); let new_proc_c = CString::new(new_proc.as_os_str().as_bytes()).unwrap(); fs::create_dir_all(&new_proc)?; - bind_mount(&old_proc_c, &new_proc_c).unwrap(); + bind_mount(&old_proc_c, &new_proc_c)?; // Pivot root to `new_root`, placing the old root at the same location. pivot_root(&new_root_c, &new_root_c)?; @@ -203,6 +178,21 @@ fn mount_tmpfs(dst: &CStr) -> Result<()> { } } +/// Mount a new procfs. +pub fn mount_proc(dst: &CStr) -> io::Result<()> { + let flags = MountFlags::NOSUID | MountFlags::NODEV | MountFlags::NOEXEC; + let fstype = CString::new("proc").unwrap(); + let res = unsafe { + libc::mount(fstype.as_ptr(), dst.as_ptr(), fstype.as_ptr(), flags.bits(), ptr::null()) + }; + + if res == 0 { + Ok(()) + } else { + Err(IoError::last_os_error()) + } +} + /// Create a new bind mount. fn bind_mount(src: &CStr, dst: &CStr) -> Result<()> { let flags = MountFlags::BIND | MountFlags::RECURSIVE; @@ -291,11 +281,11 @@ fn umount(target: &CStr) -> Result<()> { /// /// The parent and child UIDs and GIDs define the user and group mappings /// between the parent namespace and the new user namespace. -fn create_user_namespace( +pub fn create_user_namespace( child_uid: u32, child_gid: u32, extra_namespaces: Namespaces, -) -> Result<()> { +) -> io::Result<()> { // Get current user's EUID and EGID. let parent_uid = unsafe { libc::geteuid() }; let parent_gid = unsafe { libc::getegid() }; @@ -314,11 +304,11 @@ fn create_user_namespace( } /// Enter a namespace. -fn unshare(namespaces: Namespaces) -> Result<()> { +fn unshare(namespaces: Namespaces) -> io::Result<()> { let result = unsafe { libc::unshare(namespaces.bits()) }; match result { 0 => Ok(()), - _ => Err(IoError::last_os_error().into()), + _ => Err(IoError::last_os_error()), } } @@ -326,6 +316,12 @@ bitflags! { /// Mount syscall flags. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct MountFlags: libc::c_ulong { + /// Ignore suid and sgid bits. + const NOSUID = libc::MS_NOSUID; + /// Disallow access to device special files. + const NODEV = libc::MS_NODEV; + /// Disallow program execution. + const NOEXEC = libc::MS_NOEXEC; /// Create a bind mount. const BIND = libc::MS_BIND; /// Used in conjuction with [`Self::BIND`] to create a recursive bind mount, and @@ -384,7 +380,7 @@ bitflags! { bitflags! { /// Unshare system call namespace flags. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - struct Namespaces: libc::c_int { + pub struct Namespaces: libc::c_int { /// Unshare the file descriptor table, so that the calling process no longer /// shares its file descriptors with any other process. const FILES = libc::CLONE_FILES; diff --git a/src/macos.rs b/src/macos.rs index 6c4bf71..256f2be 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use std::ffi::{CStr, CString}; use std::io::{Result as IoResult, Write}; use std::path::{Path, PathBuf}; +use std::process::{Child, Command}; use std::{fs, ptr}; use bitflags::bitflags; @@ -65,7 +66,7 @@ impl Sandbox for MacSandbox { Ok(self) } - fn lock(self) -> Result<()> { + fn spawn(self, mut sandboxee: Command) -> Result { // Remove environment variables. if !self.full_env { crate::restrict_env_variables(&self.env_exceptions); @@ -80,7 +81,7 @@ impl Sandbox for MacSandbox { let result = unsafe { sandbox_init(profile.as_ptr(), 0, &mut error) }; if result == 0 { - Ok(()) + Ok(sandboxee.spawn()?) } else { unsafe { let error_text = CStr::from_ptr(error) diff --git a/tests/canonicalize.rs b/tests/canonicalize.rs deleted file mode 100644 index 9f985d0..0000000 --- a/tests/canonicalize.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::fs; - -use birdcage::{Birdcage, Exception, Sandbox}; - -fn main() { - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::Read("./".into())).unwrap(); - birdcage.lock().unwrap(); - - // Check for success on reading the `Cargo.toml` file. - let file = fs::read_to_string("./Cargo.toml").unwrap(); - assert!(file.contains("birdcage")); -} diff --git a/tests/consistent_id_mappings.rs b/tests/consistent_id_mappings.rs deleted file mode 100644 index d47580a..0000000 --- a/tests/consistent_id_mappings.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[cfg(target_os = "linux")] -use birdcage::{Birdcage, Sandbox}; - -#[cfg(target_os = "linux")] -fn main() { - let uid = unsafe { libc::getuid() }; - let gid = unsafe { libc::getgid() }; - let euid = unsafe { libc::geteuid() }; - let egid = unsafe { libc::getegid() }; - - let birdcage = Birdcage::new(); - birdcage.lock().unwrap(); - - assert_eq!(uid, unsafe { libc::getuid() }); - assert_eq!(gid, unsafe { libc::getgid() }); - assert_eq!(euid, unsafe { libc::geteuid() }); - assert_eq!(egid, unsafe { libc::getegid() }); -} - -#[cfg(not(target_os = "linux"))] -fn main() {} diff --git a/tests/exec.rs b/tests/exec.rs deleted file mode 100644 index 84fbb21..0000000 --- a/tests/exec.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::fs; -use std::path::PathBuf; -use std::process::Command; - -use birdcage::{Birdcage, Exception, Sandbox}; - -fn main() { - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::ExecuteAndRead("/usr/bin/true".into())).unwrap(); - birdcage.add_exception(Exception::ExecuteAndRead("/usr/lib".into())).unwrap(); - if PathBuf::from("/lib64").exists() { - birdcage.add_exception(Exception::ExecuteAndRead("/lib64".into())).unwrap(); - } - if PathBuf::from("/lib").exists() { - birdcage.add_exception(Exception::ExecuteAndRead("/lib".into())).unwrap(); - } - birdcage.lock().unwrap(); - - // Check for success when executing `true`. - let cmd = Command::new("/usr/bin/true").status().unwrap(); - assert!(cmd.success()); - - // Check for success on reading the `true` file. - let cmd_file = fs::read("/usr/bin/true"); - assert!(cmd_file.is_ok()); -} diff --git a/tests/exec_symlinked_dir.rs b/tests/exec_symlinked_dir.rs deleted file mode 100644 index 5cda39d..0000000 --- a/tests/exec_symlinked_dir.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::os::unix::fs as unixfs; -use std::path::PathBuf; -use std::process::Command; - -use birdcage::{Birdcage, Exception, Sandbox}; - -fn main() { - // Create symlinked executable dir. - let tempdir = tempfile::tempdir().unwrap().into_path(); - let symlink_dir = tempdir.join("bin"); - unixfs::symlink("/usr/bin", &symlink_dir).unwrap(); - - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::ExecuteAndRead(symlink_dir.clone())).unwrap(); - if PathBuf::from("/lib64").exists() { - birdcage.add_exception(Exception::ExecuteAndRead("/lib64".into())).unwrap(); - } - if PathBuf::from("/lib").exists() { - birdcage.add_exception(Exception::ExecuteAndRead("/lib".into())).unwrap(); - } - birdcage.lock().unwrap(); - - // Ensure symlinked dir's executable works. - let symlink_dir_exec = symlink_dir.join("true"); - let cmd = Command::new(symlink_dir_exec).status().unwrap(); - assert!(cmd.success()); - - // Ensure original dir's executable works. - let cmd = Command::new("/usr/bin/true").status().unwrap(); - assert!(cmd.success()); -} diff --git a/tests/exec_symlinked_dirs_exec.rs b/tests/exec_symlinked_dirs_exec.rs deleted file mode 100644 index 365e1e5..0000000 --- a/tests/exec_symlinked_dirs_exec.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::os::unix::fs as unixfs; -use std::path::PathBuf; -use std::process::Command; - -use birdcage::{Birdcage, Exception, Sandbox}; - -fn main() { - // Create symlinked executable dir. - let tempdir = tempfile::tempdir().unwrap().into_path(); - let symlink_dir = tempdir.join("bin"); - let symlink_dir_exec = symlink_dir.join("true"); - unixfs::symlink("/usr/bin", &symlink_dir).unwrap(); - - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::ExecuteAndRead(symlink_dir_exec.clone())).unwrap(); - if PathBuf::from("/lib64").exists() { - birdcage.add_exception(Exception::ExecuteAndRead("/lib64".into())).unwrap(); - } - if PathBuf::from("/lib").exists() { - birdcage.add_exception(Exception::ExecuteAndRead("/lib".into())).unwrap(); - } - birdcage.lock().unwrap(); - - // Ensure symlinked dir's executable works. - let cmd = Command::new(symlink_dir_exec).status().unwrap(); - assert!(cmd.success()); - - // Ensure original dir's executable works. - let cmd = Command::new("/usr/bin/true").status().unwrap(); - assert!(cmd.success()); -} diff --git a/tests/exec_symlinked_file.rs b/tests/exec_symlinked_file.rs deleted file mode 100644 index 13e515d..0000000 --- a/tests/exec_symlinked_file.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::fs; -use std::os::unix::fs as unixfs; -use std::path::PathBuf; -use std::process::Command; - -use birdcage::{Birdcage, Exception, Sandbox}; - -fn main() { - // Create symlinked executable. - let tempdir = tempfile::tempdir().unwrap().into_path(); - let exec_dir = tempdir.join("bin"); - fs::create_dir(&exec_dir).unwrap(); - let symlink_exec = exec_dir.join("true"); - unixfs::symlink("/usr/bin/true", &symlink_exec).unwrap(); - - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::ExecuteAndRead(symlink_exec.clone())).unwrap(); - if PathBuf::from("/lib64").exists() { - birdcage.add_exception(Exception::ExecuteAndRead("/lib64".into())).unwrap(); - } - if PathBuf::from("/lib").exists() { - birdcage.add_exception(Exception::ExecuteAndRead("/lib".into())).unwrap(); - } - birdcage.lock().unwrap(); - - // Ensure symlinked executable works. - let cmd = Command::new(symlink_exec).status().unwrap(); - assert!(cmd.success()); - - // Ensure original executable works. - let cmd = Command::new("/usr/bin/true").status().unwrap(); - assert!(cmd.success()); -} diff --git a/tests/fs.rs b/tests/fs.rs deleted file mode 100644 index 8b2ca3f..0000000 --- a/tests/fs.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::fs; - -use birdcage::{Birdcage, Exception, Sandbox}; -use tempfile::NamedTempFile; - -fn main() { - const FILE_CONTENT: &str = "expected content"; - - // Setup our test files. - let private_path = NamedTempFile::new().unwrap(); - fs::write(&private_path, FILE_CONTENT.as_bytes()).unwrap(); - let public_path = NamedTempFile::new().unwrap(); - fs::write(&public_path, FILE_CONTENT.as_bytes()).unwrap(); - - // Activate our sandbox. - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::Read(public_path.path().into())).unwrap(); - birdcage.lock().unwrap(); - - // Access to the public file is allowed. - let content = fs::read_to_string(public_path).unwrap(); - assert_eq!(content, FILE_CONTENT); - - // Access to the private file is prohibited. - let result = fs::read_to_string(private_path); - assert!(result.is_err()); -} diff --git a/tests/fs_null.rs b/tests/fs_null.rs deleted file mode 100644 index 2a49dbf..0000000 --- a/tests/fs_null.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::fs; - -use birdcage::{Birdcage, Exception, Sandbox}; - -fn main() { - // Activate our sandbox. - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::WriteAndRead("/dev/null".into())).unwrap(); - birdcage.lock().unwrap(); - - // Writing to `/dev/null` is allowed. - fs::write("/dev/null", "blub").unwrap(); -} diff --git a/tests/fs_readonly.rs b/tests/fs_readonly.rs deleted file mode 100644 index 9aaa916..0000000 --- a/tests/fs_readonly.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::fs; - -use birdcage::{Birdcage, Exception, Sandbox}; -use tempfile::NamedTempFile; - -fn main() { - const FILE_CONTENT: &str = "expected content"; - - // Setup the test file. - let file = NamedTempFile::new().unwrap(); - fs::write(&file, FILE_CONTENT.as_bytes()).unwrap(); - - // Activate our sandbox. - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::Read(file.path().into())).unwrap(); - birdcage.lock().unwrap(); - - // Reading from the file is allowed. - let content = fs::read_to_string(&file).unwrap(); - assert_eq!(content, FILE_CONTENT); - - // Writing to the file is prohibited. - let result = fs::write(&file, FILE_CONTENT.as_bytes()); - assert!(result.is_err()); -} diff --git a/tests/fs_restrict_child.rs b/tests/fs_restrict_child.rs deleted file mode 100644 index 1d1212e..0000000 --- a/tests/fs_restrict_child.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::fs; - -use birdcage::{Birdcage, Exception, Sandbox}; - -fn main() { - const FILE_CONTENT: &str = "expected content"; - - // Setup our test tree. - let tempdir = tempfile::tempdir().unwrap().into_path(); - let tempfile = tempdir.join("target-file"); - fs::write(&tempfile, FILE_CONTENT.as_bytes()).unwrap(); - - // Setup sandbox, allowing read/write to dir, but only read for the file. - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::WriteAndRead(tempdir.clone())).unwrap(); - birdcage.add_exception(Exception::Read(tempfile.clone())).unwrap(); - birdcage.lock().unwrap(); - - // Write access to directory works. - fs::create_dir(tempdir.join("boop")).unwrap(); - - // Read access to file works. - let content = fs::read_to_string(&tempfile).unwrap(); - assert_eq!(content, FILE_CONTENT); - - // Write access to file is denied. - let result = fs::write(&tempfile, "no"); - assert!(result.is_err()); -} diff --git a/tests/fs_symlink.rs b/tests/fs_symlink.rs deleted file mode 100644 index 784ea59..0000000 --- a/tests/fs_symlink.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::fs; -use std::os::unix::fs as unixfs; -use std::path::PathBuf; - -use birdcage::{Birdcage, Exception, Sandbox}; -use tempfile::NamedTempFile; - -fn main() { - const FILE_CONTENT: &str = "expected content"; - - // Setup our test files. - let private_path = NamedTempFile::new().unwrap(); - fs::write(&private_path, FILE_CONTENT.as_bytes()).unwrap(); - let public_path = NamedTempFile::new().unwrap(); - fs::write(&public_path, FILE_CONTENT.as_bytes()).unwrap(); - - // Create symlinks for the files. - let private_str = private_path.path().to_string_lossy() + "_tmpfile"; - let private = PathBuf::from(private_str.as_ref()); - let public_str = public_path.path().to_string_lossy() + "_tmpfile"; - let public = PathBuf::from(public_str.as_ref()); - unixfs::symlink(&private_path, &private).unwrap(); - unixfs::symlink(&public_path, &public).unwrap(); - - // Activate our sandbox. - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::Read(public.clone())).unwrap(); - birdcage.lock().unwrap(); - - // Access to the public file is allowed. - let content = fs::read_to_string(&public).unwrap(); - assert_eq!(content, FILE_CONTENT); - - // Access to the private file is prohibited. - let result = fs::read_to_string(&private); - assert!(result.is_err()); -} diff --git a/tests/fs_symlink_dir.rs b/tests/fs_symlink_dir.rs deleted file mode 100644 index 7b873cb..0000000 --- a/tests/fs_symlink_dir.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::fs; -use std::os::unix::fs as unixfs; -use std::path::PathBuf; - -use birdcage::{Birdcage, Exception, Sandbox}; -use tempfile::TempDir; - -fn main() { - const FILE_CONTENT: &str = "expected content"; - - // Setup our test directory. - let tempdir = TempDir::new().unwrap(); - let symlink_str = tempdir.path().to_string_lossy() + "_tmpfile"; - let symlink_path = PathBuf::from(symlink_str.as_ref()); - unixfs::symlink(&tempdir, &symlink_path).unwrap(); - - // Activate our sandbox. - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::WriteAndRead(symlink_path.clone())).unwrap(); - birdcage.lock().unwrap(); - - // Try to create a file in the symlinked directory. - let path = symlink_path.join("tmpfile"); - fs::write(&path, FILE_CONTENT.as_bytes()).unwrap(); - let content = fs::read_to_string(&path).unwrap(); - assert_eq!(content, FILE_CONTENT); -} diff --git a/tests/fs_symlink_dir_separate_perms.rs b/tests/fs_symlink_dir_separate_perms.rs deleted file mode 100644 index d652856..0000000 --- a/tests/fs_symlink_dir_separate_perms.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::fs; -use std::os::unix::fs as unixfs; - -use birdcage::{Birdcage, Exception, Sandbox}; -use tempfile::TempDir; - -fn main() { - const FILE_CONTENT: &str = "expected content"; - - // Setup our test directories. - let tempdir = TempDir::new().unwrap().into_path(); - let symlink_src = tempdir.join("src"); - fs::create_dir(&symlink_src).unwrap(); - let symlink_dst = tempdir.join("dst"); - unixfs::symlink(&symlink_src, &symlink_dst).unwrap(); - - // Add read+write for src, but also add readonly for dst. - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::WriteAndRead(symlink_src.clone())).unwrap(); - birdcage.add_exception(Exception::Read(symlink_dst.clone())).unwrap(); - birdcage.lock().unwrap(); - - // Ensure writing works. - let testfile = symlink_src.join("file"); - fs::write(&testfile, FILE_CONTENT).unwrap(); - - // Ensure reading works. - let content = fs::read_to_string(&testfile).unwrap(); - assert_eq!(content, FILE_CONTENT); -} diff --git a/tests/fs_write_also_read.rs b/tests/fs_write_also_read.rs deleted file mode 100644 index 15f4262..0000000 --- a/tests/fs_write_also_read.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::fs; - -use birdcage::{Birdcage, Exception, Sandbox}; -use tempfile::NamedTempFile; - -fn main() { - const FILE_CONTENT: &str = "expected content"; - - // Setup our test files. - let file = NamedTempFile::new().unwrap(); - - // Activate our sandbox. - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::WriteAndRead(file.path().into())).unwrap(); - birdcage.lock().unwrap(); - - // Write access is allowed. - fs::write(&file, FILE_CONTENT.as_bytes()).unwrap(); - - // Read access is allowed. - let content = fs::read_to_string(file).unwrap(); - assert_eq!(content, FILE_CONTENT); -} diff --git a/tests/net.rs b/tests/net.rs deleted file mode 100644 index adb205e..0000000 --- a/tests/net.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::net::TcpStream; - -use birdcage::{Birdcage, Exception, Sandbox}; - -fn main() { - let mut birdcage = Birdcage::new(); - birdcage.add_exception(Exception::Networking).unwrap(); - birdcage.lock().unwrap(); - - let result = TcpStream::connect("8.8.8.8:443"); - assert!(result.is_ok()); -} From 3e452a90f55cec2f156a093e78c0a8bb084da952 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Tue, 28 Nov 2023 15:47:39 +0100 Subject: [PATCH 02/10] Move network sandboxing into child --- src/linux/mod.rs | 16 +++++++++------- src/linux/namespaces.rs | 31 +++++++++++++++---------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/linux/mod.rs b/src/linux/mod.rs index 4021f22..d61d70d 100644 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -60,11 +60,6 @@ impl Sandbox for LinuxSandbox { let uid = unsafe { libc::geteuid() }; let gid = unsafe { libc::getegid() }; - // Isolate networking using a network namespace. - if !self.allow_networking { - namespaces::create_user_namespace(0, 0, Namespaces::NETWORK)?; - } - // Isolate filesystem using a mount namespace. namespaces::create_mount_namespace(self.path_exceptions)?; @@ -78,7 +73,9 @@ impl Sandbox for LinuxSandbox { // // We make use of `pre_exec` to create the remaining resource restrictions which // must be setup in the sandboxee's process context. - let child = unsafe { sandboxee.pre_exec(move || post_fork(uid, gid)).spawn()? }; + let child = unsafe { + sandboxee.pre_exec(move || post_fork(uid, gid, self.allow_networking)).spawn()? + }; Ok(child) } @@ -90,11 +87,16 @@ impl Sandbox for LinuxSandbox { // lot of children. // /// Sandboxing steps executed in the new process' context. -fn post_fork(uid: u32, gid: u32) -> io::Result<()> { +fn post_fork(uid: u32, gid: u32, allow_networking: bool) -> io::Result<()> { // Create new procfs directory. let new_proc_c = CString::new("/proc").unwrap(); namespaces::mount_proc(&new_proc_c)?; + // Isolate networking using a network namespace. + if !allow_networking { + namespaces::create_user_namespace(0, 0, Namespaces::NETWORK)?; + } + // Drop root user mapping and ensure abstract namespace is cleared. namespaces::create_user_namespace(uid, gid, Namespaces::empty())?; diff --git a/src/linux/namespaces.rs b/src/linux/namespaces.rs index bdbf0f4..40f6ab7 100644 --- a/src/linux/namespaces.rs +++ b/src/linux/namespaces.rs @@ -11,7 +11,6 @@ use std::{env, io, mem, ptr}; use bitflags::bitflags; -use crate::error::Result; use crate::linux::PathExceptions; /// Path for mount namespace's new root. @@ -21,7 +20,7 @@ const NEW_ROOT: &str = "/tmp/birdcage-root"; /// /// This will deny access to any path which isn't part of `bind_mounts`. Allowed /// paths are mounted according to their bind mount flags. -pub fn create_mount_namespace(exceptions: PathExceptions) -> Result<()> { +pub fn create_mount_namespace(exceptions: PathExceptions) -> io::Result<()> { // Create mount namespace to allow creation of new mounts. create_user_namespace(0, 0, Namespaces::MOUNT)?; @@ -97,7 +96,7 @@ pub fn create_mount_namespace(exceptions: PathExceptions) -> Result<()> { /// symlink ourselves and it's not possible to mount on top of it anyway. So /// here we make sure that symlinks are created if no bind mount was created for /// their parent directory. -fn create_symlinks(new_root: &Path, symlinks: Vec<(PathBuf, PathBuf)>) -> Result<()> { +fn create_symlinks(new_root: &Path, symlinks: Vec<(PathBuf, PathBuf)>) -> io::Result<()> { for (symlink, target) in symlinks { // Ignore symlinks if a parent bind mount exists. let unrooted_path = symlink.strip_prefix("/").unwrap(); @@ -127,7 +126,7 @@ fn create_symlinks(new_root: &Path, symlinks: Vec<(PathBuf, PathBuf)>) -> Result /// /// If `src` ends in a file, an empty file with matching permissions will be /// created. -fn copy_tree(src: impl AsRef, dst: impl AsRef) -> Result<()> { +fn copy_tree(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { let mut dst = dst.as_ref().to_path_buf(); let mut src_sub = PathBuf::new(); let src = src.as_ref(); @@ -164,7 +163,7 @@ fn copy_tree(src: impl AsRef, dst: impl AsRef) -> Result<()> { } /// Mount a new tmpfs. -fn mount_tmpfs(dst: &CStr) -> Result<()> { +fn mount_tmpfs(dst: &CStr) -> io::Result<()> { let flags = MountFlags::empty(); let fstype = CString::new("tmpfs").unwrap(); let res = unsafe { @@ -174,7 +173,7 @@ fn mount_tmpfs(dst: &CStr) -> Result<()> { if res == 0 { Ok(()) } else { - Err(IoError::last_os_error().into()) + Err(IoError::last_os_error()) } } @@ -194,7 +193,7 @@ pub fn mount_proc(dst: &CStr) -> io::Result<()> { } /// Create a new bind mount. -fn bind_mount(src: &CStr, dst: &CStr) -> Result<()> { +fn bind_mount(src: &CStr, dst: &CStr) -> io::Result<()> { let flags = MountFlags::BIND | MountFlags::RECURSIVE; let res = unsafe { libc::mount(src.as_ptr(), dst.as_ptr(), ptr::null(), flags.bits(), ptr::null()) }; @@ -202,12 +201,12 @@ fn bind_mount(src: &CStr, dst: &CStr) -> Result<()> { if res == 0 { Ok(()) } else { - Err(IoError::last_os_error().into()) + Err(IoError::last_os_error()) } } /// Remount an existing bind mount with a new set of mount flags. -fn update_mount_flags(mount: &CStr, flags: MountAttrFlags) -> Result<()> { +fn update_mount_flags(mount: &CStr, flags: MountAttrFlags) -> io::Result<()> { let attrs = MountAttr { attr_set: flags.bits(), ..Default::default() }; let res = unsafe { @@ -224,12 +223,12 @@ fn update_mount_flags(mount: &CStr, flags: MountAttrFlags) -> Result<()> { if res == 0 { Ok(()) } else { - Err(IoError::last_os_error().into()) + Err(IoError::last_os_error()) } } /// Recursively update the root to deny mount propagation. -fn deny_mount_propagation() -> Result<()> { +fn deny_mount_propagation() -> io::Result<()> { let flags = MountFlags::PRIVATE | MountFlags::RECURSIVE; let root = CString::new("/").unwrap(); let res = @@ -238,14 +237,14 @@ fn deny_mount_propagation() -> Result<()> { if res == 0 { Ok(()) } else { - Err(IoError::last_os_error().into()) + Err(IoError::last_os_error()) } } /// Change root directory to `new_root` and mount the old root in `put_old`. /// /// The `put_old` directory must be at or undearneath `new_root`. -fn pivot_root(new_root: &CStr, put_old: &CStr) -> Result<()> { +fn pivot_root(new_root: &CStr, put_old: &CStr) -> io::Result<()> { // Get target working directory path. let working_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("/")); @@ -253,7 +252,7 @@ fn pivot_root(new_root: &CStr, put_old: &CStr) -> Result<()> { unsafe { libc::syscall(libc::SYS_pivot_root, new_root.as_ptr(), put_old.as_ptr()) }; if result != 0 { - return Err(IoError::last_os_error().into()); + return Err(IoError::last_os_error()); } // Attempt to recover working directory, or switch to root. @@ -268,12 +267,12 @@ fn pivot_root(new_root: &CStr, put_old: &CStr) -> Result<()> { } /// Unmount a filesystem. -fn umount(target: &CStr) -> Result<()> { +fn umount(target: &CStr) -> io::Result<()> { let result = unsafe { libc::umount2(target.as_ptr(), libc::MNT_DETACH) }; match result { 0 => Ok(()), - _ => Err(IoError::last_os_error().into()), + _ => Err(IoError::last_os_error()), } } From 95c5142d9a9854e1de378c3b98d0e4415e2141e8 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Tue, 28 Nov 2023 16:13:22 +0100 Subject: [PATCH 03/10] Remove unnecessary mount namespace --- src/linux/mod.rs | 6 +++--- src/linux/namespaces.rs | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/linux/mod.rs b/src/linux/mod.rs index d61d70d..e88c7bc 100644 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -60,15 +60,15 @@ impl Sandbox for LinuxSandbox { let uid = unsafe { libc::geteuid() }; let gid = unsafe { libc::getegid() }; - // Isolate filesystem using a mount namespace. - namespaces::create_mount_namespace(self.path_exceptions)?; - // Setup PID namespace. // // Create a new PID namespace before spawning the child to make it PID 1. The // mount namespace is required to create `/proc` after process creation. namespaces::create_user_namespace(0, 0, Namespaces::PID | Namespaces::MOUNT)?; + // Isolate filesystem using a mount namespace. + namespaces::setup_mount_namespace(self.path_exceptions)?; + // Spawn the sandboxee. // // We make use of `pre_exec` to create the remaining resource restrictions which diff --git a/src/linux/namespaces.rs b/src/linux/namespaces.rs index 40f6ab7..b987fdb 100644 --- a/src/linux/namespaces.rs +++ b/src/linux/namespaces.rs @@ -16,14 +16,11 @@ use crate::linux::PathExceptions; /// Path for mount namespace's new root. const NEW_ROOT: &str = "/tmp/birdcage-root"; -/// Create a mount namespace to isolate filesystem access. +/// Isolate filesystem access in an existing mount namespace. /// /// This will deny access to any path which isn't part of `bind_mounts`. Allowed /// paths are mounted according to their bind mount flags. -pub fn create_mount_namespace(exceptions: PathExceptions) -> io::Result<()> { - // Create mount namespace to allow creation of new mounts. - create_user_namespace(0, 0, Namespaces::MOUNT)?; - +pub fn setup_mount_namespace(exceptions: PathExceptions) -> io::Result<()> { // Get target paths for new and old root. let new_root = PathBuf::from(NEW_ROOT); From e1f5143f047a6d8e56b0f9f94de48965740797a6 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Tue, 28 Nov 2023 16:55:03 +0100 Subject: [PATCH 04/10] Fix oldstable build --- src/linux/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/linux/mod.rs b/src/linux/mod.rs index e88c7bc..710b1a4 100644 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::ffi::CString; -use std::io::Error as IoError; +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; use std::os::unix::ffi::OsStrExt; use std::os::unix::process::CommandExt; use std::path::{Component, Path, PathBuf}; @@ -77,6 +77,10 @@ impl Sandbox for LinuxSandbox { sandboxee.pre_exec(move || post_fork(uid, gid, self.allow_networking)).spawn()? }; + // TODO + // Drop root user mapping for the parent process. + // namespaces::create_user_namespace(uid, gid, Namespaces::empty()).unwrap(); + Ok(child) } } @@ -101,7 +105,7 @@ fn post_fork(uid: u32, gid: u32, allow_networking: bool) -> io::Result<()> { namespaces::create_user_namespace(uid, gid, Namespaces::empty())?; // Setup system call filters. - SyscallFilter::apply().map_err(io::Error::other)?; + SyscallFilter::apply().map_err(|err| IoError::new(IoErrorKind::Other, err))?; // Block suid/sgid. // From dc372b650979738f647e0cb3aba88e5562e95832 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Tue, 28 Nov 2023 17:07:43 +0100 Subject: [PATCH 05/10] Fix oldstable build v2 --- src/linux/mod.rs | 4 ---- src/linux/namespaces.rs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/linux/mod.rs b/src/linux/mod.rs index 710b1a4..c4979e1 100644 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -77,10 +77,6 @@ impl Sandbox for LinuxSandbox { sandboxee.pre_exec(move || post_fork(uid, gid, self.allow_networking)).spawn()? }; - // TODO - // Drop root user mapping for the parent process. - // namespaces::create_user_namespace(uid, gid, Namespaces::empty()).unwrap(); - Ok(child) } } diff --git a/src/linux/namespaces.rs b/src/linux/namespaces.rs index b987fdb..f759a6a 100644 --- a/src/linux/namespaces.rs +++ b/src/linux/namespaces.rs @@ -20,7 +20,7 @@ const NEW_ROOT: &str = "/tmp/birdcage-root"; /// /// This will deny access to any path which isn't part of `bind_mounts`. Allowed /// paths are mounted according to their bind mount flags. -pub fn setup_mount_namespace(exceptions: PathExceptions) -> io::Result<()> { +pub(crate) fn setup_mount_namespace(exceptions: PathExceptions) -> io::Result<()> { // Get target paths for new and old root. let new_root = PathBuf::from(NEW_ROOT); From 1d11cd7a17cc1b7d33bd73ef3eb73c543fc456ec Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 29 Nov 2023 18:11:40 +0100 Subject: [PATCH 06/10] Improve environment variable test --- integration/env.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/integration/env.rs b/integration/env.rs index 259b346..80e0bfa 100644 --- a/integration/env.rs +++ b/integration/env.rs @@ -17,9 +17,7 @@ pub fn setup() -> TestSetup { } pub fn validate(_data: String) { - // The `PUBLIC` environment variable can be accessed. - assert_eq!(env::var("PUBLIC"), Ok("GOOD".into())); - - // The `PRIVATE` environment variable was removed. - assert_eq!(env::var_os("PRIVATE"), None); + // Only the `PUBLIC` environment variable remains. + let env: Vec<_> = env::vars().collect(); + assert_eq!(env, vec![("PUBLIC".into(), "GOOD".into())]); } From 99e9e992349b52ee3598c46020b70d57a16c05b4 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 29 Nov 2023 21:32:35 +0100 Subject: [PATCH 07/10] Fix doctest --- src/lib.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6fb646b..577521e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,10 +5,11 @@ //! //! # Example //! -//! ```rust,ignore +//! ```rust //! use std::fs; +//! use std::process::Command; //! -//! use birdcage::{Birdcage, Sandbox}; +//! use birdcage::{Birdcage, Exception, Sandbox}; //! use tempfile::NamedTempFile; //! //! // Setup our test file. @@ -17,12 +18,22 @@ //! // Reads without sandbox work. //! fs::read_to_string(file.path()).unwrap(); //! +//! // Allow access to our test executable. +//! let mut sandbox = Birdcage::new(); +//! sandbox.add_exception(Exception::ExecuteAndRead("/usr/bin/cat".into())).unwrap(); +//! let _ = sandbox.add_exception(Exception::ExecuteAndRead("/usr/lib64".into())); +//! let _ = sandbox.add_exception(Exception::ExecuteAndRead("/usr/lib".into())); +//! let _ = sandbox.add_exception(Exception::ExecuteAndRead("/lib64".into())); +//! let _ = sandbox.add_exception(Exception::ExecuteAndRead("/lib".into())); +//! //! // Initialize the sandbox; by default everything is prohibited. -//! Birdcage::new().spawn().unwrap(); +//! let mut command = Command::new("/usr/bin/cat"); +//! command.arg(file.path()); +//! let mut child = sandbox.spawn(command).unwrap(); //! //! // Reads with sandbox should fail. -//! let result = fs::read_to_string(file.path()); -//! assert!(result.is_err()); +//! let status = child.wait().unwrap(); +//! assert!(!status.success()); //! ``` use std::env; From 4aa409bfc0072ef5c68567c40527a464afb3828e Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 29 Nov 2023 21:56:39 +0100 Subject: [PATCH 08/10] Remove unnecessary exceptions --- src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 577521e..f3b4548 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,8 +21,6 @@ //! // Allow access to our test executable. //! let mut sandbox = Birdcage::new(); //! sandbox.add_exception(Exception::ExecuteAndRead("/usr/bin/cat".into())).unwrap(); -//! let _ = sandbox.add_exception(Exception::ExecuteAndRead("/usr/lib64".into())); -//! let _ = sandbox.add_exception(Exception::ExecuteAndRead("/usr/lib".into())); //! let _ = sandbox.add_exception(Exception::ExecuteAndRead("/lib64".into())); //! let _ = sandbox.add_exception(Exception::ExecuteAndRead("/lib".into())); //! From 03670e8574cd065435a8cc8ab2527dacc6839db8 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 29 Nov 2023 21:58:03 +0100 Subject: [PATCH 09/10] Fix docs exec path --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index f3b4548..cd79071 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ //! let _ = sandbox.add_exception(Exception::ExecuteAndRead("/lib".into())); //! //! // Initialize the sandbox; by default everything is prohibited. -//! let mut command = Command::new("/usr/bin/cat"); +//! let mut command = Command::new("/usr/cat"); //! command.arg(file.path()); //! let mut child = sandbox.spawn(command).unwrap(); //! From 67cf4ae856cb0c22d7d92023bf176a2cabbc495f Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 29 Nov 2023 22:02:20 +0100 Subject: [PATCH 10/10] Fix doc exec path properly --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index cd79071..2c8695e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,12 +20,12 @@ //! //! // Allow access to our test executable. //! let mut sandbox = Birdcage::new(); -//! sandbox.add_exception(Exception::ExecuteAndRead("/usr/bin/cat".into())).unwrap(); +//! sandbox.add_exception(Exception::ExecuteAndRead("/bin/cat".into())).unwrap(); //! let _ = sandbox.add_exception(Exception::ExecuteAndRead("/lib64".into())); //! let _ = sandbox.add_exception(Exception::ExecuteAndRead("/lib".into())); //! //! // Initialize the sandbox; by default everything is prohibited. -//! let mut command = Command::new("/usr/cat"); +//! let mut command = Command::new("/bin/cat"); //! command.arg(file.path()); //! let mut child = sandbox.spawn(command).unwrap(); //!