Skip to content

fix: validate materialized immutable_inputs paths to survive macOS te… - #23458

Open
thiago-carbonera wants to merge 2 commits into
pantsbuild:mainfrom
thiago-carbonera:fix/immutable-inputs-revalidation
Open

fix: validate materialized immutable_inputs paths to survive macOS te…#23458
thiago-carbonera wants to merge 2 commits into
pantsbuild:mainfrom
thiago-carbonera:fix/immutable-inputs-revalidation

Conversation

@thiago-carbonera

Copy link
Copy Markdown
Contributor

Fixes #23411

Problem

On macOS, a long-running pantsd instance can outlive the OS periodic temp cleaner (which reaps unaccessed files in $TMPDIR after ~3 days). ImmutableInputs::path_for_dir was memoizing paths in memory and returning them without verifying if the underlying directory still existed on disk, causing tools like Docker to fail with executable file not found in $PATH because their $PATH pointed to a deleted directory.

Solution

  • Updated path_for_dir to check path.exists() on the cached PathBuf.
  • If the path has been deleted, it invalidates the stale cache entry by replacing it with a new OnceCell and forces the directory to be re-materialized.
  • Added a regression test in local_tests.rs to simulate the exact OS reaper behavior.

@thiago-carbonera
thiago-carbonera force-pushed the fix/immutable-inputs-revalidation branch 3 times, most recently from 7ae8cba to 495f847 Compare June 30, 2026 17:57
@thiago-carbonera

Copy link
Copy Markdown
Contributor Author

@benjyw Could you review this?

@benjyw benjyw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix. Just a small comment about use style in the test.

Comment thread src/rust/fs/store/src/local_tests.rs Outdated

#[tokio::test]
async fn recovers_from_deleted_directory_issue_23411() {
use crate::{ImmutableInputs, Store};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our style is to have use statements at the file level, other than in exceptional circumstances.

) -> Result<PathBuf, StoreError> {
let digest = directory_digest.as_digest();
let cell = self.0.contents.lock().entry(digest).or_default().clone();
// we must verify the memoized path still exists on disk.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is still a race condition here: the OS might reap the tmpdir after it's read here and before it's used.

This is still a good change, as it also defends against a human deleting the dir, but to be truly robust against the macos shenanigans we would also want to not use $TMPDIR in the first place. We should probably write these under the pants cache dir.

But I think it's fine to do that in a followup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review, @benjyw!
I completely agree with the race condition. Moving these away from $TMPDIR and into the Pants cache dir seems like the most robust long-term solution against macOS shenanigans.
I've just pushed a commit moving the use statements to the file level as requested. Let me know if everything looks good to go now!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure about this, the race condition is more complicated than I first thought. For example, what if the parent dir is reaped? Then the reinitialization will always fail, no? I think TempDir::new_in() requires the parent dir to exist.

The issue is really that the default value of BootstrapOptions.local_execution_root_dir is a tempdir. So maybe that is all we need to fix, and put it under pants_workdir instead? That will be a simpler and more comprehensive fix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @benjyw, the CI caught an interesting side-effect of this architectural change.
The test test_mypyc_build failed. Because the sandboxes are now nested inside the workspace (.pants.d/workdir/local_execution/...), tools that recursively search parent directories for config files (like mypy) are breaking out of the sandbox.
Looking at the logs, mypy traversed up 9 directories (../../../../../../../../../pyproject.toml), hit the Pants repo's root pyproject.toml, and crashed trying to load mypy_typing_asserts.
Moving the execution root inside the workspace seems to break sandbox hermeticity for hierarchical config resolution. Should we reconsider the $TMPDIR cache invalidation in Rust, or is there another safe default outside the workspace we could use?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right, local_execution_root_dir is used for sandboxes as well as immutable_inputs. And we do generally want old sandboxes to be reaped I suppose.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so probably the original solution here is a decent best-effort. It will fail if the parent tempdir is reaped, but for that to happen there would have had to be no new writes at all under it in three days, which seems unlikely in normal use since all the sandboxes go there.

OK, so let's go back to your original plan.

@thiago-carbonera thiago-carbonera Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just reverted the Python changes and restored the original Rust validation approach (checking path.exists() before using the memoized OnceCell). Everything is back to the Rust implementation! Let me know if it looks good now.

@thiago-carbonera
thiago-carbonera force-pushed the fix/immutable-inputs-revalidation branch 2 times, most recently from 601a579 to 968ac95 Compare July 8, 2026 13:22
@cburroughs

Copy link
Copy Markdown
Contributor

Thanks for the contribution. We've just branched for 2.33.x, so merging this pull request now will come out in 2.34.x, please move the release notes updates to docs/notes/2.34.x.md if that's appropriate.

@thiago-carbonera
thiago-carbonera force-pushed the fix/immutable-inputs-revalidation branch from 5097ef1 to e2890cd Compare July 8, 2026 17:00
@thiago-carbonera

Copy link
Copy Markdown
Contributor Author

Done @cburroughs.

@benjyw

benjyw commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

@benjyw

benjyw commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

There is still a merge conflict

@thiago-carbonera
thiago-carbonera force-pushed the fix/immutable-inputs-revalidation branch from a3ab1ae to 4a7e191 Compare July 13, 2026 21:44
@thiago-carbonera
thiago-carbonera force-pushed the fix/immutable-inputs-revalidation branch from 4a7e191 to 7e04e5a Compare July 13, 2026 22:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants