Skip to content

cli: harden clean and Nix provenance execution - #102

Merged
noctem-o merged 1 commit into
mainfrom
codex/implement-cli/process-hardening-features
May 16, 2026
Merged

noctem-o merged 1 commit into
mainfrom
codex/implement-cli/process-hardening-features

Conversation

@noctem-o

Copy link
Copy Markdown
Owner

Motivation

  • Prevent a local CLI footgun where --clean could recursively delete an important path (root, home, repo, .git, symlinks) and make Nix provenance collection safe from unbounded/hanging external commands.
  • Provide pragmatic, std-only hardening so provenance remains diagnostic-only and no witness/verifier semantics change.

Description

  • Added a centralized safe_clean_output_dir(path: &Path, label: &str) -> Result<()> and validate_clean_output_dir(path: &Path, repo_root: &Path) -> Result<()>, and replaced direct fs::remove_dir_all call sites for all --clean paths with the helper in src/main.rs.
  • validate_clean_output_dir rejects empty paths, filesystem root, . (current dir), repository root when detectable, user home (from HOME/USERPROFILE), final-component .git, and symlinked output directories; normal output dirs like out/demo_out and nested project dirs remain allowed.
  • Hardened Nix provenance execution in src/nix_provenance.rs by introducing run_bounded_command(...) which uses closed stdin (Stdio::null()), a timeout (COMMAND_TIMEOUT = 3s), bounded stdout/stderr capture with truncation to MAX_OUTPUT_BYTES = 128 * 1024, and environment scrubbing via env_remove for selected proxy/credential-like variables (e.g., HTTP_PROXY/HTTPS_PROXY variants, ALL_PROXY, NO_PROXY, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, GITHUB_TOKEN, NIX_CONFIG).
  • Preserved original provenance semantics and default --nix-provenance=auto behavior while documenting that auto|on may run bounded local Nix commands for diagnostic provenance only.
  • Added unit tests for safe/unsafe clean paths and for the bounded-command timeout behavior, and updated CLI help strings and README to note the --clean safety guard and provenance execution note.
  • Changed files: src/main.rs, src/nix_provenance.rs, README.md.

Testing

  • Unit tests and CI-style checks were run: cargo fmt --check (ok), cargo clippy --locked --all-targets -- -D warnings (ok), and cargo test --locked (ok) which included the new tests for clean-guard and bounded command behavior.
  • End-to-end validations passed: cargo run --locked -- ordeal check --golden goldens/ordeal_witness_root.txt (ok), cargo run --locked -- demo drift --seed 1 --threads 1 --fault-profile ci --out-dir demo_out --clean (ok), cargo run --locked -- verify --witness demo_out/drift/baseline_faults (ok), cargo run --locked -- verify --witness demo_out/drift/baseline_faults --recompute-witness-root (ok), and bash scripts/check_action_pins.sh (ok).
  • The new unit tests specifically cover: validate_clean_output_dir rejects dangerous paths and permits normal output dirs, symlink rejection on Unix, and bounded provenance command timeout behavior; all passed.

Non-goals (explicit): no changes to witness-root algorithm, schema constants, canonicalization, verifier semantics, release workflows, or production/provider integrations.


Codex Task

@noctem-o
noctem-o merged commit 487cb25 into main May 16, 2026
14 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2dc7c61784

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/nix_provenance.rs
Comment on lines +163 to +166
loop {
if child.try_wait()?.is_some() {
let mut output = child.wait_with_output()?;
truncate_output(&mut output.stdout);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Consume child output while polling for timeout

run_bounded_command polls try_wait() and only reads stdout/stderr via wait_with_output() after the child has exited. Since both streams are set to Stdio::piped(), a command that writes more than the OS pipe buffer before exiting can block on write, which means it never reaches an exited state and this loop times out after 3s. In practice this can make valid nix ... --json provenance calls fail spuriously on larger outputs (e.g., large flake metadata). The timeout path needs concurrent pipe draining (or non-piped output) to avoid this regression.

Useful? React with 👍 / 👎.

Comment thread src/main.rs
Comment on lines +1666 to +1668
if let Ok(metadata) = fs::symlink_metadata(path) {
if metadata.file_type().is_symlink() {
anyhow::bail!("refusing to clean symlink output directory");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Normalize clean path before symlink guard

The symlink check uses symlink_metadata(path) directly, which can be bypassed with a trailing separator (--out-dir link/): on Unix, symlink_metadata("link/") reports the target directory rather than the symlink, so this guard does not fire. safe_clean_output_dir then calls remove_dir_all on that path, which can delete contents of the symlink target before erroring, reintroducing the destructive --clean footgun for symlinked dirs.

Useful? React with 👍 / 👎.

Comment thread src/main.rs
}

fn safe_clean_output_dir(path: &Path, label: &str) -> Result<()> {
validate_clean_output_dir(path, Path::new("."))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve repo root before validating clean target

safe_clean_output_dir passes Path::new(".") as the repository root sentinel, so the guard only protects the current working directory, not the actual Git top-level directory. If the CLI is invoked from a subdirectory, --out-dir .. (or another path resolving to the repo root) can pass validation and be recursively removed, which defeats the intended repository-root protection.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant