Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;

internal static partial class Interop
{
internal static partial class Sys
{
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetFilePathFromHandle", SetLastError = true)]
internal static unsafe partial int GetFilePathFromHandle(SafeFileHandle fd, byte* buffer, int bufferSize);

internal static unsafe string GetFilePathFromHandle(SafeFileHandle fd)
{
// PATH_MAX on Linux is 4096; macOS/BSD MAXPATHLEN is 1024.
// Using 4096 covers all Unix platforms without requiring buffer growing.
const int PathMaxSize = 4096;
byte[] buffer = ArrayPool<byte>.Shared.Rent(PathMaxSize);
try
{
int result;
fixed (byte* bufPtr = buffer)
{
result = GetFilePathFromHandle(fd, bufPtr, PathMaxSize);
}

if (result != 0)
{
return SR.IO_UnknownFileName;
}

int length = buffer.AsSpan(0, PathMaxSize).IndexOf((byte)0);
return Encoding.UTF8.GetString(buffer, 0, length < 0 ? PathMaxSize : length);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -540,5 +540,73 @@ public void InheritedHandles_ThrowsForDuplicates()

Assert.Throws<ArgumentException>(() => Process.Start(startInfo));
}

[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public void NonInheritedFileHandle_IsNotAvailableInChildProcess()
{
string path = Path.GetTempFileName();
try
{
// Create an inheritable SafeFileHandle pointing to a regular file.
using SafeFileHandle fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, FileOptions.None);

// Verify the handle is valid in the parent process.
Assert.False(fileHandle.IsInvalid);
Assert.Equal(FileHandleType.RegularFile, fileHandle.Type);
nint rawHandle = fileHandle.DangerousGetHandle();

// Verify FileStream.Name returns the correct path when opened from a handle with a cached path.
using (FileStream parentFs = new(fileHandle, FileAccess.ReadWrite))
{
Assert.Equal(path, parentFs.Name);
}

// Spawn a child process with InheritedHandles = [] (no handles inherited),
// passing the raw handle value and the file path.
RemoteInvokeOptions options = new() { CheckExitCode = true };
options.StartInfo.InheritedHandles = [];

using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke(
static (string handleStr, string filePath) =>
{
nint rawHandle = nint.Parse(handleStr);
using SafeFileHandle handle = new SafeFileHandle(rawHandle, ownsHandle: false);

if (handle.IsInvalid)
{
// Handle is invalid in the child: correct, it was not inherited.
return RemoteExecutor.SuccessExitCode;
}

// If the handle appears valid, verify it doesn't point to our file.
// (the Operating System could reuse same value for a different file)
try
{
using FileStream fs = new(handle, FileAccess.ReadWrite);
string name = fs.Name;

// If we can get the name and it matches our path, the handle was incorrectly inherited.
if (string.Equals(name, filePath, StringComparison.OrdinalIgnoreCase))
{
// The file handle was inherited — this is a test failure.
return RemoteExecutor.SuccessExitCode - 1;
}
}
catch
{
// Expected: the handle is not a valid file handle in this process.
}

return RemoteExecutor.SuccessExitCode;
},
rawHandle.ToString(),
path,
options);
}
finally
{
File.Delete(path);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -590,5 +590,7 @@ internal long GetFileLength()
FileStreamHelpers.CheckFileCall(result, Path);
return status.Size;
}

internal string? GetPath() => Interop.Sys.GetFilePathFromHandle(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
Expand Down Expand Up @@ -461,5 +462,67 @@ unsafe long GetFileLengthCore()
return storageReadCapacity.DiskLength;
}
}

internal unsafe string? GetPath()
{
const int InitialBufferSize =
#if DEBUG
26; // use a small size in debug builds to ensure the buffer-growing path is exercised
#else
4096;
#endif
char[] buffer = ArrayPool<char>.Shared.Rent(InitialBufferSize);
try
{
uint result = GetFinalPathNameByHandleHelper(buffer);

// If the function fails because lpszFilePath is too small to hold the string plus the terminating null
// character, the return value is the required buffer size, in TCHARs, including the null character.
if (result > buffer.Length)
{
char[] toReturn = buffer;
buffer = ArrayPool<char>.Shared.Rent((int)result);
ArrayPool<char>.Shared.Return(toReturn);

result = GetFinalPathNameByHandleHelper(buffer);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it guaranteed that the path cannot change between the two calls? This pattern is often wrapped in a loop (see e.g.

while ((length = Interop.Kernel32.GetEnvironmentVariable(variable, ref builder.GetPinnableReference(), (uint)builder.Capacity)) > builder.Capacity)
{
builder.EnsureCapacity((int)length);
}
)

}

// If the function fails for any other reason, the return value is zero.
if (result == 0)
{
return null;
}

// GetFinalPathNameByHandle always returns with an extended DOS prefix.
// Trim the prefix to keep the result consistent with the path stored in _path.
// \\?\UNC\server\share -> \\server\share
// \\?\C:\foo -> C:\foo
ReadOnlySpan<char> resultSpan = buffer.AsSpan(0, (int)result);
if (PathInternal.IsDeviceUNC(resultSpan))
{
// \\?\UNC\ (8 chars) -> \\ (2 chars)
return string.Concat(PathInternal.UncPathPrefix, resultSpan.Slice(PathInternal.UncExtendedPrefixLength));
}
else if (PathInternal.IsExtended(resultSpan))
{
// \\?\ (4 chars) -> (empty)
return new string(buffer, PathInternal.DevicePrefixLength, (int)result - PathInternal.DevicePrefixLength);
}

return new string(buffer, 0, (int)result);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}

uint GetFinalPathNameByHandleHelper(char[] buf)
{
fixed (char* bufPtr = buf)
{
return Interop.Kernel32.GetFinalPathNameByHandle(this, bufPtr, (uint)buf.Length, Interop.Kernel32.FILE_NAME_NORMALIZED);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand
SetHandle(preexistingHandle);
}

internal string? Path => _path;
internal string? Path => _path ??= GetPath();

/// <summary>
/// Gets the type of the file that this handle represents.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2458,6 +2458,9 @@
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetCwd.cs">
<Link>Common\Interop\Unix\System.Native\Interop.GetCwd.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetFilePathFromHandle.cs">
<Link>Common\Interop\Unix\System.Native\Interop.GetFilePathFromHandle.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetDefaultTimeZone.AnyMobile.cs" Condition="'$(TargetsAndroid)' == 'true' or '$(TargetsLinuxBionic)' == 'true' or '$(IsiOSLike)' == 'true'">
<Link>Common\Interop\Unix\System.Native\Interop.GetDefaultTimeZone.AnyMobile.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,31 @@ public void GetFileType_CachesResult()
Assert.Equal(firstCall, secondCall);
Assert.Equal(FileHandleType.RegularFile, firstCall);
}

[Fact]
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, "File path resolution not supported")]
public void Name_WhenOpenedWithPath_ReturnsPath()
{
string path = GetTestFilePath();
File.WriteAllText(path, "test");

using SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read);
using FileStream fs = new(handle, FileAccess.Read);
Assert.Equal(path, fs.Name);
}

[Fact]
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, "File path resolution not supported")]
public void Name_WhenOpenedFromHandle_ReturnsPath()
{
string path = GetTestFilePath();
File.WriteAllText(path, "test");

using SafeFileHandle originalHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read);
using SafeFileHandle handle = new(originalHandle.DangerousGetHandle(), ownsHandle: false);
using FileStream fs = new(handle, FileAccess.Read);

