CVE-2026-11677
Overview
Background
- `SimpleURLLoader`
- Chrome network-service helper (
services/network/public/cpp/simple_url_loader.cc) that performs a URL fetch and can stream the response body to a file on disk. - `create_temp_file_`
- internal flag telling
SimpleURLLoaderto download the body into a system-generated temporary file rather than a caller-suppliedpath_. - `base::CreateTemporaryFile`
- legacy API that creates an empty temp file and returns only its path, requiring a separate open-by-path step to obtain a usable
base::File. - `base::CreateAndOpenTemporaryFileInDir`
- API that atomically creates and opens a temp file in a given directory, returning an already-open
base::File(retained fd) plus itspath_.
Root Cause Analysis
The download-init path took create_temp_file_ and called base::CreateTemporaryFile(&path_) to obtain a path, then separately called file_.Initialize(path_, FLAG_WRITE | FLAG_CREATE_ALWAYS) to open that path. Between the create and the re-open, the file at path_ was referenced only by name, opening a time-of-check-to-time-of-use (TOCTOU) window in which another process could replace, delete, or swap the temp file for a symlink so that the subsequent open-by-path acted on an attacker-controlled target. The invariant violated is that a temp file’s identity must remain bound to the fd that created it; re-resolving the path breaks that binding.
The fix replaces the two-step create-then-reopen with a single base::CreateAndOpenTemporaryFileInDir(temp_dir, &path_) call that atomically creates and opens the file and hands back the retained descriptor, so no path is ever re-resolved. It also maps file_.error_details() through net::FileErrorToNetError() for accurate failure reporting instead of relying solely on logging::GetLastSystemErrorCode().
base::CreateTemporaryFile then file_.Initialize) instead of retaining the descriptor from creation; the fix eliminates the race by using base::CreateAndOpenTemporaryFileInDir, which creates and opens atomically and keeps the fd.Attack Path
- Trigger a temp-file download
Induce a browser flow that uses
SimpleURLLoaderwithcreate_temp_file_set, sobase::CreateTemporaryFile(&path_)allocates a predictable temp path. - Win the TOCTOU window
Between the empty-file creation and the
file_.Initialize(path_, ...)re-open, a local attacker deletes or replaces the file atpath_(for example with a symlink to a sensitive target). - Redirect the open
The re-open-by-path with
FLAG_CREATE_ALWAYSnow resolves to the attacker-substituted target rather than the originally created file. - Corrupt or misdirect written data Response-body bytes are streamed to the substituted target, letting the attacker clobber a chosen file or observe/interpose on the download.
Impact Assessment
SimpleURLLoader’s downloaded bytes to land on or truncate a file of their choosing, escalating a local race into unauthorized file writes. This runs in the network-service/browser context that performs the download; the precondition is co-located code that can manipulate the temp directory during the narrow window between base::CreateTemporaryFile and the subsequent file_.Initialize.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/network/public/cpp/simple_url_loader.cc |
modified |
Files Changed
services/network/public/cpp/simple_url_loader.cc
Audit Directions
- Create-then-reopen by pathFlag any code that creates a temp file for its name and later opens it via
base::File::Initializeor an open-by-path call; prefer atomic create-and-open APIs that retain the fd. - `base::CreateTemporaryFile` callersAudit remaining uses of
base::CreateTemporaryFile/GetTempDir+path handoff for TOCTOU windows and migrate them tobase::CreateAndOpenTemporaryFileInDir. - `logging::GetLastSystemErrorCode` error mappingLook for file-init failure paths that report status from
logging::GetLastSystemErrorCode/net::MapSystemErroralone without consultingbase::File::error_details(), which can mask or misreport the true failure.
Patch
From 037c5ddb3336e88e3215f9e2cb903282d032b653 Mon Sep 17 00:00:00 2001
From: Kenichi Ishibashi <bashi@chromium.org>
Date: Tue, 02 Jun 2026 20:42:51 -0700
Subject: [PATCH] Improve temporary file creation and error handling in SimpleURLLoader
This CL updates SimpleURLLoader to use
base::CreateAndOpenTemporaryFileInDir() when downloading to a temporary
file. This atomically creates and opens the file, retaining the file
descriptor directly and avoiding the previous pattern of closing and
re-opening the file by path.
Additionally, it improves error reporting on initialization failure by
explicitly mapping base::File::error_details() to a net::Error via
net::FileErrorToNetError().
Bug: 516979551
Change-Id: I353bac993212cbee76cb8a1c3c086e73ff4e6369
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7885361
Reviewed-by: mmenke <mmenke@chromium.org>
Commit-Queue: Kenichi Ishibashi <bashi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1640681}
---
diff --git a/services/network/public/cpp/simple_url_loader.cc b/services/network/public/cpp/simple_url_loader.cc
index 23e127d..1ac2f96 100644
--- a/services/network/public/cpp/simple_url_loader.cc
+++ b/services/network/public/cpp/simple_url_loader.cc
@@ -1020,28 +1020,28 @@
DCHECK(!file_.IsValid());
DCHECK(!body_reader_);
- bool have_path = !create_temp_file_;
- if (!have_path) {
- DCHECK(create_temp_file_);
- have_path = base::CreateTemporaryFile(&path_);
- // CreateTemporaryFile() creates an empty file.
- if (have_path)
- owns_file_ = true;
- }
-
- if (have_path) {
- // Try to initialize |file_|, creating the file if needed.
+ if (create_temp_file_) {
+ base::FilePath temp_dir;
+ if (base::GetTempDir(&temp_dir)) {
+ file_ = base::CreateAndOpenTemporaryFileInDir(temp_dir, &path_);
+ }
+ } else {
file_.Initialize(
path_, base::File::FLAG_WRITE | base::File::FLAG_CREATE_ALWAYS);
}
// If CreateTemporaryFile() or File::Initialize() failed, report failure.
if (!file_.IsValid()) {
+ net::Error net_error = net::FileErrorToNetError(file_.error_details());
+ if (net_error == net::OK) {
+ net_error = net::MapSystemError(logging::GetLastSystemErrorCode());
+ if (net_error == net::OK) {
+ net_error = net::ERR_FILE_NOT_FOUND;
+ }
+ }
body_handler_task_runner_->PostTask(
- FROM_HERE, base::BindOnce(std::move(on_done_callback),
- net::MapSystemError(
- logging::GetLastSystemErrorCode()),
- 0, base::FilePath()));
+ FROM_HERE, base::BindOnce(std::move(on_done_callback), net_error, 0,
+ base::FilePath()));
return;
}
Original Bug Report
Potential macOS sandbox escape via TOCTOU in SimpleURLLoader temporary file creation
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists in SimpleURLLoader’s temporary file creation mechanism on macOS. A compromised, sandboxed network service process can exploit a race condition in the shared temporary directory to swap a temporary file with a symbolic link. Because the browser process re-opens the file path without symbolic link protection, it may follow the link and overwrite arbitrary files with browser process privileges.
Affected files:
services/network/public/cpp/simple_url_loader.cc
Estimated timestamp from git blame: 2017-10-18
Summary
A potential Time-of-Check to Time-of-Use (TOCTOU) vulnerability has been identified in the temporary file writing logic of SimpleURLLoader (services/network/public/cpp/simple_url_loader.cc). When downloading to a temporary file, the code generates and closes a temporary file, and subsequently re-opens the path. On macOS, both the unsandboxed browser process and the sandboxed network service process share access to the same temporary directory, creating a race window where a compromised network service can replace the temporary file with a symbolic link, resulting in an arbitrary file write under browser privileges (sandbox escape).
Vulnerability Analysis
In services/network/public/cpp/simple_url_loader.cc, within FileWriter::StartWritingOnFileSequence (lines 1023-1036):
bool have_path = !create_temp_file_;
if (!have_path) {
DCHECK(create_temp_file_);
have_path = base::CreateTemporaryFile(&path_); // (1) Creates the file and immediately closes its file descriptor
if (have_path)
owns_file_ = true;
}
if (have_path) {
file_.Initialize(
path_, base::File::FLAG_WRITE | base::File::FLAG_CREATE_ALWAYS);
// (2) Re-opens the file by path
}
-
Step 1 (
base::CreateTemporaryFile): The helperbase::CreateTemporaryFile(defined inbase/files/file_util.cc, line 395) resolves the temporary directory and callsmkstempviaCreateAndOpenFdForTemporaryFileInDir. It immediately closes the returned file descriptor, leaving only the generated path string inpath_(e.g.,/private/var/folders/.../T/.com.google.Chrome.XXXXXX). -
Step 2 (
file_.Initialize): The browser process re-opens this path on a background thread usingfile_.Initializewithbase::File::FLAG_WRITE | base::File::FLAG_CREATE_ALWAYS. On POSIX platforms (including macOS),FLAG_CREATE_ALWAYSmaps directly toO_CREAT | O_TRUNC(inbase/files/file_posix.cc, lines 573-577). Crucially, the flag map does not employO_NOFOLLOWor check whether the target is a symbolic link.
Sandbox Context (macOS)
Although the Network Service is sandboxed, its macOS Seatbelt profile (sandbox/policy/mac/network.sb, lines 50-53) explicitly permits read and write access to the per-user temporary directory:
(allow file-read* file-write*
(subpath (param darwin-user-cache-dir))
(subpath (param darwin-user-temp-dir))
)
The parameter darwin-user-temp-dir is retrieved in the browser process via confstr(_CS_DARWIN_USER_TEMP_DIR, ...) and passed during the launching of the child process. Therefore, both the unsandboxed browser process (running SimpleURLLoader’s file sequence) and the sandboxed network process share access to the exact same temporary directory path.
Potential Exploitation Scenario
Based on static analysis, a compromised network process could potentially execute the following steps to escape the sandbox:
- Monitoring: The compromised network process monitors the
darwin-user-temp-dirfor the creation of temporary files adhering to the pattern.com.google.Chrome.XXXXXX. - The Race: Immediately after
base::CreateTemporaryFileis called and closes its file descriptor (Step 1), but before the browser process callsfile_.Initialize(Step 2), the attacker deletes the empty temporary file viaunlink(). - Symlink Placement: The attacker creates a symbolic link at that exact path pointing to a highly sensitive destination in the user’s home directory (e.g.,
~/Library/LaunchAgents/persistence.plistor~/.zshrc). - Overwrite: When the browser process calls
file_.Initialize, theopencall follows the symbolic link, truncating the targeted plist or shell script, and writes the incoming network payload directly into it. - Retry Mitigation: If the attacker misses the tight race window, they can abort the loader from the network side with
net::ERR_NETWORK_CHANGED. This triggersSimpleURLLoader’s retry mechanism (frequently used by components like theExtensionDownloader), giving the attacker multiple attempts to win the race.
Note: These are potential/suggested exploitation steps derived from static code and sandbox profile tracing, as our current tooling does not have the capability to execute code or verify runtime behavior.
Suggested Fix
Avoid closing and re-opening the file by path. Instead of calling base::CreateTemporaryFile and then file_.Initialize, utilize an API that keeps the file descriptor open upon creation, such as base::CreateAndOpenTemporaryFileInDir. This ensures that the browser retains exclusive and safe access to the file handle created atomically by mkstemp without leaving a path open to TOCTOU manipulation.
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.