Prevent tasks from modifying immutable environment variables in MT mode#13273
Prevent tasks from modifying immutable environment variables in MT mode#13273AR-May wants to merge 12 commits intodotnet:mainfrom
Conversation
| /// The string comparison to use for environment variable name comparisons, based on OS environment variable handling. | ||
| /// Windows: case-insensitive, Unix/Linux: case-sensitive. | ||
| /// </summary> | ||
| internal static readonly StringComparison EnvironmentVariableComparison = NativeMethods.IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; |
There was a problem hiding this comment.
Note that environment variable comparisons can be case-sensitive or insensitive independently from the file system’s case sensitivity.
There was a problem hiding this comment.
Pull request overview
This PR introduces environment variable immutability validation for multithreaded builds to prevent tasks from modifying variables that other concurrent project builds depend on. The implementation centralizes environment variable constants and adds runtime validation that throws when tasks attempt to modify MSBUILD-prefixed or framework-discovery-related variables.
Changes:
- Added
EnvironmentVariableClassifierto determine which environment variables are immutable (MSBUILD-prefixed variables and specific framework discovery variables) - Refactored environment variable name constants into centralized
EnvironmentVariablesNamesclass in Framework\Constants.cs - Added validation in
MultiThreadedTaskEnvironmentDriverto enforce immutability constraints when tasks modify environment variables
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Framework/EnvironmentVariableClassifier.cs | New classifier to determine if environment variables are immutable based on prefixes and specific names |
| src/Framework/Constants.cs | Added EnvironmentVariablesNames class with centralized constants, moved DotnetHostPath from Constants class |
| src/Framework/FileUtilities.cs | Added platform-aware EnvironmentVariableComparer/Comparison utilities for consistent env var name handling |
| src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs | Added validation logic to prevent modification of immutable environment variables |
| src/Tasks/Al.cs, SGen.cs, RoslynCodeTaskFactory/RoslynCodeTaskFactoryCompilers.cs | Refactored to use EnvironmentVariablesNames constants instead of hardcoded strings |
| src/Shared/FrameworkLocationHelper.cs | Refactored to use EnvironmentVariablesNames constants |
| src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs, Evaluation/Evaluator.cs, BuildManager/EnvironmentVariableValidator.cs | Updated to use EnvironmentVariablesNames.DotnetHostPath |
| src/Build/Resources/Strings.resx + xlf files | Added error message for immutable variable modification attempts |
| src/Framework.UnitTests/EnvironmentVariableClassifier_Tests.cs | Unit tests for classifier with case-sensitivity behavior verification |
| src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs | Tests for immutable variable validation in multithreaded mode |
| src/MSBuild.UnitTests/MSBuildMultithreaded_Tests.cs, Build.UnitTests/BackEnd/BuildManager_Tests.cs, Evaluation/Evaluator_Tests.cs, Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs, UnitTests.Shared/RunnerUtilities.cs | Test updates to use new constants and avoid MSBUILD-prefixed test variables |
| // Check for variables being removed (exist in current but not in new environment) | ||
| foreach (string currentVar in _environmentVariables.Keys) | ||
| { | ||
| if (!newEnvironment.ContainsKey(currentVar)) | ||
| { | ||
| EnsureVariableCanBeModified(currentVar); | ||
| } | ||
| } |
There was a problem hiding this comment.
The SetEnvironment method has a critical bug in handling case-sensitivity when checking for removed variables. The code calls newEnvironment.ContainsKey(currentVar), but this check uses the comparer of the newEnvironment dictionary, which may differ from the _environmentVariables dictionary's comparer.
For example:
_environmentVariablesusesCommunicationsUtilities.EnvironmentVariableComparer(OrdinalIgnoreCase)- If
newEnvironmentis passed with a case-sensitive comparer and contains "msbuildtest" - But
_environmentVariablescontains "MSBUILDTEST" - Then
newEnvironment.ContainsKey("MSBUILDTEST")returns false (case mismatch) - This incorrectly triggers the "variable removed" validation even though the variable is present
The fix is to use _environmentVariables.Comparer.Equals(currentVar, key) when checking if a key exists in newEnvironment, or to ensure both dictionaries use compatible comparers.
| EnvironmentVariablesNames.ProgramW6432 | ||
| ], FrameworkFileUtilities.EnvironmentVariableComparer); | ||
|
|
||
| // On case-sensitive systems, both "MSBUILD" and "MSBuild" prefixes are used |
There was a problem hiding this comment.
The comment "On case-sensitive systems, both 'MSBUILD' and 'MSBuild' prefixes are used" is misleading. The HashSet deduplication happens on all systems, but has different results:
- On Windows (case-insensitive comparer): HashSet keeps only one entry since "MSBUILD" and "MSBuild" are considered equal
- On Unix/Linux (case-sensitive comparer): HashSet keeps both entries since they are distinct
The comment implies this behavior is specific to case-sensitive systems, when in fact the deduplication logic works on all systems. Consider revising to: "The HashSet deduplication ensures both 'MSBUILD' and 'MSBuild' prefixes are available on case-sensitive systems, while on case-insensitive systems they are treated as equivalent."
| // On case-sensitive systems, both "MSBUILD" and "MSBuild" prefixes are used | |
| // The HashSet deduplication ensures both "MSBUILD" and "MSBuild" prefixes are available on case-sensitive systems, | |
| // while on case-insensitive systems they are treated as equivalent. |
51fd1f1 to
9a496bd
Compare
| /// </summary> | ||
| private EnvironmentVariableClassifier() | ||
| { | ||
| _immutableVariables = FrozenSet.ToFrozenSet([ |
There was a problem hiding this comment.
why just some of the ones that are in the class?
There was a problem hiding this comment.
I guess it was answered in #13273 (comment) comment
There was a problem hiding this comment.
This is the place that solely define the set of immutable variables.
There was a problem hiding this comment.
how to decide which ones are immutable and which ones mutable?
There was a problem hiding this comment.
e.g. are and should the DOTNET_* be mutable?
There was a problem hiding this comment.
I for time being added ones for which i am sure that they should be immutable. This set might and should extend. For adding DOTNET we would need to make sure there is no task that legitimately sets it in SDK repo or any other dotnet related repos.
There was a problem hiding this comment.
can you write that up as an issue?
There was a problem hiding this comment.
This is a case where we should consider being as restrictive as we can be in v1, since we can't change it later.
| /// The string comparison to use for environment variable name comparisons, based on OS environment variable handling. | ||
| /// Windows: case-insensitive, Unix/Linux: case-sensitive. | ||
| /// </summary> | ||
| internal static readonly StringComparison EnvironmentVariableComparison = NativeMethods.IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; |
There was a problem hiding this comment.
this is just moving from shared fileutilities to framework?
| /// in multithreaded build mode. This escape hatch should only be used for compatibility with tasks that need to | ||
| /// modify these variables and understand the risks of doing so in a concurrent build environment. | ||
| /// </summary> | ||
| public readonly bool DisableImmutableEnvironmentVariableCheck = Environment.GetEnvironmentVariable("MSBUILDDISABLEIMMUTABLEENVIRONMENTVARIABLECHECK") == "1"; |
9a496bd to
0d46746
Compare
|
@rainersigwald and I synced, the question was raised whether we actually need these checks. In multi‑process MSBuild, if a task mutates environment variables that the build engine or static cache relies on, the change can have different effects:
In multi-threaded MSBuild, if a task changes environment variables via the TaskEnvironment API, it will not affect the build engine’s behavior - the engine doesn’t use TaskEnvironment nor the static caches, as the process environment variables always would be used to populate those. So, there is a behavioral difference between the two modes, and it is inevitable. We cannot, and should not, make the build engine rely on the environment variables set by tasks, since different threads could set different values. The old behavior is inherently timing‑dependent and potentially inconsistent, depending on when projects or tasks run. The only real benefit I see from these checks is debuggability. If a solution relies on this inconsistent behavior (e.g., setting an environment variable and expecting the engine or a static cache to observe it), then with /mt the variable would be silently ignored rather than triggering an error, making such issues harder to detect. |
|
Ok, discussed on the sync, we would like to warn the customers that by modifying the environment variable they are relying on the inconsistent behavior (that their env variable is set before the static caches are populated). Main points:
|
### Context Enlighten` GetFrameworkPath` and `GetFrameworkSdkPath` tasks. ### Changes Made - `GetFrameworkPath` and `GetFrameworkSdkPath` depend on the `ToolLocationHelper` and `FrameworkLocationHelper` that uses environment variables to compute the locations of tools and frameworks. They then store it in process-wide cache. GetFrameworkPath should be safe as used environment variables are not supposed to be modified during the build by engine or tasks. To be on the safe side, merge after #13273 that ensures that those variables does not change. - added locks to `GetFrameworkSdkPath` to make it thread-safe. ### Testing Unit tests ### Notes merge after #13273 We also assume that environment variables that supposed to contain path to objects, like "COMPLUS_INSTALLROOT" is absolute. This was an implicit assumption prior this change as well. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
@AR-May do I get the discussion correctly that this should be reworked? Please draft or close the pr in that case |
Context
Some MSBuild tasks implicitly rely on environment variables remaining immutable across multiple projects builds and cache this state in-process. We would like to disallow to modify certain set of environment variables in the tasks. We can easily check that in the multithreaded mode through task environment. This will help use easier enlighten msbuild tasks that depend on immutable state and cache this state in process. These checks would work only in multithreaded mode.
Changes Made
Testing
unit tests + manual tests (in process)