Skip to content

fix: eliminate TOCTOU races in catalog_fetch() - #3910

Merged
mnriem merged 1 commit into
github:mainfrom
Quratulain-bilal:fix/catalog-fetch-toctou
Sep 24, 2026
Merged

mnriem merged 1 commit into
github:mainfrom
Quratulain-bilal:fix/catalog-fetch-toctou

Conversation

@Quratulain-bilal

Copy link
Copy Markdown
Contributor

Problem

catalog_fetch() checks exists() then calls read_text() for both file:// and bare path URLs. The file can be deleted between the two calls.

Fix

Remove the exists() pre-checks and catch FileNotFoundError from read_text().

Testing

  • Verified BundlerError is raised when catalog file is missing

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

Eliminates TOCTOU races when reading local catalogs.

Changes:

  • Removes exists() pre-checks.
  • Converts FileNotFoundError into BundlerError.
Show a summary per file
File Description
src/specify_cli/bundler/services/adapters.py Safely handles disappearing local catalog files.

Review details

Tip

Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/specify_cli/bundles/adapters.py

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

Please address Copilot feedback

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.

Review details

Suppressed comments (1)

tests/unit/test_bundler_adapters.py:228

  • The test never verifies the stated removal of the exists() pre-check: an implementation that calls exists() and then catches FileNotFoundError from read_text() still passes. Assert that exists() was not called so this regression test actually guards the PR's TOCTOU fix.
    with patch.object(adapters.Path, "__new__", return_value=mock_path):
        with pytest.raises(BundlerError, match="Catalog file not found"):
            fetcher(_source(url))
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread src/specify_cli/bundles/adapters.py
Comment thread tests/unit/test_bundler_adapters.py Outdated
@mnriem mnriem added the triage-can-wait Verdict: valid and in-scope but deprioritized; held behind the evidence gate label Sep 11, 2026
@mnriem
mnriem requested a balanced review from Copilot September 11, 2026 17:45

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.

🟢 Approval recommended

The implementation is correct; only a non-blocking test assertion is missing.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread tests/unit/test_bundler_adapters.py Outdated
@mnriem

mnriem commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

@Quratulain-bilal

Copy link
Copy Markdown
Contributor Author

Hi @mnriem — I've addressed Copilot's feedback: removed the unused exists() mock setup and added mock_path.exists.assert_not_called() as a regression guard to verify the pre-check was genuinely eliminated. All 20 tests pass. Ready for re-review when you get a chance.

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.

Copilot review overview

🟢 Approval recommended

The race condition is addressed with regression coverage and no unresolved issues.

Review effort: Balanced
Findings: None

Resolved since last review (1)

…ath URLs

Remove exists() pre-checks and catch FileNotFoundError from read_text() for both file:// and bare path catalog sources. Also catches OSError/UnicodeError to preserve the decode-error wrapping contract.

Add regression test for the TOCTOU fix covering both file:// URLs and bare paths: the mocked Path raises FileNotFoundError from read_text(), proving the exists() removal eliminates the race window, with assert_not_called() as a regression guard that no pre-check remains.

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.

Copilot review overview

🟢 Approval recommended

All reviewed changes are covered with no unresolved issues.

Review effort: Balanced
Findings: None

@mnriem
mnriem self-requested a review September 24, 2026 11:34
@mnriem
mnriem merged commit 36205c2 into github:main Sep 24, 2026
15 checks passed
@mnriem

mnriem commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Thank you!

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

Labels

triage-can-wait Verdict: valid and in-scope but deprioritized; held behind the evidence gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants