diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs index cc29702789a71f..a49078fd4bbbee 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs @@ -357,32 +357,45 @@ private static void RemoveDirectoryRecursive(string fullPath, ref Interop.Kernel else { // Name surrogate reparse point, don't recurse, simply remove the directory. - // If a mount point, we have to delete the mount point first. + // If a volume mount point, we have to delete the mount point first. + // Note that IO_REPARSE_TAG_MOUNT_POINT is used for both volume mount points + // and directory junctions. DeleteVolumeMountPoint only works for volume mount + // points; for directory junctions, RemoveDirectory handles removal directly. + Exception? mountPointException = null; if (findData.dwReserved0 == Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_MOUNT_POINT) { // Mount point. Unmount using full path plus a trailing '\'. // (Note: This doesn't remove the underlying directory) string mountPoint = Path.Join(fullPath, fileName, PathInternal.DirectorySeparatorCharAsString); - if (!Interop.Kernel32.DeleteVolumeMountPoint(mountPoint) && exception == null) + if (!Interop.Kernel32.DeleteVolumeMountPoint(mountPoint)) { errorCode = Marshal.GetLastPInvokeError(); if (errorCode != Interop.Errors.ERROR_SUCCESS && errorCode != Interop.Errors.ERROR_PATH_NOT_FOUND) { - exception = Win32Marshal.GetExceptionForWin32Error(errorCode, fileName); + mountPointException = Win32Marshal.GetExceptionForWin32Error(errorCode, fileName); } } } // 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))) { - errorCode = Marshal.GetLastPInvokeError(); - if (errorCode != Interop.Errors.ERROR_PATH_NOT_FOUND) + if (exception == null) { - exception = Win32Marshal.GetExceptionForWin32Error(errorCode, fileName); + errorCode = Marshal.GetLastPInvokeError(); + if (errorCode != Interop.Errors.ERROR_PATH_NOT_FOUND) + { + // For a true volume mount point, use its error (it indicates why the + // unmount step failed). If this is a directory junction, RemoveDirectory + // succeeds and this code path is not reached. + exception = mountPointException ?? Win32Marshal.GetExceptionForWin32Error(errorCode, fileName); + } } } + // If RemoveDirectory succeeded, mountPointException is discarded. This correctly + // handles directory junctions: DeleteVolumeMountPoint fails for them (since they + // are not volume mount points), but RemoveDirectory removes them successfully. } } } while (Interop.Kernel32.FindNextFile(handle, ref findData)); diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/Delete.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/Delete.Windows.cs index afe712f9fbab90..84360ffd5a91bb 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/Delete.Windows.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/Delete.Windows.cs @@ -11,6 +11,33 @@ namespace System.IO.Tests { public partial class Directory_Delete_str_bool : Directory_Delete_str { + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] + public void RecursiveDelete_DirectoryContainingJunction() + { + // Junctions (NTFS directory junctions) share the IO_REPARSE_TAG_MOUNT_POINT reparse + // tag with volume mount points, but DeleteVolumeMountPoint only works for volume mount + // points. Ensure that recursive delete succeeds when the directory contains a junction. + string target = GetTestFilePath(); + Directory.CreateDirectory(target); + + string linkParent = GetTestFilePath(); + Directory.CreateDirectory(linkParent); + + string junctionPath = Path.Combine(linkParent, GetTestFileName()); + Assert.True(MountHelper.CreateJunction(junctionPath, target)); + + // Both the junction and the target exist before deletion + Assert.True(Directory.Exists(junctionPath), "junction should exist before delete"); + Assert.True(Directory.Exists(target), "target should exist before delete"); + + // Recursive delete of the parent should succeed and remove the junction without following it + Delete(linkParent, recursive: true); + + Assert.False(Directory.Exists(linkParent), "parent should be deleted"); + Assert.True(Directory.Exists(target), "target should still exist after deleting junction"); + } + [Fact] [PlatformSpecific(TestPlatforms.Windows)] public void RecursiveDelete_NoListDirectoryPermission() // https://github.com/dotnet/runtime/issues/56922