Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FileStream] handle UNC and device paths #54483

Merged
merged 2 commits into from
Jun 23, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/libraries/Common/tests/Common.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
<Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="Common\System\Net\StreamBuffer.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetsWindows)'=='true'">
<Compile Include="$(CommonPath)Interop\Windows\Interop.UNICODE_STRING.cs"
Link="Common\Interop\Windows\Interop.UNICODE_STRING.cs" />
<Compile Include="$(CoreLibSharedDir)System\IO\PathInternal.Windows.cs"
Link="System\IO\PathInternal.Windows.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Xunit;

namespace Tests.System.IO
Expand Down Expand Up @@ -257,5 +260,45 @@ public void GetRootLengthDevice(string path, int length)
Assert.Equal(length + PathInternal.ExtendedPathPrefix.Length, PathInternal.GetRootLength(@"\\?\" + path));
Assert.Equal(length + PathInternal.ExtendedPathPrefix.Length, PathInternal.GetRootLength(@"\\.\" + path));
}

public static TheoryData<string, string> DosToNtPathTest_Data => new TheoryData<string, string>
{
{ @"C:\tests\file.cs", @"\??\C:\tests\file.cs" }, // typical path
{ @"\\?\C:\tests\file.cs", @"\??\C:\tests\file.cs" }, // NtPath with \\?\ prefix
{ @"\\.\device\file.cs", @"\??\device\file.cs" }, // device path with \\.\ prefix
{ @"\\server\file.cs", @"\??\UNC\server\file.cs" }, // UNC path with \\ prefix
{ @"\\?\UNC\server\file", @"\??\UNC\server\file" }, // extended UNC prefix
{ @"\??\C:\tests\file.cs", @"\??\C:\tests\file.cs" }, // NtPath with \??\ prefix (no changes required)
{ @"C:\", @"\??\C:\" }, // a short path
{ @"\\s", @"\??\UNC\s" }, // short UNC path
{ @"\\s\", @"\??\UNC\s\" }, // short UNC path with trailing
{ $@"C:\{string.Join("\\", Enumerable.Repeat("a", PathInternal.MaxShortPath + 1))}", $@"\??\C:\{string.Join("\\", Enumerable.Repeat("a", PathInternal.MaxShortPath + 1))}"}, // long path
};

[Theory, MemberData(nameof(DosToNtPathTest_Data))]
public void DosToNtPathTest(string path, string expected)
{
// first of all, we use an internal Windows API to ensure that expected value is valid
if (path.Length < PathInternal.MaxShortPath) // RtlDosPathNameToRelativeNtPathName_U_WithStatus does not support long paths
adamsitnik marked this conversation as resolved.
Show resolved Hide resolved
{
RtlDosPathNameToRelativeNtPathName_U_WithStatus(path, out Interop.UNICODE_STRING ntFileName, out IntPtr _, IntPtr.Zero);
try
{
Assert.Equal(expected, Marshal.PtrToStringUni(ntFileName.Buffer));
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(ntFileName.Buffer);
}
}

// after that, we test our implementation
var vsb = new ValueStringBuilder(stackalloc char[PathInternal.MaxShortPath]);
PathInternal.DosToNtPath(path, ref vsb);
Assert.Equal(expected, vsb.ToString());

[DllImport(Interop.Libraries.NtDll, CharSet = CharSet.Unicode)]
static extern int RtlDosPathNameToRelativeNtPathName_U_WithStatus(string DosFileName, out Interop.UNICODE_STRING NtFileName, out IntPtr FilePart, IntPtr RelativeName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,31 +56,12 @@ internal static unsafe SafeFileHandle Open(string fullPath, FileMode mode, FileA

private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize)
{
uint ntStatus;
IntPtr fileHandle;
var vsb = new ValueStringBuilder(stackalloc char[PathInternal.MaxShortPath]);

const string MandatoryNtPrefix = @"\??\";
if (fullPath.StartsWith(MandatoryNtPrefix, StringComparison.Ordinal))
{
(ntStatus, fileHandle) = Interop.NtDll.NtCreateFile(fullPath, mode, access, share, options, preallocationSize);
}
else
{
var vsb = new ValueStringBuilder(stackalloc char[256]);
vsb.Append(MandatoryNtPrefix);
PathInternal.DosToNtPath(fullPath, ref vsb);

if (fullPath.StartsWith(@"\\?\", StringComparison.Ordinal)) // NtCreateFile does not support "\\?\" prefix, only "\??\"
{
vsb.Append(fullPath.AsSpan(4));
}
else
{
vsb.Append(fullPath);
}

(ntStatus, fileHandle) = Interop.NtDll.NtCreateFile(vsb.AsSpan(), mode, access, share, options, preallocationSize);
vsb.Dispose();
}
(uint ntStatus, IntPtr fileHandle) = Interop.NtDll.NtCreateFile(vsb.AsSpan(), mode, access, share, options, preallocationSize);
vsb.Dispose();

switch (ntStatus)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ internal static partial class PathInternal
internal const string DirectorySeparatorCharAsString = "\\";

internal const string ExtendedPathPrefix = @"\\?\";
internal const string NtPrefix = @"\??\";
internal const string UncPathPrefix = @"\\";
internal const string UncExtendedPrefixToInsert = @"?\UNC\";
internal const string UncExtendedPathPrefix = @"\\?\UNC\";
Expand Down Expand Up @@ -409,5 +410,34 @@ internal static bool IsEffectivelyEmpty(ReadOnlySpan<char> path)
}
return true;
}

// this method works only for `fullPath` returned by Path.GetFullPath
// currently we don't have interest in supporting relative paths
internal static void DosToNtPath(ReadOnlySpan<char> fullPath, ref ValueStringBuilder vsb)
adamsitnik marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume for the opposite, translate NT to DOS, a helper like this should be needed, right?
For context #54253 (comment) , a Symlinks API uses DeviceIoControl which returns an NT path that I need to translate to DOS.

This is my silly attempt on doing it:
https://github.com/dotnet/runtime/blob/908530a70613af1172138e48d041d6c5d710d866/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs#L513-L518
Also, I think I mixed the names (DOS and NT) in the comments.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jozkee You could use RtlNtPathNameToDosPathName, but the problem is that it's internal and not documented, so we should not be using that.

The mapping that you have pointed to seems to be missing a few translations:

  • \??\UNC\ to \\ (files located on a remote machine)
  • \??\ to \\.\ for devices like names pipes: \\.\pipe\$pipeName

But the question is: are they valid in this context? Can someone create a link to a named pipe or a file located on a network share?

FWIW the best doc about the paths I've found so far: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symbolic links to files on network shares are possible; fsutil behavior set symlinkevaluation can enable or disable them. I don't know about symbolic links to named pipes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mapping that you have pointed to seems to be missing a few translations:

The first example you pointed should still work, as far as I know: Converting \??\UNC\ to \\?\UNC, should be equivalent. The difference is that you do not want to pass a path prefixed with \??\ to the user, and should always translate it to \\?\.

Regarding the second example: Have you seen cases where Windows gives you a path prefixed with \??\ but you were expecting it to start with \\.\? And how would you know this?

{
vsb.Append(NtPrefix);

if (fullPath.Length >= 3 && fullPath[0] == '\\' && fullPath[1] == '\\')
{
// \\.\ (Device) or \\?\ (NtPath)
adamsitnik marked this conversation as resolved.
Show resolved Hide resolved
if (fullPath.Length >= 4 && fullPath[3] == '\\' && (fullPath[2] == '.' || fullPath[2] == '?'))
{
vsb.Append(fullPath.Slice(NtPrefix.Length));
}
else // \\ (UNC)
{
vsb.Append(@"UNC\");
vsb.Append(fullPath.Slice(2));
}
}
else if (fullPath.Length >= 4 && fullPath[0] == '\\' && fullPath[1] == '?' && fullPath[2] == '?' && fullPath[3] == '\\') // \??\
{
vsb.Append(fullPath.Slice(NtPrefix.Length));
}
else
{
vsb.Append(fullPath);
}
}
}
}