High watch CVSS 8.3 chrome Race 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace in Network
DescriptionRace in Network
ComponentNetwork
Bug ClassRace
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker516979551
Fix commit037c5ddb3336 (chromium/src) +15/-15
CWECWE-362 (Race condition)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-06-03 (public git)
Disclosed2026-06-08
Patch gap5 days — fix public in source before the advisory

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 SimpleURLLoader to download the body into a system-generated temporary file rather than a caller-supplied path_.
`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 its path_.

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().

Key insight
The core mistake was closing/re-opening the temporary file by path (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

  1. Trigger a temp-file download Induce a browser flow that uses SimpleURLLoader with create_temp_file_ set, so base::CreateTemporaryFile(&path_) allocates a predictable temp path.
  2. Win the TOCTOU window Between the empty-file creation and the file_.Initialize(path_, ...) re-open, a local attacker deletes or replaces the file at path_ (for example with a symlink to a sensitive target).
  3. Redirect the open The re-open-by-path with FLAG_CREATE_ALWAYS now resolves to the attacker-substituted target rather than the originally created file.
  4. 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

An attacker with local presence able to write to the temp directory can win the create-vs-reopen race and cause 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

FunctionChangeNotes
if
services/network/public/cpp/simple_url_loader.cc
modified

Files Changed

  • services/network/public/cpp/simple_url_loader.cc

Audit Directions

  • Create-then-reopen by path
    Flag any code that creates a temp file for its name and later opens it via base::File::Initialize or an open-by-path call; prefer atomic create-and-open APIs that retain the fd.
  • `base::CreateTemporaryFile` callers
    Audit remaining uses of base::CreateTemporaryFile/GetTempDir+path handoff for TOCTOU windows and migrate them to base::CreateAndOpenTemporaryFileInDir.
  • `logging::GetLastSystemErrorCode` error mapping
    Look for file-init failure paths that report status from logging::GetLastSystemErrorCode/net::MapSystemError alone without consulting base::File::error_details(), which can mask or misreport the true failure.
From 037c5ddb3336e88e3215f9e2cb903282d032b653 Mon Sep 17 00:00:00 2001
From: Kenichi Ishibashi <[email protected]>
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 <[email protected]>
Commit-Queue: Kenichi Ishibashi <[email protected]>
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;
       }
 
Loading diff…

Original Bug Report

reported by [email protected]

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
}
  1. Step 1 (base::CreateTemporaryFile): The helper base::CreateTemporaryFile (defined in base/files/file_util.cc, line 395) resolves the temporary directory and calls mkstemp via CreateAndOpenFdForTemporaryFileInDir. It immediately closes the returned file descriptor, leaving only the generated path string in path_ (e.g., /private/var/folders/.../T/.com.google.Chrome.XXXXXX).

  2. Step 2 (file_.Initialize): The browser process re-opens this path on a background thread using file_.Initialize with base::File::FLAG_WRITE | base::File::FLAG_CREATE_ALWAYS. On POSIX platforms (including macOS), FLAG_CREATE_ALWAYS maps directly to O_CREAT | O_TRUNC (in base/files/file_posix.cc, lines 573-577). Crucially, the flag map does not employ O_NOFOLLOW or 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:

  1. Monitoring: The compromised network process monitors the darwin-user-temp-dir for the creation of temporary files adhering to the pattern .com.google.Chrome.XXXXXX.
  2. The Race: Immediately after base::CreateTemporaryFile is called and closes its file descriptor (Step 1), but before the browser process calls file_.Initialize (Step 2), the attacker deletes the empty temporary file via unlink().
  3. 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.plist or ~/.zshrc).
  4. Overwrite: When the browser process calls file_.Initialize, the open call follows the symbolic link, truncating the targeted plist or shell script, and writes the incoming network payload directly into it.
  5. 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 triggers SimpleURLLoader’s retry mechanism (frequently used by components like the ExtensionDownloader), 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.

View on issue tracker