Skip to content

[bug-fix] Harden persisted workflow run artifact access against symlinks - #4930

Open
github-actions[bot] wants to merge 4 commits into
mainfrom
fix/4914-persisted-run-symlinks-2a3452971b7ee167
Open

github-actions[bot] wants to merge 4 commits into
mainfrom
fix/4914-persisted-run-symlinks-2a3452971b7ee167

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Bug fix — persisted-run-symlinks

Proposed fix for issue #4914, applying the remediation from the bug assessment.

Verdict: valid · Severity: medium

Summary

Introduces a shared persisted-run storage boundary that rejects symlinked run directories and artifact files before workflow state, input, snapshot, or log access. Resume, status, run listing, and snapshot creation now fail closed consistently, with the policy documented for workflow users and maintainers.

Changes

File Change Notes
src/specify_cli/workflows/engine.py modified Added shared run-directory/artifact validation and no-follow artifact opens; routed state load/save, input fallback, log writes, workflow snapshot creation, resume snapshot reads, and run listing through it.
src/specify_cli/workflows/command_status.py modified Converts unsafe storage failures from run listing into clean CLI errors.
tests/specify_cli/workflows/test_run_state_storage.py added test Covers symlinked run directories and state, input, workflow snapshot, and log artifacts for reads and writes.
workflows/README.md modified Documents the persisted-run symlink policy.
workflows/ARCHITECTURE.md modified Documents the storage boundary and no-follow behavior.

Tests Added or Updated

  • tests/specify_cli/workflows/test_run_state_storage.py::test_load_rejects_symlinked_run_directory — rejects redirected per-run directories.
  • tests/specify_cli/workflows/test_run_state_storage.py::test_run_state_rejects_symlinked_persisted_artifacts — rejects redirected state and input artifacts.
  • tests/specify_cli/workflows/test_run_state_storage.py::test_run_state_rejects_symlinked_artifact_writes — rejects redirected state, input, and log writes.
  • tests/specify_cli/workflows/test_run_state_storage.py::test_workflow_snapshot_artifact_is_checked — rejects redirected workflow snapshots.

Local Verification

  • Commands run: python3 -m compileall -q src/specify_cli/workflows/engine.py src/specify_cli/workflows/command_status.py tests/specify_cli/workflows/test_run_state_storage.py → passed.
  • Commands run: git diff --check → passed.
  • Targeted pytest command → not run because pytest is unavailable in the environment; no dependencies were installed.

Deviations from Assessment

  • The current branch has no workflow definition command, so no command-specific change was possible there; persisted snapshot access is covered in the shared engine layer.
  • The implementation uses O_NOFOLLOW where available plus immediate symlink checks; no concurrent mutation test was added because the targeted test runner is unavailable.

Risks & Review Notes

  • Existing workflows that intentionally relocate run storage through symlinks will now be rejected as an intentional compatibility trade-off.
  • Filesystem behavior around concurrent replacement remains platform-dependent; no-follow leaf opens reduce this risk where supported, while the explicit policy rejects static symlinked paths.

Refs #4914 · cc @markuswondrak

Generated by 🛠️ Fix Bug from Labeled Issue for #4914 · copilot · gpt52codex · 6.54 AIC · ⌖ 0.836 AIC · ⊞ 24.4K · ◷

Apply the remediation from the bug assessment on issue #4914.

Refs #4914

Assisted-by: GitHub Copilot (model: gpt-5.2-codex, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/specify_cli/workflows/engine.py Fixed

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.

🟡 Changes recommended

Snapshot reads retain a symlink race, Ruff fails on an unused variable, and key consumer regressions remain untested.

4 open findings
What changed in this PR

Adds a shared security boundary for persisted workflow-run artifacts to reject symlink redirection.

Changes:

  • Validates run directories and artifacts before access.
  • Adds no-follow file opening and CLI error handling.
  • Documents and tests the symlink policy.
File Description
src/​specify_cli/​workflows/​engine.py Implements storage validation and protected artifact access.
src/​specify_cli/​workflows/​command_status.py Handles unsafe listing failures.
tests/​specify_cli/​workflows/​test_run_state_storage.py Adds symlink rejection tests.
workflows/​README.md Documents user-facing policy.
workflows/​ARCHITECTURE.md Documents the storage boundary.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/specify_cli/workflows/engine.py Outdated
Comment thread src/specify_cli/workflows/engine.py Outdated
Comment on lines +103 to +107
err = cli._error_console(json_output)
try:
runs = engine.list_runs()
except (OSError, ValueError) as exc:
err.print(f"[red]Error:[/red] {cli._escape_markup(str(exc))}")

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.

Added workflow status --json regressions for symlinked run directories and state.json, asserting clean failure and stderr-only output. Fixed in 79f1da7.

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.

The workflow status --json listing regressions for symlinked run directories and state.json are present and pass on the current branch (02159bf0).

Comment thread tests/specify_cli/workflows/test_run_state_storage.py
@mnriem
mnriem marked this pull request as ready for review October 12, 2026 19:32
@mnriem
mnriem self-requested a review as a code owner October 12, 2026 19:32
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

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.

🟡 Changes recommended

A parent-directory replacement race remains, and an existing unreadable-state regression test is broken.

3 open findings
3 resolved since last review

🧠 Review effort: Balanced

Comment thread src/specify_cli/workflows/engine.py Outdated
Comment thread src/specify_cli/workflows/engine.py Outdated
if state_path.exists():
try:
with open(state_path, encoding="utf-8") as f:
with self._open_artifact(state_path, "r", encoding="utf-8") as f:

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.

The unreadable-state regression now patches RunState._open_artifact and still asserts that the bad row is skipped; it passes on the current branch (02159bf0).

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

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.

🟡 Changes recommended

The unconditional Unix-specific filesystem requirements break persisted workflow operations on supported Windows systems.

3 open findings
1 resolved since last review

🧠 Review effort: Balanced

Comment thread src/specify_cli/workflows/engine.py Outdated
Comment on lines +691 to +695
directory_flag is None
or nofollow is None
or os.open not in os.supports_dir_fd
):
raise OSError("Secure workflow run-directory access is unavailable")

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.

Added a checked path-based fallback for platforms without directory-descriptor/no-follow support, including execute/resume/listing and symlink regressions. Committed as 02159bf0; the architecture note documents that this fallback cannot prevent concurrent path replacement.

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Comment on lines +87 to +89
RunState._open_artifact(
artifact, "r", dir_fd=run_fd, encoding="utf-8"
)
try:
RunState._validate_fallback_path(tmp_path)
tmp_path.unlink()
except (OSError, ValueError):

This branch has not been deployed

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

Labels

automated bug-fix Trigger the bug-fix agentic workflow bug-test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Harden persisted workflow run artifact access against symlinks

3 participants