Skip to content

Property functions using System.IO.File with relative paths break in -mt mode due to shared Environment.CurrentDirectory #13237

@JanProvaznik

Description

@JanProvaznik

Description

MSBuild property functions that invoke System.IO.File methods (e.g., ReadAllText, Exists, ReadAllLines) with relative paths produce incorrect results or fail in multi-threaded (-mt) mode. This is because these calls are dispatched via reflection and resolve relative paths using Environment.CurrentDirectory, which is a process-global shared state — unsafe when multiple worker nodes are evaluating projects concurrently.

Repro

Project file (e.g., src/Containers/packaging/package.csproj in dotnet/sdk):
xml <Target Name="PreparePackageReleaseNotesFromFile" BeforeTargets="GenerateNuspec"> <PropertyGroup> <PackageReleaseNotesFile>../docs/ReleaseNotes/v8.0.300.md</PackageReleaseNotesFile> <PackageReleaseNotes>$([System.IO.File]::ReadAllText($(PackageReleaseNotesFile)))</PackageReleaseNotes> </PropertyGroup> </Target>

Without -mt: Works correctly. ../docs/ReleaseNotes/v8.0.300.md resolves relative to the project directory.

With -mt: Fails with:
error MSB4184: The expression "[System.IO.File]::ReadAllText(../docs/ReleaseNotes/v8.0.300.md)" cannot be evaluated. Could not find a part of the path '/home/builder/dotnet/src/docs/ReleaseNotes/v8.0.300.md'.

The relative path resolved from the wrong directory — another project's directory that happened to be the current Environment.CurrentDirectory on a different worker node.

Root Cause

MSBuild's property function evaluation has two paths:

  1. Well-known functions (WellKnownFunctions.cs) — intercepted and handled with proper working directory context. For example, Path.GetFullPath correctly uses FileUtilities.CurrentThreadWorkingDirectory (line 108):
    csharp returnVal = !string.IsNullOrEmpty(FileUtilities.CurrentThreadWorkingDirectory) ? Path.GetFullPath(Path.Combine(FileUtilities.CurrentThreadWorkingDirectory, arg0)) : Path.GetFullPath(arg0);

  2. Everything else — falls through to reflection-based InvokeMember (Expander.cs line 4108):
    csharp functionResult = _receiverType.InvokeMember(_methodMethodName, _bindingFlags, Type.DefaultBinder, objectInstance, args, CultureInfo.InvariantCulture);

System.IO.File.ReadAllText is not a well-known function, so it takes path (2). When called with a relative path, the CLR resolves it using Environment.CurrentDirectory — a process-global that is concurrently modified by other MSBuild worker nodes.

Impact

This is a compatibility bug — any project file that uses [System.IO.File]::ReadAllText(relative-path) (or similar System.IO.File methods with relative paths) works correctly in single-threaded mode but silently produces wrong results or fails in -mt mode.

Found in VMR source-build testing: dotnet/sdk's src/Containers/packaging/package.csproj fails with this pattern. A grep of the VMR shows 37 usages of [System.IO.File] in project files — any using relative paths are potentially affected.

Expected Behavior

Property functions that accept file paths should resolve relative paths consistently using the project directory (MSBuildProjectDirectory) regardless of -mt mode, the same way Path.GetFullPath does.

Suggested Fix

Either:

  1. Intercept System.IO.File methods in WellKnownFunctions.cs and resolve relative paths using FileUtilities.CurrentThreadWorkingDirectory before delegating, or
  2. Set Environment.CurrentDirectory per-thread before invoking any property function via reflection (not feasible — it's process-global), or
  3. Resolve relative path arguments in the generic InvokeMember fallback path — for any System.IO.File method argument that is a relative path, prepend FileUtilities.CurrentThreadWorkingDirectory before invoking.

Option (1) is the most reliable and consistent with the existing Path.GetFullPath interception pattern.

Environment

  • MSBuild 18.5.0 (main branch, preview 2)
  • Ubuntu 24.04, -mt mode
  • Discovered during VMR 2-stage source-build with MSBuild -mt validation

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions