From 722e689a8df9c5c59d7518620a5869446c2ee1b3 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Thu, 28 May 2026 11:57:02 +0200
Subject: [PATCH 1/3] Tolerate Arm64 child in NET task host handshake bitness
check
IsAllowedBitnessMismatch is the .NET task host child-side relaxation that
lets a parent without an architecture bit on the wire (typically a .NET
Framework MSBuild) connect to an SDK child that runs on x64. On
Windows-ARM the SDK child runs as arm64 instead, but the function only
considered expectedIsX64, so any arm64 child rejected the connection ->
the launch times out and Runtime="NET" tasks fail with MSB4216.
Also tolerate expectedIsArm64. True cross-arch mismatches (parent X64 vs
child Arm64, or vice versa) remain rejected.
Make the method internal static so the test project can exercise the
tolerance matrix directly. The previous instance method did not touch any
instance state.
Partial fix for #13879 (Bug A).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../NodeEndpointOutOfProcBase_Tests.cs | 67 +++++++++++++++++++
src/Shared/NodeEndpointOutOfProcBase.cs | 17 +++--
2 files changed, 80 insertions(+), 4 deletions(-)
create mode 100644 src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs
diff --git a/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs b/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs
new file mode 100644
index 00000000000..245cad1a65b
--- /dev/null
+++ b/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs
@@ -0,0 +1,67 @@
+// 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();
+ }
+ }
+}
+
+#endif
diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs
index 16828229734..53633f6e63f 100644
--- a/src/Shared/NodeEndpointOutOfProcBase.cs
+++ b/src/Shared/NodeEndpointOutOfProcBase.cs
@@ -584,21 +584,30 @@ 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.
+ /// 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.
+ ///
/// 0x00FFFFFF is the handshake version included in component, the rest is the node type.
///
- private bool IsAllowedBitnessMismatch(int expectedOptions, int receivedOptions)
+ 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
From 5624f87952e5965b565f84e033cc6248eeb37f54 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Thu, 28 May 2026 13:39:58 +0200
Subject: [PATCH 2/3] Suppress arch bit on NET task host parent handshake
For NET task host launches the 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 creates a
wire-level mismatch with already-shipped SDK children whose arch differs
(e.g. arm64 VS launching an x64 SDK MSBuild.dll, or amd64 VS launching an
arm64 SDK MSBuild.dll). The existing child-side IsAllowedBitnessMismatch
tolerance only accepts "parent sent no arch bit", so this currently fails
with MSB4216.
In CommunicationsUtilities.GetHandshakeOptions, suppress the X64/Arm64 bit
when invoked by the parent for a NET task host (detected by Runtime="net"
in the explicit TaskHostParameters). The child path (TaskHostParameters
.Empty) is unaffected so already-deployed parents that still emit an arch
bit continue to match.
Combined with the child-side Arm64 tolerance in this PR, every parent/
child architecture combination now connects against every already-shipped
SDK that has the original (x64-only) tolerance.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../BackEnd/CommunicationsUtilities_Tests.cs | 95 +++++++++++++++++++
.../BackEnd/CommunicationsUtilities.cs | 20 +++-
2 files changed, 114 insertions(+), 1 deletion(-)
create mode 100644 src/Build.UnitTests/BackEnd/CommunicationsUtilities_Tests.cs
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/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))
{
From 1dce0bd627aee7b32f6ebbc199ca3d2d4c95005d Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Thu, 28 May 2026 13:46:59 +0200
Subject: [PATCH 3/3] Address review feedback
- Add NoArchBitParent_NoArchBitChild_NotTolerated test (catches a regression that would simplify the return to just receivedIsX86).
- Fix inverted comment on the 0x00FFFFFF mask (lower 24 bits are flags, upper byte is version).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../BackEnd/NodeEndpointOutOfProcBase_Tests.cs | 10 ++++++++++
src/Shared/NodeEndpointOutOfProcBase.cs | 3 ++-
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs b/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs
index 245cad1a65b..e0d3b465af1 100644
--- a/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs
+++ b/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs
@@ -61,6 +61,16 @@ public void Arm64Parent_X64Child_NotTolerated()
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();
+ }
}
}
diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs
index 53633f6e63f..032c6f1c64b 100644
--- a/src/Shared/NodeEndpointOutOfProcBase.cs
+++ b/src/Shared/NodeEndpointOutOfProcBase.cs
@@ -593,7 +593,8 @@ private bool IsHandshakePartValid(KeyValuePair component, int hands
/// True cross-arch mismatches (e.g. parent sent X64 but child expects Arm64, or vice
/// versa) remain rejected.
///
- /// 0x00FFFFFF is the handshake version included in component, the rest is the node type.
+ /// 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)
{