Repository navigation
Fix deadlock of renames within a sub-directory with concurrent lookups - #111
Merged
Merged
Conversation
A same-directory MoveFile, MoveDirectory or ReplaceFile in a sub-directory deadlocks with a concurrent lookup in that directory: these tests hang until their timeout on the current code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A move or replace whose paths share a parent locked that parent through the lookup of the first path, then looked the other paths up from the root again. That second walk takes the locks of the parent's ancestors while the parent is held, the opposite order to every other lookup, so a concurrent lookup below an ancestor that waits for the parent blocks the rename forever. Look the other names up directly in the parent the first lookup holds, as the Unix kernel hierarchical locking scheme does for same-directory renames. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lahma
added a commit
to lahma/test262-harness-dotnet
that referenced
this pull request
Oct 20, 2026
0.25.0 contains only our three upstream changes to MemoryFileSystem: a lock-free fast path for node locks (xoofx/zio#109), faster EnumerateItems (xoofx/zio#110) and the fix for a deadlock between a rename inside a sub-directory and concurrent lookups (xoofx/zio#111). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
A
MoveFile,MoveDirectoryorReplaceFilewhose paths are all in the same sub-directory can deadlock with any concurrent lookup in that directory, such asFileExists,OpenFileorCreateDirectory.Problem
When the paths share a parent,
MoveFileOrDirectoryandReplaceFileImpltake the filesystem lock shared. They then look each path up withEnterFindNode(..., KeepParentNodeExclusive | ...)and pass the results of the previous lookups asexistingNodes. TakeMoveFile("/d/a.txt", "/d/b.txt"):/, then/d(exclusive), releases/and locks/d/a.txt. It keeps/d./is not inexistingNodes, so it locks/again while it holds/d.Every other lookup takes the directory locks hand-over-hand from the root down. It holds
/until it has/d. So:/dand waits for/;FileExists("/d/other.txt")) holds/and waits for/d;and both block forever. Moves within
/are not affected (/is then inexistingNodes), and neither are moves across directories (they take the filesystem lock exclusively).A move loop in
/dwith aFileExists("/d/other.txt")loop on another thread deadlocks onmainwithin the first few hundred lookups, before the first move completes.Fix
The second (and third) lookup of a same-directory rename no longer walks from the root. The new
EnterFindChildlooks the name up directly in the parent that the first lookup holds, and locks only the node it finds. The result never releases the parent; the first lookup's result still does. This is how the hierarchical locking scheme of the Unix kernel thatMemoryFileSystemfollows handles same-directory renames: the parent is locked once, and both names are looked up in it.MoveFileOrDirectory: when the source and destination are in the same folder, the destination is looked up withEnterFindChildin the source's parent.ReplaceFileImpl: when source, destination and backup are in the same folder, the first path in sorted order is looked up as before, and the others withEnterFindChildin its parent.The locks taken, and the order they are taken in, are otherwise unchanged. The async counterpart (
MemoryFileSystemAsync.gen.cs) is regenerated withZio.AsyncCodeGen.A coarser alternative would be to take the filesystem lock exclusively for every move and replace, as is already done when the directories differ. That would make same-directory renames exclude every other operation on the filesystem, so I went with the fine-grained fix.
Tests
The first commit adds
TestMemoryFileSystemRenameConcurrencyon its own, so it can be checked againstmain. Each test runs 2000 renames in/dagainst a concurrentFileExists("/d/other.txt")loop, and fails if the renames don't complete within 30 s. The tests coverMoveFile,MoveDirectory,ReplaceFilewith a backup, andMoveFileAsynconMemoryFileSystemAsync.main, all four tests fail on the timeout (deadlocked).dotnet test -c Releasepasses on net10.0 (524 passed, 3 skipped) and net472 (500 passed, 3 skipped). TheZIO_NO_ASYNCbuild succeeds, andZio.AsyncCodeGen -- --checkis clean.🤖 Generated with Claude Code