Skip to content

fix(install): validate APM_LIB_DIR before rm -rf to prevent data loss (closes #1690)#1694

Merged
danielmeppiel merged 8 commits into
microsoft:mainfrom
dohwi:fix/1690-installer-safety
Jun 9, 2026
Merged

fix(install): validate APM_LIB_DIR before rm -rf to prevent data loss (closes #1690)#1694
danielmeppiel merged 8 commits into
microsoft:mainfrom
dohwi:fix/1690-installer-safety

Conversation

@dohwi

@dohwi dohwi commented Jun 8, 2026

Copy link
Copy Markdown

Summary

Adds four safety guards that run before install.sh deletes APM_LIB_DIR, fix the broken Quickstart Installation link, and document the new requirements. 32 pytest regression tests included.

Problem

install.sh exposes APM_LIB_DIR as an environment override and unconditionally runs rm -rf "$APM_LIB_DIR". A user who set APM_LIB_DIR=$HOME/.local/share while installing to $HOME/.local/bin lost unrelated application data (Atuin's local history DB). The Quickstart's Installation link is also a 404: ./getting-started/installation/ resolves to /apm/quickstart/getting-started/installation/ instead of /apm/getting-started/installation/.

Closes #1690.

Approach

Four guards, ordered so the cheapest checks fire first:

Guard Check rc Failure example
1. Absolute path Must start with / 11 ./apm
2. Suffix Must end with /apm or /lib/apm 12 $HOME/.local/share (the reported incident)
3. Blocklist Must not be an exact match for a shared system directory (resolved via readlink -f/realpath to catch symlinks) 13 $HOME, /usr, /opt, /tmp, /
4. Marker file If the directory exists and is non-empty, at least one of apm/apm.cmd/VERSION/.apm-installed must be present 14 non-empty dir with random files

The blocklist checks for exact path matches only (resolved). A path like $HOME/apm is allowed because the user has explicitly named an APM-specific directory. Suffix guard is the primary defence against accidentally broad paths.

Implementation

  • install.sh: Extracted guards into apm_lib_dir_validate() with INSTALL_SAFETY_BEGIN/INSTALL_SAFETY_END sentinel markers so tests can source the function without running the full installer.
  • tests/unit/install/test_install_safety.py: 32 regression tests that source the extracted function in a fresh bash subprocess. No Python build dependencies needed — conftest.py shadows the autouse fixtures from tests/conftest.py.
  • Docs: Fixed the Quickstart link and documented APM_LIB_DIR requirements in the installation page table and troubleshooting section.

Trade-offs

  • Blocklist is exact-match: /home/user/apm passes (suffix guard alone). A prefix-blocklist (e.g., "any child of $HOME") would accidentally reject $HOME/.local/lib/apm, which is the documented safe path. Future hardening could add a separate prefix blacklist.
  • macOS readlink: BSD readlink lacks -f. Falls back to realpath, then raw path. Suffix guard still blocks the reported incident on macOS regardless.

How to test

# Run the regression suite
python3 -m pytest tests/unit/install/test_install_safety.py -v

# Manual: the reported command should now fail safely (in a sandbox, no sudo)
bash -c '
  source <(sed -n "/^# INSTALL_SAFETY_BEGIN/,/^# INSTALL_SAFETY_END/p" install.sh)
  apm_lib_dir_validate "$HOME/.local/share" && echo "PASS" || echo "BLOCKED (rc=$?)"
'

…closes microsoft#1690)

Add four safety guards that run before the installer deletes APM_LIB_DIR:
1. Absolute-path guard: reject relative paths
2. Suffix guard: path must end with /apm or /lib/apm
3. Blocklist guard: reject shared system directories ($HOME/.local/share, /usr, /opt, /tmp, /)
4. Marker-file guard: require evidence of a prior APM install before deleting an existing non-empty directory

Also fix the broken "Installation" link in the Quickstart page and
document the new APM_LIB_DIR requirements in the installation docs.

Guards are extracted into apm_lib_dir_validate() with sentinel markers
so they can be tested directly from a shell-source harness without
running the full installer. 32 regression tests cover the 4 guards,
the reported incident, safe default paths, and sentinel invariants.
Copilot AI review requested due to automatic review settings June 8, 2026 05:28

Copilot AI 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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a safety validation layer for APM_LIB_DIR in install.sh to prevent destructive deletes on broad/shared directories (regression for issue #1690), plus a pytest-based harness that sources the exact shell function for verification and updates docs to reflect the new constraints.

Changes:

  • Added apm_lib_dir_validate() in install.sh with 4 guards (absolute path, suffix, blocklist, marker-file) and a refusal flow on unsafe paths.
  • Added pytest regression tests that extract and execute the sentinel-bounded validator block directly from install.sh, plus a local conftest.py to avoid unrelated autouse fixtures.
  • Updated installation docs/quickstart to document/point to the safety constraints.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/unit/install/test_install_safety.py New regression tests that source the exact install.sh validator block and assert return codes for safety cases.
tests/unit/install/conftest.py Overrides unrelated autouse fixtures to keep shell-script safety tests independent of Python CLI setup.
install.sh Introduces APM_LIB_DIR validation function (sentinel-bounded for tests) and refusal messaging prior to rm -rf.
docs/src/content/docs/quickstart.mdx Updates the “Installation” link target.
docs/src/content/docs/getting-started/installation.md Documents APM_LIB_DIR safety validation behavior and adds an “Important” callout.

Comment thread install.sh Outdated
Comment on lines +563 to +576
if ! apm_lib_dir_validate "$APM_LIB_DIR"; then
_rc=$?
echo -e "${RED}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}║ REFUSING: APM_LIB_DIR=\"$APM_LIB_DIR\"${NC}"
echo -e "${RED}╠══════════════════════════════════════════════════════════════╣${NC}"
case $_rc in
11) echo -e "${RED}║ APM_LIB_DIR must be an absolute path.${NC}\n${RED}║ Relative paths are not accepted for safety.${NC}" ;;
12) echo -e "${RED}║ APM_LIB_DIR must end with /apm or /lib/apm.${NC}\n${RED}║ This prevents accidental deletion of non-APM data.${NC}\n${RED}║ Example: APM_LIB_DIR=\$HOME/.local/lib/apm${NC}" ;;
13) echo -e "${RED}║ This path is a shared system directory. Installing here${NC}\n${RED}║ would delete non-APM data.${NC}\n${RED}║ Use a dedicated APM directory (e.g. /usr/local/lib/apm).${NC}" ;;
14) echo -e "${RED}║ This directory exists but does not appear to be a${NC}\n${RED}║ previous APM installation. Refusing to delete it.${NC}\n${RED}║ If you are sure, remove it manually first:${NC}\n${RED}║ rm -rf \"$APM_LIB_DIR\"${NC}" ;;
esac
echo -e "${RED}╚══════════════════════════════════════════════════════════════╝${NC}"
exit 1
fi

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in afa54c4 — removed local, fixed _rc capture, added begin sentinel assertion, switched to pytest.main().

