Nov 11, 2026·9 min read·1 visit
A TOCTOU race condition (CWE-367) in the Microsoft .NET dotnet-watch utility on Linux and macOS allows local attackers to overwrite temporary .slnx solution files in /tmp before MSBuild processes them, leading to local privilege escalation.
CVE-2026-114155 is a local elevation of privilege vulnerability affecting Microsoft .NET on Linux and macOS platforms. The vulnerability exists within the dotnet-watch (Hot Reload) component and is caused by a Time-of-Check Time-of-Use (TOCTOU) race condition (CWE-367) during the generation and execution of temporary XML solution files (.slnx) in shared system temporary directories. A local attacker with low privileges can intercept and modify the temporary solution file before MSBuild evaluates it, achieving arbitrary code execution in the security context of the user running dotnet-watch.
dotnet-watch is a file watcher and development tool bundled with the Microsoft .NET SDK that automatically recompiles and reloads applications when source code changes are detected. When monitoring multiple projects simultaneously under Hot Reload, dotnet-watch aggregates the target project references into an on-the-fly XML solution file (.slnx) and invokes MSBuild to evaluate and build the solution.
In affected versions of .NET 10.0 and .NET 11.0, the HotReloadDotNetWatcher component generates these temporary .slnx files directly within the default system temporary directory (/tmp on Unix-like operating systems). Because /tmp is a shared, world-writable namespace on Linux and macOS, creating files in this directory without strict permission enforcement or exclusive file locking exposes those files to manipulation by other local users.
This behavior introduces a Time-of-Check Time-of-Use (TOCTOU) race condition tracked as CVE-2026-114155 (CWE-367). Between the moment dotnet-watch writes the .slnx file to disk and the moment the child MSBuild process parses it, the file handle is closed and left unlocked. A local unprivileged user can exploit this timing window to replace or modify the .slnx file, injecting malicious MSBuild directives that execute arbitrary commands under the privileges of the developer or automated build service running dotnet-watch.
The root cause of CVE-2026-114155 lies in the insecure use of the Path.GetTempFileName() API combined with premature stream closure in src/sdk/src/Dotnet.Watch/Watch/HotReload/HotReloadDotNetWatcher.cs. When BuildAsync processes a non-empty list of projectPaths, it constructs a path for the temporary solution file using the expression Path.Combine(Path.GetTempFileName() + ".slnx").
When Path.GetTempFileName() executes on POSIX systems, it creates an empty file in /tmp with a unique name (for example, /tmp/tmpa1b2.tmp) using exclusive creation flags (O_CREAT | O_EXCL) and restrictive permissions. However, appending the string ".slnx" to the returned path references a second, distinct file path (/tmp/tmpa1b2.tmp.slnx) that has not yet been created or reserved on the filesystem. Furthermore, the original /tmp/tmpa1b2.tmp file is left behind as an orphaned artifact whose name is immediately visible to any local user listing /tmp.
Once dotnet-watch populates the XDocument object in memory, it writes the XML content to the .slnx path by calling doc.Save(solutionFile). This method opens a file stream, writes the serialized XML, and immediately closes and disposes of the underlying file handle. After doc.Save() returns, dotnet-watch invokes BuildFileOrProjectOrSolutionAsync(solutionFile, ...), which spawns or signals MSBuild to read and evaluate the file from disk.
Because the .slnx file is neither created with O_EXCL inside a private directory nor held open with a write-restricting lock, two distinct attack vectors emerge during the TOCTOU window. First, an attacker watching /tmp via inotify (Linux) or kqueue (macOS) can detect the creation of /tmp/tmpXXXX.tmp, immediately predict the target filename /tmp/tmpXXXX.tmp.slnx, and pre-create the .slnx file or a symbolic link before doc.Save() runs. Second, if the user's umask is permissive, or if the attacker pre-created the file and granted write permissions to the victim, the attacker can overwrite the contents of /tmp/tmpXXXX.tmp.slnx after doc.Save() closes the stream but before MSBuild opens it.
Prior to the patch, HotReloadDotNetWatcher.cs managed the lifecycle of the temporary .slnx file manually using string concatenation and a try/finally deletion block. The vulnerable implementation is shown below:
// Vulnerable implementation in src/sdk/src/Dotnet.Watch/Watch/HotReload/HotReloadDotNetWatcher.cs
else if (projectPaths is not [])
{
// 1. Creates /tmp/tmpXXXX.tmp, but derives unreserved path /tmp/tmpXXXX.tmp.slnx
var solutionFile = Path.Combine(Path.GetTempFileName() + ".slnx");
var solutionElement = new XElement("Solution");
foreach (var projectPath in projectPaths)
{
// populate solution elements
}
var doc = new XDocument(solutionElement);
// 2. Writes file to /tmp and immediately closes the file stream
doc.Save(solutionFile);
try
{
// 3. TOCTOU Window: MSBuild reads the unlocked file by path name
if (!await BuildFileOrProjectOrSolutionAsync(solutionFile, targetFramework, device, action, cancellationToken))
{
return false;
}
}
finally
{
try
{
File.Delete(solutionFile);
}
catch
{
// ignore
}
}
}In commit c46253a634a2725fd34820ad8a36a3f106f5d01d, Microsoft remediated the vulnerability by introducing a dedicated TemporaryFileManager class and refactoring HotReloadDotNetWatcher.cs to write directly to an open, locked FileStream:
// Patched implementation in src/sdk/src/Dotnet.Watch/Watch/HotReload/HotReloadDotNetWatcher.cs
else if (projectPaths is not [])
{
// Keep the file open and allow other processes to read it (but not modify) after the content is written:
using var solutionFile = _context.TemporaryFileManager.Create(".slnx", FileShare.Read);
var solutionElement = new XElement("Solution");
foreach (var projectPath in projectPaths)
{
// populate solution elements
}
var doc = new XDocument(solutionElement);
// Save directly to the open FileStream without closing the handle
doc.Save(solutionFile.Stream);
if (!await BuildFileOrProjectOrSolutionAsync(solutionFile.Stream.Name, targetFramework, device, action, cancellationToken))
{
return false;
}
}The new TemporaryFileManager class eliminates both the shared directory exposure and the unlocked file state:
// New file: src/sdk/src/Dotnet.Watch/Watch/TemporaryFileManager.cs
namespace Microsoft.DotNet.Watch;
internal sealed partial class TemporaryFileManager : IDisposable
{
internal readonly struct TemporaryFile(FileStream stream) : IDisposable
{
public FileStream Stream => stream;
public void Dispose()
{
try { stream.Dispose(); } catch { }
try { File.Delete(stream.Name); } catch { }
}
}
private const string TempDirectoryPrefix = "dotnet-watch-";
// Creates a subdirectory with 0700 (owner-only) permissions on Unix
private readonly string _directory = Directory.CreateTempSubdirectory(TempDirectoryPrefix).FullName;
private int _uniqueFileId;
public void Dispose()
{
try { Directory.Delete(_directory, recursive: true); } catch { }
}
public TemporaryFile Create(string extension, FileShare share)
=> new(new FileStream(
path: Path.Combine(_directory, Interlocked.Increment(ref _uniqueFileId) + extension),
mode: FileMode.CreateNew,
access: FileAccess.ReadWrite,
share: share));
}> [!NOTE]
> Fix Completeness Assessment: The fix is comprehensive on POSIX systems because Directory.CreateTempSubdirectory invokes mkdtemp, which atomically creates a directory with 0700 (rwx------) permissions owned by the effective user ID. Because the parent directory lacks group and other execute/search bits, unprivileged local users cannot traverse into the dotnet-watch-* subdirectory to pre-create, read, or modify files regardless of file locking semantics.
Exploiting CVE-2026-114155 requires local code execution capabilities as a low-privileged user on the same Linux or macOS host where a victim user (such as a senior developer, system administrator, or CI/CD runner) executes dotnet watch across multiple projects. Windows environments are not affected under default configurations because Path.GetTempPath() resolves to a user-isolated directory (%USERPROFILE%\AppData\Local\Temp).
To reliably win the race condition, an attacker sets up a filesystem monitoring loop on /tmp using the Linux inotify API (specifically watching for IN_CREATE events). When the victim runs dotnet watch, Path.GetTempFileName() creates /tmp/tmpXXXXXX.tmp. This immediately triggers an IN_CREATE event before dotnet-watch even begins constructing the XElement tree in memory.
Upon receiving the notification for /tmp/tmpXXXXXX.tmp, the attacker's process immediately creates /tmp/tmpXXXXXX.tmp.slnx with world-writable permissions (0666) before doc.Save(solutionFile) is reached. Because doc.Save(string) opens existing files with FileMode.Create (which truncates rather than failing if the file already exists), dotnet-watch writes the legitimate XML into the attacker-owned file and closes the stream. Immediately after IN_CLOSE_WRITE fires on /tmp/tmpXXXXXX.tmp.slnx, the attacker truncates and overwrites the file with a malicious solution or MSBuild project payload.
Alternatively, the attacker can replace the .slnx reference with a project file path containing an MSBuild <Target> definition that executes an <Exec> task:
<Project Sdk="Microsoft.NET.Sdk">
<Target Name="Build">
<Exec Command="/bin/sh -c 'cp /bin/sh /tmp/rootsh && chmod +s /tmp/rootsh'" />
</Target>
</Project>When BuildFileOrProjectOrSolutionAsync invokes MSBuild on /tmp/tmpXXXXXX.tmp.slnx (or a referenced project path injected into the .slnx file), MSBuild parses the attacker-controlled XML and executes the embedded shell commands under the UID and GID of the victim user.
Successful exploitation of CVE-2026-114155 grants the attacker arbitrary code execution within the security context of the user running dotnet-watch. In multi-user development servers, shared academic or enterprise build hosts, or containerized CI/CD environments where multiple agents share a host /tmp mount, an attacker can cross user boundaries and compromise higher-privileged accounts.
Microsoft assigned CVE-2026-114155 a CVSS v3.1 base score of 7.5 (High) with the vector CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:H. The Scope: Changed (S:C) metric reflects that a vulnerability in the .NET SDK build tool allows an unprivileged local user to escape their OS user boundary and execute code as another user or service account.
Confidentiality, Integrity, and Availability impacts are all rated High (C:H/I:H/A:H). Once code execution is achieved in the victim's context, the attacker can exfiltrate source code, steal cloud credentials, SSH keys, or NuGet API tokens stored in the victim's home directory, modify build artifacts to introduce supply chain backdoors, or terminate the victim's processes.
At the time of publication, there are no known public proof-of-concept exploits in the wild, and the vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog. The Attack Complexity is rated High (AC:H) because exploitation depends on both the victim running dotnet watch in a multi-project configuration and the attacker winning the filesystem race window.
The primary remediation is to update the Microsoft .NET SDK and associated NuGet packages to the patched releases that incorporate the TemporaryFileManager isolation mechanism. Specifically, administrators and developers on Linux and macOS should apply the following updates:
For environments where immediate patching is not possible, system administrators and developers can completely neutralize the vulnerability by configuring a private, per-user temporary directory via the TMPDIR environment variable. Because .NET's Path.GetTempPath() and Path.GetTempFileName() respect TMPDIR on POSIX systems, pointing TMPDIR to a mode 0700 directory inside the user's home directory prevents other local users from accessing or racing temporary files:
mkdir -p -m 0700 "$HOME/.tmp"
export TMPDIR="$HOME/.tmp"In automated build pipelines or shared Linux hosts, administrators can also implement systemd PrivateTmp=yes directives for build services, or enable the Linux kernel fs.protected_regular sysctl parameter (sysctl -w fs.protected_regular=2), which restricts O_CREAT file opens in world-writable sticky directories (/tmp) when the file is owned by another user.
CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Microsoft .NET 10.0 (Linux & macOS) Microsoft | >= 10.0.0, < 10.0.404 | 10.0.404 |
Microsoft .NET 11.0 (Linux & macOS) Microsoft | >= 11.0.0-preview.1, < 11.0.100 | 11.0.100 |
Microsoft.DotNet.HotReload.Watch.Aspire Microsoft | >= 10.0.300, < 11.0.100 | 11.0.100 |
| Attribute | Detail |
|---|---|
| Vulnerability Type | CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition |
| Attack Vector | Local (Shared /tmp directory on Linux and macOS) |
| CVSS v3.1 Score | 7.5 HIGH (CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:H) |
| Affected Component | Microsoft .NET SDK dotnet-watch (HotReloadDotNetWatcher) |
| Security Impact | Local Elevation of Privilege via arbitrary MSBuild code execution |
| Exploit Status | Unproven (No public PoC or active exploitation) |
| CISA KEV Status | Not Listed |
The product checks the state of a resource before using that resource, but the resource's state can change between the check and the use in a way that invalidates the results of the check. This can cause the product to perform invalid actions when the resource is in an unexpected state.
CVE-2026-118748 is a remote Denial of Service (DoS) vulnerability in Microsoft ASP.NET Core (.NET 11.0 preview releases prior to 11.0.0) and Microsoft Visual Studio 2026 (prior to 18.12.0). The flaw originates from unbounded resource allocation (CWE-770) within the Blazor static server-side rendering (static-SSR) validation pipeline, where dynamic or attacker-supplied form field identifiers are cached in a singleton-scoped memory structure without size limits or eviction policies. Unauthenticated remote attackers can exploit this condition over the network by sending repeated HTTP requests with unique field names, exhausting heap memory and terminating the host process via an unhandled OutOfMemoryException.
CVE-2026-114150 is a network-accessible information disclosure vulnerability affecting Microsoft ASP.NET Core in .NET 11.0 preview releases and Microsoft Visual Studio 2026 prior to version 18.12.0. The flaw stems from the use of uninitialized resources (CWE-908) within response buffering components, specifically ResponseBufferingStream used by middleware such as HTTP Logging and Output Caching. Because rented memory segments from ArrayPool<byte> or MemoryPool<byte> are not cleared upon return or strictly bounded to the exact written byte count during subsequent reads, residual response payloads from earlier HTTP requests remain in the buffer tail. Downstream consumers or log sinks reading these buffers expose confidential cross-session data, including authentication tokens and personally identifiable information.
An incorrect authorization vulnerability (CWE-863) exists in the ASP.NET Core SignalR HTTP Connection Dispatcher (`Microsoft.AspNetCore.Http.Connections`). The connection negotiation process failed to bind the authenticated caller identity to the generated connection state, and subsequent transport or message dispatch handlers failed to verify caller consistency. Consequently, an authenticated low-privilege attacker who obtains a valid connection identifier or token can inject messages into or hijack another user's SignalR session, achieving an elevation of privilege across security boundaries.
CVE-2026-109478 is a network-accessible Denial of Service (DoS) vulnerability in ASP.NET Core caused by inefficient algorithmic complexity (CWE-407) when parsing comma-separated HTTP request headers. Prior to the patch, the internal helper method ParsingHelpers.GetHeaderSplit repeatedly called StringValues.Concat inside a loop iterating over each comma-separated token. Because StringValues allocates a new array of size k + 1 and copies all existing elements upon each concatenation, processing N tokens incurs O(N^2) time and space complexity. An unauthenticated remote attacker can send HTTP requests with headers containing thousands of short, comma-separated values to exhaust CPU cycles and induce severe Garbage Collection (GC) pauses, rendering the web application unresponsive.
CVE-2026-114147 is a high-severity Denial of Service (DoS) vulnerability affecting ASP.NET Core and specifically the Microsoft.AspNetCore.SignalR.StackExchangeRedis scale-out provider across .NET 8.0, 9.0, 10.0, and 11.0 preview releases. The vulnerability originates from a string comparison mismatch between internal per-connection group collections and the centralized Redis channel subscription registry, compounded by a race condition during connection teardown. Remote unauthenticated attackers can exploit this flaw to leave orphaned connection contexts and active Redis pub/sub listeners pinned in memory, leading to unbounded memory exhaustion and application process termination.
CVE-2026-114156 is an improper resolution of path equivalence vulnerability (CWE-41) in Microsoft ASP.NET Core affecting .NET 11.0 preview releases and Microsoft Visual Studio 2026. The vulnerability stems from a discrepancy between how path prefix validation helpers (such as PathString.StartsWithSegments) and downstream routing mechanisms handle alternate path separators and percent-encoded characters. An unauthenticated remote attacker can exploit this mismatch over the network to bypass path-based authorization middleware and access restricted endpoints.