Skip to content

Comments

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

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

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

Conversation

Copy link
Contributor

Copilot AI commented Feb 24, 2026

Directory.Delete(path, recursive: true) on Windows throws when the directory contains NTFS directory 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 for all IO_REPARSE_TAG_MOUNT_POINT entries, but DeleteVolumeMountPoint only works for volume mount points — it always fails for junctions. That failure was stored in exception, which then prevented RemoveDirectoryInternal from running on the parent even though RemoveDirectory had already successfully 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. When RemoveDirectory succeeds (always the case for junctions), the local exception is discarded.

// Before: DeleteVolumeMountPoint failure stored directly in `exception`,
// blocking parent directory removal even when RemoveDirectory succeeded.

// After: failure captured locally; discarded if RemoveDirectory succeeds.
Exception? mountPointException = null;
if (!Interop.Kernel32.DeleteVolumeMountPoint(mountPoint))
{
    // ... capture in mountPointException, not exception
}
if (!Interop.Kernel32.RemoveDirectory(dirPath))
{
    if (exception == null)
        exception = mountPointException ?? Win32Marshal.GetExceptionForWin32Error(errorCode, fileName);
}
// RemoveDirectory success → mountPointException discarded (junction case)

Customer Impact

Any recursive directory deletion containing NTFS junctions fails. This is widespread — pnpm creates junctions extensively in node_modules. Applications doing cleanup (CI pipelines, build tools, test teardown) on such directories throw UnauthorizedAccessException or IOException depending on privilege level.

Regression

Not a regression — reproducible on .NET Framework 4.8 and all .NET Core versions tested (.NET 6+).

Testing

Added RecursiveDelete_DirectoryContainingJunction (Windows-only) to Directory/Delete.Windows.cs: creates a parent directory containing a junction to a separate target, calls Directory.Delete(parent, true), and asserts the parent is deleted while the junction target is untouched.

Risk

Low. The change is Windows-only, scoped entirely to the name-surrogate reparse point handling branch inside RemoveDirectoryRecursive. Behavior for volume mount points is preserved: when DeleteVolumeMountPoint fails and RemoveDirectory also fails (true mount point access denied scenario), mountPointException is still surfaced. Symbolic links are unaffected (different reparse tag, different code path).

Package authoring no longer needed in .NET 9

IMPORTANT: Starting with .NET 9, you no longer need to edit a NuGet package's csproj to enable building and bump the version.
Keep in mind that we still need package authoring in .NET 8 and older versions.

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>
Copilot AI changed the title [WIP] Fix Directory.Delete failure on directories containing junctions Fix Directory.Delete(path, recursive: true) failing on directories containing junctions Feb 24, 2026
Copilot AI requested a review from ViveliDuCh February 24, 2026 22:35
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