Skip to content

Add NonInheritedFileHandle_IsNotAvailableInChildProcess test to ProcessHandlesTests#126629

Merged
adamsitnik merged 15 commits intomainfrom
copilot/extend-safefilehandle-name
Apr 10, 2026
Merged

Add NonInheritedFileHandle_IsNotAvailableInChildProcess test to ProcessHandlesTests#126629
adamsitnik merged 15 commits intomainfrom
copilot/extend-safefilehandle-name

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 8, 2026

Adds a test to ProcessHandlesTests that verifies file handle inheritance behavior when spawning child processes with InheritedHandles. All other changes originally in this PR (native PAL exports, SafeFileHandle path resolution, GetFileType tests) have been reverted following reviewer feedback that OS-level path resolution returns device paths rather than the user-friendly paths C# developers expect.

Changes

Tests (System.Diagnostics.Process)

  • ProcessHandlesTests.cs: Added NonInheritedFileHandle_IsNotAvailableInChildProcess as a [ConditionalTheory] with two scenarios (InlineData(false) and InlineData(true)):
    • false: InheritedHandles = [] — verifies the handle is not accessible in the child process
    • true: InheritedHandles = [fileHandle] — verifies the handle is accessible and points to the same file in the child process
    • File is opened with FileShare.None so independent opens that honor .NET/OS sharing semantics are prevented
    • Platform-specific identity is obtained via GetSafeFileHandleId (a regular private static method defined per-platform); identity comparison uses Assert.Equal(shouldBeInherited, childId == fileId)
    • catch (Win32Exception) when (!shouldBeInherited) is used to handle the case where the handle is not valid in the child process — Win32Exception is the only exception GetSafeFileHandleId throws, and it only propagates (failing the test) when the handle was expected to be inherited
    • An explanatory code comment is included near the GetSafeFileHandleId call noting that limiting handle inheritance makes it hard to reliably check whether a handle was not inherited, because the OS can reuse the same handle number for a different pipe/file/device — hence the identity comparison approach
  • ProcessHandlesTests.Unix.cs (new): Implements GetSafeFileHandleId as private static string using Interop.Sys.FStat — returns "{status.Dev}:{status.Ino}" (device + inode, formatted with invariant culture) as the file identity for robustness across different devices/mounts; throws Win32Exception if the syscall fails
  • ProcessHandlesTests.Windows.cs: Extended with GetSafeFileHandleId as private static unsafe string using Interop.Kernel32.GetFinalPathNameByHandle with a buffer of 32,767 characters — returns the normalized file path as the file identity; throws Win32Exception on failure
  • System.Diagnostics.Process.Tests.csproj: Added ProcessHandlesTests.Unix.cs to the Unix item group, Interop.Stat.cs for Unix FStat support, and Interop.GetFinalPathNameByHandle.cs for Windows support

Copilot AI review requested due to automatic review settings April 8, 2026 06:08
Copilot AI linked an issue Apr 8, 2026 that may be closed by this pull request
Copilot AI review requested due to automatic review settings April 8, 2026 06:08
- Add SystemNative_GetFilePathFromHandle to pal_io.h and pal_io.c:
  - Linux: readlink /proc/self/fd/<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>
Copilot AI requested review from Copilot and removed request for Copilot April 8, 2026 06:50
Copilot AI changed the title [WIP] Fix FileStream.Name for instances created out of handle Implement SafeFileHandle.Name to resolve file paths from OS handles Apr 8, 2026
Copilot AI requested a review from adamsitnik April 8, 2026 07:04
Copy link
Copy Markdown
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

@copilot address my feedback

…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>
Copilot AI requested review from Copilot and removed request for Copilot April 8, 2026 07:44
Copilot AI requested review from Copilot and removed request for Copilot April 8, 2026 07:47
Copilot AI changed the title Implement SafeFileHandle.Name to resolve file paths from OS handles Make FileStream.Name work for FileStream instances created out of handle Apr 8, 2026
Copilot AI requested a review from adamsitnik April 8, 2026 07:48
Copy link
Copy Markdown
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

@copilot address my 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>
Copilot AI requested review from Copilot and removed request for Copilot April 8, 2026 08:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

Note

This review was generated by GitHub Copilot.

🤖 Copilot Code Review

Holistic Assessment

Motivation: The PR adds a targeted regression test for InheritedHandles / ProcessStartInfo file-handle inheritance behavior. The test exercises both the "not inherited" and "inherited" scenarios, and uses platform-appropriate identity comparisons (inode+dev on Unix, normalized path on Windows). The motivation is clear and the scope is appropriately small.

Approach: Using GetSafeFileHandleId (platform-split) rather than path comparison is the right call given the OS-level path normalization issues that were discovered. The logic in the child process correctly handles three cases: handle is the invalid sentinel, handle resolves to the same file, handle resolves to a different file.

