Skip to content

Fix race conditions in task host path resolution#13485

Merged
JanProvaznik merged 1 commit into
dotnet:mainfrom
AR-May:fix-msbuild-name-race-condition
Apr 7, 2026
Merged

Fix race conditions in task host path resolution#13485
JanProvaznik merged 1 commit into
dotnet:mainfrom
AR-May:fix-msbuild-name-race-condition

Conversation

@AR-May

@AR-May AR-May commented Apr 2, 2026

Copy link
Copy Markdown
Member

Fixes #13484

Context

Multithreaded (/mt) builds that launch out-of-process task hosts crash with ArgumentNullException: Value cannot be null. Parameter name: path2 in Path.Combine when multiple RequestBuilder threads simultaneously resolve the task host executable name.

Root cause

GetTaskHostNameFromHostContext in NodeProviderOutOfProcTaskHost.cs has an unsafe read-modify-write sequence on the static field s_msbuildName:

if (string.IsNullOrEmpty(s_msbuildName))            // (1) check
{
    s_msbuildName = Environment.GetEnvironmentVariable("MSBUILD_EXE_NAME"); // (2) write — may be null
    if (!string.IsNullOrEmpty(s_msbuildName))
    {
        return s_msbuildName;
    }
    s_msbuildName = Constants.MSBuildExecutableName;  // (3) write — always non-null
}
return s_msbuildName;                                // (4) read

When MSBUILD_EXE_NAME is not set, step (2) writes null into s_msbuildName. Between steps (2) and (3), another thread can read s_msbuildName at step (4)** and get null.

The returned null is passed as toolName into Path.Combine(basePath, toolName), which throws ArgumentNullException on .NET Framework (where Path.Combine does not accept null arguments).

Changes Made

Since all computations are deterministic (environment variables + constants), we use the local-variable snapshot pattern
— read the static into a local, compute if null, write back, return the local. This is lock-free and safe because reference writes are atomic in .NET. Applied the same fix to similar patterns in nearby code in NodeProviderOutOfProcTaskHost.cs and CurrentHost.cs

Testing

No extra testing. There is no stable local repro.

Fix unsynchronized shared static fields that can race when multiple
RequestBuilder threads resolve task host paths concurrently in /m builds.

NodeProviderOutOfProcTaskHost.cs:
- TaskHostNameForClr2TaskHost: use local variable to prevent returning
  a stale/null value when another thread clears the cache.
- GetMSBuildExecutablePathForNonNETRuntimes: snapshot base paths into
  locals before using them, so a concurrent caller that hasn't written
  them yet cannot propagate null into Path.Combine.
- GetOrInitializeX*Clr2Path: accept base path as parameter instead of
  reading the static directly, avoiding a null read before init.
- ClearCachedTaskHostPaths: document that it must not be called
  concurrently with methods that populate the statics.

CurrentHost.cs:
- GetCurrentHost: use local variable pattern so the returned value
  cannot be null due to interleaved reads/writes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AR-May AR-May requested review from JanProvaznik and Copilot and removed request for Copilot April 2, 2026 15:04
@AR-May AR-May self-assigned this Apr 2, 2026
Comment thread src/Build/BackEnd/Components/Communications/CurrentHost.cs
@JanProvaznik JanProvaznik merged commit f86b024 into dotnet:main Apr 7, 2026
14 checks passed
dfederm pushed a commit to dfederm/msbuild that referenced this pull request Apr 9, 2026
# Fixes dotnet#13484

### Context
Multithreaded (`/mt`) builds that launch out-of-process task hosts crash
with `ArgumentNullException: Value cannot be null. Parameter name:
path2` in `Path.Combine` when multiple `RequestBuilder` threads
simultaneously resolve the task host executable name.

### Root cause

`GetTaskHostNameFromHostContext` in `NodeProviderOutOfProcTaskHost.cs`
has an unsafe read-modify-write sequence on the static field
`s_msbuildName`:

```csharp
if (string.IsNullOrEmpty(s_msbuildName))            // (1) check
{
    s_msbuildName = Environment.GetEnvironmentVariable("MSBUILD_EXE_NAME"); // (2) write — may be null
    if (!string.IsNullOrEmpty(s_msbuildName))
    {
        return s_msbuildName;
    }
    s_msbuildName = Constants.MSBuildExecutableName;  // (3) write — always non-null
}
return s_msbuildName;                                // (4) read
```

When `MSBUILD_EXE_NAME` is not set, step (2) writes `null` into
`s_msbuildName`. Between steps (2) and (3), another thread can read
`s_msbuildName` at step (4)** and get `null`.

The returned `null` is passed as `toolName` into `Path.Combine(basePath,
toolName)`, which throws `ArgumentNullException` on .NET Framework
(where `Path.Combine` does not accept null arguments).

### Changes Made
Since all computations are deterministic (environment variables +
constants), we use the local-variable snapshot pattern
— read the static into a local, compute if null, write back, return the
local. This is lock-free and safe because reference writes are atomic in
.NET. Applied the same fix to similar patterns in nearby code in
NodeProviderOutOfProcTaskHost.cs and CurrentHost.cs

### Testing
No extra testing. There is no stable local repro.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced Jun 10, 2026
This was referenced Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Race condition in GetTaskHostNameFromHostContext causes ArgumentNullException in multithreaded builds

3 participants