Skip to content

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

@ViktorHofer

Description

@ViktorHofer

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:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Priority:1Work that is critical for the release, but we could probably ship withoutbugtriaged

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions