Skip to content

Commit 2ad2906

Browse files
authored
Warn when a local process execution sandbox cannot be deleted (#23696)
## Problem `AsyncDropSandbox::drop` deletes a local sandbox by dropping its `TempDir`, and `tempfile`'s `Drop` ignores any removal error. A sandbox that contains a read-only directory is therefore left behind in the temporary directory with no message. #23636 is one case: an extracted Go module cache leaked about 700 sandboxes and 2.1 GB per cold run, and nothing reported it. @tdyas asked there for at least a warning. ## Fix Close the `TempDir` explicitly on the blocking cleanup task and log a warning with the sandbox path when that fails. Behavior is otherwise unchanged: cleanup still runs in the background and a failure does not fail the run. ## Verification Two unit tests in `local_tests.rs`: a normal sandbox is deleted, and a sandbox with a read-only subdirectory returns an error that names its path. End to end, a cold `check` of a Go module on main (without #23636) now logs the warning for every leaked download sandbox. Notice: Claude used for code drafting, tests and verification.
1 parent 630bac3 commit 2ad2906

3 files changed

Lines changed: 51 additions & 2 deletions

File tree

‎docs/notes/2.34.x.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ Fixed `system_binary` / PATH lookup crashing with `IntrinsicError: Operation not
4545

4646
Fixed pantsd logging remote Authentication bearer tokens in plaintext when `remote_store_headers` / `remote_execution_headers` / `remote_oauth_bearer_token` (or the matching `DynamicRemoteOptions` header fields) changed and the scheduler reinitialized. Those sensitive values are now shown as `<redacted>` in the reinitialization diff ([#23685](https://github.com/pantsbuild/pants/issues/23685)).
4747

48+
Pants now logs a warning when it cannot delete a local process execution sandbox. Cleanup failures were previously ignored, so sandboxes containing read-only directories leaked in the temporary directory without any message.
49+
4850
### Goals
4951

5052
### Backends

‎src/rust/process_execution/src/local.rs‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use fs::{
2222
use futures::stream::{BoxStream, StreamExt, TryStreamExt};
2323
use futures::{FutureExt, TryFutureExt, try_join};
2424
use hashing::Digest;
25-
use log::{debug, info};
25+
use log::{debug, info, warn};
2626
use nails::execution::ExitCode;
2727
use sandboxer::Sandboxer;
2828
use serde::Serialize;
@@ -826,11 +826,27 @@ impl AsyncDropSandbox {
826826
impl Drop for AsyncDropSandbox {
827827
fn drop(&mut self) {
828828
if let Some(sandbox) = self.2.take() {
829-
let _background_cleanup = self.0.spawn_blocking(|| std::mem::drop(sandbox));
829+
let _background_cleanup = self.0.spawn_blocking(move || {
830+
if let Err(e) = remove_sandbox(sandbox) {
831+
warn!("{e}");
832+
}
833+
});
830834
}
831835
}
832836
}
833837

838+
/// Delete a sandbox directory, reporting failure instead of silently leaking it (which is what
839+
/// dropping the `TempDir` does).
840+
pub(crate) fn remove_sandbox(sandbox: TempDir) -> Result<(), String> {
841+
let path = sandbox.path().to_owned();
842+
sandbox.close().map_err(|e| {
843+
format!(
844+
"Failed to delete local process execution dir {}: {e}",
845+
path.display()
846+
)
847+
})
848+
}
849+
834850
/// Create a file called __run.sh with the env, cwd and argv used by Pants to facilitate debugging.
835851
pub fn setup_run_sh_script(
836852
sandbox_path: &Path,

‎src/rust/process_execution/src/local_tests.rs‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,3 +804,34 @@ async fn run_command_locally_in_dir(
804804
fn one_second() -> Option<Duration> {
805805
Some(Duration::from_millis(1000))
806806
}
807+
808+
#[test]
809+
fn remove_sandbox_deletes_dir() {
810+
let sandbox = TempDir::new().unwrap();
811+
let path = sandbox.path().to_owned();
812+
std::fs::write(path.join("file"), "content").unwrap();
813+
814+
local::remove_sandbox(sandbox).unwrap();
815+
assert!(!path.exists());
816+
}
817+
818+
#[test]
819+
#[cfg(unix)]
820+
fn remove_sandbox_reports_undeletable_dir() {
821+
use std::os::unix::fs::PermissionsExt;
822+
823+
// A read-only directory, like the extracted Go module cache, cannot have its entries removed.
824+
let sandbox = TempDir::new().unwrap();
825+
let path = sandbox.path().to_owned();
826+
let read_only = path.join("read_only");
827+
std::fs::create_dir(&read_only).unwrap();
828+
std::fs::write(read_only.join("file"), "content").unwrap();
829+
std::fs::set_permissions(&read_only, std::fs::Permissions::from_mode(0o555)).unwrap();
830+
831+
let err = local::remove_sandbox(sandbox).unwrap_err();
832+
assert!(err.contains(&path.display().to_string()), "{err}");
833+
assert!(path.exists());
834+
835+
std::fs::set_permissions(&read_only, std::fs::Permissions::from_mode(0o755)).unwrap();
836+
std::fs::remove_dir_all(&path).unwrap();
837+
}

0 commit comments

Comments
 (0)