Summary: ⚠️ Needs Human Review. The implementation is generally sound but has one logical gap in the inherited-handle path that could let a test pass incorrectly, plus a minor test-name mismatch. Other potential concerns (buffer size, file deletion order) raised by sub-agent reviewers were investigated and found to be safe.


Detailed Findings

⚠️ Logical gap: inherited handle with mismatched identity is silently accepted

In the child lambda, the "Handle value was reused for a different file" branch returns SuccessExitCode unconditionally, regardless of shouldBeInherited:

// Handle value was reused for a different file — not our handle, acceptable.
return RemoteExecutor.SuccessExitCode;

When shouldBeInherited = true, an explicitly inherited handle should always resolve to the same file. If childId != fileId in this case it means the inheritance silently failed — which the test should report as a failure. Consider:

// Handle value was reused for a different file — only acceptable when not inherited.
return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode;

In practice this race cannot occur (an explicitly inherited Windows HANDLE or Unix fd always preserves its identity), but the test would be more robust and self-documenting with an explicit failure.

(Flagged independently by both Gemini and GPT sub-reviews.)

💡 Test name only reflects the negative case

NonInheritedFileHandle_IsNotAvailableInChildProcess describes only the inheritHandle=false scenario. The method also tests the positive case (inheritHandle=true). A more neutral name like FileHandleInheritance_AvailabilityMatchesInheritedHandlesList or simply InheritedHandles_FileHandleAccessibility would better represent both theory cases tested.

💡 Bare catch swallows all exception types

catch
{
    // Handle is not a valid file handle in this process.
    return shouldBeInherited ? ...;
}

This catches OutOfMemoryException, ThreadAbortException, and other unexpected exceptions. For test code this is acceptable, but a narrower catch (Exception) with a comment explaining why broad catching is intentional here would make the intent clearer.


Verified as Safe (no action needed)

✅ Windows GetFinalPathNameByHandle buffer size is correct

Both sub-agent reviewers flagged const int MaxPath = 32_767 as potentially one char short. Investigated and confirmed safe: cchFilePath = 32767 tells the API the buffer holds 32767 chars (including room for the null terminator), so paths up to 32,766 chars can be returned. The Windows extended-path hard limit (UNICODE_STRING.MaximumLength / sizeof(WCHAR) – 1) means path content cannot exceed 32,766 chars in practice. No resize needed.

✅ File deletion ordering in finally

remoteHandle (declared last) disposes before fileHandle (declared first) per C# using-declaration scoping. remoteHandle.Dispose() waits for the child to exit, releasing any inherited handle. fileHandle.Dispose() then closes the parent's handle. File.Delete in finally therefore runs with no open handles on any platform.

✅ Unix file identity is robust

FormattableString.Invariant($"{status.Dev}:{status.Ino}") correctly includes the device number for cross-mount robustness and uses invariant culture for cross-process stability.

✅ FileShare.None comment is accurate

The comment correctly avoids overstating the guarantee ("independent opens that honor .NET/OS sharing semantics are prevented") rather than claiming exclusive access is enforced at the kernel level.

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

🤖 Copilot Code Review

Note

This review was generated by AI (Copilot). Multi-model review: Claude Sonnet 4.5, Gemini 2.5 Pro, and GPT-4.1.

Holistic Assessment

Motivation: The PR adds a focused test that verifies file handle inheritance semantics when spawning child processes via InheritedHandles. This fills a real gap in the existing ProcessHandlesTests coverage.

Approach: Using RemoteExecutor to launch a child process and checking file identity via inode+dev on Unix and GetFinalPathNameByHandle on Windows is reasonable and platform-appropriate. No new production code is changed.

Summary: ⚠️ Needs Human Review. The code is largely correct and well-structured. There is one ⚠️ warning about the broad catch block that could silently mask unexpected failures in the non-inherited scenario. No blocking ❌ errors were found.


Detailed Findings

⚠️ Overly broad catch block — ProcessHandlesTests.cs line 592

The catch block catches all exceptions (including NullReferenceException, OutOfMemoryException, etc.), not just the expected Win32Exception thrown by GetSafeFileHandleId when the FD/handle is invalid:

catch
{
    // Handle is not a valid file handle in this process.
    return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode;
}

Risk: When inheritHandle=false, an unexpected exception in GetSafeFileHandleId (e.g., a future bug introducing a NullReferenceException) returns SuccessExitCode, masking the failure. When inheritHandle=true, unexpected exceptions would correctly return SuccessExitCode - 1 (failure), so that direction is safe.

Consider narrowing to catch (Win32Exception):

