Skip to content

Commit 56b250b

Browse files
authored
Handle FileOptions.DeleteOnClose on Unix in SafeFileHandle.Dispose (#55153)
* Handle FileOptions.DeleteOnClose on Unix in SafeFileHandle.Dispose * Move file path into SafeFileHandle on Windows as well Doing so enables cleaning up a bunch of passing around of the file path, while also enabling the path to be fed to more error paths. * Update Net5Compat impl as well
1 parent 7edc27c commit 56b250b

16 files changed

+152
-97
lines changed

src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,18 @@ public void SafeFileHandle_IsAsync_ReturnsCorrectInformation(FileOptions options
5656
}
5757
}
5858

59-
// Unix doesn't directly support DeleteOnClose
60-
// For FileStream created out of path, we mimic it by closing the handle first
61-
// and then unlinking the path
62-
// Since SafeFileHandle does not always have the path and we can't find path for given file descriptor on Unix
63-
// this test runs only on Windows
64-
[PlatformSpecific(TestPlatforms.Windows)]
6559
[Theory]
6660
[InlineData(FileOptions.DeleteOnClose)]
6761
[InlineData(FileOptions.DeleteOnClose | FileOptions.Asynchronous)]
68-
public override void DeleteOnClose_FileDeletedAfterClose(FileOptions options) => base.DeleteOnClose_FileDeletedAfterClose(options);
62+
public void DeleteOnClose_FileDeletedAfterSafeHandleDispose(FileOptions options)
63+
{
64+
string path = GetTestFilePath();
65+
Assert.False(File.Exists(path));
66+
using (SafeFileHandle sfh = File.OpenHandle(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, options))
67+
{
68+
Assert.True(File.Exists(path));
69+
}
70+
Assert.False(File.Exists(path));
71+
}
6972
}
7073
}

src/libraries/System.IO.FileSystem/tests/FileStream/Name.cs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Globalization;
55
using System.Tests;
6+
using Microsoft.Win32.SafeHandles;
67
using Xunit;
78

89
namespace System.IO.Tests
@@ -35,14 +36,29 @@ public void NameNormalizesPath()
3536
}
3637

3738
[Fact]
38-
public void NameReturnsUnknownForHandle()
39+
public void ConstructFileStreamFromHandle_NameMatchesOriginal()
3940
{
40-
using (new ThreadCultureChange(CultureInfo.InvariantCulture))
41-
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite))
42-
using (FileStream fsh = new FileStream(fs.SafeFileHandle, FileAccess.ReadWrite))
43-
{
44-
Assert.Equal("[Unknown]", fsh.Name);
45-
}
41+
string path = GetTestFilePath();
42+
using var _ = new ThreadCultureChange(CultureInfo.InvariantCulture);
43+
44+
using FileStream fs = new FileStream(path, FileMode.Create, FileAccess.ReadWrite);
45+
Assert.Equal(path, fs.Name);
46+
47+
using FileStream fsh = new FileStream(fs.SafeFileHandle, FileAccess.ReadWrite);
48+
Assert.Equal(path, fsh.Name);
49+
}
50+
51+
[Fact]
52+
public void ConstructFileStreamFromHandleClone_NameReturnsUnknown()
53+
{
54+
string path = GetTestFilePath();
55+
using var _ = new ThreadCultureChange(CultureInfo.InvariantCulture);
56+
57+
using FileStream fs = new FileStream(path, FileMode.Create, FileAccess.ReadWrite);
58+
Assert.Equal(path, fs.Name);
59+
60+
using FileStream fsh = new FileStream(new SafeFileHandle(fs.SafeFileHandle.DangerousGetHandle(), ownsHandle: false), FileAccess.ReadWrite);
61+
Assert.Equal("[Unknown]", fsh.Name);
4662
}
4763
}
4864
}

src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.Win32.SafeHandles;
45
using Xunit;
56

