CVE-2026-12022
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchrome/common/safe_browsing/binary_feature_extractor_unittest.cc |
modified | |
ifchrome/services/file_util/public/cpp/temporary_file_getter.cc |
modified | |
TEST_Fchrome/services/file_util/public/cpp/temporary_file_getter_unittest.cc |
modified |
Files Changed
chrome/common/safe_browsing/binary_feature_extractor.ccchrome/common/safe_browsing/binary_feature_extractor_unittest.ccchrome/services/file_util/public/cpp/temporary_file_getter.ccchrome/services/file_util/public/cpp/temporary_file_getter_unittest.cc
Patch
From 17a9575ec18ddd8c29b30bbdf2a016a9a4f06518 Mon Sep 17 00:00:00 2001
From: Brian Begnoche <bcb@chromium.org>
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 <bcb@chromium.org>
Reviewed-by: Xinghui Lu <xinghuilu@chromium.org>
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
Regression Test / PoC
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
Original Bug Report
Potential macOS Sandbox Escape via Symlink-follow TOCTOU in BinaryFeatureExtractor
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 the macOS Safe Browsing implementation of BinaryFeatureExtractor. By creating a temporary file in a shared directory, closing its file descriptor, and subsequently copying data to the path using symlink-following APIs, Chrome may allow a compromised sandboxed helper process to swap the file with a symbolic link. This could potentially enable an attacker to perform arbitrary file writes with the privileges of the unsandboxed browser process.
Affected files:
chrome/common/safe_browsing/binary_feature_extractor.ccbase/files/file_util_apple.mm
Estimated timestamp from git blame: 2023-02-07
Root Cause Analysis
In BinaryFeatureExtractor::ExtractImageFeatures (located in chrome/common/safe_browsing/binary_feature_extractor.cc), a temporary file is created using base::CreateTemporaryFile:
base::FilePath temp_path;
if (!base::CreateTemporaryFile(&temp_path)) {
return false;
}
if (!base::CopyFile(file_path, temp_path)) {
base::DeleteFile(temp_path);
return false;
}
Under the hood, base::CreateTemporaryFile calls mkstemp() and immediately closes the returned file descriptor, returning only the file path (temp_path) to the caller. This introduces a Time-of-Check to Time-of-Use (TOCTOU) window before the file is reopened or written to.
Subsequently, base::CopyFile(file_path, temp_path) is called to copy the downloaded file into the temporary path. On macOS, base::CopyFile is implemented in base/files/file_util_apple.mm using the system copyfile(3) function with only the COPYFILE_DATA flag. It lacks the COPYFILE_NOFOLLOW_DST flag, meaning the system copy function will follow symbolic links at the destination path.
Trust Boundary & Shared Directory Access
On macOS, the temporary directory returned by NSTemporaryDirectory() points to the per-user temporary directory _CS_DARWIN_USER_TEMP_DIR located inside /var/folders/.
Several sandboxed helper processes (such as the Network Service, GPU process, and On-Device Model Execution service) are granted write/unlink access within this directory via the Seatbelt sandbox parameter darwin-user-temp-dir:
- Network Service (
sandbox/policy/mac/network.sb):(allow file-read* file-write* (subpath (param darwin-user-temp-dir))) - GPU Process (
sandbox/policy/mac/gpu.sb):(allow file-read* file-write-data file-write-create file-write-owner file-write-unlink (subpath (param darwin-user-temp-dir))) - On-Device Model Execution (
sandbox/policy/mac/on_device_model_execution.sb):(allow file-read* file-write* (subpath (param darwin-user-temp-dir)))
Because both the unsandboxed browser process and the compromised sandboxed helper processes run with the exact same macOS User ID (UID), they have matching discretionary access control permissions over files in this shared directory.
Potential Exploitation Scenario
An attacker who has already compromised a sandboxed helper process (such as the Network Service or GPU process) could potentially execute the following steps to escape the sandbox:
- Monitor: The compromised process monitors the shared
darwin-user-temp-dirfor the creation of files matching Chrome’s temp file prefix pattern (e.g.,.com.google.Chrome.XXXXXX). - Delete: Immediately after
base::CreateTemporaryFilecreates the file and closes its file descriptor, the compromised process callsunlink()on the temporary file path to delete it. - Symlink: The compromised process immediately calls
symlink()to create a symbolic link attemp_pathpointing to a target file outside the sandbox (e.g.,~/Library/LaunchAgents/com.pwn.plist). - Write (Sandbox Escape): When the unsandboxed browser process calls
base::CopyFile, the macOS kernel follows the symbolic link attemp_pathand writes the contents of the downloaded binary directly into the target plist file with the privileges of the browser process. This allows for persistent execution outside the sandbox.
Note: These steps are based on static analysis and represent potential exploitation vectors; our tooling does not yet have the capability to execute code or verify this via a live proof-of-concept.
Suggested Fix
Avoid closing the temporary file and reopening/writing to it by path. Instead, keep the file descriptor open after creation (for example, by utilizing base::CreateAndOpenTemporaryFileInDir) and write to the file descriptor directly rather than using path-based APIs. If path-based file copying is necessary, ensure that safe flags such as COPYFILE_NOFOLLOW_DST are used on macOS, or verify that the destination is not a symbolic link before writing.
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.