Fix Directory.Delete(path, recursive: true) failing on directories containing junctions#124830
Fix Directory.Delete(path, recursive: true) failing on directories containing junctions#124830
Conversation
…tions - Use a local `mountPointException` variable in RemoveDirectoryRecursive when handling IO_REPARSE_TAG_MOUNT_POINT reparse points. Previously, if DeleteVolumeMountPoint() failed (as it always does for directory junctions, which share the same reparse tag as volume mount points), the error was stored in `exception`, causing the parent directory deletion to fail even though RemoveDirectory() would succeed. - Now mountPointException is only promoted to exception if RemoveDirectory also fails. If RemoveDirectory succeeds (the normal case for junctions), the DeleteVolumeMountPoint error is discarded. - Add test RecursiveDelete_DirectoryContainingJunction to verify the fix. Co-authored-by: ViveliDuCh <[email protected]>
Co-authored-by: ViveliDuCh <[email protected]>
Co-authored-by: ViveliDuCh <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR fixes a bug where Directory.Delete(path, recursive: true) on Windows fails when the directory contains NTFS junctions. The root cause was that both junctions and volume mount points share the IO_REPARSE_TAG_MOUNT_POINT reparse tag, but the code incorrectly treated DeleteVolumeMountPoint failures for junctions as fatal errors, even though junctions can be successfully removed with RemoveDirectory.
Changes:
- Modified error handling in
RemoveDirectoryRecursiveto stageDeleteVolumeMountPointerrors separately, only promoting them to fatal errors ifRemoveDirectoryalso fails - Added comprehensive test coverage to verify junctions can be deleted recursively without following the junction target
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 | Refactored error handling to stage mount point deletion errors in a local variable, only promoting them to the shared exception if RemoveDirectory also fails; added clarifying comments about junction vs volume mount point behavior |
| src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/Delete.Windows.cs | Added test that creates a parent directory containing a junction, verifies recursive deletion removes the parent and junction without following the junction target |
|
|
||
| // Note that RemoveDirectory on a symbolic link will remove the link itself. | ||
| if (!Interop.Kernel32.RemoveDirectory(Path.Combine(fullPath, fileName)) && exception == null) | ||
| if (!Interop.Kernel32.RemoveDirectory(Path.Combine(fullPath, fileName))) |
There was a problem hiding this comment.
This should be reverted for simplification, and then you can remove if (exception == null).
| if (!Interop.Kernel32.RemoveDirectory(Path.Combine(fullPath, fileName))) | |
| if (!Interop.Kernel32.RemoveDirectory(Path.Combine(fullPath, fileName)) && exception == null) |
There was a problem hiding this comment.
Done in 42c750e. Reverted to && exception == null and removed the inner if block.
| string target = GetTestFilePath(); | ||
| Directory.CreateDirectory(target); | ||
|
|
||
| string linkParent = GetTestFilePath(); |
There was a problem hiding this comment.
Otherwise it makes you think this is also a link.
| string linkParent = GetTestFilePath(); | |
| string parent = GetTestFilePath(); |
|
|
||
| Assert.False(Directory.Exists(linkParent), "parent should be deleted"); | ||
| Assert.True(Directory.Exists(target), "target should still exist after deleting junction"); | ||
| } |
There was a problem hiding this comment.
Can we include tests for the following:
- A junction specified as the directory (as root) for
Delete. - Assert that if the target contains elements, they are not removed (assuming that's the expected).
- A volume mount point specified as the directory (as root) for
Delete- not related to this PR but I see those needing 2 operations, DeleteVolumeMountPoint + RemoveDirectory when they are discovered as part of the enumeration, but when they are the root, we only call RemoveDirectory.
There was a problem hiding this comment.
Added in 42c750e:
RecursiveDelete_DirectoryContainingJunctionnow creates a file inside the target and asserts it survives the junction deletion.Delete_Junction: tests a junction specified as the root argument ofDelete.Delete_VolumeMountPoint: tests a volume mount point as the root argument ofDelete, gated onIsPrivilegedAndNtfsand marked[ActiveIssue("https://github.com/dotnet/runtime/issues/14378")]consistent with the existing mount-volume tests.
…d junction tests - Revert RemoveDirectory condition back to `&& exception == null` form per review suggestion - Rename `linkParent` to `parent` in test - Add assertion that target contents survive junction deletion - Add Delete_Junction test (junction as root of Delete) - Add Delete_VolumeMountPoint test (volume mount point as root) Co-authored-by: jozkee <[email protected]>
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/Delete.Windows.cs
Outdated
Show resolved
Hide resolved
jozkee
left a comment
There was a problem hiding this comment.
Tests using MountHelper.Mount have ActiveIssue #14378, if the test passes in CI, let's follow-up with cleaning them up, if it fails, let's investigate but that shouldn't block this PR.
Thanks, @ViveliDuCh.
Directory.Delete(path, recursive: true)on Windows throws and leaves the parent directory behind when the directory contains an NTFS junction. Symbolic links are unaffected.Description
Root cause:
IO_REPARSE_TAG_MOUNT_POINTis shared by both volume mount points and directory junctions.RemoveDirectoryRecursivecalledDeleteVolumeMountPointfor every entry with this tag, but that API only works for volume mount points — it always fails for junctions. The failure was stored directly inexception, which then suppressed theRemoveDirectorycall's success and prevented the parent from being cleaned up.Fix (
FileSystem.Windows.cs): Stage theDeleteVolumeMountPointerror in a localmountPointExceptioninstead of the sharedexception. Promote it toexceptiononly ifRemoveDirectoryalso fails (using the existing&& exception == nullpattern). For junctions,RemoveDirectorysucceeds and the staged error is discarded.Tests (
Delete.Windows.cs):RecursiveDelete_DirectoryContainingJunction— creates a parent containing a junction to a separate target (with a file inside), callsDirectory.Delete(parent, true), and asserts the parent is removed while the junction target and its contents are untouched.Delete_Junction— tests a junction specified directly as the root argument ofDelete(non-recursive path), verifying the junction is removed without following it.Delete_VolumeMountPoint— tests a volume mount point specified directly as the root argument ofDelete, gated on elevated privileges and NTFS, marked[ActiveIssue]consistent with existing mount-volume tests.Original prompt
This section details on the original issue you should resolve
<issue_title>Directory.Delete(path, recursive: true) fails on directories containing junctions</issue_title>
<issue_description>### Description
On Windows, when recursively deleting a directory containing a junction,
System.IO.Directory.Delete(String, Boolean)fails.Symbolic links work as expected.
Reproduction Steps
Since there's no API to create junctions, this is mostly easily reproduced in powershell:
Expected behavior
The parent directory and junction should be removed successfully.
Actual behavior
The junction is removed, but the parent directory is left behind. The exception message depends on whether the script is run as administrator or not:
Non-admin
Admin
Regression?
I'm able to reproduce this using the script in both powershell 5.1.19041.2673 (.NET Framework 4.8.4614.0), and powershell core 7.2.11 (.NET 6.0.16)
Known Workarounds
Since the junction is removed, the operation can simply be retried to workaround the issue.
Configuration
Reproduced on Windows 22H2 (build 19045.2846)
Other information
No response</issue_description>
<agent_instructions>Look at the existing discussion about it and the suggested/explored fix for this following similar dotnet/runtime existing pattern for the test coverage requested. At the end, give an md file detailing on the reasoning behind each decision taken and the technical explanation of the solution and the background (the request that satisfies and how it does it).</agent_instructions>
Comments on the Issue (you are @copilot in this section)
@ Tagging subscribers to this area: @dotnet/area-system-io See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.Issue Details
Description
On Windows, when recursively deleting a directory containing a junction,
System.IO.Directory.Delete(String, Boolean)fails.Reproduction Steps
Since there's no API to create hardlinks (#69030), this is mostly easily reproduced in powershell: