Skip to content

MSB4216 on Windows ARM: NET task host handshake never tolerates expected=Arm64; #13741 implementation diverges from description #13879

@JanProvaznik

Description

@JanProvaznik

Summary

<UsingTask Runtime="NET" /> fails with MSB4216 on Windows-ARM. Two independent bugs:

  • Bug A (all branches, since Support launching net taskhost - initial implementation #11393): IsAllowedBitnessMismatch only tolerates expected=X64, not expected=Arm64. Any cross-bitness NET task host handshake to an ARM64 child is rejected.
  • Bug B (main only, from Default Architecture to OS arch for UsingTask Runtime="NET" #13741): GetExplicitMSBuildArchitecture(arch, runtime) returns "*" for unspecified arch under .NET Framework + Runtime="NET". The PR description says it returns the OS architecture, but the implementation returns MSBuildArchitectureValues.any. The handshake then emits no arch bit, which hits Bug A on Windows-ARM and regresses the ARM64 .NET Framework parent case (which previously worked via symmetric match).

Symptom

Roslyn's build.cmd on Windows-ARM against SDK 10.0.108:

error MSB4216: Could not run the "CompareVersions" task because MSBuild could not create or
connect to a task host with runtime "NET" and architecture "*".  Please ensure that
(1) the requested runtime and/or architecture are available on the machine, and
(2) that the required executable "C:\Program Files\dotnet\sdk\10.0.108\MSBuild.exe" exists
and can be run.

Two caveats on the message:

  • The MSBuild.exe path is a template in TaskHostTask.LogErrorUnableToCreateTaskHost — emitted regardless of whether the parent actually launches the apphost. SDK 10.0.108 ships MSBuild.dll only; the apphost first ships in 10.0.300. The apphost-vs-dotnet.exe MSBuild.dll decision doesn't affect the handshake.
  • The arch "*" is the puzzle that points at Bug B — release branches normalize it to a concrete arch in MergeTaskFactoryParameterSets. Worth confirming which MSBuild binary Roslyn's environment actually invokes.

Bug A — IsAllowedBitnessMismatch Arm64 gap

src/Shared/NodeEndpointOutOfProcBase.cs:590-602:

private bool IsAllowedBitnessMismatch(int expectedOptions, int receivedOptions)
{
    var expectedNodeType = (HandshakeOptions)(expectedOptions & 0x00FFFFFF);
    var receivedNodeType = (HandshakeOptions)(receivedOptions & 0x00FFFFFF);

    bool receivedIsX86 = !Handshake.IsHandshakeOptionEnabled(receivedNodeType, HandshakeOptions.X64) &&
                         !Handshake.IsHandshakeOptionEnabled(receivedNodeType, HandshakeOptions.Arm64);

    bool expectedIsX64 = Handshake.IsHandshakeOptionEnabled(expectedNodeType, HandshakeOptions.X64);

    return receivedIsX86 && expectedIsX64;
}

receivedIsX86 correctly excludes both X64 and Arm64. expectedIsX64 forgets Arm64. The function always returns false when the child expects Arm64 (i.e. whenever the SDK runs on a Windows-ARM machine and the parent sent no arch bit).

Introduced by #11393. Present on main, vs17.14, vs17.15, vs18.0, vs18.3vs18.7. Windows-ARM has never worked through this code path.

Bug B — GetExplicitMSBuildArchitecture(arch, runtime) returns "*" instead of OS arch

src/Framework/XMakeAttributes.cs:496-508, introduced by #13741. Call site is AssemblyTaskFactory.MergeTaskFactoryParameterSets.

The PR description states:

When runtime == "NET" and architecture is unspecified (* / CurrentArchitecture / null), returns the OS architecture (which matches the dotnet host that will actually be launched) instead of the current process architecture.

The shipped code:

internal static string GetExplicitMSBuildArchitecture(string architecture, string runtime)
{
#if NETFRAMEWORK
    if (MSBuildRuntimeValues.net.Equals(runtime, StringComparison.OrdinalIgnoreCase) &&
        (architecture == null ||
         MSBuildArchitectureValues.any.Equals(architecture, StringComparison.OrdinalIgnoreCase)))
    {
        return MSBuildArchitectureValues.any;
    }
#endif

    return GetExplicitMSBuildArchitecture(architecture);
}

It returns "any" ("*"), not the OS arch. GetHandshakeOptions only sets X64/Arm64 bits when arch equals "x64"/"arm64""*" produces no bit. The parent then sends a handshake indistinguishable from an x86 parent, which lands on Bug A.

ARM64 .NET Framework parent regression (VS Desktop 17.10+ on Windows-ARM)

Step Pre-#13741 Post-#13741
normalizedArch for <UsingTask Runtime="NET" /> "arm64" (GetCurrentMSBuildArchitecture) "*"
GetHandshakeOptions wire bit Arm64 none
Child wire bit (arm64 SDK) Arm64 Arm64
Outcome symmetric match ✅ mismatch → Bug A → MSB4216 ❌

The change is observable in the bytes on the wire, not just internal representation.

x86 / amd64 .NET Framework parent on Windows-x64

Wire bit is the same (none) before and after #13741, because "x86"/"amd64" already produced no X64/Arm64 bit. Bug A's tolerance covers expected=X64, so this path keeps working — no behavior change from #13741 here.

Branch / parent matrix on Windows-ARM

Parent main (post-#13741) vs18.x / vs17.x (no #13741)
x86 .NET Fx ❌ Bug A ❌ Bug A
amd64 .NET Fx ❌ Bug A ❌ Bug A
arm64 .NET Fx ❌ Bug A (regressed by Bug B) ✅ symmetric match
.NET (Core) MSBuild n/a — Runtime="NET" loads in-proc n/a

So Roslyn's failure on SDK 10.0.108 (whose MSBuild is a vs18.0 build) is pure Bug A. The "*" in the error string suggests the parent actually doing the build may be a main-built MSBuild on PATH; on release branches MergeTaskFactoryParameterSets would normalize the arch to a concrete value before logging.

Proposed fixes

Fix A — child-side tolerance (main + service to vs18.0)

src/Shared/NodeEndpointOutOfProcBase.cs:

bool expectedIsX64   = Handshake.IsHandshakeOptionEnabled(expectedNodeType, HandshakeOptions.X64);
bool expectedIsArm64 = Handshake.IsHandshakeOptionEnabled(expectedNodeType, HandshakeOptions.Arm64);

return receivedIsX86 && (expectedIsX64 || expectedIsArm64);

This single change unblocks Windows-ARM on every branch. Safe to service to vs18.0 for an SDK 10.0.x patch — only widens what is currently rejected, no payload format change.

Fix B — make #13741's implementation match its description (main only)

Return the OS architecture (per the PR description) when the current process arch is x64 or arm64; only fall back to "any" for an x86 .NET Framework parent (where no x86 SDK exists and Bug A's tolerance is the right answer). This also undoes the ARM64 .NET Fx parent regression without giving up #13741's intent.

A cleaner long-term option: have the parent compute the wire arch bit from the apphost RID / SDK layout it is about to launch, so the child always sees a concrete bit and IsAllowedBitnessMismatch becomes vestigial.

Tests to add

  • IsAllowedBitnessMismatch unit test with expected = Arm64, received = (no arch bit)true.
  • GetExplicitMSBuildArchitecture(arch, runtime) matrix covering {null, "*", "x86", "x64", "arm64"} × {"CLR4", "NET", null} on x86 / x64 / arm64 hosts. Currently no arm64 coverage.

References

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions