Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/Build.UnitTests/BackEnd/AppHostSupport_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,5 +308,43 @@ public void ResolveNetTaskHostLaunchPath_FallsBackToMSBuildDll_WhenAppHostMissin
useAppHost.ShouldBeFalse();
launchPath.ShouldBe(msbuildDllPath);
}

/// <summary>
/// Regression test for MSB4216 caused by drive-letter casing differences (e.g. on
/// hosted CI agents the SDK-resolved path is "D:\..." while the child .NET task host
/// resolves its own location as "d:\...").
///
/// On the .NET Framework → .NET task host path the parent passes the SDK directory
/// ($(NetCoreSdkRoot)) as toolsDirectory while the child re-resolves it from its own
/// process location. These come from independent sources, so on Windows they can differ
/// only in casing. Because Windows paths are case-insensitive, the case-sensitive salt
/// hash must normalize casing so both sides still produce the same handshake key.
/// </summary>
[WindowsOnlyFact]
public void Handshake_ToolsDirectoryCasingDoesNotAffectKey()
{
// Use explicit NET runtime and current architecture so the NET HandshakeOptions
// flag is set, which is required for passing toolsDirectory to the constructor.
var netTaskHostParams = new TaskHostParameters(
runtime: XMakeAttributes.MSBuildRuntimeValues.net,
architecture: XMakeAttributes.GetCurrentMSBuildArchitecture(),
dotnetHostPath: null,
msBuildAssemblyPath: null);

HandshakeOptions options = CommunicationsUtilities.GetHandshakeOptions(
taskHost: true,
taskHostParameters: netTaskHostParams,
nodeReuse: false);

const string upperCase = @"D:\a\_work\1\s\.dotnet\sdk\11.0.100-preview.5.26227.104";
const string lowerCase = @"d:\a\_work\1\s\.dotnet\sdk\11.0.100-preview.5.26227.104";

var upperHandshake = new Handshake(options, upperCase);
var lowerHandshake = new Handshake(options, lowerCase);

upperHandshake.GetKey().ShouldBe(lowerHandshake.GetKey(),
"On Windows, paths differing only by casing must produce identical handshake keys; " +
"otherwise a .NET Framework host and a .NET task host can fail to connect with MSB4216.");
}
}
}
15 changes: 13 additions & 2 deletions src/Framework/BackEnd/Handshake.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,21 @@ protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string? to
// Calculate salt from environment and tools directory
string handshakeSalt = Environment.GetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT") ?? "";

int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{toolsDirectory}");
// For the .NET task host the tools directory is derived differently by the host and the child:
// the .NET Framework host passes the SDK directory ($(NetCoreSdkRoot)) as an explicit string,
// while the child re-resolves it from its own process location. On Windows these may differ only
// in casing (notably the drive letter), and because the salt hash is case-sensitive that would
// cause an otherwise valid handshake to fail with MSB4216. Normalize the casing on Windows, where
// paths are case-insensitive, so both sides compute the same salt. Other node types resolve the
// tools directory the same way on both ends, so their salt is intentionally left unchanged.
string normalizedToolsDirectory = NativeMethods.IsWindows && IsNetTaskHost
? toolsDirectory.ToUpperInvariant()
: toolsDirectory;

int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{normalizedToolsDirectory}");

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

// Get session ID if needed (expensive call)
int sessionId = 0;
Expand Down
Loading