Comment thread install.sh Outdated
Comment on lines +503 to +504
apm_lib_dir_validate() {
local _lib_dir="$1"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in afa54c4 — removed local, fixed _rc capture, added begin sentinel assertion, switched to pytest.main().

Comment thread install.sh Outdated
Comment on lines +520 to +526
local _lib_dir_real
_lib_dir_real="$(readlink -f "$_lib_dir" 2>/dev/null || realpath "$_lib_dir" 2>/dev/null || echo "$_lib_dir")"

local _safe=true
while IFS= read -r _dir; do
[ -z "$_dir" ] && continue
local _dir_real

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in afa54c4 — removed local, fixed _rc capture, added begin sentinel assertion, switched to pytest.main().

Comment on lines +41 to +46
text = INSTALL_SH.read_text(encoding="utf-8")
match = SENTINEL_END.search(text)
assert match is not None, "INSTALL_SAFETY_END sentinel missing in install.sh"
start = SENTINEL_BEGIN.search(text).end()
end = match.start()
block = text[start:end]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in afa54c4 — removed local, fixed _rc capture, added begin sentinel assertion, switched to pytest.main().

Comment on lines +290 to +291
if __name__ == "__main__":
unittest.main()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in afa54c4 — removed local, fixed _rc capture, added begin sentinel assertion, switched to pytest.main().

dohwi and others added 3 commits June 8, 2026 14:39
- Fix _rc=$? capturing negated exit code (now stores validator's actual rc)
- Remove local keyword usage for POSIX sh compatibility (curl ... | sh)
- Add SENTINEL_BEGIN assertion in test harness
- Fix __main__ block to use pytest instead of unittest
Fold shepherd review feedback for issue microsoft#1690 by creating a writable derived lib parent before choosing sudo, rechecking safety immediately before deletion, and writing an APM ownership marker after install. Extend regression coverage and docs for the user-local path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fold final panel coverage and docs feedback by adding a regression trap for the lib-parent preparation failure path and aligning APM_LIB_DIR suffix wording with the actual shell guard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel

Copy link
Copy Markdown
Collaborator

Shepherd-driver advisory

Panel stance: ship_with_followups. In-scope follow-ups were folded into this PR; no out-of-scope deferrals remain.

Folded in this run

  • (panel) User-local installs now create the derived lib parent before choosing sudo, so APM_INSTALL_DIR=$HOME/.local/bin uses $HOME/.local/lib/apm without a password prompt when $HOME/.local is writable -- resolved in d9d166e.
  • (panel) Installer writes .apm-installed after copying the bundle and rechecks APM_LIB_DIR immediately before removal -- resolved in d9d166e.
  • (panel) Docs now explain the prior-install marker guard and user-local no-sudo path -- resolved in d9d166e.
  • (panel) Added fallback coverage for apm_prepare_lib_parent returning 1 when parent creation is not writable, and aligned /apm suffix wording -- resolved in 18f221d.

Copilot signals reviewed

  • Copilot posted a summary review and no inline comments; no Copilot fold items were open.

Regression-trap evidence (mutation-break gate)

  • tests/unit/install/test_install_safety.py::TestUserLocalInstall::test_prepare_parent_creates_missing_user_local_lib_without_sudo -- removed parent creation from apm_prepare_lib_parent; test FAILED as expected; guard restored.
  • tests/unit/install/test_install_safety.py::TestUserLocalInstall::test_prepare_parent_falls_back_when_parent_unwritable -- changed the helper failure return to success; test FAILED as expected; guard restored.

Lint and local validation

  • uv run --extra dev pytest tests/unit/install/test_install_safety.py -q: 43 passed.
  • uv run --extra dev ruff check src/ tests/: silent success.
  • uv run --extra dev ruff format --check src/ tests/: silent success.
  • uv run --extra dev python -m pylint --disable=all --enable=R0801 --min-similarity-lines=10 --fail-on=R0801 src/apm_cli/: 10.00/10.
  • bash scripts/lint-auth-signals.sh: clean.
  • shellcheck install.sh and bash -n install.sh: clean.

CI

gh pr checks 1694 --repo microsoft/apm --watch observed license/cla still queued after the watch window; no failing checks were reported, but CI is not green yet.

Mergeability status

PR head SHA ship recommendation iters folds defers Copilot rounds CI mergeable mergeStateStatus notes
#1694 18f221d ship_with_followups 2 4 0 1 yellow MERGEABLE BLOCKED waiting on license/cla

Ready for maintainer review once the pending CLA/status check resolves.

@dohwi

dohwi commented Jun 8, 2026

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 9, 2026
Fold shepherd review feedback by replacing installer box-drawing output with printable ASCII and trimming duplicated APM_LIB_DIR docs while keeping the regression harness wording accurate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel

Copy link
Copy Markdown
Collaborator

APM Review Panel: ship_now

The installer data-loss fix now validates APM_LIB_DIR before any destructive delete, has regression coverage for the reported incident, and keeps the new safety output/docs aligned with repo conventions.

cc @dohwi @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

The panel weighed this as a security-sensitive installer change. Supply-chain and test-coverage signals converged on the important point: the guard runs before rm -rf, broad/shared paths are rejected, non-empty non-APM directories are refused, and the exact issue #1690 reproduction is covered by tests. CLI/output and docs review surfaced two in-scope polish items, both folded in this shepherd pass.

Aligned with: secure by default: fail closed before deleting installer state; pragmatic as npm: user-local installs still work without sudo; oss community driven: the contributor fix is preserved and hardened in place.

Panel summary

Persona B R N Takeaway
Python Architect 0 0 0 Shell guard extraction is narrow and testable; no architecture concern remains.
CLI Logging Expert 0 1 0 New refusal output now uses printable ASCII instead of box-drawing glyphs.
DevX UX Expert 0 0 0 Safe paths still work and failures are actionable.
Supply Chain Security Expert 0 0 0 The destructive path is guarded before deletion with regression coverage.
OSS Growth Hacker 0 0 0 The quickstart link and install safety docs protect the first-run funnel.
Doc Writer 0 1 0 The troubleshooting callout now avoids repeating the env-var table.
Test Coverage Expert 0 0 0 43 installer safety tests passed, including broad-path and marker-file traps.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top follow-ups

None. The in-scope follow-ups surfaced by the panel were folded in this pass.

Folded in this run

  • (panel) Replaced new installer box-drawing refusal output with printable ASCII and converted the existing banner to match -- resolved in 4383c2c4.
  • (panel) Trimmed duplicate APM_LIB_DIR troubleshooting prose and kept the callout focused on marker-file/no-sudo behavior -- resolved in 4383c2c4.

Copilot signals reviewed

  • No inline copilot-pull-request-reviewer[bot] comments were present on the latest diff after shepherd re-fetch.

Regression-trap evidence (mutation-break gate)

  • tests/unit/install/test_install_safety.py::TestReportedIncident::test_reproduces_reported_issue -- deleted the suffix guard from apm_lib_dir_validate; test FAILED as expected (assert 13 == 12); guard restored.

Lint contract

uv run --extra dev ruff check src/ tests/ and uv run --extra dev ruff format --check src/ tests/ both passed before push.

CI

gh pr checks 1694 --repo microsoft/apm reports 13 successful, 1 skipped, 0 failing, 0 pending checks on 4383c2c4 after 1 CI approval/recovery iteration.

Mergeability status

Captured from gh pr view 1694 --json mergeable,mergeStateStatus,statusCheckRollup immediately after the last push of this run.

PR head SHA CEO stance iters folds defers Copilot rounds CI mergeable mergeStateStatus notes
#1694 4383c2c ship_now 1 2 0 2 green MERGEABLE BLOCKED pending required review

Convergence

1 outer iteration; 2 Copilot fetch rounds. Final panel stance: ship_now.

Ready for maintainer review.


This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.

@danielmeppiel danielmeppiel removed the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 9, 2026
@danielmeppiel danielmeppiel merged commit 89f66eb into microsoft:main Jun 9, 2026
15 checks passed
sergio-sisternes-epam pushed a commit that referenced this pull request Jun 9, 2026
…arency)

Sync main commits #1700, #1676, #1694, #1710. The #1700 feature (surface
installed hook actions during install) wove display-payload tracking
through code my refactor extracted into sibling helpers; resolve two
conflicts by keeping the extracted structure and porting #1700 semantics:

- services.py: accept the extracted _log_per_kind_results() call; move
  _log_hook_display_payloads into services_integrate.py (avoids circular
  import) and re-export it from services.py; emit hook summaries inside
  _log_per_kind_results.
- hook_integrator._integrate_merged_hooks: accept extracted
  _merge_hook_file_entries / _write_merged_config; thread per-file
  display data out via a new optional capture_entries kwarg on
  _merge_hook_file_entries; build display payloads after
  _write_merged_config finalizes (post _apm_source strip).
- Keep hook_integrator.py <=800 by relocating _iter_hook_entries /
  _summarize_command / _build_display_payload to hook_transforms.py and
  _parse_hook_json to hook_merge.py as thin delegators.

Shadow gate green: ruff, ruff format, pylint R0801 10.00/10 (EXIT=0),
auth-signals, import smoke, 3178 tests pass. All files within 800-line
guard (hook_integrator.py 799, hook_merge.py 766, hook_transforms.py 601,
services.py 690, services_integrate.py 307).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

[BUG] Installer data-loss footgun: unsafe APM_LIB_DIR deletion and broken installation docs

3 participants