Skip to content

fix: destructive-boundary guard for clear-cache#198

Merged
wyli merged 3 commits into
mainfrom
fix/clear-cache-safety-boundary
Jul 8, 2026
Merged

fix: destructive-boundary guard for clear-cache#198
wyli merged 3 commits into
mainfrom
fix/clear-cache-safety-boundary

Conversation

@wyli

@wyli wyli commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

The cache roots are user-overridable (HOLOSCAN_CLI_BUILD_PARENT_DIR / HOLOSCAN_CLI_DATA_DIR) and clear-cache passed them straight to shutil.rmtree():

HOLOSCAN_CLI_BUILD_PARENT_DIR=/ ./holohub clear-cache --build --dryrun
  Would remove: /

Separately, test --clear-cache forwarded the raw test namespace, which clear-cache treats as "clear everything" — deleting the downloaded data cache instead of only build/install artifacts.

Fix

  • clear-cache canonicalizes each candidate (new holoscan_cli.utils.io.resolve()) and refuses to remove /, $HOME, the repo root, an ancestor of any of these, or anything outside the configured cache roots. Refused paths print Refusing to remove: <path> and are skipped. This is a backstop against the worst targets, not full validation of arbitrary overrides.
  • test --clear-cache now selects build/install explicitly; data is preserved.
  • Unit tests: refused anchors, unresolvable $HOME tolerated, and the build/install-only selection.

Testing

pytest tests/unit passes; CI green on Python 3.10–3.13.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@wyli
wyli force-pushed the fix/clear-cache-safety-boundary branch 2 times, most recently from 8d6707e to 6635a5e Compare July 4, 2026 19:30
`clear-cache` fed shutil.rmtree() with directories derived from
env-overridable roots (HOLOSCAN_CLI_BUILD_PARENT_DIR / _DATA_DIR) and
repo-root globs, with no safety boundary. A hostile or fat-fingered
value such as `HOLOSCAN_CLI_BUILD_PARENT_DIR=/` made the command report
`Would remove: /` and, without --dryrun under root, would wipe the
filesystem.

Add `_is_safe_to_remove()`: canonicalize each candidate and refuse it
unless it is (1) not a critical anchor (`/`, $HOME, the repo root) or an
ancestor of one, and (2) at or under an approved cache root (the repo
tree, the build parent dir, or the data dir). Refused paths print a
`Refusing to remove:` notice and are skipped.

Also fix the policy regression where `test --clear-cache` forwarded the
test namespace directly: with build/data/install unset, clear-cache
treated it as "clear everything" including downloaded data. Select
build/install explicitly, matching historical HoloHub behavior.

Add destructive-boundary tests covering /, $HOME, the repo root, an
ancestor of the repo root, the happy path, and data preservation on
`test --clear-cache`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wyli
wyli force-pushed the fix/clear-cache-safety-boundary branch from 6635a5e to a89374f Compare July 4, 2026 19:38
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds path-safety checks to holoscan clear-cache, canonicalizes candidate paths before deletion, and changes test_cmd’s --clear-cache path to pass an explicit cache-selection Namespace. Tests cover unsafe roots, unresolvable home handling, and the new invocation shape.

Changes

Clear-cache path-safety guardrails

Layer / File(s) Summary
Path-safety helpers and removal loop
src/holoscan_cli/utils/io.py, src/holoscan_cli/commands/clear_cache.py
Adds a resolve(path) helper for canonical paths, uses it to block deletion of protected anchors and their ancestors, and updates the removal loop to skip missing or non-directory candidates, refuse unsafe paths, and preserve dry-run versus removal behavior for safe ones.

test_cmd --clear-cache Namespace wiring

Layer / File(s) Summary
test_cmd --clear-cache Namespace wiring
src/holoscan_cli/commands/test_cmd.py, tests/unit/test_cli_behaviors.py
Changes handle_test’s --clear-cache branch to build a dedicated Namespace with build and install enabled, data disabled, and dryrun preserved, then adds tests covering unsafe root refusal, unresolvable home handling, and Namespace selection.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a destructive-boundary guard to clear-cache.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

🧹 Nitpick comments (4)
tests/unit/test_cli_behaviors.py (1)

400-405: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Bare except Exception: pass flagged by Ruff (S110/BLE001).