catch (Win32Exception)
{
    // Handle is not a valid file handle in this process.
    return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode;
}

Both GetSafeFileHandleId implementations throw only Win32Exception on failure, so this narrower catch covers all expected cases while letting unexpected failures propagate and fail the test clearly.


✅ Test structure — handles edge cases correctly

The test correctly handles the OS handle-value-reuse scenario (a different file coincidentally gets the same FD/handle value in the child). The three-way result logic (invalid handle → no-op, same id → pass/fail based on shouldBeInherited, different id → treat as unrelated file) is sound.


✅ Platform-specific identity — appropriate choices

  • Unix: Dev:Ino (with FormattableString.Invariant) correctly identifies files across mounts and is stable across processes. Including Dev alongside Ino avoids cross-device collisions.
  • Windows: GetFinalPathNameByHandle with FILE_NAME_NORMALIZED gives a stable canonical path. A 32,767-character buffer covers all practical Windows path lengths.

✅ No new public API surface, no production code changed

All changes are scoped to test files. No API approval verification needed.


💡 Minor — FormattableString.Invariant is technically unnecessary for long values

long.ToString() is already culture-invariant in .NET, so FormattableString.Invariant($"{status.Dev}:{status.Ino}") is defensive but not strictly required. This is a minor nit; keeping it as is is fine as a defensive habit.

…cess 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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

Note

This review was generated by GitHub Copilot (multi-model: Claude Opus 4.6, Gemini 2.5 Pro, GPT-5.4).

🤖 Copilot Code Review

Holistic Assessment

Motivation: The PR adds a targeted, focused test for ProcessStartInfo.InheritedHandles file-handle inheritance behavior. Both the positive (inherited) and negative (not inherited) cases are exercised — this is a valuable regression guard for a subtle OS-level feature.

Approach: Using platform-specific GetSafeFileHandleId helpers (inode+dev on Unix, normalized path on Windows) rather than comparing file names is the right call given the OS-level path normalization issues that motivated the design change. The RemoteExecutor pattern is correct for this cross-process test.

Summary: ⚠️ Needs Human Review. One genuine concern — the bare catch in the child process lambda (flagged independently by all three reviewer models) — and a minor test-name cosmetic issue. The core logic, platform wiring, and handle lifecycle are all correct.


Detailed Findings

⚠️ Exception Handling — Bare catch in child lambda may mask bugs in new helpers

File: ProcessHandlesTests.cs:582-596

try
{
    string childId = GetSafeFileHandleId(handle);
    ...
}
catch   // ← catches everything, including bugs in the new helper
{
    // Handle is not a valid file handle in this process.
    return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode;
}

The only expected exception from GetSafeFileHandleId on an invalid handle is Win32Exception (both Unix FStat returning -1 and Windows GetFinalPathNameByHandle returning 0 explicitly throw it). A bare catch swallows any other exception — including bugs in the new interop helpers — and maps them to a test pass when inheritHandle == false, hiding real failures. Should be catch (Win32Exception).

Flagged by all three reviewer models.

✅ Handle lifecycle — ownsHandle: false is correct

The child wraps the raw handle value with ownsHandle: false for inspection only. Since the parent process owns the handle, the child must not close it. The using with ownsHandle: false correctly disposes the wrapper without closing the underlying handle.

✅ Test logic — all three exit-code paths are now correct

  • handle.IsInvalid → fails when shouldBeInherited, passes otherwise ✓
  • childId == fileId → passes when shouldBeInherited, fails otherwise ✓
  • childId != fileId (after latest fix) → fails when shouldBeInherited, passes otherwise ✓
  • catch (invalid file handle) → fails when shouldBeInherited, passes otherwise ✓

✅ Platform wiring — correct compilation scope

ProcessHandlesTests.Unix.cs is included for unix|browser|android|maccatalyst (matching where Interop.Stat.cs is compiled). The class-level [SkipOnPlatform(TestPlatforms.Android | ...MacCatalyst)] and [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] ensure the test doesn't run on unsupported platforms.

💡 Test name — slightly misleading for the inheritHandle=true case

NonInheritedFileHandle_IsNotAvailableInChildProcess describes only the false scenario. A name like FileHandle_ChildProcessInheritanceRespected would cover both cases. Minor — not blocking.

Copilot AI requested review from adamsitnik and Copilot and removed request for Copilot April 9, 2026 14:03
@adamsitnik adamsitnik removed the NO-REVIEW Experimental/testing PR, do NOT review it label Apr 9, 2026
Copy link
Copy Markdown
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

LGTM

@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

…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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 10, 2026

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

Note

This review was generated by GitHub Copilot (multi-model: Claude Opus 4.6, Gemini 2.5 Pro, GPT-5.4).

🤖 Copilot Code Review

Holistic Assessment

Motivation: The PR adds a well-motivated regression test for ProcessStartInfo.InheritedHandles file-handle inheritance. It correctly identifies the core challenge: handle numbers can be reused by the OS, so numeric comparison alone is insufficient — file identity (via dev:ino on Unix, final path on Windows) is the right approach.

Approach: The [ConditionalTheory] with both false and true cases, the per-platform GetSafeFileHandleId helpers, and the catch (Win32Exception) when (!shouldBeInherited) filter are all correct and well-designed. The FileShare.None exclusive open prevents ID collision with an independently-opened copy of the same file.

Summary: ✅ LGTM. No blocking correctness issues found across all three models. The findings below are all suggestions/minor notes.


Detailed Findings

💡 Suggestion — Test method name partially describes the scenario (flagged by 2 models)

File: ProcessHandlesTests.cs:547

NonInheritedFileHandle_IsNotAvailableInChildProcess only describes the inheritHandle=false case. The test now also validates that an explicitly-inherited handle is available in the child. A more neutral name (e.g., FileHandle_IsAvailableInChildProcess_OnlyWhenExplicitlyInherited) would reflect both scenarios — but the current name is not wrong, just incomplete. Reviewer discretion.

💡 Suggestion — Windows buffer overflow not fully defended (flagged by 2 models)

File: ProcessHandlesTests.Windows.cs:190-196

GetFinalPathNameByHandle returns result >= MaxPath if the buffer was too small. The current code doesn't guard that case: new string(buffer, 0, (int)result) would throw ArgumentOutOfRangeException rather than a clear error. A defensive guard costs nothing:

if (result == 0 || result >= MaxPath)
{
    throw new Win32Exception();
}

For test code this is extremely low priority (no real path approaches 32,767 chars), but it's a belt-and-suspenders improvement.

✅ Verified — [SkipOnPlatform] removal is correct

The original had [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, ...)] which was redundant with [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]. The new code correctly relies solely on ConditionalTheoryRemoteExecutor.IsSupported returns false on Browser/WASI. Removal is intentional and correct.

✅ Verified — Exception handling logic is correct

catch (Win32Exception) when (!shouldBeInherited) correctly handles the three cases:

  1. shouldBeInherited=false, handle invalid → exception caught, returns success ✅
  2. shouldBeInherited=false, handle reused for different resource → childId != fileId, Assert.Equal(false, false) passes ✅
  3. shouldBeInherited=true, handle valid and same file → childId == fileId, Assert.Equal(true, true) passes ✅
  4. shouldBeInherited=true, exception thrown → propagates, test fails ✅

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 10, 2026

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

Note

This review was generated by GitHub Copilot (multi-model: Gemini 2.5 Pro, GPT-5.4).

🤖 Copilot Code Review

Holistic Assessment

Motivation: The PR adds a focused, well-motivated test for ProcessStartInfo.InheritedHandles file-handle inheritance. The test correctly identifies the core challenge: raw handle numbers can be reused by the OS, so numeric comparison alone is insufficient—file identity (via dev:ino on Unix, normalized path on Windows) is the right approach.

Approach: The [ConditionalTheory] with both false and true cases, per-platform GetSafeFileHandleId helpers, catch (Win32Exception) when (!shouldBeInherited) filter, and FileShare.None exclusive open are all correct and well-designed. The RemoteExecutor pattern is the standard isolation mechanism for cross-process tests.

Summary: ✅ LGTM. No blocking correctness issues found across either model. The single finding below is a minor observation, not a blocker.


Detailed Findings

✅ Test Logic — Correct handle identity comparison

Assert.Equal(shouldBeInherited, childId == fileId) covers all cases correctly:

  • shouldBeInherited=true: child resolves to same file → childId == fileId → assertion passes ✓
  • shouldBeInherited=false, handle invalid → Win32Exception caught → early return success ✓
  • shouldBeInherited=false, handle reused for same file → would fail the assertion, but FileShare.None prevents this from happening ✓

✅ Error Handling — Exception narrowing is correct

catch (Win32Exception) when (!shouldBeInherited) is the only exception GetSafeFileHandleId throws on both platforms (both FStat failure on Unix and GetFinalPathNameByHandle failure on Windows). Non-Win32Exception errors propagate unconditionally, preventing them from masking bugs.

💡 Suggestion — Windows buffer size (advisory, test code)

GetFinalPathNameByHandle returns result >= MaxPath if the buffer was too small. The current code would pass an oversized result to new string(buffer, 0, (int)result), which would throw ArgumentOutOfRangeException. In practice this can't happen since MaxPath = 32_767 is the Windows maximum path length, so this is safe as-is for test code. No action needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add test that ensures handles are not inherited

5 participants