Skip to content

Fix macOS symlink handshake mismatch in .NET task host (MSB4216)#13406

Merged
YuliiaKovalova merged 14 commits intomainfrom
fix/macos-taskhost-symlink-handshake
Mar 19, 2026
Merged

Fix macOS symlink handshake mismatch in .NET task host (MSB4216)#13406
YuliiaKovalova merged 14 commits intomainfrom
fix/macos-taskhost-symlink-handshake

Conversation

@YuliiaKovalova
Copy link
Copy Markdown
Member

@YuliiaKovalova YuliiaKovalova commented Mar 18, 2026

Context

PR #13175 (App Host Support) introduced a regression on macOS when the SDK is accessed through a symlinked path. On macOS, /tmp is a symlink to /private/tmp. When Helix tests run from /tmp/helix/..., the $(NetCoreSdkRoot) MSBuild property preserves the unresolved path /tmp/helix/.../sdk/11.0.100-ci, but the child task host process resolves it to /private/tmp/helix/.../sdk/11.0.100-ci via AppContext.BaseDirectory.

Root Cause

In ResolveAppHostOrFallback, the parent passed toolsDirectory: msbuildAssemblyPath (from $(NetCoreSdkRoot)) to the Handshake constructor, while the child (NodeEndpointOutOfProcTaskHost) passed no explicit toolsDirectory, defaulting to BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryRoot (which resolves symlinks).

This produced different handshake hashes:

  • Parent: hash("/tmp/.../sdk/11.0.100-ci")
  • Child: hash("/private/tmp/.../sdk/11.0.100-ci")
  • Result: Handshake mismatch -> MSB4216

Before PR #13175, neither side passed explicit toolsDirectory, so both defaulted to BuildEnvironmentHelper and always matched.

