You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[bug-fix] Harden persisted workflow run artifact access against symlinks - #4930
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::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.
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.
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.
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).
Close artifact handles in the symlink regression and explain cleanup failure handling. Forward directory descriptors through checkpoint test mocks to preserve failure and interrupt coverage.
Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 67a8a13d-1542-470f-87b8-5a3a1dc122f5
Walk storage ancestors using descriptor-relative no-follow opens and use directory descriptors for creation and listing. Reject dangling run-root symlinks and add ancestor replacement regressions while preserving the portable fallback.
Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 67a8a13d-1542-470f-87b8-5a3a1dc122f5
Narrow symlink-free tree claim to access-time checks
workflows/README.md:527
This promises that every symlink anywhere below runs is rejected, but the implementation checks only storage ancestors and one of the four known artifacts when that path is accessed; it does not scan unrelated entries (or an unused inputs.json). Narrow this statement to the access-time policy so users are not told that the whole tree is proactively symlink-free.
Create portable atomic-write temporary files with explicit 0600 permissions. Cover temporary and final state/input modes under a permissive POSIX umask, including replacement of existing files. Clarify that symlink rejection is enforced on accessed storage paths, not a proactive tree scan.
Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 67a8a13d-1542-470f-87b8-5a3a1dc122f5
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
src/specify_cli/workflows/engine.pysrc/specify_cli/workflows/command_status.pytests/specify_cli/workflows/test_run_state_storage.pyworkflows/README.mdworkflows/ARCHITECTURE.mdTests 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
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.git diff --check→ passed.pytestis unavailable in the environment; no dependencies were installed.Deviations from Assessment
workflow definitioncommand, so no command-specific change was possible there; persisted snapshot access is covered in the shared engine layer.O_NOFOLLOWwhere available plus immediate symlink checks; no concurrent mutation test was added because the targeted test runner is unavailable.Risks & Review Notes
Refs #4914 · cc
@markuswondrak