CVE-2026-106412
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifbase/files/file_util_apple.mm |
modified | |
switchbase/files/file_util_apple.mm |
modified | |
LaunchResultcontent/browser/child_process_launcher_helper.h |
modified | |
CreateNamedPlatformChannelOnLauncherThreadcontent/browser/child_process_launcher_helper_mac.cc |
modified | |
ifcontent/browser/child_process_launcher_helper_mac.cc |
modified | |
BrowserChildProcessBackgroundedBridgecontent/browser/gpu/gpu_process_host.h |
modified | |
CATransactionGPUCoordinatorcontent/browser/gpu/gpu_process_host.h |
modified | |
GpuProcessHostcontent/browser/gpu/gpu_process_host.h |
modified |
Files Changed
third_party/blink/web_tests/platform/mac-mac26-arm64/external/wpt/webnn/conformance_tests/resample2d-gather-shape-divergence.https.any_cpu-expected.txtcontent/browser/sandbox_mac_unittest.mmthird_party/blink/web_tests/WebGPUExpectationscontent/browser/sandboxed_process_launcher_delegate.ccsandbox/policy/sandbox_type_unittest.cccontent/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txtbase/files/file_util.hthird_party/blink/web_tests/platform/mac-mac26-arm64/external/wpt/webnn/conformance_tests/resample2d-gather-shape-divergence.https.any_npu-expected.txtcontent/browser/sandboxed_process_launcher_delegate.hcontent/browser/child_process_launcher_helper.hcontent/browser/sandbox_parameters_mac.mmsandbox/policy/mac/gpu.sbcontent/browser/child_process_launcher_helper_mac.ccbase/test/launcher/test_launcher.cccontent/browser/gpu/gpu_process_host.ccsandbox/policy/sandbox_type.hbase/environment.hcontent/browser/gpu/gpu_process_host.hbase/files/file_util_apple.mmsandbox/policy/mac/common.sbthird_party/blink/web_tests/platform/mac-mac26-arm64/external/wpt/webnn/conformance_tests/resample2d-gather-shape-divergence.https.any_gpu-expected.txtcontent/browser/sandbox_parameters_mac.hcontent/browser/child_process_launcher_browsertest.ccsandbox/policy/sandbox_type.cc
Patch
From ee781affc03766363d35a6bdb65a1db359ee1a7d Mon Sep 17 00:00:00 2001 From: Bryan Oltman <[email protected]> Date: Wed, 09 Sep 2026 16:00:43 -0700 Subject: [PATCH] [macOS] Use sandboxed subdirectories for the GPU child process This CL restricts the GPU process's access to the shared Darwin per-user directories (`_CS_DARWIN_USER_DIR`, `_CS_DARWIN_USER_TEMP_DIR`, and `_CS_DARWIN_USER_CACHE_DIR`, which default to `/var/folders/.../0/`, `/var/folders/.../T/`, and `/var/folders/.../C/`) to GPU isolated subdirectories. Key changes: 1. Update the GPU process's seatbelt profile to tighten the current broad access to the Darwin user directories to sandboxed subdirectories. This is accomplished using the DIRHELPER_USER_DIR_SUFFIX environment variable, which the macOS dirhelper utility appends to the default `_CS_DARWIN` directory paths. 2. Allow different process types to specify whether whether they need the sandboxed directories mentioned above. This will allow child processes other than the GPU to take advantage of this functionality should they need it in the future. 3. Disables the flaky conformance/extensions/khr-parallel-shader-compile.html test on Intel Macs. This change is gated by the `kMacGpuSandboxDarwinUserDirs` feature flag. Cq-Include-Trybots: luci.chromium.try:mac13-arm64-rel,mac14-arm64-rel,mac15-arm64-rel,mac14-tests,mac13-tests,mac15-x64-rel-tests;luci.chrome.try:mac-rel-ready Change-Id: Id621693080084bf780f034cd0a65dc7e94753a78 Fixed: 528379972,520019273,516947138,521559611,521486621,516893912,520755056 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8279765 Reviewed-by: Alex Gough <[email protected]> Reviewed-by: Mark Mentovai <[email protected]> Reviewed-by: Avi Drissman <[email protected]> Commit-Queue: Bryan Oltman <[email protected]> Cr-Commit-Position: refs/heads/main@{#1694962} --- diff --git a/base/environment.h b/base/environment.h index 593ce0ab..d79be45 100644 --- a/base/environment.h +++ b/base/environment.h @@ -23,6 +23,17 @@ // On Posix systems, this variable contains the location of the user's home // directory. (e.g, /home/username/). inline constexpr char kHome[] = "HOME"; +inline constexpr char kTmpDir[] = "TMPDIR"; +#endif + +#if BUILDFLAG(IS_APPLE) +// Used to customize the path provided by NSTemporaryDirectory() and other paths +// retrieved from dirhelper (e.g., via confstr) on macOS. +inline constexpr char kDirHelperUserDirSuffix[] = "DIRHELPER_USER_DIR_SUFFIX"; + +// Historical environment variable set by build infrastructure for macOS temp +// dir. +inline constexpr char kMacChromiumTmpDir[] = "MAC_CHROMIUM_TMPDIR"; #endif } // namespace env_vars diff --git a/base/files/file_util.h b/base/files/file_util.h index e71ab46..5929b01 100644 --- a/base/files/file_util.h +++ b/base/files/file_util.h @@ -162,6 +162,18 @@ BASE_EXPORT void SetExtraNoExecuteAllowedPath(int path_key); #endif // BUILDFLAG(IS_WIN) +#if BUILDFLAG(IS_MAC) +enum class DarwinUserDirectory { + kUser, + kUserCache, + kUserTemp, +}; + +// Returns the canonicalized path of a Darwin user directory, or empty FilePath +// on failure. +BASE_EXPORT FilePath GetDarwinUserDirectory(DarwinUserDirectory directory); +#endif // BUILDFLAG(IS_MAC) + // Moves the given path, whether it's a file or a directory. // If a simple rename is not possible, such as in the case where the paths are // on different volumes, this will attempt to copy and delete. Returns diff --git a/base/files/file_util_apple.mm b/base/files/file_util_apple.mm index b25aaf8a..e0b00254 100644 --- a/base/files/file_util_apple.mm +++ b/base/files/file_util_apple.mm @@ -11,6 +11,7 @@ #include "base/apple/foundation_util.h" #include "base/check_op.h" +#include "base/environment.h" #include "base/files/file_path.h" #include "base/strings/string_util.h" #include "base/threading/scoped_blocking_call.h" @@ -34,7 +35,7 @@ // https://crbug.com/1266817 (allows a longer process singleton socket path). // Continue tracking MAC_CHROMIUM_TMPDIR as that's what build infrastructure // sets on macOS. - const char* env_tmpdir = getenv("MAC_CHROMIUM_TMPDIR"); + const char* env_tmpdir = getenv(base::env_vars::kMacChromiumTmpDir); if (env_tmpdir && env_tmpdir[0]) { *path = base::FilePath(env_tmpdir); return true; @@ -68,4 +69,35 @@ return FilePath("/tmp"); } +#if BUILDFLAG(IS_MAC) +FilePath GetDarwinUserDirectory(DarwinUserDirectory directory) { + int confstr_name; + switch (directory) { + case DarwinUserDirectory::kUser: + confstr_name = _CS_DARWIN_USER_DIR; + break; + case DarwinUserDirectory::kUserCache: + confstr_name = _CS_DARWIN_USER_CACHE_DIR; + break; + case DarwinUserDirectory::kUserTemp: + confstr_name = _CS_DARWIN_USER_TEMP_DIR; + break; + default: + NOTREACHED(); + } + + char path_buf[PATH_MAX]; + size_t rv = confstr(confstr_name, path_buf, sizeof(path_buf)); + if (rv == 0 || rv > sizeof(path_buf)) { + return FilePath(); + } + + // confstr() returns paths starting with `/var`, which is a symbolic link + // to `/private/var` on macOS. File read and write permissions granted to + // sandboxed processes do not respect non-canonical paths, so resolve symbolic + // links to return the canonicalized path. + return MakeAbsoluteFilePath(FilePath(path_buf)); +} +#endif // BUILDFLAG(IS_MAC) + } // namespace base diff --git a/content/browser/child_process_launcher_helper.h b/content/browser/child_process_launcher_helper.h index 407c063..285a40d7 100644 --- a/content/browser/child_process_launcher_helper.h +++ b/content/browser/child_process_launcher_helper.h @@ -18,6 +18,7 @@ #include "base/task/sequenced_task_runner.h" #include "base/time/time.h" #include "build/build_config.h" +#include "content/common/content_export.h" #include "content/public/browser/browser_child_process_host.h" #include "content/public/browser/child_process_launcher_utils.h" #include "content/public/common/result_codes.h" @@ -82,6 +83,12 @@ using FileMappedForLaunch = base::HandlesToInheritVector; #endif +#if BUILDFLAG(IS_MAC) +// Returns the suffix used to identify process-type-isolated Darwin user +// directories. +CONTENT_EXPORT std::string GetDarwinUserDirSuffix(std::string process_type); +#endif + #if BUILDFLAG(IS_IOS) class LaunchResult; @@ -282,6 +289,14 @@ void LaunchOnLauncherThread(); +#if BUILDFLAG(IS_MAC) + // Creates per process-type isolated directories for a child process within + // the Darwin system user directories (User, Cache, Temp). Updates `options`'s + // environment to configure the suffix, ensuring the child process uses the + // isolated directories. Returns true if successful. + void CreateProcessTypeDarwinUserDirs(base::LaunchOptions* options); +#endif + // Update command line and mapped handles if a log handle is being passed. void PassLoggingSwitches(base::LaunchOptions* launch_options, base::CommandLine* cmd_line); diff --git a/content/browser/child_process_launcher_helper_mac.cc b/content/browser/child_process_launcher_helper_mac.cc index 72fb33d..0eeb496b 100644 --- a/content/browser/child_process_launcher_helper_mac.cc +++ b/content/browser/child_process_launcher_helper_mac.cc @@ -4,12 +4,19 @@ #include "content/browser/child_process_launcher_helper.h" +#include "base/apple/bundle_locations.h" +#include "base/apple/foundation_util.h" #include "base/apple/mach_port_rendezvous.h" +#include "base/check.h" #include "base/command_line.h" #include "base/containers/flat_map.h" +#include "base/environment.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" #include "base/no_destructor.h" #include "base/path_service.h" #include "base/posix/global_descriptors.h" +#include "base/strings/strcat.h" #include "base/strings/stringprintf.h" #include "base/synchronization/lock.h" #include "base/thread_annotations.h" @@ -70,8 +77,22 @@ base::flat_map<sandbox::mojom::Sandbox, std::string> cache_ GUARDED_BY(lock_); }; +base::FilePath CreateSandboxChildDir(base::DarwinUserDirectory directory, + const std::string& suffix) { + base::FilePath parent_dir = base::GetDarwinUserDirectory(directory); + CHECK(!parent_dir.empty()); + base::FilePath path = parent_dir.Append(suffix); + CHECK(base::CreateDirectory(path)) + << "Failed to create directory at " << path; + return path; +} + } // namespace +std::string GetDarwinUserDirSuffix(std::string process_type) { + return base::StrCat({base::apple::BaseBundleID(), ".helper.", process_type}); +} + std::optional<mojo::NamedPlatformChannel> ChildProcessLauncherHelper::CreateNamedPlatformChannelOnLauncherThread() { CHECK(CurrentlyOnProcessLauncherTaskRunner(), base::NotFatalUntil::M159); @@ -123,6 +144,10 @@ sandbox::policy::IsUnsandboxedSandboxType(sandbox_type); if (!no_sandbox) { + if (delegate_->NeedsIsolatedDarwinUserDirs()) { + CreateProcessTypeDarwinUserDirs(options); + } + if (!LOG_IS_ON(INFO)) { // Disable os logging to com.apple.diagnosticd when logging is not // enabled. The system logging has a measureable performance impact. @@ -143,8 +168,8 @@ can_cache_policy ? sandbox::SandboxSerializer::Target::kCompiled : sandbox::SandboxSerializer::Target::kSource); compiler.SetProfile(sandbox::policy::GetSandboxProfile(sandbox_type)); - const bool sandbox_ok = - SetupSandboxParameters(sandbox_type, *command_line_.get(), &compiler); + const bool sandbox_ok = SetupSandboxParameters( + sandbox_type, *command_line_.get(), options->environment, &compiler); if (!sandbox_ok) { LOG(ERROR) << "Sandbox setup failed."; @@ -180,6 +205,44 @@ return true; } +void ChildProcessLauncherHelper::CreateProcessTypeDarwinUserDirs( + base::LaunchOptions* options) { + DCHECK(CurrentlyOnProcessLauncherTaskRunner()); + + std::unique_ptr<base::Environment> env(base::Environment::Create()); + CHECK(!env->GetVar(base::env_vars::kDirHelperUserDirSuffix).has_value()) + << base::env_vars::kDirHelperUserDirSuffix + << " is used to create sandboxed subdirectories for child processes and " + "cannot be set"; + + auto sandbox_type = + sandbox::policy::SandboxTypeFromCommandLine(*command_line_); + std::string user_dir_suffix = GetDarwinUserDirSuffix( + sandbox::policy::StringFromSandboxType(sandbox_type)); + CreateSandboxChildDir(base::DarwinUserDirectory::kUser, user_dir_suffix); + CreateSandboxChildDir(base::DarwinUserDirectory::kUserCache, user_dir_suffix); + base::FilePath temp_user_temp_dir = CreateSandboxChildDir( + base::DarwinUserDirectory::kUserTemp, user_dir_suffix); + + // Set DIRHELPER_USER_DIR_SUFFIX to customize the path provided by + // NSTemporaryDirectory() and other paths retrieved from confstr. + // + // Note that DIRHELPER_USER_DIR_SUFFIX *does not* support nested paths. + // Providing more than one path component will cause this environment + // variable to be ignored. + options->environment[base::env_vars::kDirHelperUserDirSuffix] = + user_dir_suffix; + + // Prevent the child process from inheriting MAC_CHROMIUM_TMPDIR if set, + // ensuring base::GetTempDir() in the child process falls back to + // NSTemporaryDirectory(), which respects DIRHELPER_USER_DIR_SUFFIX. + options->environment[base::env_vars::kMacChromiumTmpDir] = ""; + + // Set TMPDIR to the isolated temp directory path so code that inspects + // TMPDIR directly uses the permitted directory. + options->environment[base::env_vars::kTmpDir] = temp_user_temp_dir.value(); +} + ChildProcessLauncherHelper::Process ChildProcessLauncherHelper::LaunchProcessOnLauncherThread( const base::LaunchOptions* options, diff --git a/content/browser/gpu/gpu_process_host.cc b/content/browser/gpu/gpu_process_host.cc index fe2a75c..b8f1f3f 100644 --- a/content/browser/gpu/gpu_process_host.cc +++ b/content/browser/gpu/gpu_process_host.cc @@ -139,6 +139,13 @@ namespace content { +#if BUILDFLAG(IS_MAC) +// If enabled, the macOS sandbox for the GPU process will use process-type- +// specific subdirectories of the darwin user directories instead of the +// non-process-isolated directories themselves. +BASE_FEATURE(kMacGpuSandboxDarwinUserDirs, base::FEATURE_ENABLED_BY_DEFAULT); +#endif + base::subtle::Atomic32 GpuProcessHost::gpu_crash_count_ = 0; bool GpuProcessHost::crashed_before_ = false; int GpuProcessHost::recent_crash_count_ = 0; @@ -472,6 +479,12 @@ return sandbox::mojom::Sandbox::kGpu; } +#if BUILDFLAG(IS_MAC) + bool NeedsIsolatedDarwinUserDirs() override { + return base::FeatureList::IsEnabled(kMacGpuSandboxDarwinUserDirs); + } +#endif + private: #if BUILDFLAG(IS_WIN) // These values are persisted to logs. Entries should not be renumbered and diff --git a/content/browser/gpu/gpu_process_host.h b/content/browser/gpu/gpu_process_host.h index 98aea8754..3e11c55 100644 --- a/content/browser/gpu/gpu_process_host.h +++ b/content/browser/gpu/gpu_process_host.h @@ -51,6 +51,8 @@ #if BUILDFLAG(IS_MAC) class BrowserChildProcessBackgroundedBridge; class CATransactionGPUCoordinator; + +CONTENT_EXPORT BASE_DECLARE_FEATURE(kMacGpuSandboxDarwinUserDirs); #endif class GpuProcessHost final : public BrowserChildProcessHostDelegate, diff --git a/content/browser/sandbox_parameters_mac.h b/content/browser/sandbox_parameters_mac.h index 89e9c6b..44f8da3 100644 --- a/content/browser/sandbox_parameters_mac.h +++ b/content/browser/sandbox_parameters_mac.h @@ -5,6 +5,7 @@ #ifndef CONTENT_BROWSER_SANDBOX_PARAMETERS_MAC_H_ #define CONTENT_BROWSER_SANDBOX_PARAMETERS_MAC_H_ +#include "base/environment.h" #include "content/common/content_export.h" namespace base { @@ -26,6 +27,7 @@ CONTENT_EXPORT bool SetupSandboxParameters( sandbox::mojom::Sandbox sandbox_type, const base::CommandLine& command_line, + const base::EnvironmentMap& env, sandbox::SandboxSerializer* serializer); // Expands the SandboxType::kNetwork policy to allow reading files from diff --git a/content/browser/sandbox_parameters_mac.mm b/content/browser/sandbox_parameters_mac.mm index b97b32b..b72a3eb 100644 --- a/content/browser/sandbox_parameters_mac.mm +++ b/content/browser/sandbox_parameters_mac.mm @@ -13,6 +13,7 @@ #include "base/check.h" #include "base/command_line.h" #include "base/feature_list.h" +#include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/mac/mac_util.h" #include "base/no_destructor.h" @@ -68,33 +69,29 @@ // Retrieves the users shared darwin dirs and adds it to the profile. void AddDarwinDirs(sandbox::SandboxSerializer* serializer) { - char dir_path[PATH_MAX + 1]; - - size_t rv = confstr(_CS_DARWIN_USER_CACHE_DIR, dir_path, sizeof(dir_path)); - PCHECK(rv != 0); - CHECK(serializer->SetParameter( - sandbox::policy::kParamDarwinUserCacheDir, - sandbox::policy::GetCanonicalPath(base::FilePath(dir_path)).value())); - - rv = confstr(_CS_DARWIN_USER_DIR, dir_path, sizeof(dir_path)); - PCHECK(rv != 0); - CHECK(serializer->SetParameter( - sandbox::policy::kParamDarwinUserDir, - sandbox::policy::GetCanonicalPath(base::FilePath(dir_path)).value())); - - rv = confstr(_CS_DARWIN_USER_TEMP_DIR, dir_path, sizeof(dir_path)); - PCHECK(rv != 0); - CHECK(serializer->SetParameter( - sandbox::policy::kParamDarwinUserTempDir, - sandbox::policy::GetCanonicalPath(base::FilePath(dir_path)).value())); + base::FilePath user_dir = + base::GetDarwinUserDirectory(base::DarwinUserDirectory::kUser); + CHECK(!user_dir.empty()); + CHECK(serializer->SetParameter(sandbox::policy::kParamDarwinUserDir, + user_dir.value())); + base::FilePath user_cache_dir = ... (file diff truncated)
Regression Test / PoC
diff --git a/base/test/launcher/test_launcher.cc b/base/test/launcher/test_launcher.cc
index e5cf52b..e2ef6af 100644
--- a/base/test/launcher/test_launcher.cc
+++ b/base/test/launcher/test_launcher.cc
@@ -665,9 +665,9 @@
#if BUILDFLAG(IS_WIN)
environment->emplace(L"TMP", temp_dir.value());
#elif BUILDFLAG(IS_APPLE)
- environment->emplace("MAC_CHROMIUM_TMPDIR", temp_dir.value());
+ environment->emplace(env_vars::kMacChromiumTmpDir, temp_dir.value());
#elif BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
- environment->emplace("TMPDIR", temp_dir.value());
+ environment->emplace(env_vars::kTmpDir, temp_dir.value());
#endif
}
diff --git a/content/browser/child_process_launcher_browsertest.cc b/content/browser/child_process_launcher_browsertest.cc
index 2f6f5bd..fd63752 100644
--- a/content/browser/child_process_launcher_browsertest.cc
+++ b/content/browser/child_process_launcher_browsertest.cc
@@ -2,10 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include "content/browser/child_process_launcher.h"
+
#include "base/command_line.h"
+#include "base/files/file_enumerator.h"
#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
-#include "content/browser/child_process_launcher.h"
+#include "content/browser/gpu/gpu_process_host.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_frame_host.h"
@@ -17,6 +20,11 @@
#include "content/public/test/no_renderer_crashes_assertion.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
+#include "sandbox/policy/sandbox_type.h"
+
+#if BUILDFLAG(IS_MAC)
+#include "base/files/file_util.h"
+#endif
namespace {
@@ -97,4 +105,45 @@
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), url);
}
+#if BUILDFLAG(IS_MAC)
+// Verifies that isolated Darwin user directories are created for sandboxed
+// child processes that require them (like the GPU process).
+IN_PROC_BROWSER_TEST_F(ChildProcessLauncherBrowserTest,
+ IsolatedDarwinUserDirs) {
+ if (!base::FeatureList::IsEnabled(kMacGpuSandboxDarwinUserDirs)) {
+ GTEST_SKIP() << "Sandboxed directories will not be created for the GPU "
+ "process on macOS when kMacGpuSandboxDarwinUserDirs is "
+ "disabled.";
+ }
+ base::ScopedAllowBlockingForTesting allow_blocking;
+
+ std::string expected_prefix = internal::GetDarwinUserDirSuffix(
+ sandbox::policy::StringFromSandboxType(sandbox::mojom::Sandbox::kGpu));
+ auto FindIsolatedDirsInParent =
+ [&](const base::FilePath& dir_path) -> std::vector<base::FilePath> {
+ std::vector<base::FilePath> results;
+ base::FileEnumerator enumerator(dir_path, /*recursive=*/false,
+ base::FileEnumerator::DIRECTORIES);
+ for (base::FilePath entry = enumerator.Next(); !entry.empty();
+ entry = enumerator.Next()) {
+ if (entry.BaseName().value().find(expected_prefix) == 0) {
+ results.push_back(entry);
+ }
+ }
+ return results;
+ };
+
+ std::vector<base::FilePath> user_dirs = FindIsolatedDirsInParent(
+ base::GetDarwinUserDirectory(base::DarwinUserDirectory::kUser));
+ std::vector<base::FilePath> user_cache_dirs = FindIsolatedDirsInParent(
+ base::GetDarwinUserDirectory(base::DarwinUserDirectory::kUserCache));
+ std::vector<base::FilePath> user_temp_dirs = FindIsolatedDirsInParent(
+ base::GetDarwinUserDirectory(base::DarwinUserDirectory::kUserTemp));
+
+ EXPECT_EQ(user_dirs.size(), 1u);
+ EXPECT_EQ(user_cache_dirs.size(), 1u);
+ EXPECT_EQ(user_temp_dirs.size(), 1u);
+}
+#endif // BUILDFLAG(IS_MAC)
+
} // namespace content
diff --git a/content/browser/sandbox_mac_unittest.mm b/content/browser/sandbox_mac_unittest.mm
index 4743e22..8105fc6 100644
--- a/content/browser/sandbox_mac_unittest.mm
+++ b/content/browser/sandbox_mac_unittest.mm
@@ -6,14 +6,21 @@
#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
+#include <dirent.h>
#include <fcntl.h>
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/command_line.h"
+#include "base/files/file.h"
+#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
+#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
+#include "base/functional/callback_helpers.h"
+#include "base/json/json_reader.h"
+#include "base/json/json_writer.h"
#include "base/mac/mac_util.h"
#include "base/memory/read_only_shared_memory_region.h"
#include "base/memory/ref_counted.h"
@@ -21,11 +28,15 @@
#include "base/posix/eintr_wrapper.h"
#include "base/process/kill.h"
#include "base/strings/strcat.h"
+#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/multiprocess_test.h"
+#include "base/test/scoped_command_line.h"
#include "base/test/test_timeouts.h"
+#include "base/uuid.h"
+#include "base/values.h"
#include "content/browser/sandbox_parameters_mac.h"
#include "sandbox/mac/sandbox_serializer.h"
#include "sandbox/mac/seatbelt.h"
@@ -59,15 +70,21 @@
}
void ExecuteWithParams(const std::string& procname,
- sandbox::mojom::Sandbox sandbox_type) {
+ sandbox::mojom::Sandbox sandbox_type,
+ const std::string& suffix = "") {
std::string profile =
sandbox::policy::GetSandboxProfile(sandbox_type) + kTempDirSuffix;
sandbox::SandboxSerializer serializer(
sandbox::SandboxSerializer::Target::kSource);
serializer.SetProfile(profile);
- SetupSandboxParameters(
- sandbox_type, *base::CommandLine::ForCurrentProcess(), &serializer);
+ base::EnvironmentMap env;
+ if (!suffix.empty()) {
+ env[base::env_vars::kDirHelperUserDirSuffix] = suffix;
+ }
+ SetupSandboxParameters(sandbox_type,
+ *base::CommandLine::ForCurrentProcess(), env,
+ &serializer);
std::string error, serialized;
CHECK(serializer.SerializePolicy(serialized, error)) << error;
@@ -77,6 +94,9 @@
base::LaunchOptions options;
options.fds_to_remap.emplace_back(pipe_, pipe_);
+ if (!suffix.empty()) {
+ options.environment[base::env_vars::kDirHelperUserDirSuffix] = suffix;
+ }
base::Process process = SpawnChildWithOptions(procname, options);
ASSERT_TRUE(process.IsValid());
@@ -256,4 +276,174 @@
sandbox::mojom::Sandbox::kProxyResolver);
}
+MULTIPROCESS_TEST_MAIN(GpuIsolatedDarwinUserDirsProcess) {
+ CheckCreateSeatbeltServer();
+
+ std::unique_ptr<base::Environment> env(base::Environment::Create());
+ std::optional<std::string> suffix =
+ env->GetVar(base::env_vars::kDirHelperUserDirSuffix);
+ CHECK(suffix.has_value() && !suffix->empty());
+
+ std::string parent_file_name =
+ base::StringPrintf("parent_file_%s.txt", suffix->c_str());
+ std::string new_file_name =
+ base::StringPrintf("new_test_file_%s.txt", suffix->c_str());
+
+ auto CheckWriteDenied = [&](const base::FilePath& parent_dir) {
+ // Check creating a new file is denied.
+ base::FilePath new_file_path = parent_dir.Append(new_file_name);
+ base::File new_file(
+ new_file_path, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
+ CHECK(!new_file.IsValid());
+ CHECK_EQ(new_file.error_details(), base::File::FILE_ERROR_ACCESS_DENIED);
+
+ // Check modifying an existing file is denied.
+ base::FilePath existing_file_path = parent_dir.Append(parent_file_name);
+ base::File existing_file(existing_file_path,
+ base::File::FLAG_OPEN | base::File::FLAG_WRITE);
+ CHECK(!existing_file.IsValid());
+ CHECK_EQ(existing_file.error_details(),
+ base::File::FILE_ERROR_ACCESS_DENIED);
+ };
+
+ auto CheckWriteAllowed = [](const base::FilePath& child_dir) {
+ // Check creating a new file is allowed.
+ base::FilePath new_file_path = child_dir.Append("new_test_file.txt");
+ base::File new_file(
+ new_file_path, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
+ CHECK(new_file.IsValid());
+
+ // Check modifying an existing file is allowed.
+ base::FilePath existing_file_path = child_dir.Append("child_file.txt");
+ base::File existing_file(existing_file_path,
+ base::File::FLAG_OPEN | base::File::FLAG_WRITE);
+ CHECK(existing_file.IsValid());
+ };
+
+ auto CheckReadDenied = [&](const base::FilePath& parent_dir) {
+ // Check directory read (opendir) is denied.
+ DIR* dir = opendir(parent_dir.value().c_str());
+ CHECK(!dir);
+ CHECK(errno == EACCES || errno == EPERM);
+
+ // Check file read is denied.
+ base::FilePath file_path = parent_dir.Append(parent_file_name);
+ base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
+ CHECK(!file.IsValid());
+ CHECK_EQ(file.error_details(), base::File::FILE_ERROR_ACCESS_DENIED);
+ };
+
+ auto CheckReadAllowed = [](const base::FilePath& child_dir) {
+ // Check directory read (opendir) is allowed.
+ DIR* dir = opendir(child_dir.value().c_str());
+ CHECK(dir);
+ closedir(dir);
+
+ // Check file read is allowed.
+ base::FilePath file_path = child_dir.Append("child_file.txt");
+ base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
+ CHECK(file.IsValid());
+ char buf[100];
+ std::optional<size_t> bytes_read =
+ file.ReadAtCurrentPos(base::as_writable_bytes(base::span(buf)));
+ CHECK(bytes_read.has_value());
+ CHECK_GT(*bytes_read, 0u);
+ std::string contents(buf, *bytes_read);
+ CHECK_EQ(contents, "sandbox_read_test");
+ };
+
+ base::DarwinUserDirectory dirs[] = {
+ base::DarwinUserDirectory::kUser,
+ base::DarwinUserDirectory::kUserCache,
+ base::DarwinUserDirectory::kUserTemp,
+ };
+
+ for (auto type : dirs) {
+ base::FilePath child = base::GetDarwinUserDirectory(type);
+ base::FilePath parent = child.DirName();
+
+ CheckWriteDenied(parent);
+ CheckReadDenied(parent);
+
+ CheckWriteAllowed(child);
+ CheckReadAllowed(child);
+ }
+
+ return 0;
+}
+
+// Verifies that when sandbox-child-user-dir, sandbox-child-user-cache-dir,
+// and sandbox-child-user-temp-dir are set, the GPU process seatbelt sandbox
+// denies access to the parent directories and allows access to the isolated
+// child directories.
+TEST_F(SandboxMacTest, GpuIsolatedDarwinUserDirs) {
+ base::FilePath system_user =
+ base::GetDarwinUserDirectory(base::DarwinUserDirectory::kUser);
+ base::FilePath system_cache =
+ base::GetDarwinUserDirectory(base::DarwinUserDirectory::kUserCache);
+ base::FilePath system_temp =
+ base::GetDarwinUserDirectory(base::DarwinUserDirectory::kUserTemp);
+
+ ASSERT_FALSE(system_user.empty());
+ ASSERT_FALSE(system_cache.empty());
+ ASSERT_FALSE(system_temp.empty());
+
+ // Add a random suffix to avoid bot test flakiness
+ std::string suffix = base::Uuid::GenerateRandomV4().AsLowercaseString();
+
+ base::FilePath child_user_raw = system_user.Append(suffix);
+ base::FilePath child_cache_raw = system_cache.Append(suffix);
+ base::FilePath child_temp_raw = system_temp.Append(suffix);
+
+ ASSERT_TRUE(base::CreateDirectory(child_user_raw));
+ ASSERT_TRUE(base::CreateDirectory(child_cache_raw));
+ ASSERT_TRUE(base::CreateDirectory(child_temp_raw));
+
+ base::FilePath child_user = sandbox::policy::GetCanonicalPath(child_user_raw);
+ base::FilePath child_cache =
+ sandbox::policy::GetCanonicalPath(child_cache_raw);
+ base::FilePath child_temp = sandbox::policy::GetCanonicalPath(child_temp_raw);
... (truncated)
Original Bug Report
TOCTOU Symlink-Swap in macOS feedback_util::ZipString leads to 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 Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists on macOS in feedback_util::ZipString. The browser creates a temporary file under the shared temporary directory, discards the secure file descriptor, and subsequently reopens the path via fopen inside minizip, which resolves symbolic links. A compromised sandboxed child process with write permissions to this directory can swap the file with a symbolic link to overwrite arbitrary files with browser privileges.
Affected files:
components/feedback/feedback_util.cc
Estimated timestamp from git blame: 2013-03-22
Root Cause Analysis
In components/feedback/feedback_util.cc, feedback_util::ZipString generates a temporary file to store compressed log information. The temporary file is created via base::CreateTemporaryFile, which calls CreateTemporaryFileInDir in POSIX environments. Under the hood, this function securely creates a file with unique random characters and 0600 permissions using mkstemp and returns a valid file descriptor. However, CreateTemporaryFileInDir discards this file descriptor upon returning, meaning the secure file handle is closed:
// base/files/file_util_posix.cc:862-867
bool CreateTemporaryFileInDir(const FilePath& dir, FilePath* temp_file) {
ScopedBlockingCall scoped_blocking_call(FROM_HERE, BlockingType::MAY_BLOCK);
ScopedFD fd = CreateAndOpenFdForTemporaryFileInDir(dir, temp_file);
return fd.is_valid();
}
This leaves the browser process holding only a plain path string in zip_file. It then attempts to perform zip compression into this path:
// components/feedback/feedback_util.cc:53-59
if (!base::CreateTemporaryFile(&zip_file)) {
return std::nullopt;
}
std::string compressed_logs;
bool succeed = zip::Zip(temp_dir.GetPath(), zip_file, false) &&
base::ReadFileToString(zip_file, &compressed_logs);
When zip::Zip executes, the path is opened via zipOpen2_64 inside the minizip library, which maps to fopen(path, "wb"). Standard fopen follows symbolic links and does not employ flags like O_NOFOLLOW or O_EXCL during open.
Shared Trust Boundary on macOS
While macOS sandboxed helper processes (such as the Network Service, GPU, or On-Device Model processes) run inside a sandbox, they are granted read/write permissions to the shared user temporary directory _CS_DARWIN_USER_TEMP_DIR via custom Seatbelt sandbox profiles. For example, the Network Service sandbox profile (sandbox/policy/mac/network.sb) allows:
(allow file-read* file-write*
(subpath (param darwin-user-temp-dir)))
And the On-Device Model Execution profile (sandbox/policy/mac/on_device_model_execution.sb) similarly allows:
(allow file-read* file-write*
(subpath (param darwin-user-temp-dir)))
Because all Chrome processes execute under the same macOS UID, standard POSIX filesystem permissions offer no isolation inside this shared directory. Thus, a compromised helper process is fully capable of deleting or swapping files in the temporary directory.
Potential Attack Scenario
Note: These are potential steps as our tooling is static and we have not run an active proof-of-concept.
- An attacker gains remote code execution (RCE) in a sandboxed child process, such as the Network Service process.
- The compromised child process spawns a thread to continuously poll
SHARED_TEMP_DIR(e.g.,/var/folders/xx/yyyyy_zzzz/T/) looking for entries matching.com.google.Chrome.*. - The user initiates a feedback report (e.g., via Help -> Report an Issue), which triggers
feedback_util::ZipStringon a background thread in the browser process. - Between the browser closing the secure
mkstempfile descriptor inbase::CreateTemporaryFileand re-opening it inzip::Zip, a tight TOCTOU race window opens. - The attacker’s polling thread detects the temporary file, immediately deletes it via
unlink, and replaces it with a symbolic link pointing to a critical file in the user’s home folder (e.g.,~/.zshrc). - The browser process executes
zip::Zip, which opens the path withfopen(path, "wb"), resolving the symlink and truncating the target file to 0 bytes before writing a zip payload inside. - Alternatively, the attacker can target the second race window between
zip::Zipcompleting andbase::ReadFileToStringreading the file. By replacing the temporary zip file with a symbolic link to a sensitive resource (e.g.,~/.ssh/id_rsa), the browser reads the file contents into the feedback attachment. The attacker, controlling the compromised Network Service, can intercept and exfiltrate this sensitive data during upload.
Impact
If successfully exploited, this provides a sandbox escape on macOS, allowing a compromised helper process to achieve arbitrary file writes (with truncated/zip content) and arbitrary file reads with browser privilege.
Suggested Remediation
Instead of discarding the file descriptor and opening the temporary zip file by path, the code should utilize a file descriptor-based API. Specifically, use base::CreateAndOpenTemporaryFile (or similar helper) to securely hold onto the open file descriptor, and then utilize the descriptor-based zipping API zip::ZipFiles which accepts a destination file descriptor dest_fd instead of a FilePath destination:
// Use a file descriptor based flow rather than path based zip operations
bool ZipFiles(const base::FilePath& src_dir,
Paths src_relative_paths,
int dest_fd);
Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf
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.
Raised in root component due to access or custom field issues on 1033360