Skip to content

Fix Directory.Delete(recursive: true) failing on directories containing junctions#124830

Draft
Copilot wants to merge 4 commits intomainfrom
copilot/fix-directory-delete-junctions
Draft

Fix Directory.Delete(recursive: true) failing on directories containing junctions#124830
Copilot wants to merge 4 commits intomainfrom
copilot/fix-directory-delete-junctions

Conversation

Copy link
Contributor

Copilot AI commented Feb 24, 2026

Directory.Delete(path, recursive: true) throws when the target directory contains NTFS junctions. The junction is removed but the parent directory is left behind.

Description

Root cause: IO_REPARSE_TAG_MOUNT_POINT is shared by both volume mount points and directory junctions. RemoveDirectoryRecursive called DeleteVolumeMountPoint on all entries with this tag, but that API only works for volume mount points — it always fails for junctions. That failure was stored directly in exception, blocking RemoveDirectoryInternal from running on the parent even though the subsequent RemoveDirectory call succeeded and removed the junction.

Fix: Capture the DeleteVolumeMountPoint failure in a local mountPointException instead of the loop's exception. Only promote it to exception if RemoveDirectory also fails. For junctions, RemoveDirectory succeeds and the local exception is discarded.

// Before: DeleteVolumeMountPoint failure poisons `exception`,
// blocking parent deletion even though RemoveDirectory succeeded.

// After:
Exception? mountPointException = null;
if (!Interop.Kernel32.DeleteVolumeMountPoint(mountPoint))
    mountPointException = ...; // captured locally, not in `exception`

if (!Interop.Kernel32.RemoveDirectory(dirPath))
{
    if (exception == null)
        // For true volume mount points: surface the unmount error.
        // For junctions: RemoveDirectory succeeds, never reaches here.
        exception = mountPointException ?? Win32Marshal.GetExceptionForWin32Error(errorCode, fileName);
}

Test: Added RecursiveDelete_DirectoryContainingJunction (Windows-only) — creates a parent directory with a junction to a separate target, calls Directory.Delete(parent, true), and asserts the parent is gone while the junction target is untouched.

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:

New-Item -Type Directory 'parent'
New-Item -Type Directory 'target'
New-Item -Type Junction 'parent/link' -Target (Resolve-Path 'target')

try {
    [System.IO.Directory]::Delete((Resolve-Path "parent"), $true)
}
catch {
    $_.Exception.ToString() | Write-Host
}

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
System.Management.Automation.MethodInvocationException: Exception calling "Delete" with "2" argument(s): "Access to the path 'link' is denied."
 ---> System.UnauthorizedAccessException: Access to the path 'link' is denied.
   at System.IO.FileSystem.RemoveDirectoryRecursive(String fullPath, WIN32_FIND_DATA& findData, Boolean topLevel)
   at System.IO.FileSystem.RemoveDirectory(String fullPath, Boolean recursive)
   at CallSite.Target(Closure , CallSite , Type , Object , Boolean )
   --- End of inner exception stack trace ---
   at System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception exception)     
   at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
Admin
System.Management.Automation.MethodInvocationException: Exception calling "Delete" with "2" argument(s): "The parameter is incorrect. : 'link'"
 ---> System.IO.IOException: The parameter is incorrect. : 'link'
   at System.IO.FileSystem.RemoveDirectoryRecursive(String fullPath, WIN32_FIND_DATA& findData, Boolean topLevel)
   at System.IO.FileSystem.RemoveDirectory(String fullPath, Boolean recursive)
   at CallSite.Target(Closure , CallSite , Type , Object , Boolean )
   --- End of inner exception stack trace ---
   at System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception exception)
   at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)

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:

New-Item -Type Directory 'parent'
New-Item -Type Directory 'target'
New-Item -Type Junction 'parent/link' -Target (Re...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes dotnet/runtime#86249

<!-- START COPILOT CODING AGENT TIPS -->
---

✨ Let Copilot coding agent [set things up for you](https://github.com/dotnet/runtime/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits February 24, 2026 22:33
…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 <50237907+ViveliDuCh@users.noreply.github.com>
Co-authored-by: ViveliDuCh <50237907+ViveliDuCh@users.noreply.github.com>
Co-authored-by: ViveliDuCh <50237907+ViveliDuCh@users.noreply.github.com>
Copilot AI changed the title Fix Directory.Delete(path, recursive: true) failing on directories containing junctions Fix Directory.Delete(recursive: true) failing on directories containing junctions Feb 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants