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 condition in FileSystem
DescriptionRace condition in FileSystem
ComponentFileSystem
Sub-componentGoogle_apis
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.
Tracker522294538
Fix commit8d02bbd914aa (chromium/src) +63/-2
CWECWE-367 (TOCTOU race)
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-06-10 (per vendor advisory)
Fix landed2026-07-02 (public git)
Disclosed2026-08-25
Patch gap54 days — fix public in source before the advisory

Background

`UrlFetchRequestBase::WriteFileData`
the google_apis routine that opens the caller-supplied output_file_path and writes downloaded response bytes into it.
`DownloadData::output_file` / `output_file_path`
a base::File handle and its target path; the file is opened lazily inside WriteFileData when the handle is not already valid.
Symlink following (TOCTOU)
a time-of-check/time-of-use window where a path created earlier as a regular temp file is replaced by a symbolic link before it is reopened, so the open follows the link to an attacker-chosen target.
`O_NOFOLLOW`
a POSIX open flag that makes the call fail if the final path component is a symbolic link, preventing writes through the link.

Root Cause Analysis

UrlFetchRequestBase::WriteFileData opens download_data->output_file_path only when output_file is not already valid, and this open happens some time after the caller may have created that path as a temporary file. Pre-patch it used base::File::Initialize with FLAG_CREATE_ALWAYS | FLAG_WRITE, which follows symbolic links; if an attacker replaced the final path component with a symlink during that window, the download bytes were written through the link to whatever target the link pointed at. The invariant violated is that a download must only write to the actual file the caller staged, never to a redirected target chosen by a racing process.

The fix, on POSIX, opens the path directly with open(..., O_CREAT | O_TRUNC | O_WRONLY | O_NOFOLLOW | O_CLOEXEC, S_IRUSR | S_IWUSR) under a ScopedBlockingCall; if the final component is now a symlink the open fails, WriteFileData returns false, and the request is cancelled (reported as OTHER_ERROR) rather than writing through the link. The regression test confirms a symlinked output path leaves the link target’s contents untouched and the request reports non-success.

Key insight
The core mistake was reopening a pre-staged output path with a symlink-following open, creating a TOCTOU where a swapped-in symlink redirects the write; the fix uses O_NOFOLLOW so a symlinked final component aborts the write instead of following it.

Attack Path

  1. Observe the staged path A local attacker with access to the download’s output directory identifies the output_file_path created as a temporary file before WriteFileData reopens it.
  2. Win the race In the window between staging and the (previously symlink-following) reopen, the attacker replaces the final path component with a symbolic link pointing at a sensitive target file.
  3. Redirect the write Pre-patch, base::File::Initialize follows the link, so the downloaded response bytes are written to the attacker-chosen target, overwriting or creating it with attacker-influenced content.
  4. Post-fix failure With O_NOFOLLOW, the reopen fails on the symlink, WriteFileData returns false, and the request is cancelled with OTHER_ERROR, leaving the link target untouched.

Impact Assessment

A local attacker able to race the download output path can cause Chrome’s google_apis download machinery to write response data through a symlink to a file of the attacker’s choosing, an arbitrary-write/overwrite primitive limited to POSIX platforms and to files writable in that context. The precondition is local access to swap the final path component within the TOCTOU window; the advisory classifies this as a race condition in FileSystem and reports no in-the-wild exploitation. Any escalation from the redirected write (e.g. corrupting a security-relevant file) is beyond what the patch demonstrates (inferred).

Changed Functions

FunctionChangeNotes
TEST_F
google_apis/drive/drive_base_requests_server_unittest.cc
modified

Files Changed

  • google_apis/common/base_requests.cc
  • google_apis/drive/drive_base_requests_server_unittest.cc

Audit Directions

  • Reopen-after-stage patterns
    Audit any code that creates a temp/output path and later reopens it by path (rather than by retained fd), especially with CREATE_ALWAYS/create-truncate semantics, for symlink-following TOCTOU on POSIX.
  • Missing `O_NOFOLLOW`
    Search base::File::Initialize and raw open call sites that write attacker-influenced or downloaded data to caller-supplied paths and confirm they refuse symlinked final components.
  • Cross-platform gaps
    Where a POSIX-only O_NOFOLLOW guard is added, verify the corresponding Windows path (e.g. reparse-point handling) has an equivalent protection or is genuinely not exposed.
From 8d02bbd914aaf89947d8055e10f41d45eab99fc2 Mon Sep 17 00:00:00 2001
From: Fergal Daly <[email protected]>
Date: Thu, 02 Jul 2026 02:33:00 -0700
Subject: [PATCH] google_apis: Do not follow symlinks when writing download output

UrlFetchRequestBase::WriteFileData() opens the caller-supplied output
path some time after the caller may have created it as a temporary file.
On POSIX, open the path with O_NOFOLLOW so that the write fails if the
final path component has been replaced by a symbolic link in the
meantime, and the request is cancelled with OTHER_ERROR instead of
writing through the link.

Add a DownloadFileRequestBase regression test that supplies a symlink as
the output path and verifies that the link target is left untouched and
the request reports failure.

Fixed: 522294538
Change-Id: Ia436d9f66fbb36d221d437a3a507c03e8cad9b04
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8029006
Reviewed-by: Mingyu Lei <[email protected]>
Auto-Submit: Fergal Daly <[email protected]>
Reviewed-by: Fergal Daly <[email protected]>
Commit-Queue: Fergal Daly <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1655969}
---
diff --git a/google_apis/common/base_requests.cc b/google_apis/common/base_requests.cc
index 4ac59d3..037901c 100644
--- a/google_apis/common/base_requests.cc
+++ b/google_apis/common/base_requests.cc
@@ -13,6 +13,7 @@
 #include <utility>
 
 #include "base/containers/span.h"
+#include "base/files/file.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
 #include "base/json/json_reader.h"
@@ -21,6 +22,7 @@
 #include "base/task/sequenced_task_runner.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/values.h"
+#include "build/build_config.h"
 #include "google_apis/common/request_sender.h"
 #include "google_apis/common/task_util.h"
 #include "google_apis/credentials_mode.h"
@@ -31,6 +33,15 @@
 #include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
 
+#if BUILDFLAG(IS_POSIX)
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#include "base/files/scoped_file.h"
+#include "base/posix/eintr_wrapper.h"
+#include "base/threading/scoped_blocking_call.h"
+#endif
+
 namespace {
 
 // Template for optional OAuth2 authorization HTTP header.
@@ -336,9 +347,24 @@
 bool UrlFetchRequestBase::WriteFileData(std::string file_data,
                                         DownloadData* download_data) {
   if (!download_data->output_file.IsValid()) {
+#if BUILDFLAG(IS_POSIX)
+    // The output path may refer to a temporary file that was created in
+    // advance and is being reopened here, so do not follow symbolic links.
+    base::ScopedBlockingCall scoped_blocking_call(
+        FROM_HERE, base::BlockingType::MAY_BLOCK);
+    base::ScopedFD fd(
+        HANDLE_EINTR(open(download_data->output_file_path.value().c_str(),
+                          O_CREAT | O_TRUNC | O_WRONLY | O_NOFOLLOW | O_CLOEXEC,
+                          S_IRUSR | S_IWUSR)));
+    if (!fd.is_valid()) {
+      return false;
+    }
+    download_data->output_file = base::File(std::move(fd));
+#else
     download_data->output_file.Initialize(
         download_data->output_file_path,
         base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
+#endif
     if (!download_data->output_file.IsValid())
       return false;
   }
diff --git a/google_apis/drive/drive_base_requests_server_unittest.cc b/google_apis/drive/drive_base_requests_server_unittest.cc
index 8f3fc55..d80f5c1 100644
--- a/google_apis/drive/drive_base_requests_server_unittest.cc
+++ b/google_apis/drive/drive_base_requests_server_unittest.cc
@@ -2,8 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "google_apis/drive/drive_base_requests.h"
-
 #include <memory>
 
 #include "base/files/file_util.h"
@@ -11,10 +9,12 @@
 #include "base/functional/bind.h"
 #include "base/run_loop.h"
 #include "base/test/task_environment.h"
+#include "build/build_config.h"
 #include "google_apis/common/dummy_auth_service.h"
 #include "google_apis/common/request_sender.h"
 #include "google_apis/common/task_util.h"
 #include "google_apis/common/test_util.h"
+#include "google_apis/drive/drive_base_requests.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
@@ -166,4 +166,39 @@
   // Do not verify the not found message.
 }
 
+#if BUILDFLAG(IS_POSIX)
+TEST_F(BaseRequestsServerTest, DownloadFileRequest_SymlinkOutputPath) {
+  const std::string kTargetContents = "must not be overwritten";
+  const base::FilePath target_path =
+      GetTestCachedFilePath(base::FilePath::FromUTF8Unsafe("symlink_target"));
+  ASSERT_TRUE(base::WriteFile(target_path, kTargetContents));
+
+  const base::FilePath link_path =
+      GetTestCachedFilePath(base::FilePath::FromUTF8Unsafe("symlink_output"));
+  ASSERT_TRUE(base::CreateSymbolicLink(target_path, link_path));
+
+  ApiErrorCode result_code = OTHER_ERROR;
+  base::FilePath temp_file;
+  {
+    base::RunLoop run_loop;
+    std::unique_ptr<DownloadFileRequestBase> request =
+        std::make_unique<DownloadFileRequestBase>(
+            request_sender_.get(),
+            test_util::CreateQuitCallback(
+                &run_loop,
+                test_util::CreateCopyResultCallback(&result_code, &temp_file)),
+            GetContentCallback(), ProgressCallback(),
+            test_server_.GetURL("/files/drive/testfile.txt"), link_path);
+    request_sender_->StartRequestWithAuthRetry(std::move(request));
+    run_loop.Run();
+  }
+
+  EXPECT_NE(HTTP_SUCCESS, result_code);
+
+  std::string target_contents;
+  ASSERT_TRUE(base::ReadFileToString(target_path, &target_contents));
+  EXPECT_EQ(kTargetContents, target_contents);
+}
+#endif  // BUILDFLAG(IS_POSIX)
+
 }  // namespace google_apis
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/google_apis/drive/drive_base_requests_server_unittest.cc b/google_apis/drive/drive_base_requests_server_unittest.cc
index 8f3fc55..d80f5c1 100644
--- a/google_apis/drive/drive_base_requests_server_unittest.cc
+++ b/google_apis/drive/drive_base_requests_server_unittest.cc
@@ -2,8 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "google_apis/drive/drive_base_requests.h"
-
 #include <memory>
 
 #include "base/files/file_util.h"
@@ -11,10 +9,12 @@
 #include "base/functional/bind.h"
 #include "base/run_loop.h"
 #include "base/test/task_environment.h"
+#include "build/build_config.h"
 #include "google_apis/common/dummy_auth_service.h"
 #include "google_apis/common/request_sender.h"
 #include "google_apis/common/task_util.h"
 #include "google_apis/common/test_util.h"
+#include "google_apis/drive/drive_base_requests.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
@@ -166,4 +166,39 @@
   // Do not verify the not found message.
 }
 
+#if BUILDFLAG(IS_POSIX)
+TEST_F(BaseRequestsServerTest, DownloadFileRequest_SymlinkOutputPath) {
+  const std::string kTargetContents = "must not be overwritten";
+  const base::FilePath target_path =
+      GetTestCachedFilePath(base::FilePath::FromUTF8Unsafe("symlink_target"));
+  ASSERT_TRUE(base::WriteFile(target_path, kTargetContents));
+
+  const base::FilePath link_path =
+      GetTestCachedFilePath(base::FilePath::FromUTF8Unsafe("symlink_output"));
+  ASSERT_TRUE(base::CreateSymbolicLink(target_path, link_path));
+
+  ApiErrorCode result_code = OTHER_ERROR;
+  base::FilePath temp_file;
+  {
+    base::RunLoop run_loop;
+    std::unique_ptr<DownloadFileRequestBase> request =
+        std::make_unique<DownloadFileRequestBase>(
+            request_sender_.get(),
+            test_util::CreateQuitCallback(
+                &run_loop,
+                test_util::CreateCopyResultCallback(&result_code, &temp_file)),
+            GetContentCallback(), ProgressCallback(),
+            test_server_.GetURL("/files/drive/testfile.txt"), link_path);
+    request_sender_->StartRequestWithAuthRetry(std::move(request));
+    run_loop.Run();
+  }
+
+  EXPECT_NE(HTTP_SUCCESS, result_code);
+
+  std::string target_contents;
+  ASSERT_TRUE(base::ReadFileToString(target_path, &target_contents));
+  EXPECT_EQ(kTargetContents, target_contents);
+}
+#endif  // BUILDFLAG(IS_POSIX)
+
 }  // namespace google_apis
Loading diff…

Original Bug Report

reported by [email protected]

Potential macOS RemoteToLocalSyncer TOCTOU allows arbitrary file write and sandbox escape

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 TOCTOU vulnerability in RemoteToLocalSyncer::DownloadFile on macOS allows a compromised sandboxed process to achieve arbitrary file truncation and write operations at browser-process privilege. This occurs because the browser process discards the safe file descriptor of a generated temporary file, subsequently re-opening the path without O_NOFOLLOW. Consequently, a compromised child process with write permissions to the shared temporary directory could swap the file for a symbolic link during the network download round-trip.

Affected files:

  • chrome/browser/sync_file_system/drive_backend/remote_to_local_syncer.cc
  • google_apis/common/base_requests.cc
  • chrome/browser/sync_file_system/sync_file_system_service.cc

Estimated timestamp from git blame: 2013-11-12

Detailed Description

A potential Time-of-Check to Time-of-Use (TOCTOU) vulnerability has been identified in the RemoteToLocalSyncer::DownloadFile function on macOS. The issue stems from the way temporary download files are managed and subsequently populated with network data.

Potential Vulnerability Mechanism

  1. Temporary File Creation and FD Discarding: RemoteToLocalSyncer::DownloadFile creates a temporary file under the system temporary directory (which resolves to _CS_DARWIN_USER_TEMP_DIR via NSTemporaryDirectory() on macOS) by calling CreateTemporaryFile:

    // chrome/browser/sync_file_system/drive_backend/remote_to_local_syncer.cc
    storage::ScopedFile file = CreateTemporaryFile(...);
    base::FilePath path = file.path();
    

    The underlying implementation in base::CreateTemporaryFileInDir generates the path, invokes mkstemp(), and immediately closes the safe file descriptor, returning only the raw path to the caller.

  2. Network Process Round-Trip and Path Re-opening: The naked file path is passed to the Drive download stack (drive_service()->DownloadFile). The UrlFetchRequestBase streams the download via the network service. When data is received, the browser process re-opens the file path on its blocking task runner using the base::File::FLAG_CREATE_ALWAYS flag:

    // google_apis/common/base_requests.cc
    download_data->output_file.Initialize(
        download_data->output_file_path,
        base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
    
  3. Unsafe POSIX open() Flags: In base/files/file_posix.cc, FLAG_CREATE_ALWAYS is translated to O_CREAT | O_TRUNC (with O_WRONLY), but does not include O_NOFOLLOW:

    if (flags & FLAG_CREATE_ALWAYS) {
      open_flags = O_CREAT | O_TRUNC;
    }
    

Potential Attack Path (Theoretical Analysis)

Because the automated security tooling does not have the capability to execute code or verify runtime behavior, the following is a theoretical sequence of steps an attacker might follow to exploit this behavior:

  1. An attacker first obtains code execution within a sandboxed child process (such as the Network Process or GPU Process) on macOS.
  2. Both the unsandboxed browser process and the sandboxed child processes share access to the per-user temporary directory (_CS_DARWIN_USER_TEMP_DIR). The child processes have file-write* permissions (which allow unlink and symlink operations) on this directory.
  3. The attacker monitors the temporary directory for files conforming to the temporary template (e.g., .<bundle-id>.XXXXXX).
  4. Upon detection, the attacker unlinks the temporary file and creates a symbolic link at that path pointing to a sensitive file owned by the victim (for example, ~/.zshrc).
  5. When the browser process receives the download data stream, it opens the path via open(). Without O_NOFOLLOW, the kernel follows the symbolic link, leading to the truncation and overwrite of the target user file (~/.zshrc) with the downloaded content.

If the attacker controls the downloaded content (which is the case if they have compromised the Network Process and can spoof or modify response streams), they can achieve arbitrary code execution outside of the sandbox upon the next shell initiation.

Suggested Fix

To resolve this potential vulnerability, the file descriptor returned by the temporary file creation function should be retained and passed directly to the writer, avoiding any name-based re-opening. Alternatively, if name-based re-opening is necessary, the file must be opened using safety flags such as O_NOFOLLOW or the directory must be exclusive to the browser process and inaccessible to sandboxed child processes.

Evaluated with Chrome root at commit: b2fea2e31df308d0f04e4ae47def4c4f939ee141


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