Fix #84344 to ensure sufficient stack by calling RuntimeHelpers.EnsureSufficientExecutionStack()#122761
Conversation
1994d04 to
b713486
Compare
|
Rebase as of 29th December, 2025. |
|
Looks like the CI is green (before rebase). I think I will mark this PR as ready (not DRAFT anymore). |
There was a problem hiding this comment.
Pull request overview
This PR fixes issue #84344 by preventing stack overflow exceptions when recursively deleting deeply nested directory structures. The fix adds stack space validation before making recursive calls in Directory.Delete.
Key Changes
- Added
RuntimeHelpers.EnsureSufficientExecutionStack()calls before recursive directory deletion to prevent stack overflow - Applied consistently to both Windows and Unix filesystem implementations
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs | Added using directive and stack validation before recursive RemoveDirectoryRecursive call |
| src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs | Added using directive and stack validation before recursive RemoveDirectoryRecursive call |
|
Done adding unit test for this specific nesting scenario, using 6000 depth nesting instead of original 14000 as the test will run a long time if we use the original 14000 as the depth. @dotnet/area-system-io please review. Let me know if the tag is incorrect so I should tag the right team. |
|
Update to limit the depth to only 3000 for non Windows. |
|
It seems the test should only run on Windows. Done updating the test. |
…ient stack space when calling Directory.Delete recursively. It is implemented in both FileSystem.Unix.cs and FileSystem.Windows.cs
…ue, and ensure the nested depth is large enough but not too large as the original scenario (14000). I use 6000 to test whether the stack is still sufficient and run without exception thrown. use directive for Windows set the depth to 6000, for non Windows set the depth to 3000 to avoid System.IO.PathTooLongException on non Windows. Set the unit test of RecursiveDelete_issue84344 to only runs on Windows, because Linux max path length is 4096,
0bb0917 to
063b473
Compare
| if (isDirectory) | ||
| { | ||
| // Since this is a recursive call, we have to ensure that we have sufficient stack space. | ||
| RuntimeHelpers.EnsureSufficientExecutionStack(); |
There was a problem hiding this comment.
I'm a little concerned this is going to start introducing failures that weren't there before. Previously if we recurred too deeply the process would crash with a stack overflow, but EnsureSufficientExecutionStack is fairly conservative about what "sufficient execution stack" means, and it could end up throwing an exception well before the process would otherwise crash. I expect there are some cases today of very nested directories that are working today because they're not deep enough to cause a crash but would start throwing an exception with this because they are deep enough to hit the stack execution depth limit.
Alternatives might include:
- Switching the whole implementation to be iterative with a manually-managed stack rather than recursive.
- Only doing so once TryEnsureSufficientExecutionStack returns false.
- When TryEnsureSufficientExecutionStack returns false, fork off an additional thread to continue the recursive processing.
There was a problem hiding this comment.
Thank yoju for the feedback, @stephentoub ! 👍
Let me refactor it.
| DirectoryInfo subDir = testDir.CreateSubdirectory("test_issue84344"); | ||
| for (int i = 0; i < depth; i++) | ||
| { | ||
| subDir = subDir.CreateSubdirectory("a"); | ||
| } | ||
| // Now delete the subdir recursively. | ||
| Directory.Delete(subDir.FullName, recursive: true); |
There was a problem hiding this comment.
This test currently deletes only the leaf directory: subDir is reassigned in the loop to the deepest "a" directory, so Directory.Delete(subDir.FullName, recursive: true) won't exercise recursive deletion and will leave thousands of parent directories behind. Keep a reference to the top of the created tree (e.g., the initial test_issue84344 directory or testDir) and delete that instead; also ensure cleanup on failure (e.g., via try/finally) to avoid polluting the test temp root.
| DirectoryInfo subDir = testDir.CreateSubdirectory("test_issue84344"); | |
| for (int i = 0; i < depth; i++) | |
| { | |
| subDir = subDir.CreateSubdirectory("a"); | |
| } | |
| // Now delete the subdir recursively. | |
| Directory.Delete(subDir.FullName, recursive: true); | |
| DirectoryInfo rootSubDir = testDir.CreateSubdirectory("test_issue84344"); | |
| DirectoryInfo currentSubDir = rootSubDir; | |
| for (int i = 0; i < depth; i++) | |
| { | |
| currentSubDir = currentSubDir.CreateSubdirectory("a"); | |
| } | |
| // Now delete the subdir recursively, starting from the top of the created tree. | |
| try | |
| { | |
| Directory.Delete(rootSubDir.FullName, recursive: true); | |
| } | |
| finally | |
| { | |
| try | |
| { | |
| if (rootSubDir.Exists) | |
| { | |
| Directory.Delete(rootSubDir.FullName, recursive: true); | |
| } | |
| } | |
| catch (IOException) | |
| { | |
| } | |
| catch (UnauthorizedAccessException) | |
| { | |
| } | |
| } |
| [Fact] | ||
| [PlatformSpecific(TestPlatforms.Windows)] | ||
| public void RecursiveDelete_issue84344() | ||
| { | ||
| // Test the scenario described in the GitHub issue #84344 where the nesting depth of subdirectories is huge (14000). | ||
| // But this test will take a long time to run if we use 14000 as the depth, because the original depth number is too deep. | ||
| // Therefore we reduce the depth to 9000 for quick testing purpose but with a large enough number. | ||
| // See https://github.com/dotnet/runtime/issues/84344 | ||
| // Although the scenario on the original issue is done on Windows, | ||
| // we should be able to run this test on all platforms to test the recursive Directory.Delete(). | ||
| // Unfortunately, on Unix/Linux the max path length is limited to 4096, therefore this test is targeted to only runs on Windows. | ||
| DirectoryInfo testDir = new DirectoryInfo(GetTestFilePath()); | ||
| var depth = 9000; | ||
|
|
||
| // Create a specific subdir only for this test. | ||
| DirectoryInfo subDir = testDir.CreateSubdirectory("test_issue84344"); | ||
| for (int i = 0; i < depth; i++) |
There was a problem hiding this comment.
This adds a very deep nesting (9000) using CreateSubdirectory in a tight loop, but the test isn't marked [OuterLoop]. This is likely to be very slow/flaky in CI and duplicates coverage with the existing RecursiveDelete_DeepNesting test just above. Consider reusing/adjusting the existing deep-nesting test (including its [OuterLoop] annotation) rather than adding a second long-running variant.
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/Delete.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/Delete.cs
Show resolved
Hide resolved
…Directory/Delete.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| [Fact] | ||
| [PlatformSpecific(TestPlatforms.Windows)] | ||
| public void RecursiveDelete_Issue84344() | ||
| { | ||
| // Test the scenario described in the GitHub issue #84344 where the nesting depth of subdirectories is huge (14000). | ||
| // But this test will take a long time to run if we use 14000 as the depth, because the original depth number is too deep. | ||
| // Therefore we reduce the depth to 9000 for quick testing purpose but with a large enough number. | ||
| // See https://github.com/dotnet/runtime/issues/84344 | ||
| // Although the scenario on the original issue is done on Windows, | ||
| // we should be able to run this test on all platforms to test the recursive Directory.Delete(). | ||
| // Unfortunately, on Unix/Linux the max path length is limited to 4096, therefore this test is targeted to only runs on Windows. | ||
| DirectoryInfo testDir = new DirectoryInfo(GetTestFilePath()); | ||
| var depth = 9000; | ||
|
|
There was a problem hiding this comment.
This test creates 9000 nested directories but is not marked as [OuterLoop]. Given the existing RecursiveDelete_DeepNesting test notes that 10K depth can take minutes, this is likely to be too slow/flaky for inner-loop CI. Consider either marking it as OuterLoop (and/or lowering the depth to the minimum that reproduces the original stack issue).
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/Delete.cs
Outdated
Show resolved
Hide resolved
…Directory/Delete.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…Directory/Delete.cs commit update of the comment to only runs the test on Windows, not on all supported platforms Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…Directory/Delete.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| } | ||
|
|
||
| [Fact] | ||
| [PlatformSpecific(TestPlatforms.Windows)] |
There was a problem hiding this comment.
This test creates 9000 nested directories which makes it very slow (similar to RecursiveDelete_DeepNesting which creates 2000 nested directories and is marked as [OuterLoop]). Consider adding the [OuterLoop] attribute to prevent this test from running in the default test suite, similar to the pattern used in RecursiveDelete_DeepNesting at line 266.
| [PlatformSpecific(TestPlatforms.Windows)] | |
| [PlatformSpecific(TestPlatforms.Windows)] | |
| [OuterLoop("This test is very slow.", ~TestPlatforms.Browser)] |
Fix #84344 to ensure sufficient stack by calling
RuntimeHelpers.EnsureSufficientExecutionStack()when callingDirectory.Deleterecursively.It is implemented in both FileSystem.Unix.cs and FileSystem.Windows.cs
I'm creating in draft first, to check the CI.
Looks like this PR needs to have a unit test to cover at least the same scenario as mentioned in the original issue, but unfortunately I could not find the place or file to add a unit test for calling
Directory.Deleterecursively.PS: apologize it takes me more than 3 months to submit this PR, as I finally have been able to test to run locally 🙏