Skip to content

fix(list_files): dedupe raced directory entries against seen_dir_paths - #789

Open
thomwebb wants to merge 1 commit into
mpfaffenberger:mainfrom
thomwebb:fix/list-files-toctou-dir-dedup
Open

fix(list_files): dedupe raced directory entries against seen_dir_paths#789
thomwebb wants to merge 1 commit into
mpfaffenberger:mainfrom
thomwebb:fix/list-files-toctou-dir-dedup

Conversation

@thomwebb

Copy link
Copy Markdown
Collaborator

Summary

Tiny non-blocking follow-up to #787, flagged during review of that PR.

_list_files synthesizes parent-directory entries for a recursive listing and tracks which ones it's already added in a seen_dir_paths set (#787 replaced an O(n^2) scan with this set). That set is only populated inside the parent-synthesis branch, though — not at the main append site where each rg --files entry gets added.

rg --files only ever lists files, so in normal operation the main append site never sees entry_type == "directory". But the code re-checks each path against the live filesystem (os.path.isfile() / os.path.isdir()) before classifying it, so if something on disk swaps a listed path for a directory between rg's enumeration and that check (TOCTOU), the main site can append a directory entry that seen_dir_paths doesn't know about. If a sibling file also references that same path as a parent — in either order — the directory gets listed twice.

The pre-#787 O(n^2) scan happened to avoid this because it checked all of results, not just synthesized parents, so this is a narrow behavior change introduced by that PR, not a pre-existing bug.

Fix

Record directory entries added at the main append site in seen_dir_paths too, and skip appending if the path is already there. Covers both orderings: directory-in-rg-output-before-the-file, and after.

Tests

Added TestListFilesDirectoryToctouRace with two tests that mock subprocess.run to force the race deterministically (a real timing-dependent race isn't reliably testable). Verified both fail on the pre-fix code with a duplicated directory line, and pass after the fix.

Verification

  • tests/tools/test_file_operations_coverage.py: 77/77 pass (75 existing + 2 new).
  • tests/tools/: 609/609 pass.
  • ruff check: same 27 pre-existing findings on both touched files, before and after — confirmed by diffing against the pre-fix commit. ruff format --check clean.

Severity is low (worst case is one cosmetic duplicate line, not a crash or data loss) — flagging as a quick correctness follow-up since it was easy to fix and test properly once identified.

@thomwebb thomwebb self-assigned this Aug 17, 2026
PR mpfaffenberger#787 replaced the O(n^2) parent-directory dedup scan with a set,
but the set was only populated inside the parent-synthesis branch. A
directory can also reach the main append site directly if something
on disk swaps a listed path for a directory between rg's enumeration
and the os.path.isfile()/isdir() recheck (rg --files lists files
only, so this never happens in normal operation).

In that TOCTOU race, the main append site could add a directory entry
seen_dir_paths didn't know about, so a sibling file synthesizing the
same path as a parent (in either order) would duplicate it. The old
O(n^2) scan happened to catch this because it checked all of results,
not just synthesized parents.

Record main-site directory entries in seen_dir_paths too, and skip if
one is already there. Covered by two new deterministic tests that
mock rg's output to force the race in both orderings; both fail on
the pre-fix code with a duplicated directory line and pass after.
@thomwebb
thomwebb force-pushed the fix/list-files-toctou-dir-dedup branch from fb1bbbf to 51570bc Compare August 17, 2026 22:11
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.

1 participant