Repository: phylum-dev/birdcage (language: rust)

Issue:
Cleanup temporary test directories
Many of the integration tests currently leak temporary directories (because they can't be removed once the sandbox is in place). With the new test harness in #64, we can now clean these up. See [this discussion][1].

[1]: https://github.com/phylum-dev/birdcage/pull/64#discussion_r1409797338


Discussion / hints:

Fix patch:
diff --git a/integration/canonicalize.rs b/integration/canonicalize.rs
index 8c9bb26..3ea01b0 100644
--- a/integration/canonicalize.rs
+++ b/integration/canonicalize.rs
@@ -1,10 +1,11 @@
 use std::fs;
+use std::path::PathBuf;
 
 use birdcage::{Birdcage, Exception, Sandbox};
 
 use crate::TestSetup;
 
-pub fn setup() -> TestSetup {
+pub fn setup(_tempdir: PathBuf) -> TestSetup {
     let mut sandbox = Birdcage::new();
     sandbox.add_exception(Exception::Read("./".into())).unwrap();
 
diff --git a/integration/consistent_id_mappings.rs b/integration/consistent_id_mappings.rs
index 5b77326..fd677e6 100644
--- a/integration/consistent_id_mappings.rs
+++ b/integration/consistent_id_mappings.rs
@@ -1,3 +1,4 @@
+use std::path::PathBuf;
 use birdcage::{Birdcage, Sandbox};
 use serde::{Deserialize, Serialize};
 
@@ -11,7 +12,7 @@ struct TestData {
     egid: u32,
 }
 
-pub fn setup() -> TestSetup {
+pub fn setup(_tempdir: PathBuf) -> TestSetup {
     let uid = unsafe { libc::getuid() };
     let gid = unsafe { libc::getgid() };
     let euid = unsafe { libc::geteuid() };
diff --git a/integration/delete_before_lockdown.rs b/integration/delete_before_lockdown.rs
index f0ad37b..751a91f 100644
--- a/integration/delete_before_lockdown.rs
+++ b/integration/delete_before_lockdown.rs
@@ -1,10 +1,11 @@
+use std::path::PathBuf;
 
 use birdcage::{Birdcage, Exception, Sandbox};
 use tempfile::NamedTempFile;
 
 use crate::TestSetup;
 
-pub fn setup() -> TestSetup {
+pub fn setup(_tempdir: PathBuf) -> TestSetup {
     // Create temporary file.
     let tempfile = NamedTempFile::new().unwrap();
 
diff --git a/integration/env.rs b/integration/env.rs
index 80e0bfa..b9a4670 100644
--- a/integration/env.rs
+++ b/integration/env.rs
@@ -1,10 +1,11 @@
+use std::path::PathBuf;
 use std::env;
 
 use birdcage::{Birdcage, Exception, Sandbox};
 
 use crate::TestSetup;
 
-pub fn setup() -> TestSetup {
+pub fn setup(_tempdir: PathBuf) -> TestSetup {
     // Setup our environment variables
     env::set_var("PUBLIC", "GOOD");
     env::set_var("PRIVATE", "BAD");
diff --git a/integration/exec.rs b/integration/exec.rs
index 1557360..e551764 100644
--- a/integration/exec.rs
+++ b/integration/exec.rs
@@ -1,3 +1,4 @@
+use std::path::PathBuf;
 use std::fs;
 use std::process::Command;
 
@@ -5,7 +6,7 @@ use birdcage::{Birdcage, Exception, Sandbox};
 
 use crate::TestSetup;
 
-pub fn setup() -> TestSetup {
+pub fn setup(_tempdir: PathBuf) -> TestSetup {
     let mut sandbox = Birdcage::new();
     sandbox.add_exception(Exception::ExecuteAndRead("/usr/bin/true".into())).unwrap();
 
diff --git a/integration/exec_symlinked_dir.rs b/integration/exec_symlinked_dir.rs
index 2f141b3..c7ab4b2 100644
--- a/integration/exec_symlinked_dir.rs
+++ b/integration/exec_symlinked_dir.rs
@@ -12,9 +12,8 @@ struct TestData {
     symlink_dir: PathBuf,
 }
 
-pub fn setup() -> TestSetup {
+pub fn setup(tempdir: PathBuf) -> TestSetup {
     // Create symlinked executable dir.
-    let tempdir = tempfile::tempdir().unwrap().into_path();
     let symlink_dir = tempdir.join("bin");
     unixfs::symlink("/usr/bin", &symlink_dir).unwrap();
 
diff --git a/integration/exec_symlinked_dirs_exec.rs b/integration/exec_symlinked_dirs_exec.rs
index d4e9c6b..1f6c94a 100644
--- a/integration/exec_symlinked_dirs_exec.rs
+++ b/integration/exec_symlinked_dirs_exec.rs
@@ -12,9 +12,8 @@ struct TestData {
     symlink_dir_exec: PathBuf,
 }
 
-pub fn setup() -> TestSetup {
+pub fn setup(tempdir: PathBuf) -> TestSetup {
     // Create symlinked executable dir.
-    let tempdir = tempfile::tempdir().unwrap().into_path();
     let symlink_dir = tempdir.join("bin");
     let symlink_dir_exec = symlink_dir.join("true");
     unixfs::symlink("/usr/bin", &symlink_dir).unwrap();
diff --git a/integration/exec_symlinked_file.rs b/integration/exec_symlinked_file.rs
index 75960a0..676cc83 100644
--- a/integration/exec_symlinked_file.rs
+++ b/integration/exec_symlinked_file.rs
@@ -13,9 +13,8 @@ struct TestData {
     symlink_exec: PathBuf,
 }
 
-pub fn setup() -> TestSetup {
+pub fn setup(tempdir: PathBuf) -> TestSetup {
     // Create symlinked executable.
-    let tempdir = tempfile::tempdir().unwrap().into_path();
     let exec_dir = tempdir.join("bin");
     fs::create_dir(&exec_dir).unwrap();
     let symlink_exec = exec_dir.join("true");
diff --git a/integration/fs.rs b/integration/fs.rs
index 70e90ac..6dffa36 100644
--- a/integration/fs.rs
+++ b/integration/fs.rs
@@ -3,7 +3,6 @@ use std::path::PathBuf;
 
 use birdcage::{Birdcage, Exception, Sandbox};
 use serde::{Deserialize, Serialize};
-use tempfile::NamedTempFile;
 
 use crate::TestSetup;
 
@@ -15,11 +14,11 @@ struct TestData {
     private_path: PathBuf,
 }
 
-pub fn setup() -> TestSetup {
+pub fn setup(tempdir: PathBuf) -> TestSetup {
     // Setup our test files.
-    let private_path = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap();
+    let private_path = tempdir.join("private");
     fs::write(&private_path, FILE_CONTENT.as_bytes()).unwrap();
-    let public_path = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap();
+    let public_path = tempdir.join("public");
     fs::write(&public_path, FILE_CONTENT.as_bytes()).unwrap();
 
     // Setup sandbox exceptions.
diff --git a/integration/fs_broken_symlink.rs b/integration/fs_broken_symlink.rs
index 7981dba..9fd514e 100644
--- a/integration/fs_broken_symlink.rs
+++ b/integration/fs_broken_symlink.rs
@@ -1,11 +1,10 @@
-use std::fs;
+use std::fs::{self, File};
 use std::os::unix::fs as unixfs;
 use std::path::PathBuf;
 
 use birdcage::error::Error;
 use birdcage::{Birdcage, Exception, Sandbox};
 use serde::{Deserialize, Serialize};
-use tempfile::NamedTempFile;
 
 use crate::TestSetup;
 
@@ -14,14 +13,18 @@ struct TestData {
     symlink: PathBuf,
 }
 
-pub fn setup() -> TestSetup {
-    // Setup a symlink without target.
-    let tempfile = NamedTempFile::new().unwrap();
-    let tempfile_path = tempfile.path().to_path_buf();
-    let symlink_str = tempfile_path.to_string_lossy() + "_tmpfile";
+pub fn setup(tempdir: PathBuf) -> TestSetup {
+    // Create a target for the symlink.
+    let tempfile_path = tempdir.join("broken_target");
+    File::create(&tempfile_path).unwrap();
+
+    // Setup a symlink to the target file.
+    let symlink_str = tempfile_path.to_string_lossy() + "_symlink";
     let symlink = PathBuf::from(symlink_str.as_ref());
-    unixfs::symlink(&tempfile, &symlink).unwrap();
-    drop(tempfile);
+    unixfs::symlink(&tempfile_path, &symlink).unwrap();
+
+    // Remove the target, breaking the symlink.
+    fs::remove_file(&tempfile_path).unwrap();
     assert!(!tempfile_path.exists());
 
     // Sandbox exception fails with invalid path error.
diff --git a/integration/fs_null.rs b/integration/fs_null.rs
index 07a7db4..040d59a 100644
--- a/integration/fs_null.rs
+++ b/integration/fs_null.rs
@@ -1,10 +1,11 @@
+use std::path::PathBuf;
 use std::fs;
 
 use birdcage::{Birdcage, Exception, Sandbox};
 
 use crate::TestSetup;
 
-pub fn setup() -> TestSetup {
+pub fn setup(_tempdir: PathBuf) -> TestSetup {
     // Activate our sandbox.
     let mut sandbox = Birdcage::new();
     sandbox.add_exception(Exception::WriteAndRead("/dev/null".into())).unwrap();
diff --git a/integration/fs_readonly.rs b/integration/fs_readonly.rs
index fecc4ab..e7e3e10 100644
--- a/integration/fs_readonly.rs
+++ b/integration/fs_readonly.rs
@@ -3,7 +3,6 @@ use std::path::PathBuf;
 
 use birdcage::{Birdcage, Exception, Sandbox};
 use serde::{Deserialize, Serialize};
-use tempfile::NamedTempFile;
 
 use crate::TestSetup;
 
@@ -14,9 +13,9 @@ struct TestData {
     file: PathBuf,
 }
 
-pub fn setup() -> TestSetup {
+pub fn setup(tempdir: PathBuf) -> TestSetup {
     // Setup the test file.
-    let file = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap();
+    let file = tempdir.join("fs_readonly");
     fs::write(&file, FILE_CONTENT.as_bytes()).unwrap();
 
     // Activate our sandbox.
diff --git a/integration/fs_restrict_child.rs b/integration/fs_restrict_child.rs
index d5811f2..516f9c3 100644
--- a/integration/fs_restrict_child.rs
+++ b/integration/fs_restrict_child.rs
@@ -14,9 +14,8 @@ struct TestData {
     tempdir: PathBuf,
 }
 
-pub fn setup() -> TestSetup {
+pub fn setup(tempdir: PathBuf) -> TestSetup {
     // Setup our test tree.
-    let tempdir = tempfile::tempdir().unwrap().into_path();
     let tempfile = tempdir.join("target-file");
     fs::write(&tempfile, FILE_CONTENT.as_bytes()).unwrap();
 
diff --git a/integration/fs_symlink.rs b/integration/fs_symlink.rs
index 034fb68..23408eb 100644
--- a/integration/fs_symlink.rs
+++ b/integration/fs_symlink.rs
@@ -4,7 +4,6 @@ use std::path::PathBuf;
 
 use birdcage::{Birdcage, Exception, Sandbox};
 use serde::{Deserialize, Serialize};
-use tempfile::NamedTempFile;
 
 use crate::TestSetup;
 
@@ -16,11 +15,11 @@ struct TestData {
     public: PathBuf,
 }
 
-pub fn setup() -> TestSetup {
+pub fn setup(tempdir: PathBuf) -> TestSetup {
     // Setup our test files.
-    let private_path = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap();
+    let private_path = tempdir.join("private");
     fs::write(&private_path, FILE_CONTENT.as_bytes()).unwrap();
-    let public_path = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap();
+    let public_path = tempdir.join("public");
     fs::write(&public_path, FILE_CONTENT.as_bytes()).unwrap();
 
     // Create symlinks for the files.
diff --git a/integration/fs_symlink_dir.rs b/integration/fs_symlink_dir.rs
index 5ec06f4..7b35a12 100644
--- a/integration/fs_symlink_dir.rs
+++ b/integration/fs_symlink_dir.rs
@@ -4,7 +4,6 @@ use std::path::PathBuf;
 
 use birdcage::{Birdcage, Exception, Sandbox};
 use serde::{Deserialize, Serialize};
-use tempfile::TempDir;
 
 use crate::TestSetup;
 
@@ -12,22 +11,22 @@ const FILE_CONTENT: &str = "expected content";
 
 #[derive(Serialize, Deserialize)]
 struct TestData {
-    symlink_path: PathBuf,
+    symlink: PathBuf,
 }
 
-pub fn setup() -> TestSetup {
+pub fn setup(tempdir: PathBuf) -> TestSetup {
     // Setup our test directory.
-    let tempdir = TempDir::new().unwrap().into_path();
-    let symlink_str = tempdir.to_string_lossy() + "_tmpfile";
-    let symlink_path = PathBuf::from(symlink_str.as_ref());
-    unixfs::symlink(&tempdir, &symlink_path).unwrap();
+    let symlink_target = tempdir.join("target");
+    fs::create_dir(symlink_target).unwrap();
+    let symlink = tempdir.join("symlink");
+    unixfs::symlink(&tempdir, &symlink).unwrap();
 
     // Activate our sandbox.
     let mut sandbox = Birdcage::new();
-    sandbox.add_exception(Exception::WriteAndRead(symlink_path.clone())).unwrap();
+    sandbox.add_exception(Exception::WriteAndRead(symlink.clone())).unwrap();
 
     // Serialize test data.
-    let data = TestData { symlink_path };
+    let data = TestData { symlink };
     let data = serde_json::to_string(&data).unwrap();
 
     TestSetup { sandbox, data }
@@ -38,7 +37,7 @@ pub fn validate(data: String) {
     let data: TestData = serde_json::from_str(&data).unwrap();
 
     // Try to create a file in the symlinked directory.
-    let path = data.symlink_path.join("tmpfile");
+    let path = data.symlink.join("tmpfile");
     fs::write(&path, FILE_CONTENT.as_bytes()).unwrap();
     let content = fs::read_to_string(&path).unwrap();
     assert_eq!(content, FILE_CONTENT);
diff --git a/integration/fs_symlink_dir_separate_perms.rs b/integration/fs_symlink_dir_separate_perms.rs
index b33eb5f..98fa79c 100644
--- a/integration/fs_symlink_dir_separate_perms.rs
+++ b/integration/fs_symlink_dir_separate_perms.rs
@@ -4,7 +4,6 @@ use std::path::PathBuf;
 
 use birdcage::{Birdcage, Exception, Sandbox};
 use serde::{Deserialize, Serialize};
-use tempfile::TempDir;
 
 use crate::TestSetup;
 
@@ -15,9 +14,8 @@ struct TestData {
     symlink_src: PathBuf,
 }
 
-pub fn setup() -> TestSetup {
+pub fn setup(tempdir: PathBuf) -> TestSetup {
     // Setup our test directories.
-    let tempdir = TempDir::new().unwrap().into_path();
     let symlink_src = tempdir.join("src");
     fs::create_dir(&symlink_src).unwrap();
     let symlink_dst = tempdir.join("dst");
diff --git a/integration/fs_write_also_read.rs b/integration/fs_write_also_read.rs
index 00e46ad..4d53443 100644
--- a/integration/fs_write_also_read.rs
+++ b/integration/fs_write_also_read.rs
@@ -1,9 +1,8 @@
-use std::fs;
+use std::fs::{self, File};
 use std::path::PathBuf;
 
 use birdcage::{Birdcage, Exception, Sandbox};
 use serde::{Deserialize, Serialize};
-use tempfile::NamedTempFile;
 
 use crate::TestSetup;
 
@@ -11,19 +10,20 @@ const FILE_CONTENT: &str = "expected content";
 
 #[derive(Serialize, Deserialize)]
 struct TestData {
-    file: PathBuf,
+    path: PathBuf,
 }
 
-pub fn setup() -> TestSetup {
+pub fn setup(tempdir: PathBuf) -> TestSetup {
     // Setup our test files.
-    let file = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap();
+    let path = tempdir.join("fs_write_also_read");
+    File::create(&path).unwrap();
 
     // Activate our sandbox.
     let mut sandbox = Birdcage::new();
-    sandbox.add_exception(Exception::WriteAndRead(file.clone())).unwrap();
+    sandbox.add_exception(Exception::WriteAndRead(path.clone())).unwrap();
 
     // Serialize test data.
-    let data = TestData { file };
+    let data = TestData { path };
     let data = serde_json::to_string(&data).unwrap();
 
     TestSetup { sandbox, data }
@@ -34,9 +34,9 @@ pub fn validate(data: String) {
     let data: TestData = serde_json::from_str(&data).unwrap();
 
     // Write access is allowed.
-    fs::write(&data.file, FILE_CONTENT.as_bytes()).unwrap();
+    fs::write(&data.path, FILE_CONTENT.as_bytes()).unwrap();
 
     // Read access is allowed.
-    let content = fs::read_to_string(data.file).unwrap();
+    let content = fs::read_to_string(data.path).unwrap();
     assert_eq!(content, FILE_CONTENT);
 }
diff --git a/integration/full_env.rs b/integration/full_env.rs
index 3347895..a9372e0 100644
--- a/integration/full_env.rs
+++ b/integration/full_env.rs
@@ -1,10 +1,11 @@
+use std::path::PathBuf;
 use std::env;
 
 use birdcage::{Birdcage, Exception, Sandbox};
 
 use crate::TestSetup;
 
-pub fn setup() -> TestSetup {
+pub fn setup(_tempdir: PathBuf) -> TestSetup {
     // Setup our environment variables
     env::set_var("PUBLIC", "GOOD");
 
diff --git a/integration/full_sandbox.rs b/integration/full_sandbox.rs
index 286b19d..b83038c 100644
--- a/integration/full_sandbox.rs
+++ b/integration/full_sandbox.rs
@@ -5,7 +5,6 @@ use std::{env, fs};
 
 use birdcage::{Birdcage, Sandbox};
 use serde::{Deserialize, Serialize};
-use tempfile::NamedTempFile;
 
 use crate::TestSetup;
 
@@ -14,11 +13,11 @@ struct TestData {
     path: PathBuf,
 }
 
-pub fn setup() -> TestSetup {
+pub fn setup(tempdir: PathBuf) -> TestSetup {
     const FILE_CONTENT: &str = "expected content";
 
     // Create testfile.
-    let path = NamedTempFile::new().unwrap().into_temp_path().keep().unwrap();
+    let path = tempdir.join("full_sandbox");
 
     // Ensure non-sandboxed write works.
     fs::write(&path, FILE_CONTENT.as_bytes()).unwrap();
diff --git a/integration/harness.rs b/integration/harness.rs
index 69388bb..4a40405 100644
--- a/integration/harness.rs
+++ b/integration/harness.rs
@@ -1,3 +1,4 @@
+use std::path::PathBuf;
 use std::process::{self, Command, Stdio};
 
 use birdcage::{Birdcage, Exception, Sandbox};
@@ -57,9 +58,13 @@ fn main() {
     };
 
     // Run setup or test validation.
-    match args.next() {
-        Some(test_data) => test.2(test_data),
-        None => run_setup(&test_name, &test.1),
+    let arg = args.next().unwrap();
+    match arg.as_str() {
+        "--setup" => {
+            let tempdir = args.next().unwrap();
+            run_setup(&test_name, tempdir, &test.1);
+        },
+        _ => test.2(arg),
     }
 }
 
@@ -71,18 +76,22 @@ fn spawn_tests() {
 
     // Spawn child processes for all tests.
     let current_exe = std::env::current_exe().unwrap();
-    let children: Vec<_> = TESTS
-        .iter()
-        .map(|(cmd, ..)| {
-            let child =
-                Command::new(&current_exe).args([cmd]).stderr(Stdio::piped()).spawn().unwrap();
-            (cmd, child)
-        })
-        .collect();
+    let mut children = Vec::new();
+    for (cmd, ..) in TESTS {
+        let tempdir = tempfile::tempdir().unwrap();
+        let child = Command::new(&current_exe)
+            .arg(cmd)
+            .arg("--setup")
+            .arg(tempdir.path())
+            .stderr(Stdio::piped())
+            .spawn()
+            .unwrap();
+        children.push((cmd, child, tempdir));
+    }
 
     // Check results for each test.
     let mut passed = 0;
-    for (name, child) in children {
+    for (name, child, tempdir) in children {
         let output = match child.wait_with_output() {
             Ok(output) => output,
             Err(err) => {
@@ -104,6 +113,9 @@ fn spawn_tests() {
             eprintln!("test {TEST_DIR}/{name}.rs ... \x1b[32mok\x1b[0m");
             passed += 1;
         }
+
+        // Cleanup tempdir.
+        tempdir.close().unwrap();
     }
 
     // Print total results.
@@ -118,9 +130,9 @@ fn spawn_tests() {
 }
 
 /// Run test's setup step and spawn validation child.
-fn run_setup(test_name: &str, setup: &fn() -> TestSetup) {
+fn run_setup(test_name: &str, tempdir: String, setup: &fn(PathBuf) -> TestSetup) {
     // Run test setup.
-    let mut test_setup = setup();
+    let mut test_setup = setup(PathBuf::from(tempdir));
 
     // Add exceptions to allow self-execution.
     let current_exe = std::env::current_exe().unwrap();
@@ -150,7 +162,7 @@ macro_rules! test_mods {
             mod $mod;
         )*
 
-        const TESTS: &[(&str, fn() -> $crate::TestSetup, fn(String))] = &[$(
+        const TESTS: &[(&str, fn(std::path::PathBuf) -> $crate::TestSetup, fn(String))] = &[$(
             $( #[$cfg] )?
             (stringify!($mod), $mod :: setup, $mod :: validate),
         )*];
diff --git a/integration/missing_exception.rs b/integration/missing_exception.rs
index a0ce814..bee797d 100644
--- a/integration/missing_exception.rs
+++ b/integration/missing_exception.rs
@@ -5,7 +5,7 @@ use birdcage::{Birdcage, Exception, Sandbox};
 
 use crate::TestSetup;
 
-pub fn setup() -> TestSetup {
+pub fn setup(_tempdir: PathBuf) -> TestSetup {
     let mut sandbox = Birdcage::new();
 
     // Add a path that doesn't exist.
diff --git a/integration/net.rs b/integration/net.rs
index 19516e7..4b925e0 100644
--- a/integration/net.rs
+++ b/integration/net.rs
@@ -1,10 +1,11 @@
+use std::path::PathBuf;
 use std::net::TcpStream;
 
 use birdcage::{Birdcage, Exception, Sandbox};
 
 use crate::TestSetup;
 
-pub fn setup() -> TestSetup {
+pub fn setup(_tempdir: PathBuf) -> TestSetup {
     // Setup sandbox exceptions.
     let mut sandbox = Birdcage::new();
     sandbox.add_exception(Exception::Networking).unwrap();
diff --git a/integration/seccomp.rs b/integration/seccomp.rs
index 32d209e..3e3d78d 100644
--- a/integration/seccomp.rs
+++ b/integration/seccomp.rs
@@ -1,10 +1,11 @@
+use std::path::PathBuf;
 use std::ffi::CString;
 
 use birdcage::{Birdcage, Sandbox};
 
 use crate::TestSetup;
 
-pub fn setup() -> TestSetup {
+pub fn setup(_tempdir: PathBuf) -> TestSetup {
     TestSetup { sandbox: Birdcage::new(), data: String::new() }
 }
 
diff --git a/src/lib.rs b/src/lib.rs
index 2c8695e..d30a575 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -10,13 +10,9 @@
 //! use std::process::Command;
 //!
 //! use birdcage::{Birdcage, Exception, Sandbox};
-//! use tempfile::NamedTempFile;
-//!
-//! // Setup our test file.
-//! let file = NamedTempFile::new().unwrap();
 //!
 //! // Reads without sandbox work.
-//! fs::read_to_string(file.path()).unwrap();
+//! fs::read_to_string("./Cargo.toml").unwrap();
 //!
 //! // Allow access to our test executable.
 //! let mut sandbox = Birdcage::new();
@@ -26,7 +22,7 @@
 //!
 //! // Initialize the sandbox; by default everything is prohibited.
 //! let mut command = Command::new("/bin/cat");
-//! command.arg(file.path());
+//! command.arg("./Cargo.toml");
 //! let mut child = sandbox.spawn(command).unwrap();
 //!
 //! // Reads with sandbox should fail.


Test patch: