Skip to content

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

Open
Quratulain-bilal wants to merge 3 commits into
github:mainfrom
Quratulain-bilal:fix/catalog-fetch-toctou
Open

fix: eliminate TOCTOU races in catalog_fetch()#3910
Quratulain-bilal wants to merge 3 commits 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/bundler/services/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

…ath URLs

Remove exists() pre-checks and catch FileNotFoundError from read_text()
to provide clear BundlerError messages even under race conditions.
…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: mocked Path is observable as present (exists() returns True)
but read_text() raises FileNotFoundError, proving the exists() removal
eliminates the race window.

Co-authored-by: GitHub Copilot (model: mimo-v2.5-free, supervised)

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

raise BundlerError(f"Catalog file not found: {path}")
return load_json(path)
try:
return loads_json(path.read_text(encoding="utf-8"), origin=str(path))
Comment thread tests/unit/test_bundler_adapters.py Outdated
Comment on lines +211 to +216
"""Regression guard: a file that disappears between the old exists() pre-check
and read_text() must raise BundlerError, not a raw FileNotFoundError.

The mocked Path is observable as present (exists() returns True) but
read_text() raises FileNotFoundError, simulating a deletion between the two
calls — the exact race window the exists() removal eliminates."""
Remove load_json from imports (F401) since the TOCTOU fix switched to
loads_json(path.read_text()). Improve test docstring to accurately
describe the behavioral change.
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.

3 participants