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 100% rename from examples/ipc_server_with_database.rs rename to extrasafe/examples/ipc_server_with_database.rs 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 100% rename from examples/network_server.rs rename to extrasafe/examples/network_server.rs diff --git a/examples/no_files_allow_stdout.rs b/extrasafe/examples/no_files_allow_stdout.rs similarity index 100% rename from examples/no_files_allow_stdout.rs rename to extrasafe/examples/no_files_allow_stdout.rs diff --git a/examples/server_with_database.rs b/extrasafe/examples/server_with_database.rs similarity index 100% rename from examples/server_with_database.rs rename to extrasafe/examples/server_with_database.rs diff --git a/examples/simple_network.rs b/extrasafe/examples/simple_network.rs similarity index 100% rename from examples/simple_network.rs rename to extrasafe/examples/simple_network.rs diff --git a/examples/time.rs b/extrasafe/examples/time.rs similarity index 100% rename from examples/time.rs rename to extrasafe/examples/time.rs diff --git a/examples/user-guide.rs b/extrasafe/examples/user-guide.rs similarity index 100% rename from examples/user-guide.rs rename to extrasafe/examples/user-guide.rs 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 100% rename from src/builtins/basic.rs rename to extrasafe/src/builtins/basic.rs diff --git a/src/builtins/danger_zone.rs b/extrasafe/src/builtins/danger_zone.rs similarity index 100% rename from src/builtins/danger_zone.rs rename to extrasafe/src/builtins/danger_zone.rs diff --git a/src/builtins/mod.rs b/extrasafe/src/builtins/mod.rs similarity index 100% rename from src/builtins/mod.rs rename to extrasafe/src/builtins/mod.rs diff --git a/src/builtins/network.rs b/extrasafe/src/builtins/network.rs similarity index 100% rename from src/builtins/network.rs rename to extrasafe/src/builtins/network.rs 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 diff --git a/src/builtins/systemio.rs b/extrasafe/src/builtins/systemio.rs similarity index 100% rename from src/builtins/systemio.rs rename to extrasafe/src/builtins/systemio.rs diff --git a/src/builtins/time.rs b/extrasafe/src/builtins/time.rs similarity index 100% rename from src/builtins/time.rs rename to extrasafe/src/builtins/time.rs 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 diff --git a/src/isolate/isolate_sys.rs b/extrasafe/src/isolate/isolate_sys.rs similarity index 100% rename from src/isolate/isolate_sys.rs rename to extrasafe/src/isolate/isolate_sys.rs diff --git a/src/isolate/mod.rs b/extrasafe/src/isolate/mod.rs similarity index 100% rename from src/isolate/mod.rs rename to extrasafe/src/isolate/mod.rs diff --git a/src/landlock.rs b/extrasafe/src/landlock.rs similarity index 100% rename from src/landlock.rs rename to extrasafe/src/landlock.rs diff --git a/src/lib.rs b/extrasafe/src/lib.rs similarity index 100% rename from src/lib.rs rename to extrasafe/src/lib.rs diff --git a/src/macros.rs b/extrasafe/src/macros.rs similarity index 100% rename from src/macros.rs rename to extrasafe/src/macros.rs diff --git a/tests/arg_comparisons.rs b/extrasafe/tests/arg_comparisons.rs similarity index 100% rename from tests/arg_comparisons.rs rename to extrasafe/tests/arg_comparisons.rs diff --git a/tests/bad_combination.rs b/extrasafe/tests/bad_combination.rs similarity index 100% rename from tests/bad_combination.rs rename to extrasafe/tests/bad_combination.rs diff --git a/tests/default_deny.rs b/extrasafe/tests/default_deny.rs similarity index 100% rename from tests/default_deny.rs rename to extrasafe/tests/default_deny.rs diff --git a/tests/inherit_filters.rs b/extrasafe/tests/inherit_filters.rs similarity index 100% rename from tests/inherit_filters.rs rename to extrasafe/tests/inherit_filters.rs diff --git a/tests/landlock_allthreads_fail.rs b/extrasafe/tests/landlock_allthreads_fail.rs similarity index 100% rename from tests/landlock_allthreads_fail.rs rename to extrasafe/tests/landlock_allthreads_fail.rs diff --git a/tests/landlock_basic.rs b/extrasafe/tests/landlock_basic.rs similarity index 100% rename from tests/landlock_basic.rs rename to extrasafe/tests/landlock_basic.rs diff --git a/tests/landlock_conflicts.rs b/extrasafe/tests/landlock_conflicts.rs similarity index 100% rename from tests/landlock_conflicts.rs rename to extrasafe/tests/landlock_conflicts.rs diff --git a/tests/multiple_conditions.rs b/extrasafe/tests/multiple_conditions.rs similarity index 100% rename from tests/multiple_conditions.rs rename to extrasafe/tests/multiple_conditions.rs diff --git a/tests/multiple_filters.rs b/extrasafe/tests/multiple_filters.rs similarity index 100% rename from tests/multiple_filters.rs rename to extrasafe/tests/multiple_filters.rs diff --git a/tests/network.rs b/extrasafe/tests/network.rs similarity index 100% rename from tests/network.rs rename to extrasafe/tests/network.rs 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 100% rename from tests/ruleset_union.rs rename to extrasafe/tests/ruleset_union.rs diff --git a/tests/sleep.rs b/extrasafe/tests/sleep.rs similarity index 100% rename from tests/sleep.rs rename to extrasafe/tests/sleep.rs 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 100% rename from tests/test_ref_ruleset.rs rename to extrasafe/tests/test_ref_ruleset.rs diff --git a/tests/tests_can_fail.rs b/extrasafe/tests/tests_can_fail.rs similarity index 100% rename from tests/tests_can_fail.rs rename to extrasafe/tests/tests_can_fail.rs diff --git a/tests/thread_multi.rs b/extrasafe/tests/thread_multi.rs similarity index 100% rename from tests/thread_multi.rs rename to extrasafe/tests/thread_multi.rs diff --git a/tests/thread_single.rs b/extrasafe/tests/thread_single.rs similarity index 100% rename from tests/thread_single.rs rename to extrasafe/tests/thread_single.rs diff --git a/tests/unsupported_os.rs b/extrasafe/tests/unsupported_os.rs similarity index 100% rename from tests/unsupported_os.rs rename to extrasafe/tests/unsupported_os.rs 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