diff --git a/src/Build.UnitTests/BackEnd/CommunicationsUtilities_Tests.cs b/src/Build.UnitTests/BackEnd/CommunicationsUtilities_Tests.cs new file mode 100644 index 00000000000..ddfd878bd28 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/CommunicationsUtilities_Tests.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Engine.UnitTests.BackEnd +{ + /// + /// Tests for covering the parent + /// side of NET task host launches: the parent must suppress its own architecture bit on + /// the wire so already-shipped SDK children (whose IsAllowedBitnessMismatch tolerates + /// "parent sent no arch bit") accept the connection regardless of either process arch. + /// + public sealed class CommunicationsUtilities_Tests + { + [Theory] + [InlineData(XMakeAttributes.MSBuildArchitectureValues.x64)] + [InlineData(XMakeAttributes.MSBuildArchitectureValues.arm64)] + [InlineData(XMakeAttributes.MSBuildArchitectureValues.x86)] + public void GetHandshakeOptions_NetTaskHostParent_SuppressesArchBit(string parentArchitecture) + { + var parameters = new TaskHostParameters( + runtime: XMakeAttributes.MSBuildRuntimeValues.net, + architecture: parentArchitecture); + + HandshakeOptions options = CommunicationsUtilities.GetHandshakeOptions( + taskHost: true, + taskHostParameters: parameters); + + options.HasFlag(HandshakeOptions.NET).ShouldBeTrue(); + options.HasFlag(HandshakeOptions.X64).ShouldBeFalse(); + options.HasFlag(HandshakeOptions.Arm64).ShouldBeFalse(); + } + + [Fact] + public void GetHandshakeOptions_NonNetTaskHostParent_KeepsX64ArchBit() + { + var parameters = new TaskHostParameters( + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.x64); + + HandshakeOptions options = CommunicationsUtilities.GetHandshakeOptions( + taskHost: true, + taskHostParameters: parameters); + + options.HasFlag(HandshakeOptions.X64).ShouldBeTrue(); + } + + [Fact] + public void GetHandshakeOptions_NonNetTaskHostParent_KeepsArm64ArchBit() + { + var parameters = new TaskHostParameters( + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.arm64); + + HandshakeOptions options = CommunicationsUtilities.GetHandshakeOptions( + taskHost: true, + taskHostParameters: parameters); + + options.HasFlag(HandshakeOptions.Arm64).ShouldBeTrue(); + } + + [Fact] + public void GetHandshakeOptions_NetTaskHostChild_KeepsArchBit() + { + // The child invokes GetHandshakeOptions with TaskHostParameters.Empty; the helper + // then derives architectureFlagToSet from GetCurrentMSBuildArchitecture(). The + // suppression must not apply: the child needs to keep its own arch bit so + // already-deployed parents that still emit one continue to match. + HandshakeOptions options = CommunicationsUtilities.GetHandshakeOptions( + taskHost: true, + taskHostParameters: TaskHostParameters.Empty); + + string currentArch = XMakeAttributes.GetCurrentMSBuildArchitecture(); + if (currentArch.Equals(XMakeAttributes.MSBuildArchitectureValues.x64, System.StringComparison.OrdinalIgnoreCase)) + { + options.HasFlag(HandshakeOptions.X64).ShouldBeTrue(); + } + else if (currentArch.Equals(XMakeAttributes.MSBuildArchitectureValues.arm64, System.StringComparison.OrdinalIgnoreCase)) + { + options.HasFlag(HandshakeOptions.Arm64).ShouldBeTrue(); + } + else + { + // x86 or unknown: no arch bit is expected on the child side either. + options.HasFlag(HandshakeOptions.X64).ShouldBeFalse(); + options.HasFlag(HandshakeOptions.Arm64).ShouldBeFalse(); + } + } + } +} diff --git a/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs b/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs new file mode 100644 index 00000000000..e0d3b465af1 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Internal; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Engine.UnitTests.BackEnd +{ + /// + /// Tests for , the .NET task + /// host child-side tolerance that lets a parent which did not emit an architecture bit + /// (e.g. a .NET Framework MSBuild) connect to an SDK child running on x64 or arm64. + /// + public sealed class NodeEndpointOutOfProcBase_Tests + { + private const HandshakeOptions BaseNet = HandshakeOptions.TaskHost | HandshakeOptions.NET; + + [Fact] + public void NoArchBitParent_X64Child_IsTolerated() + { + NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch( + expectedOptions: (int)(BaseNet | HandshakeOptions.X64), + receivedOptions: (int)BaseNet).ShouldBeTrue(); + } + + [Fact] + public void NoArchBitParent_Arm64Child_IsTolerated() + { + NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch( + expectedOptions: (int)(BaseNet | HandshakeOptions.Arm64), + receivedOptions: (int)BaseNet).ShouldBeTrue(); + } + + [Fact] + public void X64Parent_X64Child_NotConsideredMismatch() + { + // Equal handshakes never hit IsAllowedBitnessMismatch in production; verify it + // still returns false so the tolerance is scoped to the no-arch-bit parent only. + NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch( + expectedOptions: (int)(BaseNet | HandshakeOptions.X64), + receivedOptions: (int)(BaseNet | HandshakeOptions.X64)).ShouldBeFalse(); + } + + [Fact] + public void X64Parent_Arm64Child_NotTolerated() + { + // True architecture mismatch (parent sent X64, child expects Arm64) is rejected. + NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch( + expectedOptions: (int)(BaseNet | HandshakeOptions.Arm64), + receivedOptions: (int)(BaseNet | HandshakeOptions.X64)).ShouldBeFalse(); + } + + [Fact] + public void Arm64Parent_X64Child_NotTolerated() + { + NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch( + expectedOptions: (int)(BaseNet | HandshakeOptions.X64), + receivedOptions: (int)(BaseNet | HandshakeOptions.Arm64)).ShouldBeFalse(); + } + + [Fact] + public void NoArchBitParent_NoArchBitChild_NotTolerated() + { + // The tolerance is scoped to x64/arm64 children; an x86-equivalent (no arch bit) + // child must not silently accept any handshake. + NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch( + expectedOptions: (int)BaseNet, + receivedOptions: (int)BaseNet).ShouldBeFalse(); + } + } +} + +#endif diff --git a/src/Framework/BackEnd/CommunicationsUtilities.cs b/src/Framework/BackEnd/CommunicationsUtilities.cs index 195fbc08faf..d6c8dfba4f7 100644 --- a/src/Framework/BackEnd/CommunicationsUtilities.cs +++ b/src/Framework/BackEnd/CommunicationsUtilities.cs @@ -580,7 +580,25 @@ internal static HandshakeOptions GetHandshakeOptions( } } - if (!string.IsNullOrEmpty(architectureFlagToSet)) + // For the NET task host, the launched child process's architecture is determined by + // what the .NET SDK shipped (x64 today, arm64 in the future), not by the parent's + // process architecture. Emitting the parent's arch bit here creates a wire-level + // mismatch with already-shipped SDK children whose arch differs (e.g. arm64 VS + // launching an x64 SDK MSBuild.dll). Suppress the arch bit so the child's existing + // IsAllowedBitnessMismatch tolerance ("parent sent no arch bit") accepts the + // connection regardless of either side's process architecture. + // + // Only the parent suppresses — i.e. when GetHandshakeOptions is invoked with concrete + // taskHostParameters describing a specific runtime/architecture to launch. The child + // (which calls GetHandshakeOptions with TaskHostParameters.Empty) keeps its own arch + // bit so that already-deployed parents that still emit an arch bit continue to match + // (or fall through to IsAllowedBitnessMismatch's tolerance). + bool isNetTaskHostParent = + taskHost && + !taskHostParameters.IsEmpty && + XMakeAttributes.MSBuildRuntimeValues.net.Equals(taskHostParameters.Runtime, StringComparison.OrdinalIgnoreCase); + + if (!isNetTaskHostParent && !string.IsNullOrEmpty(architectureFlagToSet)) { if (architectureFlagToSet!.Equals(XMakeAttributes.MSBuildArchitectureValues.x64, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index 16828229734..032c6f1c64b 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -584,21 +584,31 @@ private bool IsHandshakePartValid(KeyValuePair component, int hands #if NET /// - /// NET Task host allows MSBuild.exe to connect to it even if they have bitness mismatch. - /// 0x00FFFFFF is the handshake version included in component, the rest is the node type. - /// - private bool IsAllowedBitnessMismatch(int expectedOptions, int receivedOptions) + /// The .NET task host child tolerates a parent that did not emit an architecture bit + /// on the wire (which is indistinguishable from x86) when the child itself is x64 or + /// arm64. This is the .NET Framework parent → .NET SDK child scenario: the parent + /// typically cannot describe the SDK child's architecture, but the launched task host + /// process is whatever the SDK ships (x64 on Windows-x64, arm64 on Windows-arm64). + /// + /// True cross-arch mismatches (e.g. parent sent X64 but child expects Arm64, or vice + /// versa) remain rejected. + /// + /// The lower 24 bits (mask 0x00FFFFFF) carry the HandshakeOptions flags; the upper + /// byte carries the handshake version and is ignored here. + /// + internal static bool IsAllowedBitnessMismatch(int expectedOptions, int receivedOptions) { var expectedNodeType = (HandshakeOptions)(expectedOptions & 0x00FFFFFF); var receivedNodeType = (HandshakeOptions)(receivedOptions & 0x00FFFFFF); - // not X64 or Arm64 means we are running on x86 + // not X64 or Arm64 means the wire-form architecture is x86 (or no arch bit). bool receivedIsX86 = !Handshake.IsHandshakeOptionEnabled(receivedNodeType, HandshakeOptions.X64) && !Handshake.IsHandshakeOptionEnabled(receivedNodeType, HandshakeOptions.Arm64); bool expectedIsX64 = Handshake.IsHandshakeOptionEnabled(expectedNodeType, HandshakeOptions.X64); + bool expectedIsArm64 = Handshake.IsHandshakeOptionEnabled(expectedNodeType, HandshakeOptions.Arm64); - return receivedIsX86 && expectedIsX64; + return receivedIsX86 && (expectedIsX64 || expectedIsArm64); } #endif