dotnet / dotnet/msbuild

MSB4216: .NET task host handshake fails due to case-sensitive drive-letter casing in salt (VS 18.6 + .NET 11 SDK)

Open
#14,026 4 comments 0 reactions 0 assignees View on GitHub
bug Priority:1 triaged
Dominant language
C#
Stars
5.5k
Forks
1.5k
Avg merge
1d 8h
Merged PRs (30d)
141

Description

## Summary

The .NET task host fails to connect with **MSB4216** when launched from a .NET Framework MSBuild host (e.g. VS) on hosted CI agents, because the handshake **salt is a case-sensitive hash of a filesystem path** and the two sides derive that path from independent sources whose drive-letter casing can differ.

```
InstallDotNetCore.targets(17,5): error MSB4216: Could not run the "InstallDotNetCore" task
because MSBuild could not create or connect to a task host with runtime "NET" and architecture "x86".
...
Handshake failed. Received -1569896665 from host for Salt but expected -930293759.
Probably the host is a different MSBuild build.
```

Observed in an internal build: VS MSBuild `18.6.3+84d3e95b4` (host) launching the .NET 11 SDK `11.0.100-preview.5.26227.104` task host.

## Root cause

The handshake salt is computed as a **case-sensitive** hash:

```
salt = GetHashCode($"{MSBUILDNODEHANDSHAKESALT}{toolsDirectory}")
```

The two sides derive `toolsDirectory` differently:

| Side | Source | Value in failing build |
|------|--------|------------------------|
| Host (`NodeProviderOutOfProcTaskHost.ResolveAppHostOrFallback`, `#else` branch) | `$(NetCoreSdkRoot)` via `taskHostParameters.MSBuildAssemblyPath` | `D:\a\_work\1\s\.dotnet\sdk\11.0.100-preview.5.26227.104` |
| Child (`NodeEndpointOutOfProcTaskHost.GetHandshake` -> `new Handshake(options)`) | `BuildEnvironmentHelper.MSBuildToolsDirectoryRoot` (self-resolved) | `d:\a\_work\1\s\.dotnet\sdk\11.0.100-preview.5.26227.104` |

Computing the hash of both strings confirms the mismatch exactly matches the on-the-wire values:

- `D:\...26227.104` -> `-1569896665` (host wrote)
- `d:\...26227.104` -> `-930293759` (child expected)

The "VS bin vs SDK" tools-directory lines that appear in the diagnostic logs are a red herring from an unrelated handshake context; the actual mismatch is purely **drive-letter casing of the same SDK path**.

## Why it surfaced now / "how could it have ever worked"

This is a latent fragility rather than a single regressing commit. The host-side salt logic in VS 18.6.3 is identical to `main`. The cross-install path-in-salt was introduced incrementally:

- **#11543** (Aug 2025) - first derived the NET task host SDK path from a project property (`RuntimeIdentifierGraphPath`).
- **#13176** (Feb 2026) - switched the source to `$(NetCoreSdkRoot)` (exposed by dotnet/sdk `e0c4dda`, new in **.NET 11**). Both old and new resolve to `$(MSBuildThisFileDirectory)`, so this did not itself change the casing source.
- **#13175** (Mar 2026) - App Host support wired `new Handshake(hostContext, toolsDirectory: msbuildAssemblyPath)` for the Framework -> .NET cross-install case, feeding the externally-sourced path into the salt.

It "worked" only while the host-provided path and the child's self-resolved path happened to share casing. The trigger is the **VS 18.6.x + .NET 11 SDK** combination on hosted agents, where the SDK-resolver path is spelled `D:\` but the child resolves `d:\`. It is environment/SDK-driven, not a product behavior change.

## Fix

Normalize path casing on Windows before hashing, on both handshake implementations (the host and the .NET child both use `src/Framework/BackEnd/Handshake.cs`; the CLR2 host uses `src/MSBuildTaskHost/CommunicationsUtilities.cs`):

- `Handshake.cs`: `toolsDirectory.ToUpperInvariant()` when `NativeMethods.IsWindows`.
- `MSBuildTaskHost/CommunicationsUtilities.cs`: unconditional `ToUpperInvariant()` (Windows-only assembly).

### Caveat

Because the NET task host handshake uses sentinel version `99.99.99.99`, the host and child can legitimately be **different MSBuild versions**. Both sides must carry the normalization for the salts to agree, so the fix only fully takes effect once both a VS MSBuild build and the SDK MSBuild include it.

## Affected files

- `src/Framework/BackEnd/Handshake.cs`
- `src/MSBuildTaskHost/CommunicationsUtilities.cs`
- `src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs` (context)
- `src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs` (context)

## Why this is NOT the VS-path-vs-SDK-path (toolset directory) difference

It is tempting to read the diagnostic log line `Tools directory root is c:\program files\...\visual studio\...\bin` and conclude the host hashed its own VS tools directory while the child hashed the SDK directory. That is not what happens here:

- For the Framework -> .NET cross-install task host, the host **deliberately does not use its own tools directory**. `ResolveAppHostOrFallback` passes `toolsDirectory: msbuildAssemblyPath` (the resolved **SDK** path) exactly so the salt matches the SDK child. The VS `...\MSBuild\Current\Bin` path is therefore **never fed into this handshake's salt**.
- The `c:\program files\...\visual studio\...\bin` "Tools directory root" line in the logs comes from a **different, unrelated handshake context** (e.g. a regular worker-node handshake), not the .NET task host handshake that produced this MSB4216.
- Decisive evidence: **both** salt values on the wire decode to the **same SDK path**, differing only by drive-letter casing:
- `D:\...\sdk\11.0.100-preview.5.26227.104` -> `-1569896665` (host wrote)
- `d:\...\sdk\11.0.100-preview.5.26227.104` -> `-930293759` (child expected)

Neither value corresponds to any VS path. If the VS tools directory had actually entered the salt, the host value would hash `c:\program files\...\visual studio\...\bin`, which produces a completely different number (`-617072364` / `-199492840`). It does not.

So the bug is purely the **case-sensitive hashing of the SDK path** (drive-letter casing), independent of the VS-vs-SDK install location difference, which is already handled correctly.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.