Skip to content

Commit

Permalink
Add PID namespace support (#64)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cd-work authored Nov 29, 2023
1 parent a5c350e commit d552000
Show file tree
Hide file tree
Showing 45 changed files with 997 additions and 613 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
106 changes: 4 additions & 102 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down
9 changes: 5 additions & 4 deletions examples/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ fn main() -> Result<(), Box<dyn Error>> {
}

// 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);
}
18 changes: 18 additions & 0 deletions integration/canonicalize.rs
Original file line number Diff line number Diff line change
@@ -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"));
}
37 changes: 37 additions & 0 deletions integration/consistent_id_mappings.rs
Original file line number Diff line number Diff line change
@@ -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() });
}
23 changes: 23 additions & 0 deletions integration/env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use std::env;

use birdcage::{Birdcage, Exception, Sandbox};

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 sandbox = Birdcage::new();
sandbox.add_exception(Exception::Environment("PUBLIC".into())).unwrap();

TestSetup { sandbox, data: String::new() }
}

pub fn validate(_data: String) {
// Only the `PUBLIC` environment variable remains.
let env: Vec<_> = env::vars().collect();
assert_eq!(env, vec![("PUBLIC".into(), "GOOD".into())]);
}
23 changes: 23 additions & 0 deletions integration/exec.rs
Original file line number Diff line number Diff line change
@@ -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());
}
43 changes: 43 additions & 0 deletions integration/exec_symlinked_dir.rs
Original file line number Diff line number Diff line change
@@ -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());
}
43 changes: 43 additions & 0 deletions integration/exec_symlinked_dirs_exec.rs
Original file line number Diff line number Diff line change
@@ -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());
}
45 changes: 45 additions & 0 deletions integration/exec_symlinked_file.rs
Original file line number Diff line number Diff line change
@@ -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());
}
Loading

0 comments on commit d552000

Please sign in to comment.