Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
95 changes: 95 additions & 0 deletions src/Build.UnitTests/BackEnd/CommunicationsUtilities_Tests.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Tests for <see cref="CommunicationsUtilities.GetHandshakeOptions"/> 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 <c>IsAllowedBitnessMismatch</c> tolerates
/// "parent sent no arch bit") accept the connection regardless of either process arch.
/// </summary>
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();
}
}
}
}
77 changes: 77 additions & 0 deletions src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Tests for <see cref="NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch"/>, 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.
/// </summary>
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();
Comment thread
ViktorHofer marked this conversation as resolved.
}

[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
20 changes: 19 additions & 1 deletion src/Framework/BackEnd/CommunicationsUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get this comment, what future?

// 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 =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this whole connection decision tree would greatly benefit from a flowchart/visualizations in docs

taskHost &&
!taskHostParameters.IsEmpty &&
XMakeAttributes.MSBuildRuntimeValues.net.Equals(taskHostParameters.Runtime, StringComparison.OrdinalIgnoreCase);

if (!isNetTaskHostParent && !string.IsNullOrEmpty(architectureFlagToSet))
{
if (architectureFlagToSet!.Equals(XMakeAttributes.MSBuildArchitectureValues.x64, StringComparison.OrdinalIgnoreCase))
{
Expand Down
22 changes: 16 additions & 6 deletions src/Shared/NodeEndpointOutOfProcBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -584,21 +584,31 @@ private bool IsHandshakePartValid(KeyValuePair<string, int> component, int hands

#if NET
/// <summary>
/// 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.
/// </summary>
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.
/// </summary>
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

Expand Down
Loading