From 919eea81ded0a1a4c4fc182bf77e947e93fa85b1 Mon Sep 17 00:00:00 2001 From: Sebastian Walz Date: Mon, 14 Oct 2024 15:24:28 +0200 Subject: [PATCH] refactor: introduce cargo-workspace --- Cargo.toml | 55 +- examples/isolate_test.rs | 429 ------------- examples/user_guide_isolate.rs | 42 -- extrasafe/Cargo.toml | 46 ++ extrasafe/README.md | 1 + .../examples}/ipc_server_with_database.rs | 123 ++-- extrasafe/examples/isolate_test.rs | 565 ++++++++++++++++++ .../examples}/network_server.rs | 14 +- .../examples}/no_files_allow_stdout.rs | 2 +- .../examples}/server_with_database.rs | 101 ++-- .../examples}/simple_network.rs | 45 +- {examples => extrasafe/examples}/time.rs | 20 +- .../examples}/user-guide.rs | 40 +- extrasafe/examples/user_guide_isolate.rs | 49 ++ {src => extrasafe/src}/builtins/basic.rs | 12 +- .../src}/builtins/danger_zone.rs | 14 +- {src => extrasafe/src}/builtins/mod.rs | 4 +- {src => extrasafe/src}/builtins/network.rs | 81 ++- {src => extrasafe/src}/builtins/pipes.rs | 2 +- {src => extrasafe/src}/builtins/systemio.rs | 129 ++-- {src => extrasafe/src}/builtins/time.rs | 8 +- {src => extrasafe/src}/error.rs | 4 +- {src => extrasafe/src}/isolate/isolate_sys.rs | 178 ++++-- {src => extrasafe/src}/isolate/mod.rs | 29 +- {src => extrasafe/src}/landlock.rs | 10 +- {src => extrasafe/src}/lib.rs | 90 ++- {src => extrasafe/src}/macros.rs | 76 ++- {tests => extrasafe/tests}/arg_comparisons.rs | 44 +- {tests => extrasafe/tests}/bad_combination.rs | 77 +-- {tests => extrasafe/tests}/default_deny.rs | 9 +- {tests => extrasafe/tests}/inherit_filters.rs | 5 +- .../tests}/landlock_allthreads_fail.rs | 16 +- {tests => extrasafe/tests}/landlock_basic.rs | 223 ++++--- .../tests}/landlock_conflicts.rs | 86 +-- extrasafe/tests/multiple_conditions.rs | 29 + .../tests}/multiple_filters.rs | 37 +- {tests => extrasafe/tests}/network.rs | 88 +-- {tests => extrasafe/tests}/open_readonly.rs | 0 {tests => extrasafe/tests}/ruleset_union.rs | 63 +- extrasafe/tests/sleep.rs | 27 + {tests => extrasafe/tests}/sysno.rs | 0 .../tests}/test_ref_ruleset.rs | 6 +- {tests => extrasafe/tests}/tests_can_fail.rs | 3 +- {tests => extrasafe/tests}/thread_multi.rs | 8 +- {tests => extrasafe/tests}/thread_single.rs | 9 +- {tests => extrasafe/tests}/unsupported_os.rs | 3 +- {tests => extrasafe/tests}/use_fd.rs | 0 tests/multiple_conditions.rs | 22 - tests/sleep.rs | 27 - 49 files changed, 1740 insertions(+), 1211 deletions(-) delete mode 100644 examples/isolate_test.rs delete mode 100644 examples/user_guide_isolate.rs create mode 100644 extrasafe/Cargo.toml create mode 100644 extrasafe/README.md rename {examples => extrasafe/examples}/ipc_server_with_database.rs (84%) create mode 100644 extrasafe/examples/isolate_test.rs rename {examples => extrasafe/examples}/network_server.rs (95%) rename {examples => extrasafe/examples}/no_files_allow_stdout.rs (97%) rename {examples => extrasafe/examples}/server_with_database.rs (85%) rename {examples => extrasafe/examples}/simple_network.rs (76%) rename {examples => extrasafe/examples}/time.rs (52%) rename {examples => extrasafe/examples}/user-guide.rs (73%) create mode 100644 extrasafe/examples/user_guide_isolate.rs rename {src => extrasafe/src}/builtins/basic.rs (97%) rename {src => extrasafe/src}/builtins/danger_zone.rs (94%) rename {src => extrasafe/src}/builtins/mod.rs (93%) rename {src => extrasafe/src}/builtins/network.rs (89%) rename {src => extrasafe/src}/builtins/pipes.rs (100%) rename {src => extrasafe/src}/builtins/systemio.rs (84%) rename {src => extrasafe/src}/builtins/time.rs (75%) rename {src => extrasafe/src}/error.rs (100%) rename {src => extrasafe/src}/isolate/isolate_sys.rs (76%) rename {src => extrasafe/src}/isolate/mod.rs (92%) rename {src => extrasafe/src}/landlock.rs (91%) rename {src => extrasafe/src}/lib.rs (92%) rename {src => extrasafe/src}/macros.rs (65%) rename {tests => extrasafe/tests}/arg_comparisons.rs (84%) rename {tests => extrasafe/tests}/bad_combination.rs (72%) rename {tests => extrasafe/tests}/default_deny.rs (95%) rename {tests => extrasafe/tests}/inherit_filters.rs (96%) rename {tests => extrasafe/tests}/landlock_allthreads_fail.rs (58%) rename {tests => extrasafe/tests}/landlock_basic.rs (73%) rename {tests => extrasafe/tests}/landlock_conflicts.rs (55%) create mode 100644 extrasafe/tests/multiple_conditions.rs rename {tests => extrasafe/tests}/multiple_filters.rs (67%) rename {tests => extrasafe/tests}/network.rs (80%) rename {tests => extrasafe/tests}/open_readonly.rs (100%) rename {tests => extrasafe/tests}/ruleset_union.rs (69%) create mode 100644 extrasafe/tests/sleep.rs rename {tests => extrasafe/tests}/sysno.rs (100%) rename {tests => extrasafe/tests}/test_ref_ruleset.rs (70%) rename {tests => extrasafe/tests}/tests_can_fail.rs (88%) rename {tests => extrasafe/tests}/thread_multi.rs (91%) rename {tests => extrasafe/tests}/thread_single.rs (93%) rename {tests => extrasafe/tests}/unsupported_os.rs (78%) rename {tests => extrasafe/tests}/use_fd.rs (100%) delete mode 100644 tests/multiple_conditions.rs delete mode 100644 tests/sleep.rs diff --git a/Cargo.toml b/Cargo.toml index 53b1996..6afa26e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,40 +1,47 @@ -[package] -name = "extrasafe" +[workspace] +members = [ + "extrasafe" +] +resolver = "2" + +[workspace.package] version = "0.5.1" +authors = [ + "Harry Stern ", +] edition = "2021" -authors = ["Harry Stern ",] -description = "Make your code extrasafe by reducing what it can access." +rust-version = "1.74.0" repository = "https://github.com/boustrophedon/extrasafe" license = "MIT" -keywords = ["security", "seccomp", "landlock", "syscall"] -categories = ["os::linux-apis"] +categories = [ "os::linux-apis" ] -[package.metadata.docs.rs] +[workspace.package.metadata.docs.rs] all-features = true -[features] -landlock = ["dep:landlock"] -isolate = [] - -[dependencies] -seccompiler = { version = "^0.4", default-features = false } -libc = "^0.2" -syscalls = { version = "^0.6", default-features = false } -landlock = { version ="^0.3", optional = true } - -[dev-dependencies] +[workspace.dependencies] bytes = "^1" crossbeam = "^0.8" crossbeam-queue = "^0.3" crossbeam-channel = "^0.5" +landlock = "^0.3" +libc = "^0.2" +rusqlite = "^0.32" tempfile = "^3" tokio = "^1.15" -hyper = { version = "^0.14", features = ["http1", "server", "runtime", "tcp"] } warp = "^0.3" -rusqlite = "^0.26" -[target.'cfg(target_env = "musl")'.dev-dependencies] -reqwest = { version = "^0.11", default-features = false, features = ["rustls-tls"] } +[workspace.dependencies.hyper] +version = "^0.14" +features = [ "http1", "server", "runtime", "tcp" ] + +[workspace.dependencies.reqwest] +version = "^0.11" +default-features = false + +[workspace.dependencies.seccompiler] +version = "^0.4" +default-features = false -[target.'cfg(not(target_env = "musl"))'.dev-dependencies] -reqwest = { version = "^0.11" } +[workspace.dependencies.syscalls] +version = "^0.6" +default-features = false diff --git a/examples/isolate_test.rs b/examples/isolate_test.rs deleted file mode 100644 index 5450405..0000000 --- a/examples/isolate_test.rs +++ /dev/null @@ -1,429 +0,0 @@ -#![cfg(feature = "isolate")] -/// Tests for isolate have to go in examples because tests in the tests/ directory get compiled as -/// test binaries and have their main fn overridden - -// TODO: check unix domain sockets work as expected with isolated network namespace - -use std::io::prelude::*; -use std::os::unix::net::{UnixStream, UnixListener}; -use std::path::PathBuf; -use std::fs::File; - -use std::collections::HashMap; -use extrasafe::isolate::Isolate; - -fn check_isolate_output(isolate_name: &'static str, data: &[&str], envs: &HashMap) { - let output = Isolate::run(isolate_name, envs).expect("running isolate failed"); - - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - let outinfo = format!("\nstdout:\n{}\nstderr:\n{}", stdout, stderr); - - assert!(output.status.success(), "{:?}\n{}", output.status, outinfo); - for s in data { - assert!(stdout.contains(s), "{}", outinfo); - } - - // check tmp dir is cleaned up - for path in std::fs::read_dir("/tmp").unwrap().flatten() { - let path = path.path(); - // NOTE: this might fail if you ran a test and it failed in a way the temp dir couldn't - // be cleaned up (e.g. the strace segfault thing) - assert!(!path.starts_with(isolate_name), "tmp dir still exists: {:?}", path.display()); - } - - println!("{} passed", isolate_name); -} - -fn check_isolate_output_fail(isolate_name: &'static str, data: &[&str], envs: &HashMap) { - let output = Isolate::run(isolate_name, envs).expect("running isolate failed"); - - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - let outinfo = format!("\nstdout:\n{}\nstderr:\n{}", stdout, stderr); - - assert!(!output.status.success(), "isolate incorrently exited successfully: {:?}", output.status); - for s in data { - assert!(stderr.contains(s), "{}", outinfo); - } - println!("{} passed", isolate_name); -} - - -/// Test that running an isolate that assert(false) prints it to stderr and has a nonzero exit -/// code. -fn test_isolate_fail() { - check_isolate_output_fail("isolate_fail", &["wild panic",], &HashMap::new()); -} - -/// Test that printing hello in the isolate is captured in the parent -fn test_isolate_hello() { - check_isolate_output("isolate_hello", &["hello",], &HashMap::new()); -} - -/// Test that the isolate's uid is 0 -fn test_isolate_uid() { - check_isolate_output("isolate_uid", &["uid: 0",], &HashMap::new()); -} - -/// Test that we can mount a new proc and the mountinfo is correct -fn test_check_mountinfo() { - let output = Isolate::run("check_mountinfo", &HashMap::new()).expect("running isolate failed"); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - assert!(stdout.contains("/ / "), "missing root mount"); - assert!(!stdout.contains("/tmp"), "tmp from parent namespace visible"); - assert!(!stdout.contains("/proc_orig"), "proc from parent namespace visible"); - assert!(stderr.is_empty(), "stderr: {}", stderr); - println!("check_mountinfo passed"); -} - -/// Test that we can bindmount a unix socket in a tempdir into the isolate and send a message from -/// the child to the parent. -fn test_unix_socket() { - let tempdir = tempfile::tempdir().unwrap(); - let path = tempdir.path().join("parent.sock"); - let envs = vec![("ISOLATE_SOCKET_PATH".to_string(), path.display().to_string())].into_iter().collect(); - let handle = std::thread::spawn(move || { - let output = Isolate::run("isolate_unix_socket", &envs).expect("running isolate failed"); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - assert!(output.status.success(), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - }); - - let mut conn = UnixListener::bind(path).unwrap().incoming() - .next() - .unwrap() - .unwrap(); - let mut resp = String::new(); - conn.read_to_string(&mut resp).unwrap(); - - assert_eq!(resp, "hello from isolate"); - // make sure isolate runner thread exited successfully (although if it didn't we wouldn't - // actually get here presumably because we'd be stuck at the unix socket's accept) - let res = handle.join(); - assert!(res.is_ok(), "{:?}", res.unwrap_err()); - println!("isolate_unix_socket passed"); -} - -/// Test we can bindmount multiple directories at once -fn test_multiple_binds() { - let tempdir1 = tempfile::tempdir().unwrap(); - let tempdir2 = tempfile::tempdir().unwrap(); - let path1 = tempdir1.path(); - let path2 = tempdir2.path(); - let envs = vec![ - ("ISOLATE_DIR_1".to_string(), path1.display().to_string()), - ("ISOLATE_DIR_2".to_string(), path2.display().to_string()), - ].into_iter().collect(); - - // run isolate - let output = Isolate::run("isolate_multiple_binds", &envs).expect("running isolate failed"); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - assert!(output.status.success(), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - - // read back values written inside isolate - let f1 = std::fs::read_to_string(path1.join("hello")).unwrap(); - let f2 = std::fs::read_to_string(path2.join("hey")).unwrap(); - - assert_eq!(f1, "abc"); - assert_eq!(f2, "xyz"); - println!("isolate_multiple_binds passed"); -} - -// TODO consolidate these into check_isolate_output/fails - -/// Test tmpfs size limit parameter works -fn test_tmpfs_size_limit() { - let output = Isolate::run("isolate_size_limit", &HashMap::new()) - .expect("running isolate failed"); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - // The isolate unwraps a write larger than the size limit for the tmpfs - assert!(!output.status.success(), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - assert!(stderr.contains("large file failed"), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - assert!(stderr.contains("No space left on device"), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - - println!("isolate_tmpfs_size passed"); -} - -/// Test making a network request succeeds if network is kept -/// Obviously, this requires that the parent namespace has a working network connection. -fn test_with_network() { - let output = Isolate::run("isolate_with_network", &HashMap::new()) - .expect("running isolate failed"); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - assert!(output.status.success(), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - println!("isolate_with_network passed"); -} - -/// Test making a network request does not succeed if new network namespace is created -fn test_no_network() { - let output = Isolate::run("isolate_no_network", &HashMap::new()) - .expect("running isolate failed"); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - assert!(!output.status.success(), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - assert!(stderr.contains("ConnectError"), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - println!("isolate_no_network passed"); -} - -/// Test we can use a standard `extrasafe::SafetyContext` inside an `Isolate` -fn test_safetycontext() { - let output = Isolate::run("isolate_with_safetycontext", &HashMap::new()) - .expect("running isolate failed"); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - assert!(output.status.success(), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - assert!(stdout.contains("we can print to stdout"), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - assert!(stderr.contains("we can print to stderr"), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - println!("isolate_with_safetycontext passed"); - -} - -/// Test an `Isolate` will not bindmount outside of its root -fn test_isolate_bad_bindmount() { - // test /.. - let output = Isolate::run("isolate_bad_bindmount_absolute", &HashMap::new()) - .expect("running isolate failed"); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - assert!(!output.status.success(), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - assert!(stderr.contains("dst directory must not contain .. paths:"), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - - // test ./ - let output = Isolate::run("isolate_bad_bindmount_relative", &HashMap::new()) - .expect("running isolate failed"); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - assert!(!output.status.success(), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - assert!(stderr.contains("dst directory must not contain . paths:"), "{:?}\nstdout {}\nstderr {}", output.status, stdout, stderr); - println!("isolate_bad_bindmount passed"); -} - - -fn isolate_uid(name: &'static str) -> Isolate { - fn uid() { - let uid = unsafe { libc::getuid() }; - println!("uid: {}", uid); - } - Isolate::new(name, uid) -} - -fn isolate_fail(name: &'static str) -> Isolate { - fn fail() { - panic!("a wild panic appears"); - } - Isolate::new(name, fail) -} - -fn isolate_hello(name: &'static str) -> Isolate { - fn hello() { - println!("hello"); - } - Isolate::new(name, hello) -} - -#[allow(unsafe_code)] -fn check_mountinfo() { - use std::ffi::CString; - - // mount new proc - std::fs::create_dir_all("/proc").unwrap(); - let proc_dircstr = CString::new("/proc").unwrap(); - let proc_cstr = CString::new("proc").unwrap(); - let rc = unsafe { libc::mount(proc_cstr.as_ptr(), - proc_dircstr.as_ptr(), - proc_cstr.as_ptr(), - 0, - std::ptr::null()) }; - assert!(rc >= 0, "failed to mount new proc"); - - // unmount old proc - let orig_proc_dircstr = CString::new("/orig_proc").unwrap(); - let rc = unsafe { libc::umount2(orig_proc_dircstr.as_ptr(), libc::MNT_DETACH) }; - assert!(rc >= 0, "failed to unmount old proc"); - - // read new proc mountinfo and print to be captured in test_check_mountinfo - let s = std::fs::read_to_string("/proc/self/mountinfo").unwrap(); - println!("mountinfo:\n{}", s); -} - -fn isolate_unix_socket(name: &'static str) -> Isolate { - fn unix_hello() { - let mut stream = UnixStream::connect("/isolate.sock").unwrap(); - stream.write_all(b"hello from isolate").unwrap(); - } - let path = std::env::var("ISOLATE_SOCKET_PATH").unwrap(); - let path = PathBuf::from(path); - Isolate::new(name, unix_hello) - .add_bind_mount(path, "/isolate.sock") -} - -fn isolate_multiple_binds(name: &'static str) -> Isolate { - fn multiple_binds() { - File::create("/path1/hello").unwrap() - .write_all(b"abc").unwrap(); - File::create("/path2/hey").unwrap() - .write_all(b"xyz").unwrap(); - } - - let path1 = std::env::var("ISOLATE_DIR_1").unwrap(); - let path1 = PathBuf::from(path1); - let path2 = std::env::var("ISOLATE_DIR_2").unwrap(); - let path2 = PathBuf::from(path2); - Isolate::new(name, multiple_binds) - .add_bind_mount(path1, "/path1") - .add_bind_mount(path2, "/path2") -} - -fn isolate_size_limit(name: &'static str) -> Isolate { - #[allow(clippy::cast_possible_truncation)] - fn big_write() { - let size = 1_500_000; - let mut v = Vec::with_capacity(size); - for i in 0..size { - v.push((i % 255) as u8); - } - - // This will fail - File::create("/test").unwrap() - .write_all(&v) - .expect("writing large file failed"); - } - - Isolate::new(name, big_write) - .set_rootfs_size(1) -} - -// make an https request to example.org and check we can connect -fn network_call() { - let runtime = tokio::runtime::Builder::new_current_thread() - .worker_threads(1) - .enable_all() - .build() - .unwrap(); - runtime.block_on(async { - let resp = reqwest::get("https://example.org/").await; - - // will succeed in with_network and fail in no_network - assert!( - resp.is_ok(), - "failed getting example.org response: {:?}", - resp.unwrap_err() - ); - }); -} - -fn isolate_with_network(name: &'static str) -> Isolate { - Isolate::new(name, network_call) - // ssl and dns files are all over the place. - // If you wanted you could further restrict it via landlock or by mounting only specific - // files and directories but it highly depends on your operating system and DNS setup. One - // thing in particular to note is that if a file exists but it's a symlink to somewhere - // outside the filesystem, something (e.g. openssl) might see that the file is there and - // it can stat it, but then will try to read the file and crash. - .add_bind_mount("/etc", "/etc") - .add_bind_mount("/usr", "/usr") - .add_bind_mount("/run", "/run") - .add_bind_mount("/lib", "/lib") - .new_network(false) -} - -fn isolate_no_network(name: &'static str) -> Isolate { - Isolate::new(name, network_call) - .add_bind_mount("/", "/") -} - -fn isolate_bad_bindmount_absolute(name: &'static str) -> Isolate { - // we will not reach the actual isolate so it doesn't matter what function we call - fn hello() { - println!("hello"); - } - Isolate::new(name, hello) - .add_bind_mount("/", "/a/b/../../..") -} - -fn isolate_bad_bindmount_relative(name: &'static str) -> Isolate { - // we will not reach the actual isolate so it doesn't matter what function we call - fn hello() { - println!("hello"); - } - Isolate::new(name, hello) - .add_bind_mount("/", "./a/") -} - -fn isolate_with_safetycontext(name: &'static str) -> Isolate { - use extrasafe::SafetyContext; - use extrasafe::builtins::*; - fn use_safetycontext() { - SafetyContext::new() - .enable(SystemIO::nothing() - .allow_stdout() - .allow_stderr() - ).unwrap() - // can apply to all threads because we're in a separate process - .apply_to_all_threads().unwrap(); - - println!("we can print to stdout!"); - eprintln!("we can print to stderr!"); - - assert!(File::create("test").is_err(), "shouldn't be able to open files"); - } - - Isolate::new(name, use_safetycontext) -} - -fn main() { - // Hooks first - Isolate::main_hook("isolate_hello", isolate_hello); - Isolate::main_hook("isolate_uid", isolate_uid); - Isolate::main_hook("check_mountinfo", |s| { - Isolate::new(s, check_mountinfo) - .add_bind_mount("/proc", "/orig_proc") - }); - Isolate::main_hook("isolate_size_limit", isolate_size_limit); - Isolate::main_hook("isolate_fail", isolate_fail); - Isolate::main_hook("isolate_unix_socket", isolate_unix_socket); - Isolate::main_hook("isolate_multiple_binds", isolate_multiple_binds); - Isolate::main_hook("isolate_with_network", isolate_with_network); - Isolate::main_hook("isolate_no_network", isolate_no_network); - Isolate::main_hook("isolate_with_safetycontext", isolate_with_safetycontext); - Isolate::main_hook("isolate_bad_bindmount_absolute", isolate_bad_bindmount_absolute); - Isolate::main_hook("isolate_bad_bindmount_relative", isolate_bad_bindmount_relative); - - let argv0 = std::env::args().next().unwrap(); - if argv0.contains("isolate_test") { - // These tests actually launch the isolates, which then hit the hooks above after - // re-execing - test_isolate_hello(); - test_isolate_uid(); - test_check_mountinfo(); - test_unix_socket(); - test_multiple_binds(); - test_with_network(); - test_safetycontext(); - - // TODO: for some reason these tests where the isolate panics make strace think there are - // segfaults happening, and gets stuck continully outputting sigsegv messages even though - // the program runs fine without strace. for now just put them at the end, but we should - // investigate why it happens. - // valgrind doesn't catch anything. - test_no_network(); - test_tmpfs_size_limit(); - test_isolate_fail(); - test_isolate_bad_bindmount(); - } - else { - panic!("isolate didn't hit its hook: {}", argv0); - } -} diff --git a/examples/user_guide_isolate.rs b/examples/user_guide_isolate.rs deleted file mode 100644 index dfc4bdb..0000000 --- a/examples/user_guide_isolate.rs +++ /dev/null @@ -1,42 +0,0 @@ -#![cfg(feature = "isolate")] -use extrasafe::isolate::Isolate; -use std::collections::HashMap; -use std::path::PathBuf; - -const EXAMPLE_ISOLATE: &str = "user guide isolate"; - -/// The function that ultimately runs inside the Isolate -fn do_cool_thing() { - println!("I'm going to read some files from /cooldir and do cool stuff with it!"); - // TODO: do cool stuff with the files in /cooldir -} - -/// Isolate configuration that happens when the program is re-executed after `Isolate::run` -fn setup_isolate(name: &'static str) -> Isolate { - let path = std::env::var("COOL_DIRECTORY").unwrap(); - let path = PathBuf::from(path); - Isolate::new(name, do_cool_thing) - // This will mount /a/b/c from the parent into /cooldir in the child, - // but not until after entering the namespace. - .add_bind_mount(path, "/cooldir") - // Limit the amount of data that can be written to the filesystem the - // Isolate lives in. - .set_rootfs_size(1) -} - -fn main() { - // Once the Isolate::run call is made, this will run the setup function, - // enter the namespace and call the the function provided. The first time the program runs, - // this code will be ignored. - Isolate::main_hook(EXAMPLE_ISOLATE, setup_isolate); - - // ... somewhere later in the program - - let env_vars = HashMap::from([("COOL_DIRECTORY".to_string(), "/".to_string())]); - - // `Isolate::run` returns a normal `std::process::Output` - let output = Isolate::run(EXAMPLE_ISOLATE, &env_vars).unwrap(); - - assert!(output.status.success()); - println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); -} diff --git a/extrasafe/Cargo.toml b/extrasafe/Cargo.toml new file mode 100644 index 0000000..c20cde1 --- /dev/null +++ b/extrasafe/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "extrasafe" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +description = "Make your code extrasafe by reducing what it can access." +documentation = "https://docs.rs/extrasafe/latest/extrasafe" +readme = "README.md" +#homepage = "" +repository.workspace = true +license.workspace = true +keywords = [ "security", "seccomp", "landlock", "syscall" ] +categories.workspace = true +metadata.workspace = true + +[features] +landlock = [ "dep:landlock" ] +isolate = [ ] + +[dependencies] +libc.workspace = true +seccompiler.workspace = true +syscalls.workspace = true + +# Optional Dependencies: +landlock = { workspace = true, optional = true } + +[dev-dependencies] +bytes.workspace = true +crossbeam.workspace = true +crossbeam-queue.workspace = true +crossbeam-channel.workspace = true +hyper.workspace = true +rusqlite.workspace = true +tempfile.workspace = true +tokio.workspace = true +warp.workspace = true + +[target.'cfg(target_env = "musl")'.dev-dependencies.reqwest] +workspace = true +features = [ "rustls-tls" ] + +[target.'cfg(not(target_env = "musl"))'.dev-dependencies.reqwest] +workspace = true +features = [ "default" ] diff --git a/extrasafe/README.md b/extrasafe/README.md new file mode 100644 index 0000000..d1ed2f2 --- /dev/null +++ b/extrasafe/README.md @@ -0,0 +1 @@ +# extrasafe diff --git a/examples/ipc_server_with_database.rs b/extrasafe/examples/ipc_server_with_database.rs similarity index 84% rename from examples/ipc_server_with_database.rs rename to extrasafe/examples/ipc_server_with_database.rs index 43309f0..cf4cd88 100644 --- a/examples/ipc_server_with_database.rs +++ b/extrasafe/examples/ipc_server_with_database.rs @@ -34,7 +34,10 @@ type DbConn = Arc>; fn run_subprocess(cmd: &[&str]) -> std::process::Child { let exe_path = std::env::current_exe().unwrap(); - let args: Vec<_> = ["run_main", "--", "--sub"].iter().chain(cmd.iter()).collect(); + let args: Vec<_> = ["run_main", "--", "--sub"] + .iter() + .chain(cmd.iter()) + .collect(); std::process::Command::new(exe_path.to_str().unwrap()) .arg0(format!("{}-subprocess", cmd[0])) @@ -56,19 +59,22 @@ fn run_webserver(db_socket_path: &str, our_socket_path: &str) { println!("webserver thread connecting to db unix socket"); let socket = UnixDatagram::bind(our_socket_path).expect("failed to create unix dg socket"); - socket.connect(db_socket_path).expect("failed to connect to db socket"); + socket + .connect(db_socket_path) + .expect("failed to connect to db socket"); let db_socket: DbConn = Arc::new(Mutex::new(socket)); // set up runtime let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() - .build().unwrap(); + .build() + .unwrap(); let listener = std::net::TcpListener::bind("127.0.0.1:5576").unwrap(); // extrasafe context SafetyContext::new() - .enable(Networking::nothing() - .allow_running_tcp_servers()).unwrap() + .enable(Networking::nothing().allow_running_tcp_servers()) + .unwrap() .apply_to_current_thread() .unwrap(); @@ -110,8 +116,7 @@ fn run_webserver(db_socket_path: &str, our_socket_path: &str) { .to_string(); messages - }) - ); + })); let svc = warp::service(routes); let make_svc = hyper::service::make_service_fn(move |_| { @@ -146,25 +151,31 @@ fn run_db(socket_path: &str) { db.pragma_update(None, "locking_mode", "exclusive").unwrap(); db.pragma_update(None, "journal_mode", "wal").unwrap(); - db.execute("CREATE TABLE messages ( msg TEXT NOT NULL );", []).unwrap(); + db.execute("CREATE TABLE messages ( msg TEXT NOT NULL );", []) + .unwrap(); let mut get_rows = db.prepare("SELECT msg FROM messages;").unwrap(); let mut insert_row = db.prepare("INSERT INTO messages VALUES (?)").unwrap(); // after opening connection socket and db file, set extrasafe context SafetyContext::new() - .enable(Networking::nothing() - .allow_connect() - .yes_really() - .allow_running_unix_servers() - ).unwrap() - .enable(SystemIO::nothing() - .allow_read() - .allow_write() - .allow_metadata() - .allow_ioctl() - .allow_close()).unwrap() - .enable(Threads::nothing() - .allow_sleep().yes_really()).unwrap() + .enable( + Networking::nothing() + .allow_connect() + .yes_really() + .allow_running_unix_servers(), + ) + .unwrap() + .enable( + SystemIO::nothing() + .allow_read() + .allow_write() + .allow_metadata() + .allow_ioctl() + .allow_close(), + ) + .unwrap() + .enable(Threads::nothing().allow_sleep().yes_really()) + .unwrap() .apply_to_current_thread() .unwrap(); @@ -174,7 +185,8 @@ fn run_db(socket_path: &str) { loop { println!("db server waiting for unix socket message"); let mut buf: [u8; 100] = [0; 100]; - let (count, return_addr) = sock.recv_from(&mut buf) + let (count, return_addr) = sock + .recv_from(&mut buf) .expect("failed reading request to db server"); let buf = String::from_utf8(buf[..count].to_vec()) @@ -185,18 +197,17 @@ fn run_db(socket_path: &str) { let msg: DBMsg; if buf == "list" { msg = DBMsg::List; - } - else if buf.starts_with("write") { + } else if buf.starts_with("write") { msg = DBMsg::Write(buf[6..].to_string()); - } - else { + } else { panic!("unknown message recieved in db: {}", buf); } match msg { DBMsg::List => { let messages: Vec = get_rows - .query_map([], |row| row.get(0)).unwrap() + .query_map([], |row| row.get(0)) + .unwrap() .map(Result::unwrap) .collect(); @@ -219,8 +230,8 @@ fn run_client_write(msg: &str) { // Set up extrasafe context SafetyContext::new() - .enable(Networking::nothing() - .allow_start_tcp_clients()).unwrap() + .enable(Networking::nothing().allow_start_tcp_clients()) + .unwrap() .apply_to_current_thread() .unwrap(); println!("about to make request with msg {}", msg); @@ -259,34 +270,35 @@ fn run_client_read() { // enable extrasafe context let ctx = SafetyContext::new() - .enable(Networking::nothing() - // Necessary for DNS - .allow_start_udp_servers().yes_really() - .allow_start_tcp_clients() - ).unwrap() + .enable( + Networking::nothing() + // Necessary for DNS + .allow_start_udp_servers() + .yes_really() + .allow_start_tcp_clients(), + ) + .unwrap() // For some reason only if we make two requests with a client does it use multiple threads, // so we only need them in the reader thread rather than the writer. - .enable(Threads::nothing() - .allow_create()).unwrap(); + .enable(Threads::nothing().allow_create()) + .unwrap(); #[cfg(not(feature = "landlock"))] - let ctx = ctx.enable( + let ctx = ctx + .enable( SystemIO::nothing() .allow_open_readonly() .allow_read() .allow_metadata() .allow_close(), - ).unwrap(); + ) + .unwrap(); #[cfg(feature = "landlock")] - let ctx = ctx.enable( - SystemIO::nothing() - .allow_dns_files() - .allow_ssl_files() - ).unwrap(); - - ctx.apply_to_current_thread() + let ctx = ctx + .enable(SystemIO::nothing().allow_dns_files().allow_ssl_files()) .unwrap(); + ctx.apply_to_current_thread().unwrap(); // make request runtime.block_on(async { @@ -326,16 +338,13 @@ fn main() { if args.contains(&"--sub".into()) { // If args is "example_prog [possible other options] --sub subcommand subargs...", run the subcommand if let Some(idx) = args.iter().position(|s| s == "db") { - run_db(&args[idx+1]); - } - else if let Some(idx) = args.iter().position(|s| s == "webserver") { - run_webserver(&args[idx+1], &args[idx+2]); - } - else if args.contains(&"read_client".into()) { + run_db(&args[idx + 1]); + } else if let Some(idx) = args.iter().position(|s| s == "webserver") { + run_webserver(&args[idx + 1], &args[idx + 2]); + } else if args.contains(&"read_client".into()) { run_client_read(); - } - else if let Some(idx) = args.iter().position(|s| s == "write_client") { - run_client_write(&args[idx+1]); + } else if let Some(idx) = args.iter().position(|s| s == "write_client") { + run_client_write(&args[idx + 1]); } return; } @@ -354,7 +363,11 @@ fn main() { let mut db_child = run_subprocess(&["db", db_path.to_str().unwrap()]); std::thread::sleep(std::time::Duration::from_millis(100)); - let mut webserver_child = run_subprocess(&["webserver", db_path.to_str().unwrap(), web_path.to_str().unwrap()]); + let mut webserver_child = run_subprocess(&[ + "webserver", + db_path.to_str().unwrap(), + web_path.to_str().unwrap(), + ]); std::thread::sleep(std::time::Duration::from_millis(100)); // -- write "hello" to db diff --git a/extrasafe/examples/isolate_test.rs b/extrasafe/examples/isolate_test.rs new file mode 100644 index 0000000..d24e665 --- /dev/null +++ b/extrasafe/examples/isolate_test.rs @@ -0,0 +1,565 @@ +//! Tests for isolate have to go in examples because tests in the tests/ directory get compiled as +//! test binaries and have their main fn overridden +// TODO: check unix domain sockets work as expected with isolated network namespace + +#[cfg(feature = "isolate")] +mod with_isolate { + use std::fs::File; + use std::io::prelude::*; + use std::os::unix::net::{UnixListener, UnixStream}; + use std::path::PathBuf; + + use extrasafe::isolate::Isolate; + use std::collections::HashMap; + + fn check_isolate_output( + isolate_name: &'static str, + data: &[&str], + envs: &HashMap, + ) { + let output = Isolate::run(isolate_name, envs).expect("running isolate failed"); + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let outinfo = format!("\nstdout:\n{}\nstderr:\n{}", stdout, stderr); + + assert!(output.status.success(), "{:?}\n{}", output.status, outinfo); + for s in data { + assert!(stdout.contains(s), "{}", outinfo); + } + + // check tmp dir is cleaned up + for path in std::fs::read_dir("/tmp").unwrap().flatten() { + let path = path.path(); + // NOTE: this might fail if you ran a test and it failed in a way the temp dir couldn't + // be cleaned up (e.g. the strace segfault thing) + assert!( + !path.starts_with(isolate_name), + "tmp dir still exists: {:?}", + path.display() + ); + } + + println!("{} passed", isolate_name); + } + + fn check_isolate_output_fail( + isolate_name: &'static str, + data: &[&str], + envs: &HashMap, + ) { + let output = Isolate::run(isolate_name, envs).expect("running isolate failed"); + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let outinfo = format!("\nstdout:\n{}\nstderr:\n{}", stdout, stderr); + + assert!( + !output.status.success(), + "isolate incorrently exited successfully: {:?}", + output.status + ); + for s in data { + assert!(stderr.contains(s), "{}", outinfo); + } + println!("{} passed", isolate_name); + } + + /// Test that running an isolate that assert(false) prints it to stderr and has a nonzero exit + /// code. + fn test_isolate_fail() { + check_isolate_output_fail("isolate_fail", &["wild panic"], &HashMap::new()); + } + + /// Test that printing hello in the isolate is captured in the parent + fn test_isolate_hello() { + check_isolate_output("isolate_hello", &["hello"], &HashMap::new()); + } + + /// Test that the isolate's uid is 0 + fn test_isolate_uid() { + check_isolate_output("isolate_uid", &["uid: 0"], &HashMap::new()); + } + + /// Test that we can mount a new proc and the mountinfo is correct + fn test_check_mountinfo() { + let output = + Isolate::run("check_mountinfo", &HashMap::new()).expect("running isolate failed"); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + assert!(stdout.contains("/ / "), "missing root mount"); + assert!( + !stdout.contains("/tmp"), + "tmp from parent namespace visible" + ); + assert!( + !stdout.contains("/proc_orig"), + "proc from parent namespace visible" + ); + assert!(stderr.is_empty(), "stderr: {}", stderr); + println!("check_mountinfo passed"); + } + + /// Test that we can bindmount a unix socket in a tempdir into the isolate and send a message from + /// the child to the parent. + fn test_unix_socket() { + let tempdir = tempfile::tempdir().unwrap(); + let path = tempdir.path().join("parent.sock"); + let envs = vec![( + "ISOLATE_SOCKET_PATH".to_string(), + path.display().to_string(), + )] + .into_iter() + .collect(); + let handle = std::thread::spawn(move || { + let output = + Isolate::run("isolate_unix_socket", &envs).expect("running isolate failed"); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + assert!( + output.status.success(), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + }); + + let mut conn = UnixListener::bind(path) + .unwrap() + .incoming() + .next() + .unwrap() + .unwrap(); + let mut resp = String::new(); + conn.read_to_string(&mut resp).unwrap(); + + assert_eq!(resp, "hello from isolate"); + // make sure isolate runner thread exited successfully (although if it didn't we wouldn't + // actually get here presumably because we'd be stuck at the unix socket's accept) + let res = handle.join(); + assert!(res.is_ok(), "{:?}", res.unwrap_err()); + println!("isolate_unix_socket passed"); + } + + /// Test we can bindmount multiple directories at once + fn test_multiple_binds() { + let tempdir1 = tempfile::tempdir().unwrap(); + let tempdir2 = tempfile::tempdir().unwrap(); + let path1 = tempdir1.path(); + let path2 = tempdir2.path(); + let envs = vec![ + ("ISOLATE_DIR_1".to_string(), path1.display().to_string()), + ("ISOLATE_DIR_2".to_string(), path2.display().to_string()), + ] + .into_iter() + .collect(); + + // run isolate + let output = Isolate::run("isolate_multiple_binds", &envs).expect("running isolate failed"); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + assert!( + output.status.success(), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + + // read back values written inside isolate + let f1 = std::fs::read_to_string(path1.join("hello")).unwrap(); + let f2 = std::fs::read_to_string(path2.join("hey")).unwrap(); + + assert_eq!(f1, "abc"); + assert_eq!(f2, "xyz"); + println!("isolate_multiple_binds passed"); + } + + // TODO consolidate these into check_isolate_output/fails + + /// Test tmpfs size limit parameter works + fn test_tmpfs_size_limit() { + let output = + Isolate::run("isolate_size_limit", &HashMap::new()).expect("running isolate failed"); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + // The isolate unwraps a write larger than the size limit for the tmpfs + assert!( + !output.status.success(), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + assert!( + stderr.contains("large file failed"), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + assert!( + stderr.contains("No space left on device"), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + + println!("isolate_tmpfs_size passed"); + } + + /// Test making a network request succeeds if network is kept + /// Obviously, this requires that the parent namespace has a working network connection. + fn test_with_network() { + let output = + Isolate::run("isolate_with_network", &HashMap::new()).expect("running isolate failed"); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + assert!( + output.status.success(), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + println!("isolate_with_network passed"); + } + + /// Test making a network request does not succeed if new network namespace is created + fn test_no_network() { + let output = + Isolate::run("isolate_no_network", &HashMap::new()).expect("running isolate failed"); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + assert!( + !output.status.success(), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + assert!( + stderr.contains("ConnectError"), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + println!("isolate_no_network passed"); + } + + /// Test we can use a standard `extrasafe::SafetyContext` inside an `Isolate` + fn test_safetycontext() { + let output = Isolate::run("isolate_with_safetycontext", &HashMap::new()) + .expect("running isolate failed"); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + assert!( + output.status.success(), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + assert!( + stdout.contains("we can print to stdout"), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + assert!( + stderr.contains("we can print to stderr"), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + println!("isolate_with_safetycontext passed"); + } + + /// Test an `Isolate` will not bindmount outside of its root + fn test_isolate_bad_bindmount() { + // test /.. + let output = Isolate::run("isolate_bad_bindmount_absolute", &HashMap::new()) + .expect("running isolate failed"); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + assert!( + !output.status.success(), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + assert!( + stderr.contains("dst directory must not contain .. paths:"), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + + // test ./ + let output = Isolate::run("isolate_bad_bindmount_relative", &HashMap::new()) + .expect("running isolate failed"); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + assert!( + !output.status.success(), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + assert!( + stderr.contains("dst directory must not contain . paths:"), + "{:?}\nstdout {}\nstderr {}", + output.status, + stdout, + stderr + ); + println!("isolate_bad_bindmount passed"); + } + + fn isolate_uid(name: &'static str) -> Isolate { + fn uid() { + let uid = unsafe { libc::getuid() }; + println!("uid: {}", uid); + } + Isolate::new(name, uid) + } + + fn isolate_fail(name: &'static str) -> Isolate { + fn fail() { + panic!("a wild panic appears"); + } + Isolate::new(name, fail) + } + + fn isolate_hello(name: &'static str) -> Isolate { + fn hello() { + println!("hello"); + } + Isolate::new(name, hello) + } + + #[allow(unsafe_code)] + fn check_mountinfo() { + use std::ffi::CString; + + // mount new proc + std::fs::create_dir_all("/proc").unwrap(); + let proc_dircstr = CString::new("/proc").unwrap(); + let proc_cstr = CString::new("proc").unwrap(); + let rc = unsafe { + libc::mount( + proc_cstr.as_ptr(), + proc_dircstr.as_ptr(), + proc_cstr.as_ptr(), + 0, + std::ptr::null(), + ) + }; + assert!(rc >= 0, "failed to mount new proc"); + + // unmount old proc + let orig_proc_dircstr = CString::new("/orig_proc").unwrap(); + let rc = unsafe { libc::umount2(orig_proc_dircstr.as_ptr(), libc::MNT_DETACH) }; + assert!(rc >= 0, "failed to unmount old proc"); + + // read new proc mountinfo and print to be captured in test_check_mountinfo + let s = std::fs::read_to_string("/proc/self/mountinfo").unwrap(); + println!("mountinfo:\n{}", s); + } + + fn isolate_unix_socket(name: &'static str) -> Isolate { + fn unix_hello() { + let mut stream = UnixStream::connect("/isolate.sock").unwrap(); + stream.write_all(b"hello from isolate").unwrap(); + } + let path = std::env::var("ISOLATE_SOCKET_PATH").unwrap(); + let path = PathBuf::from(path); + Isolate::new(name, unix_hello).add_bind_mount(path, "/isolate.sock") + } + + fn isolate_multiple_binds(name: &'static str) -> Isolate { + fn multiple_binds() { + File::create("/path1/hello") + .unwrap() + .write_all(b"abc") + .unwrap(); + File::create("/path2/hey") + .unwrap() + .write_all(b"xyz") + .unwrap(); + } + + let path1 = std::env::var("ISOLATE_DIR_1").unwrap(); + let path1 = PathBuf::from(path1); + let path2 = std::env::var("ISOLATE_DIR_2").unwrap(); + let path2 = PathBuf::from(path2); + Isolate::new(name, multiple_binds) + .add_bind_mount(path1, "/path1") + .add_bind_mount(path2, "/path2") + } + + fn isolate_size_limit(name: &'static str) -> Isolate { + #[allow(clippy::cast_possible_truncation)] + fn big_write() { + let size = 1_500_000; + let mut v = Vec::with_capacity(size); + for i in 0..size { + v.push((i % 255) as u8); + } + + // This will fail + File::create("/test") + .unwrap() + .write_all(&v) + .expect("writing large file failed"); + } + + Isolate::new(name, big_write).set_rootfs_size(1) + } + + // make an https request to example.org and check we can connect + fn network_call() { + let runtime = tokio::runtime::Builder::new_current_thread() + .worker_threads(1) + .enable_all() + .build() + .unwrap(); + runtime.block_on(async { + let resp = reqwest::get("https://example.org/").await; + + // will succeed in with_network and fail in no_network + assert!( + resp.is_ok(), + "failed getting example.org response: {:?}", + resp.unwrap_err() + ); + }); + } + + fn isolate_with_network(name: &'static str) -> Isolate { + Isolate::new(name, network_call) + // ssl and dns files are all over the place. + // If you wanted you could further restrict it via landlock or by mounting only specific + // files and directories but it highly depends on your operating system and DNS setup. One + // thing in particular to note is that if a file exists but it's a symlink to somewhere + // outside the filesystem, something (e.g. openssl) might see that the file is there and + // it can stat it, but then will try to read the file and crash. + .add_bind_mount("/etc", "/etc") + .add_bind_mount("/usr", "/usr") + .add_bind_mount("/run", "/run") + .add_bind_mount("/lib", "/lib") + .new_network(false) + } + + fn isolate_no_network(name: &'static str) -> Isolate { + Isolate::new(name, network_call).add_bind_mount("/", "/") + } + + fn isolate_bad_bindmount_absolute(name: &'static str) -> Isolate { + // we will not reach the actual isolate so it doesn't matter what function we call + fn hello() { + println!("hello"); + } + Isolate::new(name, hello).add_bind_mount("/", "/a/b/../../..") + } + + fn isolate_bad_bindmount_relative(name: &'static str) -> Isolate { + // we will not reach the actual isolate so it doesn't matter what function we call + fn hello() { + println!("hello"); + } + Isolate::new(name, hello).add_bind_mount("/", "./a/") + } + + fn isolate_with_safetycontext(name: &'static str) -> Isolate { + use extrasafe::builtins::*; + use extrasafe::SafetyContext; + fn use_safetycontext() { + SafetyContext::new() + .enable(SystemIO::nothing().allow_stdout().allow_stderr()) + .unwrap() + // can apply to all threads because we're in a separate process + .apply_to_all_threads() + .unwrap(); + + println!("we can print to stdout!"); + eprintln!("we can print to stderr!"); + + assert!( + File::create("test").is_err(), + "shouldn't be able to open files" + ); + } + + Isolate::new(name, use_safetycontext) + } + + pub(super) fn main() { + // Hooks first + Isolate::main_hook("isolate_hello", isolate_hello); + Isolate::main_hook("isolate_uid", isolate_uid); + Isolate::main_hook("check_mountinfo", |s| { + Isolate::new(s, check_mountinfo).add_bind_mount("/proc", "/orig_proc") + }); + Isolate::main_hook("isolate_size_limit", isolate_size_limit); + Isolate::main_hook("isolate_fail", isolate_fail); + Isolate::main_hook("isolate_unix_socket", isolate_unix_socket); + Isolate::main_hook("isolate_multiple_binds", isolate_multiple_binds); + Isolate::main_hook("isolate_with_network", isolate_with_network); + Isolate::main_hook("isolate_no_network", isolate_no_network); + Isolate::main_hook("isolate_with_safetycontext", isolate_with_safetycontext); + Isolate::main_hook( + "isolate_bad_bindmount_absolute", + isolate_bad_bindmount_absolute, + ); + Isolate::main_hook( + "isolate_bad_bindmount_relative", + isolate_bad_bindmount_relative, + ); + + let argv0 = std::env::args().next().unwrap(); + if argv0.contains("isolate_test") { + // These tests actually launch the isolates, which then hit the hooks above after + // re-execing + test_isolate_hello(); + test_isolate_uid(); + test_check_mountinfo(); + test_unix_socket(); + test_multiple_binds(); + test_with_network(); + test_safetycontext(); + + // TODO: for some reason these tests where the isolate panics make strace think there are + // segfaults happening, and gets stuck continully outputting sigsegv messages even though + // the program runs fine without strace. for now just put them at the end, but we should + // investigate why it happens. + // valgrind doesn't catch anything. + test_no_network(); + test_tmpfs_size_limit(); + test_isolate_fail(); + test_isolate_bad_bindmount(); + } else { + panic!("isolate didn't hit its hook: {}", argv0); + } + } +} + +fn main() { + #[cfg(feature = "isolate")] + with_isolate::main() +} diff --git a/examples/network_server.rs b/extrasafe/examples/network_server.rs similarity index 95% rename from examples/network_server.rs rename to extrasafe/examples/network_server.rs index cba3b6d..8533f02 100644 --- a/examples/network_server.rs +++ b/extrasafe/examples/network_server.rs @@ -87,12 +87,14 @@ fn main() { // we can enable safetycontext, rather than just waiting 50ms. thread::sleep(std::time::Duration::from_millis(50)); SafetyContext::new() - .enable(Networking::nothing() - .allow_running_tcp_servers() - .allow_start_tcp_clients() - ).unwrap() - .enable(Threads::nothing() - .allow_create()).unwrap() + .enable( + Networking::nothing() + .allow_running_tcp_servers() + .allow_start_tcp_clients(), + ) + .unwrap() + .enable(Threads::nothing().allow_create()) + .unwrap() .apply_to_all_threads() .unwrap(); diff --git a/examples/no_files_allow_stdout.rs b/extrasafe/examples/no_files_allow_stdout.rs similarity index 97% rename from examples/no_files_allow_stdout.rs rename to extrasafe/examples/no_files_allow_stdout.rs index e85d4e9..d8ee6e7 100644 --- a/examples/no_files_allow_stdout.rs +++ b/extrasafe/examples/no_files_allow_stdout.rs @@ -11,7 +11,7 @@ fn main() { .enable( extrasafe::builtins::SystemIO::nothing() .allow_stdout() - .allow_stderr() + .allow_stderr(), ) .unwrap() .apply_to_all_threads(); diff --git a/examples/server_with_database.rs b/extrasafe/examples/server_with_database.rs similarity index 85% rename from examples/server_with_database.rs rename to extrasafe/examples/server_with_database.rs index 77d4c89..5f9fcba 100644 --- a/examples/server_with_database.rs +++ b/extrasafe/examples/server_with_database.rs @@ -61,18 +61,20 @@ fn run_server() { // spawn db server thread std::thread::Builder::new() .name("db".into()) - .spawn(move || run_db(&db_queue)).unwrap(); - + .spawn(move || run_db(&db_queue)) + .unwrap(); + // set up runtime let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() - .build().unwrap(); + .build() + .unwrap(); let listener = std::net::TcpListener::bind("127.0.0.1:5575").unwrap(); // extrasafe context SafetyContext::new() - .enable(Networking::nothing() - .allow_running_tcp_servers()).unwrap() + .enable(Networking::nothing().allow_running_tcp_servers()) + .unwrap() .apply_to_current_thread() .unwrap(); @@ -98,8 +100,7 @@ fn run_server() { let messages = recv.recv().unwrap(); messages.join("\n") - }) - ); + })); let svc = warp::service(routes); let make_svc = hyper::service::make_service_fn(move |_| { @@ -130,20 +131,24 @@ fn run_db(queue: &DbConn) { db.pragma_update(None, "locking_mode", "exclusive").unwrap(); db.pragma_update(None, "journal_mode", "wal").unwrap(); - db.execute("CREATE TABLE messages ( msg TEXT NOT NULL );", []).unwrap(); + db.execute("CREATE TABLE messages ( msg TEXT NOT NULL );", []) + .unwrap(); let mut get_rows = db.prepare("SELECT msg FROM messages;").unwrap(); let mut insert_row = db.prepare("INSERT INTO messages VALUES (?)").unwrap(); // after opening file, set extrasafe context SafetyContext::new() - .enable(SystemIO::nothing() - .allow_read() - .allow_write() - .allow_metadata() - .allow_ioctl() - .allow_close()).unwrap() - .enable(Threads::nothing() - .allow_sleep().yes_really()).unwrap() + .enable( + SystemIO::nothing() + .allow_read() + .allow_write() + .allow_metadata() + .allow_ioctl() + .allow_close(), + ) + .unwrap() + .enable(Threads::nothing().allow_sleep().yes_really()) + .unwrap() .apply_to_current_thread() .unwrap(); @@ -162,7 +167,8 @@ fn run_db(queue: &DbConn) { match msg { DBMsg::List(send) => { let messages: Vec = get_rows - .query_map([], |row| row.get(0)).unwrap() + .query_map([], |row| row.get(0)) + .unwrap() .map(Result::unwrap) .collect(); @@ -179,12 +185,13 @@ fn run_client_write(msg: &str) { // set up runtime let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() - .build().unwrap(); + .build() + .unwrap(); // Set up extrasafe context SafetyContext::new() - .enable(Networking::nothing() - .allow_start_tcp_clients()).unwrap() + .enable(Networking::nothing().allow_start_tcp_clients()) + .unwrap() .apply_to_current_thread() .unwrap(); println!("about to make request with msg {}", msg); @@ -216,40 +223,43 @@ fn run_client_read() { let runtime = tokio::runtime::Builder::new_current_thread() .worker_threads(1) .enable_all() - .build().unwrap(); + .build() + .unwrap(); let client = reqwest::Client::new(); // enable extrasafe context let ctx = SafetyContext::new() - .enable(Networking::nothing() - // Necessary for DNS - .allow_start_udp_servers().yes_really() - .allow_start_tcp_clients() - ).unwrap() + .enable( + Networking::nothing() + // Necessary for DNS + .allow_start_udp_servers() + .yes_really() + .allow_start_tcp_clients(), + ) + .unwrap() // For some reason only if we make two requests with a client does it use multiple threads, // so we only need them in the reader thread rather than the writer. - .enable(Threads::nothing() - .allow_create()).unwrap(); + .enable(Threads::nothing().allow_create()) + .unwrap(); #[cfg(feature = "landlock")] - let ctx = ctx.enable( - SystemIO::nothing() - .allow_dns_files() - .allow_ssl_files() - ).unwrap(); + let ctx = ctx + .enable(SystemIO::nothing().allow_dns_files().allow_ssl_files()) + .unwrap(); #[cfg(not(feature = "landlock"))] - let ctx = ctx.enable( + let ctx = ctx + .enable( SystemIO::nothing() .allow_open_readonly() .allow_read() .allow_metadata() .allow_close(), - ).unwrap(); - - ctx.apply_to_current_thread() + ) .unwrap(); + ctx.apply_to_current_thread().unwrap(); + // make request runtime.block_on(async { // Show that we can resolve dns and do ssl. Data returned isn't checked or used anywhere, @@ -262,7 +272,10 @@ fn run_client_read() { res.unwrap_err() ); let text = res.unwrap(); - println!("first 10 bytes of response from example.org {}", &text[..10]); + println!( + "first 10 bytes of response from example.org {}", + &text[..10] + ); let res = client.get("http://127.0.0.1:5575/read").send().await; assert!( @@ -281,7 +294,8 @@ fn main() { // -- Spawn server let _server_thread = std::thread::Builder::new() .name("server".into()) - .spawn(run_server).unwrap(); + .spawn(run_server) + .unwrap(); // give server time to start up std::thread::sleep(std::time::Duration::from_millis(100)); @@ -289,7 +303,8 @@ fn main() { // -- write "hello" to db let client1_thread = std::thread::Builder::new() .name("client1".into()) - .spawn(|| run_client_write("hello")).unwrap(); + .spawn(|| run_client_write("hello")) + .unwrap(); let res1 = client1_thread.join(); assert!(res1.is_ok(), "client1 failed: {:?}", res1.unwrap_err()); @@ -297,7 +312,8 @@ fn main() { // -- write "extrasafe" to db let client2_thread = std::thread::Builder::new() .name("client2".into()) - .spawn(|| run_client_write("extrasafe")).unwrap(); + .spawn(|| run_client_write("extrasafe")) + .unwrap(); let res2 = client2_thread.join(); assert!(res2.is_ok(), "client2 failed: {:?}", res2.unwrap_err()); @@ -305,7 +321,8 @@ fn main() { // -- read back, check messages are there in order let client3_thread = std::thread::Builder::new() .name("client3".into()) - .spawn(run_client_read).unwrap(); + .spawn(run_client_read) + .unwrap(); let res3 = client3_thread.join(); assert!(res3.is_ok(), "client3 failed: {:?}", res3.unwrap_err()); } diff --git a/examples/simple_network.rs b/extrasafe/examples/simple_network.rs similarity index 76% rename from examples/simple_network.rs rename to extrasafe/examples/simple_network.rs index 7d5d7ae..a6eedfa 100644 --- a/examples/simple_network.rs +++ b/extrasafe/examples/simple_network.rs @@ -5,52 +5,54 @@ //! the root certificate store is included in the binary. See the `Cargo.toml` configuration for //! musl. -use extrasafe::*; use extrasafe::builtins::{danger_zone::Threads, Networking, SystemIO}; +use extrasafe::*; fn main() { // do as much setup before enabling extrasafe so we can enable the least amount of syscalls let runtime = tokio::runtime::Builder::new_current_thread() .worker_threads(1) .enable_all() - .build().unwrap(); + .build() + .unwrap(); let client = reqwest::Client::new(); let ctx = SafetyContext::new() - .enable(Networking::nothing() - // Necessary for DNS - .allow_start_udp_servers().yes_really() - .allow_start_tcp_clients() - ).unwrap() + .enable( + Networking::nothing() + // Necessary for DNS + .allow_start_udp_servers() + .yes_really() + .allow_start_tcp_clients(), + ) + .unwrap() // hyper (via reqwest) seems to want to spawn a separate blocking thread to do DNS and it // doesn't seem like it can be preallocated easily. // TODO: investigate using runtime::Builder::thread_keep_alive and max_blocking_threads to // effectively preallocate by then just doing `block_on(|| ())` - .enable(Threads::nothing() - .allow_create() - ).unwrap(); + .enable(Threads::nothing().allow_create()) + .unwrap(); // allow access to dns and ssl files // note that allowing ssl file access isn't necessary if using rustls with webpki-certs // and allowing the dns files isn't strictly necessary either depending on various system // configurations #[cfg(feature = "landlock")] - let ctx = ctx.enable( - SystemIO::nothing() - .allow_dns_files() - .allow_ssl_files() - ).unwrap(); + let ctx = ctx + .enable(SystemIO::nothing().allow_dns_files().allow_ssl_files()) + .unwrap(); #[cfg(not(feature = "landlock"))] - let ctx = ctx.enable( + let ctx = ctx + .enable( SystemIO::nothing() .allow_open_readonly() .allow_read() .allow_metadata() .allow_close(), - ).unwrap(); - - ctx.apply_to_current_thread() + ) .unwrap(); + ctx.apply_to_current_thread().unwrap(); + // make an http request runtime.block_on(async { // Show that we can resolve dns and do ssl. Data returned isn't checked or used anywhere, @@ -63,7 +65,10 @@ fn main() { res.unwrap_err() ); let text = res.unwrap(); - println!("first 10 bytes of response from example.org {}", &text[..10]); + println!( + "first 10 bytes of response from example.org {}", + &text[..10] + ); }); } diff --git a/examples/time.rs b/extrasafe/examples/time.rs similarity index 52% rename from examples/time.rs rename to extrasafe/examples/time.rs index 7abf07b..256792d 100644 --- a/examples/time.rs +++ b/extrasafe/examples/time.rs @@ -1,20 +1,16 @@ -use extrasafe::{SafetyContext, builtins}; -use builtins::{Time, SystemIO}; +use builtins::{SystemIO, Time}; +use extrasafe::{builtins, SafetyContext}; fn main() { SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_stdout() - ).unwrap() - + .enable(SystemIO::nothing().allow_stdout()) + .unwrap() // On most systems this won't have an effect because glibc and musl both use vDSOs that // compute time directly via rdtsc rather than calling the syscalls directly. - .enable( - Time::nothing() - .allow_gettime() - ).unwrap() - .apply_to_current_thread().unwrap(); + .enable(Time::nothing().allow_gettime()) + .unwrap() + .apply_to_current_thread() + .unwrap(); let time = std::time::SystemTime::now(); println!("time gave us: {:#?}", time); diff --git a/examples/user-guide.rs b/extrasafe/examples/user-guide.rs similarity index 73% rename from examples/user-guide.rs rename to extrasafe/examples/user-guide.rs index 2937d88..0ff4f9e 100644 --- a/examples/user-guide.rs +++ b/extrasafe/examples/user-guide.rs @@ -1,20 +1,19 @@ fn simple_example() { - use extrasafe::builtins::{SystemIO, Networking}; + use extrasafe::builtins::{Networking, SystemIO}; let ctx = extrasafe::SafetyContext::new(); let ctx = ctx - .enable( - SystemIO::nothing() - .allow_open_readonly() - ).expect("Failed to add systemio ruleset to context") + .enable(SystemIO::nothing().allow_open_readonly()) + .expect("Failed to add systemio ruleset to context") // The Networking RuleSet includes both read and write, but our files will be opened // readonly so we can't actually write to them. We can still write to stdout and stderr // though. .enable( Networking::nothing() .allow_start_tcp_clients() - .allow_running_tcp_clients() - ).expect("Failed to add networking ruleset to context"); + .allow_running_tcp_clients(), + ) + .expect("Failed to add networking ruleset to context"); ctx.apply_to_current_thread() .expect("Failed to apply seccomp filters"); @@ -22,8 +21,8 @@ fn simple_example() { } fn custom_ruleset() { - use extrasafe::*; use extrasafe::syscalls::Sysno; + use extrasafe::*; use std::collections::HashMap; @@ -40,11 +39,8 @@ fn custom_ruleset() { const SOCK_STREAM: u64 = libc::SOCK_STREAM as u64; let rule = SeccompRule::new(Sysno::socket) - .and_condition( - seccomp_arg_filter!(arg0 & SOCK_STREAM == SOCK_STREAM)); - HashMap::from([ - (Sysno::socket, vec![rule,]) - ]) + .and_condition(seccomp_arg_filter!(arg0 & SOCK_STREAM == SOCK_STREAM)); + HashMap::from([(Sysno::socket, vec![rule])]) } fn name(&self) -> &'static str { @@ -53,8 +49,10 @@ fn custom_ruleset() { } extrasafe::SafetyContext::new() - .enable(MyRuleSet).unwrap() - .apply_to_current_thread().unwrap(); + .enable(MyRuleSet) + .unwrap() + .apply_to_current_thread() + .unwrap(); } #[cfg(feature = "landlock")] @@ -65,11 +63,13 @@ fn with_landlock() { extrasafe::SafetyContext::new() .enable( - extrasafe::builtins::SystemIO::nothing() - .allow_create_in_dir(&tmp_dir_allow) - .allow_write_file(&tmp_dir_allow) - ).unwrap() - .apply_to_current_thread().unwrap(); + extrasafe::builtins::SystemIO::nothing() + .allow_create_in_dir(&tmp_dir_allow) + .allow_write_file(&tmp_dir_allow), + ) + .unwrap() + .apply_to_current_thread() + .unwrap(); // Opening arbitrary files now fails! let res = File::create(tmp_dir_deny.join("evil.txt")); diff --git a/extrasafe/examples/user_guide_isolate.rs b/extrasafe/examples/user_guide_isolate.rs new file mode 100644 index 0000000..e90d32b --- /dev/null +++ b/extrasafe/examples/user_guide_isolate.rs @@ -0,0 +1,49 @@ +#[cfg(feature = "isolate")] +mod with_isolate { + use extrasafe::isolate::Isolate; + use std::collections::HashMap; + use std::path::PathBuf; + + const EXAMPLE_ISOLATE: &str = "user guide isolate"; + + /// The function that ultimately runs inside the Isolate + fn do_cool_thing() { + println!("I'm going to read some files from /cooldir and do cool stuff with it!"); + // TODO: do cool stuff with the files in /cooldir + } + + /// Isolate configuration that happens when the program is re-executed after `Isolate::run` + fn setup_isolate(name: &'static str) -> Isolate { + let path = std::env::var("COOL_DIRECTORY").unwrap(); + let path = PathBuf::from(path); + Isolate::new(name, do_cool_thing) + // This will mount /a/b/c from the parent into /cooldir in the child, + // but not until after entering the namespace. + .add_bind_mount(path, "/cooldir") + // Limit the amount of data that can be written to the filesystem the + // Isolate lives in. + .set_rootfs_size(1) + } + + pub(super) fn main() { + // Once the Isolate::run call is made, this will run the setup function, + // enter the namespace and call the the function provided. The first time the program runs, + // this code will be ignored. + Isolate::main_hook(EXAMPLE_ISOLATE, setup_isolate); + + // ... somewhere later in the program + + let env_vars = HashMap::from([("COOL_DIRECTORY".to_string(), "/".to_string())]); + + // `Isolate::run` returns a normal `std::process::Output` + let output = Isolate::run(EXAMPLE_ISOLATE, &env_vars).unwrap(); + + assert!(output.status.success()); + println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); + } +} + +fn main() { + #[cfg(feature = "isolate")] + with_isolate::main() +} diff --git a/src/builtins/basic.rs b/extrasafe/src/builtins/basic.rs similarity index 97% rename from src/builtins/basic.rs rename to extrasafe/src/builtins/basic.rs index 5d33789..6c6d0ca 100644 --- a/src/builtins/basic.rs +++ b/extrasafe/src/builtins/basic.rs @@ -24,7 +24,6 @@ impl RuleSet for BasicCapabilities { Sysno::mprotect, Sysno::munlock, Sysno::munlockall, - // Rust installs a signal handler to distinguish stack overflows from other faults // https://github.com/iximeow/rust/blob/master/src/libstd/sys/unix/stack_overflow.rs#L46 // (I learned this by getting a segfault when not allowing sigaction/etc and then @@ -34,35 +33,28 @@ impl RuleSet for BasicCapabilities { Sysno::rt_sigaction, Sysno::rt_sigprocmask, Sysno::rt_sigreturn, - // Futex management Sysno::futex, Sysno::get_robust_list, Sysno::set_robust_list, - // Readlink isn't dangerous because you still need to be able to open the file to do // anything with the resolved name. Sysno::readlink, - // Getpid/tid is fine. Sysno::getpid, Sysno::gettid, - // Get kernel info Sysno::uname, - // Could maybe put in a separate ruleset Sysno::getrandom, - // Thread affinity and yield seems okay to put here but I could be convinced to put it // in the Multiprocessing ruleset. they probably should be there. - Sysno::sched_getaffinity, Sysno::sched_setaffinity, + Sysno::sched_getaffinity, + Sysno::sched_setaffinity, Sysno::sched_yield, - // rseq is used in newer glibc for some initialization purposes. // It's kind of complicated but does not appear to be dangerous. Sysno::rseq, - // Exiting is probably fine. Sysno::exit, Sysno::exit_group, diff --git a/src/builtins/danger_zone.rs b/extrasafe/src/builtins/danger_zone.rs similarity index 94% rename from src/builtins/danger_zone.rs rename to extrasafe/src/builtins/danger_zone.rs index 8e18bca..44d53fe 100644 --- a/src/builtins/danger_zone.rs +++ b/extrasafe/src/builtins/danger_zone.rs @@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet}; use syscalls::Sysno; -use crate::{SeccompRule, RuleSet}; +use crate::{RuleSet, SeccompRule}; use super::YesReally; @@ -91,10 +91,14 @@ pub struct ForkAndExec; impl RuleSet for ForkAndExec { fn simple_rules(&self) -> Vec { let mut rules = vec![ - Sysno::fork, Sysno::vfork, - Sysno::execve, Sysno::execveat, - Sysno::wait4, Sysno::waitid, - Sysno::clone, Sysno::clone3, + Sysno::fork, + Sysno::vfork, + Sysno::execve, + Sysno::execveat, + Sysno::wait4, + Sysno::waitid, + Sysno::clone, + Sysno::clone3, ]; // musl creates a pipe when it starts a new process, and fails the operation if it can't diff --git a/src/builtins/mod.rs b/extrasafe/src/builtins/mod.rs similarity index 93% rename from src/builtins/mod.rs rename to extrasafe/src/builtins/mod.rs index 43b3c50..c24ecf7 100644 --- a/src/builtins/mod.rs +++ b/extrasafe/src/builtins/mod.rs @@ -16,9 +16,7 @@ impl YesReally { /// Make a [`YesReally`]. pub fn new(inner: T) -> YesReally { - YesReally { - inner, - } + YesReally { inner } } } diff --git a/src/builtins/network.rs b/extrasafe/src/builtins/network.rs similarity index 89% rename from src/builtins/network.rs rename to extrasafe/src/builtins/network.rs index c959c1d..5c6bb09 100644 --- a/src/builtins/network.rs +++ b/extrasafe/src/builtins/network.rs @@ -5,39 +5,58 @@ use std::collections::{HashMap, HashSet}; use syscalls::Sysno; use super::YesReally; -use crate::{SeccompRule, RuleSet}; +use crate::{RuleSet, SeccompRule}; // TODO: make bind calls conditional on the DGRAM/UNIX/STREAM flag in each function // TODO: add io_uring const NET_IO_SYSCALLS: &[Sysno] = &[ - Sysno::epoll_create, Sysno::epoll_create1, - Sysno::epoll_ctl, Sysno::epoll_wait, Sysno::epoll_pwait, Sysno::epoll_pwait2, - Sysno::select, Sysno::pselect6, - Sysno::poll, Sysno::ppoll, - - Sysno::accept, Sysno::accept4, - + Sysno::epoll_create, + Sysno::epoll_create1, + Sysno::epoll_ctl, + Sysno::epoll_wait, + Sysno::epoll_pwait, + Sysno::epoll_pwait2, + Sysno::select, + Sysno::pselect6, + Sysno::poll, + Sysno::ppoll, + Sysno::accept, + Sysno::accept4, // used in reqwest::blocking I guess to notify when blocking reads finish? - Sysno::eventfd, Sysno::eventfd2, - + Sysno::eventfd, + Sysno::eventfd2, // Used to set tcp_nodelay - Sysno::fcntl, Sysno::ioctl, + Sysno::fcntl, + Sysno::ioctl, Sysno::getsockopt, Sysno::setsockopt, - // Misc socket info Sysno::getpeername, Sysno::getsockname, ]; // listen is technically not a "read" syscall but you'd never listen and not read. -const NET_READ_SYSCALLS: &[Sysno] = &[Sysno::listen, - Sysno::recvfrom, Sysno::recvmsg, Sysno::recvmmsg, - Sysno::read, Sysno::readv, Sysno::preadv, Sysno::preadv2]; -const NET_WRITE_SYSCALLS: &[Sysno] = &[Sysno::sendto, Sysno::sendmsg, Sysno::sendmmsg, - Sysno::sendfile, - Sysno::write, Sysno::writev, Sysno::pwritev, Sysno::pwritev2]; +const NET_READ_SYSCALLS: &[Sysno] = &[ + Sysno::listen, + Sysno::recvfrom, + Sysno::recvmsg, + Sysno::recvmmsg, + Sysno::read, + Sysno::readv, + Sysno::preadv, + Sysno::preadv2, +]; +const NET_WRITE_SYSCALLS: &[Sysno] = &[ + Sysno::sendto, + Sysno::sendmsg, + Sysno::sendmmsg, + Sysno::sendfile, + Sysno::write, + Sysno::writev, + Sysno::pwritev, + Sysno::pwritev2, +]; // TODO: refactor Socket rule creation to reduce duplication in the allow_start_*_server functions @@ -105,14 +124,16 @@ impl Networking { let rule = SeccompRule::new(Sysno::socket) .and_condition(seccomp_arg_filter!(arg0 & AF_INET == AF_INET)) .and_condition(seccomp_arg_filter!(arg1 & SOCK_STREAM == SOCK_STREAM)); - self.custom.entry(Sysno::socket) + self.custom + .entry(Sysno::socket) .or_insert_with(Vec::new) .push(rule); // IPv6 let rule = SeccompRule::new(Sysno::socket) .and_condition(seccomp_arg_filter!(arg0 & AF_INET6 == AF_INET6)) .and_condition(seccomp_arg_filter!(arg1 & SOCK_STREAM == SOCK_STREAM)); - self.custom.entry(Sysno::socket) + self.custom + .entry(Sysno::socket) .or_insert_with(Vec::new) .push(rule); @@ -151,14 +172,16 @@ impl Networking { let rule = SeccompRule::new(Sysno::socket) .and_condition(seccomp_arg_filter!(arg0 & AF_INET == AF_INET)) .and_condition(seccomp_arg_filter!(arg1 & SOCK_DGRAM == SOCK_DGRAM)); - self.custom.entry(Sysno::socket) + self.custom + .entry(Sysno::socket) .or_insert_with(Vec::new) .push(rule); // IPv6 let rule = SeccompRule::new(Sysno::socket) .and_condition(seccomp_arg_filter!(arg0 & AF_INET6 == AF_INET6)) .and_condition(seccomp_arg_filter!(arg1 & SOCK_DGRAM == SOCK_DGRAM)); - self.custom.entry(Sysno::socket) + self.custom + .entry(Sysno::socket) .or_insert_with(Vec::new) .push(rule); @@ -195,17 +218,19 @@ impl Networking { let rule = SeccompRule::new(Sysno::socket) .and_condition(seccomp_arg_filter!(arg0 & AF_INET == AF_INET)) .and_condition(seccomp_arg_filter!(arg1 & SOCK_STREAM == SOCK_STREAM)); - self.custom.entry(Sysno::socket) + self.custom + .entry(Sysno::socket) .or_insert_with(Vec::new) .push(rule); // IPv6 let rule = SeccompRule::new(Sysno::socket) .and_condition(seccomp_arg_filter!(arg0 & AF_INET6 == AF_INET6)) .and_condition(seccomp_arg_filter!(arg1 & SOCK_STREAM == SOCK_STREAM)); - self.custom.entry(Sysno::socket) + self.custom + .entry(Sysno::socket) .or_insert_with(Vec::new) .push(rule); - + self.allowed.extend(&[Sysno::connect]); self.allowed.extend(NET_IO_SYSCALLS); self.allowed.extend(NET_READ_SYSCALLS); @@ -242,14 +267,16 @@ impl Networking { let rule = SeccompRule::new(Sysno::socket) .and_condition(seccomp_arg_filter!(arg0 & AF_UNIX == AF_UNIX)) .and_condition(seccomp_arg_filter!(arg1 & SOCK_STREAM == SOCK_STREAM)); - self.custom.entry(Sysno::socket) + self.custom + .entry(Sysno::socket) .or_insert_with(Vec::new) .push(rule); // DGRAM let rule = SeccompRule::new(Sysno::socket) .and_condition(seccomp_arg_filter!(arg0 & AF_UNIX == AF_UNIX)) .and_condition(seccomp_arg_filter!(arg1 & SOCK_DGRAM == SOCK_DGRAM)); - self.custom.entry(Sysno::socket) + self.custom + .entry(Sysno::socket) .or_insert_with(Vec::new) .push(rule); diff --git a/src/builtins/pipes.rs b/extrasafe/src/builtins/pipes.rs similarity index 100% rename from src/builtins/pipes.rs rename to extrasafe/src/builtins/pipes.rs index 6dc8d15..679bb59 100644 --- a/src/builtins/pipes.rs +++ b/extrasafe/src/builtins/pipes.rs @@ -1,7 +1,7 @@ //! Contains a [`RuleSet`] for allowing pipes -use syscalls::Sysno; use crate::RuleSet; +use syscalls::Sysno; /// [`Pipes`] allows you to create anonymous pipes for inter-process communication via the `pipe` /// syscalls. diff --git a/src/builtins/systemio.rs b/extrasafe/src/builtins/systemio.rs similarity index 84% rename from src/builtins/systemio.rs rename to extrasafe/src/builtins/systemio.rs index 9ff3dfe..79354aa 100644 --- a/src/builtins/systemio.rs +++ b/extrasafe/src/builtins/systemio.rs @@ -1,6 +1,6 @@ //! Contains a [`RuleSet`] for allowing IO-related syscalls, like file opening, reading, and writing. -use std::collections::{HashSet, HashMap}; +use std::collections::{HashMap, HashSet}; use std::fs::File; use std::os::unix::io::AsRawFd; @@ -9,24 +9,45 @@ use std::path::{Path, PathBuf}; use syscalls::Sysno; -#[cfg(feature = "landlock")] -use crate::LandlockRule; #[cfg(feature = "landlock")] use crate::landlock::{access, AccessFs, BitFlags}; +#[cfg(feature = "landlock")] +use crate::LandlockRule; -use crate::{RuleSet, SeccompRule}; use super::YesReally; +use crate::{RuleSet, SeccompRule}; -pub(crate) const IO_READ_SYSCALLS: &[Sysno] = &[Sysno::read, Sysno::readv, Sysno::preadv, Sysno::preadv2, Sysno::pread64, Sysno::lseek]; -pub(crate) const IO_WRITE_SYSCALLS: &[Sysno] = &[Sysno::write, Sysno::writev, Sysno::pwritev, Sysno::pwritev2, Sysno::pwrite64, - Sysno::fsync, Sysno::fdatasync, Sysno::lseek]; +pub(crate) const IO_READ_SYSCALLS: &[Sysno] = &[ + Sysno::read, + Sysno::readv, + Sysno::preadv, + Sysno::preadv2, + Sysno::pread64, + Sysno::lseek, +]; +pub(crate) const IO_WRITE_SYSCALLS: &[Sysno] = &[ + Sysno::write, + Sysno::writev, + Sysno::pwritev, + Sysno::pwritev2, + Sysno::pwrite64, + Sysno::fsync, + Sysno::fdatasync, + Sysno::lseek, +]; pub(crate) const IO_OPEN_SYSCALLS: &[Sysno] = &[Sysno::open, Sysno::openat, Sysno::openat2]; pub(crate) const IO_IOCTL_SYSCALLS: &[Sysno] = &[Sysno::ioctl, Sysno::fcntl]; // TODO: may want to separate fd-based and filename-based? -pub(crate) const IO_METADATA_SYSCALLS: &[Sysno] = &[Sysno::stat, Sysno::fstat, Sysno::newfstatat, - Sysno::lstat, Sysno::statx, - Sysno::getdents, Sysno::getdents64, - Sysno::getcwd]; +pub(crate) const IO_METADATA_SYSCALLS: &[Sysno] = &[ + Sysno::stat, + Sysno::fstat, + Sysno::newfstatat, + Sysno::lstat, + Sysno::statx, + Sysno::getdents, + Sysno::getdents64, + Sysno::getcwd, +]; pub(crate) const IO_CLOSE_SYSCALLS: &[Sysno] = &[Sysno::close, Sysno::close_range]; pub(crate) const IO_UNLINK_SYSCALLS: &[Sysno] = &[Sysno::unlink, Sysno::unlinkat]; @@ -56,7 +77,7 @@ impl SystemIO { allowed: HashSet::new(), custom: HashMap::new(), #[cfg(feature = "landlock")] - landlock_rules: HashMap::new() + landlock_rules: HashMap::new(), } } @@ -65,7 +86,8 @@ impl SystemIO { SystemIO::nothing() .allow_read() .allow_write() - .allow_open().yes_really() + .allow_open() + .yes_really() .allow_metadata() .allow_unlink() .allow_close() @@ -122,18 +144,20 @@ impl SystemIO { // WRONLY or RDWR is required for O_TMPFILE so we're fine to leave it out anyway. // const O_TMPFILE: u64 = libc::O_TMPFILE as u64; - const WRITECREATE: u64 = O_WRONLY | O_RDWR | O_APPEND | O_CREAT | O_EXCL;// | O_TMPFILE; + const WRITECREATE: u64 = O_WRONLY | O_RDWR | O_APPEND | O_CREAT | O_EXCL; // | O_TMPFILE; // flags are the second argument for open but the third for openat let rule = SeccompRule::new(Sysno::open) .and_condition(seccomp_arg_filter!(arg1 & WRITECREATE == 0)); - self.custom.entry(Sysno::open) + self.custom + .entry(Sysno::open) .or_insert_with(Vec::new) .push(rule); let rule = SeccompRule::new(Sysno::openat) .and_condition(seccomp_arg_filter!(arg2 & WRITECREATE == 0)); - self.custom.entry(Sysno::openat) + self.custom + .entry(Sysno::openat) .or_insert_with(Vec::new) .push(rule); @@ -163,9 +187,9 @@ impl SystemIO { /// Allow reading from stdin pub fn allow_stdin(mut self) -> SystemIO { - let rule = SeccompRule::new(Sysno::read) - .and_condition(seccomp_arg_filter!(arg0 == 0)); - self.custom.entry(Sysno::read) + let rule = SeccompRule::new(Sysno::read).and_condition(seccomp_arg_filter!(arg0 == 0)); + self.custom + .entry(Sysno::read) .or_insert_with(Vec::new) .push(rule); @@ -174,9 +198,9 @@ impl SystemIO { /// Allow writing to stdout pub fn allow_stdout(mut self) -> SystemIO { - let rule = SeccompRule::new(Sysno::write) - .and_condition(seccomp_arg_filter!(arg0 == 1)); - self.custom.entry(Sysno::write) + let rule = SeccompRule::new(Sysno::write).and_condition(seccomp_arg_filter!(arg0 == 1)); + self.custom + .entry(Sysno::write) .or_insert_with(Vec::new) .push(rule); @@ -185,9 +209,9 @@ impl SystemIO { /// Allow writing to stderr pub fn allow_stderr(mut self) -> SystemIO { - let rule = SeccompRule::new(Sysno::write) - .and_condition(seccomp_arg_filter!(arg0 == 2)); - self.custom.entry(Sysno::write) + let rule = SeccompRule::new(Sysno::write).and_condition(seccomp_arg_filter!(arg0 == 2)); + self.custom + .entry(Sysno::write) .or_insert_with(Vec::new) .push(rule); @@ -203,18 +227,21 @@ impl SystemIO { /// it's possible that the fd will be reused and therefore may be read from. #[allow(clippy::missing_panics_doc)] pub fn allow_file_read(mut self, file: &File) -> SystemIO { - let fd = file.as_raw_fd().try_into().expect("provided fd was negative"); + let fd = file + .as_raw_fd() + .try_into() + .expect("provided fd was negative"); for &syscall in IO_READ_SYSCALLS { - let rule = SeccompRule::new(syscall) - .and_condition(seccomp_arg_filter!(arg0 == fd)); - self.custom.entry(syscall) + let rule = SeccompRule::new(syscall).and_condition(seccomp_arg_filter!(arg0 == fd)); + self.custom + .entry(syscall) .or_insert_with(Vec::new) .push(rule); } for &syscall in IO_METADATA_SYSCALLS { - let rule = SeccompRule::new(syscall) - .and_condition(seccomp_arg_filter!(arg0 == fd)); - self.custom.entry(syscall) + let rule = SeccompRule::new(syscall).and_condition(seccomp_arg_filter!(arg0 == fd)); + self.custom + .entry(syscall) .or_insert_with(Vec::new) .push(rule); } @@ -231,10 +258,13 @@ impl SystemIO { /// it's possible that the fd will be reused and therefore may be written to. #[allow(clippy::missing_panics_doc)] pub fn allow_file_write(mut self, file: &File) -> SystemIO { - let fd = file.as_raw_fd().try_into().expect("provided fd was negative"); - let rule = SeccompRule::new(Sysno::write) - .and_condition(seccomp_arg_filter!(arg0 == fd)); - self.custom.entry(Sysno::write) + let fd = file + .as_raw_fd() + .try_into() + .expect("provided fd was negative"); + let rule = SeccompRule::new(Sysno::write).and_condition(seccomp_arg_filter!(arg0 == fd)); + self.custom + .entry(Sysno::write) .or_insert_with(Vec::new) .push(rule); @@ -267,7 +297,9 @@ impl RuleSet for SystemIO { impl SystemIO { fn insert_flags>(&mut self, path: P, new_flags: BitFlags) { let path = path.as_ref().to_path_buf(); - let _flag = self.landlock_rules.entry(path.clone()) + let _flag = self + .landlock_rules + .entry(path.clone()) .and_modify(|existing_flags| existing_flags.access_rules.insert(new_flags)) .or_insert_with(|| LandlockRule::new(&path, new_flags)); } @@ -286,7 +318,8 @@ impl SystemIO { self.allow_close() .allow_read() .allow_metadata() - .allow_open().yes_really() + .allow_open() + .yes_really() } /// Use Landlock to allow only the specified file to be written to. If this function is called @@ -302,7 +335,8 @@ impl SystemIO { self.allow_close() .allow_write() .allow_metadata() - .allow_open().yes_really() + .allow_open() + .yes_really() } /// Use Landlock to allow files to be created in the given directory. If this function is called @@ -331,7 +365,8 @@ impl SystemIO { self.allow_metadata() .allow_close() .allow_ioctl() - .allow_open().yes_really() + .allow_open() + .yes_really() } /// Use Landlock to allow creating directories. If this function is called multiple times, all @@ -400,20 +435,28 @@ impl SystemIO { self.allow_close() .allow_read() .allow_metadata() - .allow_open().yes_really() + .allow_open() + .yes_really() } /// Use Landlock to allow access to DNS files, like /etc/resolv.conf pub fn allow_dns_files(mut self) -> SystemIO { let new_flags = access::read_path(); // TODO: libnss exec perms? - for path in &["/etc/resolv.conf", "/etc/hosts", "/etc/host.conf", "/etc/nsswitch.conf", "/etc/gai.conf"] { + for path in &[ + "/etc/resolv.conf", + "/etc/hosts", + "/etc/host.conf", + "/etc/nsswitch.conf", + "/etc/gai.conf", + ] { self.insert_flags(path, new_flags); } // allow relevant syscalls as well self.allow_close() .allow_read() .allow_metadata() - .allow_open().yes_really() + .allow_open() + .yes_really() } } diff --git a/src/builtins/time.rs b/extrasafe/src/builtins/time.rs similarity index 75% rename from src/builtins/time.rs rename to extrasafe/src/builtins/time.rs index badf0bb..472c960 100644 --- a/src/builtins/time.rs +++ b/extrasafe/src/builtins/time.rs @@ -22,10 +22,10 @@ impl Time { } } -/// On most 64 bit systems glibc and musl both use the -/// [`vDSO`](https://man7.org/linux/man-pages/man7/vdso.7.html) to compute the time directly with -/// rdtsc rather than calling the `clock_gettime` syscall, so in most cases you don't need to -/// actually enable this. + /// On most 64 bit systems glibc and musl both use the + /// [`vDSO`](https://man7.org/linux/man-pages/man7/vdso.7.html) to compute the time directly with + /// rdtsc rather than calling the `clock_gettime` syscall, so in most cases you don't need to + /// actually enable this. pub fn allow_gettime(mut self) -> Time { self.allowed .extend([Sysno::clock_gettime, Sysno::clock_getres]); diff --git a/src/error.rs b/extrasafe/src/error.rs similarity index 100% rename from src/error.rs rename to extrasafe/src/error.rs index 2c65291..79e5944 100644 --- a/src/error.rs +++ b/extrasafe/src/error.rs @@ -7,10 +7,10 @@ use std::path::PathBuf; use seccompiler::Error as SeccompilerError; -#[cfg(feature = "landlock")] -use landlock::RulesetError as LandlockError; #[cfg(feature = "landlock")] use landlock::PathFdError; +#[cfg(feature = "landlock")] +use landlock::RulesetError as LandlockError; #[derive(Debug)] /// The error type produced by [`crate::SafetyContext`] diff --git a/src/isolate/isolate_sys.rs b/extrasafe/src/isolate/isolate_sys.rs similarity index 76% rename from src/isolate/isolate_sys.rs rename to extrasafe/src/isolate/isolate_sys.rs index 7190b26..d061e4f 100644 --- a/src/isolate/isolate_sys.rs +++ b/extrasafe/src/isolate/isolate_sys.rs @@ -12,12 +12,12 @@ //! - `run_isolate` unpacks the config data and uses it to set up a new tmpfs and bindmounts inside it, then does `pivot_root` into the tmpfs. //! - Finally, `run_isolate` calls `func` and then exits when it's done. #![allow(unsafe_code)] -use std::path::{Path, PathBuf}; -use std::fs::File; -use std::ffi::CString; use super::IsolateError; +use std::ffi::CString; +use std::fs::File; use std::io::Write; use std::os::fd::FromRawFd; +use std::path::{Path, PathBuf}; use std::collections::HashMap; @@ -34,7 +34,7 @@ macro_rules! fail_negative { let msg = format!("{}: {}", $message, err); panic!("{}", msg); } - } + }; } /// Panic if the first parameter passed is a null pointer. The provided message and the error @@ -46,15 +46,14 @@ macro_rules! fail_null { let msg = format!("{}: {}", $message, err); panic!("{}", msg); } - } + }; } /// Check rc for negative return code and create `std::io::Error` fn check_err(retcode: i32) -> std::io::Result<()> { if retcode >= 0 { std::io::Result::Ok(()) - } - else { + } else { std::io::Result::Err(std::io::Error::last_os_error()) } } @@ -81,7 +80,13 @@ pub struct IsolateConfigData { } impl IsolateConfigData { - pub fn new(isolate_name: &'static str, bindmounts: HashMap, func: fn() -> (), root_fs_size: u32, tempdir: PathBuf) -> IsolateConfigData { + pub fn new( + isolate_name: &'static str, + bindmounts: HashMap, + func: fn() -> (), + root_fs_size: u32, + tempdir: PathBuf, + ) -> IsolateConfigData { let parent_user = unsafe { libc::geteuid() }; let parent_group = unsafe { libc::getegid() }; IsolateConfigData { @@ -91,7 +96,7 @@ impl IsolateConfigData { root_fs_size, parent_user, parent_group, - tempdir + tempdir, } } } @@ -102,8 +107,7 @@ impl IsolateConfigData { fn map_user_to_root(parent_user: libc::uid_t, parent_group: libc::gid_t) { std::fs::write("/proc/self/uid_map", format!("0 {parent_user} 1\n")) .expect("failed to map child id"); - std::fs::write("/proc/self/setgroups", "deny\n") - .expect("failed to enable child group mapping"); + std::fs::write("/proc/self/setgroups", "deny\n").expect("failed to enable child group mapping"); std::fs::write("/proc/self/gid_map", format!("0 {parent_group} 1\n")) .expect("failed to map child gid"); } @@ -114,7 +118,7 @@ extern "C" fn run_isolate(data: *mut libc::c_void) -> i32 { // syscall. All heap pointers are still valid, they just point to a new copy of the data. let dataptr: *mut IsolateConfigData = data.cast::(); let config_data = unsafe { Box::from_raw(dataptr) }; - + let isolate_name_cstr = CString::new(config_data.isolate_name) .expect("please don't put null bytes in your isolate name"); let cstr_ptr = isolate_name_cstr.as_ptr(); @@ -137,7 +141,10 @@ extern "C" fn run_isolate(data: *mut libc::c_void) -> i32 { /// Make a tempdir in /tmp in which to mount our private tmpfs where the isolate will eventually /// live pub fn make_tempdir(isolate_name: &str) -> PathBuf { - assert!(isolate_name.is_ascii(), "tmpdir template name must be ascii"); + assert!( + isolate_name.is_ascii(), + "tmpdir template name must be ascii" + ); let template_str = format!("/tmp/{}.XXXXXX\0", isolate_name); let mut dir_buf: Vec = template_str.clone().into_bytes(); @@ -148,9 +155,8 @@ pub fn make_tempdir(isolate_name: &str) -> PathBuf { // remove null byte let _ = dir_buf.pop(); - let dir = String::from_utf8(dir_buf) - .expect("mkdtemp template string should always be utf8"); - + let dir = String::from_utf8(dir_buf).expect("mkdtemp template string should always be utf8"); + PathBuf::from(dir) } @@ -160,20 +166,28 @@ fn mount_tmpfs(tempdir: &Path, max_size: u32) { let tmp_cstr = CString::new("tmpfs").unwrap(); let options = CString::new(format!("size={}m", max_size)).unwrap(); let options_ptr: *const libc::c_void = options.as_ptr().cast::(); - let rc = unsafe { libc::mount(tmp_cstr.as_ptr(), - tmp_dircstr.as_ptr(), - tmp_cstr.as_ptr(), - 0, - options_ptr) }; + let rc = unsafe { + libc::mount( + tmp_cstr.as_ptr(), + tmp_dircstr.as_ptr(), + tmp_cstr.as_ptr(), + 0, + options_ptr, + ) + }; fail_negative!(rc, "failed to mount tmpfs after clone"); // make sure the mount is private let empty_cstr = CString::new("").unwrap(); - let rc = unsafe { libc::mount(empty_cstr.as_ptr(), - tmp_dircstr.as_ptr(), - empty_cstr.as_ptr(), - libc::MS_REC | libc::MS_PRIVATE, - std::ptr::null()) }; + let rc = unsafe { + libc::mount( + empty_cstr.as_ptr(), + tmp_dircstr.as_ptr(), + empty_cstr.as_ptr(), + libc::MS_REC | libc::MS_PRIVATE, + std::ptr::null(), + ) + }; fail_negative!(rc, "failed to make tmpfs private after mounting"); } @@ -186,37 +200,64 @@ fn do_bindmount(root: &Path, src: &Path, dst: &Path) { // in general but I'm not sure if you could actually do anything "bad" with it that you // couldn't do if an attacker otherwise controlled a dst path. for a in dst.ancestors() { - assert!(!a.ends_with("."), "bindmount dst directory must not contain . paths: {}", dst.display()); - assert!(!a.ends_with(".."), "bindmount dst directory must not contain .. paths: {}", dst.display()); + assert!( + !a.ends_with("."), + "bindmount dst directory must not contain . paths: {}", + dst.display() + ); + assert!( + !a.ends_with(".."), + "bindmount dst directory must not contain .. paths: {}", + dst.display() + ); } - let dst = if dst.is_absolute() { dst.strip_prefix("/").unwrap() } else { dst }; + let dst = if dst.is_absolute() { + dst.strip_prefix("/").unwrap() + } else { + dst + }; let dst = root.join(dst); // if directory, create all directories // else if file, socket, etc., create all parent directories and make empty file to bindmount // on to. if src.is_dir() { - std::fs::create_dir_all(&dst) - .unwrap_or_else(|_| panic!("failed to create dst directory (or parent directories) when bindmounting {}", dst.display())); - } - else { + std::fs::create_dir_all(&dst).unwrap_or_else(|_| { + panic!( + "failed to create dst directory (or parent directories) when bindmounting {}", + dst.display() + ) + }); + } else { if let Some(parent) = dst.parent() { - std::fs::create_dir_all(parent) - .unwrap_or_else(|_| panic!("failed to create parent directories when bindmounting {}", dst.display())); + std::fs::create_dir_all(parent).unwrap_or_else(|_| { + panic!( + "failed to create parent directories when bindmounting {}", + dst.display() + ) + }); } - drop(File::create(&dst) - .unwrap_or_else(|_| panic!("failed to create empty file when bindmounting {}", dst.display()))); + drop(File::create(&dst).unwrap_or_else(|_| { + panic!( + "failed to create empty file when bindmounting {}", + dst.display() + ) + })); } let src_dircstr = CString::new(src.as_os_str().as_encoded_bytes()).unwrap(); let dst_dircstr = CString::new(dst.as_os_str().as_encoded_bytes()).unwrap(); let bind_cstr = CString::new("bind").unwrap(); - let rc = unsafe { libc::mount(src_dircstr.as_ptr(), - dst_dircstr.as_ptr(), - bind_cstr.as_ptr(), - libc::MS_REC | libc::MS_BIND, - std::ptr::null()) }; + let rc = unsafe { + libc::mount( + src_dircstr.as_ptr(), + dst_dircstr.as_ptr(), + bind_cstr.as_ptr(), + libc::MS_REC | libc::MS_BIND, + std::ptr::null(), + ) + }; fail_negative!(rc, format!("failed to bindmount. do you have permissions for the src directory? dst must also exist! (it should be an empty file or directory)\nsrc: {:?}, dst: {:?}", src, dst)); } @@ -231,7 +272,10 @@ fn do_pivot_root(tmpfs: &Path) { let curdir_cstr = CString::new(".").unwrap(); let curdir_ptr = curdir_cstr.as_ptr(); let rc = unsafe { libc::syscall(libc::SYS_pivot_root, curdir_ptr, curdir_ptr) }; - fail_negative!(rc, format!("failed to pivot_root . . into {}", tmpfs.display())); + fail_negative!( + rc, + format!("failed to pivot_root . . into {}", tmpfs.display()) + ); // now unmount old / with MNT_DETACH let rc = unsafe { libc::umount2(curdir_ptr, libc::MNT_DETACH) }; @@ -242,30 +286,38 @@ pub fn create_memfd_from_self_exe() -> Result { // Per the memfd_open manpage, multiple memfds can have the same name without issue. let memfd_name = CString::new("isolate_memfd").unwrap(); - let exe_data = std::fs::read("/proc/self/exe") - .map_err(IsolateError::MemFd)?; + let exe_data = std::fs::read("/proc/self/exe").map_err(IsolateError::MemFd)?; let exe_bytes = &exe_data; let fsize = exe_bytes.len() as u64; let memfd = unsafe { libc::memfd_create(memfd_name.as_ptr(), 0) }; - check_err(memfd) - .map_err(IsolateError::MemFd)?; + check_err(memfd).map_err(IsolateError::MemFd)?; let mut memfd_file = unsafe { std::fs::File::from_raw_fd(memfd) }; memfd_file.set_len(fsize).expect("ftruncate on memfd"); - let _count = memfd_file.write(exe_bytes).expect("write exe data to memfd after sizing"); + let _count = memfd_file + .write(exe_bytes) + .expect("write exe data to memfd after sizing"); Ok(memfd_file) } -pub fn clone_into_namespace(stack: &mut [u8], - config_data: IsolateConfigData, - new_network: bool) -> - (libc::pid_t, libc::id_t) { - let flags = libc::CLONE_NEWNS | libc::CLONE_NEWUSER | libc::CLONE_NEWPID | libc::CLONE_NEWIPC | libc::CLONE_NEWUTS | libc::CLONE_PIDFD; +pub fn clone_into_namespace( + stack: &mut [u8], + config_data: IsolateConfigData, + new_network: bool, +) -> (libc::pid_t, libc::id_t) { + let flags = libc::CLONE_NEWNS + | libc::CLONE_NEWUSER + | libc::CLONE_NEWPID + | libc::CLONE_NEWIPC + | libc::CLONE_NEWUTS + | libc::CLONE_PIDFD; let flags = if new_network { flags | libc::CLONE_NEWNET - } else { flags }; + } else { + flags + }; - let mut pidfd: libc::pid_t = 0; + let mut pidfd: libc::pid_t = 0; // the argument used for pidfd is defined as an i32/pid_t but waitid takes a u32/id_t so we // convert on return let pidfd_ref: *mut libc::pid_t = &mut pidfd; @@ -273,19 +325,29 @@ pub fn clone_into_namespace(stack: &mut [u8], //let stack_ptr = unsafe { std::mem::transmute::<*mut u8, *mut libc::c_void>(stack.as_mut_ptr().wrapping_add(CHILD_STACK_SIZE)) }; // The stack grows down, so we need to provide clone a pointer to the end of our stack data // vec. - let stack_ptr: *mut libc::c_void = stack.as_mut_ptr().wrapping_add(CHILD_STACK_SIZE).cast::(); + let stack_ptr: *mut libc::c_void = stack + .as_mut_ptr() + .wrapping_add(CHILD_STACK_SIZE) + .cast::(); let fnptr = run_isolate; let data = Box::new(config_data); let data_ptr: *mut libc::c_void = Box::into_raw(data).cast::(); - + let pid = unsafe { libc::clone(fnptr, stack_ptr, flags, data_ptr, pidfd_ref) }; (pid, pidfd.try_into().unwrap()) } pub fn wait_for_child(pidfd: libc::id_t) -> i32 { let mut child_status: libc::siginfo_t = unsafe { std::mem::zeroed() }; - let rc = unsafe {libc::waitid(libc::P_PIDFD, pidfd, &mut child_status, libc::__WALL | libc::WEXITED) }; + let rc = unsafe { + libc::waitid( + libc::P_PIDFD, + pidfd, + &mut child_status, + libc::__WALL | libc::WEXITED, + ) + }; fail_negative!(rc, "waitid failed"); unsafe { child_status.si_status() } } diff --git a/src/isolate/mod.rs b/extrasafe/src/isolate/mod.rs similarity index 92% rename from src/isolate/mod.rs rename to extrasafe/src/isolate/mod.rs index 2b81c8b..3247c09 100644 --- a/src/isolate/mod.rs +++ b/extrasafe/src/isolate/mod.rs @@ -1,6 +1,6 @@ //! Extrasafe's `Isolate` allows you to run a subprocess in a [user //! namespace](https://man7.org/linux/man-pages/man7/user_namespaces.7.html), which allows you to -//! isolate your program in order to e.g. run +//! isolate your program in order to e.g. run //! //! Specifically, you can isolate: //! - The filesystem. The Isolate creates a temporary directory and mounts a tmpfs onto it, and @@ -20,10 +20,10 @@ // - bind mount directory list // - tmpfs size? -use std::path::{Path, PathBuf}; use std::collections::HashMap; -use std::os::unix::process::CommandExt; use std::os::fd::AsRawFd; +use std::os::unix::process::CommandExt; +use std::path::{Path, PathBuf}; use std::process::Output; mod isolate_sys; @@ -38,7 +38,6 @@ pub enum IsolateError { Command(std::io::Error), /// An error that occurred while configuring a bindmount (in the subprocess but before clone) BindmountConfig(std::io::Error), - } /// Allows creation of subprocesses which then use Linux user namespaces to isolate the program @@ -84,7 +83,7 @@ impl Isolate { pub fn add_bind_mount(mut self, src: impl AsRef, dst: impl AsRef) -> Isolate { let src = src.as_ref(); let dst = dst.as_ref(); - + let _old = self.bindmounts.insert(src.into(), dst.into()); self } @@ -109,7 +108,10 @@ impl Isolate { /// /// # Errors /// Will return an error if starting the Command fails. - pub fn run(isolate_name: &'static str, envs: &HashMap) -> Result { + pub fn run( + isolate_name: &'static str, + envs: &HashMap, + ) -> Result { let memfd = system::create_memfd_from_self_exe()?; std::process::Command::new(format!("/proc/self/fd/{}", memfd.as_raw_fd())) .arg0(isolate_name) @@ -130,8 +132,7 @@ impl Isolate { struct TempDirCleanup(pub PathBuf); impl Drop for TempDirCleanup { fn drop(&mut self) { - std::fs::remove_dir(&self.0) - .expect("tmpfs dir was not empty"); + std::fs::remove_dir(&self.0).expect("tmpfs dir was not empty"); } } @@ -146,7 +147,8 @@ impl Isolate { let mut child_stack = Vec::with_capacity(system::CHILD_STACK_SIZE); - let (_child_pid, child_pidfd) = isolate.isolate_and_run(&mut child_stack, tempdir.clone()); + let (_child_pid, child_pidfd) = + isolate.isolate_and_run(&mut child_stack, tempdir.clone()); let child_ret = system::wait_for_child(child_pidfd); drop(tempdir_clean); @@ -158,14 +160,19 @@ impl Isolate { /// `clone`s into a new namespace, creates a tmpfs at `tempdir`, bindmounts the relevant /// directories into it, `pivot_root`s into the tmpfs, and runs `self.func` - fn isolate_and_run(&self, child_stack: &mut [u8], tempdir: PathBuf) -> (libc::pid_t, libc::id_t) { + fn isolate_and_run( + &self, + child_stack: &mut [u8], + tempdir: PathBuf, + ) -> (libc::pid_t, libc::id_t) { let new_network = self.new_network; let data = system::IsolateConfigData::new( self.isolate_name, self.bindmounts.clone(), self.func, self.root_fs_size, - tempdir); + tempdir, + ); system::clone_into_namespace(child_stack, data, new_network) } } diff --git a/src/landlock.rs b/extrasafe/src/landlock.rs similarity index 91% rename from src/landlock.rs rename to extrasafe/src/landlock.rs index 3ce20a8..aa33e6f 100644 --- a/src/landlock.rs +++ b/extrasafe/src/landlock.rs @@ -5,7 +5,10 @@ use std::path::{Path, PathBuf}; pub use landlock::RulesetError as LandlockError; -pub use landlock::{ABI, Access, AccessFs, BitFlags, Compatible, CompatLevel, PathBeneath, PathFd, Ruleset, RulesetAttr, RulesetCreatedAttr}; +pub use landlock::{ + Access, AccessFs, BitFlags, CompatLevel, Compatible, PathBeneath, PathFd, Ruleset, RulesetAttr, + RulesetCreatedAttr, ABI, +}; /// A Landlock rule. It consists of a path and a collection of access rights which determine what /// actions can be performed on that path. @@ -21,10 +24,7 @@ impl LandlockRule { /// Create a new Landlock Rule. pub fn new>(path: P, access_rules: BitFlags) -> LandlockRule { let path = path.as_ref().into(); - LandlockRule { - path, - access_rules - } + LandlockRule { path, access_rules } } } diff --git a/src/lib.rs b/extrasafe/src/lib.rs similarity index 92% rename from src/lib.rs rename to extrasafe/src/lib.rs index e57e092..0ca91b8 100644 --- a/src/lib.rs +++ b/extrasafe/src/lib.rs @@ -13,18 +13,17 @@ //! See the [`SafetyContext`] struct's documentation and the tests/ and examples/ directories for //! more information on how to use it. - // Filter is the entire, top-level seccomp filter chain. All SeccompilerRules are or-ed together. // Vec<(i64, Vec)>, Vec is empty if Rule has no filters. // Rule is a syscall + multiple argument filters. All argument filters are and-ed together in a // single Rule. // ArgumentFilter is a single condition on a single argument // Comparator is used in an ArgumentFilter to choose the comparison operation -pub use seccompiler::SeccompFilter as SeccompilerFilter; -pub use seccompiler::SeccompRule as SeccompilerRule; -pub use seccompiler::SeccompCondition as SeccompilerArgumentFilter; pub use seccompiler::Error as SeccompilerError; pub use seccompiler::SeccompCmpOp as SeccompilerComparator; +pub use seccompiler::SeccompCondition as SeccompilerArgumentFilter; +pub use seccompiler::SeccompFilter as SeccompilerFilter; +pub use seccompiler::SeccompRule as SeccompilerRule; use seccompiler::SeccompAction; @@ -46,9 +45,9 @@ pub use landlock::*; #[cfg(feature = "isolate")] pub mod isolate; +use std::collections::{BTreeMap, HashMap}; #[cfg(feature = "landlock")] use std::path::PathBuf; -use std::collections::{BTreeMap, HashMap}; #[derive(Debug, Clone, PartialEq)] /// A restriction on the arguments of a syscall. May be combined with other @@ -89,7 +88,11 @@ impl SeccompArgumentFilter { #[must_use] /// Create a new [`SeccompArgumentFilter`]. You should probably use the [`seccomp_arg_filter!`] /// instead. - pub fn new(arg_idx: u8, comparator: SeccompilerComparator, value: u64) -> SeccompArgumentFilter { + pub fn new( + arg_idx: u8, + comparator: SeccompilerComparator, + value: u64, + ) -> SeccompArgumentFilter { // TODO: add quirks mode file and check whether syscall's parameter at index `arg_idx` is // 32 or 64 bit (and also I guess if it even has that many arguments) SeccompArgumentFilter::new64(arg_idx, comparator, value) @@ -98,7 +101,11 @@ impl SeccompArgumentFilter { #[must_use] /// Create a new [`SeccompArgumentFilter`] that checks all 64 bits of the provided argument. /// You should probably use the [`seccomp_arg_filter!`] instead. - pub fn new64(arg_idx: u8, comparator: SeccompilerComparator, value: u64) -> SeccompArgumentFilter { + pub fn new64( + arg_idx: u8, + comparator: SeccompilerComparator, + value: u64, + ) -> SeccompArgumentFilter { SeccompArgumentFilter { arg_idx, comparator, @@ -111,7 +118,11 @@ impl SeccompArgumentFilter { /// Create a new [`SeccompArgumentFilter`] that checks 32 bits of the provided argument. /// You should probably use the [`seccomp_arg_filter!`] instead. See the struct's documentation /// for why this is needed. - pub fn new32(arg_idx: u8, comparator: SeccompilerComparator, value: u32) -> SeccompArgumentFilter { + pub fn new32( + arg_idx: u8, + comparator: SeccompilerComparator, + value: u32, + ) -> SeccompArgumentFilter { // Note that it doesn't matter if we convert with or without sign extension here since the // point is that we'll only compare the least significant 32 bits anyway. let value = u64::from(value); @@ -125,9 +136,17 @@ impl SeccompArgumentFilter { pub(crate) fn into_seccompiler(self) -> Result { use seccompiler::SeccompCmpArgLen; - let arg_len = if self.is_64bit { SeccompCmpArgLen::Qword } else { SeccompCmpArgLen::Dword }; - Ok(SeccompilerArgumentFilter::new(self.arg_idx, arg_len, - self.comparator, self.value)?) + let arg_len = if self.is_64bit { + SeccompCmpArgLen::Qword + } else { + SeccompCmpArgLen::Dword + }; + Ok(SeccompilerArgumentFilter::new( + self.arg_idx, + arg_len, + self.comparator, + self.value, + )?) } } @@ -167,8 +186,11 @@ impl SeccompRule { return Ok(None); } - let argument_filters: Vec = self.argument_filters.into_iter() - .map(SeccompArgumentFilter::into_seccompiler).collect::>()?; + let argument_filters: Vec = self + .argument_filters + .into_iter() + .map(SeccompArgumentFilter::into_seccompiler) + .collect::>()?; Ok(Some(SeccompilerRule::new(argument_filters)?)) } @@ -293,13 +315,10 @@ impl SafetyContext { let mut rules = rules.conditional_rules(); for syscall in base_syscalls { let rule = SeccompRule::new(syscall); - rules.entry(syscall) - .or_insert_with(Vec::new) - .push(rule); + rules.entry(syscall).or_insert_with(Vec::new).push(rule); } - rules.into_values().flatten() - .collect() + rules.into_values().flatten().collect() } /// Enable the simple and conditional rules provided by the [`RuleSet`]. @@ -319,12 +338,18 @@ impl SafetyContext { #[cfg(feature = "landlock")] fn enable_landlock_rules(&mut self, policy: &R) -> Result<(), ExtraSafeError> { let name = policy.name(); - let rules = policy.landlock_rules().into_iter() + let rules = policy + .landlock_rules() + .into_iter() .map(|rule| (rule.path.clone(), LabeledLandlockRule(name, rule))); for (path, labeled_rule) in rules { if let Some(existing_rule) = self.landlock_rules.get(&path) { - return Err(ExtraSafeError::DuplicatePath(path.clone(), existing_rule.0, labeled_rule.0)); + return Err(ExtraSafeError::DuplicatePath( + path.clone(), + existing_rule.0, + labeled_rule.0, + )); } // value here is always none because we checked above that we're not inserting a path // that already exists @@ -358,8 +383,7 @@ impl SafetyContext { labeled_existing_rule.0, labeled_new_rule.0, )); - } - else if !new_is_simple && existing_is_simple { + } else if !new_is_simple && existing_is_simple { return Err(ExtraSafeError::ConditionalNoEffectError( new_rule.syscall, labeled_new_rule.0, @@ -527,7 +551,10 @@ impl SafetyContext { // should be allowed without restriction } let result = rules_map.insert(syscall, seccompiler_rules); - assert!(result.is_none(), "extrasafe logic error: somehow inserted the same syscall's rules twice"); + assert!( + result.is_none(), + "extrasafe logic error: somehow inserted the same syscall's rules twice" + ); } #[cfg(not(all(target_arch = "x86_64", target_os = "linux")))] @@ -537,15 +564,16 @@ impl SafetyContext { rules_map, SeccompAction::Errno(self.errno), SeccompAction::Allow, - std::env::consts::ARCH.try_into().expect("invalid arches are prevented above"), + std::env::consts::ARCH + .try_into() + .expect("invalid arches are prevented above"), )?; let bpf_filter: seccompiler::BpfProgram = seccompiler_filter.try_into()?; if self.all_threads { seccompiler::apply_filter_all_threads(&bpf_filter)?; - } - else { + } else { seccompiler::apply_filter(&bpf_filter)?; } @@ -554,11 +582,11 @@ impl SafetyContext { #[cfg(feature = "landlock")] fn apply_landlock_rules(&self) -> Result<(), ExtraSafeError> { - let abi = ABI::V2; - let mut landlock_ruleset = Ruleset::default() + let abi = ABI::V2; + let mut landlock_ruleset = Ruleset::default() .set_compatibility(CompatLevel::HardRequirement) - .handle_access(AccessFs::from_all(abi))? - .create()?; + .handle_access(AccessFs::from_all(abi))? + .create()?; for LabeledLandlockRule(_policy_name, rule) in self.landlock_rules.values() { // If path does not exist or is not accessible, just ignore it @@ -567,7 +595,7 @@ impl SafetyContext { landlock_ruleset = landlock_ruleset.add_rule(path_beneath)?; } } - let _status = landlock_ruleset.restrict_self(); + let _status = landlock_ruleset.restrict_self(); Ok(()) } } diff --git a/src/macros.rs b/extrasafe/src/macros.rs similarity index 65% rename from src/macros.rs rename to extrasafe/src/macros.rs index 242c645..b9d961f 100644 --- a/src/macros.rs +++ b/extrasafe/src/macros.rs @@ -19,40 +19,86 @@ #[macro_export] macro_rules! seccomp_arg_filter { ($argno:ident <= $value:expr) => { - $crate::SeccompArgumentFilter::new(match_argno!($argno), $crate::SeccompilerComparator::Le, $value) + $crate::SeccompArgumentFilter::new( + match_argno!($argno), + $crate::SeccompilerComparator::Le, + $value, + ) }; ($argno:ident < $value:expr) => { - $crate::SeccompArgumentFilter::new(match_argno!($argno), $crate::SeccompilerComparator::Lt, $value) + $crate::SeccompArgumentFilter::new( + match_argno!($argno), + $crate::SeccompilerComparator::Lt, + $value, + ) }; ($argno:ident >= $value:expr) => { - $crate::SeccompArgumentFilter::new(match_argno!($argno), $crate::SeccompilerComparator::Ge, $value) + $crate::SeccompArgumentFilter::new( + match_argno!($argno), + $crate::SeccompilerComparator::Ge, + $value, + ) }; ($argno:ident > $value:expr) => { - $crate::SeccompArgumentFilter::new(match_argno!($argno), $crate::SeccompilerComparator::Gt, $value) + $crate::SeccompArgumentFilter::new( + match_argno!($argno), + $crate::SeccompilerComparator::Gt, + $value, + ) }; ($argno:ident == $value:expr) => { - $crate::SeccompArgumentFilter::new(match_argno!($argno), $crate::SeccompilerComparator::Eq, $value) + $crate::SeccompArgumentFilter::new( + match_argno!($argno), + $crate::SeccompilerComparator::Eq, + $value, + ) }; ($argno:ident != $value:expr) => { - $crate::SeccompArgumentFilter::new(match_argno!($argno), $crate::SeccompilerComparator::Ne, $value) + $crate::SeccompArgumentFilter::new( + match_argno!($argno), + $crate::SeccompilerComparator::Ne, + $value, + ) }; ($argno:ident & $mask:tt == $value:expr) => { - $crate::SeccompArgumentFilter::new(match_argno!($argno), $crate::SeccompilerComparator::MaskedEq($mask), $value) + $crate::SeccompArgumentFilter::new( + match_argno!($argno), + $crate::SeccompilerComparator::MaskedEq($mask), + $value, + ) + }; + ($_other:expr) => { + compile_error!( + "usage: `arg[0-5] {<=, <, >=, >, ==, !=} ` or `arg[0-5] & == `" + ) }; - ($_other:expr) => {compile_error!("usage: `arg[0-5] {<=, <, >=, >, ==, !=} ` or `arg[0-5] & == `")}; } #[doc(hidden)] #[macro_export] /// Internal macro for `seccomp_arg_filter!` macro_rules! match_argno { - (arg0) => {0}; - (arg1) => {1}; - (arg2) => {2}; - (arg3) => {3}; - (arg4) => {4}; - (arg5) => {5}; - ($_other:expr) => {compile_error!("Seccomp argument filters must start with argX where X is 0-5")}; + (arg0) => { + 0 + }; + (arg1) => { + 1 + }; + (arg2) => { + 2 + }; + (arg3) => { + 3 + }; + (arg4) => { + 4 + }; + (arg5) => { + 5 + }; + ($_other:expr) => { + compile_error!("Seccomp argument filters must start with argX where X is 0-5") + }; } /// These tests just test that the macro expands correctly, not that the comparators do what they diff --git a/tests/arg_comparisons.rs b/extrasafe/tests/arg_comparisons.rs similarity index 84% rename from tests/arg_comparisons.rs rename to extrasafe/tests/arg_comparisons.rs index f233c22..30db494 100644 --- a/tests/arg_comparisons.rs +++ b/extrasafe/tests/arg_comparisons.rs @@ -1,8 +1,8 @@ #![allow(unsafe_code)] // allow unsafe to call syscalls directly -use extrasafe::*; use builtins::SystemIO; +use extrasafe::*; use syscalls::Sysno; use std::collections::HashMap; @@ -16,8 +16,7 @@ impl RuleSet for IoctlRestricted64 { fn conditional_rules(&self) -> HashMap> { let cmp = SeccompArgumentFilter::new64(1, SeccompilerComparator::Eq, self.0); - let rule = SeccompRule::new(Sysno::ioctl) - .and_condition(cmp); + let rule = SeccompRule::new(Sysno::ioctl).and_condition(cmp); HashMap::from([(Sysno::ioctl, vec![rule])]) } @@ -36,8 +35,7 @@ impl RuleSet for IoctlRestricted32 { fn conditional_rules(&self) -> HashMap> { let cmp = SeccompArgumentFilter::new32(1, SeccompilerComparator::Eq, self.0); - let rule = SeccompRule::new(Sysno::ioctl) - .and_condition(cmp); + let rule = SeccompRule::new(Sysno::ioctl).and_condition(cmp); HashMap::from([(Sysno::ioctl, vec![rule])]) } @@ -47,7 +45,6 @@ impl RuleSet for IoctlRestricted32 { } } - struct GetUidRestricted; impl RuleSet for GetUidRestricted { fn simple_rules(&self) -> Vec { @@ -77,11 +74,12 @@ fn cmp_arg_syscall_unused_parameter() { assert!(uid1 > 0); extrasafe::SafetyContext::new() - .enable(SystemIO::nothing() - .allow_stdout() - .allow_stderr()).unwrap() - .enable(GetUidRestricted).unwrap() - .apply_to_current_thread().unwrap(); + .enable(SystemIO::nothing().allow_stdout().allow_stderr()) + .unwrap() + .enable(GetUidRestricted) + .unwrap() + .apply_to_current_thread() + .unwrap(); // SAFETY: getuid just gives the current user's uid let uid2 = unsafe { libc::getuid() }; @@ -92,7 +90,7 @@ macro_rules! assert_errno { ($e: expr) => { let errno = std::io::Error::last_os_error().raw_os_error().unwrap(); assert_eq!(errno, $e); - } + }; } // See https://github.com/rust-vmm/seccompiler/issues/59 for more details on below two tests @@ -102,11 +100,12 @@ fn cmp_arg_64bit_ioctl_musl_glibc_diff() { let seccomp_errno = 999; extrasafe::SafetyContext::new() .with_errno(seccomp_errno) - .enable(SystemIO::nothing() - .allow_stdout() - .allow_stderr()).unwrap() - .enable(IoctlRestricted64(value)).unwrap() - .apply_to_current_thread().unwrap(); + .enable(SystemIO::nothing().allow_stdout().allow_stderr()) + .unwrap() + .enable(IoctlRestricted64(value)) + .unwrap() + .apply_to_current_thread() + .unwrap(); // On glibc, the second parameter is a u64, so the value seen by the kernel matches the value // in our seccomp filter, and the ioctl call is allowed. It then sets errno to -9 since 4321 is @@ -134,11 +133,12 @@ fn cmp_arg_32bit_ioctl_musl_glibc_same() { let seccomp_errno = 999; extrasafe::SafetyContext::new() .with_errno(seccomp_errno) - .enable(SystemIO::nothing() - .allow_stdout() - .allow_stderr()).unwrap() - .enable(IoctlRestricted32(value)).unwrap() - .apply_to_current_thread().unwrap(); + .enable(SystemIO::nothing().allow_stdout().allow_stderr()) + .unwrap() + .enable(IoctlRestricted32(value)) + .unwrap() + .apply_to_current_thread() + .unwrap(); // here both tests are the same except for value being i32 on musl #[cfg(target_env = "gnu")] diff --git a/tests/bad_combination.rs b/extrasafe/tests/bad_combination.rs similarity index 72% rename from tests/bad_combination.rs rename to extrasafe/tests/bad_combination.rs index b44071b..964f8c8 100644 --- a/tests/bad_combination.rs +++ b/extrasafe/tests/bad_combination.rs @@ -24,8 +24,8 @@ impl RuleSet for JustWrite { /// (This is because the simple rule would override the conditional one) fn invalid_combination_new_simple() { let res = extrasafe::SafetyContext::new() - .enable(SystemIO::nothing() - .allow_stdout()).unwrap() + .enable(SystemIO::nothing().allow_stdout()) + .unwrap() .enable(SystemIO::everything()); assert!( @@ -40,10 +40,13 @@ fn invalid_combination_new_simple() { #[test] fn invalid_combination_new_conditional() { let res = extrasafe::SafetyContext::new() - .enable(SystemIO::everything()).unwrap() - .enable(SystemIO::nothing() - .allow_stdout()); - assert!(res.is_err(), "Extrasafe didn't fail when adding conflicting rules"); + .enable(SystemIO::everything()) + .unwrap() + .enable(SystemIO::nothing().allow_stdout()); + assert!( + res.is_err(), + "Extrasafe didn't fail when adding conflicting rules" + ); let err = res.unwrap_err(); assert_eq!(err.to_string(), "A conditional rule on syscall `write` from RuleSet `SystemIO` would be overridden by a simple rule from RuleSet `SystemIO`."); @@ -53,8 +56,8 @@ fn invalid_combination_new_conditional() { /// same as above but with different rulesets to check the error message fn invalid_combination_new_simple_different_name() { let res = extrasafe::SafetyContext::new() - .enable(SystemIO::nothing() - .allow_stdout()).unwrap() + .enable(SystemIO::nothing().allow_stdout()) + .unwrap() .enable(JustWrite); assert!( res.is_err(), @@ -68,10 +71,13 @@ fn invalid_combination_new_simple_different_name() { #[test] fn invalid_combination_new_conditional_different_name() { let res = extrasafe::SafetyContext::new() - .enable(JustWrite).unwrap() - .enable(SystemIO::nothing() - .allow_stdout()); - assert!(res.is_err(), "Extrasafe didn't fail when adding conflicting rules"); + .enable(JustWrite) + .unwrap() + .enable(SystemIO::nothing().allow_stdout()); + assert!( + res.is_err(), + "Extrasafe didn't fail when adding conflicting rules" + ); let err = res.unwrap_err(); assert_eq!(err.to_string(), "A conditional rule on syscall `write` from RuleSet `SystemIO` would be overridden by a simple rule from RuleSet `JustWrite`."); @@ -80,13 +86,12 @@ fn invalid_combination_new_conditional_different_name() { #[test] /// Test that adding a conditional and simple rule in the same `RuleSet` produces an error fn invalid_combination_read_and_stdin() { - - let res = extrasafe::SafetyContext::new() - .enable(SystemIO::nothing() - .allow_read() - .allow_stdin() - ); - assert!(res.is_err(), "Extrasafe didn't fail when adding conflicting rules"); + let res = + extrasafe::SafetyContext::new().enable(SystemIO::nothing().allow_read().allow_stdin()); + assert!( + res.is_err(), + "Extrasafe didn't fail when adding conflicting rules" + ); let err = res.unwrap_err(); assert_eq!(err.to_string(), "A conditional rule on syscall `read` from RuleSet `SystemIO` would be overridden by a simple rule from RuleSet `SystemIO`."); @@ -95,12 +100,7 @@ fn invalid_combination_read_and_stdin() { #[test] /// Test that adding duplicate simple rules in the same `RuleSet` doesn't produce an error fn not_invalid_combination_duplicate_simple() { - - let res = extrasafe::SafetyContext::new() - .enable(SystemIO::nothing() - .allow_read() - .allow_read() - ); + let res = extrasafe::SafetyContext::new().enable(SystemIO::nothing().allow_read().allow_read()); assert!(res.is_ok()); let res = res.unwrap().apply_to_current_thread(); @@ -110,13 +110,10 @@ fn not_invalid_combination_duplicate_simple() { #[test] /// Test that adding duplicate simple rules in the same `RuleSet` doesn't produce an error fn not_invalid_combination_duplicate_simple2() { - let res = extrasafe::SafetyContext::new() - .enable(SystemIO::nothing() - .allow_read()).unwrap() - .enable(SystemIO::nothing() - .allow_read() - ); + .enable(SystemIO::nothing().allow_read()) + .unwrap() + .enable(SystemIO::nothing().allow_read()); assert!(res.is_ok()); let res = res.unwrap().apply_to_current_thread(); @@ -126,12 +123,8 @@ fn not_invalid_combination_duplicate_simple2() { #[test] /// Test that adding duplicate conditional rules in the same `RuleSet` doesn't produce an error fn not_invalid_combination_duplicate_conditional() { - - let res = extrasafe::SafetyContext::new() - .enable(SystemIO::nothing() - .allow_stdin() - .allow_stdin() - ); + let res = + extrasafe::SafetyContext::new().enable(SystemIO::nothing().allow_stdin().allow_stdin()); assert!(res.is_ok()); let res = res.unwrap().apply_to_current_thread(); @@ -141,14 +134,10 @@ fn not_invalid_combination_duplicate_conditional() { #[test] /// Test that adding duplicate conditional rules from different `RuleSet`s doesn't produce an error fn not_invalid_combination_duplicate_conditional2() { - let res = extrasafe::SafetyContext::new() - .enable(SystemIO::nothing() - .allow_stdin() - ).unwrap() - .enable(SystemIO::nothing() - .allow_stdin() - ); + .enable(SystemIO::nothing().allow_stdin()) + .unwrap() + .enable(SystemIO::nothing().allow_stdin()); assert!(res.is_ok()); let res = res.unwrap().apply_to_current_thread(); diff --git a/tests/default_deny.rs b/extrasafe/tests/default_deny.rs similarity index 95% rename from tests/default_deny.rs rename to extrasafe/tests/default_deny.rs index 0efa8da..c84cea2 100644 --- a/tests/default_deny.rs +++ b/extrasafe/tests/default_deny.rs @@ -22,7 +22,8 @@ fn filesystem_no_read() { drop(file); let res = extrasafe::SafetyContext::new() - .enable(Basic).unwrap() + .enable(Basic) + .unwrap() .apply_to_current_thread(); assert!(res.is_ok(), "Extrasafe failed {:?}", res.unwrap_err()); @@ -47,7 +48,8 @@ fn filesystem_no_write() { let mut file = tempfile().unwrap(); let res = extrasafe::SafetyContext::new() - .enable(Basic).unwrap() + .enable(Basic) + .unwrap() .apply_to_current_thread(); assert!(res.is_ok(), "Extrasafe failed {:?}", res.unwrap_err()); @@ -72,7 +74,8 @@ fn filesystem_no_create() { let dir = tempdir().unwrap(); let res = extrasafe::SafetyContext::new() - .enable(Basic).unwrap() + .enable(Basic) + .unwrap() .apply_to_current_thread(); assert!(res.is_ok(), "Extrasafe failed {:?}", res.unwrap_err()); diff --git a/tests/inherit_filters.rs b/extrasafe/tests/inherit_filters.rs similarity index 96% rename from tests/inherit_filters.rs rename to extrasafe/tests/inherit_filters.rs index fdf5ca2..31bfb72 100644 --- a/tests/inherit_filters.rs +++ b/extrasafe/tests/inherit_filters.rs @@ -1,15 +1,14 @@ //! Tests that demonstrate seccomp filters are inherited by child processes. -use extrasafe::SafetyContext; use extrasafe::builtins::danger_zone::{ForkAndExec, Threads}; +use extrasafe::SafetyContext; #[test] /// Enable seccomp *only on this thread*, create a new thread, try to create a file and check that /// it fails. fn new_thread_inherits_restrictions() { SafetyContext::new() - .enable(Threads::nothing() - .allow_create()) + .enable(Threads::nothing().allow_create()) .unwrap() .apply_to_current_thread() .unwrap(); diff --git a/tests/landlock_allthreads_fail.rs b/extrasafe/tests/landlock_allthreads_fail.rs similarity index 58% rename from tests/landlock_allthreads_fail.rs rename to extrasafe/tests/landlock_allthreads_fail.rs index 5a26890..d4cf1dd 100644 --- a/tests/landlock_allthreads_fail.rs +++ b/extrasafe/tests/landlock_allthreads_fail.rs @@ -9,13 +9,17 @@ fn test_landlock_apply_to_all_fails() { let dir = tempfile::tempdir().unwrap(); let res = extrasafe::SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_read_path(&dir) - ).unwrap() + .enable(SystemIO::nothing().allow_read_path(&dir)) + .unwrap() .landlock_only() .apply_to_all_threads(); - assert!(res.is_err(), "Did not error when applying to all threads with landlock rules"); - assert!(res.unwrap_err().to_string().contains("Landlock does not support syncing to all threads")); + assert!( + res.is_err(), + "Did not error when applying to all threads with landlock rules" + ); + assert!(res + .unwrap_err() + .to_string() + .contains("Landlock does not support syncing to all threads")); } diff --git a/tests/landlock_basic.rs b/extrasafe/tests/landlock_basic.rs similarity index 73% rename from tests/landlock_basic.rs rename to extrasafe/tests/landlock_basic.rs index 05c4fce..09c51f7 100644 --- a/tests/landlock_basic.rs +++ b/extrasafe/tests/landlock_basic.rs @@ -2,15 +2,19 @@ use std::path::Path; -use std::io::{Read, Write}; use std::fs::{create_dir, read_dir, remove_dir, remove_file, File}; +use std::io::{Read, Write}; use extrasafe::builtins::SystemIO; /// helper to check a file can be read fn can_read_file(path: &Path, expected_data: &str) { let res = File::open(path); - assert!(res.is_ok(), "Failed to open allowed file: {:?}", res.unwrap_err()); + assert!( + res.is_ok(), + "Failed to open allowed file: {:?}", + res.unwrap_err() + ); let mut f = res.unwrap(); let mut file_contents = String::new(); @@ -23,12 +27,20 @@ fn can_read_file(path: &Path, expected_data: &str) { /// helper to check a file can be written to fn can_write_file(path: &Path, write_data: &str) { let res = File::options().append(true).open(path); - assert!(res.is_ok(), "Failed to open allowed file: {:?}", res.unwrap_err()); + assert!( + res.is_ok(), + "Failed to open allowed file: {:?}", + res.unwrap_err() + ); let mut f = res.unwrap(); let res = f.write_all(write_data.as_bytes()); - assert!(res.is_ok(), "failed to write to file: {:?}", res.unwrap_err()); + assert!( + res.is_ok(), + "failed to write to file: {:?}", + res.unwrap_err() + ); drop(f); // after writing the data, check we can read it back @@ -38,7 +50,10 @@ fn can_write_file(path: &Path, write_data: &str) { /// helper to check a file cannot be opened fn can_not_open_file(path: &Path) { let res = File::open(path); - assert!(res.is_err(), "Incorrectly succeeded in opening file for reading"); + assert!( + res.is_err(), + "Incorrectly succeeded in opening file for reading" + ); } // TODO: distinguish between not being able to remove dir due to not being empty vs denied via @@ -65,11 +80,10 @@ fn test_landlock_read_file() { drop(f); extrasafe::SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_read_path(&allowed_file) - ).unwrap() - .apply_to_current_thread().unwrap(); + .enable(SystemIO::nothing().allow_read_path(&allowed_file)) + .unwrap() + .apply_to_current_thread() + .unwrap(); // read allowed, fail to open denied can_read_file(&allowed_file, "test allowed"); @@ -89,9 +103,11 @@ fn test_landlock_write_file() { .enable( SystemIO::nothing() .allow_write_file(&dir) - .allow_read_path(&dir) - ).unwrap() - .apply_to_current_thread().unwrap(); + .allow_read_path(&dir), + ) + .unwrap() + .apply_to_current_thread() + .unwrap(); can_write_file(&allowed_file, "test data"); @@ -110,18 +126,24 @@ fn test_landlock_create_file_in_path() { let denied_file = dir_denied.path().join("denied.txt"); extrasafe::SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_create_in_dir(&dir_allowed) - ).unwrap() - .apply_to_current_thread().unwrap(); + .enable(SystemIO::nothing().allow_create_in_dir(&dir_allowed)) + .unwrap() + .apply_to_current_thread() + .unwrap(); // create succeeds in one directory, fails in other let res = File::create(allowed_file); - assert!(res.is_ok(), "Failed to create file in allowed directory: {:?}", res.unwrap_err()); + assert!( + res.is_ok(), + "Failed to create file in allowed directory: {:?}", + res.unwrap_err() + ); let res = File::create(denied_file); - assert!(res.is_err(), "Incorrectly suceeded in creating file in directory we did not allow"); + assert!( + res.is_err(), + "Incorrectly suceeded in creating file in directory we did not allow" + ); } #[test] @@ -142,9 +164,11 @@ fn test_landlock_delete_file() { .enable( SystemIO::nothing() .allow_remove_file(&dir_allowed) - .allow_list_dir(&dir_allowed) - ).unwrap() - .apply_to_current_thread().unwrap(); + .allow_list_dir(&dir_allowed), + ) + .unwrap() + .apply_to_current_thread() + .unwrap(); let res = remove_file(&allowed_file); assert!(res.is_ok(), "Failed to remove file: {}", res.unwrap_err()); @@ -153,7 +177,10 @@ fn test_landlock_delete_file() { assert_eq!(dir.collect::>().len(), 0); let res = remove_file(&denied_file); - assert!(res.is_err(), "Incorrectly succeeded in removing file that was not allowed"); + assert!( + res.is_err(), + "Incorrectly succeeded in removing file that was not allowed" + ); } #[test] @@ -175,11 +202,10 @@ fn test_landlock_read_dir() { drop(f); extrasafe::SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_read_path(allowed_subdir) - ).unwrap() - .apply_to_current_thread().unwrap(); + .enable(SystemIO::nothing().allow_read_path(allowed_subdir)) + .unwrap() + .apply_to_current_thread() + .unwrap(); // read allowed, fail to open denied can_read_file(&allowed_file, "test allowed"); @@ -193,11 +219,10 @@ fn test_landlock_create_dir() { let dir_denied = tempfile::tempdir().unwrap(); extrasafe::SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_create_dir(&dir_allowed) - ).unwrap() - .apply_to_current_thread().unwrap(); + .enable(SystemIO::nothing().allow_create_dir(&dir_allowed)) + .unwrap() + .apply_to_current_thread() + .unwrap(); let allowed_subdir = dir_allowed.path().join("test_allowed"); let res = create_dir(allowed_subdir); @@ -219,20 +244,26 @@ fn test_landlock_list_dir() { drop(f); extrasafe::SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_list_dir(&dir_allowed) - ).unwrap() - .apply_to_current_thread().unwrap(); + .enable(SystemIO::nothing().allow_list_dir(&dir_allowed)) + .unwrap() + .apply_to_current_thread() + .unwrap(); let res = read_dir(&dir_allowed); - assert!(res.is_ok(), "Failed to list directory: {}", res.unwrap_err()); + assert!( + res.is_ok(), + "Failed to list directory: {}", + res.unwrap_err() + ); let dir = res.unwrap(); assert_eq!(dir.collect::>().len(), 1); let res = read_dir(&dir_denied); - assert!(res.is_err(), "Incorrectly succeeded in reading directory that was not allowed"); + assert!( + res.is_err(), + "Incorrectly succeeded in reading directory that was not allowed" + ); } #[test] @@ -251,15 +282,24 @@ fn test_landlock_delete_dir() { .enable( SystemIO::nothing() .allow_remove_dir(&dir_allowed) - .allow_list_dir(&dir_allowed) - ).unwrap() - .apply_to_current_thread().unwrap(); + .allow_list_dir(&dir_allowed), + ) + .unwrap() + .apply_to_current_thread() + .unwrap(); let res = remove_dir(&allowed_subdir); - assert!(res.is_ok(), "Failed to remove directory: {}", res.unwrap_err()); + assert!( + res.is_ok(), + "Failed to remove directory: {}", + res.unwrap_err() + ); let res = remove_dir(&denied_subdir); - assert!(res.is_err(), "Incorrectly succeeded in removing directory that was not allowed"); + assert!( + res.is_err(), + "Incorrectly succeeded in removing directory that was not allowed" + ); // check dir is empty let mut dir = read_dir(&dir_allowed).unwrap(); @@ -289,9 +329,11 @@ fn test_landlock_one_ruleset() { .allow_list_dir(&allowed_subdir_write) .allow_read_path(&allowed_subdir_write) .allow_write_file(&allowed_subdir_write) - .allow_remove_dir(&dir_allowed) - ).unwrap() - .apply_to_current_thread().unwrap(); + .allow_remove_dir(&dir_allowed), + ) + .unwrap() + .apply_to_current_thread() + .unwrap(); // create file in write dir let allowed_file = allowed_subdir_write.as_path().join("allowed.txt"); @@ -301,19 +343,31 @@ fn test_landlock_one_ruleset() { // check we can list ro directory let res = read_dir(&allowed_subdir); - assert!(res.is_ok(), "Failed to list ro directory: {}", res.unwrap_err()); + assert!( + res.is_ok(), + "Failed to list ro directory: {}", + res.unwrap_err() + ); let mut dir = res.unwrap(); assert!(dir.next().is_none()); // check we can list rw directory let res = read_dir(&allowed_subdir_write); - assert!(res.is_ok(), "Failed to list rw directory: {}", res.unwrap_err()); + assert!( + res.is_ok(), + "Failed to list rw directory: {}", + res.unwrap_err() + ); let dir = res.unwrap(); assert_eq!(dir.collect::>().len(), 1); // check we can remove ro directory (even though we can't write to it!) let res = remove_dir(&allowed_subdir); - assert!(res.is_ok(), "Failed to remove ro directory: {}", res.unwrap_err()); + assert!( + res.is_ok(), + "Failed to remove ro directory: {}", + res.unwrap_err() + ); // check we can read file we wrote to can_read_file(&allowed_file, "test allowed write directory"); @@ -342,15 +396,18 @@ fn test_landlock_different_rulesets() { .allow_list_dir(&allowed_subdir) .allow_read_path(&allowed_subdir) .allow_remove_dir(&dir_allowed) - .allow_list_dir(&dir_allowed) - ).unwrap() + .allow_list_dir(&dir_allowed), + ) + .unwrap() .enable( SystemIO::nothing() .allow_create_in_dir(&allowed_subdir_write) .allow_read_path(&allowed_subdir_write) - .allow_write_file(&allowed_subdir_write) - ).unwrap() - .apply_to_current_thread().unwrap(); + .allow_write_file(&allowed_subdir_write), + ) + .unwrap() + .apply_to_current_thread() + .unwrap(); // create file in write dir let allowed_file = allowed_subdir_write.as_path().join("allowed.txt"); @@ -360,19 +417,31 @@ fn test_landlock_different_rulesets() { // check we can list ro directory let res = read_dir(&allowed_subdir); - assert!(res.is_ok(), "Failed to list ro directory: {}", res.unwrap_err()); + assert!( + res.is_ok(), + "Failed to list ro directory: {}", + res.unwrap_err() + ); let mut dir = res.unwrap(); assert!(dir.next().is_none()); // check we can list rw directory let res = read_dir(&allowed_subdir_write); - assert!(res.is_ok(), "Failed to list rw directory: {}", res.unwrap_err()); + assert!( + res.is_ok(), + "Failed to list rw directory: {}", + res.unwrap_err() + ); let dir = res.unwrap(); assert_eq!(dir.collect::>().len(), 1); // check we can remove ro directory (even though we can't write to it!) let res = remove_dir(&allowed_subdir); - assert!(res.is_ok(), "Failed to remove ro directory: {}", res.unwrap_err()); + assert!( + res.is_ok(), + "Failed to remove ro directory: {}", + res.unwrap_err() + ); // check we can read file we wrote to can_read_file(&allowed_file, "test allowed write directory"); @@ -388,14 +457,16 @@ fn test_nonexistant_file_no_error() { let nonexistant = dir.path().join("bad"); let res = extrasafe::SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_create_in_dir(nonexistant) - ).unwrap() + .enable(SystemIO::nothing().allow_create_in_dir(nonexistant)) + .unwrap() .landlock_only() .apply_to_current_thread(); - assert!(res.is_ok(), "Errored when passing nonexistant file to landlock rule: {:?} ", res.unwrap_err()); + assert!( + res.is_ok(), + "Errored when passing nonexistant file to landlock rule: {:?} ", + res.unwrap_err() + ); } #[test] @@ -404,17 +475,17 @@ fn test_duplicate_path() { let dir = tempfile::tempdir().unwrap(); let res = extrasafe::SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_create_in_dir(&dir) - ).unwrap() - .enable( - SystemIO::nothing() - .allow_create_in_dir(&dir) - ); - - assert!(res.is_err(), "Did not error on passing same dir in multiple rulesets"); + .enable(SystemIO::nothing().allow_create_in_dir(&dir)) + .unwrap() + .enable(SystemIO::nothing().allow_create_in_dir(&dir)); + + assert!( + res.is_err(), + "Did not error on passing same dir in multiple rulesets" + ); let err = res.unwrap_err(); assert!(err.to_string().contains("The same path")); - assert!(err.to_string().contains("was used in two different landlock rules.")); + assert!(err + .to_string() + .contains("was used in two different landlock rules.")); } diff --git a/tests/landlock_conflicts.rs b/extrasafe/tests/landlock_conflicts.rs similarity index 55% rename from tests/landlock_conflicts.rs rename to extrasafe/tests/landlock_conflicts.rs index 52400c8..c4743bc 100644 --- a/tests/landlock_conflicts.rs +++ b/extrasafe/tests/landlock_conflicts.rs @@ -1,6 +1,6 @@ #![cfg(feature = "landlock")] -use std::fs::{File, read_dir}; +use std::fs::{read_dir, File}; use extrasafe::builtins::SystemIO; @@ -11,46 +11,44 @@ fn landlock_with_seccomp_arg_filters_fails() { let path = tempfile::tempdir().unwrap(); // same ruleset - let res = extrasafe::SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_open_readonly() - .allow_list_dir(&path) - ); - - assert!(res.is_err(), "Enabling filter succeeded with landlock and seccomp arg-restricted open"); + let res = extrasafe::SafetyContext::new().enable( + SystemIO::nothing() + .allow_open_readonly() + .allow_list_dir(&path), + ); + + assert!( + res.is_err(), + "Enabling filter succeeded with landlock and seccomp arg-restricted open" + ); // TODO: seccomp/landlock clash error reporting // let err = res.unwrap_err(); // assert_eq!(err.to_string().contains("xxx")); // different rulesets, landlock first let res = extrasafe::SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_open_readonly() - ).unwrap() - .enable( - SystemIO::nothing() - .allow_read_path(&path) - ); - - assert!(res.is_err(), "Enabling filter succeeded with landlock and seccomp arg-restricted open"); + .enable(SystemIO::nothing().allow_open_readonly()) + .unwrap() + .enable(SystemIO::nothing().allow_read_path(&path)); + + assert!( + res.is_err(), + "Enabling filter succeeded with landlock and seccomp arg-restricted open" + ); // TODO: seccomp/landlock clash error reporting // let err = res.unwrap_err(); // assert!(err.to_string().contains("xxx")); // different rulesets, seccomp first let res = extrasafe::SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_read_path(&path) - ).unwrap() - .enable( - SystemIO::nothing() - .allow_open_readonly() - ); - - assert!(res.is_err(), "Enabling filter succeeded with landlock and seccomp arg-restricted open"); + .enable(SystemIO::nothing().allow_read_path(&path)) + .unwrap() + .enable(SystemIO::nothing().allow_open_readonly()); + + assert!( + res.is_err(), + "Enabling filter succeeded with landlock and seccomp arg-restricted open" + ); // TODO: seccomp/landlock clash error reporting // let err = res.unwrap_err(); // assert!(err.to_string().contains("xxx")); @@ -64,7 +62,10 @@ fn landlock_only() { .landlock_only() .apply_to_current_thread(); - assert!(res.is_err(), "extrasafe did not error when applying with no seccomp or landlock rules"); + assert!( + res.is_err(), + "extrasafe did not error when applying with no seccomp or landlock rules" + ); let err = res.unwrap_err(); assert!(err.to_string().contains("No rules were enabled")); @@ -72,12 +73,11 @@ fn landlock_only() { let dir = tempfile::tempdir().unwrap(); extrasafe::SafetyContext::new() - .enable( - SystemIO::nothing() - .allow_create_in_dir(&dir) - ).unwrap() + .enable(SystemIO::nothing().allow_create_in_dir(&dir)) + .unwrap() .landlock_only() - .apply_to_current_thread().unwrap(); + .apply_to_current_thread() + .unwrap(); // test that we can run arbitrary syscalls let pid = unsafe { libc::getpid() }; @@ -86,11 +86,21 @@ fn landlock_only() { // test that we can create in the given directory let file_path = dir.path().join("okay.txt"); let file_res = File::create(file_path); - assert!(file_res.is_ok(), "Failed to create file in allowed dir: {:?}", file_res.unwrap_err()); + assert!( + file_res.is_ok(), + "Failed to create file in allowed dir: {:?}", + file_res.unwrap_err() + ); // test that we can't list paths let list_res = read_dir(&dir); - assert!(list_res.is_err(), "Incorrectly succeeded in listing directory"); + assert!( + list_res.is_err(), + "Incorrectly succeeded in listing directory" + ); let list_res = read_dir("/etc"); - assert!(list_res.is_err(), "Incorrectly succeeded in listing directory"); + assert!( + list_res.is_err(), + "Incorrectly succeeded in listing directory" + ); } diff --git a/extrasafe/tests/multiple_conditions.rs b/extrasafe/tests/multiple_conditions.rs new file mode 100644 index 0000000..f92aa22 --- /dev/null +++ b/extrasafe/tests/multiple_conditions.rs @@ -0,0 +1,29 @@ +use std::io::Write; + +use extrasafe::*; + +#[test] +/// Test that if multiple `RuleSets` have conditional rules, any of them will work i.e. they are +/// or-ed together across all `RuleSets`. +fn multiple_rulsets_conditional() { + SafetyContext::new() + .enable(builtins::SystemIO::nothing().allow_stdout()) + .unwrap() + .enable(builtins::SystemIO::nothing().allow_stderr()) + .unwrap() + .apply_to_current_thread() + .unwrap(); + + let res = writeln!(std::io::stdout(), "we can print to stdout"); + assert!( + res.is_ok(), + "failed to write to stdout: {:?}", + res.unwrap_err() + ); + let res = writeln!(std::io::stderr(), "we can print to stderr"); + assert!( + res.is_ok(), + "failed to write to stderr: {:?}", + res.unwrap_err() + ); +} diff --git a/tests/multiple_filters.rs b/extrasafe/tests/multiple_filters.rs similarity index 67% rename from tests/multiple_filters.rs rename to extrasafe/tests/multiple_filters.rs index 6b7f233..3ab4790 100644 --- a/tests/multiple_filters.rs +++ b/extrasafe/tests/multiple_filters.rs @@ -1,5 +1,5 @@ -use extrasafe::*; use extrasafe::syscalls::Sysno; +use extrasafe::*; use std::collections::HashMap; @@ -22,23 +22,25 @@ impl RuleSet for Seccomp { /// really doesn't ever make sense to enable multiple filters. fn filter_stacking_works_but_may_give_unintended_results() { SafetyContext::new() - .enable(builtins::SystemIO::nothing() - .allow_stdout() - .allow_stderr() - .allow_open().yes_really() - .allow_metadata() - ).unwrap() - .enable(Seccomp).unwrap() - .apply_to_current_thread().unwrap(); + .enable( + builtins::SystemIO::nothing() + .allow_stdout() + .allow_stderr() + .allow_open() + .yes_really() + .allow_metadata(), + ) + .unwrap() + .enable(Seccomp) + .unwrap() + .apply_to_current_thread() + .unwrap(); let res = SafetyContext::new() - .enable(builtins::SystemIO::nothing() - .allow_stdout() - .allow_stderr() - ).unwrap() - .enable(builtins::danger_zone::Threads::nothing() - .allow_create() - ).unwrap() + .enable(builtins::SystemIO::nothing().allow_stdout().allow_stderr()) + .unwrap() + .enable(builtins::danger_zone::Threads::nothing().allow_create()) + .unwrap() .apply_to_current_thread(); assert!( res.is_ok(), @@ -47,8 +49,7 @@ fn filter_stacking_works_but_may_give_unintended_results() { ); println!("test"); - let res = std::thread::Builder::new() - .spawn(|| println!("will not run")); + let res = std::thread::Builder::new().spawn(|| println!("will not run")); assert!(res.is_err(), "Even though clone was enabled on the second filter, it was not in the first and so isn't allowed."); let res = std::fs::File::open("will_not_be_opened.txt"); diff --git a/tests/network.rs b/extrasafe/tests/network.rs similarity index 80% rename from tests/network.rs rename to extrasafe/tests/network.rs index dc0b92e..150c01f 100644 --- a/tests/network.rs +++ b/extrasafe/tests/network.rs @@ -16,7 +16,6 @@ use std::thread; /// main thread, enable seccomp, send the message and get a response. Then try to bind a new socket /// and check that it fails. fn test_udp() { - // These block on send until reciever has finished recv. let (sender1, recv1) = sync_channel::<()>(0); @@ -47,14 +46,10 @@ fn test_udp() { // create safetycontext after server and client have been bound. SafetyContext::new() - .enable( - Networking::nothing() - .allow_running_udp_sockets() - ).unwrap() - .enable( - Threads::nothing() - .allow_create() - ).unwrap() + .enable(Networking::nothing().allow_running_udp_sockets()) + .unwrap() + .enable(Threads::nothing().allow_create()) + .unwrap() .apply_to_current_thread() .unwrap(); @@ -115,11 +110,10 @@ fn test_tcp() { // create safetycontext after server and client have been bound. SafetyContext::new() - .enable(Networking::nothing() - .allow_running_tcp_clients() - ).unwrap() - .enable(Threads::nothing() - .allow_create()).unwrap() + .enable(Networking::nothing().allow_running_tcp_clients()) + .unwrap() + .enable(Threads::nothing().allow_create()) + .unwrap() .apply_to_current_thread() .unwrap(); @@ -152,28 +146,30 @@ fn test_tcp() { /// instead open and bind before applying your policy. fn test_start_tcp() { SafetyContext::new() - .enable( - Networking::nothing() - .allow_start_tcp_servers().yes_really() - ).unwrap() - .enable( - Threads::nothing() - .allow_create() - ).unwrap() + .enable(Networking::nothing().allow_start_tcp_servers().yes_really()) + .unwrap() + .enable(Threads::nothing().allow_create()) + .unwrap() .apply_to_current_thread() .unwrap(); let tcp_res = std::net::TcpListener::bind("127.0.0.1:0"); assert!(tcp_res.is_ok(), "Failed to bind tcp server"); let udp_res = std::net::UdpSocket::bind("127.0.0.1:0"); - assert!(udp_res.is_err(), "Incorrectly succeeded in binding udp socket"); + assert!( + udp_res.is_err(), + "Incorrectly succeeded in binding udp socket" + ); // test ipv6 as well let tcp_res = std::net::TcpListener::bind("[::1]:0"); assert!(tcp_res.is_ok(), "Failed to bind tcp server"); let udp_res = std::net::UdpSocket::bind("[::1]:0"); - assert!(udp_res.is_err(), "Incorrectly succeeded in binding udp socket"); + assert!( + udp_res.is_err(), + "Incorrectly succeeded in binding udp socket" + ); } #[test] @@ -181,28 +177,30 @@ fn test_start_tcp() { /// instead open and bind before applying your policy. fn test_start_udp() { SafetyContext::new() - .enable( - Networking::nothing() - .allow_start_udp_servers().yes_really() - ).unwrap() - .enable( - Threads::nothing() - .allow_create() - ).unwrap() + .enable(Networking::nothing().allow_start_udp_servers().yes_really()) + .unwrap() + .enable(Threads::nothing().allow_create()) + .unwrap() .apply_to_current_thread() .unwrap(); let udp_res = std::net::UdpSocket::bind("127.0.0.1:0"); assert!(udp_res.is_ok(), "Failed to bind udp server"); let udp_res = std::net::TcpListener::bind("127.0.0.1:0"); - assert!(udp_res.is_err(), "Incorrectly succeeded in binding udp socket"); + assert!( + udp_res.is_err(), + "Incorrectly succeeded in binding udp socket" + ); // test ipv6 as well let udp_res = std::net::UdpSocket::bind("[::1]:0"); assert!(udp_res.is_ok(), "Failed to bind udp server"); let tcp_res = std::net::TcpListener::bind("[::1]:0"); - assert!(tcp_res.is_err(), "Incorrectly succeeded in binding tcp socket"); + assert!( + tcp_res.is_err(), + "Incorrectly succeeded in binding tcp socket" + ); } #[test] @@ -216,12 +214,12 @@ fn test_start_unix() { SafetyContext::new() .enable( Networking::nothing() - .allow_start_unix_servers().yes_really() - ).unwrap() - .enable( - Threads::nothing() - .allow_create() - ).unwrap() + .allow_start_unix_servers() + .yes_really(), + ) + .unwrap() + .enable(Threads::nothing().allow_create()) + .unwrap() .apply_to_current_thread() .unwrap(); @@ -229,8 +227,14 @@ fn test_start_unix() { assert!(unix_res.is_ok(), "Failed to bind tcp server"); let udp_res = std::net::UdpSocket::bind("127.0.0.1:0"); - assert!(udp_res.is_err(), "Incorrectly succeeded in binding udp socket"); + assert!( + udp_res.is_err(), + "Incorrectly succeeded in binding udp socket" + ); let tcp_res = std::net::TcpListener::bind("[::1]:0"); - assert!(tcp_res.is_err(), "Incorrectly succeeded in binding tcp socket"); + assert!( + tcp_res.is_err(), + "Incorrectly succeeded in binding tcp socket" + ); } diff --git a/tests/open_readonly.rs b/extrasafe/tests/open_readonly.rs similarity index 100% rename from tests/open_readonly.rs rename to extrasafe/tests/open_readonly.rs diff --git a/tests/ruleset_union.rs b/extrasafe/tests/ruleset_union.rs similarity index 69% rename from tests/ruleset_union.rs rename to extrasafe/tests/ruleset_union.rs index 2567bea..d2e9775 100644 --- a/tests/ruleset_union.rs +++ b/extrasafe/tests/ruleset_union.rs @@ -1,10 +1,9 @@ -use std::path::Path; use std::fs::File; use std::io::{Read, Write}; +use std::path::Path; -use extrasafe::*; use builtins::SystemIO; - +use extrasafe::*; // Tests to make sure we don't run into this issue // https://github.com/rust-vmm/seccompiler/issues/42 @@ -15,18 +14,20 @@ use builtins::SystemIO; fn different_rulesets_same_syscall() { SafetyContext::new() // First RuleSet: stdout, stderr - .enable(SystemIO::nothing() - .allow_read() - .allow_stdout() - .allow_stderr() - .allow_metadata() - ).unwrap() .enable( - // Second RuleSet: stderr only - SystemIO::nothing() - .allow_stderr() - .allow_metadata() - .allow_close(), + SystemIO::nothing() + .allow_read() + .allow_stdout() + .allow_stderr() + .allow_metadata(), + ) + .unwrap() + .enable( + // Second RuleSet: stderr only + SystemIO::nothing() + .allow_stderr() + .allow_metadata() + .allow_close(), ) .unwrap() .apply_to_current_thread() @@ -34,9 +35,17 @@ fn different_rulesets_same_syscall() { // Try to write to stdout and stderr let res = writeln!(std::io::stdout(), "we can print to stdout"); - assert!(res.is_ok(), "failed to write to stdout: {:?}", res.unwrap_err()); + assert!( + res.is_ok(), + "failed to write to stdout: {:?}", + res.unwrap_err() + ); let res = writeln!(std::io::stderr(), "we can print to stderr"); - assert!(res.is_ok(), "failed to write to stderr: {:?}", res.unwrap_err()); + assert!( + res.is_ok(), + "failed to write to stderr: {:?}", + res.unwrap_err() + ); } fn create_testfile(path: &Path, filename: &str) -> File { @@ -51,7 +60,6 @@ fn create_testfile(path: &Path, filename: &str) -> File { File::open(&path).unwrap() } - #[test] /// Same as above but with mask instead of == and also 3 rulesets fn different_rulesets_same_syscall2() { @@ -68,19 +76,14 @@ fn different_rulesets_same_syscall2() { // Add three different rulesets each allowing reads to a different file SafetyContext::new() - .enable(SystemIO::nothing() - .allow_stdout() - .allow_stderr() - ).unwrap() - .enable(SystemIO::nothing() - .allow_file_read(&file1) - ).unwrap() - .enable(SystemIO::nothing() - .allow_file_read(&file2) - ).unwrap() - .enable(SystemIO::nothing() - .allow_file_read(&file3) - ).unwrap() + .enable(SystemIO::nothing().allow_stdout().allow_stderr()) + .unwrap() + .enable(SystemIO::nothing().allow_file_read(&file1)) + .unwrap() + .enable(SystemIO::nothing().allow_file_read(&file2)) + .unwrap() + .enable(SystemIO::nothing().allow_file_read(&file3)) + .unwrap() .apply_to_current_thread() .unwrap(); diff --git a/extrasafe/tests/sleep.rs b/extrasafe/tests/sleep.rs new file mode 100644 index 0000000..78fbeea --- /dev/null +++ b/extrasafe/tests/sleep.rs @@ -0,0 +1,27 @@ +use extrasafe::builtins::{danger_zone::Threads, SystemIO}; + +#[test] +// assert is just unhelpful assert_eq message +#[should_panic(expected = "assertion")] +fn insomnia() { + extrasafe::SafetyContext::new() + .enable(SystemIO::nothing().allow_stdout().allow_stderr()) + .unwrap() + .apply_to_current_thread() + .unwrap(); + + std::thread::sleep(std::time::Duration::from_secs(1)); +} + +#[test] +fn comfy() { + extrasafe::SafetyContext::new() + .enable(SystemIO::nothing().allow_stdout().allow_stderr()) + .unwrap() + .enable(Threads::nothing().allow_sleep().yes_really()) + .unwrap() + .apply_to_current_thread() + .unwrap(); + + std::thread::sleep(std::time::Duration::from_millis(1)); +} diff --git a/tests/sysno.rs b/extrasafe/tests/sysno.rs similarity index 100% rename from tests/sysno.rs rename to extrasafe/tests/sysno.rs diff --git a/tests/test_ref_ruleset.rs b/extrasafe/tests/test_ref_ruleset.rs similarity index 70% rename from tests/test_ref_ruleset.rs rename to extrasafe/tests/test_ref_ruleset.rs index 7879846..af09176 100644 --- a/tests/test_ref_ruleset.rs +++ b/extrasafe/tests/test_ref_ruleset.rs @@ -1,9 +1,11 @@ -use extrasafe::RuleSet; use extrasafe::builtins::BasicCapabilities; +use extrasafe::RuleSet; #[test] /// Test if `RuleSets` can be references. fn ref_ruleset() -> Result<(), extrasafe::ExtraSafeError> { let ruleset: &dyn RuleSet = &BasicCapabilities; - extrasafe::SafetyContext::new().enable(ruleset)?.apply_to_current_thread() + extrasafe::SafetyContext::new() + .enable(ruleset)? + .apply_to_current_thread() } diff --git a/tests/tests_can_fail.rs b/extrasafe/tests/tests_can_fail.rs similarity index 88% rename from tests/tests_can_fail.rs rename to extrasafe/tests/tests_can_fail.rs index 849440e..bb35c94 100644 --- a/tests/tests_can_fail.rs +++ b/extrasafe/tests/tests_can_fail.rs @@ -6,7 +6,8 @@ /// failure propagates to the cli fn seccomp_active_tests_fail() { let res = extrasafe::SafetyContext::new() - .enable(extrasafe::builtins::BasicCapabilities).unwrap() + .enable(extrasafe::builtins::BasicCapabilities) + .unwrap() .apply_to_current_thread(); assert!(res.is_ok(), "Extrasafe failed {:?}", res.unwrap_err()); diff --git a/tests/thread_multi.rs b/extrasafe/tests/thread_multi.rs similarity index 91% rename from tests/thread_multi.rs rename to extrasafe/tests/thread_multi.rs index 7b28213..fbb281f 100644 --- a/tests/thread_multi.rs +++ b/extrasafe/tests/thread_multi.rs @@ -16,10 +16,10 @@ fn sync_thread_contexts() { let seccomp_thread = thread::spawn(move || { extrasafe::SafetyContext::new() - .enable(SystemIO::nothing() - .allow_stdout() - .allow_stderr()).unwrap() - .apply_to_all_threads().unwrap(); + .enable(SystemIO::nothing().allow_stdout().allow_stderr()) + .unwrap() + .apply_to_all_threads() + .unwrap(); // setup_done sender1.send(()).unwrap(); diff --git a/tests/thread_single.rs b/extrasafe/tests/thread_single.rs similarity index 93% rename from tests/thread_single.rs rename to extrasafe/tests/thread_single.rs index 504fb71..7e5d462 100644 --- a/tests/thread_single.rs +++ b/extrasafe/tests/thread_single.rs @@ -16,17 +16,16 @@ use std::fs::File; /// threads. This is achieved in this test by blocking IO on one thread and not on another, and /// checking IO can be performed in the other thread after loading the context in the first. fn different_threads_with_different_contexts() { - // These channels will block on send until the receiver has called recv. let (sender1, recv1) = sync_channel::<()>(0); let (sender2, recv2) = sync_channel::<()>(0); let seccomp_thread = thread::spawn(move || { extrasafe::SafetyContext::new() - .enable(SystemIO::nothing() - .allow_stdout() - .allow_stderr()).unwrap() - .apply_to_current_thread().unwrap(); + .enable(SystemIO::nothing().allow_stdout().allow_stderr()) + .unwrap() + .apply_to_current_thread() + .unwrap(); // setup_done sender1.send(()).unwrap(); diff --git a/tests/unsupported_os.rs b/extrasafe/tests/unsupported_os.rs similarity index 78% rename from tests/unsupported_os.rs rename to extrasafe/tests/unsupported_os.rs index 3df8bc3..0daeb22 100644 --- a/tests/unsupported_os.rs +++ b/extrasafe/tests/unsupported_os.rs @@ -1,8 +1,7 @@ #[cfg(not(target_os = "linux"))] #[test] fn returns_unsupported_os_error() { - let res = extrasafe::SafetyContext::new() - .apply_to_all_threads(); + let res = extrasafe::SafetyContext::new().apply_to_all_threads(); assert!( res.is_err(), diff --git a/tests/use_fd.rs b/extrasafe/tests/use_fd.rs similarity index 100% rename from tests/use_fd.rs rename to extrasafe/tests/use_fd.rs diff --git a/tests/multiple_conditions.rs b/tests/multiple_conditions.rs deleted file mode 100644 index 298ff34..0000000 --- a/tests/multiple_conditions.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::io::Write; - -use extrasafe::*; - -#[test] -/// Test that if multiple `RuleSets` have conditional rules, any of them will work i.e. they are -/// or-ed together across all `RuleSets`. -fn multiple_rulsets_conditional() { - SafetyContext::new() - .enable(builtins::SystemIO::nothing() - .allow_stdout() - ).unwrap() - .enable(builtins::SystemIO::nothing() - .allow_stderr() - ).unwrap() - .apply_to_current_thread().unwrap(); - - let res = writeln!(std::io::stdout(), "we can print to stdout"); - assert!(res.is_ok(), "failed to write to stdout: {:?}", res.unwrap_err()); - let res = writeln!(std::io::stderr(), "we can print to stderr"); - assert!(res.is_ok(), "failed to write to stderr: {:?}", res.unwrap_err()); -} diff --git a/tests/sleep.rs b/tests/sleep.rs deleted file mode 100644 index 5d235d8..0000000 --- a/tests/sleep.rs +++ /dev/null @@ -1,27 +0,0 @@ -use extrasafe::builtins::{SystemIO, danger_zone::Threads}; - -#[test] -// assert is just unhelpful assert_eq message -#[should_panic(expected = "assertion")] -fn insomnia() { - extrasafe::SafetyContext::new() - .enable(SystemIO::nothing() - .allow_stdout() - .allow_stderr()).unwrap() - .apply_to_current_thread().unwrap(); - - std::thread::sleep(std::time::Duration::from_secs(1)); -} - -#[test] -fn comfy() { - extrasafe::SafetyContext::new() - .enable(SystemIO::nothing() - .allow_stdout() - .allow_stderr()).unwrap() - .enable(Threads::nothing() - .allow_sleep().yes_really()).unwrap() - .apply_to_current_thread().unwrap(); - - std::thread::sleep(std::time::Duration::from_millis(1)); -}