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