CVE-2026-91727
Overview
Background
- Darwin user directories
- the per-user scratch locations macOS exposes via
confstr(_CS_DARWIN_USER_DIR,_CS_DARWIN_USER_TEMP_DIR,_CS_DARWIN_USER_CACHE_DIR), resolving to/var/folders/.../{0,T,C}/and shared across every process in a user session. - Seatbelt sandbox profile
- the macOS
.sbpolicy (e.g.sandbox/policy/mac/gpu.sb) that grants a child process file-read/write access by literal path, evaluated against canonical paths only. - `DIRHELPER_USER_DIR_SUFFIX`
- an environment variable read by the macOS
dirhelperservice that causes it to append a caller-chosen suffix to the default_CS_DARWIN_*paths, so a process can be steered to a private subdirectory. - Exclusive directory creation
- creating a directory that fails outright if the leaf already exists or a parent is missing, so a pre-existing (potentially attacker-planted) directory can never be silently adopted.
Root Cause Analysis
Before the patch, sandboxed child processes on macOS — the GPU process in particular — were granted seatbelt access to the shared per-user Darwin directories (/var/folders/.../0/, /T/, /C/), which are common to the browser process and every other process the user runs. This broad grant meant a compromised child could read and tamper with files that other processes placed in those shared scratch locations, violating the invariant that a sandboxed child’s writable filesystem footprint should be private to that process. A second, related defect is reference resolution: confstr returns paths under /var, which is a symlink to /private/var, and the code notes that “file read and write permissions granted to sandboxed processes do not respect non-canonical paths,” so grants keyed on the raw /var path did not match the real canonical path.
The fix creates a per-process subdirectory inside each Darwin directory at launch time (CreateProcessIsolatedDarwinUserDirs), steers the child into it via DIRHELPER_USER_DIR_SUFFIX, tightens the seatbelt profile to that isolated subtree, canonicalizes the paths with MakeAbsoluteFilePath in GetDarwinUserDirectory, and creates the subdirectory with CreateDirectoryExclusive so an already-present/squatted directory can never be adopted. This works because each child now sees only its own freshly-created, canonically-named directory rather than the shared per-user tree.
/var paths; the fix scopes each child to an exclusively-created, symlink-resolved, per-process subdirectory keyed by a unique DIRHELPER_USER_DIR_SUFFIX.Attack Path
- Compromise a sandboxed child An attacker achieves code execution inside the GPU process (or another child using the shared Darwin dirs) via a separate renderer/GPU bug, giving them the child’s seatbelt-limited privileges.
- Reach the shared scratch tree
Because the pre-patch seatbelt profile grants read/write to the whole
/var/folders/.../{0,T,C}/per-user directories, the compromised child can enumerate and open files that the browser process or unrelated user processes have written there. - Read or tamper with cross-process files (inferred)
The child leverages that shared access to exfiltrate sensitive data left in the shared temp/cache dirs or to overwrite files another, higher-privileged process later reads, since nothing isolates its filesystem view. The specific target files and any resulting corruption are not shown by the diff and are
(inferred). - Exploit path-canonicalization mismatch (inferred)
Where grants were keyed on the raw
/varsymlink path rather than/private/var, the mismatch between the granted path and the enforced canonical path could widen or misalign what the sandbox actually permits; the diff shows the canonicalization fix but not a concrete exploit of the mismatch, so this is(inferred). - Escalation (inferred)
Any escalation beyond shared-directory read/write — full sandbox escape or code execution in another process — is not demonstrated by the patch and is
(inferred).
Impact Assessment
LogicError in the Extensions component (“incorrect reference resolution”), which aligns with the non-canonical-path / shared-directory resolution defect fixed here, but the metadata’s component labeling does not match the patch’s macOS sandbox scope and should be treated cautiously (inferred). No exploited-in-the-wild status or severity rating is stated in the provided advisory data, and any sandbox escape or code-execution escalation beyond shared-directory access is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifbase/files/file_util_apple.mm |
modified | |
switchbase/files/file_util_apple.mm |
modified | |
ifcontent/browser/child_process_launcher_helper.cc |
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 | |
forcontent/browser/child_process_launcher_helper_mac.cc |
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.mmcontent/browser/sandboxed_process_launcher_delegate.ccthird_party/blink/web_tests/WebGPUExpectationscontent/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.mmcontent/browser/child_process_launcher_helper_mac.ccsandbox/policy/mac/gpu.sbbase/test/launcher/test_launcher.cccontent/browser/gpu/gpu_process_host.ccbase/files/scoped_temp_dir.hcontent/browser/child_process_launcher_helper.ccbase/environment.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.ccbase/files/scoped_temp_dir.ccbase/files/scoped_temp_dir_unittest.cc
Audit Directions
- Shared per-user scratch grantsAudit every seatbelt/
.sbrule and sandbox parameter that grants access to_CS_DARWIN_USER_*or other shared per-user locations; any child still pointed at the shared tree rather than a private subdirectory has the same exposure. - Non-canonical path in security checksLook for permission grants, allow-lists, or path comparisons that use raw
/var(or other symlinked) paths withoutMakeAbsoluteFilePath/canonicalization, since seatbelt and other enforcers only honor canonical paths. - Non-exclusive directory/file creation in privileged setupReview process-launch and temp-dir setup code that creates directories with
CreateDirectory/Setrather than exclusive creation, where a pre-existing or attacker-planted path could be silently adopted or its contents reused.
Patch
From 184ba1c1ecc60dc52f1b6480b14828b8311e67fb Mon Sep 17 00:00:00 2001 From: Bryan Oltman <[email protected]> Date: Fri, 14 Aug 2026 12:34:10 -0700 Subject: [PATCH] [macOS] Use process-isolated 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 isolated per-process subdirectories, created at process launch time. Key changes: 1. Update the GPU process's seatbelt profile to tighten the current broad access to the Darwin user directories to a process-isolated 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 isolated 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. This is a roll-forward of https://crrev.com/c/7965675 with fixes: 1. common.sb has been updated to allow issuance of sandbox extensions for the new Metal shader cache location, fixing some of the CI failures caused by the original CL. 2. `ChildProcessLauncherHelper::CreateProcessIsolatedDarwinUserDirs` uses `CreateUniqueTempDirUnderPath` (with the Darwin user temp dir as the path) instead of `CreateUniqueTempDir` to avoid providing a subdir of MAC_CHROMIUM_TMPDIR as the sandboxed Darwin user temp dir. 3. More WPT test expectation updates. Fixed: 528379972,520019273,516947138,521559611,521486621,516893912 Cq-Include-Trybots: luci.chromium.try:mac15-x64-rel-tests Change-Id: I0572c413180723aed8369b959335aef099b4ab6a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8209919 Reviewed-by: Avi Drissman <[email protected]> Commit-Queue: Bryan Oltman <[email protected]> Reviewed-by: Mark Mentovai <[email protected]> Cr-Commit-Position: refs/heads/main@{#1679814} --- 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 32e7134..ca2e5f20 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 @@ -522,6 +534,11 @@ // Backward-compatible convenience method for the above. BASE_EXPORT bool CreateDirectory(const FilePath& full_path); +// Creates a directory exclusively. Unlike `CreateDirectory()`, this function +// fails if the directory at the leaf path already exists or if any parent +// directory does not exist. +BASE_EXPORT File::Error CreateDirectoryExclusive(const FilePath& full_path); + // Returns the file size, or std::nullopt on failure. BASE_EXPORT std::optional<int64_t> GetFileSize(const FilePath& file_path); 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/base/files/scoped_temp_dir.cc b/base/files/scoped_temp_dir.cc index 09262ae..756650a8 100644 --- a/base/files/scoped_temp_dir.cc +++ b/base/files/scoped_temp_dir.cc @@ -68,6 +68,19 @@ return true; } +#if BUILDFLAG(IS_MAC) +bool ScopedTempDir::CreateDirectoryExclusive(const FilePath& path) { + CHECK(path_.empty()); + + if (base::File::Mkdir(path, S_IRWXU) != File::FILE_OK) { + return false; + } + + path_ = path; + return true; +} +#endif + bool ScopedTempDir::Set(const FilePath& path) { if (!path_.empty()) { return false; diff --git a/base/files/scoped_temp_dir.h b/base/files/scoped_temp_dir.h index a765d05..89aab0d8 100644 --- a/base/files/scoped_temp_dir.h +++ b/base/files/scoped_temp_dir.h @@ -47,6 +47,13 @@ const FilePath& path, FilePath::StringViewType prefix = GetDefaultTempDirPrefix()); +#if BUILDFLAG(IS_MAC) + // Creates a directory exclusively at |path| and takes ownership of it (fails + // if the directory already exists or if any parent directory does not exist). + // Don't call multiple times unless Take() has been called first. + [[nodiscard]] bool CreateDirectoryExclusive(const FilePath& path); +#endif + // Takes ownership of directory at |path|, creating it if necessary. // Don't call multiple times unless Take() has been called first. [[nodiscard]] bool Set(const FilePath& path); diff --git a/content/browser/child_process_launcher_helper.cc b/content/browser/child_process_launcher_helper.cc index 34a9ba1..d0d0d8f 100644 --- a/content/browser/child_process_launcher_helper.cc +++ b/content/browser/child_process_launcher_helper.cc @@ -254,6 +254,20 @@ base::Process::Open(process_id_.value()).ForgetPriority(); } #endif // BUILDFLAG(IS_CHROMEOS) +#if BUILDFLAG(IS_MAC) + if (scoped_sandboxed_user_dir_) { + GetProcessLauncherTaskRunner()->DeleteSoon( + FROM_HERE, std::move(scoped_sandboxed_user_dir_)); + } + if (scoped_sandboxed_user_cache_dir_) { + GetProcessLauncherTaskRunner()->DeleteSoon( + FROM_HERE, std::move(scoped_sandboxed_user_cache_dir_)); + } + if (scoped_sandboxed_user_temp_dir_) { + GetProcessLauncherTaskRunner()->DeleteSoon( + FROM_HERE, std::move(scoped_sandboxed_user_temp_dir_)); + } +#endif #if BUILDFLAG(IS_IOS) GetProcessLauncherTaskRunner()->DeleteSoon(FROM_HERE, std::move(scoped_temp_dir_)); diff --git a/content/browser/child_process_launcher_helper.h b/content/browser/child_process_launcher_helper.h index 2115126..a5af530 100644 --- a/content/browser/child_process_launcher_helper.h +++ b/content/browser/child_process_launcher_helper.h @@ -8,6 +8,7 @@ #include <memory> #include <optional> +#include "base/files/scoped_temp_dir.h" #include "base/memory/raw_ptr.h" #include "base/memory/read_only_shared_memory_region.h" #include "base/memory/ref_counted.h" @@ -18,6 +19,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" @@ -43,7 +45,7 @@ #if BUILDFLAG(IS_MAC) #include "sandbox/mac/seatbelt_exec.h" -#endif // BUILDFLAG(IS_MAC) +#endif #if BUILDFLAG(IS_FUCHSIA) #include "sandbox/policy/fuchsia/sandbox_policy_fuchsia.h" @@ -82,6 +84,12 @@ using FileMappedForLaunch = base::HandlesToInheritVector; #endif +#if BUILDFLAG(IS_MAC) +// Returns the suffix used to identify process-isolated Darwin user directories +// created by this browser process. +CONTENT_EXPORT std::string GetProcessIsolatedDarwinUserDirSuffix(); +#endif + #if BUILDFLAG(IS_IOS) class LaunchResult; @@ -282,6 +290,14 @@ void LaunchOnLauncherThread(); +#if BUILDFLAG(IS_MAC) + // Creates unique, process-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. + bool CreateProcessIsolatedDarwinUserDirs(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); @@ -363,6 +379,12 @@ base::win::ScopedHandle log_handle_; #endif +#if BUILDFLAG(IS_MAC) + std::unique_ptr<base::ScopedTempDir> scoped_sandboxed_user_dir_; + std::unique_ptr<base::ScopedTempDir> scoped_sandboxed_user_cache_dir_; + std::unique_ptr<base::ScopedTempDir> scoped_sandboxed_user_temp_dir_; +#endif + #if BUILDFLAG(IS_IOS) std::unique_ptr<base::ScopedTempDir> scoped_temp_dir_; std::optional<int> exit_code_; diff --git a/content/browser/child_process_launcher_helper_mac.cc b/content/browser/child_process_launcher_helper_mac.cc index 185e4ade..5a7fbee 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" @@ -69,8 +76,27 @@ base::flat_map<sandbox::mojom::Sandbox, std::string> cache_ GUARDED_BY(lock_); }; +std::unique_ptr<base::ScopedTempDir> 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); + auto scoped_dir = std::make_unique<base::ScopedTempDir>(); + CHECK(scoped_dir->CreateDirectoryExclusive(path)) + << "Failed to create process-isolated directory at " << path; + return scoped_dir; +} + } // namespace +std::string GetProcessIsolatedDarwinUserDirSuffix() { + return base::StrCat({base::apple::BaseBundleID(), ".", + base::NumberToString(base::GetCurrentProcId()), + ".child"}); +} + std::optional<mojo::NamedPlatformChannel> ChildProcessLauncherHelper::CreateNamedPlatformChannelOnLauncherThread() { DCHECK(CurrentlyOnProcessLauncherTaskRunner()); @@ -121,6 +147,11 @@ sandbox::policy::IsUnsandboxedSandboxType(sandbox_type); if (!no_sandbox) { + if (delegate_->NeedsProcessIsolatedDarwinUserDirs() && + !CreateProcessIsolatedDarwinUserDirs(options)) { + return false; + } + 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. @@ -141,8 +172,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."; @@ -178,6 +209,74 @@ return true; } +bool ChildProcessLauncherHelper::CreateProcessIsolatedDarwinUserDirs( + base::LaunchOptions* options) { + DCHECK(CurrentlyOnProcessLauncherTaskRunner()); + + base::FilePath darwin_user_temp_dir = + base::GetDarwinUserDirectory(base::DarwinUserDirectory::kUserTemp); + std::string user_dir_suffix = GetProcessIsolatedDarwinUserDirSuffix(); + + // Retry in the event that the directory name created by + // CreateUniqueTempDirUnderPath exists in one of the other darwin user dirs. + const int kMaxTries = 3; + for (int i = 0; i < kMaxTries; i++) { + auto temp_user_temp_dir = std::make_unique<base::ScopedTempDir>(); + if (!temp_user_temp_dir->CreateUniqueTempDirUnderPath(darwin_user_temp_dir, + user_dir_suffix)) { + continue; + } + + std::string last_path_component = + temp_user_temp_dir->GetPath().BaseName().value(); + ... (file diff truncated)
Regression Test / PoC
diff --git a/base/files/scoped_temp_dir_unittest.cc b/base/files/scoped_temp_dir_unittest.cc
index 0f1da4d..8f7acf0 100644
--- a/base/files/scoped_temp_dir_unittest.cc
+++ b/base/files/scoped_temp_dir_unittest.cc
@@ -163,6 +163,36 @@
EXPECT_FALSE(DirectoryExists(dir_path));
}
+#if BUILDFLAG(IS_MAC)
+TEST(ScopedTempDir, CreateDirectoryExclusive) {
+ FilePath test_path;
+ CreateNewTempDirectory(FILE_PATH_LITERAL("scoped_temp_dir"), &test_path);
+
+ // Against an existing dir, CreateDirectoryExclusive should fail.
+ EXPECT_TRUE(DirectoryExists(test_path));
+ {
+ ScopedTempDir dir;
+ EXPECT_FALSE(dir.CreateDirectoryExclusive(test_path));
+ EXPECT_FALSE(dir.IsValid());
+ }
+ // The directory should NOT be deleted, because CreateDirectoryExclusive
+ // failed and did not take ownership.
+ EXPECT_TRUE(DirectoryExists(test_path));
+
+ // If the directory does not exist, CreateDirectoryExclusive should succeed
+ // and create it.
+ EXPECT_TRUE(DeleteFile(test_path));
+ EXPECT_FALSE(DirectoryExists(test_path));
+ {
+ ScopedTempDir dir;
+ EXPECT_TRUE(dir.CreateDirectoryExclusive(test_path));
+ EXPECT_TRUE(dir.IsValid());
+ }
+ // Now it should be deleted because it went out of scope.
+ EXPECT_FALSE(DirectoryExists(test_path));
+}
+#endif
+
#if BUILDFLAG(IS_WIN)
TEST(ScopedTempDir, LockedTempDir) {
ScopedTempDir dir;
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..8553d14 100644
--- a/content/browser/child_process_launcher_browsertest.cc
+++ b/content/browser/child_process_launcher_browsertest.cc
@@ -2,10 +2,12 @@
// 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/renderer_host/render_process_host_impl.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_frame_host.h"
@@ -18,6 +20,11 @@
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
+#if BUILDFLAG(IS_MAC)
+#include "base/apple/foundation_util.h"
+#include "base/files/file_util.h"
+#endif
+
namespace {
class MockChildProcessLauncherClient
@@ -97,4 +104,40 @@
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) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+
+ std::string expected_prefix =
+ internal::GetProcessIsolatedDarwinUserDirSuffix();
+ 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> temp_dirs = FindIsolatedDirsInParent(
+ base::GetDarwinUserDirectory(base::DarwinUserDirectory::kUserTemp));
+ std::vector<base::FilePath> cache_dirs = FindIsolatedDirsInParent(
+ base::GetDarwinUserDirectory(base::DarwinUserDirectory::kUserCache));
+ std::vector<base::FilePath> user_dirs = FindIsolatedDirsInParent(
+ base::GetDarwinUserDirectory(base::DarwinUserDirectory::kUser));
+
+ EXPECT_EQ(temp_dirs.size(), 1u);
+ EXPECT_EQ(cache_dirs.size(), 1u);
+ EXPECT_EQ(user_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);
+ }
... (truncated)