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 01/15] 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 02/15] 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 03/15] 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 04/15] 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 05/15] 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 06/15] 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 07/15] 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 08/15] 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; From 7d71270a455375e5a0eb5f00a41192b24b9320b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:26:56 +0000 Subject: [PATCH 09/15] Revert all FileStream.Name changes; redesign test to use GetSafeFileHandleId with exclusive file access Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/6ec05b86-f21d-4cf6-af8f-9e376bf6ccdf Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Interop.GetFilePathFromHandle.cs | 45 ------------- .../tests/ProcessHandlesTests.Unix.cs | 16 +++++ .../tests/ProcessHandlesTests.Windows.cs | 13 ++++ .../tests/ProcessHandlesTests.cs | 27 ++++---- .../System.Diagnostics.Process.Tests.csproj | 5 ++ .../Win32/SafeHandles/SafeFileHandle.Unix.cs | 2 - .../SafeHandles/SafeFileHandle.Windows.cs | 63 ------------------- .../Win32/SafeHandles/SafeFileHandle.cs | 2 +- .../System.Private.CoreLib.Shared.projitems | 3 - .../SafeFileHandle/GetFileType.cs | 26 -------- 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 | 42 ------------- src/native/libs/System.Native/pal_io.h | 8 --- src/native/libs/configure.cmake | 5 -- 15 files changed, 46 insertions(+), 213 deletions(-) delete mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs create mode 100644 src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.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 deleted file mode 100644 index ae998437e089d6..00000000000000 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetFilePathFromHandle.cs +++ /dev/null @@ -1,45 +0,0 @@ -// 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.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.Shared.Return(buffer); - } - } - } -} diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs new file mode 100644 index 00000000000000..8deae5701362c8 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -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. + +using Microsoft.Win32.SafeHandles; + +namespace System.Diagnostics.Tests +{ + public partial class ProcessHandlesTests + { + private static partial string GetSafeFileHandleId(SafeFileHandle handle) + { + Interop.Sys.FStat(handle, out Interop.Sys.FileStatus status); + return status.Ino.ToString(); + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index 0734dcb66b6593..2a6ed05d387531 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -181,5 +181,18 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) [LibraryImport(Interop.Libraries.Kernel32)] private static partial int ResumeThread(nint hThread); + + private static unsafe partial string GetSafeFileHandleId(SafeFileHandle handle) + { + const int MaxPath = 4096; + char[] buffer = new char[MaxPath]; + uint result; + fixed (char* ptr = buffer) + { + result = Interop.Kernel32.GetFinalPathNameByHandle(handle, ptr, (uint)MaxPath, Interop.Kernel32.FILE_NAME_NORMALIZED); + } + + return result > 0 ? new string(buffer, 0, (int)result) : handle.DangerousGetHandle().ToString(); + } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index 7e0fc563ad0f83..b107590f6a3a95 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -547,27 +547,22 @@ 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); + // Open with exclusive access so no other process can open the same file. + using SafeFileHandle fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None, 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); - } + string id = GetSafeFileHandleId(fileHandle); - // Spawn a child process with InheritedHandles = [] (no handles inherited), - // passing the raw handle value and the file path. + // Spawn a child process with InheritedHandles = [] (no handles inherited). RemoteInvokeOptions options = new() { CheckExitCode = true }; options.StartInfo.InheritedHandles = []; using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke( - static (string handleStr, string filePath) => + static (string handleStr, string fileId) => { nint rawHandle = nint.Parse(handleStr); using SafeFileHandle handle = new SafeFileHandle(rawHandle, ownsHandle: false); @@ -582,13 +577,11 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess() // (the Operating System could reuse same value for a different file) try { - using FileStream fs = new(handle, FileAccess.ReadWrite); - string name = fs.Name; + string childId = GetSafeFileHandleId(handle); - // If we can get the name and it matches our path, the handle was incorrectly inherited. - if (string.Equals(name, filePath, StringComparison.OrdinalIgnoreCase)) + // If the ID matches, the handle was incorrectly inherited. + if (string.Equals(childId, fileId, StringComparison.OrdinalIgnoreCase)) { - // The file handle was inherited — this is a test failure. return RemoteExecutor.SuccessExitCode - 1; } } @@ -600,7 +593,7 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess() return RemoteExecutor.SuccessExitCode; }, rawHandle.ToString(), - path, + id, options); } finally @@ -608,5 +601,7 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess() File.Delete(path); } } + + private static partial string GetSafeFileHandleId(SafeFileHandle handle); } } diff --git a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj index 9fe9c4c3f3dd72..ddc1d90710e085 100644 --- a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj +++ b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj @@ -68,11 +68,14 @@ Link="Common\Interop\Windows\Kernel32\Interop.SECURITY_ATTRIBUTES.cs" /> + + + 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 1866d3dae68c48..f65aaf042bf891 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,7 +590,5 @@ internal long GetFileLength() FileStreamHelpers.CheckFileCall(result, Path); return status.Size; } - - 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 6325621e4d95a8..393154799ce2f4 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,7 +2,6 @@ // 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; @@ -462,67 +461,5 @@ 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.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 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 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.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 d674ded856e1e7..2cd5123f739c88 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 => _path ??= GetPath(); + internal string? Path => _path; /// /// 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 9b0d9796b1037d..096397e4998e80 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,9 +2458,6 @@ 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.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs index dd18e8e5798294..086d9e850ec6c4 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,31 +86,5 @@ 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); - } } } diff --git a/src/native/libs/Common/pal_config.h.in b/src/native/libs/Common/pal_config.h.in index 58e73f06a415a9..b546abba1c91c3 100644 --- a/src/native/libs/Common/pal_config.h.in +++ b/src/native/libs/Common/pal_config.h.in @@ -9,7 +9,6 @@ #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 a395131dbaf8b6..ffff33bfadd6cc 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -107,7 +107,6 @@ 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 d63ddd41dce64b..707d95646ea6b1 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -1327,48 +1327,6 @@ 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/ 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 5665fb92040e34..dbfc304b0b552c 100644 --- a/src/native/libs/System.Native/pal_io.h +++ b/src/native/libs/System.Native/pal_io.h @@ -804,14 +804,6 @@ 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 feb4f3652760a0..d5fbbc082dd3d4 100644 --- a/src/native/libs/configure.cmake +++ b/src/native/libs/configure.cmake @@ -152,11 +152,6 @@ 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 869635bbcca92032ca35417490a1ecc4676c3acc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:56:56 +0000 Subject: [PATCH 10/15] Address review feedback: FStat error handling, simplify string comparison, convert to ConditionalTheory Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/0a7cf6fd-afe9-4f5f-bb48-d9a2a8c531b9 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/ProcessHandlesTests.Unix.cs | 6 +++- .../tests/ProcessHandlesTests.cs | 31 ++++++++++++------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index 8deae5701362c8..b0a56cf3d8ac6b 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.ComponentModel; using Microsoft.Win32.SafeHandles; namespace System.Diagnostics.Tests @@ -9,7 +10,10 @@ public partial class ProcessHandlesTests { private static partial string GetSafeFileHandleId(SafeFileHandle handle) { - Interop.Sys.FStat(handle, out Interop.Sys.FileStatus status); + if (Interop.Sys.FStat(handle, out Interop.Sys.FileStatus status) != 0) + { + throw new Win32Exception(); + } return status.Ino.ToString(); } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index b107590f6a3a95..c339edef30224e 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -541,8 +541,10 @@ public void InheritedHandles_ThrowsForDuplicates() Assert.Throws(() => Process.Start(startInfo)); } - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void NonInheritedFileHandle_IsNotAvailableInChildProcess() + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(false)] + [InlineData(true)] + public void NonInheritedFileHandle_IsNotAvailableInChildProcess(bool inheritHandle) { string path = Path.GetTempFileName(); try @@ -557,43 +559,48 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess() string id = GetSafeFileHandleId(fileHandle); - // Spawn a child process with InheritedHandles = [] (no handles inherited). RemoteInvokeOptions options = new() { CheckExitCode = true }; - options.StartInfo.InheritedHandles = []; + // When inheritHandle is true, explicitly add the handle to the allow-list so it gets inherited. + // When inheritHandle is false, use an empty allow-list so no handles are inherited. + options.StartInfo.InheritedHandles = inheritHandle ? [fileHandle] : []; using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke( - static (string handleStr, string fileId) => + static (string handleStr, string fileId, string inheritHandleStr) => { + bool shouldBeInherited = bool.Parse(inheritHandleStr); 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; + // Handle is invalid in the child: only acceptable when not inherited. + return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode; } - // If the handle appears valid, verify it doesn't point to our file. + // If the handle appears valid, check whether it points to our file. // (the Operating System could reuse same value for a different file) try { string childId = GetSafeFileHandleId(handle); - // If the ID matches, the handle was incorrectly inherited. - if (string.Equals(childId, fileId, StringComparison.OrdinalIgnoreCase)) + if (childId == fileId) { - return RemoteExecutor.SuccessExitCode - 1; + // Handle points to our file — correct only when inherited. + return shouldBeInherited ? RemoteExecutor.SuccessExitCode : RemoteExecutor.SuccessExitCode - 1; } } catch { - // Expected: the handle is not a valid file handle in this process. + // Handle is not a valid file handle in this process. + return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode; } + // Handle value was reused for a different file — not our handle, acceptable. return RemoteExecutor.SuccessExitCode; }, rawHandle.ToString(), id, + inheritHandle.ToString(), options); } finally From 1954cec871696d5aa7ab519dec4fba12889c9bf2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:21:47 +0000 Subject: [PATCH 11/15] Remove partial method declaration; use regular private static methods in platform files; throw Win32Exception on GetFinalPathNameByHandle failure Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/54131ccf-6ca9-4ae8-b413-3d42208687a6 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/ProcessHandlesTests.Unix.cs | 2 +- .../tests/ProcessHandlesTests.Windows.cs | 9 +++++++-- .../tests/ProcessHandlesTests.cs | 2 -- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index b0a56cf3d8ac6b..90fc76313f1496 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -8,7 +8,7 @@ namespace System.Diagnostics.Tests { public partial class ProcessHandlesTests { - private static partial string GetSafeFileHandleId(SafeFileHandle handle) + private static string GetSafeFileHandleId(SafeFileHandle handle) { if (Interop.Sys.FStat(handle, out Interop.Sys.FileStatus status) != 0) { diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index 2a6ed05d387531..351c7480daf71c 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -182,7 +182,7 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) [LibraryImport(Interop.Libraries.Kernel32)] private static partial int ResumeThread(nint hThread); - private static unsafe partial string GetSafeFileHandleId(SafeFileHandle handle) + private static unsafe string GetSafeFileHandleId(SafeFileHandle handle) { const int MaxPath = 4096; char[] buffer = new char[MaxPath]; @@ -192,7 +192,12 @@ private static unsafe partial string GetSafeFileHandleId(SafeFileHandle handle) result = Interop.Kernel32.GetFinalPathNameByHandle(handle, ptr, (uint)MaxPath, Interop.Kernel32.FILE_NAME_NORMALIZED); } - return result > 0 ? new string(buffer, 0, (int)result) : handle.DangerousGetHandle().ToString(); + if (result == 0) + { + throw new Win32Exception(); + } + + return new string(buffer, 0, (int)result); } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index c339edef30224e..e8c21bd9882db1 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -608,7 +608,5 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess(bool inheritHand File.Delete(path); } } - - private static partial string GetSafeFileHandleId(SafeFileHandle handle); } } From b98513197855fcd4a6e6bc9cdf79954a9bc42a6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:49:39 +0000 Subject: [PATCH 12/15] Address review feedback: MaxPath=32767, include Dev in Unix ID, fix FileShare.None comment Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/e719570a-eb48-4a75-ad51-da013711cec0 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/ProcessHandlesTests.Unix.cs | 2 +- .../tests/ProcessHandlesTests.Windows.cs | 2 +- .../System.Diagnostics.Process/tests/ProcessHandlesTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index 90fc76313f1496..70bc434b2b3c25 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -14,7 +14,7 @@ private static string GetSafeFileHandleId(SafeFileHandle handle) { throw new Win32Exception(); } - return status.Ino.ToString(); + return FormattableString.Invariant($"{status.Dev}:{status.Ino}"); } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index 351c7480daf71c..a95e36aa1a5c7c 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -184,7 +184,7 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) private static unsafe string GetSafeFileHandleId(SafeFileHandle handle) { - const int MaxPath = 4096; + const int MaxPath = 32_767; char[] buffer = new char[MaxPath]; uint result; fixed (char* ptr = buffer) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index e8c21bd9882db1..edb53a5e9f2b4f 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -549,7 +549,7 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess(bool inheritHand string path = Path.GetTempFileName(); try { - // Open with exclusive access so no other process can open the same file. + // Open with FileShare.None so independent opens that honor .NET/OS sharing semantics are prevented. using SafeFileHandle fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None, FileOptions.None); // Verify the handle is valid in the parent process. From 0ddd296748b817790e40c87b5a7204f14e539edd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:21:13 +0000 Subject: [PATCH 13/15] Fix false positive in NonInheritedFileHandle_IsNotAvailableInChildProcess test Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/2da2a1d1-f3e5-43e5-8c03-ac8016a6a5ef Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../System.Diagnostics.Process/tests/ProcessHandlesTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index edb53a5e9f2b4f..570fbd3cbcf43d 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -595,8 +595,9 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess(bool inheritHand return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode; } - // Handle value was reused for a different file — not our handle, acceptable. - return RemoteExecutor.SuccessExitCode; + // Handle value was reused for a different file. + // This is only acceptable when the handle was not expected to be inherited. + return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode; }, rawHandle.ToString(), id, From 6250aee1de87894a86a14f2fc4a62997770b0bf0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:03:38 +0000 Subject: [PATCH 14/15] Simplify child process handle check per reviewer suggestion Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/2f389c85-bdc5-4872-a751-b96dccc802dc Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/ProcessHandlesTests.cs | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index 570fbd3cbcf43d..101e3ca4fa88f4 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -571,33 +571,21 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess(bool inheritHand nint rawHandle = nint.Parse(handleStr); using SafeFileHandle handle = new SafeFileHandle(rawHandle, ownsHandle: false); - if (handle.IsInvalid) - { - // Handle is invalid in the child: only acceptable when not inherited. - return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode; - } + // Check whether it points to our file (the Operating System could reuse same value for a different file). + string? childId = null; - // If the handle appears valid, check whether it points to our file. - // (the Operating System could reuse same value for a different file) try { - string childId = GetSafeFileHandleId(handle); - - if (childId == fileId) - { - // Handle points to our file — correct only when inherited. - return shouldBeInherited ? RemoteExecutor.SuccessExitCode : RemoteExecutor.SuccessExitCode - 1; - } + childId = GetSafeFileHandleId(handle); } - catch + catch (Exception) when (!shouldBeInherited) { // Handle is not a valid file handle in this process. - return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode; + return RemoteExecutor.SuccessExitCode; } - // Handle value was reused for a different file. - // This is only acceptable when the handle was not expected to be inherited. - return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode; + Assert.Equal(shouldBeInherited, childId == fileId); + return RemoteExecutor.SuccessExitCode; }, rawHandle.ToString(), id, From 9cbade4ff6536f85465c1365520ef51dd053202b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:30:12 +0000 Subject: [PATCH 15/15] Apply code review feedback: add explanatory comment and narrow catch to Win32Exception Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8678cac7-b1b2-42ee-ae2b-dde2db2fa421 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/ProcessHandlesTests.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index 101e3ca4fa88f4..be8b0268a6b1bf 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -557,6 +557,11 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess(bool inheritHand Assert.Equal(FileHandleType.RegularFile, fileHandle.Type); nint rawHandle = fileHandle.DangerousGetHandle(); + // When we start a process and limit handle inheritance, it's hard to reliably check that + // a given file handle/descriptor was not inherited, because the OS can simply re-use the + // exact same number for a different pipe/file/device. The idea here is to get something + // that can help us identify the given file handle and just compare the ID that the parent + // and child processes have obtained. string id = GetSafeFileHandleId(fileHandle); RemoteInvokeOptions options = new() { CheckExitCode = true }; @@ -578,7 +583,7 @@ public void NonInheritedFileHandle_IsNotAvailableInChildProcess(bool inheritHand { childId = GetSafeFileHandleId(handle); } - catch (Exception) when (!shouldBeInherited) + catch (Win32Exception) when (!shouldBeInherited) { // Handle is not a valid file handle in this process. return RemoteExecutor.SuccessExitCode;