Skip to content

Fix #84344 to ensure sufficient stack by calling RuntimeHelpers.EnsureSufficientExecutionStack()#122761

Open
eriawan wants to merge 8 commits intodotnet:mainfrom
eriawan:fix-recursive-system.io-directory-dotnetruntime-issue
Open

Fix #84344 to ensure sufficient stack by calling RuntimeHelpers.EnsureSufficientExecutionStack()#122761
eriawan wants to merge 8 commits intodotnet:mainfrom
eriawan:fix-recursive-system.io-directory-dotnetruntime-issue

Conversation

@eriawan
Copy link
Member

@eriawan eriawan commented Dec 28, 2025

Fix #84344 to ensure sufficient stack by calling RuntimeHelpers.EnsureSufficientExecutionStack() when calling Directory.Delete recursively.

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.Delete recursively.

PS: apologize it takes me more than 3 months to submit this PR, as I finally have been able to test to run locally 🙏

@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Dec 28, 2025
@eriawan eriawan force-pushed the fix-recursive-system.io-directory-dotnetruntime-issue branch from 1994d04 to b713486 Compare December 29, 2025 13:29
@eriawan
Copy link
Member Author

eriawan commented Dec 29, 2025

Rebase as of 29th December, 2025.

@eriawan
Copy link
Member Author

eriawan commented Dec 29, 2025

Looks like the CI is green (before rebase). I think I will mark this PR as ready (not DRAFT anymore).

@eriawan eriawan marked this pull request as ready for review December 29, 2025 14:47
Copilot AI review requested due to automatic review settings December 29, 2025 14:47
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@eriawan
Copy link
Member Author

eriawan commented Dec 29, 2025

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.
The test should be ok on both Windows, Unix (Linux) and others as long as it supports Directory.Delete and RuntimeHelpers.EnsureSufficientExecutionStack()

@dotnet/area-system-io please review.

Let me know if the tag is incorrect so I should tag the right team.

@eriawan
Copy link
Member Author

eriawan commented Dec 29, 2025

Update to limit the depth to only 3000 for non Windows.

@eriawan
Copy link
Member Author

eriawan commented Dec 30, 2025

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,
if (isDirectory)
{
// Since this is a recursive call, we have to ensure that we have sufficient stack space.
RuntimeHelpers.EnsureSufficientExecutionStack();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank yoju for the feedback, @stephentoub ! 👍
Let me refactor it.

Copilot AI review requested due to automatic review settings February 9, 2026 15:36
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

Comment on lines +306 to +312
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);
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)
{
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +291 to +307
[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++)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
…Directory/Delete.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 9, 2026 15:54
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment on lines +291 to +304
[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;

Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
eriawan and others added 2 commits February 9, 2026 23:08
…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>
Copilot AI review requested due to automatic review settings February 17, 2026 22:18
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

}

[Fact]
[PlatformSpecific(TestPlatforms.Windows)]
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
[PlatformSpecific(TestPlatforms.Windows)]
[PlatformSpecific(TestPlatforms.Windows)]
[OuterLoop("This test is very slow.", ~TestPlatforms.Browser)]

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.IO community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Directory.Delete(string, bool) throws StackOverflowException

3 participants