Changes Made

  • On .NET Core (#if RUNTIME_TYPE_NETCORE): omit explicit toolsDirectory so both parent and child default to BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryRoot, which resolves symlinks consistently via AppContext.BaseDirectory.
  • On .NET Framework: keep toolsDirectory: msbuildAssemblyPath because the parent (VS) and child (.NET task host) are in different directories, and Windows has no symlink issues.

Testing

  • Handshake_ExternalPathCanMismatch_DefaultAlwaysMatches - proves that an external path (like $(NetCoreSdkRoot)) produces a different handshake than the default, and that omitting toolsDirectory on both sides always matches.
  • Handshake_WithSymlinkedToolsDirectory_ProducesDifferentKey - proves the bug mechanism with real symlinks on Unix.
  • Existing E2E tests for TaskHostFactory tasks.
  • SDK test validation on Helix macOS (the original failing environment).

Notes

This fix addresses the MSB4216 errors seen in SDK Helix tests for ComputeWasmBuildAssets, ComputeManagedAssemblies, MarshalingPInvokeScanner, and other TaskHostFactory tasks on macOS.

YuliiaKovalova and others added 8 commits March 18, 2026 10:47
…B4216

On macOS, /tmp is a symlink to /private/tmp. PR #13175 changed
ResolveAppHostOrFallback to pass an explicit toolsDirectory to the
parent's Handshake constructor, sourced from the MSBuild property
(NetCoreSdkRoot = MSBuildThisFileDirectory). The child task host
process computes its toolsDirectory from AppContext.BaseDirectory
via MSBuildToolsDirectoryRoot.

On macOS, MSBuild properties preserve the unresolved /tmp form while
AppContext.BaseDirectory resolves to /private/tmp. The different
strings produce different hashes in the handshake salt, resulting in
mismatched pipe names — the parent listens on one pipe, the child
connects to another — causing MSB4216.

Before #13175, both parent and child used MSBuildToolsDirectoryRoot
as the default (no explicit toolsDirectory), so both resolved
symlinks identically and the handshake matched.

The fix uses BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryRoot
for the handshake toolsDirectory on the parent side — the same source
the child uses — ensuring path strings match regardless of symlinks.
File I/O operations continue to use the original msbuildAssemblyPath
(both path forms work for filesystem access on macOS).

Fixes dotnet/sdk#53350 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Regression test for macOS /tmp -> /private/tmp symlink issue.
Verifies that ResolveAppHostOrFallback uses the same toolsDirectory
source (MSBuildToolsDirectoryRoot) as the child process, ensuring
the handshake hash and pipe name match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Creates a real symlink on Unix and proves that using the symlink path
vs the resolved real path in the Handshake constructor produces
different keys. This is the exact bug on macOS where /tmp -> /private/tmp:
the parent used the unresolved MSBuild property path while the child
used the resolved AppContext.BaseDirectory path.

The test also verifies the fix: using MSBuildToolsDirectoryRoot on
both sides always produces matching keys.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Temporarily revert the handshake fix to confirm that:
1. Handshake_WithSymlinkedToolsDirectory_ProducesDifferentKey passes
   (proving symlink paths produce different keys)
2. ResolveAppHostOrFallback_HandshakeMatchesChildHandshake passes
   (this uses MSBuildToolsDirectoryRoot on both sides, so it passes
   regardless — it validates the fix approach, not the bug)

If the symlink test passes on macOS CI, it confirms that the
/tmp -> /private/tmp symlink causes handshake key mismatches,
proving the root cause of the MSB4216 failures.

The fix should be re-applied after validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TaskHostParameters is in the Microsoft.Build.Framework namespace.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Directory.CreateSymbolicLink is only available in .NET 7+.
The test is already [UnixOnlyFact] so it only runs on macOS/Linux
.NET Core builds. The #if NET guard prevents compilation errors
on .NET Framework (Windows Full/Core) builds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Creates a symlink to the bootstrap SDK directory and launches the
MSBuild apphost through it. This reproduces the macOS /tmp -> /private/tmp
regression where the parent handshake uses the unresolved symlink path
from MSBuild properties while the child resolves to the real path via
AppContext.BaseDirectory -> handshake mismatch -> MSB4216.

Without the fix (reverted in this PR), this test should FAIL on macOS CI
with MSB4216, proving the symlink is the root cause.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add using System.Collections.Generic for Dictionary<string, string>
- Remove custom message from ShouldNotContain (Shouldly string overload
  interprets second arg as customMessage differently)
- Add explicit shellExecute parameter

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
YuliiaKovalova and others added 2 commits March 19, 2026 07:41
Remove explicit toolsDirectory from Handshake in ResolveAppHostOrFallback.

Root cause: On macOS, /tmp is a symlink to /private/tmp. The parent
process passed toolsDirectory from $(NetCoreSdkRoot) MSBuild property
(which preserves the unresolved symlink path /tmp/...), while the
child task host computed its toolsDirectory from
BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryRoot (derived from
AppContext.BaseDirectory, which resolves symlinks to /private/tmp/...).
This produced different handshake hashes, causing the parent to fail to
connect to the child task host process with MSB4216.

Fix: Don't pass explicit toolsDirectory to the Handshake constructor.
Both parent and child now default to BuildEnvironmentHelper, which
consistently resolves symlinks via AppContext.BaseDirectory.

Before PR #13175, neither side passed explicit toolsDirectory, so both
defaulted to BuildEnvironmentHelper and always matched. PR #13175
introduced the asymmetry by passing  on the parent side.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mit on .NET Core

On .NET Framework (VS), parent and child MSBuild are in different
directories (VS dir vs SDK dir), so explicit toolsDirectory is needed.
Windows has no symlink issues.

On .NET Core, parent and child are always from the same SDK, so both
can safely default to BuildEnvironmentHelper (which resolves symlinks
via AppContext.BaseDirectory).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@YuliiaKovalova YuliiaKovalova changed the title Fix/macos taskhost symlink handshake Fix macos taskhost symlink handshake Mar 19, 2026
The previous test was tautological - it passed BuildEnvironmentHelper
as explicit toolsDirectory, which is the same as the default. It would
pass with or without the fix.

The updated test proves two things:
1. Both sides omitting toolsDirectory produces matching keys (the fix)
2. An external path (like NetCoreSdkRoot) produces a DIFFERENT key
   than the default (the bug mechanism)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@YuliiaKovalova YuliiaKovalova changed the title Fix macos taskhost symlink handshake Fix macOS symlink handshake mismatch in .NET task host (MSB4216) Mar 19, 2026
@YuliiaKovalova YuliiaKovalova marked this pull request as ready for review March 19, 2026 07:07
Copilot AI review requested due to automatic review settings March 19, 2026 07:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a macOS regression in the .NET task host launch path where symlink-resolved vs unresolved SDK directories could cause parent/child handshake salt mismatches (MSB4216), preventing the parent from connecting to the task host.

Changes:

  • On RUNTIME_TYPE_NETCORE, stop passing an explicit toolsDirectory to Handshake so both parent and child default to BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryRoot (symlink-resolved).
  • Keep explicit toolsDirectory: msbuildAssemblyPath on .NET Framework to preserve the required VS-parent/SDK-child directory separation behavior.
  • Add/adjust regression tests covering handshake mismatches and a symlinked SDK execution scenario.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs Adjusts handshake construction to avoid symlink-induced mismatches on .NET Core while preserving .NET Framework behavior.
src/Build.UnitTests/NetTaskHost_E2E_Tests.cs Adds an E2E Unix regression test that runs MSBuild via a symlinked SDK path and asserts MSB4216 does not occur.
src/Build.UnitTests/BackEnd/AppHostSupport_Tests.cs Adds unit-level handshake regression coverage around external toolsDirectory values and real symlink vs real-path key mismatches.

YuliiaKovalova and others added 2 commits March 19, 2026 08:12
…host

- Handshake_ExternalPathCanMismatch_DefaultAlwaysMatches: guard with
  #if NET and use explicit NET runtime TaskHostParameters so the NET
  HandshakeOptions flag is set, avoiding VerifyThrow on .NET Framework.
- NetTaskHost_SymlinkedSdkPath: replace silent return with Assert.Fail
  when apphost is missing, so the test doesn't silently pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GetHandshakeOptions requires non-null architecture when taskHostParameters
has a non-null runtime. Use GetCurrentMSBuildArchitecture().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Member

@ViktorHofer ViktorHofer left a comment

Choose a reason for hiding this comment

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

Nice find!

@YuliiaKovalova YuliiaKovalova merged commit 673ef31 into main Mar 19, 2026
10 checks passed
@YuliiaKovalova YuliiaKovalova deleted the fix/macos-taskhost-symlink-handshake branch March 19, 2026 08:34
AR-May pushed a commit to AR-May/msbuild that referenced this pull request Mar 19, 2026
…net#13406)

### Context

PR dotnet#13175 (App Host Support) introduced a regression on macOS when the
SDK is accessed through a symlinked path. On macOS, `/tmp` is a symlink
to `/private/tmp`. When Helix tests run from `/tmp/helix/...`, the
`$(NetCoreSdkRoot)` MSBuild property preserves the unresolved path
`/tmp/helix/.../sdk/11.0.100-ci`, but the child task host process
resolves it to `/private/tmp/helix/.../sdk/11.0.100-ci` via
`AppContext.BaseDirectory`.

### Root Cause

In `ResolveAppHostOrFallback`, the parent passed `toolsDirectory:
msbuildAssemblyPath` (from `$(NetCoreSdkRoot)`) to the `Handshake`
constructor, while the child (`NodeEndpointOutOfProcTaskHost`) passed no
explicit `toolsDirectory`, defaulting to
`BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryRoot` (which
resolves symlinks).

This produced different handshake hashes:
- **Parent**: `hash("/tmp/.../sdk/11.0.100-ci")`
- **Child**: `hash("/private/tmp/.../sdk/11.0.100-ci")`
- **Result**: Handshake mismatch -> MSB4216

Before PR dotnet#13175, neither side passed explicit `toolsDirectory`, so both
defaulted to `BuildEnvironmentHelper` and always matched.

### Changes Made

- On .NET Core (`#if RUNTIME_TYPE_NETCORE`): omit explicit
`toolsDirectory` so both parent and child default to
`BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryRoot`, which
resolves symlinks consistently via `AppContext.BaseDirectory`.
- On .NET Framework: keep `toolsDirectory: msbuildAssemblyPath` because
the parent (VS) and child (.NET task host) are in **different
directories**, and Windows has no symlink issues.
- Updated regression test to validate actual fix behavior (not
tautological).

### Testing

- `Handshake_ExternalPathCanMismatch_DefaultAlwaysMatches` - proves that
an external path (like `$(NetCoreSdkRoot)`) produces a different
handshake than the default, and that omitting `toolsDirectory` on both
sides always matches.
- `Handshake_WithSymlinkedToolsDirectory_ProducesDifferentKey` - proves
the bug mechanism with real symlinks on Unix.
- Existing E2E tests for TaskHostFactory tasks.
- SDK test validation on Helix macOS (the original failing environment).

### Notes

This fix addresses the MSB4216 errors seen in SDK Helix tests for
`ComputeWasmBuildAssets`, `ComputeManagedAssemblies`,
`MarshalingPInvokeScanner`, and other `TaskHostFactory` tasks on macOS.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to 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