Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace in Safe Browsing
DescriptionRace in Safe Browsing
ComponentSafe Browsing
Bug ClassRace
Tracker516926968
Fix commit17a9575ec18d (chromium/src) +89/-20
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-16

Changed Functions

FunctionChangeNotes
TEST_F
chrome/common/safe_browsing/binary_feature_extractor_unittest.cc
modified
if
chrome/services/file_util/public/cpp/temporary_file_getter.cc
modified
TEST_F
chrome/services/file_util/public/cpp/temporary_file_getter_unittest.cc
modified

Files Changed

  • chrome/common/safe_browsing/binary_feature_extractor.cc
  • chrome/common/safe_browsing/binary_feature_extractor_unittest.cc
  • chrome/services/file_util/public/cpp/temporary_file_getter.cc
  • chrome/services/file_util/public/cpp/temporary_file_getter_unittest.cc
From 17a9575ec18ddd8c29b30bbdf2a016a9a4f06518 Mon Sep 17 00:00:00 2001
From: Brian Begnoche <[email protected]>
Date: Wed, 03 Jun 2026 10:15:21 -0700
Subject: [PATCH] [Safe Browsing] Rework temp file TOCTOU vulnerability fixes

Prevent symlink-following/TOCTOU race conditions in macOS Safe Browsing
and File Util temporary file operations by strictly relying on the
atomic creation of the file descriptor via
base::CreateAndOpenTemporaryFileInDir().

Fixed: 516929496, 516926968
Change-Id: Ie71124e0f7ac153aed46afe9b49b8822a990c057
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7891306
Commit-Queue: Brian Begnoche <[email protected]>
Reviewed-by: Xinghui Lu <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1641021}
---

diff --git a/chrome/common/safe_browsing/binary_feature_extractor.cc b/chrome/common/safe_browsing/binary_feature_extractor.cc
index 7860af7..11ff5138 100644
--- a/chrome/common/safe_browsing/binary_feature_extractor.cc
+++ b/chrome/common/safe_browsing/binary_feature_extractor.cc
@@ -4,6 +4,8 @@
 
 #include "chrome/common/safe_browsing/binary_feature_extractor.h"
 
+#include "build/build_config.h"
+
 #include <memory>
 #include <utility>
 
@@ -28,21 +30,33 @@
     ExtractHeadersOption options,
     ClientDownloadRequest_ImageHeaders* image_headers,
     google::protobuf::RepeatedPtrField<std::string>* signed_data) {
+  base::FilePath temp_dir;
+  if (!base::GetTempDir(&temp_dir)) {
+    return false;
+  }
+
   base::FilePath temp_path;
-  if (!base::CreateTemporaryFile(&temp_path)) {
+  base::File temp_file = base::CreateAndOpenTemporaryFileInDir(
+      temp_dir, &temp_path,
+      base::File::FLAG_WIN_TEMPORARY | base::File::FLAG_DELETE_ON_CLOSE);
+  if (!temp_file.IsValid()) {
     return false;
   }
 
-  if (!base::CopyFile(file_path, temp_path)) {
-    base::DeleteFile(temp_path);
-    return false;
-  }
 
-  base::File temp_file;
-  temp_file.Initialize(temp_path, base::File::FLAG_OPEN |
-                                      base::File::FLAG_READ |
-                                      base::File::FLAG_WIN_TEMPORARY |
-                                      base::File::FLAG_DELETE_ON_CLOSE);
+
+  {
+    base::File source_file(file_path,
+                           base::File::FLAG_OPEN | base::File::FLAG_READ);
+    if (!source_file.IsValid()) {
+      return false;
+    }
+
+    if (!base::CopyFileContents(source_file, temp_file)) {
+      base::DeleteFile(temp_path);
+      return false;
+    }
+  }
 
   base::MemoryMappedFile mapped_file;
   if (!mapped_file.Initialize(std::move(temp_file))) {
diff --git a/chrome/common/safe_browsing/binary_feature_extractor_unittest.cc b/chrome/common/safe_browsing/binary_feature_extractor_unittest.cc
index db635c8..cb9efcd 100644
--- a/chrome/common/safe_browsing/binary_feature_extractor_unittest.cc
+++ b/chrome/common/safe_browsing/binary_feature_extractor_unittest.cc
@@ -155,4 +155,27 @@
       path_, BinaryFeatureExtractor::kDefaultOptions, &image_headers, nullptr);
 }
 
+TEST_F(BinaryFeatureExtractorTest, ExtractImageFeaturesContentMatch) {
+  constexpr char kTestData[] = "Safe copy of interesting binary content";
+  WriteFileToHash(base::as_byte_span(std::string_view(kTestData)));
+
+  scoped_refptr<MockBinaryFeatureExtractor> mock_extractor(
+      new MockBinaryFeatureExtractor());
+  EXPECT_CALL(*mock_extractor, ExtractImageFeaturesFromData(_, _, _, _))
+      .WillOnce(
+          [&](base::span<const uint8_t> data,
+              BinaryFeatureExtractor::ExtractHeadersOption options,
+              ClientDownloadRequest_ImageHeaders* image_headers,
+              google::protobuf::RepeatedPtrField<std::string>* signed_data) {
+            EXPECT_EQ(std::string_view(kTestData),
+                      std::string_view(reinterpret_cast<const char*>(data.data()),
+                                       data.size()));
+            return true;
+          });
+
+  ClientDownloadRequest_ImageHeaders image_headers;
+  EXPECT_TRUE(mock_extractor->ExtractImageFeatures(
+      path_, BinaryFeatureExtractor::kDefaultOptions, &image_headers, nullptr));
+}
+
 }  // namespace safe_browsing
diff --git a/chrome/services/file_util/public/cpp/temporary_file_getter.cc b/chrome/services/file_util/public/cpp/temporary_file_getter.cc
index 77c00ad..7aeece8b 100644
--- a/chrome/services/file_util/public/cpp/temporary_file_getter.cc
+++ b/chrome/services/file_util/public/cpp/temporary_file_getter.cc
@@ -4,6 +4,8 @@
 
 #include "chrome/services/file_util/public/cpp/temporary_file_getter.h"
 
+#include "build/build_config.h"
+
 #include "base/files/file_util.h"
 #include "base/task/thread_pool.h"
 
@@ -12,17 +14,25 @@
 constexpr int kMaxNumberOfFilesAllowed = 10;
 
 base::File TemporaryFileGetterHelper(int num_files_requested) {
-  base::FilePath temp_path;
-  base::File temp_file;
-  if (num_files_requested <= kMaxNumberOfFilesAllowed &&
-      base::CreateTemporaryFile(&temp_path)) {
-    temp_file.Initialize(
-        temp_path, (base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_READ |
-                    base::File::FLAG_WRITE | base::File::FLAG_WIN_TEMPORARY |
-                    base::File::FLAG_DELETE_ON_CLOSE));
-  } else {
-    temp_file = base::File();
+  if (num_files_requested > kMaxNumberOfFilesAllowed) {
+    return base::File();
   }
+
+  base::FilePath temp_dir;
+  if (!base::GetTempDir(&temp_dir)) {
+    return base::File();
+  }
+
+  base::FilePath temp_path;
+  base::File temp_file = base::CreateAndOpenTemporaryFileInDir(
+      temp_dir, &temp_path,
+      base::File::FLAG_WIN_TEMPORARY | base::File::FLAG_DELETE_ON_CLOSE);
+  if (!temp_file.IsValid()) {
+    return base::File();
+  }
+
+
+
   return temp_file;
 }
 }  // namespace
diff --git a/chrome/services/file_util/public/cpp/temporary_file_getter_unittest.cc b/chrome/services/file_util/public/cpp/temporary_file_getter_unittest.cc
index 91017ca0..3249500 100644
--- a/chrome/services/file_util/public/cpp/temporary_file_getter_unittest.cc
+++ b/chrome/services/file_util/public/cpp/temporary_file_getter_unittest.cc
@@ -45,4 +45,26 @@
   EXPECT_FALSE(temp_file_.IsValid());
 }
 
+TEST_F(TemporaryFileGetterTest, GetTempFileWriteReadTest) {
+  auto callback = base::BindOnce(&UpdateTempFile, &temp_file_);
+  temp_file_getter_.RequestTemporaryFile(std::move(callback));
+  task_environment_.RunUntilIdle();
+  ASSERT_TRUE(temp_file_.IsValid());
+
+  constexpr char kTestData[] = "Some test data to write to temporary file getter";
+  std::optional<size_t> bytes_written =
+      temp_file_.WriteAtCurrentPos(base::as_byte_span(std::string_view(kTestData)));
+  ASSERT_TRUE(bytes_written.has_value());
+  EXPECT_EQ(std::size(kTestData) - 1, bytes_written.value());
+
+  ASSERT_TRUE(temp_file_.Seek(base::File::FROM_BEGIN, 0) == 0);
+
+  char read_buffer[sizeof(kTestData)] = {0};
+  std::optional<size_t> bytes_read =
+      temp_file_.ReadAtCurrentPos(base::as_writable_byte_span(read_buffer).first(bytes_written.value()));
+  ASSERT_TRUE(bytes_read.has_value());
+  EXPECT_EQ(bytes_written.value(), bytes_read.value());
+  EXPECT_EQ(std::string_view(kTestData), std::string_view(read_buffer, bytes_read.value()));
+}
+
 }  // namespace safe_browsing
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/common/safe_browsing/binary_feature_extractor_unittest.cc b/chrome/common/safe_browsing/binary_feature_extractor_unittest.cc
index db635c8..cb9efcd 100644
--- a/chrome/common/safe_browsing/binary_feature_extractor_unittest.cc
+++ b/chrome/common/safe_browsing/binary_feature_extractor_unittest.cc
@@ -155,4 +155,27 @@
       path_, BinaryFeatureExtractor::kDefaultOptions, &image_headers, nullptr);
 }
 
+TEST_F(BinaryFeatureExtractorTest, ExtractImageFeaturesContentMatch) {
+  constexpr char kTestData[] = "Safe copy of interesting binary content";
+  WriteFileToHash(base::as_byte_span(std::string_view(kTestData)));
+
+  scoped_refptr<MockBinaryFeatureExtractor> mock_extractor(
+      new MockBinaryFeatureExtractor());
+  EXPECT_CALL(*mock_extractor, ExtractImageFeaturesFromData(_, _, _, _))
+      .WillOnce(
+          [&](base::span<const uint8_t> data,
+              BinaryFeatureExtractor::ExtractHeadersOption options,
+              ClientDownloadRequest_ImageHeaders* image_headers,
+              google::protobuf::RepeatedPtrField<std::string>* signed_data) {
+            EXPECT_EQ(std::string_view(kTestData),
+                      std::string_view(reinterpret_cast<const char*>(data.data()),
+                                       data.size()));
+            return true;
+          });
+
+  ClientDownloadRequest_ImageHeaders image_headers;
+  EXPECT_TRUE(mock_extractor->ExtractImageFeatures(
+      path_, BinaryFeatureExtractor::kDefaultOptions, &image_headers, nullptr));
+}
+
 }  // namespace safe_browsing
diff --git a/chrome/services/file_util/public/cpp/temporary_file_getter_unittest.cc b/chrome/services/file_util/public/cpp/temporary_file_getter_unittest.cc
index 91017ca0..3249500 100644
--- a/chrome/services/file_util/public/cpp/temporary_file_getter_unittest.cc
+++ b/chrome/services/file_util/public/cpp/temporary_file_getter_unittest.cc
@@ -45,4 +45,26 @@
   EXPECT_FALSE(temp_file_.IsValid());
 }
 
+TEST_F(TemporaryFileGetterTest, GetTempFileWriteReadTest) {
+  auto callback = base::BindOnce(&UpdateTempFile, &temp_file_);
+  temp_file_getter_.RequestTemporaryFile(std::move(callback));
+  task_environment_.RunUntilIdle();
+  ASSERT_TRUE(temp_file_.IsValid());
+
+  constexpr char kTestData[] = "Some test data to write to temporary file getter";
+  std::optional<size_t> bytes_written =
+      temp_file_.WriteAtCurrentPos(base::as_byte_span(std::string_view(kTestData)));
+  ASSERT_TRUE(bytes_written.has_value());
+  EXPECT_EQ(std::size(kTestData) - 1, bytes_written.value());
+
+  ASSERT_TRUE(temp_file_.Seek(base::File::FROM_BEGIN, 0) == 0);
+
+  char read_buffer[sizeof(kTestData)] = {0};
+  std::optional<size_t> bytes_read =
+      temp_file_.ReadAtCurrentPos(base::as_writable_byte_span(read_buffer).first(bytes_written.value()));
+  ASSERT_TRUE(bytes_read.has_value());
+  EXPECT_EQ(bytes_written.value(), bytes_read.value());
+  EXPECT_EQ(std::string_view(kTestData), std::string_view(read_buffer, bytes_read.value()));
+}
+
 }  // namespace safe_browsing
Loading diff…

Original Bug Report

reported by [email protected]

Potential macOS Sandbox Escape via Symlink TOCTOU in TemporaryFileGetter

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 in TemporaryFileGetter on macOS allows a symlink race. Because sandboxed child processes like the GPU or Network processes share access to the same temporary directory, a compromised child could replace the closed temporary file with a symlink. This can lead to an arbitrary file write primitive outside the sandbox with browser privileges.

Affected files:

  • chrome/services/file_util/public/cpp/temporary_file_getter.cc

Estimated timestamp from git blame: 2023-02-24

Summary of Potential Vulnerability

A potential Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists in TemporaryFileGetterHelper within chrome/services/file_util/public/cpp/temporary_file_getter.cc.

