Skip to content

Commit

Permalink
[release/6.0-preview6] [FileStream] handle UNC and device paths (#54595)
Browse files Browse the repository at this point in the history
* 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 <adam.sitnik@gmail.com>
  • Loading branch information
github-actions[bot] and adamsitnik authored Jun 25, 2021
1 parent 589a29b commit 1ddcf4c
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

internal static partial class Interop
{
internal static partial class Kernel32
{
// Value taken from https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfileinformationbyhandle#remarks:
internal const int FileAllocationInfo = 5;

internal struct FILE_ALLOCATION_INFO
{
internal long AllocationSize;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.IO.Strategies;
using System.Runtime.InteropServices;
using System.Threading;

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

private SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle, FileOptions fileOptions) : base(ownsHandle)
{
SetHandle(preexistingHandle);

_fileOptions = fileOptions;
}

public bool IsAsync => (GetFileOptions() & FileOptions.Asynchronous) != 0;

internal bool CanSeek => !IsClosed && GetFileType() == Interop.Kernel32.FileTypes.FILE_TYPE_DISK;
Expand All @@ -43,59 +37,106 @@ internal static unsafe SafeFileHandle Open(string fullPath, FileMode mode, FileA
{
using (DisableMediaInsertionPrompt.Create())
{
SafeFileHandle fileHandle = new SafeFileHandle(
NtCreateFile(fullPath, mode, access, share, options, preallocationSize),
ownsHandle: true,
options);
// we don't use NtCreateFile as there is no public and reliable way
// of converting DOS to NT file paths (RtlDosPathNameToRelativeNtPathName_U_WithStatus is not documented)
SafeFileHandle fileHandle = CreateFile(fullPath, mode, access, share, options);

if (FileStreamHelpers.ShouldPreallocate(preallocationSize, access, mode))
{
Preallocate(fullPath, preallocationSize, fileHandle);
}

fileHandle.InitThreadPoolBindingIfNeeded();

return fileHandle;
}
}

private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize)
private static unsafe SafeFileHandle CreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
{
uint ntStatus;
IntPtr fileHandle;
Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = default;
if ((share & FileShare.Inheritable) != 0)
{
secAttrs = new Interop.Kernel32.SECURITY_ATTRIBUTES
{
nLength = (uint)sizeof(Interop.Kernel32.SECURITY_ATTRIBUTES),
bInheritHandle = Interop.BOOL.TRUE
};
}

const string MandatoryNtPrefix = @"\??\";
if (fullPath.StartsWith(MandatoryNtPrefix, StringComparison.Ordinal))
int fAccess =
((access & FileAccess.Read) == FileAccess.Read ? Interop.Kernel32.GenericOperations.GENERIC_READ : 0) |
((access & FileAccess.Write) == FileAccess.Write ? Interop.Kernel32.GenericOperations.GENERIC_WRITE : 0);

// Our Inheritable bit was stolen from Windows, but should be set in
// the security attributes class. Don't leave this bit set.
share &= ~FileShare.Inheritable;

// Must use a valid Win32 constant here...
if (mode == FileMode.Append)
{
(ntStatus, fileHandle) = Interop.NtDll.NtCreateFile(fullPath, mode, access, share, options, preallocationSize);
mode = FileMode.OpenOrCreate;
}
else

int flagsAndAttributes = (int)options;

// For mitigating local elevation of privilege attack through named pipes
// make sure we always call CreateFile with SECURITY_ANONYMOUS so that the
// named pipe server can't impersonate a high privileged client security context
// (note that this is the effective default on CreateFile2)
flagsAndAttributes |= (Interop.Kernel32.SecurityOptions.SECURITY_SQOS_PRESENT | Interop.Kernel32.SecurityOptions.SECURITY_ANONYMOUS);

SafeFileHandle fileHandle = Interop.Kernel32.CreateFile(fullPath, fAccess, share, &secAttrs, mode, flagsAndAttributes, IntPtr.Zero);
if (fileHandle.IsInvalid)
{
var vsb = new ValueStringBuilder(stackalloc char[256]);
vsb.Append(MandatoryNtPrefix);
// Return a meaningful exception with the full path.

if (fullPath.StartsWith(@"\\?\", StringComparison.Ordinal)) // NtCreateFile does not support "\\?\" prefix, only "\??\"
{
vsb.Append(fullPath.AsSpan(4));
}
else
// NT5 oddity - when trying to open "C:\" as a Win32FileStream,
// we usually get ERROR_PATH_NOT_FOUND from the OS. We should
// probably be consistent w/ every other directory.
int errorCode = Marshal.GetLastPInvokeError();

if (errorCode == Interop.Errors.ERROR_PATH_NOT_FOUND && fullPath!.Length == PathInternal.GetRootLength(fullPath))
{
vsb.Append(fullPath);
errorCode = Interop.Errors.ERROR_ACCESS_DENIED;
}

(ntStatus, fileHandle) = Interop.NtDll.NtCreateFile(vsb.AsSpan(), mode, access, share, options, preallocationSize);
vsb.Dispose();
throw Win32Marshal.GetExceptionForWin32Error(errorCode, fullPath);
}

switch (ntStatus)
{
case Interop.StatusOptions.STATUS_SUCCESS:
return fileHandle;
case Interop.StatusOptions.STATUS_DISK_FULL:
throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, fullPath, preallocationSize));
// NtCreateFile has a bug and it reports STATUS_INVALID_PARAMETER for files
// that are too big for the current file system. Example: creating a 4GB+1 file on a FAT32 drive.
case Interop.StatusOptions.STATUS_INVALID_PARAMETER when preallocationSize > 0:
case Interop.StatusOptions.STATUS_FILE_TOO_LARGE:
throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, fullPath, preallocationSize));
default:
int error = (int)Interop.NtDll.RtlNtStatusToDosError((int)ntStatus);
throw Win32Marshal.GetExceptionForWin32Error(error, fullPath);
fileHandle._fileOptions = options;
return fileHandle;
}

private static unsafe void Preallocate(string fullPath, long preallocationSize, SafeFileHandle fileHandle)
{
var allocationInfo = new Interop.Kernel32.FILE_ALLOCATION_INFO
{
AllocationSize = preallocationSize
};

if (!Interop.Kernel32.SetFileInformationByHandle(
fileHandle,
Interop.Kernel32.FileAllocationInfo,
&allocationInfo,
(uint)sizeof(Interop.Kernel32.FILE_ALLOCATION_INFO)))
{
int errorCode = Marshal.GetLastPInvokeError();

// we try to mimic the atomic NtCreateFile here:
// if preallocation fails, close the handle and delete the file
fileHandle.Dispose();
Interop.Kernel32.DeleteFile(fullPath);

switch (errorCode)
{
case Interop.Errors.ERROR_DISK_FULL:
throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, fullPath, preallocationSize));
case Interop.Errors.ERROR_FILE_TOO_LARGE:
throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, fullPath, preallocationSize));
default:
throw Win32Marshal.GetExceptionForWin32Error(errorCode, fullPath);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,9 @@
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.FILE_BASIC_INFO.cs">
<Link>Common\Interop\Windows\Kernel32\Interop.FILE_BASIC_INFO.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.FILE_ALLOCATION_INFO.cs">
<Link>Common\Interop\Windows\Kernel32\Interop.FILE_ALLOCATION_INFO.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.FILE_END_OF_FILE_INFO.cs">
<Link>Common\Interop\Windows\Kernel32\Interop.FILE_END_OF_FILE_INFO.cs</Link>
</Compile>
Expand Down Expand Up @@ -1611,6 +1614,9 @@
<Compile Include="$(CommonPath)Interop\Windows\Interop.UNICODE_STRING.cs">
<Link>Common\Interop\Windows\Interop.UNICODE_STRING.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.SecurityOptions.cs">
<Link>Common\Interop\Windows\Kernel32\Interop.SecurityOptions.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Windows\Interop.SECURITY_QUALITY_OF_SERVICE.cs">
<Link>Common\Interop\Windows\Interop.SECURITY_QUALITY_OF_SERVICE.cs</Link>
</Compile>
Expand Down

0 comments on commit 1ddcf4c

Please sign in to comment.