67
namespace System.IO.Tests
@@ -79,7 +80,7 @@ public void ValidFileOptions_Encrypted(FileOptions option)
7980
[Theory]
8081
[InlineData(FileOptions.DeleteOnClose)]
8182
[InlineData(FileOptions.DeleteOnClose | FileOptions.Asynchronous)]
82-
public virtual void DeleteOnClose_FileDeletedAfterClose(FileOptions options)
83+
public void DeleteOnClose_FileDeletedAfterClose(FileOptions options)
8384
{
8485
string path = GetTestFilePath();
8586
Assert.False(File.Exists(path));
@@ -89,5 +90,37 @@ public virtual void DeleteOnClose_FileDeletedAfterClose(FileOptions options)
8990
}
9091
Assert.False(File.Exists(path));
9192
}
93+
94+
[Theory]
95+
[InlineData(FileOptions.DeleteOnClose)]
96+
[InlineData(FileOptions.DeleteOnClose | FileOptions.Asynchronous)]
97+
public void DeleteOnClose_FileDeletedAfterSafeHandleRelease(FileOptions options)
98+
{
99+
string path = GetTestFilePath();
100+
Assert.False(File.Exists(path));
101+
102+
using (FileStream fs = CreateFileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x1000, options))
103+
{
104+
Assert.True(File.Exists(path));
105+
106+
bool added = false;
107+
try
108+
{
109+
fs.SafeFileHandle.DangerousAddRef(ref added);
110+
111+
fs.Dispose();
112+
Assert.True(File.Exists(path));
113+
}
114+
finally
115+
{
116+
if (added)
117+
{
118+
fs.SafeFileHandle.DangerousRelease();
119+
}
120+
121+
Assert.False(File.Exists(path));
122+
}
123+
}
124+
}
92125
}
93126
}

src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ internal sealed unsafe class OverlappedValueTaskSource : IValueTaskSource<int>,
4444
internal static readonly IOCompletionCallback s_ioCallback = IOCallback;
4545

4646
internal readonly PreAllocatedOverlapped _preallocatedOverlapped;
47-
private readonly SafeFileHandle _fileHandle;
47+
internal readonly SafeFileHandle _fileHandle;
4848
internal MemoryHandle _memoryHandle;
4949
internal ManualResetValueTaskSourceCore<int> _source; // mutable struct; do not make this readonly
5050
private NativeOverlapped* _overlapped;

