Skip to content

Commit 1ddcf4c

Browse files
[release/6.0-preview6] [FileStream] handle UNC and device paths (#54595)
* handle UNC and device paths * stop using NtCreateFile as there is no public and reliable way of mapping DOS to NT paths Co-authored-by: Adam Sitnik <[email protected]>
1 parent 589a29b commit 1ddcf4c

File tree

3 files changed

+106
-43
lines changed

3 files changed

+106
-43
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
internal static partial class Interop
5+
{
6+
internal static partial class Kernel32
7+
{
8+
// Value taken from https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfileinformationbyhandle#remarks:
9+
internal const int FileAllocationInfo = 5;
10+
11+
internal struct FILE_ALLOCATION_INFO
12+
{
13+
internal long AllocationSize;
14+
}
15+
}
16+
}

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

Lines changed: 84 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
using System;
55
using System.Diagnostics;
66
using System.IO;
7-
using System.Text;
7+
using System.IO.Strategies;
8+
using System.Runtime.InteropServices;
89
using System.Threading;
910

1011
namespace Microsoft.Win32.SafeHandles
@@ -24,13 +25,6 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand
2425
SetHandle(preexistingHandle);
2526
}
2627

27-
private SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle, FileOptions fileOptions) : base(ownsHandle)
28-
{
29-
SetHandle(preexistingHandle);
30-
31-
_fileOptions = fileOptions;
32-
}
33-
3428
public bool IsAsync => (GetFileOptions() & FileOptions.Asynchronous) != 0;
3529

3630
internal bool CanSeek => !IsClosed && GetFileType() == Interop.Kernel32.FileTypes.FILE_TYPE_DISK;
@@ -43,59 +37,106 @@ internal static unsafe SafeFileHandle Open(string fullPath, FileMode mode, FileA
4337
{
4438
using (DisableMediaInsertionPrompt.Create())
4539
{
46-
SafeFileHandle fileHandle = new SafeFileHandle(
47-
NtCreateFile(fullPath, mode, access, share, options, preallocationSize),
48-
ownsHandle: true,
49-
options);
40+
// we don't use NtCreateFile as there is no public and reliable way
41+
// of converting DOS to NT file paths (RtlDosPathNameToRelativeNtPathName_U_WithStatus is not documented)
42+
SafeFileHandle fileHandle = CreateFile(fullPath, mode, access, share, options);
43+
44+
if (FileStreamHelpers.ShouldPreallocate(preallocationSize, access, mode))
45+
{
46+
Preallocate(fullPath, preallocationSize, fileHandle);
47+
}
5048

5149
fileHandle.InitThreadPoolBindingIfNeeded();
5250

5351
return fileHandle;
5452
}
5553
}
5654

57-
private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize)
55+
private static unsafe SafeFileHandle CreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
5856
{
59-
uint ntStatus;
60-
IntPtr fileHandle;
57+
Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = default;
58+
if ((share & FileShare.Inheritable) != 0)
59+
{
60+
secAttrs = new Interop.Kernel32.SECURITY_ATTRIBUTES
61+
{
62+
nLength = (uint)sizeof(Interop.Kernel32.SECURITY_ATTRIBUTES),
63+
bInheritHandle = Interop.BOOL.TRUE
64+
};
65+
}
6166

62-
const string MandatoryNtPrefix = @"\??\";
63-
if (fullPath.StartsWith(MandatoryNtPrefix, StringComparison.Ordinal))
67+
int fAccess =
68+
((access & FileAccess.Read) == FileAccess.Read ? Interop.Kernel32.GenericOperations.GENERIC_READ : 0) |
69+
((access & FileAccess.Write) == FileAccess.Write ? Interop.Kernel32.GenericOperations.GENERIC_WRITE : 0);
70+
71+
// Our Inheritable bit was stolen from Windows, but should be set in
72+
// the security attributes class. Don't leave this bit set.
73+
share &= ~FileShare.Inheritable;
74+
75+
// Must use a valid Win32 constant here...
76+
if (mode == FileMode.Append)
6477
{
65-
(ntStatus, fileHandle) = Interop.NtDll.NtCreateFile(fullPath, mode, access, share, options, preallocationSize);
78+
mode = FileMode.OpenOrCreate;
6679
}
67-
else
80+
81+
int flagsAndAttributes = (int)options;
82+
83+
// For mitigating local elevation of privilege attack through named pipes
84+
// make sure we always call CreateFile with SECURITY_ANONYMOUS so that the
85+
// named pipe server can't impersonate a high privileged client security context
86+
// (note that this is the effective default on CreateFile2)
87+
flagsAndAttributes |= (Interop.Kernel32.SecurityOptions.SECURITY_SQOS_PRESENT | Interop.Kernel32.SecurityOptions.SECURITY_ANONYMOUS);
88+
89+
SafeFileHandle fileHandle = Interop.Kernel32.CreateFile(fullPath, fAccess, share, &secAttrs, mode, flagsAndAttributes, IntPtr.Zero);
90+
if (fileHandle.IsInvalid)
6891
{
69-
var vsb = new ValueStringBuilder(stackalloc char[256]);
70-
vsb.Append(MandatoryNtPrefix);
92+
// Return a meaningful exception with the full path.
7193

72-
if (fullPath.StartsWith(@"\\?\", StringComparison.Ordinal)) // NtCreateFile does not support "\\?\" prefix, only "\??\"
73-
{
74-
vsb.Append(fullPath.AsSpan(4));
75-
}
76-
else
94+
// NT5 oddity - when trying to open "C:\" as a Win32FileStream,
95+
// we usually get ERROR_PATH_NOT_FOUND from the OS. We should
96+
// probably be consistent w/ every other directory.
97+
int errorCode = Marshal.GetLastPInvokeError();
98+
99+
if (errorCode == Interop.Errors.ERROR_PATH_NOT_FOUND && fullPath!.Length == PathInternal.GetRootLength(fullPath))
77100
{
78-
vsb.Append(fullPath);
101+
errorCode = Interop.Errors.ERROR_ACCESS_DENIED;
79102
}
80103

81-
(ntStatus, fileHandle) = Interop.NtDll.NtCreateFile(vsb.AsSpan(), mode, access, share, options, preallocationSize);
82-
vsb.Dispose();
104+
throw Win32Marshal.GetExceptionForWin32Error(errorCode, fullPath);
83105
}
84106

85-
switch (ntStatus)
86-
{
87-
case Interop.StatusOptions.STATUS_SUCCESS:
88-
return fileHandle;
89-
case Interop.StatusOptions.STATUS_DISK_FULL:
90-
throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, fullPath, preallocationSize));
91-
// NtCreateFile has a bug and it reports STATUS_INVALID_PARAMETER for files
92-
// that are too big for the current file system. Example: creating a 4GB+1 file on a FAT32 drive.
93-
case Interop.StatusOptions.STATUS_INVALID_PARAMETER when preallocationSize > 0:
94-
case Interop.StatusOptions.STATUS_FILE_TOO_LARGE:
95-
throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, fullPath, preallocationSize));
96-
default:
97-
int error = (int)Interop.NtDll.RtlNtStatusToDosError((int)ntStatus);
98-
throw Win32Marshal.GetExceptionForWin32Error(error, fullPath);
107+
fileHandle._fileOptions = options;
108+
return fileHandle;
109+
}
110+
111+
private static unsafe void Preallocate(string fullPath, long preallocationSize, SafeFileHandle fileHandle)
112+
{
113+
var allocationInfo = new Interop.Kernel32.FILE_ALLOCATION_INFO
114+
{
115+
AllocationSize = preallocationSize
116+
};
117+
118+
if (!Interop.Kernel32.SetFileInformationByHandle(
119+
fileHandle,
120+
Interop.Kernel32.FileAllocationInfo,
121+
&allocationInfo,
122+
(uint)sizeof(Interop.Kernel32.FILE_ALLOCATION_INFO)))
123+
{
124+
int errorCode = Marshal.GetLastPInvokeError();
125+
126+
// we try to mimic the atomic NtCreateFile here:
127+
// if preallocation fails, close the handle and delete the file
128+
fileHandle.Dispose();
129+
Interop.Kernel32.DeleteFile(fullPath);
130+
131+
switch (errorCode)
132+
{
133+
case Interop.Errors.ERROR_DISK_FULL:
134+
throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, fullPath, preallocationSize));
135+
case Interop.Errors.ERROR_FILE_TOO_LARGE:
136+
throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, fullPath, preallocationSize));
137+
default:
138+
throw Win32Marshal.GetExceptionForWin32Error(errorCode, fullPath);
139+
}
99140
}
100141
}
101142

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,9 @@
14251425
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.FILE_BASIC_INFO.cs">
14261426
<Link>Common\Interop\Windows\Kernel32\Interop.FILE_BASIC_INFO.cs</Link>
14271427
</Compile>
1428+
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.FILE_ALLOCATION_INFO.cs">
1429+
<Link>Common\Interop\Windows\Kernel32\Interop.FILE_ALLOCATION_INFO.cs</Link>
1430+
</Compile>
14281431
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.FILE_END_OF_FILE_INFO.cs">
14291432
<Link>Common\Interop\Windows\Kernel32\Interop.FILE_END_OF_FILE_INFO.cs</Link>
14301433
</Compile>
@@ -1611,6 +1614,9 @@
16111614
<Compile Include="$(CommonPath)Interop\Windows\Interop.UNICODE_STRING.cs">
16121615
<Link>Common\Interop\Windows\Interop.UNICODE_STRING.cs</Link>
16131616
</Compile>
1617+
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.SecurityOptions.cs">
1618+
<Link>Common\Interop\Windows\Kernel32\Interop.SecurityOptions.cs</Link>
1619+
</Compile>
16141620
<Compile Include="$(CommonPath)Interop\Windows\Interop.SECURITY_QUALITY_OF_SERVICE.cs">
16151621
<Link>Common\Interop\Windows\Interop.SECURITY_QUALITY_OF_SERVICE.cs</Link>
16161622
</Compile>

0 commit comments

Comments
 (0)