Skip to content

Commit cc8eddb

Browse files
ViktorHoferCopilot
andcommitted
Normalize tools directory casing in task host handshake salt
The .NET task host handshake salt is a case-sensitive hash of a filesystem path. The .NET Framework host passes the SDK directory ($(NetCoreSdkRoot)) as the tools directory while the child task host re-resolves it from its own process location. On Windows these can differ only by drive-letter casing (e.g. "D:\..." vs "d:\..."), which produced different salts and caused the otherwise-valid handshake to fail with MSB4216 (seen with VS 18.6 launching the .NET 11 SDK task host on hosted CI agents). Normalize the tools directory casing on Windows before hashing so both sides compute the same salt. The CLR2 MSBuildTaskHost (Windows-only) is normalized unconditionally to match. Fixes #14026 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent bdbdde0 commit cc8eddb

3 files changed

Lines changed: 52 additions & 2 deletions

File tree

src/Build.UnitTests/BackEnd/AppHostSupport_Tests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,5 +308,43 @@ public void ResolveNetTaskHostLaunchPath_FallsBackToMSBuildDll_WhenAppHostMissin
308308
useAppHost.ShouldBeFalse();
309309
launchPath.ShouldBe(msbuildDllPath);
310310
}
311+
312+
/// <summary>
313+
/// Regression test for MSB4216 caused by drive-letter casing differences (e.g. on
314+
/// hosted CI agents the SDK-resolved path is "D:\..." while the child .NET task host
315+
/// resolves its own location as "d:\...").
316+
///
317+
/// On the .NET Framework → .NET task host path the parent passes the SDK directory
318+
/// ($(NetCoreSdkRoot)) as toolsDirectory while the child re-resolves it from its own
319+
/// process location. These come from independent sources, so on Windows they can differ
320+
/// only in casing. Because Windows paths are case-insensitive, the case-sensitive salt
321+
/// hash must normalize casing so both sides still produce the same handshake key.
322+
/// </summary>
323+
[WindowsOnlyFact]
324+
public void Handshake_ToolsDirectoryCasingDoesNotAffectKey()
325+
{
326+
// Use explicit NET runtime and current architecture so the NET HandshakeOptions
327+
// flag is set, which is required for passing toolsDirectory to the constructor.
328+
var netTaskHostParams = new TaskHostParameters(
329+
runtime: XMakeAttributes.MSBuildRuntimeValues.net,
330+
architecture: XMakeAttributes.GetCurrentMSBuildArchitecture(),
331+
dotnetHostPath: null,
332+
msBuildAssemblyPath: null);
333+
334+
HandshakeOptions options = CommunicationsUtilities.GetHandshakeOptions(
335+
taskHost: true,
336+
taskHostParameters: netTaskHostParams,
337+
nodeReuse: false);
338+
339+
const string upperCase = @"D:\a\_work\1\s\.dotnet\sdk\11.0.100-preview.5.26227.104";
340+
const string lowerCase = @"d:\a\_work\1\s\.dotnet\sdk\11.0.100-preview.5.26227.104";
341+
342+
var upperHandshake = new Handshake(options, upperCase);
343+
var lowerHandshake = new Handshake(options, lowerCase);
344+
345+
upperHandshake.GetKey().ShouldBe(lowerHandshake.GetKey(),
346+
"On Windows, paths differing only by casing must produce identical handshake keys; " +
347+
"otherwise a .NET Framework host and a .NET task host can fail to connect with MSB4216.");
348+
}
311349
}
312350
}

src/Framework/BackEnd/Handshake.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,14 @@ protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string? to
8080
// Calculate salt from environment and tools directory
8181
string handshakeSalt = Environment.GetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT") ?? "";
8282

83-
int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{toolsDirectory}");
83+
// The tools directory can be derived differently by the host and the child node (e.g. the host
84+
// passes an explicit path while the child resolves it from its own process location). On Windows
85+
// these may differ only in casing (notably the drive letter), and because the salt hash is
86+
// case-sensitive that would cause an otherwise valid handshake to fail with MSB4216. Normalize
87+
// the casing on Windows, where paths are case-insensitive, so both sides compute the same salt.
88+
string normalizedToolsDirectory = NativeMethods.IsWindows ? toolsDirectory.ToUpperInvariant() : toolsDirectory;
89+
90+
int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{normalizedToolsDirectory}");
8491

8592
CommunicationsUtilities.Trace($"Handshake salt is {handshakeSalt}");
8693
CommunicationsUtilities.Trace($"Tools directory root is {toolsDirectory}");

src/MSBuildTaskHost/CommunicationsUtilities.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,12 @@ public Handshake(HandshakeOptions nodeType)
209209

210210
// Calculate salt from environment and tools directory
211211
string handshakeSalt = Environment.GetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT") ?? "";
212-
int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{toolsDirectory}");
212+
213+
// Normalize casing so the salt matches the host, which may derive the same directory with
214+
// different casing (e.g. the drive letter). MSBuildTaskHost only runs on Windows, where paths
215+
// are case-insensitive, so the case-sensitive salt hash must not depend on casing. See the
216+
// matching logic in Microsoft.Build.Internal.Handshake.
217+
int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{toolsDirectory.ToUpperInvariant()}");
213218

214219
CommunicationsUtilities.Trace($"Handshake salt is {handshakeSalt}");
215220
CommunicationsUtilities.Trace($"Tools directory root is {toolsDirectory}");

0 commit comments

Comments
 (0)