//! A throwaway copy of a mission checkout, for commands that run as ROOT. //! //! Three places in this codebase run a real command against a mission's tree — //! the judge's verification (`evaluator_tools::Sandbox`), the benchmark runner, //! and the `on_green_tests` delivery gate. All three enter a container running as //! root with the missions root bind-mounted, and all three run something that //! writes `target/`. All three now go through here; the judge was the last to //! move, having carried its own copy of this logic since before it existed. //! //! Run against the live checkout, that breaks the single-writer invariant: the //! tree is owned by uid 65532 and now contains root-owned build output, so the //! next phase's `cargo` hits permission-denied on a directory it cannot write. //! The harness's uid probe reports it as `uids=0,65532`. //! //! # The cleanup half, which is the part that keeps being got wrong //! //! The copy inherits the same problem: its `target/` is root-owned, so the //! server process (uid 65532) **cannot delete it**. A `Drop` calling //! `std::fs::remove_dir_all` fails, and because that error is discarded the tree //! survives forever — measured at 1.2 MB per benchmark run and 16 MB of stranded //! judge sandboxes before this existed. //! //! So removal goes back through the container, as root, where the files were //! written. `Drop` remains only as a fallback for the paths where nothing has //! run as root yet, and does not pretend to be more. use std::path::{Path, PathBuf}; /// Where throwaway copies live: siblings of the per-mission directories, like /// `_outputs` and `_verify`, so reaping a mission cannot race a running command. pub fn copy_root(kind: &str, mission_id: uuid::Uuid) -> PathBuf { crate::mission_workspace::missions_root() .join(kind) .join(mission_id.to_string()) } /// Delete a copy from inside the container that wrote it. /// /// Best-effort and loud: a housekeeping failure must not cost a real verdict or /// a real benchmark, but it must not be silent either — silence is how the leaks /// this module exists for went unnoticed for a day. pub async fn purge(container: &str, root: &Path) { let Ok(docker) = crate::container_exec::connect() else { return; }; let argv = vec![ "rm".to_string(), "-rf".to_string(), root.display().to_string(), ]; // Explicitly root: this exists to delete files an EARLIER root-run exec // created, which uid 65532 cannot touch. Everything else now runs as 65532 // (see `container_exec`), so this is cleaning up history, not policy. if let Err(e) = crate::container_exec::exec_as_root( &docker, container, Some("/"), &argv, std::time::Duration::from_secs(120), ) .await { eprintln!( "root_copy: could not remove {} from {container}: {e}", root.display() ); } } /// A copy of a checkout, removed when it goes out of scope. pub struct RootCopy { root: PathBuf, workdir: PathBuf, } impl RootCopy { /// Copy `source` into `root`, returning a handle whose `workdir` is the tree /// to run in. /// /// Packed through `mission_fs::pack_dir`, so the copy carries exactly what a /// delivered diff carries — no `target/`, no `node_modules/`. One exclusion /// list, four consumers. pub fn of(source: &Path, root: &Path) -> Result { let archive = crate::mission_fs::pack_dir(source, "repo") .map_err(|e| format!("pack {} for a root-run command: {e}", source.display()))?; crate::mission_fs::unpack_into(&archive, root) .map_err(|e| format!("unpack copy into {}: {e}", root.display()))?; let workdir = root.join("repo"); if !workdir.is_dir() { return Err(format!("copy missing at {}", workdir.display())); } Ok(RootCopy { root: root.to_path_buf(), workdir, }) } pub fn workdir(&self) -> &Path { &self.workdir } /// Take the working directory and give up automatic cleanup. /// /// For a caller whose copy outlives this handle — `evaluator_tools::Sandbox` /// hands the path to a judge that has not run yet, so letting `Drop` fire on /// return would delete the tree out from under it. That caller becomes /// responsible for calling [`purge`], which is the only thing that can /// remove root-owned build output anyway. /// /// Spelled as a method rather than `mem::forget` at the call site, so the /// transfer of responsibility is visible in the type rather than implied by /// a leak. pub fn into_workdir(self) -> PathBuf { let workdir = self.workdir.clone(); std::mem::forget(self); workdir } } impl Drop for RootCopy { /// Fallback only. This CANNOT remove root-owned build output — see /// [`purge`], which is what actually clears a copy something has run in. fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.root); } } #[cfg(test)] mod tests { use super::*; /// A copy must be a SIBLING of the per-mission directory, never inside it: /// `teardown_container` removes `/` wholesale and /// would take a running command's tree with it. #[test] fn copies_live_beside_the_mission_directory_not_inside_it() { let mission = uuid::Uuid::now_v7(); let mission_dir = crate::mission_workspace::missions_root().join(mission.to_string()); for kind in ["_bench", "_gate", "_verify"] { let root = copy_root(kind, mission); assert!(!root.starts_with(&mission_dir), "{root:?}"); assert!( root.starts_with(crate::mission_workspace::missions_root().join(kind)), "{root:?}" ); } } /// The copy is not the checkout. Stated as a test because the whole defect /// class is "ran the real command against the real tree". #[test] fn a_copy_is_never_the_checkout() { let mission = uuid::Uuid::now_v7(); let live = crate::mission_workspace::checkout_path(mission); for kind in ["_bench", "_gate", "_verify"] { assert_ne!(copy_root(kind, mission).join("repo"), live); } } }