src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
1212
{
1313
// not using bool? as it's not thread safe
1414
private volatile NullableBool _canSeek = NullableBool.Undefined;
15+
private bool _deleteOnClose;
1516

1617
public SafeFileHandle() : this(ownsHandle: true)
1718
{
@@ -36,6 +37,7 @@ private static SafeFileHandle Open(string path, Interop.Sys.OpenFlags flags, int
3637
{
3738
Debug.Assert(path != null);
3839
SafeFileHandle handle = Interop.Sys.Open(path, flags, mode);
40+
handle._path = path;
3941

4042
if (handle.IsInvalid)
4143
{
@@ -51,7 +53,7 @@ private static SafeFileHandle Open(string path, Interop.Sys.OpenFlags flags, int
5153

5254
bool isDirectory = (error.Error == Interop.Error.ENOENT) &&
5355
((flags & Interop.Sys.OpenFlags.O_CREAT) != 0
54-
|| !DirectoryExists(Path.GetDirectoryName(Path.TrimEndingDirectorySeparator(path!))!));
56+
|| !DirectoryExists(System.IO.Path.GetDirectoryName(System.IO.Path.TrimEndingDirectorySeparator(path!))!));
5557

5658
Interop.CheckIo(
5759
error.Error,
@@ -117,6 +119,17 @@ protected override bool ReleaseHandle()
117119
// problem trying to unlock it.)
118120
Interop.Sys.FLock(handle, Interop.Sys.LockOperations.LOCK_UN); // ignore any errors
119121

122+
// If DeleteOnClose was requested when constructed, delete the file now.
123+
// (Unix doesn't directly support DeleteOnClose, so we mimic it here.)
124+
if (_deleteOnClose)
125+
{
126+
// Since we still have the file open, this will end up deleting
127+
// it (assuming we're the only link to it) once it's closed, but the
128+
// name will be removed immediately.
129+
Debug.Assert(_path is not null);
130+
Interop.Sys.Unlink(_path); // ignore errors; it's valid that the path may no longer exist
131+
}
132+
120133
// Close the descriptor. Although close is documented to potentially fail with EINTR, we never want
121134
// to retry, as the descriptor could actually have been closed, been subsequently reassigned, and
122135
// be in use elsewhere in the process. Instead, we simply check whether the call was successful.
@@ -234,6 +247,7 @@ private static Interop.Sys.OpenFlags PreOpenConfigurationFromOptions(FileMode mo
234247
private void Init(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize)
235248
{
236249
IsAsync = (options & FileOptions.Asynchronous) != 0;
250+
_deleteOnClose = (options & FileOptions.DeleteOnClose) != 0;
237251

238252
// Lock the file if requested via FileShare. This is only advisory locking. FileShare.None implies an exclusive
239253
// lock on the file and all other modes use a shared lock. While this is not as granular as Windows, not mandatory,

src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ private static unsafe SafeFileHandle CreateFile(string fullPath, FileMode mode,
103103
throw Win32Marshal.GetExceptionForWin32Error(errorCode, fullPath);
104104
}
105105

106+
fileHandle._path = fullPath;
106107
fileHandle._fileOptions = options;
107108
return fileHandle;
108109
}

src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ namespace Microsoft.Win32.SafeHandles
77
{
88
public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
99
{
10+
private string? _path;
11+
1012
public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHandle)
1113
{
1214
SetHandle(preexistingHandle);
1315
}
16+
17+
internal string? Path => _path;
1418
}
1519
}

src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ public static partial class RandomAccess
1717
// that get stackalloced in the Linux kernel.
1818
private const int IovStackThreshold = 8;
1919

20-
internal static long GetFileLength(SafeFileHandle handle, string? path)
20+
internal static long GetFileLength(SafeFileHandle handle)
2121
{
2222
int result = Interop.Sys.FStat(handle, out Interop.Sys.FileStatus status);
23-
FileStreamHelpers.CheckFileCall(result, path);
23+
FileStreamHelpers.CheckFileCall(result, handle.Path);
2424
return status.Size;
2525
}
2626

@@ -29,7 +29,7 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span<byte> buffer
2929
fixed (byte* bufPtr = &MemoryMarshal.GetReference(buffer))
3030
{
3131
int result = Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset);
32-
FileStreamHelpers.CheckFileCall(result, path: null);
32+
FileStreamHelpers.CheckFileCall(result, handle.Path);
3333
return result;
3434
}
3535
}
@@ -64,7 +64,7 @@ internal static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnly
6464
}
6565
}
6666

67-
return FileStreamHelpers.CheckFileCall(result, path: null);
67+
return FileStreamHelpers.CheckFileCall(result, handle.Path);
6868
}
6969

7070
private static ValueTask<int> ReadAtOffsetAsync(SafeFileHandle handle, Memory<byte> buffer, long fileOffset, CancellationToken cancellationToken)
@@ -79,7 +79,7 @@ internal static unsafe int WriteAtOffset(SafeFileHandle handle, ReadOnlySpan<byt
7979
fixed (byte* bufPtr = &MemoryMarshal.GetReference(buffer))
8080
{
8181
int result = Interop.Sys.PWrite(handle, bufPtr, buffer.Length, fileOffset);
82-
FileStreamHelpers.CheckFileCall(result, path: null);
82+
FileStreamHelpers.CheckFileCall(result, handle.Path);
8383
return result;
8484
}
8585
}
@@ -114,7 +114,7 @@ internal static unsafe long WriteGatherAtOffset(SafeFileHandle handle, IReadOnly
114114
}
115115
}
116116

117-
return FileStreamHelpers.CheckFileCall(result, path: null);
117+
return FileStreamHelpers.CheckFileCall(result, handle.Path);
118118
}
119119

120120
private static ValueTask<int> WriteAtOffsetAsync(SafeFileHandle handle, ReadOnlyMemory<byte> buffer, long fileOffset, CancellationToken cancellationToken)

src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,23 @@ public static partial class RandomAccess
1616
{
1717
private static readonly IOCompletionCallback s_callback = AllocateCallback();
1818

19-
internal static unsafe long GetFileLength(SafeFileHandle handle, string? path)
19+
internal static unsafe long GetFileLength(SafeFileHandle handle)
2020
{
2121
Interop.Kernel32.FILE_STANDARD_INFO info;
2222

2323
if (!Interop.Kernel32.GetFileInformationByHandleEx(handle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO)))
2424
{
25-
throw Win32Marshal.GetExceptionForLastWin32Error(path);
25+
throw Win32Marshal.GetExceptionForLastWin32Error(handle.Path);
2626
}
2727

2828
return info.EndOfFile;
2929
}
3030

31-
internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span<byte> buffer, long fileOffset, string? path = null)
31+
internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span<byte> buffer, long fileOffset)
3232
{
3333
if (handle.IsAsync)
3434
{
35-
return ReadSyncUsingAsyncHandle(handle, buffer, fileOffset, path);
35+
return ReadSyncUsingAsyncHandle(handle, buffer, fileOffset);
3636
}
3737

3838
NativeOverlapped overlapped = GetNativeOverlappedForSyncHandle(handle, fileOffset);
@@ -55,12 +55,12 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span<byte> buffer
5555
// For pipes, ERROR_BROKEN_PIPE is the normal end of the pipe.
5656
return 0;
5757
default:
58-
throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
58+
throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
5959
}
6060
}
6161
}
6262

63-
private static unsafe int ReadSyncUsingAsyncHandle(SafeFileHandle handle, Span<byte> buffer, long fileOffset, string? path)
63+
private static unsafe int ReadSyncUsingAsyncHandle(SafeFileHandle handle, Span<byte> buffer, long fileOffset)
6464
{
6565
handle.EnsureThreadPoolBindingInitialized();
6666

@@ -105,7 +105,7 @@ private static unsafe int ReadSyncUsingAsyncHandle(SafeFileHandle handle, Span<b
105105
return 0;
106106

107107
default:
108-
throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
108+
throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
109109
}
110110
}
111111
}
@@ -120,11 +120,11 @@ private static unsafe int ReadSyncUsingAsyncHandle(SafeFileHandle handle, Span<b
120120
}
121121
}
122122

123-
internal static unsafe int WriteAtOffset(SafeFileHandle handle, ReadOnlySpan<byte> buffer, long fileOffset, string? path = null)
123+
internal static unsafe int WriteAtOffset(SafeFileHandle handle, ReadOnlySpan<byte> buffer, long fileOffset)
124124
{
125125
if (handle.IsAsync)
126126
{
127-
return WriteSyncUsingAsyncHandle(handle, buffer, fileOffset, path);
127+
return WriteSyncUsingAsyncHandle(handle, buffer, fileOffset);
128128
}
129129

130130
NativeOverlapped overlapped = GetNativeOverlappedForSyncHandle(handle, fileOffset);
@@ -141,12 +141,12 @@ internal static unsafe int WriteAtOffset(SafeFileHandle handle, ReadOnlySpan<byt
141141
case Interop.Errors.ERROR_NO_DATA: // EOF on a pipe
142142
return 0;
143143
default:
144-
throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
144+
throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
145145
}
146146
}
147147
}
148148

149-
private static unsafe int WriteSyncUsingAsyncHandle(SafeFileHandle handle, ReadOnlySpan<byte> buffer, long fileOffset, string? path)
149+
private static unsafe int WriteSyncUsingAsyncHandle(SafeFileHandle handle, ReadOnlySpan<byte> buffer, long fileOffset)
150150
{
151151
handle.EnsureThreadPoolBindingInitialized();
152152

@@ -193,7 +193,7 @@ private static unsafe int WriteSyncUsingAsyncHandle(SafeFileHandle handle, ReadO
193193
throw new IOException(SR.IO_FileTooLong);
194194

195195
default:
196-
throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
196+
throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
197197
}
198198
}
199199
}
@@ -469,7 +469,7 @@ private static unsafe ValueTask<int> ReadFileScatterAsync(SafeFileHandle handle,
469469
default:
470470
// Error. Callback will not be called.
471471
vts.Dispose();
472-
return ValueTask.FromException<int>(Win32Marshal.GetExceptionForWin32Error(errorCode));
472+
return ValueTask.FromException<int>(Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path));
473473
}
474474
}
475475
}

src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public static long GetLength(SafeFileHandle handle)
2323
{
2424
ValidateInput(handle, fileOffset: 0);
2525

26-
return GetFileLength(handle, path: null);
26+
return GetFileLength(handle);
2727
}
2828

2929
/// <summary>

0 commit comments

Comments
 (0)