Assert.Equal(path, fs.Name);
}
}
}
1 change: 1 addition & 0 deletions src/native/libs/Common/pal_config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#cmakedefine01 HAVE_FLOCK64
#cmakedefine01 HAVE_F_DUPFD_CLOEXEC
#cmakedefine01 HAVE_F_DUPFD
#cmakedefine01 HAVE_F_GETPATH
#cmakedefine01 HAVE_O_CLOEXEC
#cmakedefine01 HAVE_GETIFADDRS
#cmakedefine01 HAVE_UTSNAME_DOMAINNAME
Expand Down
1 change: 1 addition & 0 deletions src/native/libs/System.Native/entrypoints.c
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ static const Entry s_sysNative[] =
DllImportEntry(SystemNative_Read)
DllImportEntry(SystemNative_ReadFromNonblocking)
DllImportEntry(SystemNative_ReadLink)
DllImportEntry(SystemNative_GetFilePathFromHandle)
DllImportEntry(SystemNative_Rename)
DllImportEntry(SystemNative_RmDir)
DllImportEntry(SystemNative_Sync)
Expand Down
42 changes: 42 additions & 0 deletions src/native/libs/System.Native/pal_io.c
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,48 @@ int32_t SystemNative_ReadLink(const char* path, char* buffer, int32_t bufferSize
return (int32_t)count;
}

int32_t SystemNative_GetFilePathFromHandle(intptr_t fd, char* buffer, int32_t bufferSize)
{
assert(buffer != NULL && bufferSize > 0);

#if HAVE_F_GETPATH
// Apple platforms, FreeBSD, and Solaris support F_GETPATH
if (bufferSize < MAXPATHLEN)
{
errno = ENAMETOOLONG;
return -1;
}
if (fcntl((int)fd, F_GETPATH, buffer) == -1)
{
return -1;
}
return 0;
#elif defined(TARGET_LINUX)
// Linux: use /proc/self/fd/<fd> symlink
char procPath[32];
snprintf(procPath, sizeof(procPath), "/proc/self/fd/%d", (int)fd);
ssize_t count = readlink(procPath, buffer, (size_t)bufferSize);
if (count == -1)
{
return -1;
}
if (count >= bufferSize)
{
// Buffer too small; the path was truncated
errno = ENAMETOOLONG;
return -1;
}
buffer[count] = '\0';
return 0;
#else
(void)fd;
(void)buffer;
(void)bufferSize;
errno = ENOTSUP;
return -1;
#endif
}

int32_t SystemNative_Rename(const char* oldPath, const char* newPath)
{
int32_t result;
Expand Down
8 changes: 8 additions & 0 deletions src/native/libs/System.Native/pal_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,14 @@ PALEXPORT int32_t SystemNative_INotifyRemoveWatch(intptr_t fd, int32_t wd);
*/
PALEXPORT char* SystemNative_RealPath(const char* path);

/**
* Attempts to get the path of the file associated with the provided file descriptor.
*
* Returns 0 on success, or -1 if an error occurred (in which case, errno is set appropriately).
* On platforms that do not support this operation, returns -1 with errno set to ENOTSUP.
*/
PALEXPORT int32_t SystemNative_GetFilePathFromHandle(intptr_t fd, char* buffer, int32_t bufferSize);

/**
* Attempts to retrieve the ID of the process at the end of the given socket
*
Expand Down
5 changes: 5 additions & 0 deletions src/native/libs/configure.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ check_symbol_exists(
fcntl.h
HAVE_F_DUPFD)

check_symbol_exists(
F_GETPATH
fcntl.h
HAVE_F_GETPATH)

check_function_exists(
getifaddrs
HAVE_GETIFADDRS)
Expand Down
Loading