Consider narrowing the caught exception type or at least logging/asserting on it so a real regression in handle_test (unrelated to the intentionally-incomplete namespace) doesn't silently pass the test.

♻️ Proposed tweak
     try:
         test_cmd.handle_test(cli, args)
-    except Exception:
+    except Exception as exc:  # noqa: BLE001 - namespace intentionally incomplete past clear-cache branch
         # Downstream steps may need more of the namespace than this focused
         # test supplies; the clear-cache selection is already captured.
-        pass
+        assert "clear_cache" not in str(exc).lower()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_cli_behaviors.py` around lines 400 - 405, The test in
test_cli_behaviors around test_cmd.handle_test is swallowing all exceptions with
a bare except Exception: pass, which hides real failures. Narrow the exception
handling to the specific expected incomplete-namespace error for this scenario,
or assert/log the exception before ignoring it, so handle_test regressions are
not silently masked.

Source: Linters/SAST tools

.github/workflows/main.yaml (1)

132-170: 🩺 Stability & Availability | 🔵 Trivial

Gating build on an external repo's test suite adds a flakiness/availability risk to the release path.

build now requires holohub-integration to succeed, which checks out and runs nvidia-holoscan/holohub's own wrapper test suite. If that upstream repo is unavailable, has an unrelated breakage, or its test suite regresses independently of this repo, every push/PR here gets blocked from producing a build artifact even though holoscan-cli itself is fine. Consider whether this reverse-integration check should be continue-on-error or otherwise decoupled from the release-critical path, or whether it should only be required on main/release/*.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/main.yaml around lines 132 - 170, Decouple the external
HoloHub reverse-integration check from the release-critical build path in the
holohub-integration job and the build job’s needs list. Keep the HoloHub wrapper
test run in main.yaml, but make it non-blocking for artifact production by using
continue-on-error, moving it out of build dependencies, or scoping it to
main/release workflows only. Reference the holohub-integration job and the build
job dependency chain when updating the workflow.
.github/workflows/release.yaml (1)

253-296: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider extracting shared smoke-test steps to reduce cross-workflow duplication.

The wheel/create-extra/sdist smoke sequence here is now duplicated verbatim between main.yaml and release.yaml. A composite action would keep both in sync going forward, though this is optional given the current size of the duplication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yaml around lines 253 - 296, The smoke-test
sequence in the release workflow duplicates the same wheel, create-extra, and
sdist validation steps already present elsewhere, so refactor those repeated
steps into a shared composite action or reusable workflow. Update the smoke-test
job in release.yaml to call the shared unit identified by the current
install/test steps (for example, the wheel install, Validate create extra from
wheel, Install sdist in clean venv, and Source-distribution smoke test blocks)
so both workflows stay in sync with one implementation.
src/holoscan_cli/commands/run.py (1)

44-45: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Inconsistent quoting for --build-with vs --configure-args.

_local_build_command uses shlex.quote() for --configure-args (line 55) but naive double-quote interpolation for --build-with. If args.with_operators ever contains a ", it would break the generated command string executed via bash -c in the builder container. This mirrors a pre-existing pattern elsewhere in the file, so it's not a regression, but worth tightening while touching this code.

♻️ Suggested fix
     if getattr(args, "with_operators", None):
-        command += f' --build-with "{args.with_operators}"'
+        command += f" --build-with {shlex.quote(args.with_operators)}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/holoscan_cli/commands/run.py` around lines 44 - 45, The
_local_build_command logic in run.py builds `--build-with` using naive
double-quote interpolation, unlike the `--configure-args` handling, which can
break the bash command when `args.with_operators` contains quotes or other
shell-sensitive characters. Update the command construction in
`_local_build_command` to quote `args.with_operators` safely using the same
approach as `--configure-args` (for example via shlex.quote) so the generated
string remains valid when executed by bash -c.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/main.yaml:
- Around line 134-165: The holohub-integration job is missing an explicit
permissions block and currently inherits broader default GITHUB_TOKEN access
than needed. Add a permissions setting for that job with contents: read only,
keeping the rest of the job steps unchanged and using the holohub-integration
job name as the anchor for the update.
- Around line 138-147: Both checkout steps in the workflow job are leaving git
credentials persisted by default, which is unnecessary here. Update the two
actions/checkout usages in the main workflow job to explicitly disable
credential persistence by setting persist-credentials to false for both the
holoscan-cli checkout and the HoloHub checkout, since no later step in this job
needs push or tag access.

