Issue Description
Multithreaded MSBuild can deadlock when two invocations share a TaskHost, both block in BuildProjectFile, and the earlier invocation completes first. NodeProviderOutOfProcTaskHost routes that invocation's messages to the most recently attached handler rather than its original owner.
Node reuse is not required. This is distinct from mixed-version TaskHost adoption and deserialization failures in #15062.
Steps to Reproduce
Build two wrapper projects concurrently with -mt -m:2 -nr:false. Both wrappers request different targets in the same project configuration:
- Target A runs a task that calls ChildA through
BuildProjectFile. ChildA yields its node and waits at a file gate.
- Target B runs another invocation in the same TaskHost and calls ChildB. ChildB opens ChildA's gate and requests target A's result.
- ChildA completes. Invocation A returns while invocation B remains blocked waiting for ChildB.
A regression test accompanying the fix constructs this graph with file gates, not timing-based success conditions. The normal-mode run is the control.
Expected Behavior
Each invocation's log messages, callback requests, and completion reach its original handler. Target A completes, ChildB obtains A's result, and target B completes with its own output values.
Actual Behavior
The provider's top handler is B. A's return log and successful TaskHostTaskComplete are queued to B, while A's queue remains empty. B is synchronously waiting for A's target result and cannot drain its queue. A cannot finish without the completion packet held in B's queue.
Analysis
The handler stack assumes last-in, first-out completion. Reentrant callbacks allow an earlier invocation to resume and finish while a later invocation remains blocked. Callback request IDs already correlate child-build responses inside the TaskHost; they do not identify the owner of the subsequent task completion at the parent.
Read-only snapshots in Linux diagnostic build 1600871 directly show A's return log and successful completion in B's queue. The reduced graph reproduced twice in MT mode, remaining blocked at the 120-second bound; normal mode completed in 0.676 seconds. Windows reproductions show the same ordering.
The broader investigation is recorded in #15047. Inner logs from the original VMR timeouts were lost: their matching wait signature is supporting evidence, not recovered invocation identity for every original timeout. This issue tracks the independently reproduced ownership defect, not the entire investigation.
Versions & Configurations
- Linux: Azure Linux 3.0, SDK
11.0.100-rc.1.26420.103, bootstrap MSBuild 18.11.0-1.26420.103+1d599674e; diagnostic build 1600871.
- Windows: reproduced with a source-built MSBuild 18.12 bootstrap. The regression passed in normal mode and timed out in MT mode before the fix.
- Reduced graph:
-m:2, -mt, -nr:false, MSBuild Server disabled; no graph-build switch. Reproduces within one build, without cross-build reuse.
- Last known working version: not established.
- Fix compatibility boundary: invocation-aware routing requires both peers to support the new versioned task configuration. Older peers and legacy CLR4 version-0 configuration retain their existing wire format and do not gain that guarantee.
Issue Description
Multithreaded MSBuild can deadlock when two invocations share a TaskHost, both block in
BuildProjectFile, and the earlier invocation completes first.NodeProviderOutOfProcTaskHostroutes that invocation's messages to the most recently attached handler rather than its original owner.Node reuse is not required. This is distinct from mixed-version TaskHost adoption and deserialization failures in #15062.
Steps to Reproduce
Build two wrapper projects concurrently with
-mt -m:2 -nr:false. Both wrappers request different targets in the same project configuration:BuildProjectFile. ChildA yields its node and waits at a file gate.A regression test accompanying the fix constructs this graph with file gates, not timing-based success conditions. The normal-mode run is the control.
Expected Behavior
Each invocation's log messages, callback requests, and completion reach its original handler. Target A completes, ChildB obtains A's result, and target B completes with its own output values.
Actual Behavior
The provider's top handler is B. A's return log and successful
TaskHostTaskCompleteare queued to B, while A's queue remains empty. B is synchronously waiting for A's target result and cannot drain its queue. A cannot finish without the completion packet held in B's queue.Analysis
The handler stack assumes last-in, first-out completion. Reentrant callbacks allow an earlier invocation to resume and finish while a later invocation remains blocked. Callback request IDs already correlate child-build responses inside the TaskHost; they do not identify the owner of the subsequent task completion at the parent.
Read-only snapshots in Linux diagnostic build 1600871 directly show A's return log and successful completion in B's queue. The reduced graph reproduced twice in MT mode, remaining blocked at the 120-second bound; normal mode completed in 0.676 seconds. Windows reproductions show the same ordering.
The broader investigation is recorded in #15047. Inner logs from the original VMR timeouts were lost: their matching wait signature is supporting evidence, not recovered invocation identity for every original timeout. This issue tracks the independently reproduced ownership defect, not the entire investigation.
Versions & Configurations
11.0.100-rc.1.26420.103, bootstrap MSBuild18.11.0-1.26420.103+1d599674e; diagnostic build 1600871.-m:2,-mt,-nr:false, MSBuild Server disabled; no graph-build switch. Reproduces within one build, without cross-build reuse.