When a temporary file is requested, the helper calls base::CreateTemporaryFile(&temp_path) to allocate a new path under the temporary directory. On POSIX-based systems, this function creates the file using mkstemp but immediately closes the returned file descriptor, leaving the path vacant. Subsequently, TemporaryFileGetterHelper attempts to reopen this path using temp_file.Initialize with the base::File::FLAG_CREATE_ALWAYS flag:

// chrome/services/file_util/public/cpp/temporary_file_getter.cc
base::File TemporaryFileGetterHelper(int num_files_requested) {
  base::FilePath temp_path;
  base::File temp_file;
  if (num_files_requested <= kMaxNumberOfFilesAllowed &&
      base::CreateTemporaryFile(&temp_path)) {            // (1) File created, FD closed
    temp_file.Initialize(
        temp_path, (base::File::FLAG_CREATE_ALWAYS |       // (2) Re-opened BY PATH
                    base::File::FLAG_READ | base::File::FLAG_WRITE |
                    base::File::FLAG_WIN_TEMPORARY |
                    base::File::FLAG_DELETE_ON_CLOSE));
  } ...
}

Potential Attack Vector on macOS

On macOS, this pattern is potentially exploitable because of the shared temporary directory and sandbox configurations:

  1. Shared Temp Directory: GetTempDir() resolves to NSTemporaryDirectory() (which corresponds to _CS_DARWIN_USER_TEMP_DIR). The browser passes this same temporary directory to child sandboxes via the darwin-user-temp-dir parameter during process launch.
  2. Sandboxed Child Access: Highly exposed sandboxed child processes, such as the GPU process (sandbox/policy/mac/gpu.sb) and the Network service (sandbox/policy/mac/network.sb), are granted read, write, create, and unlink access within darwin-user-temp-dir:
    (allow file-read* file-write-data file-write-create file-write-owner file-write-unlink
      (subpath (param darwin-user-temp-dir))
    )
    
  3. Symlink Race: If an attacker compromises the GPU or Network process, they can monitor the temporary directory. As soon as a temporary file matching the Chrome temp pattern (e.g., .com.google.Chrome.XXXXXX) is created and closed, the attacker can atomically unlink it and replace it with a symbolic link pointing to an arbitrary file outside the sandbox (e.g., ~/Library/LaunchAgents/com.pwn.plist or ~/.zshrc).
  4. Arbitrary File Write: When the unsandboxed browser process reopens the path via temp_file.Initialize, it does not use O_NOFOLLOW on macOS/POSIX. It follows the symlink, truncates the target file to 0 bytes via O_TRUNC (from FLAG_CREATE_ALWAYS), and obtains a writable file descriptor. Due to FLAG_DELETE_ON_CLOSE, it unlinks the symlink but leaves the open descriptor intact. The browser then returns this descriptor over Mojo to the sandboxed file utility service (e.g., during Safe Browsing Zip/DMG/Rar extraction), allowing the attacker-controlled contents of a downloaded file to be written to the target location.

Potential Steps to Trigger the Vulnerability

(Note: These are suggested/potential steps; our tooling does not currently have the ability to run code or verify a live proof of concept).

  1. The attacker exploits a separate vulnerability to run code in a sandboxed child process that has access to the shared darwin-user-temp-dir (such as the GPU or Network process).
  2. The sandboxed child process sets up a file system monitor (e.g., via kqueue or directory polling) on the shared temporary directory to watch for files matching the Chrome temp template (e.g., starting with .com.google.Chrome.).
  3. A user downloads an archive, triggering a legitimate Safe Browsing archive analysis in the sandboxed utility process, which requests a temporary file via the TemporaryFileGetter Mojo interface.
  4. When base::CreateTemporaryFile creates the file and closes its file descriptor, the monitoring child process immediately unlinks it and replaces it with a symbolic link pointing to a critical user file (e.g., ~/.zshrc).
  5. The browser process calls temp_file.Initialize, following the symbolic link and opening the target file for writing. This file descriptor is sent to the sandboxed utility process, which writes the extracted file contents to it, completing the arbitrary file write and escaping the sandbox.

Suggested Remediation

To prevent the symlink race, the browser process should avoid closing and reopening temporary files by path.

Instead of calling base::CreateTemporaryFile and then calling temp_file.Initialize, use a helper that creates and keeps the file descriptor open in a single atomic step, such as base::CreateAndOpenTemporaryFileInDir (or base::CreateAndOpenTemporaryStream). Alternatively, if the file must be reopened, ensure that O_NOFOLLOW is used on POSIX platforms to prevent the file system from traversing symbolic links during the open call.

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