Repository navigation
Conversation
TempCopySweeper::lockIsFree() opened `$lockPath` via fopen($lockPath, 'c') with no check that the path is still a regular file -- the same shape #225 closed in reclaimStaleOwnLock(). removeIfStale()'s own is_file($lockPath) check already blocks the simple case, but a lockPath swapped for a symlink in the window between that check and lockIsFree()'s own fopen() could still make the sweeper create and probe/lock a file anywhere this process can write. lockIsFree() now re-checks isLinkOrJunction($lockPath) || !isRegularFile($lockPath) immediately before its own fopen(), mirroring reclaimStaleOwnLock()'s guard. Tested directly against lockIsFree() via reflection (it is private), the same way #225's own test exercises reclaimStaleOwnLock() directly, since the actual TOCTOU race cannot be pinned deterministically without disproportionate test-infra engineering. Co-Authored-By: Claude Sonnet 5 <[email protected]>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Follow-up from #225:
TempCopySweeper::lockIsFree()opened$lockPathviafopen($lockPath, 'c')with no symlink/junction guard immediately before the call, the same TOCTOU shape #225 closed in the sibling methodreclaimStaleOwnLock().Adds
if (self::isLinkOrJunction($lockPath) || !self::isRegularFile($lockPath)) { return false; }immediately before thefopen()call insidelockIsFree(), byte-for-byte mirroringreclaimStaleOwnLock()'s own pre-fopen guard from #225.removeIfStale()'s own pre-existingis_file($lockPath)check already blocked the simple, non-race case the issue described; this closes the remaining TOCTOU window. The genuine mid-call symlink-swap race cannot be pinned deterministically in a unit test without disproportionate test-infra engineering (per the issue body itself), so instead of a race-pinning test,lockIsFree()(private) is tested directly via PHP Reflection -- the same convention already used in this suite (tests/Unit/RectorRunnerFallbackBranchesTest.php:69, tests/Unit/RectorRunnerTest.php, tests/Unit/Lsp/RectorDiagnosticsSourceTest.php:515, tests/Unit/RectorToolContainmentTest.php:91) -- proving the guard in isolation.removeKnownContents()'s other$lockPathuses (is_file,file_get_contents,unlink) are deliberately left unguarded: none hasfopen()'s create-on-missing-target danger, andunlinkon a symlink removes the link itself, never the target. This scoping was confirmed independently by two spawned reviewers in self-review.Testing
testLockIsFreeDoesNotFollowASymlinkedLockPath(must-not-fire): a symlink to a nonexistent external target; assertslockIsFree()returnsfalseand the external target is never created.testLockIsFreeReturnsTrueForAGenuineFreeLockFile: a must-fire positive control pairing the above.testLockIsFreeDoesNotFollowASymlinkedLockPathfailed -- "a symlinked lock path must never be treated as free to take / Failed asserting that true is false."TempCopySweeperTestfile -- 16 tests (14 pre-existing + 2 new), 56 assertions, 2 skipped (pre-existing platform skips).vendor/bin/phpstan analyseon both changed files: no errors.tests.fullnot-run.removeKnownContents()'s other$lockPathuses (is_file,file_get_contents,unlink) are deliberately left unguarded: none hasfopen()'s create-on-missing-target danger, andunlinkon a symlink removes the link itself, never the target -- flagged by the auditor as a contextual observation, not ranked, not routed, pre-existing code outside this diff's scope.Closes #228
🤖 Generated with Claude Code