Fix existence cache kind poisoning - #14249
Conversation
Keep file, directory, and file-or-directory existence checks in separate caches so a file check against a directory cannot make later directory checks incorrectly return false. Fixes dotnet#13566 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a correctness bug in MSBuild’s evaluation file system caching where a cached FileExists(path) miss could incorrectly affect later DirectoryExists(path) queries for the same path, leading to incorrect empty wildcard expansions in evaluation scenarios like GetDirectoryNameOfFileAbove(..., "") followed by project-level globbing.
Changes:
- Split the existence cache in
CachingFileSystemWrapperby query kind (file, directory, file-or-directory) to prevent cross-kind cache poisoning. - Added a direct regression unit test that validates file/directory existence checks don’t interfere with each other in the cache.
- Added an evaluation-level regression test covering
GetDirectoryNameOfFileAbove(..., "")followed by wildcard item expansion across allEvaluationContextsharing policies.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/Framework/FileSystem/CachingFileSystemWrapper.cs | Splits existence caching into separate dictionaries per existence query kind to restore correct semantics. |
| src/Build.UnitTests/Definition/ProjectEvaluationContext_Tests.cs | Adds regression tests for existence-cache poisoning and the GetDirectoryNameOfFileAbove(..., "") + wildcard expansion scenario. |
There was a problem hiding this comment.
Caution
agentic threat detected
Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.
Code Review — PR #14249 "Fix existence cache kind poisoning"
24-dimension review complete. No blocking or major findings. The fix is correct, minimal, and well-tested.
Root cause & fix assessment
The bug was real and the fix is the right approach: FileExists, DirectoryExists, and FileOrDirectoryExists have distinct semantics and must not share a cache. Three separate ConcurrentDictionary instances cleanly enforce this — each query kind now gets its own private namespace, eliminating cross-kind poisoning with no correctness tradeoff.
Findings summary
| Dimension | Result |
|---|---|
| 1. Backwards Compatibility | ✅ LGTM — internal-only change, no observable API break |
| 2. ChangeWave Discipline | ✅ LGTM — pure bug fix, opt-out not needed |
| 3. Performance & Allocation | ✅ LGTM — 2 extra dicts allocated once per context; lambdas unchanged |
| 4. Test Coverage | ✅ LGTM — unit regression + integration regression across all 3 sharing policies |
| 5. Error Message Quality | ✅ LGTM — N/A |
| 6. Logging & Diagnostics | ✅ LGTM — N/A |
| 7. String Comparison | ✅ LGTM — key comparison unchanged (pre-existing ordinal) |
| 8. API Surface | ✅ LGTM — internal sealed, nothing public changed |
| 9. Target Authoring | ✅ LGTM — N/A |
| 10. Design | ✅ LGTM — three independent dicts is the idiomatic, obvious fix |
| 11. Cross-Platform | ✅ LGTM — no hardcoded separators, path handling unchanged |
| 12. Code Simplification | new ConcurrentDictionary<string, bool>() → new() (see inline) |
| 13. Concurrency & Thread Safety | ✅ LGTM — ConcurrentDictionary.GetOrAdd throughout; strictly safer than before |
| 14. Naming Precision | ✅ LGTM — _directoryExistenceCache / _fileExistenceCache / _fileOrDirectoryExistenceCache are clear |
| 15. SDK Integration | ✅ LGTM — N/A |
| 16. Idiomatic C# | |
| 17. File I/O & Path Handling | ✅ LGTM — path semantics unchanged |
| 18. Documentation | ✅ LGTM — self-documenting names; no docs needed |
| 19. Build Infrastructure | ✅ LGTM — N/A |
| 20. Scope & PR Discipline | ✅ LGTM — single concern, properly linked to #13566 |
| 21. Evaluation Model Integrity | ✅ LGTM — fix makes evaluation more order-independent |
| 22. Correctness & Edge Cases | ✅ LGTM — original repro covered; reverse direction covered; FileOrDirectoryExists independence covered |
| 23. Dependency Management | ✅ LGTM — no new package references |
| 24. Security | ✅ LGTM — no security regression |
One inline NIT
On CachingFileSystemWrapper.cs lines 15–17: the three new ConcurrentDictionary fields use verbose initializer syntax. The repo style (C# 14) prefers target-typed new(). This is purely cosmetic — the logic is correct as written.
Generated by Expert Code Review (on open) for #14249 · 1.1K AIC · ⊞ 30.3K
Updated [Microsoft.Build.Utilities.Core](https://github.com/dotnet/msbuild) from 18.9.6 to 18.10.1. <details> <summary>Release notes</summary> _Sourced from [Microsoft.Build.Utilities.Core's releases](https://github.com/dotnet/msbuild/releases)._ ## 18.10.1 ## What's Changed * [vs16.11] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13103 * [vs17.12] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13796 * [vs17.8] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13902 * [vs17.11] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13903 * [vs17.12] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13909 * [vs17.12] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13986 * Add vs18.9 to merge-flow config; retire vs18.3 by @JanProvaznik in dotnet/msbuild#14214 * Bump labeler-cache-retention to use issue-labeler v2.1.0 by @jeffhandley in dotnet/msbuild#14171 * Bump main to 18.10.0 after vs18.9 snap by @JanProvaznik in dotnet/msbuild#14216 * Improve release skill: Phase 2 DARC rules, VMR backflow, deterministic baseline by @JanProvaznik in dotnet/msbuild#14220 * Determinize release: hardcode OptProf baseline + Phase 3.2 baseline resolver by @JanProvaznik in dotnet/msbuild#14222 * Serialize BuildRequestConfiguration.RequestedTargets to fix solution metaproject MSB4057 in parallel builds by @ViktorHofer in dotnet/msbuild#14223 * [main] Update dependencies from nuget/nuget.client by @dotnet-maestro[bot] in dotnet/msbuild#14203 * Core support for AbsolutePath/FileInfo/DirectoryInfo and ITaskItem<T> as task parameters by @baronfel in dotnet/msbuild#13971 * [main] Update dependencies from dotnet/roslyn by @dotnet-maestro[bot] in dotnet/msbuild#14206 * Fix existence cache kind poisoning by @AlesProkop in dotnet/msbuild#14249 * [main] Source code updates from dotnet/dotnet by @dotnet-maestro[bot] in dotnet/msbuild#14226 * Don't disable the MSBuild server for /mt builds when node reuse is off by @AR-May in dotnet/msbuild#14248 * Enhance expert reviewer guidelines with additional checks. by @AR-May in dotnet/msbuild#14255 * [main] Source code updates from dotnet/dotnet by @dotnet-maestro[bot] in dotnet/msbuild#14253 * [main] Update dependencies from dotnet/roslyn by @dotnet-maestro[bot] in dotnet/msbuild#14268 * [main] Update dependencies from nuget/nuget.client by @dotnet-maestro[bot] in dotnet/msbuild#14267 * Bump github/gh-aw-actions/setup from 0.81.6 to 0.82.2 by @dependabot[bot] in dotnet/msbuild#14266 * Avoid boxing the struct enumerator in PropertyDictionary<T>.GetEnumerator() by @nareshjo in dotnet/msbuild#14272 * Refresh copy marker when implementation output changes by @AlesProkop in dotnet/msbuild#14231 * Send task-host build process environment as delta by @OvesN in dotnet/msbuild#14126 * Add regression coverage for metadata newline preservation by @VolPlita in dotnet/msbuild#14261 * Fix EmbedInBinlog items with relative paths from child projects by @huulinhnguyen-dev in dotnet/msbuild#13990 * Stop requiring VersionPrefix updates in servicing - insert prerelease versions to VS by @ViktorHofer in dotnet/msbuild#14277 * Fix WriteLinesToFile rewriting unchanged file when custom encoding is used by @huulinhnguyen-dev in dotnet/msbuild#14146 * Enable trim/AOT analyzers for Microsoft.Build and clean up annotations by @JeremyKuhne in dotnet/msbuild#14064 * [automated] Merge branch 'vs18.9' => 'main' by @github-actions[bot] in dotnet/msbuild#14291 * Fix MicroBuild plugin feed URL to use allowed pkgs.dev.azure.com format by @AlesProkop in dotnet/msbuild#14295 * Pass ExcludeRestorePackageImports during restore to avoid redundant evaluations by @ViktorHofer with @Copilot in dotnet/msbuild#14274 * [vs18.7] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13988 * Adopt Clever Test Selection (CTS) as parallel, non-blocking PR pipeline by @jankratochvilcz in dotnet/msbuild#14212 * Harden exceptions when connecting to server by @JanProvaznik in dotnet/msbuild#14292 * Update MicrosoftBuildVersion in analyzer template by @github-actions[bot] in dotnet/msbuild#13886 * Fix MSBuild Server client dropping build result under WaitAny race (#14172) by @JanProvaznik in dotnet/msbuild#14251 * Partially revert #13660: remove NuGet RestoreTask transient TaskHost workaround by @JanProvaznik in dotnet/msbuild#14297 * Disable daily AI credits guardrail for Expert Code Review workflow by @JanProvaznik with @Copilot in dotnet/msbuild#14314 * Localized file check-in by OneLocBuild Task: Build definition ID 9434: Build ID 14614733 by @dotnet-bot in dotnet/msbuild#14246 * Add opt-in partial (stop-after-pass) project evaluation by @ViktorHofer in dotnet/msbuild#14290 * Use partial evaluation for -getProperty/-getItem without a target by @ViktorHofer in dotnet/msbuild#14296 * [main] Source code updates from dotnet/dotnet by @dotnet-maestro[bot] in dotnet/msbuild#14324 * [main] Update dependencies from dotnet/roslyn by @dotnet-maestro[bot] in dotnet/msbuild#14333 * [main] Update dependencies from nuget/nuget.client by @dotnet-maestro[bot] in dotnet/msbuild#14330 * Bump github/gh-aw-actions/setup from 0.82.2 to 0.82.8 by @dependabot[bot] in dotnet/msbuild#14328 * Restrict partial evaluation to ProjectInstance by @ViktorHofer in dotnet/msbuild#14340 ... (truncated) Commits viewable in [compare view](dotnet/msbuild@v18.9.6...v18.10.1). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Fixes #13566
Context
CachingFileSystemWrapperused a single existence cache forFileExists,DirectoryExists, andFileOrDirectoryExists. That allowed aFileExists(directoryPath)miss to poison a laterDirectoryExists(directoryPath)lookup, which could make project-level wildcard expansion return empty results after calls such asGetDirectoryNameOfFileAbove(..., '').Changes Made
GetDirectoryNameOfFileAbove(..., '')followed by project-level wildcard item expansion.Compatibility
This is a bug fix that restores correct filesystem query semantics. It does not add warnings, errors, public API, or ChangeWave-gated behavior.
Testing
dotnet test src\Build.UnitTests\Microsoft.Build.Engine.UnitTests.csproj -f net10.0 -- --filter-class "*ProjectEvaluationContext_Tests"dotnet, then verified it passes withartifacts\bin\bootstrap\core\dotnet.exefrom this branch..projrepro with installeddotnet msbuildvs this branch's bootstrap.