IPC: handle disconnects and protocol corruption gracefully - #8602

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience
May 27, 2026
Merged

IPC: handle disconnects and protocol corruption gracefully#8602
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Hardens the Microsoft.Testing.Platform IPC layer (named-pipe transport between the test host and its in-process clients) so that a peer disconnect or a corrupt/short header byte no longer takes down the host or client process.

Audit items addressed

Part of the P0+P1 exception-handling audit (items #4#10, IPC).

Changes

NamedPipeServer.cs

  • Replaced ApplicationStateGuard.Unreachable() throws in the header read path with graceful disconnect handling (tolerate short reads, treat mid-header EOF as a normal disconnect).
  • Added bounds check currentMessageSize <= 0 → log warning and return cleanly instead of crashing on a corrupt header.
  • Wrapped server-side WriteAsync/FlushAsync/WaitForPipeDrain in try/catch (IOException/ObjectDisposedException) → set clientDisconnected and exit the loop after resetting buffers, rather than tearing down the host.

NamedPipeClient.cs

  • Symmetric write-side hardening: catches the same IOException/ObjectDisposedException and routes through the existing _environment.Exit(GenericFailure) path used by the read-EOF handler.
  • Short-read tolerance on the response header; bounds check currentMessageSize <= 0 → exit on corruption.
  • Wrapped response Deserialize in try/catch (excluding OperationCanceledException) so protocol corruption exits cleanly instead of bubbling an undecorated deserialization exception.

Test

  • New regression test NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost sends a zero-byte size header via a raw NamedPipeClientStream and asserts the server stays alive instead of throwing ApplicationStateGuard.Unreachable.
  • All existing Microsoft.Testing.Platform.UnitTests IPC tests still pass on net9.0.

Notes

  • No public API changes.
  • Behavior matches the existing GenericFailure exit code used for the read-EOF path on the client side.
  • WaitConnectionAsync FailFast is intentionally preserved; only loop-body IO faults get graceful handling.

Replace ApplicationStateGuard.Unreachable() throws in IPC header/payload reads with graceful disconnect handling. Tolerate short reads, treat mid-header EOF as graceful disconnect, validate currentMessageSize > 0, and catch IOException/ObjectDisposedException during write/flush/drain so a peer disconnect cannot crash the host or client process.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 13:51

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the Microsoft.Testing.Platform named-pipe IPC transport so disconnects and certain protocol-corruption scenarios don’t crash the host/client process (replacing prior “unreachable” paths with graceful exits).

Changes:

  • Server: tolerate short/EOF header reads, reject non-positive message sizes, and handle write-side disconnects without FailFast.
  • Client: add write-side disconnect handling, tolerate short response headers, and treat response deserialization failures as a generic IPC failure exit.
  • Tests: add a regression test to ensure an invalid (0) message-size header doesn’t crash the host.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.csAdds regression coverage for invalid message-size header handling on the server.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.csMakes the server loop tolerant to mid-header EOF/short reads and write-side disconnects; logs and exits cleanly on certain corrupt headers.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.csAdds write-side disconnect handling, short-read header handling, and exits cleanly on corruption/deserialization errors.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 4

Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.cs Outdated
Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs Outdated
Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Test Coverage Gaps

The PR introduces 8 new error paths but only tests 1 (server-side zero message size). Missing test scenarios:

Client-side (4 untested paths):

  1. Write-side IOException: Server closes pipe during WriteAsync → verify _environment.Exit(GenericFailure) called
  2. Mid-header disconnect: Server sends 2 bytes then closes → verify _environment.Exit and IOException thrown
  3. Negative message size: Server sends -1 header → verify _environment.Exit and InvalidOperationException thrown
  4. Deserialization failure: Server sends corrupted payload → verify _environment.Exit called

Server-side (3 untested paths):

  1. Mid-header disconnect: Client sends 2 bytes then closes → verify graceful loop exit
  2. Negative message size: Client sends -1 header → verify warning logged and loop exits (existing test only covers zero, not negative)
  3. Byte-count overflow: Client declares N bytes but writes >N bytes → verify warning logged and loop exits
  4. Write-side IOException: Client disconnects during server reply → verify graceful loop exit

All 8 scenarios are concrete failing interleavings that the new code explicitly handles. Recommend adding these tests to prevent regressions.

Generated by Expert Code Review (on open) for issue #8602 · ● 4M ·

// If currentRequestSize is 0, we need to read the message size
if (currentMessageSize == 0)
{
// We need at least sizeof(int) bytes to parse the message-size header. A pipe read can

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is adding a lot of additional code. Is there a concrete bug that makes it worth adding this?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(Reading this as a question rather than a request to revert — happy to do either.)

No specific user-visible repro for the write-side hardening, but it's there to keep the client symmetric with the existing read-EOF path right below it: today, if the server disconnects mid-write the client surfaces a raw IOException/ObjectDisposedException to whoever called RequestReplyAsync (e.g. the test host orchestration code) instead of the deterministic _environment.Exit(GenericFailure) we already use for read-EOF. Two cases this matters in practice:

  1. dotnet test orchestration tears down the test host process mid-RPC — caller gets a raw IO exception instead of the documented exit code.
  2. The MTP host crashes/FailFasts on its end while the client is flushing — same thing.

The concrete server-side bug fixed in this PR (the one with the regression test) is the ApplicationStateGuard.Unreachable() -> FailFast crash on a corrupt/short header byte. The client write-side changes are essentially: 3 lines of catch (Exception ex) when (ex is IOException or ObjectDisposedException) doing what the read-EOF block 30 lines above already does.

If you'd rather I scope this PR down to just the server-side crash + the matching read-side tightening and pull the write-side symmetry into a separate PR (or just drop it), happy to do that — let me know.

- Tighten message-size validation on both server and client: payload must
contain at least a 4-byte serializer id, so reject sizes < sizeof(int)
(not just <= 0) as protocol corruption.
- Client: add the symmetric `missingBytesToReadOfWholeMessage < 0` guard
the server already has, so an over-long body exits cleanly instead of
hanging.
- Server: drop the useless `missingBytesToReadOfCurrentChunk = currentReadBytes;`
reassignment inside the header accumulation loop (recomputed at line 182).
- Test: use `TaskCompletionSource` for the callback signal and assert
after the server has been disposed, so the loop task has definitely
completed before we check that the callback did not run.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build failed due to a code style violation: a using statement can be simplified to a using declaration.

Root cause: IDE0063 style rule violation

The newly added test method NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost uses a traditional using statement with braces on line 261:

using(varraw=newSystem.IO.Pipes.NamedPipeClientStream(...)){// ...}

The project enforces [IDE0063]((learn.microsoft.com/redacted) as an error, which requires the simpler using declaration pattern:

usingvarraw=newSystem.IO.Pipes.NamedPipeClientStream(...);// ...

This single style violation is reported for each target framework the project builds (net8.0 and net9.0), resulting in multiple error instances from the same root cause.

Affected files / errors

Proposed fix

Convert the traditional using statement to a using declaration and adjust the code block accordingly:

 Task waitConnection = server.WaitConnectionAsync(_testContext.CancellationToken);
- using (var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous))- {- await raw.ConnectAsync(_testContext.CancellationToken);- await waitConnection;+ using var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous);+ await raw.ConnectAsync(_testContext.CancellationToken);+ await waitConnection;- // Write a zero-length message size header (invalid per protocol).- byte[] invalidHeader = BitConverter.GetBytes(0);- await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);- await raw.FlushAsync(_testContext.CancellationToken);- }+ // Write a zero-length message size header (invalid per protocol).+ byte[] invalidHeader = BitConverter.GetBytes(0);+ await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);+ await raw.FlushAsync(_testContext.CancellationToken);

Build overview

Configuration: Debug
Exit code: 1 (failure)
Error count: 2 (4 instances across target frameworks)
Warning count: 0

Failed project:

  • Microsoft.Testing.Platform.UnitTests.csproj (net8.0, net9.0)
All MSBuild errors (2)
CodeProjectFile:LineMessage
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net8.0)
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net9.0)

🤖 Generated by the Build Failure Analysis workflow · commit 60797b2

Generated by Build Failure Analysis for issue #8602 · ● 2.2M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8602 · ● 2.2M

Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
CopilotAI review requested due to automatic review settings May 27, 2026 09:00

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new

Per review feedback, the carefully-analyzed invariants in the IPC read
loop (Unreachable() guards on short header reads and overshoot of the
declared message size) should not be weakened into 'graceful' handlers
without evidence the conditions can occur. Replacing them with
LogWarning is also wrong: those are internal-bug states, not
user-actionable warnings.
Reverted from the original PR:
- NamedPipeServer: restore 'currentReadBytes < sizeof(int)' Unreachable
- NamedPipeServer: restore 'missingBytesToReadOfWholeMessage < 0' Unreachable
- NamedPipeServer: drop the added 'currentMessageSize < sizeof(int)' warning
- NamedPipeServer: restore the existing client-disconnect LogDebugAsync and
the Dispose/DisposeAsync LogError diagnostics
- NamedPipeClient: drop the symmetric (untested) header short-read loop,
the 'currentMessageSize < sizeof(int)' check, the
'missingBytesToReadOfWholeMessage < 0' check, and the try/catch around
Deserialize
- IPCTests: drop the NamedPipeServer_InvalidMessageSizeHeader regression
test (no longer applicable now that the invariant throw is restored)
Kept (legitimate runtime conditions caused by a real peer disconnect):
- NamedPipeServer: try/catch on WriteAsync/FlushAsync/WaitForPipeDrain
for IOException/ObjectDisposedException -> exit the loop cleanly
- NamedPipeClient: symmetric try/catch on the write side ->
_environment.Exit(GenericFailure)
Also strengthen .github/agents/expert-reviewer.agent.md to flag this
pattern in future reviews (new rules under Algorithmic Correctness and
Defensive Coding at Boundaries: don't weaken ApplicationStateGuard
.Unreachable without evidence; don't log internal-bug states as warnings;
defensive coding belongs at trust boundaries, not on internal invariants
between trusted in-process components).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Thanks for the careful review — you're right on both points and I just pushed a commit that addresses them.

What I changed back (your feedback)

  • NamedPipeServer.cs: restored throw ApplicationStateGuard.Unreachable() for both currentReadBytes < sizeof(int) and missingBytesToReadOfWholeMessage < 0. Also dropped the added currentMessageSize < sizeof(int)LogWarning path. You're right that LogWarning is the wrong severity for an unexpected-state condition the user can't act on, and replacing a documented invariant with handling logic without evidence the condition is reachable is regressing the design that was carefully worked out with Marco.
  • NamedPipeClient.cs: same reasoning — dropped the symmetric short-read loop, the size bounds check, the overshoot check, and the try/catch around Deserialize that I had mirrored. None of them were driven by evidence and they were adding untested error paths.
  • Dropped the NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost regression test (it was only validating the behavior we just reverted).

What I kept

Only the parts where the exceptions are caused by an externally observable condition (the peer process crashing/exiting mid-write), not by an internal invariant:

  • NamedPipeServer.cs: try/catch (IOException or ObjectDisposedException) around the reply WriteAsync/FlushAsync/WaitForPipeDrain so a client that disconnects between request and reply doesn't FailFast the host.
  • NamedPipeClient.cs: symmetric try/catch on the request write side that routes through the same _environment.Exit(GenericFailure) path the existing read-EOF handler uses.

These match the existing read-EOF handling (currentReadBytes == 0 -> graceful return / Exit(GenericFailure)) and don't touch any invariant.

Expert reviewer

I also updated .github/agents/expert-reviewer.agent.md so the same pattern gets flagged in future reviews — added rules to Algorithmic Correctness and Defensive Coding at Boundaries:

  • Don't weaken ApplicationStateGuard.Unreachable() / Debug.Assert / invariant throws into ""graceful"" handling without a concrete repro.
  • An internal-bug condition is LogError + abort, never LogWarning (which is user-actionable severity).
  • Defensive coding belongs at trust boundaries, not on internal invariants between trusted in-process components.

All 965 Microsoft.Testing.Platform.UnitTests still pass on net9.0.

@Evangelink
Amaury Levé (Evangelink) merged commit ce14f31 into mainMay 27, 2026
24 of 26 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/ipc-disconnect-resilience branch May 27, 2026 11:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

IPC: handle disconnects and protocol corruption gracefully - #8602

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience
May 27, 2026
Merged

IPC: handle disconnects and protocol corruption gracefully#8602
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Hardens the Microsoft.Testing.Platform IPC layer (named-pipe transport between the test host and its in-process clients) so that a peer disconnect or a corrupt/short header byte no longer takes down the host or client process.

Audit items addressed

Part of the P0+P1 exception-handling audit (items #4#10, IPC).

Changes

NamedPipeServer.cs

  • Replaced ApplicationStateGuard.Unreachable() throws in the header read path with graceful disconnect handling (tolerate short reads, treat mid-header EOF as a normal disconnect).
  • Added bounds check currentMessageSize <= 0 → log warning and return cleanly instead of crashing on a corrupt header.
  • Wrapped server-side WriteAsync/FlushAsync/WaitForPipeDrain in try/catch (IOException/ObjectDisposedException) → set clientDisconnected and exit the loop after resetting buffers, rather than tearing down the host.

NamedPipeClient.cs

  • Symmetric write-side hardening: catches the same IOException/ObjectDisposedException and routes through the existing _environment.Exit(GenericFailure) path used by the read-EOF handler.
  • Short-read tolerance on the response header; bounds check currentMessageSize <= 0 → exit on corruption.
  • Wrapped response Deserialize in try/catch (excluding OperationCanceledException) so protocol corruption exits cleanly instead of bubbling an undecorated deserialization exception.

Test

  • New regression test NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost sends a zero-byte size header via a raw NamedPipeClientStream and asserts the server stays alive instead of throwing ApplicationStateGuard.Unreachable.
  • All existing Microsoft.Testing.Platform.UnitTests IPC tests still pass on net9.0.

Notes

  • No public API changes.
  • Behavior matches the existing GenericFailure exit code used for the read-EOF path on the client side.
  • WaitConnectionAsync FailFast is intentionally preserved; only loop-body IO faults get graceful handling.

Replace ApplicationStateGuard.Unreachable() throws in IPC header/payload reads with graceful disconnect handling. Tolerate short reads, treat mid-header EOF as graceful disconnect, validate currentMessageSize > 0, and catch IOException/ObjectDisposedException during write/flush/drain so a peer disconnect cannot crash the host or client process.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 13:51

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the Microsoft.Testing.Platform named-pipe IPC transport so disconnects and certain protocol-corruption scenarios don’t crash the host/client process (replacing prior “unreachable” paths with graceful exits).

Changes:

  • Server: tolerate short/EOF header reads, reject non-positive message sizes, and handle write-side disconnects without FailFast.
  • Client: add write-side disconnect handling, tolerate short response headers, and treat response deserialization failures as a generic IPC failure exit.
  • Tests: add a regression test to ensure an invalid (0) message-size header doesn’t crash the host.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.csAdds regression coverage for invalid message-size header handling on the server.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.csMakes the server loop tolerant to mid-header EOF/short reads and write-side disconnects; logs and exits cleanly on certain corrupt headers.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.csAdds write-side disconnect handling, short-read header handling, and exits cleanly on corruption/deserialization errors.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 4

Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.cs Outdated
Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs Outdated
Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Test Coverage Gaps

The PR introduces 8 new error paths but only tests 1 (server-side zero message size). Missing test scenarios:

Client-side (4 untested paths):

  1. Write-side IOException: Server closes pipe during WriteAsync → verify _environment.Exit(GenericFailure) called
  2. Mid-header disconnect: Server sends 2 bytes then closes → verify _environment.Exit and IOException thrown
  3. Negative message size: Server sends -1 header → verify _environment.Exit and InvalidOperationException thrown
  4. Deserialization failure: Server sends corrupted payload → verify _environment.Exit called

Server-side (3 untested paths):

  1. Mid-header disconnect: Client sends 2 bytes then closes → verify graceful loop exit
  2. Negative message size: Client sends -1 header → verify warning logged and loop exits (existing test only covers zero, not negative)
  3. Byte-count overflow: Client declares N bytes but writes >N bytes → verify warning logged and loop exits
  4. Write-side IOException: Client disconnects during server reply → verify graceful loop exit

All 8 scenarios are concrete failing interleavings that the new code explicitly handles. Recommend adding these tests to prevent regressions.

Generated by Expert Code Review (on open) for issue #8602 · ● 4M ·

// If currentRequestSize is 0, we need to read the message size
if (currentMessageSize == 0)
{
// We need at least sizeof(int) bytes to parse the message-size header. A pipe read can

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is adding a lot of additional code. Is there a concrete bug that makes it worth adding this?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(Reading this as a question rather than a request to revert — happy to do either.)

No specific user-visible repro for the write-side hardening, but it's there to keep the client symmetric with the existing read-EOF path right below it: today, if the server disconnects mid-write the client surfaces a raw IOException/ObjectDisposedException to whoever called RequestReplyAsync (e.g. the test host orchestration code) instead of the deterministic _environment.Exit(GenericFailure) we already use for read-EOF. Two cases this matters in practice:

  1. dotnet test orchestration tears down the test host process mid-RPC — caller gets a raw IO exception instead of the documented exit code.
  2. The MTP host crashes/FailFasts on its end while the client is flushing — same thing.

The concrete server-side bug fixed in this PR (the one with the regression test) is the ApplicationStateGuard.Unreachable() -> FailFast crash on a corrupt/short header byte. The client write-side changes are essentially: 3 lines of catch (Exception ex) when (ex is IOException or ObjectDisposedException) doing what the read-EOF block 30 lines above already does.

If you'd rather I scope this PR down to just the server-side crash + the matching read-side tightening and pull the write-side symmetry into a separate PR (or just drop it), happy to do that — let me know.

- Tighten message-size validation on both server and client: payload must
contain at least a 4-byte serializer id, so reject sizes < sizeof(int)
(not just <= 0) as protocol corruption.
- Client: add the symmetric `missingBytesToReadOfWholeMessage < 0` guard
the server already has, so an over-long body exits cleanly instead of
hanging.
- Server: drop the useless `missingBytesToReadOfCurrentChunk = currentReadBytes;`
reassignment inside the header accumulation loop (recomputed at line 182).
- Test: use `TaskCompletionSource` for the callback signal and assert
after the server has been disposed, so the loop task has definitely
completed before we check that the callback did not run.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build failed due to a code style violation: a using statement can be simplified to a using declaration.

Root cause: IDE0063 style rule violation

The newly added test method NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost uses a traditional using statement with braces on line 261:

using(varraw=newSystem.IO.Pipes.NamedPipeClientStream(...)){// ...}

The project enforces [IDE0063]((learn.microsoft.com/redacted) as an error, which requires the simpler using declaration pattern:

usingvarraw=newSystem.IO.Pipes.NamedPipeClientStream(...);// ...

This single style violation is reported for each target framework the project builds (net8.0 and net9.0), resulting in multiple error instances from the same root cause.

Affected files / errors

Proposed fix

Convert the traditional using statement to a using declaration and adjust the code block accordingly:

 Task waitConnection = server.WaitConnectionAsync(_testContext.CancellationToken);
- using (var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous))- {- await raw.ConnectAsync(_testContext.CancellationToken);- await waitConnection;+ using var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous);+ await raw.ConnectAsync(_testContext.CancellationToken);+ await waitConnection;- // Write a zero-length message size header (invalid per protocol).- byte[] invalidHeader = BitConverter.GetBytes(0);- await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);- await raw.FlushAsync(_testContext.CancellationToken);- }+ // Write a zero-length message size header (invalid per protocol).+ byte[] invalidHeader = BitConverter.GetBytes(0);+ await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);+ await raw.FlushAsync(_testContext.CancellationToken);

Build overview

Configuration: Debug
Exit code: 1 (failure)
Error count: 2 (4 instances across target frameworks)
Warning count: 0

Failed project:

  • Microsoft.Testing.Platform.UnitTests.csproj (net8.0, net9.0)
All MSBuild errors (2)
CodeProjectFile:LineMessage
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net8.0)
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net9.0)

🤖 Generated by the Build Failure Analysis workflow · commit 60797b2

Generated by Build Failure Analysis for issue #8602 · ● 2.2M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8602 · ● 2.2M

Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
CopilotAI review requested due to automatic review settings May 27, 2026 09:00

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new

Per review feedback, the carefully-analyzed invariants in the IPC read
loop (Unreachable() guards on short header reads and overshoot of the
declared message size) should not be weakened into 'graceful' handlers
without evidence the conditions can occur. Replacing them with
LogWarning is also wrong: those are internal-bug states, not
user-actionable warnings.
Reverted from the original PR:
- NamedPipeServer: restore 'currentReadBytes < sizeof(int)' Unreachable
- NamedPipeServer: restore 'missingBytesToReadOfWholeMessage < 0' Unreachable
- NamedPipeServer: drop the added 'currentMessageSize < sizeof(int)' warning
- NamedPipeServer: restore the existing client-disconnect LogDebugAsync and
the Dispose/DisposeAsync LogError diagnostics
- NamedPipeClient: drop the symmetric (untested) header short-read loop,
the 'currentMessageSize < sizeof(int)' check, the
'missingBytesToReadOfWholeMessage < 0' check, and the try/catch around
Deserialize
- IPCTests: drop the NamedPipeServer_InvalidMessageSizeHeader regression
test (no longer applicable now that the invariant throw is restored)
Kept (legitimate runtime conditions caused by a real peer disconnect):
- NamedPipeServer: try/catch on WriteAsync/FlushAsync/WaitForPipeDrain
for IOException/ObjectDisposedException -> exit the loop cleanly
- NamedPipeClient: symmetric try/catch on the write side ->
_environment.Exit(GenericFailure)
Also strengthen .github/agents/expert-reviewer.agent.md to flag this
pattern in future reviews (new rules under Algorithmic Correctness and
Defensive Coding at Boundaries: don't weaken ApplicationStateGuard
.Unreachable without evidence; don't log internal-bug states as warnings;
defensive coding belongs at trust boundaries, not on internal invariants
between trusted in-process components).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Thanks for the careful review — you're right on both points and I just pushed a commit that addresses them.

What I changed back (your feedback)

  • NamedPipeServer.cs: restored throw ApplicationStateGuard.Unreachable() for both currentReadBytes < sizeof(int) and missingBytesToReadOfWholeMessage < 0. Also dropped the added currentMessageSize < sizeof(int)LogWarning path. You're right that LogWarning is the wrong severity for an unexpected-state condition the user can't act on, and replacing a documented invariant with handling logic without evidence the condition is reachable is regressing the design that was carefully worked out with Marco.
  • NamedPipeClient.cs: same reasoning — dropped the symmetric short-read loop, the size bounds check, the overshoot check, and the try/catch around Deserialize that I had mirrored. None of them were driven by evidence and they were adding untested error paths.
  • Dropped the NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost regression test (it was only validating the behavior we just reverted).

What I kept

Only the parts where the exceptions are caused by an externally observable condition (the peer process crashing/exiting mid-write), not by an internal invariant:

  • NamedPipeServer.cs: try/catch (IOException or ObjectDisposedException) around the reply WriteAsync/FlushAsync/WaitForPipeDrain so a client that disconnects between request and reply doesn't FailFast the host.
  • NamedPipeClient.cs: symmetric try/catch on the request write side that routes through the same _environment.Exit(GenericFailure) path the existing read-EOF handler uses.

These match the existing read-EOF handling (currentReadBytes == 0 -> graceful return / Exit(GenericFailure)) and don't touch any invariant.

Expert reviewer

I also updated .github/agents/expert-reviewer.agent.md so the same pattern gets flagged in future reviews — added rules to Algorithmic Correctness and Defensive Coding at Boundaries:

  • Don't weaken ApplicationStateGuard.Unreachable() / Debug.Assert / invariant throws into ""graceful"" handling without a concrete repro.
  • An internal-bug condition is LogError + abort, never LogWarning (which is user-actionable severity).
  • Defensive coding belongs at trust boundaries, not on internal invariants between trusted in-process components.

All 965 Microsoft.Testing.Platform.UnitTests still pass on net9.0.

@Evangelink
Amaury Levé (Evangelink) merged commit ce14f31 into mainMay 27, 2026
24 of 26 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/ipc-disconnect-resilience branch May 27, 2026 11:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

IPC: handle disconnects and protocol corruption gracefully - #8602

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience
May 27, 2026
Merged

IPC: handle disconnects and protocol corruption gracefully#8602
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Hardens the Microsoft.Testing.Platform IPC layer (named-pipe transport between the test host and its in-process clients) so that a peer disconnect or a corrupt/short header byte no longer takes down the host or client process.

Audit items addressed

Part of the P0+P1 exception-handling audit (items #4#10, IPC).

Changes

NamedPipeServer.cs

  • Replaced ApplicationStateGuard.Unreachable() throws in the header read path with graceful disconnect handling (tolerate short reads, treat mid-header EOF as a normal disconnect).
  • Added bounds check currentMessageSize <= 0 → log warning and return cleanly instead of crashing on a corrupt header.
  • Wrapped server-side WriteAsync/FlushAsync/WaitForPipeDrain in try/catch (IOException/ObjectDisposedException) → set clientDisconnected and exit the loop after resetting buffers, rather than tearing down the host.

NamedPipeClient.cs

  • Symmetric write-side hardening: catches the same IOException/ObjectDisposedException and routes through the existing _environment.Exit(GenericFailure) path used by the read-EOF handler.
  • Short-read tolerance on the response header; bounds check currentMessageSize <= 0 → exit on corruption.
  • Wrapped response Deserialize in try/catch (excluding OperationCanceledException) so protocol corruption exits cleanly instead of bubbling an undecorated deserialization exception.

Test

  • New regression test NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost sends a zero-byte size header via a raw NamedPipeClientStream and asserts the server stays alive instead of throwing ApplicationStateGuard.Unreachable.
  • All existing Microsoft.Testing.Platform.UnitTests IPC tests still pass on net9.0.

Notes

  • No public API changes.
  • Behavior matches the existing GenericFailure exit code used for the read-EOF path on the client side.
  • WaitConnectionAsync FailFast is intentionally preserved; only loop-body IO faults get graceful handling.

Replace ApplicationStateGuard.Unreachable() throws in IPC header/payload reads with graceful disconnect handling. Tolerate short reads, treat mid-header EOF as graceful disconnect, validate currentMessageSize > 0, and catch IOException/ObjectDisposedException during write/flush/drain so a peer disconnect cannot crash the host or client process.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 13:51

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the Microsoft.Testing.Platform named-pipe IPC transport so disconnects and certain protocol-corruption scenarios don’t crash the host/client process (replacing prior “unreachable” paths with graceful exits).

Changes:

  • Server: tolerate short/EOF header reads, reject non-positive message sizes, and handle write-side disconnects without FailFast.
  • Client: add write-side disconnect handling, tolerate short response headers, and treat response deserialization failures as a generic IPC failure exit.
  • Tests: add a regression test to ensure an invalid (0) message-size header doesn’t crash the host.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.csAdds regression coverage for invalid message-size header handling on the server.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.csMakes the server loop tolerant to mid-header EOF/short reads and write-side disconnects; logs and exits cleanly on certain corrupt headers.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.csAdds write-side disconnect handling, short-read header handling, and exits cleanly on corruption/deserialization errors.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 4

Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.cs Outdated
Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs Outdated
Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Test Coverage Gaps

The PR introduces 8 new error paths but only tests 1 (server-side zero message size). Missing test scenarios:

Client-side (4 untested paths):

  1. Write-side IOException: Server closes pipe during WriteAsync → verify _environment.Exit(GenericFailure) called
  2. Mid-header disconnect: Server sends 2 bytes then closes → verify _environment.Exit and IOException thrown
  3. Negative message size: Server sends -1 header → verify _environment.Exit and InvalidOperationException thrown
  4. Deserialization failure: Server sends corrupted payload → verify _environment.Exit called

Server-side (3 untested paths):

  1. Mid-header disconnect: Client sends 2 bytes then closes → verify graceful loop exit
  2. Negative message size: Client sends -1 header → verify warning logged and loop exits (existing test only covers zero, not negative)
  3. Byte-count overflow: Client declares N bytes but writes >N bytes → verify warning logged and loop exits
  4. Write-side IOException: Client disconnects during server reply → verify graceful loop exit

All 8 scenarios are concrete failing interleavings that the new code explicitly handles. Recommend adding these tests to prevent regressions.

Generated by Expert Code Review (on open) for issue #8602 · ● 4M ·

// If currentRequestSize is 0, we need to read the message size
if (currentMessageSize == 0)
{
// We need at least sizeof(int) bytes to parse the message-size header. A pipe read can

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is adding a lot of additional code. Is there a concrete bug that makes it worth adding this?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(Reading this as a question rather than a request to revert — happy to do either.)

No specific user-visible repro for the write-side hardening, but it's there to keep the client symmetric with the existing read-EOF path right below it: today, if the server disconnects mid-write the client surfaces a raw IOException/ObjectDisposedException to whoever called RequestReplyAsync (e.g. the test host orchestration code) instead of the deterministic _environment.Exit(GenericFailure) we already use for read-EOF. Two cases this matters in practice:

  1. dotnet test orchestration tears down the test host process mid-RPC — caller gets a raw IO exception instead of the documented exit code.
  2. The MTP host crashes/FailFasts on its end while the client is flushing — same thing.

The concrete server-side bug fixed in this PR (the one with the regression test) is the ApplicationStateGuard.Unreachable() -> FailFast crash on a corrupt/short header byte. The client write-side changes are essentially: 3 lines of catch (Exception ex) when (ex is IOException or ObjectDisposedException) doing what the read-EOF block 30 lines above already does.

If you'd rather I scope this PR down to just the server-side crash + the matching read-side tightening and pull the write-side symmetry into a separate PR (or just drop it), happy to do that — let me know.

- Tighten message-size validation on both server and client: payload must
contain at least a 4-byte serializer id, so reject sizes < sizeof(int)
(not just <= 0) as protocol corruption.
- Client: add the symmetric `missingBytesToReadOfWholeMessage < 0` guard
the server already has, so an over-long body exits cleanly instead of
hanging.
- Server: drop the useless `missingBytesToReadOfCurrentChunk = currentReadBytes;`
reassignment inside the header accumulation loop (recomputed at line 182).
- Test: use `TaskCompletionSource` for the callback signal and assert
after the server has been disposed, so the loop task has definitely
completed before we check that the callback did not run.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build failed due to a code style violation: a using statement can be simplified to a using declaration.

Root cause: IDE0063 style rule violation

The newly added test method NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost uses a traditional using statement with braces on line 261:

using(varraw=newSystem.IO.Pipes.NamedPipeClientStream(...)){// ...}

The project enforces [IDE0063]((learn.microsoft.com/redacted) as an error, which requires the simpler using declaration pattern:

usingvarraw=newSystem.IO.Pipes.NamedPipeClientStream(...);// ...

This single style violation is reported for each target framework the project builds (net8.0 and net9.0), resulting in multiple error instances from the same root cause.

Affected files / errors

Proposed fix

Convert the traditional using statement to a using declaration and adjust the code block accordingly:

 Task waitConnection = server.WaitConnectionAsync(_testContext.CancellationToken);
- using (var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous))- {- await raw.ConnectAsync(_testContext.CancellationToken);- await waitConnection;+ using var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous);+ await raw.ConnectAsync(_testContext.CancellationToken);+ await waitConnection;- // Write a zero-length message size header (invalid per protocol).- byte[] invalidHeader = BitConverter.GetBytes(0);- await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);- await raw.FlushAsync(_testContext.CancellationToken);- }+ // Write a zero-length message size header (invalid per protocol).+ byte[] invalidHeader = BitConverter.GetBytes(0);+ await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);+ await raw.FlushAsync(_testContext.CancellationToken);

Build overview

Configuration: Debug
Exit code: 1 (failure)
Error count: 2 (4 instances across target frameworks)
Warning count: 0

Failed project:

  • Microsoft.Testing.Platform.UnitTests.csproj (net8.0, net9.0)
All MSBuild errors (2)
CodeProjectFile:LineMessage
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net8.0)
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net9.0)

🤖 Generated by the Build Failure Analysis workflow · commit 60797b2

Generated by Build Failure Analysis for issue #8602 · ● 2.2M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8602 · ● 2.2M

Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
CopilotAI review requested due to automatic review settings May 27, 2026 09:00

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new

Per review feedback, the carefully-analyzed invariants in the IPC read
loop (Unreachable() guards on short header reads and overshoot of the
declared message size) should not be weakened into 'graceful' handlers
without evidence the conditions can occur. Replacing them with
LogWarning is also wrong: those are internal-bug states, not
user-actionable warnings.
Reverted from the original PR:
- NamedPipeServer: restore 'currentReadBytes < sizeof(int)' Unreachable
- NamedPipeServer: restore 'missingBytesToReadOfWholeMessage < 0' Unreachable
- NamedPipeServer: drop the added 'currentMessageSize < sizeof(int)' warning
- NamedPipeServer: restore the existing client-disconnect LogDebugAsync and
the Dispose/DisposeAsync LogError diagnostics
- NamedPipeClient: drop the symmetric (untested) header short-read loop,
the 'currentMessageSize < sizeof(int)' check, the
'missingBytesToReadOfWholeMessage < 0' check, and the try/catch around
Deserialize
- IPCTests: drop the NamedPipeServer_InvalidMessageSizeHeader regression
test (no longer applicable now that the invariant throw is restored)
Kept (legitimate runtime conditions caused by a real peer disconnect):
- NamedPipeServer: try/catch on WriteAsync/FlushAsync/WaitForPipeDrain
for IOException/ObjectDisposedException -> exit the loop cleanly
- NamedPipeClient: symmetric try/catch on the write side ->
_environment.Exit(GenericFailure)
Also strengthen .github/agents/expert-reviewer.agent.md to flag this
pattern in future reviews (new rules under Algorithmic Correctness and
Defensive Coding at Boundaries: don't weaken ApplicationStateGuard
.Unreachable without evidence; don't log internal-bug states as warnings;
defensive coding belongs at trust boundaries, not on internal invariants
between trusted in-process components).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Thanks for the careful review — you're right on both points and I just pushed a commit that addresses them.

What I changed back (your feedback)

  • NamedPipeServer.cs: restored throw ApplicationStateGuard.Unreachable() for both currentReadBytes < sizeof(int) and missingBytesToReadOfWholeMessage < 0. Also dropped the added currentMessageSize < sizeof(int)LogWarning path. You're right that LogWarning is the wrong severity for an unexpected-state condition the user can't act on, and replacing a documented invariant with handling logic without evidence the condition is reachable is regressing the design that was carefully worked out with Marco.
  • NamedPipeClient.cs: same reasoning — dropped the symmetric short-read loop, the size bounds check, the overshoot check, and the try/catch around Deserialize that I had mirrored. None of them were driven by evidence and they were adding untested error paths.
  • Dropped the NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost regression test (it was only validating the behavior we just reverted).

What I kept

Only the parts where the exceptions are caused by an externally observable condition (the peer process crashing/exiting mid-write), not by an internal invariant:

  • NamedPipeServer.cs: try/catch (IOException or ObjectDisposedException) around the reply WriteAsync/FlushAsync/WaitForPipeDrain so a client that disconnects between request and reply doesn't FailFast the host.
  • NamedPipeClient.cs: symmetric try/catch on the request write side that routes through the same _environment.Exit(GenericFailure) path the existing read-EOF handler uses.

These match the existing read-EOF handling (currentReadBytes == 0 -> graceful return / Exit(GenericFailure)) and don't touch any invariant.

Expert reviewer

I also updated .github/agents/expert-reviewer.agent.md so the same pattern gets flagged in future reviews — added rules to Algorithmic Correctness and Defensive Coding at Boundaries:

  • Don't weaken ApplicationStateGuard.Unreachable() / Debug.Assert / invariant throws into ""graceful"" handling without a concrete repro.
  • An internal-bug condition is LogError + abort, never LogWarning (which is user-actionable severity).
  • Defensive coding belongs at trust boundaries, not on internal invariants between trusted in-process components.

All 965 Microsoft.Testing.Platform.UnitTests still pass on net9.0.

@Evangelink
Amaury Levé (Evangelink) merged commit ce14f31 into mainMay 27, 2026
24 of 26 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/ipc-disconnect-resilience branch May 27, 2026 11:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

IPC: handle disconnects and protocol corruption gracefully - #8602

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience
May 27, 2026
Merged

IPC: handle disconnects and protocol corruption gracefully#8602
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Hardens the Microsoft.Testing.Platform IPC layer (named-pipe transport between the test host and its in-process clients) so that a peer disconnect or a corrupt/short header byte no longer takes down the host or client process.

Audit items addressed

Part of the P0+P1 exception-handling audit (items #4#10, IPC).

Changes

NamedPipeServer.cs

  • Replaced ApplicationStateGuard.Unreachable() throws in the header read path with graceful disconnect handling (tolerate short reads, treat mid-header EOF as a normal disconnect).
  • Added bounds check currentMessageSize <= 0 → log warning and return cleanly instead of crashing on a corrupt header.
  • Wrapped server-side WriteAsync/FlushAsync/WaitForPipeDrain in try/catch (IOException/ObjectDisposedException) → set clientDisconnected and exit the loop after resetting buffers, rather than tearing down the host.

NamedPipeClient.cs

  • Symmetric write-side hardening: catches the same IOException/ObjectDisposedException and routes through the existing _environment.Exit(GenericFailure) path used by the read-EOF handler.
  • Short-read tolerance on the response header; bounds check currentMessageSize <= 0 → exit on corruption.
  • Wrapped response Deserialize in try/catch (excluding OperationCanceledException) so protocol corruption exits cleanly instead of bubbling an undecorated deserialization exception.

Test

  • New regression test NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost sends a zero-byte size header via a raw NamedPipeClientStream and asserts the server stays alive instead of throwing ApplicationStateGuard.Unreachable.
  • All existing Microsoft.Testing.Platform.UnitTests IPC tests still pass on net9.0.

Notes

  • No public API changes.
  • Behavior matches the existing GenericFailure exit code used for the read-EOF path on the client side.
  • WaitConnectionAsync FailFast is intentionally preserved; only loop-body IO faults get graceful handling.

Replace ApplicationStateGuard.Unreachable() throws in IPC header/payload reads with graceful disconnect handling. Tolerate short reads, treat mid-header EOF as graceful disconnect, validate currentMessageSize > 0, and catch IOException/ObjectDisposedException during write/flush/drain so a peer disconnect cannot crash the host or client process.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 13:51

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the Microsoft.Testing.Platform named-pipe IPC transport so disconnects and certain protocol-corruption scenarios don’t crash the host/client process (replacing prior “unreachable” paths with graceful exits).

Changes:

  • Server: tolerate short/EOF header reads, reject non-positive message sizes, and handle write-side disconnects without FailFast.
  • Client: add write-side disconnect handling, tolerate short response headers, and treat response deserialization failures as a generic IPC failure exit.
  • Tests: add a regression test to ensure an invalid (0) message-size header doesn’t crash the host.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.csAdds regression coverage for invalid message-size header handling on the server.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.csMakes the server loop tolerant to mid-header EOF/short reads and write-side disconnects; logs and exits cleanly on certain corrupt headers.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.csAdds write-side disconnect handling, short-read header handling, and exits cleanly on corruption/deserialization errors.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 4

Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.cs Outdated
Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs Outdated
Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Test Coverage Gaps

The PR introduces 8 new error paths but only tests 1 (server-side zero message size). Missing test scenarios:

Client-side (4 untested paths):

  1. Write-side IOException: Server closes pipe during WriteAsync → verify _environment.Exit(GenericFailure) called
  2. Mid-header disconnect: Server sends 2 bytes then closes → verify _environment.Exit and IOException thrown
  3. Negative message size: Server sends -1 header → verify _environment.Exit and InvalidOperationException thrown
  4. Deserialization failure: Server sends corrupted payload → verify _environment.Exit called

Server-side (3 untested paths):

  1. Mid-header disconnect: Client sends 2 bytes then closes → verify graceful loop exit
  2. Negative message size: Client sends -1 header → verify warning logged and loop exits (existing test only covers zero, not negative)
  3. Byte-count overflow: Client declares N bytes but writes >N bytes → verify warning logged and loop exits
  4. Write-side IOException: Client disconnects during server reply → verify graceful loop exit

All 8 scenarios are concrete failing interleavings that the new code explicitly handles. Recommend adding these tests to prevent regressions.

Generated by Expert Code Review (on open) for issue #8602 · ● 4M ·

// If currentRequestSize is 0, we need to read the message size
if (currentMessageSize == 0)
{
// We need at least sizeof(int) bytes to parse the message-size header. A pipe read can

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is adding a lot of additional code. Is there a concrete bug that makes it worth adding this?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(Reading this as a question rather than a request to revert — happy to do either.)

No specific user-visible repro for the write-side hardening, but it's there to keep the client symmetric with the existing read-EOF path right below it: today, if the server disconnects mid-write the client surfaces a raw IOException/ObjectDisposedException to whoever called RequestReplyAsync (e.g. the test host orchestration code) instead of the deterministic _environment.Exit(GenericFailure) we already use for read-EOF. Two cases this matters in practice:

  1. dotnet test orchestration tears down the test host process mid-RPC — caller gets a raw IO exception instead of the documented exit code.
  2. The MTP host crashes/FailFasts on its end while the client is flushing — same thing.

The concrete server-side bug fixed in this PR (the one with the regression test) is the ApplicationStateGuard.Unreachable() -> FailFast crash on a corrupt/short header byte. The client write-side changes are essentially: 3 lines of catch (Exception ex) when (ex is IOException or ObjectDisposedException) doing what the read-EOF block 30 lines above already does.

If you'd rather I scope this PR down to just the server-side crash + the matching read-side tightening and pull the write-side symmetry into a separate PR (or just drop it), happy to do that — let me know.

- Tighten message-size validation on both server and client: payload must
contain at least a 4-byte serializer id, so reject sizes < sizeof(int)
(not just <= 0) as protocol corruption.
- Client: add the symmetric `missingBytesToReadOfWholeMessage < 0` guard
the server already has, so an over-long body exits cleanly instead of
hanging.
- Server: drop the useless `missingBytesToReadOfCurrentChunk = currentReadBytes;`
reassignment inside the header accumulation loop (recomputed at line 182).
- Test: use `TaskCompletionSource` for the callback signal and assert
after the server has been disposed, so the loop task has definitely
completed before we check that the callback did not run.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build failed due to a code style violation: a using statement can be simplified to a using declaration.

Root cause: IDE0063 style rule violation

The newly added test method NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost uses a traditional using statement with braces on line 261:

using(varraw=newSystem.IO.Pipes.NamedPipeClientStream(...)){// ...}

The project enforces [IDE0063]((learn.microsoft.com/redacted) as an error, which requires the simpler using declaration pattern:

usingvarraw=newSystem.IO.Pipes.NamedPipeClientStream(...);// ...

This single style violation is reported for each target framework the project builds (net8.0 and net9.0), resulting in multiple error instances from the same root cause.

Affected files / errors

Proposed fix

Convert the traditional using statement to a using declaration and adjust the code block accordingly:

 Task waitConnection = server.WaitConnectionAsync(_testContext.CancellationToken);
- using (var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous))- {- await raw.ConnectAsync(_testContext.CancellationToken);- await waitConnection;+ using var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous);+ await raw.ConnectAsync(_testContext.CancellationToken);+ await waitConnection;- // Write a zero-length message size header (invalid per protocol).- byte[] invalidHeader = BitConverter.GetBytes(0);- await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);- await raw.FlushAsync(_testContext.CancellationToken);- }+ // Write a zero-length message size header (invalid per protocol).+ byte[] invalidHeader = BitConverter.GetBytes(0);+ await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);+ await raw.FlushAsync(_testContext.CancellationToken);

Build overview

Configuration: Debug
Exit code: 1 (failure)
Error count: 2 (4 instances across target frameworks)
Warning count: 0

Failed project:

  • Microsoft.Testing.Platform.UnitTests.csproj (net8.0, net9.0)
All MSBuild errors (2)
CodeProjectFile:LineMessage
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net8.0)
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net9.0)

🤖 Generated by the Build Failure Analysis workflow · commit 60797b2

Generated by Build Failure Analysis for issue #8602 · ● 2.2M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8602 · ● 2.2M

Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
CopilotAI review requested due to automatic review settings May 27, 2026 09:00

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new

Per review feedback, the carefully-analyzed invariants in the IPC read
loop (Unreachable() guards on short header reads and overshoot of the
declared message size) should not be weakened into 'graceful' handlers
without evidence the conditions can occur. Replacing them with
LogWarning is also wrong: those are internal-bug states, not
user-actionable warnings.
Reverted from the original PR:
- NamedPipeServer: restore 'currentReadBytes < sizeof(int)' Unreachable
- NamedPipeServer: restore 'missingBytesToReadOfWholeMessage < 0' Unreachable
- NamedPipeServer: drop the added 'currentMessageSize < sizeof(int)' warning
- NamedPipeServer: restore the existing client-disconnect LogDebugAsync and
the Dispose/DisposeAsync LogError diagnostics
- NamedPipeClient: drop the symmetric (untested) header short-read loop,
the 'currentMessageSize < sizeof(int)' check, the
'missingBytesToReadOfWholeMessage < 0' check, and the try/catch around
Deserialize
- IPCTests: drop the NamedPipeServer_InvalidMessageSizeHeader regression
test (no longer applicable now that the invariant throw is restored)
Kept (legitimate runtime conditions caused by a real peer disconnect):
- NamedPipeServer: try/catch on WriteAsync/FlushAsync/WaitForPipeDrain
for IOException/ObjectDisposedException -> exit the loop cleanly
- NamedPipeClient: symmetric try/catch on the write side ->
_environment.Exit(GenericFailure)
Also strengthen .github/agents/expert-reviewer.agent.md to flag this
pattern in future reviews (new rules under Algorithmic Correctness and
Defensive Coding at Boundaries: don't weaken ApplicationStateGuard
.Unreachable without evidence; don't log internal-bug states as warnings;
defensive coding belongs at trust boundaries, not on internal invariants
between trusted in-process components).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Thanks for the careful review — you're right on both points and I just pushed a commit that addresses them.

What I changed back (your feedback)

  • NamedPipeServer.cs: restored throw ApplicationStateGuard.Unreachable() for both currentReadBytes < sizeof(int) and missingBytesToReadOfWholeMessage < 0. Also dropped the added currentMessageSize < sizeof(int)LogWarning path. You're right that LogWarning is the wrong severity for an unexpected-state condition the user can't act on, and replacing a documented invariant with handling logic without evidence the condition is reachable is regressing the design that was carefully worked out with Marco.
  • NamedPipeClient.cs: same reasoning — dropped the symmetric short-read loop, the size bounds check, the overshoot check, and the try/catch around Deserialize that I had mirrored. None of them were driven by evidence and they were adding untested error paths.
  • Dropped the NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost regression test (it was only validating the behavior we just reverted).

What I kept

Only the parts where the exceptions are caused by an externally observable condition (the peer process crashing/exiting mid-write), not by an internal invariant:

  • NamedPipeServer.cs: try/catch (IOException or ObjectDisposedException) around the reply WriteAsync/FlushAsync/WaitForPipeDrain so a client that disconnects between request and reply doesn't FailFast the host.
  • NamedPipeClient.cs: symmetric try/catch on the request write side that routes through the same _environment.Exit(GenericFailure) path the existing read-EOF handler uses.

These match the existing read-EOF handling (currentReadBytes == 0 -> graceful return / Exit(GenericFailure)) and don't touch any invariant.

Expert reviewer

I also updated .github/agents/expert-reviewer.agent.md so the same pattern gets flagged in future reviews — added rules to Algorithmic Correctness and Defensive Coding at Boundaries:

  • Don't weaken ApplicationStateGuard.Unreachable() / Debug.Assert / invariant throws into ""graceful"" handling without a concrete repro.
  • An internal-bug condition is LogError + abort, never LogWarning (which is user-actionable severity).
  • Defensive coding belongs at trust boundaries, not on internal invariants between trusted in-process components.

All 965 Microsoft.Testing.Platform.UnitTests still pass on net9.0.

@Evangelink
Amaury Levé (Evangelink) merged commit ce14f31 into mainMay 27, 2026
24 of 26 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/ipc-disconnect-resilience branch May 27, 2026 11:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

IPC: handle disconnects and protocol corruption gracefully - #8602

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience
May 27, 2026
Merged

IPC: handle disconnects and protocol corruption gracefully#8602
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Hardens the Microsoft.Testing.Platform IPC layer (named-pipe transport between the test host and its in-process clients) so that a peer disconnect or a corrupt/short header byte no longer takes down the host or client process.

Audit items addressed

Part of the P0+P1 exception-handling audit (items #4#10, IPC).

Changes

NamedPipeServer.cs

  • Replaced ApplicationStateGuard.Unreachable() throws in the header read path with graceful disconnect handling (tolerate short reads, treat mid-header EOF as a normal disconnect).
  • Added bounds check currentMessageSize <= 0 → log warning and return cleanly instead of crashing on a corrupt header.
  • Wrapped server-side WriteAsync/FlushAsync/WaitForPipeDrain in try/catch (IOException/ObjectDisposedException) → set clientDisconnected and exit the loop after resetting buffers, rather than tearing down the host.

NamedPipeClient.cs

  • Symmetric write-side hardening: catches the same IOException/ObjectDisposedException and routes through the existing _environment.Exit(GenericFailure) path used by the read-EOF handler.
  • Short-read tolerance on the response header; bounds check currentMessageSize <= 0 → exit on corruption.
  • Wrapped response Deserialize in try/catch (excluding OperationCanceledException) so protocol corruption exits cleanly instead of bubbling an undecorated deserialization exception.

Test

  • New regression test NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost sends a zero-byte size header via a raw NamedPipeClientStream and asserts the server stays alive instead of throwing ApplicationStateGuard.Unreachable.
  • All existing Microsoft.Testing.Platform.UnitTests IPC tests still pass on net9.0.

Notes

  • No public API changes.
  • Behavior matches the existing GenericFailure exit code used for the read-EOF path on the client side.
  • WaitConnectionAsync FailFast is intentionally preserved; only loop-body IO faults get graceful handling.

Replace ApplicationStateGuard.Unreachable() throws in IPC header/payload reads with graceful disconnect handling. Tolerate short reads, treat mid-header EOF as graceful disconnect, validate currentMessageSize > 0, and catch IOException/ObjectDisposedException during write/flush/drain so a peer disconnect cannot crash the host or client process.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 13:51

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the Microsoft.Testing.Platform named-pipe IPC transport so disconnects and certain protocol-corruption scenarios don’t crash the host/client process (replacing prior “unreachable” paths with graceful exits).

Changes:

  • Server: tolerate short/EOF header reads, reject non-positive message sizes, and handle write-side disconnects without FailFast.
  • Client: add write-side disconnect handling, tolerate short response headers, and treat response deserialization failures as a generic IPC failure exit.
  • Tests: add a regression test to ensure an invalid (0) message-size header doesn’t crash the host.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.csAdds regression coverage for invalid message-size header handling on the server.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.csMakes the server loop tolerant to mid-header EOF/short reads and write-side disconnects; logs and exits cleanly on certain corrupt headers.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.csAdds write-side disconnect handling, short-read header handling, and exits cleanly on corruption/deserialization errors.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 4

Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.cs Outdated
Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs Outdated
Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Test Coverage Gaps

The PR introduces 8 new error paths but only tests 1 (server-side zero message size). Missing test scenarios:

Client-side (4 untested paths):

  1. Write-side IOException: Server closes pipe during WriteAsync → verify _environment.Exit(GenericFailure) called
  2. Mid-header disconnect: Server sends 2 bytes then closes → verify _environment.Exit and IOException thrown
  3. Negative message size: Server sends -1 header → verify _environment.Exit and InvalidOperationException thrown
  4. Deserialization failure: Server sends corrupted payload → verify _environment.Exit called

Server-side (3 untested paths):

  1. Mid-header disconnect: Client sends 2 bytes then closes → verify graceful loop exit
  2. Negative message size: Client sends -1 header → verify warning logged and loop exits (existing test only covers zero, not negative)
  3. Byte-count overflow: Client declares N bytes but writes >N bytes → verify warning logged and loop exits
  4. Write-side IOException: Client disconnects during server reply → verify graceful loop exit

All 8 scenarios are concrete failing interleavings that the new code explicitly handles. Recommend adding these tests to prevent regressions.

Generated by Expert Code Review (on open) for issue #8602 · ● 4M ·

// If currentRequestSize is 0, we need to read the message size
if (currentMessageSize == 0)
{
// We need at least sizeof(int) bytes to parse the message-size header. A pipe read can

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is adding a lot of additional code. Is there a concrete bug that makes it worth adding this?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(Reading this as a question rather than a request to revert — happy to do either.)

No specific user-visible repro for the write-side hardening, but it's there to keep the client symmetric with the existing read-EOF path right below it: today, if the server disconnects mid-write the client surfaces a raw IOException/ObjectDisposedException to whoever called RequestReplyAsync (e.g. the test host orchestration code) instead of the deterministic _environment.Exit(GenericFailure) we already use for read-EOF. Two cases this matters in practice:

  1. dotnet test orchestration tears down the test host process mid-RPC — caller gets a raw IO exception instead of the documented exit code.
  2. The MTP host crashes/FailFasts on its end while the client is flushing — same thing.

The concrete server-side bug fixed in this PR (the one with the regression test) is the ApplicationStateGuard.Unreachable() -> FailFast crash on a corrupt/short header byte. The client write-side changes are essentially: 3 lines of catch (Exception ex) when (ex is IOException or ObjectDisposedException) doing what the read-EOF block 30 lines above already does.

If you'd rather I scope this PR down to just the server-side crash + the matching read-side tightening and pull the write-side symmetry into a separate PR (or just drop it), happy to do that — let me know.

- Tighten message-size validation on both server and client: payload must
contain at least a 4-byte serializer id, so reject sizes < sizeof(int)
(not just <= 0) as protocol corruption.
- Client: add the symmetric `missingBytesToReadOfWholeMessage < 0` guard
the server already has, so an over-long body exits cleanly instead of
hanging.
- Server: drop the useless `missingBytesToReadOfCurrentChunk = currentReadBytes;`
reassignment inside the header accumulation loop (recomputed at line 182).
- Test: use `TaskCompletionSource` for the callback signal and assert
after the server has been disposed, so the loop task has definitely
completed before we check that the callback did not run.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build failed due to a code style violation: a using statement can be simplified to a using declaration.

Root cause: IDE0063 style rule violation

The newly added test method NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost uses a traditional using statement with braces on line 261:

using(varraw=newSystem.IO.Pipes.NamedPipeClientStream(...)){// ...}

The project enforces [IDE0063]((learn.microsoft.com/redacted) as an error, which requires the simpler using declaration pattern:

usingvarraw=newSystem.IO.Pipes.NamedPipeClientStream(...);// ...

This single style violation is reported for each target framework the project builds (net8.0 and net9.0), resulting in multiple error instances from the same root cause.

Affected files / errors

Proposed fix

Convert the traditional using statement to a using declaration and adjust the code block accordingly:

 Task waitConnection = server.WaitConnectionAsync(_testContext.CancellationToken);
- using (var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous))- {- await raw.ConnectAsync(_testContext.CancellationToken);- await waitConnection;+ using var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous);+ await raw.ConnectAsync(_testContext.CancellationToken);+ await waitConnection;- // Write a zero-length message size header (invalid per protocol).- byte[] invalidHeader = BitConverter.GetBytes(0);- await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);- await raw.FlushAsync(_testContext.CancellationToken);- }+ // Write a zero-length message size header (invalid per protocol).+ byte[] invalidHeader = BitConverter.GetBytes(0);+ await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);+ await raw.FlushAsync(_testContext.CancellationToken);

Build overview

Configuration: Debug
Exit code: 1 (failure)
Error count: 2 (4 instances across target frameworks)
Warning count: 0

Failed project:

  • Microsoft.Testing.Platform.UnitTests.csproj (net8.0, net9.0)
All MSBuild errors (2)
CodeProjectFile:LineMessage
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net8.0)
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net9.0)

🤖 Generated by the Build Failure Analysis workflow · commit 60797b2

Generated by Build Failure Analysis for issue #8602 · ● 2.2M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8602 · ● 2.2M

Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
CopilotAI review requested due to automatic review settings May 27, 2026 09:00

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new

Per review feedback, the carefully-analyzed invariants in the IPC read
loop (Unreachable() guards on short header reads and overshoot of the
declared message size) should not be weakened into 'graceful' handlers
without evidence the conditions can occur. Replacing them with
LogWarning is also wrong: those are internal-bug states, not
user-actionable warnings.
Reverted from the original PR:
- NamedPipeServer: restore 'currentReadBytes < sizeof(int)' Unreachable
- NamedPipeServer: restore 'missingBytesToReadOfWholeMessage < 0' Unreachable
- NamedPipeServer: drop the added 'currentMessageSize < sizeof(int)' warning
- NamedPipeServer: restore the existing client-disconnect LogDebugAsync and
the Dispose/DisposeAsync LogError diagnostics
- NamedPipeClient: drop the symmetric (untested) header short-read loop,
the 'currentMessageSize < sizeof(int)' check, the
'missingBytesToReadOfWholeMessage < 0' check, and the try/catch around
Deserialize
- IPCTests: drop the NamedPipeServer_InvalidMessageSizeHeader regression
test (no longer applicable now that the invariant throw is restored)
Kept (legitimate runtime conditions caused by a real peer disconnect):
- NamedPipeServer: try/catch on WriteAsync/FlushAsync/WaitForPipeDrain
for IOException/ObjectDisposedException -> exit the loop cleanly
- NamedPipeClient: symmetric try/catch on the write side ->
_environment.Exit(GenericFailure)
Also strengthen .github/agents/expert-reviewer.agent.md to flag this
pattern in future reviews (new rules under Algorithmic Correctness and
Defensive Coding at Boundaries: don't weaken ApplicationStateGuard
.Unreachable without evidence; don't log internal-bug states as warnings;
defensive coding belongs at trust boundaries, not on internal invariants
between trusted in-process components).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Thanks for the careful review — you're right on both points and I just pushed a commit that addresses them.

What I changed back (your feedback)

  • NamedPipeServer.cs: restored throw ApplicationStateGuard.Unreachable() for both currentReadBytes < sizeof(int) and missingBytesToReadOfWholeMessage < 0. Also dropped the added currentMessageSize < sizeof(int)LogWarning path. You're right that LogWarning is the wrong severity for an unexpected-state condition the user can't act on, and replacing a documented invariant with handling logic without evidence the condition is reachable is regressing the design that was carefully worked out with Marco.
  • NamedPipeClient.cs: same reasoning — dropped the symmetric short-read loop, the size bounds check, the overshoot check, and the try/catch around Deserialize that I had mirrored. None of them were driven by evidence and they were adding untested error paths.
  • Dropped the NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost regression test (it was only validating the behavior we just reverted).

What I kept

Only the parts where the exceptions are caused by an externally observable condition (the peer process crashing/exiting mid-write), not by an internal invariant:

  • NamedPipeServer.cs: try/catch (IOException or ObjectDisposedException) around the reply WriteAsync/FlushAsync/WaitForPipeDrain so a client that disconnects between request and reply doesn't FailFast the host.
  • NamedPipeClient.cs: symmetric try/catch on the request write side that routes through the same _environment.Exit(GenericFailure) path the existing read-EOF handler uses.

These match the existing read-EOF handling (currentReadBytes == 0 -> graceful return / Exit(GenericFailure)) and don't touch any invariant.

Expert reviewer

I also updated .github/agents/expert-reviewer.agent.md so the same pattern gets flagged in future reviews — added rules to Algorithmic Correctness and Defensive Coding at Boundaries:

  • Don't weaken ApplicationStateGuard.Unreachable() / Debug.Assert / invariant throws into ""graceful"" handling without a concrete repro.
  • An internal-bug condition is LogError + abort, never LogWarning (which is user-actionable severity).
  • Defensive coding belongs at trust boundaries, not on internal invariants between trusted in-process components.

All 965 Microsoft.Testing.Platform.UnitTests still pass on net9.0.

@Evangelink
Amaury Levé (Evangelink) merged commit ce14f31 into mainMay 27, 2026
24 of 26 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/ipc-disconnect-resilience branch May 27, 2026 11:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

IPC: handle disconnects and protocol corruption gracefully - #8602

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience
May 27, 2026
Merged

IPC: handle disconnects and protocol corruption gracefully#8602
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Hardens the Microsoft.Testing.Platform IPC layer (named-pipe transport between the test host and its in-process clients) so that a peer disconnect or a corrupt/short header byte no longer takes down the host or client process.

Audit items addressed

Part of the P0+P1 exception-handling audit (items #4#10, IPC).

Changes

NamedPipeServer.cs

  • Replaced ApplicationStateGuard.Unreachable() throws in the header read path with graceful disconnect handling (tolerate short reads, treat mid-header EOF as a normal disconnect).
  • Added bounds check currentMessageSize <= 0 → log warning and return cleanly instead of crashing on a corrupt header.
  • Wrapped server-side WriteAsync/FlushAsync/WaitForPipeDrain in try/catch (IOException/ObjectDisposedException) → set clientDisconnected and exit the loop after resetting buffers, rather than tearing down the host.

NamedPipeClient.cs

  • Symmetric write-side hardening: catches the same IOException/ObjectDisposedException and routes through the existing _environment.Exit(GenericFailure) path used by the read-EOF handler.
  • Short-read tolerance on the response header; bounds check currentMessageSize <= 0 → exit on corruption.
  • Wrapped response Deserialize in try/catch (excluding OperationCanceledException) so protocol corruption exits cleanly instead of bubbling an undecorated deserialization exception.

Test

  • New regression test NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost sends a zero-byte size header via a raw NamedPipeClientStream and asserts the server stays alive instead of throwing ApplicationStateGuard.Unreachable.
  • All existing Microsoft.Testing.Platform.UnitTests IPC tests still pass on net9.0.

Notes

  • No public API changes.
  • Behavior matches the existing GenericFailure exit code used for the read-EOF path on the client side.
  • WaitConnectionAsync FailFast is intentionally preserved; only loop-body IO faults get graceful handling.

Replace ApplicationStateGuard.Unreachable() throws in IPC header/payload reads with graceful disconnect handling. Tolerate short reads, treat mid-header EOF as graceful disconnect, validate currentMessageSize > 0, and catch IOException/ObjectDisposedException during write/flush/drain so a peer disconnect cannot crash the host or client process.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 13:51

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the Microsoft.Testing.Platform named-pipe IPC transport so disconnects and certain protocol-corruption scenarios don’t crash the host/client process (replacing prior “unreachable” paths with graceful exits).

Changes:

  • Server: tolerate short/EOF header reads, reject non-positive message sizes, and handle write-side disconnects without FailFast.
  • Client: add write-side disconnect handling, tolerate short response headers, and treat response deserialization failures as a generic IPC failure exit.
  • Tests: add a regression test to ensure an invalid (0) message-size header doesn’t crash the host.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.csAdds regression coverage for invalid message-size header handling on the server.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.csMakes the server loop tolerant to mid-header EOF/short reads and write-side disconnects; logs and exits cleanly on certain corrupt headers.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.csAdds write-side disconnect handling, short-read header handling, and exits cleanly on corruption/deserialization errors.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 4

Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.cs Outdated
Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs Outdated
Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Test Coverage Gaps

The PR introduces 8 new error paths but only tests 1 (server-side zero message size). Missing test scenarios:

Client-side (4 untested paths):

  1. Write-side IOException: Server closes pipe during WriteAsync → verify _environment.Exit(GenericFailure) called
  2. Mid-header disconnect: Server sends 2 bytes then closes → verify _environment.Exit and IOException thrown
  3. Negative message size: Server sends -1 header → verify _environment.Exit and InvalidOperationException thrown
  4. Deserialization failure: Server sends corrupted payload → verify _environment.Exit called

Server-side (3 untested paths):

  1. Mid-header disconnect: Client sends 2 bytes then closes → verify graceful loop exit
  2. Negative message size: Client sends -1 header → verify warning logged and loop exits (existing test only covers zero, not negative)
  3. Byte-count overflow: Client declares N bytes but writes >N bytes → verify warning logged and loop exits
  4. Write-side IOException: Client disconnects during server reply → verify graceful loop exit

All 8 scenarios are concrete failing interleavings that the new code explicitly handles. Recommend adding these tests to prevent regressions.

Generated by Expert Code Review (on open) for issue #8602 · ● 4M ·

// If currentRequestSize is 0, we need to read the message size
if (currentMessageSize == 0)
{
// We need at least sizeof(int) bytes to parse the message-size header. A pipe read can

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is adding a lot of additional code. Is there a concrete bug that makes it worth adding this?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(Reading this as a question rather than a request to revert — happy to do either.)

No specific user-visible repro for the write-side hardening, but it's there to keep the client symmetric with the existing read-EOF path right below it: today, if the server disconnects mid-write the client surfaces a raw IOException/ObjectDisposedException to whoever called RequestReplyAsync (e.g. the test host orchestration code) instead of the deterministic _environment.Exit(GenericFailure) we already use for read-EOF. Two cases this matters in practice:

  1. dotnet test orchestration tears down the test host process mid-RPC — caller gets a raw IO exception instead of the documented exit code.
  2. The MTP host crashes/FailFasts on its end while the client is flushing — same thing.

The concrete server-side bug fixed in this PR (the one with the regression test) is the ApplicationStateGuard.Unreachable() -> FailFast crash on a corrupt/short header byte. The client write-side changes are essentially: 3 lines of catch (Exception ex) when (ex is IOException or ObjectDisposedException) doing what the read-EOF block 30 lines above already does.

If you'd rather I scope this PR down to just the server-side crash + the matching read-side tightening and pull the write-side symmetry into a separate PR (or just drop it), happy to do that — let me know.

- Tighten message-size validation on both server and client: payload must
contain at least a 4-byte serializer id, so reject sizes < sizeof(int)
(not just <= 0) as protocol corruption.
- Client: add the symmetric `missingBytesToReadOfWholeMessage < 0` guard
the server already has, so an over-long body exits cleanly instead of
hanging.
- Server: drop the useless `missingBytesToReadOfCurrentChunk = currentReadBytes;`
reassignment inside the header accumulation loop (recomputed at line 182).
- Test: use `TaskCompletionSource` for the callback signal and assert
after the server has been disposed, so the loop task has definitely
completed before we check that the callback did not run.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build failed due to a code style violation: a using statement can be simplified to a using declaration.

Root cause: IDE0063 style rule violation

The newly added test method NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost uses a traditional using statement with braces on line 261:

using(varraw=newSystem.IO.Pipes.NamedPipeClientStream(...)){// ...}

The project enforces [IDE0063]((learn.microsoft.com/redacted) as an error, which requires the simpler using declaration pattern:

usingvarraw=newSystem.IO.Pipes.NamedPipeClientStream(...);// ...

This single style violation is reported for each target framework the project builds (net8.0 and net9.0), resulting in multiple error instances from the same root cause.

Affected files / errors

Proposed fix

Convert the traditional using statement to a using declaration and adjust the code block accordingly:

 Task waitConnection = server.WaitConnectionAsync(_testContext.CancellationToken);
- using (var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous))- {- await raw.ConnectAsync(_testContext.CancellationToken);- await waitConnection;+ using var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous);+ await raw.ConnectAsync(_testContext.CancellationToken);+ await waitConnection;- // Write a zero-length message size header (invalid per protocol).- byte[] invalidHeader = BitConverter.GetBytes(0);- await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);- await raw.FlushAsync(_testContext.CancellationToken);- }+ // Write a zero-length message size header (invalid per protocol).+ byte[] invalidHeader = BitConverter.GetBytes(0);+ await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);+ await raw.FlushAsync(_testContext.CancellationToken);

Build overview

Configuration: Debug
Exit code: 1 (failure)
Error count: 2 (4 instances across target frameworks)
Warning count: 0

Failed project:

  • Microsoft.Testing.Platform.UnitTests.csproj (net8.0, net9.0)
All MSBuild errors (2)
CodeProjectFile:LineMessage
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net8.0)
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net9.0)

🤖 Generated by the Build Failure Analysis workflow · commit 60797b2

Generated by Build Failure Analysis for issue #8602 · ● 2.2M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8602 · ● 2.2M

Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
CopilotAI review requested due to automatic review settings May 27, 2026 09:00

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new

Per review feedback, the carefully-analyzed invariants in the IPC read
loop (Unreachable() guards on short header reads and overshoot of the
declared message size) should not be weakened into 'graceful' handlers
without evidence the conditions can occur. Replacing them with
LogWarning is also wrong: those are internal-bug states, not
user-actionable warnings.
Reverted from the original PR:
- NamedPipeServer: restore 'currentReadBytes < sizeof(int)' Unreachable
- NamedPipeServer: restore 'missingBytesToReadOfWholeMessage < 0' Unreachable
- NamedPipeServer: drop the added 'currentMessageSize < sizeof(int)' warning
- NamedPipeServer: restore the existing client-disconnect LogDebugAsync and
the Dispose/DisposeAsync LogError diagnostics
- NamedPipeClient: drop the symmetric (untested) header short-read loop,
the 'currentMessageSize < sizeof(int)' check, the
'missingBytesToReadOfWholeMessage < 0' check, and the try/catch around
Deserialize
- IPCTests: drop the NamedPipeServer_InvalidMessageSizeHeader regression
test (no longer applicable now that the invariant throw is restored)
Kept (legitimate runtime conditions caused by a real peer disconnect):
- NamedPipeServer: try/catch on WriteAsync/FlushAsync/WaitForPipeDrain
for IOException/ObjectDisposedException -> exit the loop cleanly
- NamedPipeClient: symmetric try/catch on the write side ->
_environment.Exit(GenericFailure)
Also strengthen .github/agents/expert-reviewer.agent.md to flag this
pattern in future reviews (new rules under Algorithmic Correctness and
Defensive Coding at Boundaries: don't weaken ApplicationStateGuard
.Unreachable without evidence; don't log internal-bug states as warnings;
defensive coding belongs at trust boundaries, not on internal invariants
between trusted in-process components).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Thanks for the careful review — you're right on both points and I just pushed a commit that addresses them.

What I changed back (your feedback)

  • NamedPipeServer.cs: restored throw ApplicationStateGuard.Unreachable() for both currentReadBytes < sizeof(int) and missingBytesToReadOfWholeMessage < 0. Also dropped the added currentMessageSize < sizeof(int)LogWarning path. You're right that LogWarning is the wrong severity for an unexpected-state condition the user can't act on, and replacing a documented invariant with handling logic without evidence the condition is reachable is regressing the design that was carefully worked out with Marco.
  • NamedPipeClient.cs: same reasoning — dropped the symmetric short-read loop, the size bounds check, the overshoot check, and the try/catch around Deserialize that I had mirrored. None of them were driven by evidence and they were adding untested error paths.
  • Dropped the NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost regression test (it was only validating the behavior we just reverted).

What I kept

Only the parts where the exceptions are caused by an externally observable condition (the peer process crashing/exiting mid-write), not by an internal invariant:

  • NamedPipeServer.cs: try/catch (IOException or ObjectDisposedException) around the reply WriteAsync/FlushAsync/WaitForPipeDrain so a client that disconnects between request and reply doesn't FailFast the host.
  • NamedPipeClient.cs: symmetric try/catch on the request write side that routes through the same _environment.Exit(GenericFailure) path the existing read-EOF handler uses.

These match the existing read-EOF handling (currentReadBytes == 0 -> graceful return / Exit(GenericFailure)) and don't touch any invariant.

Expert reviewer

I also updated .github/agents/expert-reviewer.agent.md so the same pattern gets flagged in future reviews — added rules to Algorithmic Correctness and Defensive Coding at Boundaries:

  • Don't weaken ApplicationStateGuard.Unreachable() / Debug.Assert / invariant throws into ""graceful"" handling without a concrete repro.
  • An internal-bug condition is LogError + abort, never LogWarning (which is user-actionable severity).
  • Defensive coding belongs at trust boundaries, not on internal invariants between trusted in-process components.

All 965 Microsoft.Testing.Platform.UnitTests still pass on net9.0.

@Evangelink
Amaury Levé (Evangelink) merged commit ce14f31 into mainMay 27, 2026
24 of 26 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/ipc-disconnect-resilience branch May 27, 2026 11:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

IPC: handle disconnects and protocol corruption gracefully - #8602

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience
May 27, 2026
Merged

IPC: handle disconnects and protocol corruption gracefully#8602
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Hardens the Microsoft.Testing.Platform IPC layer (named-pipe transport between the test host and its in-process clients) so that a peer disconnect or a corrupt/short header byte no longer takes down the host or client process.

Audit items addressed

Part of the P0+P1 exception-handling audit (items #4#10, IPC).

Changes

NamedPipeServer.cs

  • Replaced ApplicationStateGuard.Unreachable() throws in the header read path with graceful disconnect handling (tolerate short reads, treat mid-header EOF as a normal disconnect).
  • Added bounds check currentMessageSize <= 0 → log warning and return cleanly instead of crashing on a corrupt header.
  • Wrapped server-side WriteAsync/FlushAsync/WaitForPipeDrain in try/catch (IOException/ObjectDisposedException) → set clientDisconnected and exit the loop after resetting buffers, rather than tearing down the host.

NamedPipeClient.cs

  • Symmetric write-side hardening: catches the same IOException/ObjectDisposedException and routes through the existing _environment.Exit(GenericFailure) path used by the read-EOF handler.
  • Short-read tolerance on the response header; bounds check currentMessageSize <= 0 → exit on corruption.
  • Wrapped response Deserialize in try/catch (excluding OperationCanceledException) so protocol corruption exits cleanly instead of bubbling an undecorated deserialization exception.

Test

  • New regression test NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost sends a zero-byte size header via a raw NamedPipeClientStream and asserts the server stays alive instead of throwing ApplicationStateGuard.Unreachable.
  • All existing Microsoft.Testing.Platform.UnitTests IPC tests still pass on net9.0.

Notes

  • No public API changes.
  • Behavior matches the existing GenericFailure exit code used for the read-EOF path on the client side.
  • WaitConnectionAsync FailFast is intentionally preserved; only loop-body IO faults get graceful handling.

Replace ApplicationStateGuard.Unreachable() throws in IPC header/payload reads with graceful disconnect handling. Tolerate short reads, treat mid-header EOF as graceful disconnect, validate currentMessageSize > 0, and catch IOException/ObjectDisposedException during write/flush/drain so a peer disconnect cannot crash the host or client process.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 13:51

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the Microsoft.Testing.Platform named-pipe IPC transport so disconnects and certain protocol-corruption scenarios don’t crash the host/client process (replacing prior “unreachable” paths with graceful exits).

Changes:

  • Server: tolerate short/EOF header reads, reject non-positive message sizes, and handle write-side disconnects without FailFast.
  • Client: add write-side disconnect handling, tolerate short response headers, and treat response deserialization failures as a generic IPC failure exit.
  • Tests: add a regression test to ensure an invalid (0) message-size header doesn’t crash the host.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.csAdds regression coverage for invalid message-size header handling on the server.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.csMakes the server loop tolerant to mid-header EOF/short reads and write-side disconnects; logs and exits cleanly on certain corrupt headers.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.csAdds write-side disconnect handling, short-read header handling, and exits cleanly on corruption/deserialization errors.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 4

Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.cs Outdated
Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs Outdated
Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Test Coverage Gaps

The PR introduces 8 new error paths but only tests 1 (server-side zero message size). Missing test scenarios:

Client-side (4 untested paths):

  1. Write-side IOException: Server closes pipe during WriteAsync → verify _environment.Exit(GenericFailure) called
  2. Mid-header disconnect: Server sends 2 bytes then closes → verify _environment.Exit and IOException thrown
  3. Negative message size: Server sends -1 header → verify _environment.Exit and InvalidOperationException thrown
  4. Deserialization failure: Server sends corrupted payload → verify _environment.Exit called

Server-side (3 untested paths):

  1. Mid-header disconnect: Client sends 2 bytes then closes → verify graceful loop exit
  2. Negative message size: Client sends -1 header → verify warning logged and loop exits (existing test only covers zero, not negative)
  3. Byte-count overflow: Client declares N bytes but writes >N bytes → verify warning logged and loop exits
  4. Write-side IOException: Client disconnects during server reply → verify graceful loop exit

All 8 scenarios are concrete failing interleavings that the new code explicitly handles. Recommend adding these tests to prevent regressions.

Generated by Expert Code Review (on open) for issue #8602 · ● 4M ·

// If currentRequestSize is 0, we need to read the message size
if (currentMessageSize == 0)
{
// We need at least sizeof(int) bytes to parse the message-size header. A pipe read can

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is adding a lot of additional code. Is there a concrete bug that makes it worth adding this?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(Reading this as a question rather than a request to revert — happy to do either.)

No specific user-visible repro for the write-side hardening, but it's there to keep the client symmetric with the existing read-EOF path right below it: today, if the server disconnects mid-write the client surfaces a raw IOException/ObjectDisposedException to whoever called RequestReplyAsync (e.g. the test host orchestration code) instead of the deterministic _environment.Exit(GenericFailure) we already use for read-EOF. Two cases this matters in practice:

  1. dotnet test orchestration tears down the test host process mid-RPC — caller gets a raw IO exception instead of the documented exit code.
  2. The MTP host crashes/FailFasts on its end while the client is flushing — same thing.

The concrete server-side bug fixed in this PR (the one with the regression test) is the ApplicationStateGuard.Unreachable() -> FailFast crash on a corrupt/short header byte. The client write-side changes are essentially: 3 lines of catch (Exception ex) when (ex is IOException or ObjectDisposedException) doing what the read-EOF block 30 lines above already does.

If you'd rather I scope this PR down to just the server-side crash + the matching read-side tightening and pull the write-side symmetry into a separate PR (or just drop it), happy to do that — let me know.

- Tighten message-size validation on both server and client: payload must
contain at least a 4-byte serializer id, so reject sizes < sizeof(int)
(not just <= 0) as protocol corruption.
- Client: add the symmetric `missingBytesToReadOfWholeMessage < 0` guard
the server already has, so an over-long body exits cleanly instead of
hanging.
- Server: drop the useless `missingBytesToReadOfCurrentChunk = currentReadBytes;`
reassignment inside the header accumulation loop (recomputed at line 182).
- Test: use `TaskCompletionSource` for the callback signal and assert
after the server has been disposed, so the loop task has definitely
completed before we check that the callback did not run.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build failed due to a code style violation: a using statement can be simplified to a using declaration.

Root cause: IDE0063 style rule violation

The newly added test method NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost uses a traditional using statement with braces on line 261:

using(varraw=newSystem.IO.Pipes.NamedPipeClientStream(...)){// ...}

The project enforces [IDE0063]((learn.microsoft.com/redacted) as an error, which requires the simpler using declaration pattern:

usingvarraw=newSystem.IO.Pipes.NamedPipeClientStream(...);// ...

This single style violation is reported for each target framework the project builds (net8.0 and net9.0), resulting in multiple error instances from the same root cause.

Affected files / errors

Proposed fix

Convert the traditional using statement to a using declaration and adjust the code block accordingly:

 Task waitConnection = server.WaitConnectionAsync(_testContext.CancellationToken);
- using (var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous))- {- await raw.ConnectAsync(_testContext.CancellationToken);- await waitConnection;+ using var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous);+ await raw.ConnectAsync(_testContext.CancellationToken);+ await waitConnection;- // Write a zero-length message size header (invalid per protocol).- byte[] invalidHeader = BitConverter.GetBytes(0);- await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);- await raw.FlushAsync(_testContext.CancellationToken);- }+ // Write a zero-length message size header (invalid per protocol).+ byte[] invalidHeader = BitConverter.GetBytes(0);+ await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);+ await raw.FlushAsync(_testContext.CancellationToken);

Build overview

Configuration: Debug
Exit code: 1 (failure)
Error count: 2 (4 instances across target frameworks)
Warning count: 0

Failed project:

  • Microsoft.Testing.Platform.UnitTests.csproj (net8.0, net9.0)
All MSBuild errors (2)
CodeProjectFile:LineMessage
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net8.0)
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net9.0)

🤖 Generated by the Build Failure Analysis workflow · commit 60797b2

Generated by Build Failure Analysis for issue #8602 · ● 2.2M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8602 · ● 2.2M

Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
CopilotAI review requested due to automatic review settings May 27, 2026 09:00

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new

Per review feedback, the carefully-analyzed invariants in the IPC read
loop (Unreachable() guards on short header reads and overshoot of the
declared message size) should not be weakened into 'graceful' handlers
without evidence the conditions can occur. Replacing them with
LogWarning is also wrong: those are internal-bug states, not
user-actionable warnings.
Reverted from the original PR:
- NamedPipeServer: restore 'currentReadBytes < sizeof(int)' Unreachable
- NamedPipeServer: restore 'missingBytesToReadOfWholeMessage < 0' Unreachable
- NamedPipeServer: drop the added 'currentMessageSize < sizeof(int)' warning
- NamedPipeServer: restore the existing client-disconnect LogDebugAsync and
the Dispose/DisposeAsync LogError diagnostics
- NamedPipeClient: drop the symmetric (untested) header short-read loop,
the 'currentMessageSize < sizeof(int)' check, the
'missingBytesToReadOfWholeMessage < 0' check, and the try/catch around
Deserialize
- IPCTests: drop the NamedPipeServer_InvalidMessageSizeHeader regression
test (no longer applicable now that the invariant throw is restored)
Kept (legitimate runtime conditions caused by a real peer disconnect):
- NamedPipeServer: try/catch on WriteAsync/FlushAsync/WaitForPipeDrain
for IOException/ObjectDisposedException -> exit the loop cleanly
- NamedPipeClient: symmetric try/catch on the write side ->
_environment.Exit(GenericFailure)
Also strengthen .github/agents/expert-reviewer.agent.md to flag this
pattern in future reviews (new rules under Algorithmic Correctness and
Defensive Coding at Boundaries: don't weaken ApplicationStateGuard
.Unreachable without evidence; don't log internal-bug states as warnings;
defensive coding belongs at trust boundaries, not on internal invariants
between trusted in-process components).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Thanks for the careful review — you're right on both points and I just pushed a commit that addresses them.

What I changed back (your feedback)

  • NamedPipeServer.cs: restored throw ApplicationStateGuard.Unreachable() for both currentReadBytes < sizeof(int) and missingBytesToReadOfWholeMessage < 0. Also dropped the added currentMessageSize < sizeof(int)LogWarning path. You're right that LogWarning is the wrong severity for an unexpected-state condition the user can't act on, and replacing a documented invariant with handling logic without evidence the condition is reachable is regressing the design that was carefully worked out with Marco.
  • NamedPipeClient.cs: same reasoning — dropped the symmetric short-read loop, the size bounds check, the overshoot check, and the try/catch around Deserialize that I had mirrored. None of them were driven by evidence and they were adding untested error paths.
  • Dropped the NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost regression test (it was only validating the behavior we just reverted).

What I kept

Only the parts where the exceptions are caused by an externally observable condition (the peer process crashing/exiting mid-write), not by an internal invariant:

  • NamedPipeServer.cs: try/catch (IOException or ObjectDisposedException) around the reply WriteAsync/FlushAsync/WaitForPipeDrain so a client that disconnects between request and reply doesn't FailFast the host.
  • NamedPipeClient.cs: symmetric try/catch on the request write side that routes through the same _environment.Exit(GenericFailure) path the existing read-EOF handler uses.

These match the existing read-EOF handling (currentReadBytes == 0 -> graceful return / Exit(GenericFailure)) and don't touch any invariant.

Expert reviewer

I also updated .github/agents/expert-reviewer.agent.md so the same pattern gets flagged in future reviews — added rules to Algorithmic Correctness and Defensive Coding at Boundaries:

  • Don't weaken ApplicationStateGuard.Unreachable() / Debug.Assert / invariant throws into ""graceful"" handling without a concrete repro.
  • An internal-bug condition is LogError + abort, never LogWarning (which is user-actionable severity).
  • Defensive coding belongs at trust boundaries, not on internal invariants between trusted in-process components.

All 965 Microsoft.Testing.Platform.UnitTests still pass on net9.0.

@Evangelink
Amaury Levé (Evangelink) merged commit ce14f31 into mainMay 27, 2026
24 of 26 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/ipc-disconnect-resilience branch May 27, 2026 11:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

IPC: handle disconnects and protocol corruption gracefully - #8602

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience
May 27, 2026
Merged

IPC: handle disconnects and protocol corruption gracefully#8602
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/ipc-disconnect-resilience

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Hardens the Microsoft.Testing.Platform IPC layer (named-pipe transport between the test host and its in-process clients) so that a peer disconnect or a corrupt/short header byte no longer takes down the host or client process.

Audit items addressed

Part of the P0+P1 exception-handling audit (items #4#10, IPC).

Changes

NamedPipeServer.cs

  • Replaced ApplicationStateGuard.Unreachable() throws in the header read path with graceful disconnect handling (tolerate short reads, treat mid-header EOF as a normal disconnect).
  • Added bounds check currentMessageSize <= 0 → log warning and return cleanly instead of crashing on a corrupt header.
  • Wrapped server-side WriteAsync/FlushAsync/WaitForPipeDrain in try/catch (IOException/ObjectDisposedException) → set clientDisconnected and exit the loop after resetting buffers, rather than tearing down the host.

NamedPipeClient.cs

  • Symmetric write-side hardening: catches the same IOException/ObjectDisposedException and routes through the existing _environment.Exit(GenericFailure) path used by the read-EOF handler.
  • Short-read tolerance on the response header; bounds check currentMessageSize <= 0 → exit on corruption.
  • Wrapped response Deserialize in try/catch (excluding OperationCanceledException) so protocol corruption exits cleanly instead of bubbling an undecorated deserialization exception.

Test

  • New regression test NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost sends a zero-byte size header via a raw NamedPipeClientStream and asserts the server stays alive instead of throwing ApplicationStateGuard.Unreachable.
  • All existing Microsoft.Testing.Platform.UnitTests IPC tests still pass on net9.0.

Notes

  • No public API changes.
  • Behavior matches the existing GenericFailure exit code used for the read-EOF path on the client side.
  • WaitConnectionAsync FailFast is intentionally preserved; only loop-body IO faults get graceful handling.

Replace ApplicationStateGuard.Unreachable() throws in IPC header/payload reads with graceful disconnect handling. Tolerate short reads, treat mid-header EOF as graceful disconnect, validate currentMessageSize > 0, and catch IOException/ObjectDisposedException during write/flush/drain so a peer disconnect cannot crash the host or client process.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 13:51

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the Microsoft.Testing.Platform named-pipe IPC transport so disconnects and certain protocol-corruption scenarios don’t crash the host/client process (replacing prior “unreachable” paths with graceful exits).

Changes:

  • Server: tolerate short/EOF header reads, reject non-positive message sizes, and handle write-side disconnects without FailFast.
  • Client: add write-side disconnect handling, tolerate short response headers, and treat response deserialization failures as a generic IPC failure exit.
  • Tests: add a regression test to ensure an invalid (0) message-size header doesn’t crash the host.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.csAdds regression coverage for invalid message-size header handling on the server.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.csMakes the server loop tolerant to mid-header EOF/short reads and write-side disconnects; logs and exits cleanly on certain corrupt headers.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.csAdds write-side disconnect handling, short-read header handling, and exits cleanly on corruption/deserialization errors.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 4

Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeServer.cs Outdated
Comment threadsrc/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs Outdated
Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Test Coverage Gaps

The PR introduces 8 new error paths but only tests 1 (server-side zero message size). Missing test scenarios:

Client-side (4 untested paths):

  1. Write-side IOException: Server closes pipe during WriteAsync → verify _environment.Exit(GenericFailure) called
  2. Mid-header disconnect: Server sends 2 bytes then closes → verify _environment.Exit and IOException thrown
  3. Negative message size: Server sends -1 header → verify _environment.Exit and InvalidOperationException thrown
  4. Deserialization failure: Server sends corrupted payload → verify _environment.Exit called

Server-side (3 untested paths):

  1. Mid-header disconnect: Client sends 2 bytes then closes → verify graceful loop exit
  2. Negative message size: Client sends -1 header → verify warning logged and loop exits (existing test only covers zero, not negative)
  3. Byte-count overflow: Client declares N bytes but writes >N bytes → verify warning logged and loop exits
  4. Write-side IOException: Client disconnects during server reply → verify graceful loop exit

All 8 scenarios are concrete failing interleavings that the new code explicitly handles. Recommend adding these tests to prevent regressions.

Generated by Expert Code Review (on open) for issue #8602 · ● 4M ·

// If currentRequestSize is 0, we need to read the message size
if (currentMessageSize == 0)
{
// We need at least sizeof(int) bytes to parse the message-size header. A pipe read can

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is adding a lot of additional code. Is there a concrete bug that makes it worth adding this?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(Reading this as a question rather than a request to revert — happy to do either.)

No specific user-visible repro for the write-side hardening, but it's there to keep the client symmetric with the existing read-EOF path right below it: today, if the server disconnects mid-write the client surfaces a raw IOException/ObjectDisposedException to whoever called RequestReplyAsync (e.g. the test host orchestration code) instead of the deterministic _environment.Exit(GenericFailure) we already use for read-EOF. Two cases this matters in practice:

  1. dotnet test orchestration tears down the test host process mid-RPC — caller gets a raw IO exception instead of the documented exit code.
  2. The MTP host crashes/FailFasts on its end while the client is flushing — same thing.

The concrete server-side bug fixed in this PR (the one with the regression test) is the ApplicationStateGuard.Unreachable() -> FailFast crash on a corrupt/short header byte. The client write-side changes are essentially: 3 lines of catch (Exception ex) when (ex is IOException or ObjectDisposedException) doing what the read-EOF block 30 lines above already does.

If you'd rather I scope this PR down to just the server-side crash + the matching read-side tightening and pull the write-side symmetry into a separate PR (or just drop it), happy to do that — let me know.

- Tighten message-size validation on both server and client: payload must
contain at least a 4-byte serializer id, so reject sizes < sizeof(int)
(not just <= 0) as protocol corruption.
- Client: add the symmetric `missingBytesToReadOfWholeMessage < 0` guard
the server already has, so an over-long body exits cleanly instead of
hanging.
- Server: drop the useless `missingBytesToReadOfCurrentChunk = currentReadBytes;`
reassignment inside the header accumulation loop (recomputed at line 182).
- Test: use `TaskCompletionSource` for the callback signal and assert
after the server has been disposed, so the loop task has definitely
completed before we check that the callback did not run.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build failed due to a code style violation: a using statement can be simplified to a using declaration.

Root cause: IDE0063 style rule violation

The newly added test method NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost uses a traditional using statement with braces on line 261:

using(varraw=newSystem.IO.Pipes.NamedPipeClientStream(...)){// ...}

The project enforces [IDE0063]((learn.microsoft.com/redacted) as an error, which requires the simpler using declaration pattern:

usingvarraw=newSystem.IO.Pipes.NamedPipeClientStream(...);// ...

This single style violation is reported for each target framework the project builds (net8.0 and net9.0), resulting in multiple error instances from the same root cause.

Affected files / errors

Proposed fix

Convert the traditional using statement to a using declaration and adjust the code block accordingly:

 Task waitConnection = server.WaitConnectionAsync(_testContext.CancellationToken);
- using (var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous))- {- await raw.ConnectAsync(_testContext.CancellationToken);- await waitConnection;+ using var raw = new System.IO.Pipes.NamedPipeClientStream(".", pipeNameDescription.Name, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous);+ await raw.ConnectAsync(_testContext.CancellationToken);+ await waitConnection;- // Write a zero-length message size header (invalid per protocol).- byte[] invalidHeader = BitConverter.GetBytes(0);- await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);- await raw.FlushAsync(_testContext.CancellationToken);- }+ // Write a zero-length message size header (invalid per protocol).+ byte[] invalidHeader = BitConverter.GetBytes(0);+ await raw.WriteAsync(invalidHeader, 0, invalidHeader.Length, _testContext.CancellationToken);+ await raw.FlushAsync(_testContext.CancellationToken);

Build overview

Configuration: Debug
Exit code: 1 (failure)
Error count: 2 (4 instances across target frameworks)
Warning count: 0

Failed project:

  • Microsoft.Testing.Platform.UnitTests.csproj (net8.0, net9.0)
All MSBuild errors (2)
CodeProjectFile:LineMessage
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net8.0)
IDE0063Microsoft.Testing.Platform.UnitTestsIPCTests.cs:261'using' statement can be simplified (net9.0)

🤖 Generated by the Build Failure Analysis workflow · commit 60797b2

Generated by Build Failure Analysis for issue #8602 · ● 2.2M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8602 · ● 2.2M

Comment threadtest/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/IPCTests.cs Outdated
CopilotAI review requested due to automatic review settings May 27, 2026 09:00

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new

Per review feedback, the carefully-analyzed invariants in the IPC read
loop (Unreachable() guards on short header reads and overshoot of the
declared message size) should not be weakened into 'graceful' handlers
without evidence the conditions can occur. Replacing them with
LogWarning is also wrong: those are internal-bug states, not
user-actionable warnings.
Reverted from the original PR:
- NamedPipeServer: restore 'currentReadBytes < sizeof(int)' Unreachable
- NamedPipeServer: restore 'missingBytesToReadOfWholeMessage < 0' Unreachable
- NamedPipeServer: drop the added 'currentMessageSize < sizeof(int)' warning
- NamedPipeServer: restore the existing client-disconnect LogDebugAsync and
the Dispose/DisposeAsync LogError diagnostics
- NamedPipeClient: drop the symmetric (untested) header short-read loop,
the 'currentMessageSize < sizeof(int)' check, the
'missingBytesToReadOfWholeMessage < 0' check, and the try/catch around
Deserialize
- IPCTests: drop the NamedPipeServer_InvalidMessageSizeHeader regression
test (no longer applicable now that the invariant throw is restored)
Kept (legitimate runtime conditions caused by a real peer disconnect):
- NamedPipeServer: try/catch on WriteAsync/FlushAsync/WaitForPipeDrain
for IOException/ObjectDisposedException -> exit the loop cleanly
- NamedPipeClient: symmetric try/catch on the write side ->
_environment.Exit(GenericFailure)
Also strengthen .github/agents/expert-reviewer.agent.md to flag this
pattern in future reviews (new rules under Algorithmic Correctness and
Defensive Coding at Boundaries: don't weaken ApplicationStateGuard
.Unreachable without evidence; don't log internal-bug states as warnings;
defensive coding belongs at trust boundaries, not on internal invariants
between trusted in-process components).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Thanks for the careful review — you're right on both points and I just pushed a commit that addresses them.

What I changed back (your feedback)

  • NamedPipeServer.cs: restored throw ApplicationStateGuard.Unreachable() for both currentReadBytes < sizeof(int) and missingBytesToReadOfWholeMessage < 0. Also dropped the added currentMessageSize < sizeof(int)LogWarning path. You're right that LogWarning is the wrong severity for an unexpected-state condition the user can't act on, and replacing a documented invariant with handling logic without evidence the condition is reachable is regressing the design that was carefully worked out with Marco.
  • NamedPipeClient.cs: same reasoning — dropped the symmetric short-read loop, the size bounds check, the overshoot check, and the try/catch around Deserialize that I had mirrored. None of them were driven by evidence and they were adding untested error paths.
  • Dropped the NamedPipeServer_InvalidMessageSizeHeader_DoesNotCrashHost regression test (it was only validating the behavior we just reverted).

What I kept

Only the parts where the exceptions are caused by an externally observable condition (the peer process crashing/exiting mid-write), not by an internal invariant:

  • NamedPipeServer.cs: try/catch (IOException or ObjectDisposedException) around the reply WriteAsync/FlushAsync/WaitForPipeDrain so a client that disconnects between request and reply doesn't FailFast the host.
  • NamedPipeClient.cs: symmetric try/catch on the request write side that routes through the same _environment.Exit(GenericFailure) path the existing read-EOF handler uses.

These match the existing read-EOF handling (currentReadBytes == 0 -> graceful return / Exit(GenericFailure)) and don't touch any invariant.

Expert reviewer

I also updated .github/agents/expert-reviewer.agent.md so the same pattern gets flagged in future reviews — added rules to Algorithmic Correctness and Defensive Coding at Boundaries:

  • Don't weaken ApplicationStateGuard.Unreachable() / Debug.Assert / invariant throws into ""graceful"" handling without a concrete repro.
  • An internal-bug condition is LogError + abort, never LogWarning (which is user-actionable severity).
  • Defensive coding belongs at trust boundaries, not on internal invariants between trusted in-process components.

All 965 Microsoft.Testing.Platform.UnitTests still pass on net9.0.

@Evangelink
Amaury Levé (Evangelink) merged commit ce14f31 into mainMay 27, 2026
24 of 26 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/ipc-disconnect-resilience branch May 27, 2026 11:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@Youssef1313