Skip to content

Fix AbsolutePath.GetCanonicalForm process state leak on Windows #13788

Open
OvesN wants to merge 6 commits into
mainfrom
dev/veronikao/fix-absolutepath-canonicalform-leak-13748
Open

Fix AbsolutePath.GetCanonicalForm process state leak on Windows #13788
OvesN wants to merge 6 commits into
mainfrom
dev/veronikao/fix-absolutepath-canonicalform-leak-13748

Conversation

@OvesN
Copy link
Copy Markdown
Contributor

@OvesN OvesN commented May 15, 2026

Fixes #13748

Context

AbsolutePath.Value was not guaranteed to be fully qualified when the struct was constructed via AbsolutePath(string, AbsolutePath). On Windows, Path.Combine(basePath, path) returns the second argument unchanged when it is rooted-but-not-fully-qualified (\foo root-relative, X:foo drive-relative), so Value could end up as a non-absolute string. A later GetCanonicalForm() call would then resolve it via System.IO.Path.GetFullPath, which consults process-global state (current drive, per-drive cwd) — breaking determinism and multithreaded task isolation.

Changes

  • AbsolutePath(string, AbsolutePath) anchors the two Windows rooted-but-not-fully-qualified shapes against basePath at construction time, via a new private MakeFullyQualified helper:
    • Root-relative (\foo, /foo) → re-rooted under basePath's root using Path.GetPathRoot.
    • Drive-relative (X:foo) → drive dropped, remainder anchored under basePath via Path.Combine.
  • Anchoring uses pure string operations rather than Path.GetFullPath(path, basePath) so the un-normalized form is preserved. GetCanonicalForm() remains the single normalization step.
  • The fix is conditioned on #if NETFRAMEWORK || NET because Path.IsPathFullyQualified is not available in netstandard2.0. The netstandard2.0 build is a compat surface only and never runs at runtime in MSBuild.

Testing

AnchorsToBasePath_NotProcessState covers:

  • Root-relative and drive-relative inputs against a drive-letter base path (same and different drive).
  • Root-relative and drive-relative inputs against UNC and DOS-device base paths.
  • Pass-through verification for fully qualified inputs (drive, UNC, DOS device) — confirms they are not modified.
  • Plain relative inputs (foo, sub\file.txt) anchored to basePath — confirms CWD is never consulted.

@OvesN OvesN force-pushed the dev/veronikao/fix-absolutepath-canonicalform-leak-13748 branch from 61f2570 to 68dbbfa Compare May 15, 2026 15:21
…h on Windows (#13748)

On Windows, Path.Combine 'resets' on a rooted second argument, so AbsolutePath(string, AbsolutePath) was leaving root-relative ('\foo') and drive-relative ('X:foo') inputs as Value unchanged. The result was a path that was rooted but not fully qualified — i.e. not a valid absolute path. Any subsequent use of Value (direct I/O or GetCanonicalForm) then resolved the path against process-global state (current drive, per-drive cwd), producing non-deterministic results across host processes and breaking multithreaded task isolation.

Anchor those shapes against basePath inside the constructor using purely string-based operations, so Value is always a valid, fully qualified absolute path. GetCanonicalForm is left untouched and continues to call System.IO.Path.GetFullPath(string) — preserving its existing canonicalization semantics (including invalid-character rejection) across .NET and .NET Framework.

The two-argument Path.GetFullPath overload is intentionally avoided: on .NET Framework it lives in Microsoft.IO.Path with different internal logic.

Adds regression theory GetCanonicalForm_WindowsRootedButNotFullyQualifiedPath_AnchorsToBasePath_NotProcessState covering both root-relative and drive-relative inputs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@OvesN OvesN force-pushed the dev/veronikao/fix-absolutepath-canonicalform-leak-13748 branch from 68dbbfa to 839fd38 Compare May 15, 2026 15:39
@OvesN OvesN marked this pull request as ready for review May 25, 2026 11:59
Copilot AI review requested due to automatic review settings May 25, 2026 11:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses non-deterministic Windows path canonicalization in Microsoft.Build.Framework.AbsolutePath by ensuring rooted-but-not-fully-qualified inputs (e.g. \foo, X:foo) are anchored to the provided base path rather than process-global state (current drive / per-drive CWD), improving multithreaded task isolation.

Changes:

  • Updates AbsolutePath(string, AbsolutePath) to pre-anchor Windows rooted-but-not-fully-qualified shapes during construction via a helper.
  • Adds a Windows-only regression theory covering root-relative, drive-relative, UNC, and device-path base paths to ensure determinism for both Value and GetCanonicalForm().
  • (Review finding) The anchoring logic is currently excluded from the .NETStandard TFM, leaving the original behavior there.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/Framework/PathHelpers/AbsolutePath.cs Adds Windows anchoring logic to avoid Path.GetFullPath resolving against process-global state for certain rooted-but-not-fully-qualified inputs.
src/Framework.UnitTests/AbsolutePath_Tests.cs Adds Windows-only regression coverage asserting construction and canonicalization are anchored to the supplied base path.

Comment thread src/Framework/PathHelpers/AbsolutePath.cs Outdated
Comment on lines +114 to +116
#if NETFRAMEWORK || NET
combined = MakeFullyQualified(combined, basePath.Value);
#endif
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The netstandard2.0 build of Microsoft.Build.Framework exists only as a compatibility surface — MSBuild itself never executes against it at runtime

Copy link
Copy Markdown
Member

@AR-May AR-May May 28, 2026

Choose a reason for hiding this comment

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

nit: This might be a good comment to have in the code as well to make it more friendly, I was baffled the first time I saw such a condition.

@OvesN OvesN self-assigned this May 25, 2026
@OvesN OvesN requested review from AR-May and JanProvaznik May 25, 2026 13:19
Copy link
Copy Markdown
Member

@JanProvaznik JanProvaznik left a comment

Choose a reason for hiding this comment

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

the base idea is right
only must fix is the static

Comment thread src/Framework/PathHelpers/AbsolutePath.cs
Comment thread src/Framework/PathHelpers/AbsolutePath.cs
Comment thread src/Framework/PathHelpers/AbsolutePath.cs
Comment thread src/Framework/PathHelpers/AbsolutePath.cs
Comment thread src/Framework/PathHelpers/AbsolutePath.cs
Comment thread src/Framework/PathHelpers/AbsolutePath.cs Outdated
Comment thread src/Framework/PathHelpers/AbsolutePath.cs Outdated
Comment on lines +114 to +116
#if NETFRAMEWORK || NET
combined = MakeFullyQualified(combined, basePath.Value);
#endif
Copy link
Copy Markdown
Member

@AR-May AR-May May 28, 2026

Choose a reason for hiding this comment

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

nit: This might be a good comment to have in the code as well to make it more friendly, I was baffled the first time I saw such a condition.

Comment thread src/Framework/PathHelpers/AbsolutePath.cs
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.

AbsolutePath.GetCanonicalForm leaks process state on Windows for rooted-but-not-fully-qualified inputs

5 participants