[FileStream] handle UNC and device paths (#54483)
authorAdam Sitnik <adam.sitnik@gmail.com>
Wed, 23 Jun 2021 06:53:55 +0000 (08:53 +0200)
committerGitHub <noreply@github.com>
Wed, 23 Jun 2021 06:53:55 +0000 (08:53 +0200)
* stop using NtCreateFile as there is no public and reliable way of mapping DOS to NT paths

src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ALLOCATION_INFO.cs [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ALLOCATION_INFO.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ALLOCATION_INFO.cs
new file mode 100644 (file)
index 0000000..0233626
--- /dev/null
@@ -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;
+        }
+    }
+}
index 36710dc..5aa7a2c 100644 (file)
@@ -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
@@ -24,13 +25,6 @@ namespace Microsoft.Win32.SafeHandles
             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;
@@ -43,10 +37,14 @@ namespace Microsoft.Win32.SafeHandles
         {
             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();
 
@@ -54,48 +52,91 @@ namespace Microsoft.Win32.SafeHandles
             }
         }
 
-        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);
+                }
             }
         }
 
index a7ce085..2af41ff 100644 (file)
     <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>
     <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>