From da0bff0872a7b0a77c45bbd51e254aa97dd5c69a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 06:08:24 +0000 Subject: [PATCH 1/8] Initial plan From c922ff28107f69c04dc27827af2c5cc2485799dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 06:50:14 +0000 Subject: [PATCH 2/8] Implement SafeFileHandle.Name property with OS-level path resolution - Add SystemNative_GetFilePathFromHandle to pal_io.h and pal_io.c: - Linux: readlink /proc/self/fd/ - BSD/macOS: fcntl(fd, F_GETPATH, buf) - Other: errno=ENOTSUP, return -1 - Add HAVE_F_GETPATH cmake check and pal_config.h.in define - Add DllImportEntry for new native function - Add Interop.GetFilePathFromHandle.cs Unix P/Invoke wrapper - Add public Name property to SafeFileHandle.cs - Add GetName() implementations to SafeFileHandle.Unix.cs and SafeFileHandle.Windows.cs - Update OSFileStreamStrategy.Name to use SafeFileHandle.Name - Update System.Runtime ref assembly with new Name property - Add Name tests to GetFileType.cs, GetFileType.Unix.cs, GetFileType.Windows.cs - Add NonInheritedFileHandle_IsNotAvailableInChildProcess test Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/65fff4ad-9081-4ef8-b744-02fef761db67 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Interop.GetFilePathFromHandle.cs | 61 +++++++++++++++++ .../tests/ProcessHandlesTests.cs | 67 +++++++++++++++++++ .../Win32/SafeHandles/SafeFileHandle.Unix.cs | 11 +++ .../SafeHandles/SafeFileHandle.Windows.cs | 50 ++++++++++++++ .../Win32/SafeHandles/SafeFileHandle.cs | 33 +++++++++ .../System.Private.CoreLib.Shared.projitems | 3 + .../IO/Strategies/OSFileStreamStrategy.cs | 2 +- .../System.Runtime/ref/System.Runtime.cs | 1 + .../SafeFileHandle/GetFileType.Unix.cs | 21 ++++++ .../SafeFileHandle/GetFileType.Windows.cs | 15 +++++ .../SafeFileHandle/GetFileType.cs | 40 +++++++++++ src/native/libs/Common/pal_config.h.in | 1 + src/native/libs/System.Native/entrypoints.c | 1 + src/native/libs/System.Native/pal_io.c | 44 ++++++++++++ src/native/libs/System.Native/pal_io.h | 8 +++ src/native/libs/configure.cmake | 5 ++ 16 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs new file mode 100644 index 00000000000000..6d9167a7bb15f2 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs @@ -0,0 +1,61 @@ +// 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; + +internal static partial class Interop +{ + internal static partial class Sys + { + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetFilePathFromHandle", SetLastError = true)] + internal static unsafe partial int GetFilePathFromHandle(IntPtr fd, byte* buffer, int bufferSize); + + internal static unsafe string? GetFilePathFromHandle(IntPtr fd) + { + const int InitialBufferSize = 256; + + byte[] arrayBuffer = ArrayPool.Shared.Rent(InitialBufferSize); + try + { + while (true) + { + int result; + fixed (byte* bufPtr = arrayBuffer) + { + result = GetFilePathFromHandle(fd, bufPtr, arrayBuffer.Length); + } + + if (result == 0) + { + // Success: find null terminator to determine the length + ReadOnlySpan span = arrayBuffer; + int length = span.IndexOf((byte)0); + return length < 0 + ? Encoding.UTF8.GetString(arrayBuffer) + : Encoding.UTF8.GetString(arrayBuffer, 0, length); + } + + ErrorInfo errorInfo = GetLastErrorInfo(); + if (errorInfo.Error == Error.ENAMETOOLONG) + { + // Buffer was too small, try again with a larger one + byte[] toReturn = arrayBuffer; + arrayBuffer = ArrayPool.Shared.Rent(toReturn.Length * 2); + ArrayPool.Shared.Return(toReturn); + continue; + } + + // ENOTSUP or any other error: return null to signal unknown path + return null; + } + } + finally + { + ArrayPool.Shared.Return(arrayBuffer); + } + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index cb32430c3af591..cd2edb868379e2 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -540,5 +540,72 @@ public void InheritedHandles_ThrowsForDuplicates() Assert.Throws(() => Process.Start(startInfo)); } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, "RemoteExecutor is not supported")] + 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(); + + // 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. + // (On some platforms the handle value may be reused for something else.) + try + { + using FileStream fs = new FileStream( + new SafeFileHandle(rawHandle, ownsHandle: false), + FileAccess.ReadWrite); + string name = fs.SafeFileHandle.Name; + + // If we can get the name and it matches our path, the handle was incorrectly inherited. + if (string.Equals(name, filePath, StringComparison.OrdinalIgnoreCase) || + string.Equals(name, filePath, StringComparison.Ordinal)) + { + // 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); + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs index f65aaf042bf891..33cc7de930760e 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs @@ -590,5 +590,16 @@ internal long GetFileLength() FileStreamHelpers.CheckFileCall(result, Path); return status.Size; } + + internal string GetName() + { + if (_path is not null) + { + return _path; + } + + string? path = Interop.Sys.GetFilePathFromHandle(handle); + return path ?? SR.IO_UnknownFileName; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index 393154799ce2f4..81c82781528e78 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -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; @@ -461,5 +462,54 @@ unsafe long GetFileLengthCore() return storageReadCapacity.DiskLength; } } + + internal unsafe string GetName() + { + if (_path is not null) + { + return _path; + } + + const int InitialBufferSize = 4096; + char[] buffer = ArrayPool.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.Shared.Rent((int)result); + ArrayPool.Shared.Return(toReturn); + + result = GetFinalPathNameByHandleHelper(buffer); + } + + // If the function fails for any other reason, the return value is zero. + if (result == 0) + { + return SR.IO_UnknownFileName; + } + + // GetFinalPathNameByHandle always returns with extended DOS prefix (\\?\). + // Trim the prefix to keep the result consistent with the path stored in _path. + int start = PathInternal.IsExtended(new string(buffer, 0, (int)result).AsSpan()) ? 4 : 0; + return new string(buffer, start, (int)result - start); + } + finally + { + ArrayPool.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); + } + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs index 2cd5123f739c88..eddf1c69ba4700 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs @@ -40,6 +40,39 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand internal string? Path => _path; + /// + /// Gets the path of the file that this handle represents. + /// + /// + /// A string that represents the path of the file, + /// or [Unknown] if the path cannot be determined. + /// + /// The handle is closed. + /// + /// + /// If the was created by opening a file via + /// or , + /// this property returns the path that was provided to those APIs. + /// + /// + /// If the handle was created from a raw OS handle (for example, via + /// ), this property attempts to + /// retrieve the path from the operating system. + /// On Windows, GetFinalPathNameByHandle is used. + /// On Linux, the /proc/self/fd symlink is read. + /// On macOS and FreeBSD, fcntl(F_GETPATH) is used. + /// On other platforms, [Unknown] is returned. + /// + /// + public string Name + { + get + { + ObjectDisposedException.ThrowIf(IsClosed, this); + return GetName(); + } + } + /// /// Gets the type of the file that this handle represents. /// diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 096397e4998e80..9b0d9796b1037d 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2458,6 +2458,9 @@ Common\Interop\Unix\System.Native\Interop.GetCwd.cs + + Common\Interop\Unix\System.Native\Interop.GetFilePathFromHandle.cs + Common\Interop\Unix\System.Native\Interop.GetDefaultTimeZone.AnyMobile.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs index 81c5370f725de7..87f59f7ecffa61 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs @@ -88,7 +88,7 @@ public sealed override long Position set => Seek(value, SeekOrigin.Begin); } - internal sealed override string Name => _fileHandle.Path ?? SR.IO_UnknownFileName; + internal sealed override string Name => _fileHandle.Name; internal sealed override bool IsClosed => _fileHandle.IsClosed; diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 867cc434767ac1..8eba01974e7e2c 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -23,6 +23,7 @@ public SafeFileHandle() : base (default(bool)) { } public SafeFileHandle(System.IntPtr preexistingHandle, bool ownsHandle) : base (default(bool)) { } public override bool IsInvalid { get { throw null; } } public bool IsAsync { get { throw null; } } + public string Name { get { throw null; } } public System.IO.FileHandleType Type { get { throw null; } } protected override bool ReleaseHandle() { throw null; } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs index e31fbeafc545ba..e9fa5ce25acffe 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs @@ -94,5 +94,26 @@ public void GetFileType_BlockDevice() throw new SkipTestException("Insufficient privileges to open block device"); } } + + [Fact] + [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, "File path resolution not supported")] + public void Name_WhenOpenedFromRawHandle_ReturnsResolvedPath() + { + string path = GetTestFilePath(); + File.WriteAllText(path, "test"); + + using SafeFileHandle originalHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); + using SafeFileHandle handle = new SafeFileHandle(originalHandle.DangerousGetHandle(), ownsHandle: false); + + string name = handle.Name; + + // On platforms that support path resolution from file descriptor (Linux, macOS, FreeBSD), + // the resolved path should end with the file name portion of the original path. + // On other platforms, [Unknown] is returned. + if (name != "[Unknown]") + { + Assert.EndsWith(Path.GetFileName(path), name, StringComparison.Ordinal); + } + } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs index 90ad33818e2f1d..52b740e4e4e303 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs @@ -97,5 +97,20 @@ public unsafe void GetFileType_SymbolicLink() Assert.Equal(FileHandleType.SymbolicLink, handle.Type); } } + + [Fact] + public unsafe void Name_WhenOpenedFromRawHandle_ReturnsResolvedPath() + { + string path = GetTestFilePath(); + File.WriteAllText(path, "test"); + + using SafeFileHandle originalHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); + using SafeFileHandle handle = new SafeFileHandle(originalHandle.DangerousGetHandle(), ownsHandle: false); + + string name = handle.Name; + + // GetFinalPathNameByHandle resolves the path; it should end with the file name. + Assert.EndsWith(Path.GetFileName(path), name, StringComparison.OrdinalIgnoreCase); + } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs index 086d9e850ec6c4..07b69a2233661c 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs @@ -86,5 +86,45 @@ 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); + Assert.Equal(path, handle.Name); + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, "File path resolution not supported")] + public void Name_ClosedHandle_ThrowsObjectDisposedException() + { + string path = GetTestFilePath(); + File.WriteAllText(path, "test"); + + SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); + handle.Dispose(); + + Assert.Throws(() => handle.Name); + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, "File path resolution not supported")] + public void Name_WhenOpenedFromHandle_ReturnsPathOrUnknown() + { + string path = GetTestFilePath(); + File.WriteAllText(path, "test"); + + using SafeFileHandle originalHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); + using SafeFileHandle handle = new SafeFileHandle(originalHandle.DangerousGetHandle(), ownsHandle: false); + + // The name should either be a resolved path or [Unknown], depending on platform support. + string name = handle.Name; + Assert.NotNull(name); + Assert.NotEmpty(name); + } } } diff --git a/src/native/libs/Common/pal_config.h.in b/src/native/libs/Common/pal_config.h.in index b546abba1c91c3..58e73f06a415a9 100644 --- a/src/native/libs/Common/pal_config.h.in +++ b/src/native/libs/Common/pal_config.h.in @@ -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 diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index ffff33bfadd6cc..a395131dbaf8b6 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -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) diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c index 707d95646ea6b1..cb9e52d34b6443 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -1327,6 +1327,50 @@ 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); + assert(bufferSize >= 0); + + if (bufferSize <= 0) + { + errno = EINVAL; + return -1; + } + +#if HAVE_F_GETPATH + // Apple platforms and FreeBSD support F_GETPATH + if (fcntl((int)fd, F_GETPATH, buffer) == -1) + { + return -1; + } + return 0; +#elif defined(TARGET_LINUX) + // Linux: use /proc/self/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; diff --git a/src/native/libs/System.Native/pal_io.h b/src/native/libs/System.Native/pal_io.h index dbfc304b0b552c..5665fb92040e34 100644 --- a/src/native/libs/System.Native/pal_io.h +++ b/src/native/libs/System.Native/pal_io.h @@ -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 * diff --git a/src/native/libs/configure.cmake b/src/native/libs/configure.cmake index d5fbbc082dd3d4..feb4f3652760a0 100644 --- a/src/native/libs/configure.cmake +++ b/src/native/libs/configure.cmake @@ -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) From b600d324647a3ab86b95333ae855b74c822b48df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 07:44:34 +0000 Subject: [PATCH 3/8] Address PR review feedback: make API internal, fix UNC handling, use SafeFileHandle in interop Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d6f0695b-ba5d-44a8-aa39-c07ea398bcd0 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Interop.GetFilePathFromHandle.cs | 50 +++++++------------ .../tests/ProcessHandlesTests.cs | 9 ++-- .../Win32/SafeHandles/SafeFileHandle.Unix.cs | 5 +- .../SafeHandles/SafeFileHandle.Windows.cs | 30 ++++++++--- .../Win32/SafeHandles/SafeFileHandle.cs | 35 +------------ .../IO/Strategies/OSFileStreamStrategy.cs | 2 +- .../System.Runtime/ref/System.Runtime.cs | 1 - .../SafeFileHandle/GetFileType.Unix.cs | 3 +- .../SafeFileHandle/GetFileType.Windows.cs | 5 +- .../SafeFileHandle/GetFileType.cs | 19 ++----- src/native/libs/System.Native/pal_io.c | 2 +- 11 files changed, 58 insertions(+), 103 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs index 6d9167a7bb15f2..d56f1d157e58b8 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs @@ -5,56 +5,40 @@ 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(IntPtr fd, byte* buffer, int bufferSize); + internal static unsafe partial int GetFilePathFromHandle(SafeFileHandle fd, byte* buffer, int bufferSize); - internal static unsafe string? GetFilePathFromHandle(IntPtr fd) + internal static unsafe string? GetFilePathFromHandle(SafeFileHandle fd) { - const int InitialBufferSize = 256; - - byte[] arrayBuffer = ArrayPool.Shared.Rent(InitialBufferSize); + // 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.Shared.Rent(PathMaxSize); try { - while (true) + int result; + fixed (byte* bufPtr = buffer) { - int result; - fixed (byte* bufPtr = arrayBuffer) - { - result = GetFilePathFromHandle(fd, bufPtr, arrayBuffer.Length); - } - - if (result == 0) - { - // Success: find null terminator to determine the length - ReadOnlySpan span = arrayBuffer; - int length = span.IndexOf((byte)0); - return length < 0 - ? Encoding.UTF8.GetString(arrayBuffer) - : Encoding.UTF8.GetString(arrayBuffer, 0, length); - } - - ErrorInfo errorInfo = GetLastErrorInfo(); - if (errorInfo.Error == Error.ENAMETOOLONG) - { - // Buffer was too small, try again with a larger one - byte[] toReturn = arrayBuffer; - arrayBuffer = ArrayPool.Shared.Rent(toReturn.Length * 2); - ArrayPool.Shared.Return(toReturn); - continue; - } + result = GetFilePathFromHandle(fd, bufPtr, PathMaxSize); + } - // ENOTSUP or any other error: return null to signal unknown path + if (result != 0) + { return null; } + + int length = ((ReadOnlySpan)buffer).IndexOf((byte)0); + return Encoding.UTF8.GetString(buffer, 0, length < 0 ? PathMaxSize : length); } finally { - ArrayPool.Shared.Return(arrayBuffer); + ArrayPool.Shared.Return(buffer); } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index cd2edb868379e2..e37c288866e138 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -578,14 +578,11 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess() // (On some platforms the handle value may be reused for something else.) try { - using FileStream fs = new FileStream( - new SafeFileHandle(rawHandle, ownsHandle: false), - FileAccess.ReadWrite); - string name = fs.SafeFileHandle.Name; + 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) || - string.Equals(name, filePath, StringComparison.Ordinal)) + if (string.Equals(name, filePath, StringComparison.OrdinalIgnoreCase)) { // The file handle was inherited — this is a test failure. return RemoteExecutor.SuccessExitCode - 1; diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs index 33cc7de930760e..f25e3bab43ddd3 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs @@ -591,15 +591,14 @@ internal long GetFileLength() return status.Size; } - internal string GetName() + internal string? GetPath() { if (_path is not null) { return _path; } - string? path = Interop.Sys.GetFilePathFromHandle(handle); - return path ?? SR.IO_UnknownFileName; + return Interop.Sys.GetFilePathFromHandle(this); } } } diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index 81c82781528e78..5ede8bb7897f67 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -463,14 +463,19 @@ unsafe long GetFileLengthCore() } } - internal unsafe string GetName() + internal unsafe string? GetPath() { if (_path is not null) { return _path; } - const int InitialBufferSize = 4096; + 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.Shared.Rent(InitialBufferSize); try { @@ -490,13 +495,26 @@ internal unsafe string GetName() // If the function fails for any other reason, the return value is zero. if (result == 0) { - return SR.IO_UnknownFileName; + return null; } - // GetFinalPathNameByHandle always returns with extended DOS prefix (\\?\). + // GetFinalPathNameByHandle always returns with an extended DOS prefix. // Trim the prefix to keep the result consistent with the path stored in _path. - int start = PathInternal.IsExtended(new string(buffer, 0, (int)result).AsSpan()) ? 4 : 0; - return new string(buffer, start, (int)result - start); + // \\?\UNC\server\share -> \\server\share + // \\?\C:\foo -> C:\foo + ReadOnlySpan 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 { diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs index eddf1c69ba4700..9cc93329930a8d 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs @@ -38,40 +38,7 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand SetHandle(preexistingHandle); } - internal string? Path => _path; - - /// - /// Gets the path of the file that this handle represents. - /// - /// - /// A string that represents the path of the file, - /// or [Unknown] if the path cannot be determined. - /// - /// The handle is closed. - /// - /// - /// If the was created by opening a file via - /// or , - /// this property returns the path that was provided to those APIs. - /// - /// - /// If the handle was created from a raw OS handle (for example, via - /// ), this property attempts to - /// retrieve the path from the operating system. - /// On Windows, GetFinalPathNameByHandle is used. - /// On Linux, the /proc/self/fd symlink is read. - /// On macOS and FreeBSD, fcntl(F_GETPATH) is used. - /// On other platforms, [Unknown] is returned. - /// - /// - public string Name - { - get - { - ObjectDisposedException.ThrowIf(IsClosed, this); - return GetName(); - } - } + internal string? Path => GetPath(); /// /// Gets the type of the file that this handle represents. diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs index 87f59f7ecffa61..81c5370f725de7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs @@ -88,7 +88,7 @@ public sealed override long Position set => Seek(value, SeekOrigin.Begin); } - internal sealed override string Name => _fileHandle.Name; + internal sealed override string Name => _fileHandle.Path ?? SR.IO_UnknownFileName; internal sealed override bool IsClosed => _fileHandle.IsClosed; diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 8eba01974e7e2c..867cc434767ac1 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -23,7 +23,6 @@ public SafeFileHandle() : base (default(bool)) { } public SafeFileHandle(System.IntPtr preexistingHandle, bool ownsHandle) : base (default(bool)) { } public override bool IsInvalid { get { throw null; } } public bool IsAsync { get { throw null; } } - public string Name { get { throw null; } } public System.IO.FileHandleType Type { get { throw null; } } protected override bool ReleaseHandle() { throw null; } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs index e9fa5ce25acffe..a6772e0c425735 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs @@ -104,8 +104,9 @@ public void Name_WhenOpenedFromRawHandle_ReturnsResolvedPath() using SafeFileHandle originalHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); using SafeFileHandle handle = new SafeFileHandle(originalHandle.DangerousGetHandle(), ownsHandle: false); + using FileStream fs = new(handle, FileAccess.Read); - string name = handle.Name; + string name = fs.Name; // On platforms that support path resolution from file descriptor (Linux, macOS, FreeBSD), // the resolved path should end with the file name portion of the original path. diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs index 52b740e4e4e303..8d1a4d70255816 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs @@ -99,15 +99,16 @@ public unsafe void GetFileType_SymbolicLink() } [Fact] - public unsafe void Name_WhenOpenedFromRawHandle_ReturnsResolvedPath() + public void Name_WhenOpenedFromRawHandle_ReturnsResolvedPath() { string path = GetTestFilePath(); File.WriteAllText(path, "test"); using SafeFileHandle originalHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); using SafeFileHandle handle = new SafeFileHandle(originalHandle.DangerousGetHandle(), ownsHandle: false); + using FileStream fs = new(handle, FileAccess.Read); - string name = handle.Name; + string name = fs.Name; // GetFinalPathNameByHandle resolves the path; it should end with the file name. Assert.EndsWith(Path.GetFileName(path), name, StringComparison.OrdinalIgnoreCase); diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs index 07b69a2233661c..22d69e4e26c84c 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs @@ -95,20 +95,8 @@ public void Name_WhenOpenedWithPath_ReturnsPath() File.WriteAllText(path, "test"); using SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); - Assert.Equal(path, handle.Name); - } - - [Fact] - [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, "File path resolution not supported")] - public void Name_ClosedHandle_ThrowsObjectDisposedException() - { - string path = GetTestFilePath(); - File.WriteAllText(path, "test"); - - SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); - handle.Dispose(); - - Assert.Throws(() => handle.Name); + using FileStream fs = new(handle, FileAccess.Read); + Assert.Equal(path, fs.Name); } [Fact] @@ -120,9 +108,10 @@ public void Name_WhenOpenedFromHandle_ReturnsPathOrUnknown() using SafeFileHandle originalHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); using SafeFileHandle handle = new SafeFileHandle(originalHandle.DangerousGetHandle(), ownsHandle: false); + using FileStream fs = new(handle, FileAccess.Read); // The name should either be a resolved path or [Unknown], depending on platform support. - string name = handle.Name; + string name = fs.Name; Assert.NotNull(name); Assert.NotEmpty(name); } diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c index cb9e52d34b6443..61f4c641f7c41e 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -1339,7 +1339,7 @@ int32_t SystemNative_GetFilePathFromHandle(intptr_t fd, char* buffer, int32_t bu } #if HAVE_F_GETPATH - // Apple platforms and FreeBSD support F_GETPATH + // Apple platforms, FreeBSD, and Solaris support F_GETPATH if (fcntl((int)fd, F_GETPATH, buffer) == -1) { return -1; From 635eed0ea330a3e41f3d959573d37b83e3bedf7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 07:47:02 +0000 Subject: [PATCH 4/8] Fix null terminator search to be bounded by PathMaxSize Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d6f0695b-ba5d-44a8-aa39-c07ea398bcd0 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs index d56f1d157e58b8..43171371b14654 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs @@ -33,7 +33,7 @@ internal static partial class Sys return null; } - int length = ((ReadOnlySpan)buffer).IndexOf((byte)0); + int length = ((ReadOnlySpan)buffer).Slice(0, PathMaxSize).IndexOf((byte)0); return Encoding.UTF8.GetString(buffer, 0, length < 0 ? PathMaxSize : length); } finally From 9a7fd3ebf51def8e797712f414e593c8729c7180 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:54:33 +0000 Subject: [PATCH 5/8] Address second round of PR review feedback Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/bb17e842-35f7-4bae-8d99-bf98e21541b2 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Interop.GetFilePathFromHandle.cs | 2 +- .../tests/ProcessHandlesTests.cs | 6 ++++- .../Win32/SafeHandles/SafeFileHandle.Unix.cs | 10 +-------- .../SafeFileHandle/GetFileType.Unix.cs | 22 ------------------- .../SafeFileHandle/GetFileType.Windows.cs | 16 -------------- .../SafeFileHandle/GetFileType.cs | 9 +++----- src/native/libs/System.Native/pal_io.c | 9 +------- 7 files changed, 11 insertions(+), 63 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs index 43171371b14654..3c9803c8840e15 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs @@ -33,7 +33,7 @@ internal static partial class Sys return null; } - int length = ((ReadOnlySpan)buffer).Slice(0, PathMaxSize).IndexOf((byte)0); + int length = buffer.AsSpan(0, PathMaxSize).IndexOf((byte)0); return Encoding.UTF8.GetString(buffer, 0, length < 0 ? PathMaxSize : length); } finally diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index e37c288866e138..67a49fa9218880 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -554,6 +554,10 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess() // Verify the handle is valid in the parent process. Assert.False(fileHandle.IsInvalid); Assert.Equal(FileHandleType.RegularFile, fileHandle.Type); + using (FileStream parentFs = new(fileHandle, FileAccess.ReadWrite, leaveOpen: true)) + { + Assert.Equal(path, parentFs.Name); + } nint rawHandle = fileHandle.DangerousGetHandle(); @@ -575,7 +579,7 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess() } // If the handle appears valid, verify it doesn't point to our file. - // (On some platforms the handle value may be reused for something else.) + // (the Operating System could reuse same value for a different file) try { using FileStream fs = new(handle, FileAccess.ReadWrite); diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs index f25e3bab43ddd3..048d456b1c6da7 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs @@ -591,14 +591,6 @@ internal long GetFileLength() return status.Size; } - internal string? GetPath() - { - if (_path is not null) - { - return _path; - } - - return Interop.Sys.GetFilePathFromHandle(this); - } + internal string? GetPath() => _path ??= Interop.Sys.GetFilePathFromHandle(this); } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs index a6772e0c425735..e31fbeafc545ba 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs @@ -94,27 +94,5 @@ public void GetFileType_BlockDevice() throw new SkipTestException("Insufficient privileges to open block device"); } } - - [Fact] - [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, "File path resolution not supported")] - public void Name_WhenOpenedFromRawHandle_ReturnsResolvedPath() - { - string path = GetTestFilePath(); - File.WriteAllText(path, "test"); - - using SafeFileHandle originalHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); - using SafeFileHandle handle = new SafeFileHandle(originalHandle.DangerousGetHandle(), ownsHandle: false); - using FileStream fs = new(handle, FileAccess.Read); - - string name = fs.Name; - - // On platforms that support path resolution from file descriptor (Linux, macOS, FreeBSD), - // the resolved path should end with the file name portion of the original path. - // On other platforms, [Unknown] is returned. - if (name != "[Unknown]") - { - Assert.EndsWith(Path.GetFileName(path), name, StringComparison.Ordinal); - } - } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs index 8d1a4d70255816..90ad33818e2f1d 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs @@ -97,21 +97,5 @@ public unsafe void GetFileType_SymbolicLink() Assert.Equal(FileHandleType.SymbolicLink, handle.Type); } } - - [Fact] - public void Name_WhenOpenedFromRawHandle_ReturnsResolvedPath() - { - string path = GetTestFilePath(); - File.WriteAllText(path, "test"); - - using SafeFileHandle originalHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); - using SafeFileHandle handle = new SafeFileHandle(originalHandle.DangerousGetHandle(), ownsHandle: false); - using FileStream fs = new(handle, FileAccess.Read); - - string name = fs.Name; - - // GetFinalPathNameByHandle resolves the path; it should end with the file name. - Assert.EndsWith(Path.GetFileName(path), name, StringComparison.OrdinalIgnoreCase); - } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs index 22d69e4e26c84c..dd18e8e5798294 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs @@ -101,19 +101,16 @@ public void Name_WhenOpenedWithPath_ReturnsPath() [Fact] [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, "File path resolution not supported")] - public void Name_WhenOpenedFromHandle_ReturnsPathOrUnknown() + 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 SafeFileHandle(originalHandle.DangerousGetHandle(), ownsHandle: false); + using SafeFileHandle handle = new(originalHandle.DangerousGetHandle(), ownsHandle: false); using FileStream fs = new(handle, FileAccess.Read); - // The name should either be a resolved path or [Unknown], depending on platform support. - string name = fs.Name; - Assert.NotNull(name); - Assert.NotEmpty(name); + Assert.Equal(path, fs.Name); } } } diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c index 61f4c641f7c41e..c6a428746d64e5 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -1329,14 +1329,7 @@ int32_t SystemNative_ReadLink(const char* path, char* buffer, int32_t bufferSize int32_t SystemNative_GetFilePathFromHandle(intptr_t fd, char* buffer, int32_t bufferSize) { - assert(buffer != NULL || bufferSize == 0); - assert(bufferSize >= 0); - - if (bufferSize <= 0) - { - errno = EINVAL; - return -1; - } + assert(buffer != NULL && bufferSize > 0); #if HAVE_F_GETPATH // Apple platforms, FreeBSD, and Solaris support F_GETPATH From a33d3f9af87ef8e01a050225b0ab205404e62fda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:32:19 +0000 Subject: [PATCH 6/8] Fix build failure: remove leaveOpen param, save rawHandle before FileStream, remove redundant SkipOnPlatform Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/45057e03-9697-4819-a2c0-72b02fddccf6 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/ProcessHandlesTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index 67a49fa9218880..7e0fc563ad0f83 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -542,7 +542,6 @@ public void InheritedHandles_ThrowsForDuplicates() } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, "RemoteExecutor is not supported")] public void NonInheritedFileHandle_IsNotAvailableInChildProcess() { string path = Path.GetTempFileName(); @@ -554,13 +553,14 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess() // Verify the handle is valid in the parent process. Assert.False(fileHandle.IsInvalid); Assert.Equal(FileHandleType.RegularFile, fileHandle.Type); - using (FileStream parentFs = new(fileHandle, FileAccess.ReadWrite, leaveOpen: true)) + 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); } - nint rawHandle = fileHandle.DangerousGetHandle(); - // Spawn a child process with InheritedHandles = [] (no handles inherited), // passing the raw handle value and the file path. RemoteInvokeOptions options = new() { CheckExitCode = true }; From f0d33c3950fbc8f251a63c6e97cef19a5007317a Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 8 Apr 2026 13:13:14 +0200 Subject: [PATCH 7/8] fix OSX build --- src/native/libs/System.Native/pal_io.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c index c6a428746d64e5..0935a1bfefbf70 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -1333,6 +1333,7 @@ int32_t SystemNative_GetFilePathFromHandle(intptr_t fd, char* buffer, int32_t bu #if HAVE_F_GETPATH // Apple platforms, FreeBSD, and Solaris support F_GETPATH + (void)bufferSize; if (fcntl((int)fd, F_GETPATH, buffer) == -1) { return -1; From b97f00327434872d5ffa0db090b5caecfa31477a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:02:15 +0000 Subject: [PATCH 8/8] Address reviewer feedback: centralize caching in Path, add MAXPATHLEN guard, return SR.IO_UnknownFileName on Unix failure Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/c0bb5bd4-b358-456c-a074-116c8a17d415 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Unix/System.Native/Interop.GetFilePathFromHandle.cs | 4 ++-- .../src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs | 2 +- .../Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs | 5 ----- .../src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs | 2 +- src/native/libs/System.Native/pal_io.c | 6 +++++- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs index 3c9803c8840e15..ae998437e089d6 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs @@ -14,7 +14,7 @@ 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) + 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. @@ -30,7 +30,7 @@ internal static partial class Sys if (result != 0) { - return null; + return SR.IO_UnknownFileName; } int length = buffer.AsSpan(0, PathMaxSize).IndexOf((byte)0); diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs index 048d456b1c6da7..1866d3dae68c48 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs @@ -591,6 +591,6 @@ internal long GetFileLength() return status.Size; } - internal string? GetPath() => _path ??= Interop.Sys.GetFilePathFromHandle(this); + internal string? GetPath() => Interop.Sys.GetFilePathFromHandle(this); } } diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index 5ede8bb7897f67..6325621e4d95a8 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -465,11 +465,6 @@ unsafe long GetFileLengthCore() internal unsafe string? GetPath() { - if (_path is not null) - { - return _path; - } - const int InitialBufferSize = #if DEBUG 26; // use a small size in debug builds to ensure the buffer-growing path is exercised diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs index 9cc93329930a8d..d674ded856e1e7 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs @@ -38,7 +38,7 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand SetHandle(preexistingHandle); } - internal string? Path => GetPath(); + internal string? Path => _path ??= GetPath(); /// /// Gets the type of the file that this handle represents. diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c index 0935a1bfefbf70..d63ddd41dce64b 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -1333,7 +1333,11 @@ int32_t SystemNative_GetFilePathFromHandle(intptr_t fd, char* buffer, int32_t bu #if HAVE_F_GETPATH // Apple platforms, FreeBSD, and Solaris support F_GETPATH - (void)bufferSize; + if (bufferSize < MAXPATHLEN) + { + errno = ENAMETOOLONG; + return -1; + } if (fcntl((int)fd, F_GETPATH, buffer) == -1) { return -1;