dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.99k stars 4.67k forks source link

Directory.Delete(path, recursive: true) fails on directories containing junctions #86249

Open andrewhickman opened 1 year ago

andrewhickman commented 1 year ago

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

ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/area-system-io See info in 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)`](https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.delete?view=net-7.0#system-io-directory-delete(system-string-system-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 (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 retry operation can simply be retried to workaround the issue. ### Configuration _No response_ ### Other information _No response_
Author: andrewhickman
Assignees: -
Labels: `area-System.IO`, `untriaged`
Milestone: -
danmoseley commented 1 year ago

@andrewhickman thanks for the report, any interest in debugging some? Visual Studio should be able to automatically pull sources and symbols down.

AliveDevil commented 7 months ago

I can provide a failing unit test, that fails in both .NET Framework and .NET 8 using CfApi, if thats of any help.

danmoseley commented 7 months ago

@AliveDevil Any interest in combining that with a fix and offering a PR?

AliveDevil commented 7 months ago

Looked further into this: I'm hitting an issue with Directory.Delete where a folder, which has been customized (i.e. changed folder icon) can't be deleted anymore because the folder has the Read Only attribute. So, unfortunately not directly related to this issue here.

SchreinerK commented 6 months ago

Debugging this issue shows that a call to DeleteVolumeMountPoint causes the error.

The directory link to delete with (Directory.Delete(@"C:\Test", true) is a junction (mklink C:\Test\link C:\Test\sample /J) not a volume mount point.

FileSystem.cs (.net 8.0)
in private static void RemoveDirectoryRecursive(string fullPath, ref Interop.Kernel32.WIN32_FIND_DATA findData, bool topLevel)

if (findData.dwReserved0 == 2684354563u)
{
    string mountPoint = Path.Join(fullPath, stringFromFixedBuffer2, "\\");
    if (!Interop.Kernel32.DeleteVolumeMountPoint(mountPoint) && ex == null)
    {
        lastPInvokeError = Marshal.GetLastPInvokeError();
        if (lastPInvokeError != 0 && lastPInvokeError != 3)
        {
            ex = Win32Marshal.GetExceptionForWin32Error(lastPInvokeError, stringFromFixedBuffer2);
        }
    }
}