From eb8b6a7a86a875abed28483b3f9967d0d22bae86 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Sat, 24 Oct 2026 05:22:23 -0400 Subject: [PATCH] test: write fake executables from a child so parallel tests cannot make them busy Five lib fixtures wrote a script in-process and then exec'd it. A sibling test thread that forks during the write inherits the write descriptor until it execs, so the exec fails with ETXTBSY ("Text file busy"). Main 64096740 went red on two of them. test_exec::write_executable writes through a short-lived sh child, so this process never holds the descriptor and no fork can inherit it. Before: 12/200 full-lib runs failed at --test-threads=8 (four tests; the seeds one confirmed as Text file busy). After: 0/500. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF --- src/adapter.rs | 5 ++--- src/adapter/manifest.rs | 7 ++++--- src/apt_owned.rs | 5 ++--- src/dp_alias.rs | 7 ++----- src/lib.rs | 2 ++ src/release_update/rollback.rs | 10 +++------- src/test_exec.rs | 33 +++++++++++++++++++++++++++++++++ 7 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 src/test_exec.rs diff --git a/src/adapter.rs b/src/adapter.rs index 8d6ff14..e0d3fae 100644 --- a/src/adapter.rs +++ b/src/adapter.rs @@ -1899,12 +1899,11 @@ pub fn verify_member(name: &str) -> Result<()> { #[cfg(all(test, unix))] mod quipu_alias_layout_tests { use super::verify_quipu_alias; - use std::os::unix::fs::{symlink, PermissionsExt}; + use std::os::unix::fs::symlink; use std::{fs, path::Path}; fn script(path: &Path, version: &str) { - fs::write(path, format!("#!/bin/sh\necho '{version}'\n")).unwrap(); - fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap(); + crate::test_exec::write_executable(path, &format!("#!/bin/sh\necho '{version}'\n")); } #[test] diff --git a/src/adapter/manifest.rs b/src/adapter/manifest.rs index 2e16cb6..1d36436 100644 --- a/src/adapter/manifest.rs +++ b/src/adapter/manifest.rs @@ -862,7 +862,6 @@ mod release_verification_tests { #[cfg(all(test, unix))] mod seeds_identity_tests { use super::*; - use std::os::unix::fs::PermissionsExt; #[test] fn seeds_identity_accepts_both_editions_and_refuses_foreign_sd() { @@ -877,8 +876,10 @@ mod seeds_identity_tests { ("sd is an intuitive find and replace CLI", false), ("seeds is a different program", false), ] { - std::fs::write(&executable, format!("#!/bin/sh\nprintf '%s\\n' '{text}'\n")).unwrap(); - std::fs::set_permissions(&executable, std::fs::Permissions::from_mode(0o755)).unwrap(); + crate::test_exec::write_executable( + &executable, + &format!("#!/bin/sh\nprintf '%s\\n' '{text}'\n"), + ); assert_eq!(identify(&executable, program).is_ok(), accepted, "{text}"); } } diff --git a/src/apt_owned.rs b/src/apt_owned.rs index 83190e8..41fbef3 100644 --- a/src/apt_owned.rs +++ b/src/apt_owned.rs @@ -66,12 +66,11 @@ fn refuse_if_owned(tool: &str, path: &Path, owner: Option) -> anyhow::Re #[cfg(all(test, unix))] mod tests { use super::*; - use std::os::unix::fs::{symlink, PermissionsExt}; + use std::os::unix::fs::symlink; fn fake_dpkg_query(dir: &Path, body: &str) -> std::path::PathBuf { let path = dir.join("dpkg-query"); - fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap(); - fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap(); + crate::test_exec::write_executable(&path, &format!("#!/bin/sh\n{body}\n")); path } diff --git a/src/dp_alias.rs b/src/dp_alias.rs index f4013ae..113856a 100644 --- a/src/dp_alias.rs +++ b/src/dp_alias.rs @@ -207,9 +207,8 @@ mod tests { /// first: delete that read and `keeps_a_different_local_target` fails. #[cfg(unix)] fn fake_dp(dir: &Path) -> PathBuf { - use std::os::unix::fs::PermissionsExt; let script = dir.join("dp"); - std::fs::write( + crate::test_exec::write_executable( &script, r#"#!/bin/sh store="$(dirname "$0")/bd.rule" @@ -229,9 +228,7 @@ case "$1" in *) exit 2 ;; esac "#, - ) - .unwrap(); - std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap(); + ); script } diff --git a/src/lib.rs b/src/lib.rs index 7ba1042..34dacc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,4 +31,6 @@ mod quipu_auth; mod quipu_credential; #[cfg(unix)] pub mod quipu_mcp; +#[cfg(all(test, unix))] +mod test_exec; pub mod vocabulary; diff --git a/src/release_update/rollback.rs b/src/release_update/rollback.rs index 0a8e22f..a98144a 100644 --- a/src/release_update/rollback.rs +++ b/src/release_update/rollback.rs @@ -185,7 +185,6 @@ fn rollback_at( #[cfg(test)] mod tests { use super::*; - use std::os::unix::fs::PermissionsExt; fn fixture() -> (tempfile::TempDir, Plan, PathBuf, PathBuf, String, String) { let dir = tempfile::tempdir().unwrap(); let state_path = dir.path().join("state.json"); @@ -213,10 +212,8 @@ mod tests { }; let destination = dir.path().join("sd"); let prior = dir.path().join("prior"); - fs::write(&destination, script("0.2.2")).unwrap(); - fs::write(&prior, script("0.2.0")).unwrap(); - fs::set_permissions(&destination, fs::Permissions::from_mode(0o755)).unwrap(); - fs::set_permissions(&prior, fs::Permissions::from_mode(0o755)).unwrap(); + crate::test_exec::write_executable(&destination, &script("0.2.2")); + crate::test_exec::write_executable(&prior, &script("0.2.0")); let current_hash = hash(&destination).unwrap(); let prior_hash = hash(&prior).unwrap(); let backup = dir.path().join("release-backups/sd").join(&prior_hash); @@ -260,8 +257,7 @@ mod tests { fn failed_functional_proof_restores_current_bytes_and_state() { let (dir, plan, state, destination, current, _prior) = fixture(); let prior = dir.path().join("broken"); - fs::write(&prior, "#!/bin/sh\ncase \"$1\" in --help) echo 'seeds is the beads-compatible tracker';; --version) echo 'sd 0.2.0 (seeds)';; *) exit 7;; esac\n").unwrap(); - fs::set_permissions(&prior, fs::Permissions::from_mode(0o755)).unwrap(); + crate::test_exec::write_executable(&prior, "#!/bin/sh\ncase \"$1\" in --help) echo 'seeds is the beads-compatible tracker';; --version) echo 'sd 0.2.0 (seeds)';; *) exit 7;; esac\n"); let prior_hash = hash(&prior).unwrap(); let backup = dir.path().join("release-backups/sd").join(&prior_hash); atomic_copy(&prior, &backup).unwrap(); diff --git a/src/test_exec.rs b/src/test_exec.rs new file mode 100644 index 0000000..23ef029 --- /dev/null +++ b/src/test_exec.rs @@ -0,0 +1,33 @@ +//! Test-only: write a fake executable that a parallel test cannot make busy. +//! +//! Parallel tests write scripts and then exec them. If any sibling thread forks +//! while this process holds the script open for writing, the forked child +//! inherits that descriptor until it calls exec (`O_CLOEXEC` does not close the +//! window between fork and exec). An exec of the script in that window fails +//! with ETXTBSY, "Text file busy" (aegis-zke). +//! +//! Writing through a short-lived `sh` child means the write descriptor only +//! ever exists in that child. This process never holds it, so no sibling fork +//! can inherit it, and once the child has exited the file has no writer. + +use std::io::Write; +use std::path::Path; +use std::process::{Command, Stdio}; + +/// Write `contents` to `path` with mode 0755, from a child process. +pub(crate) fn write_executable(path: &Path, contents: &str) { + let mut child = Command::new("/bin/sh") + .args(["-c", "cat > \"$1\" && chmod 0755 \"$1\"", "sh"]) + .arg(path) + .stdin(Stdio::piped()) + .spawn() + .expect("spawn sh to write a fake executable"); + child + .stdin + .take() + .expect("piped stdin") + .write_all(contents.as_bytes()) + .expect("write fake executable contents"); + let status = child.wait().expect("wait for the fake executable writer"); + assert!(status.success(), "writing {} failed", path.display()); +}