Purpose
Parking a detailed cross-repository audit for later triage. This is not asserting that every item needs an immediate fix; it separates proven behavioral defects from design risks, documentation drift, and missing coverage.
The audit was independently repeated with GPT-5.6 Sol and Claude Opus 4.8, then disagreements were resolved against the source.
Audited revisions:
Executive summary
Verdict: partially coherent.
The MSBuild-side transport is reasonably coherent: one compatible client at a time talks to a reusable entry-node process keyed by an MSBuild handshake; requests carry arguments, environment, working directory, culture, console capabilities, telemetry, and output is returned over packets.
The cross-layer product contract is not coherent:
- The SDK presents global controls such as
--disable-build-servers and dotnet build-server shutdown.
- MSBuild implements a family of narrowly keyed, opportunistic, single-client processes.
- Request-scoped intent such as disabled, persistent, ephemeral,
/mt, GC profile, and interactive behavior is not represented in server identity.
- Shutdown cannot enumerate the fleet that the SDK wording implies.
- Several process-lifetime states are not reset between requests.
The central design mismatch is:
Request-scoped intent is layered onto a launch-scoped server identity.
That mismatch directly causes the short-lived-server reuse bug, sticky GC behavior, opt-out disagreement, shutdown scope problems, and interactive input ambiguity.
Actual implementation contract
Enablement
- The SDK enables the server by default by injecting
MSBUILDUSESERVER=1 when the user has not already set it:
MSBuildForwardingAppWithoutLogging.cs#L20-L25,
#L101-L107.
- Standalone MSBuild enables it only for
MSBUILDUSESERVER=1, or implicitly for /mt when the variable is unset/empty. Any other non-empty value opts out:
XMake.cs#L515-L558.
/nodeReuse:false normally disqualifies the server. /mt is a special case: it still uses a server but asks it to terminate after the build:
XMake.cs#L452-L482.
Identity and compatibility
The server pipe is MSBuildServer-{handshake hash}:
OutOfProcServerNode.cs#L180-L187.
The key includes handshake options, tools-root-derived salt, and full MSBuild file version:
ServerNodeHandshake.cs#L20-L54,
Handshake.cs#L85-L125.
Architecture and Windows elevation are represented:
CommunicationsUtilities.cs#L625-L681.
The key does not represent:
- persistent versus ephemeral intent;
/mt;
- GC profile;
- interactive/input requirements;
- Windows session ID (
ServerNodeHandshake explicitly passes includeSessionId: false);
- a discoverable instance identifier.
Lifetime and concurrency
Request boundary
The request carries command-line arguments, startup directory, complete environment, culture/UI culture, console configuration, telemetry, and ShutdownAfterBuild:
ServerNodeBuildCommand.cs#L16-L112.
There is no stdin channel. stdout/stderr are transported with ServerNodeConsoleWrite:
ServerNodeConsoleWrite.cs#L6-L47.
Findings
1. High: a "short-lived" /mt /nodeReuse:false request can reuse and terminate a resident server
Classification: proven behavioral defect.
shutdownServerAfterBuild is computed as request state:
XMake.cs#L452-L482.
However, the client first checks for and connects to any existing server with the same handshake:
MSBuildClient.cs#L210-L238.
ShutdownAfterBuild is only included in the later build-command packet:
MSBuildClient.cs#L601-L642.
The server then exits after that build:
OutOfProcServerNode.cs#L461-L485.
Consequences
- A no-reuse request can consume process/static/cache state from a persistent server.
- It then destroys that server, affecting subsequent commands that expected it to remain warm.
- SDK
--disable-build-servers does not provide its documented "ignore persistent servers" behavior when /mt is enabled.
- The documentation explicitly calls this a "fresh process," which is not guaranteed:
MSBuild-Server.md#L14-L22.
Repro sketch
- Enable/warm the persistent server with a normal build.
- Run the same MSBuild installation and handshake with
/mt /nr:false.
- Observe the second build execute in the first server PID.
- Observe that PID exit afterward.
Coverage hole
The short-lived test explicitly shuts down the existing server before running, so it only proves the cold-start case:
MSBuildServer_Tests.cs#L201-L222.
Suggested direction
Ephemeral requests need a separate identity:
- unique pipe/nonce per request; or
- an ephemeral bit in the handshake plus a guarantee that it never attaches to persistent instances.
2. High: dotnet build-server shutdown --msbuild reports success after MSBuild reports failure
Classification: proven cross-layer defect.
MSBuild shutdown returns false when:
- the server is busy;
- connection fails;
- the shutdown packet cannot be sent;
- the final protocol result is not successful.
Evidence:
MSBuildClient.cs#L270-L321.
BuildManager.ShutdownAllNodes() discards that boolean:
BuildManager.cs#L1491-L1500.
The SDK abstraction is void:
MSBuildServer.cs#L10-L19.
The command treats any non-faulted task as success:
BuildServerShutdownCommand.cs#L51-L80.
Repro sketch
- Keep a matching server busy with a long-running build.
- Run
dotnet build-server shutdown --msbuild.
TryShutdownServer returns false after the 250 ms busy retry.
- The SDK prints success and exits 0.
- The server remains alive.
Suggested direction
Expose a typed result, for example:
Stopped
NotFound
Busy
ConnectionFailed
ProtocolFailed
TimedOut
The SDK should return nonzero when a selected instance remains running.
3. High: SDK shutdown only addresses one handshake while presenting broad/global semantics
Classification: proven product-contract defect.
The SDK provider emits one synthetic controller and does not enumerate real MSBuild instances:
BuildServerProvider.cs#L23-L30.
Shutdown constructs the current MSBuild client's one handshake:
MSBuildClient.cs#L278-L283,
#L639-L642.
The following independently create different identities:
- MSBuild file version;
- tools root / SDK installation path;
- architecture;
- Windows elevation;
MSBUILDNODEHANDSHAKESALT.
Therefore a command using SDK B cannot generally shut down a server launched by SDK A. Running the command from a differently selected global.json, dotnet root, architecture, elevation, or test salt can return success while the other server remains.
The SDK documentation says:
Shuts down build servers that are started from dotnet. By default, all servers are shut down.
dotnet-build-server.1#L36-L55
Suggested direction
Use discoverable per-user instance descriptors containing at least:
- PID;
- pipe;
- protocol/version;
- tools root;
- architecture;
- elevation;
- session/user scope;
- persistent/ephemeral mode;
- owned child processes;
- current state.
Then define whether shutdown means "current compatible server" or "all current-user MSBuild servers."
4. High: DOTNET_CLI_USE_MSBUILD_SERVER=false is not an authoritative opt-out for /mt
Classification: proven cross-layer defect.
When DOTNET_CLI_USE_MSBUILD_SERVER is false, the SDK merely declines to set MSBUILDUSESERVER=1:
MSBuildForwardingAppWithoutLogging.cs#L20-L25,
#L101-L107.
MSBuild interprets unset/empty plus /mt as implicit server enablement:
XMake.cs#L531-L558.
The decision table tests this behavior:
XMake_Tests.cs#L3280-L3300.
Repro sketch
DOTNET_CLI_USE_MSBUILD_SERVER=0 dotnet msbuild project.proj -mt
With no explicit MSBUILDUSESERVER, the server is still selected.
This can also occur without a visible /mt argument through MSBUILDFORCEMULTITHREADED=1 or response files.
Suggested direction
When the SDK opt-out is false and the user has not explicitly set MSBUILDUSESERVER, inject:
Define precedence when both variables are explicitly present.
5. High: interactive builds are server-eligible, but the protocol has no stdin
Classification: proven unsupported-behavior gap; real-world prevalence requires research.
Interactive mode is not one of the server-incompatible switches:
XMake.cs#L441-L450.
There is an integration test requiring the server to start for -interactive, but it verifies only the PID/lifetime:
MSBuildServer_Tests.cs#L425-L443.
The request protocol has no stdin packet:
ServerNodeBuildCommand.cs#L16-L112.
Only stdout/stderr are returned:
ServerNodeConsoleWrite.cs#L6-L47.
For a reused server, even inheriting the original launcher's stdin could not bind it to the current client terminal.
Possible directions
- Make interactive builds server-ineligible until input is supported.
- Define a bidirectional input protocol with cancellation and secret-input behavior.
- Explicitly document that "interactive" only affects MSBuild/NuGet policy and does not promise
Console.In.
Real-world task/logger usage of Console.Read* will be researched and added as a follow-up comment.
6. Medium: GC mode is sticky from the first cold launch
Classification: proven lifecycle/performance defect.
Server GC is injected only while launching a new /mt server:
MSBuildClient.cs#L543-L568.
GC mode is absent from the handshake:
MSBuildClient.cs#L639-L642,
ServerNodeHandshake.cs#L25-L34.
Therefore:
- normal build then
/mt: /mt may use Workstation GC;
/mt then normal build: the normal build keeps Server GC.
Existing tests isolate each case with a fresh handshake and never test transitions:
MSBuildServer_Tests.cs#L650-L706.
The documentation also says a non-/mt server "only orchestrates," but normal scheduling prefers the in-process node:
NodeManager.cs#L91-L105.
The standard server test's task executes in the server PID:
MSBuildServer_Tests.cs#L101-L117.
Suggested direction
Either:
- put GC profile in server identity; or
- choose one invariant server GC policy and document it.
7. Medium: reusable TaskHost sidecars are omitted from public shutdown
Classification: proven lifecycle gap.
Normal tidy build completion allows TaskHost reuse:
BuildManager.cs#L2421-L2437.
Public BuildManager.ShutdownAllNodes() shuts:
- the MSBuild Server;
- the regular
NodeManager.
It does not invoke TaskHostNodeManager:
BuildManager.cs#L1491-L1500.
TaskHost shutdown exists separately:
TaskHostNodeManager.cs#L62-L77.
Impact
dotnet build-server shutdown --msbuild can leave nodemode-2 processes, loaded custom-task assemblies, and file handles alive until their own timeout.
Suggested direction
Define whether these are owned children of the server. If so, register and stop them as part of server shutdown.
8. Medium: concrete process state leaks between server requests
Classification: proven internal defects exposed by persistence.
Deferred logger messages accumulate
ResetBuildState() only resets parser state:
XMake.cs#L1498-L1505.
The static deferred-message list is retained:
XMake.cs#L1525-L1529.
Terminal-logger/environment decisions append messages every invocation:
XMake.cs#L2764-L2795,
#L2861-L2882.
Later builds copy the complete retained list:
XMake.cs#L2020-L2055.
This can create stale/duplicate diagnostics and unbounded list growth during server reuse.
Process priority is not restored
/lowPriority lowers the current server process:
XMake.cs#L2462-L2484.
A later normal request initializes its local lowPriority flag to false but never restores the server process to normal priority.
Suggested direction
Create an explicit per-request reset boundary covering all non-cache mutable state. Cache retention should be deliberate and separately enumerated.
This aligns with the still-open broader static-lifetime problem:
dotnet#12246
9. Low: cold-start launch coordination releases the mutex before launching
Classification: proven efficiency/race defect.
The launch mutex scope ends before process creation:
MSBuildClient.cs#L500-L525.
The process is launched afterward:
MSBuildClient.cs#L525-L578.
The server-side running mutex later ensures one process survives:
OutOfProcServerNode.cs#L110-L123.
This preserves eventual safety but permits duplicate cold launches, churn, and avoidable fallback under contention.
10. Medium/low: identity and session partitioning are underspecified
Classification: proven same-user cross-session coupling; cross-user impact is an OS-security design risk, not a proven exploit.
ServerNodeHandshake excludes Windows session ID:
ServerNodeHandshake.cs#L20-L22.
The base handshake otherwise documents session separation for RDP:
Handshake.cs#L111-L121.
Pipes are current-user secured:
NodeEndpointOutOfProcBase.cs#L246-L285.
Mutex names are machine-global and do not contain a user/session discriminator:
OutOfProcServerNode.cs#L180-L187.
The guaranteed behavior is that two Windows sessions for the same user share one server identity. Cross-user mutex interference depends on platform/default DACL behavior and needs a dedicated test before treating it as a security bug.
11. Documentation and command-surface drift
Wrong environment variable
MSBuild documentation says to use the nonexistent DOTNET_CLI_DO_NOT_USE_MSBUILD_SERVER:
MSBuild-Server.md#L5-L8.
The SDK implements DOTNET_CLI_USE_MSBUILD_SERVER; MSBuild implements MSBUILDUSESERVER.
Misleading "build from scratch" claim
SDK documentation claims --disable-build-servers disables all caching and forces a build from scratch:
dotnet-build.1#L153-L159.
The option only forwards:
UseRazorBuildServer=false
UseSharedCompilation=false
/nodeReuse:false
CommonOptions.cs#L265-L271.
It does not clean outputs or disable ordinary MSBuild incremental behavior, NuGet caches, project-system caches, or every MSBuild invocation surface.
Incomplete command coverage
Some commands that invoke MSBuildForwardingApp, including package add/list and some Microsoft.Testing.Platform paths, do not expose the same per-command server-disable surface.
Stale "MSBuild Server V1" terminology
The SDK environment-variable documentation describes DOTNET_CLI_USE_MSBUILDNOINPROCNODE / MSBUILDNOINPROCNODE as "MSBuild Server V1":
dotnet-environment-variables.7#L533-L539.
This is a different worker-node behavior and makes the term "MSBuild Server" ambiguous.
Missing tests
Highest-value additions:
- Warm persistent server followed by
/mt /nr:false: assert a different PID, no warm-state reuse, and survival of the resident server.
- Busy real server plus
dotnet build-server shutdown --msbuild: assert nonzero exit and no success message.
- Two SDK versions/tools roots/architectures/elevation states/salts: verify explicitly defined current/all shutdown semantics.
DOTNET_CLI_USE_MSBUILD_SERVER=0 plus /mt and MSBUILDFORCEMULTITHREADED=1.
- GC transition matrix: normal ->
/mt, /mt -> normal.
- An interactive task that actually reads stdin, rather than checking only PID.
- Server plus reusable TaskHost shutdown: assert all owned processes exit.
- Two-request reset matrix for deferred diagnostics, process priority, telemetry, handlers, environment-derived traits, and retained caches.
- Barrier-controlled concurrent cold launch.
- Same-user multi-session and multi-user identity/security tests.
- SDK command-schema/E2E matrix across every
MSBuildForwardingApp consumer.
The existing SDK shutdown tests mock IBuildServer and therefore cannot detect the discarded false result:
BuildServerShutdownCommandTests.cs#L101-L158.
The MSBuild shutdown integration test is currently quarantined on Windows:
MSBuildServer_Tests.cs#L370-L403.
Proposed coherent target design
Explicit mode
Represent server intent as an enum rather than independent side effects:
Disabled
Persistent
EphemeralForMt
Explicit false must always beat implicit /mt enablement.
Typed identity
Identity should deliberately account for:
- tools path/version and protocol;
- runtime host;
- architecture;
- elevation;
- user/session;
- GC profile;
- persistent/ephemeral reuse class.
Ephemeral isolation
Ephemeral requests must never attach to or terminate persistent instances.
Registry-based discovery
Maintain per-user descriptors for real instance enumeration and reliable shutdown.
Typed shutdown
Return real results instead of collapsing everything to non-throwing success.
Owned child lifecycle
Track and shut down reusable worker and TaskHost children consistently.
Interactive contract
Either implement bidirectional stdin or make interactive invocations ineligible.
Per-build reset contract
Reset all non-cache mutable process state. Explicitly inventory which caches are intentionally retained and how they are invalidated.
Suggested triage/fix order
- Preserve shutdown failures and stop printing false success.
- Separate ephemeral and persistent server identities.
- Make SDK/direct-MSBuild opt-outs authoritative.
- Add real server discovery and define shutdown scope.
- Include TaskHosts/owned children in lifecycle management.
- Disable server for interactive builds pending stdin support.
- Fix concrete per-request state leaks and launch locking.
- Align command surfaces, documentation, and the test matrix.
Important non-findings / qualifications
- The process environment is replaced between builds; claiming that environment variables themselves simply leak from request A into request B is overstated. Static task/process state remains the broader risk.
- A busy server falling back rather than queueing is current policy, not itself a defect.
- Connection failure after dispatch must not blindly rerun the build, because doing so could duplicate side effects.
MSBuildClientExitType.Success represents protocol completion, not necessarily a successful build result.
- Cross-user pipe hijacking is not established: pipe security is current-user-based. The mutex/user namespace question needs platform tests rather than speculation.
- Servers are not immortal: the default idle connection timeout is 15 minutes.
Open research item
The source proves that interactive builds have no stdin transport. A follow-up comment will investigate whether real public MSBuild tasks/loggers actually use Console.Read, Console.ReadLine, Console.ReadKey, Console.In, or direct standard-input APIs, and whether those usages run on the central/server node.
Purpose
Parking a detailed cross-repository audit for later triage. This is not asserting that every item needs an immediate fix; it separates proven behavioral defects from design risks, documentation drift, and missing coverage.
The audit was independently repeated with GPT-5.6 Sol and Claude Opus 4.8, then disagreements were resolved against the source.
Audited revisions:
93022af5849a9c3023d40cf7a97f54d15f43a2f6de83a9ce2ac64704b5b0ef5b175dfcaec0f0d08cExecutive summary
Verdict: partially coherent.
The MSBuild-side transport is reasonably coherent: one compatible client at a time talks to a reusable entry-node process keyed by an MSBuild handshake; requests carry arguments, environment, working directory, culture, console capabilities, telemetry, and output is returned over packets.
The cross-layer product contract is not coherent:
--disable-build-serversanddotnet build-server shutdown./mt, GC profile, and interactive behavior is not represented in server identity.The central design mismatch is:
That mismatch directly causes the short-lived-server reuse bug, sticky GC behavior, opt-out disagreement, shutdown scope problems, and interactive input ambiguity.
Actual implementation contract
Enablement
MSBUILDUSESERVER=1when the user has not already set it:MSBuildForwardingAppWithoutLogging.cs#L20-L25,#L101-L107.MSBUILDUSESERVER=1, or implicitly for/mtwhen the variable is unset/empty. Any other non-empty value opts out:XMake.cs#L515-L558./nodeReuse:falsenormally disqualifies the server./mtis a special case: it still uses a server but asks it to terminate after the build:XMake.cs#L452-L482.Identity and compatibility
The server pipe is
MSBuildServer-{handshake hash}:OutOfProcServerNode.cs#L180-L187.The key includes handshake options, tools-root-derived salt, and full MSBuild file version:
ServerNodeHandshake.cs#L20-L54,Handshake.cs#L85-L125.Architecture and Windows elevation are represented:
CommunicationsUtilities.cs#L625-L681.The key does not represent:
/mt;ServerNodeHandshakeexplicitly passesincludeSessionId: false);Lifetime and concurrency
NodeEndpointOutOfProcBase.cs#L246-L285.MSBuildClient.cs#L225-L238.MSBUILDNODECONNECTIONTIMEOUT, 15 minutes by default, before terminating:CommunicationsUtilities.cs#L42-L78,NodeEndpointOutOfProcBase.cs#L380-L408.Request boundary
The request carries command-line arguments, startup directory, complete environment, culture/UI culture, console configuration, telemetry, and
ShutdownAfterBuild:ServerNodeBuildCommand.cs#L16-L112.There is no stdin channel. stdout/stderr are transported with
ServerNodeConsoleWrite:ServerNodeConsoleWrite.cs#L6-L47.Findings
1. High: a "short-lived"
/mt /nodeReuse:falserequest can reuse and terminate a resident serverClassification: proven behavioral defect.
shutdownServerAfterBuildis computed as request state:XMake.cs#L452-L482.However, the client first checks for and connects to any existing server with the same handshake:
MSBuildClient.cs#L210-L238.ShutdownAfterBuildis only included in the later build-command packet:MSBuildClient.cs#L601-L642.The server then exits after that build:
OutOfProcServerNode.cs#L461-L485.Consequences
--disable-build-serversdoes not provide its documented "ignore persistent servers" behavior when/mtis enabled.MSBuild-Server.md#L14-L22.Repro sketch
/mt /nr:false.Coverage hole
The short-lived test explicitly shuts down the existing server before running, so it only proves the cold-start case:
MSBuildServer_Tests.cs#L201-L222.Suggested direction
Ephemeral requests need a separate identity:
2. High:
dotnet build-server shutdown --msbuildreports success after MSBuild reports failureClassification: proven cross-layer defect.
MSBuild shutdown returns
falsewhen:Evidence:
MSBuildClient.cs#L270-L321.BuildManager.ShutdownAllNodes()discards that boolean:BuildManager.cs#L1491-L1500.The SDK abstraction is
void:MSBuildServer.cs#L10-L19.The command treats any non-faulted task as success:
BuildServerShutdownCommand.cs#L51-L80.Repro sketch
dotnet build-server shutdown --msbuild.TryShutdownServerreturns false after the 250 ms busy retry.Suggested direction
Expose a typed result, for example:
StoppedNotFoundBusyConnectionFailedProtocolFailedTimedOutThe SDK should return nonzero when a selected instance remains running.
3. High: SDK shutdown only addresses one handshake while presenting broad/global semantics
Classification: proven product-contract defect.
The SDK provider emits one synthetic controller and does not enumerate real MSBuild instances:
BuildServerProvider.cs#L23-L30.Shutdown constructs the current MSBuild client's one handshake:
MSBuildClient.cs#L278-L283,#L639-L642.The following independently create different identities:
MSBUILDNODEHANDSHAKESALT.Therefore a command using SDK B cannot generally shut down a server launched by SDK A. Running the command from a differently selected
global.json, dotnet root, architecture, elevation, or test salt can return success while the other server remains.The SDK documentation says:
dotnet-build-server.1#L36-L55Suggested direction
Use discoverable per-user instance descriptors containing at least:
Then define whether shutdown means "current compatible server" or "all current-user MSBuild servers."
4. High:
DOTNET_CLI_USE_MSBUILD_SERVER=falseis not an authoritative opt-out for/mtClassification: proven cross-layer defect.
When
DOTNET_CLI_USE_MSBUILD_SERVERis false, the SDK merely declines to setMSBUILDUSESERVER=1:MSBuildForwardingAppWithoutLogging.cs#L20-L25,#L101-L107.MSBuild interprets unset/empty plus
/mtas implicit server enablement:XMake.cs#L531-L558.The decision table tests this behavior:
XMake_Tests.cs#L3280-L3300.Repro sketch
With no explicit
MSBUILDUSESERVER, the server is still selected.This can also occur without a visible
/mtargument throughMSBUILDFORCEMULTITHREADED=1or response files.Suggested direction
When the SDK opt-out is false and the user has not explicitly set
MSBUILDUSESERVER, inject:Define precedence when both variables are explicitly present.
5. High: interactive builds are server-eligible, but the protocol has no stdin
Classification: proven unsupported-behavior gap; real-world prevalence requires research.
Interactive mode is not one of the server-incompatible switches:
XMake.cs#L441-L450.There is an integration test requiring the server to start for
-interactive, but it verifies only the PID/lifetime:MSBuildServer_Tests.cs#L425-L443.The request protocol has no stdin packet:
ServerNodeBuildCommand.cs#L16-L112.Only stdout/stderr are returned:
ServerNodeConsoleWrite.cs#L6-L47.For a reused server, even inheriting the original launcher's stdin could not bind it to the current client terminal.
Possible directions
Console.In.Real-world task/logger usage of
Console.Read*will be researched and added as a follow-up comment.6. Medium: GC mode is sticky from the first cold launch
Classification: proven lifecycle/performance defect.
Server GC is injected only while launching a new
/mtserver:MSBuildClient.cs#L543-L568.GC mode is absent from the handshake:
MSBuildClient.cs#L639-L642,ServerNodeHandshake.cs#L25-L34.Therefore:
/mt:/mtmay use Workstation GC;/mtthen normal build: the normal build keeps Server GC.Existing tests isolate each case with a fresh handshake and never test transitions:
MSBuildServer_Tests.cs#L650-L706.The documentation also says a non-
/mtserver "only orchestrates," but normal scheduling prefers the in-process node:NodeManager.cs#L91-L105.The standard server test's task executes in the server PID:
MSBuildServer_Tests.cs#L101-L117.Suggested direction
Either:
7. Medium: reusable TaskHost sidecars are omitted from public shutdown
Classification: proven lifecycle gap.
Normal tidy build completion allows TaskHost reuse:
BuildManager.cs#L2421-L2437.Public
BuildManager.ShutdownAllNodes()shuts:NodeManager.It does not invoke
TaskHostNodeManager:BuildManager.cs#L1491-L1500.TaskHost shutdown exists separately:
TaskHostNodeManager.cs#L62-L77.Impact
dotnet build-server shutdown --msbuildcan leave nodemode-2 processes, loaded custom-task assemblies, and file handles alive until their own timeout.Suggested direction
Define whether these are owned children of the server. If so, register and stop them as part of server shutdown.
8. Medium: concrete process state leaks between server requests
Classification: proven internal defects exposed by persistence.
Deferred logger messages accumulate
ResetBuildState()only resets parser state:XMake.cs#L1498-L1505.The static deferred-message list is retained:
XMake.cs#L1525-L1529.Terminal-logger/environment decisions append messages every invocation:
XMake.cs#L2764-L2795,#L2861-L2882.Later builds copy the complete retained list:
XMake.cs#L2020-L2055.This can create stale/duplicate diagnostics and unbounded list growth during server reuse.
Process priority is not restored
/lowPrioritylowers the current server process:XMake.cs#L2462-L2484.A later normal request initializes its local
lowPriorityflag to false but never restores the server process to normal priority.Suggested direction
Create an explicit per-request reset boundary covering all non-cache mutable state. Cache retention should be deliberate and separately enumerated.
This aligns with the still-open broader static-lifetime problem:
dotnet#12246
9. Low: cold-start launch coordination releases the mutex before launching
Classification: proven efficiency/race defect.
The launch mutex scope ends before process creation:
MSBuildClient.cs#L500-L525.The process is launched afterward:
MSBuildClient.cs#L525-L578.The server-side running mutex later ensures one process survives:
OutOfProcServerNode.cs#L110-L123.This preserves eventual safety but permits duplicate cold launches, churn, and avoidable fallback under contention.
10. Medium/low: identity and session partitioning are underspecified
Classification: proven same-user cross-session coupling; cross-user impact is an OS-security design risk, not a proven exploit.
ServerNodeHandshakeexcludes Windows session ID:ServerNodeHandshake.cs#L20-L22.The base handshake otherwise documents session separation for RDP:
Handshake.cs#L111-L121.Pipes are current-user secured:
NodeEndpointOutOfProcBase.cs#L246-L285.Mutex names are machine-global and do not contain a user/session discriminator:
OutOfProcServerNode.cs#L180-L187.The guaranteed behavior is that two Windows sessions for the same user share one server identity. Cross-user mutex interference depends on platform/default DACL behavior and needs a dedicated test before treating it as a security bug.
11. Documentation and command-surface drift
Wrong environment variable
MSBuild documentation says to use the nonexistent
DOTNET_CLI_DO_NOT_USE_MSBUILD_SERVER:MSBuild-Server.md#L5-L8.The SDK implements
DOTNET_CLI_USE_MSBUILD_SERVER; MSBuild implementsMSBUILDUSESERVER.Misleading "build from scratch" claim
SDK documentation claims
--disable-build-serversdisables all caching and forces a build from scratch:dotnet-build.1#L153-L159.The option only forwards:
UseRazorBuildServer=falseUseSharedCompilation=false/nodeReuse:falseCommonOptions.cs#L265-L271.It does not clean outputs or disable ordinary MSBuild incremental behavior, NuGet caches, project-system caches, or every MSBuild invocation surface.
Incomplete command coverage
Some commands that invoke
MSBuildForwardingApp, including package add/list and some Microsoft.Testing.Platform paths, do not expose the same per-command server-disable surface.Stale "MSBuild Server V1" terminology
The SDK environment-variable documentation describes
DOTNET_CLI_USE_MSBUILDNOINPROCNODE/MSBUILDNOINPROCNODEas "MSBuild Server V1":dotnet-environment-variables.7#L533-L539.This is a different worker-node behavior and makes the term "MSBuild Server" ambiguous.
Missing tests
Highest-value additions:
/mt /nr:false: assert a different PID, no warm-state reuse, and survival of the resident server.dotnet build-server shutdown --msbuild: assert nonzero exit and no success message.DOTNET_CLI_USE_MSBUILD_SERVER=0plus/mtandMSBUILDFORCEMULTITHREADED=1./mt,/mt-> normal.MSBuildForwardingAppconsumer.The existing SDK shutdown tests mock
IBuildServerand therefore cannot detect the discarded false result:BuildServerShutdownCommandTests.cs#L101-L158.The MSBuild shutdown integration test is currently quarantined on Windows:
MSBuildServer_Tests.cs#L370-L403.Proposed coherent target design
Explicit mode
Represent server intent as an enum rather than independent side effects:
Explicit false must always beat implicit
/mtenablement.Typed identity
Identity should deliberately account for:
Ephemeral isolation
Ephemeral requests must never attach to or terminate persistent instances.
Registry-based discovery
Maintain per-user descriptors for real instance enumeration and reliable shutdown.
Typed shutdown
Return real results instead of collapsing everything to non-throwing success.
Owned child lifecycle
Track and shut down reusable worker and TaskHost children consistently.
Interactive contract
Either implement bidirectional stdin or make interactive invocations ineligible.
Per-build reset contract
Reset all non-cache mutable process state. Explicitly inventory which caches are intentionally retained and how they are invalidated.
Suggested triage/fix order
Important non-findings / qualifications
MSBuildClientExitType.Successrepresents protocol completion, not necessarily a successful build result.Open research item
The source proves that interactive builds have no stdin transport. A follow-up comment will investigate whether real public MSBuild tasks/loggers actually use
Console.Read,Console.ReadLine,Console.ReadKey,Console.In, or direct standard-input APIs, and whether those usages run on the central/server node.