In `@src/holoscan_cli/commands/clear_cache.py`:
- Around line 46-48: Guard the anchor resolution in clear_cache by handling
Path.home() as a best-effort lookup instead of assuming it always succeeds. In
the clear_cache flow around _resolve and the anchors set, catch the RuntimeError
from Path.home() and omit that anchor when the home directory cannot be
resolved, while still building anchors from _resolve("/") and
_resolve(cli.HOLOHUB_ROOT). This keeps clear-cache from aborting before the
removal checks run.

In `@src/holoscan_cli/commands/setup_cmd.py`:
- Around line 49-56: Always clear PYTHONHOME in the environment builder for
setup scripts, since the current _build_env logic in setup_cmd.py only removes
it inside the virtualenv branch and can leave a stale value behind under system
Python. Update the _build_env function so PYTHONHOME is removed unconditionally
from the copied env before returning it, while keeping the existing PATH and
VIRTUAL_ENV handling in place.

In `@src/holoscan_cli/utils/host_setup.py`:
- Around line 229-235: The apt-key pipeline in the host setup logic uses
PATH-based lookups for wget and gpg, which should be replaced with absolute
executable paths. Update the subprocess calls in the key download/dearmor flow
within the host setup routine so the commands reference fixed binaries directly
while preserving the existing pipeline behavior. Use the existing subprocess.run
calls around the kitware key retrieval and dearmor steps as the place to make
this change.

In `@src/holoscan_cli/utils/io.py`:
- Around line 233-240: The environment-preservation logic in the sudo command
builder is too permissive because the helper currently adds sudo -E alongside
the explicit NAME=value allowlist. Update the command construction in the io
utility so only the whitelisted variables from preserve_env are passed through
via /usr/bin/env, and remove the -E flag from both the execution and display
prefixes while keeping the existing allowlist loop intact.

---

Nitpick comments:
In @.github/workflows/main.yaml:
- Around line 132-170: Decouple the external HoloHub reverse-integration check
from the release-critical build path in the holohub-integration job and the
build job’s needs list. Keep the HoloHub wrapper test run in main.yaml, but make
it non-blocking for artifact production by using continue-on-error, moving it
out of build dependencies, or scoping it to main/release workflows only.
Reference the holohub-integration job and the build job dependency chain when
updating the workflow.

In @.github/workflows/release.yaml:
- Around line 253-296: The smoke-test sequence in the release workflow
duplicates the same wheel, create-extra, and sdist validation steps already
present elsewhere, so refactor those repeated steps into a shared composite
action or reusable workflow. Update the smoke-test job in release.yaml to call
the shared unit identified by the current install/test steps (for example, the
wheel install, Validate create extra from wheel, Install sdist in clean venv,
and Source-distribution smoke test blocks) so both workflows stay in sync with
one implementation.

In `@src/holoscan_cli/commands/run.py`:
- Around line 44-45: The _local_build_command logic in run.py builds
`--build-with` using naive double-quote interpolation, unlike the
`--configure-args` handling, which can break the bash command when
`args.with_operators` contains quotes or other shell-sensitive characters.
Update the command construction in `_local_build_command` to quote
`args.with_operators` safely using the same approach as `--configure-args` (for
example via shlex.quote) so the generated string remains valid when executed by
bash -c.

In `@tests/unit/test_cli_behaviors.py`:
- Around line 400-405: The test in test_cli_behaviors around
test_cmd.handle_test is swallowing all exceptions with a bare except Exception:
pass, which hides real failures. Narrow the exception handling to the specific
expected incomplete-namespace error for this scenario, or assert/log the
exception before ignoring it, so handle_test regressions are not silently
masked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bbdad0f6-fe95-49a7-b92c-4345a5aa89c6

📥 Commits

Reviewing files that changed from the base of the PR and between e1c0db7 and 1b25f0d.

📒 Files selected for processing (26)
  • .github/CI.md
  • .github/workflows/main.yaml
  • .github/workflows/release.yaml
  • src/holoscan_cli/commands/clear_cache.py
  • src/holoscan_cli/commands/run.py
  • src/holoscan_cli/commands/setup_cmd.py
  • src/holoscan_cli/commands/test_cmd.py
  • src/holoscan_cli/container/core.py
  • src/holoscan_cli/container/parsers.py
  • src/holoscan_cli/setup_scripts/Dockerfile.util
  • src/holoscan_cli/setup_scripts/benchmarking.sh
  • src/holoscan_cli/setup_scripts/debug.sh
  • src/holoscan_cli/setup_scripts/sccache.sh
  • src/holoscan_cli/setup_scripts/xvfb.sh
  • src/holoscan_cli/system_check.py
  • src/holoscan_cli/utils/env_info.py
  • src/holoscan_cli/utils/host_setup.py
  • src/holoscan_cli/utils/io.py
  • tests/unit/test_cli_behaviors.py
  • tests/unit/test_container_core.py
  • tests/unit/test_env_info.py
  • tests/unit/test_host_setup.py
  • tests/unit/test_io.py
  • tests/unit/test_lifecycle_commands.py
  • tests/unit/test_package_data.py
  • tests/unit/test_setup_cmd.py

Comment thread .github/workflows/main.yaml Outdated
Comment thread .github/workflows/main.yaml Outdated
Comment thread src/holoscan_cli/commands/clear_cache.py Outdated
Comment thread src/holoscan_cli/commands/setup_cmd.py Outdated
Comment thread src/holoscan_cli/utils/host_setup.py Outdated
Comment thread src/holoscan_cli/utils/io.py Outdated

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
tests/unit/test_cli_behaviors.py (1)

296-299: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow the caught exception type.

pytest.raises(Exception) is flagged by Ruff (B017) as a blind exception catch. The test relies on container.verbose = args.verbose raising AttributeError (the passed Namespace omits verbose); asserting that specific type documents intent and prevents the test from silently passing if an unrelated exception fires earlier for the wrong reason.

♻️ Proposed fix
-    with pytest.raises(Exception):  # downstream local run needs more of the namespace
+    with pytest.raises(AttributeError):  # downstream local run needs more of the namespace
         test_cmd.handle_test(cli, args)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_cli_behaviors.py` around lines 296 - 299, Update the test in
test_cli_behaviors by narrowing the broad pytest.raises(Exception) around
test_cmd.handle_test to the specific AttributeError raised when
container.verbose is accessed from the incomplete Namespace. This makes the
intent explicit and keeps the test from passing on unrelated failures; locate it
via handle_test and the Namespace setup with clear_cache/local.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/unit/test_cli_behaviors.py`:
- Around line 296-299: Update the test in test_cli_behaviors by narrowing the
broad pytest.raises(Exception) around test_cmd.handle_test to the specific
AttributeError raised when container.verbose is accessed from the incomplete
Namespace. This makes the intent explicit and keeps the test from passing on
unrelated failures; locate it via handle_test and the Namespace setup with
clear_cache/local.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 04b50610-789e-4abe-8c9b-22d243744bb5

📥 Commits

Reviewing files that changed from the base of the PR and between 1b25f0d and a89374f.

📒 Files selected for processing (3)
  • src/holoscan_cli/commands/clear_cache.py
  • src/holoscan_cli/commands/test_cmd.py
  • tests/unit/test_cli_behaviors.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/holoscan_cli/commands/test_cmd.py
  • src/holoscan_cli/commands/clear_cache.py

Path.home() raises RuntimeError when the home directory cannot be
determined; treat it as a best-effort anchor lookup so clear-cache
still runs its removal checks with the remaining anchors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wyli
wyli requested a review from tbirdso July 6, 2026 15:56
… tests

Move the path canonicalizer into holoscan_cli.utils.io as a public
helper, condense the guard docstrings/comments, collapse the four-way
dangerous-root parametrize into one loop, and make the test_cmd
forwarding test stop deterministically via a SystemExit sentinel
instead of a catch-all pytest.raises(Exception).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@tbirdso tbirdso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good catch. LGTM

@wyli
wyli merged commit c99ba99 into main Jul 8, 2026
27 checks passed
@wyli
wyli deleted the fix/clear-cache-safety-boundary branch July 8, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants