Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in Core
DescriptionRace condition in Core
ComponentCore
Sub-componentMacOS
Bug ClassRace
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker516947138
Fix commit184ba1c1ecc6 (chromium/src) +631/-61
CWECWE-367 (TOCTOU race)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-08-14 (public git)
Disclosed2026-09-15
Patch gap32 days — fix public in source before the advisory

Background

Darwin per-user directories
the shared _CS_DARWIN_USER_DIR, _CS_DARWIN_USER_TEMP_DIR, and _CS_DARWIN_USER_CACHE_DIR (queried via confstr), defaulting to /var/folders/.../{0,T,C}/, which every process running as the same macOS user reads and writes in common.
Seatbelt profile
the macOS sandbox policy (sandbox/policy/mac/gpu.sb, common.sb) that grants a Chrome child process its filesystem permissions; before this change it granted the GPU process broad access to the shared Darwin user directories above.
`DIRHELPER_USER_DIR_SUFFIX`
an environment variable honored by the macOS dirhelper utility that makes confstr/NSTemporaryDirectory() append a caller-chosen suffix to the default _CS_DARWIN paths, so a process can be steered into its own subdirectory.
Exclusive directory creation
creating a directory with mkdir such that the call fails atomically (EEXIST) if the leaf already exists, so the creator can never be handed a pre-existing, attacker-planted directory or symlink.

Root Cause Analysis

Before the patch, the macOS GPU process ran with a seatbelt profile that permitted broad read/write access to the Darwin per-user directories (/var/folders/.../0/, /T/, /C/), which are shared by every process of the same user. Because these locations are predictable and world-shared (per user), the sandboxed GPU process operated on mutable filesystem state that other, non-sandboxed local processes could observe, plant, or swap — a classic time-of-check/time-of-use race on shared temp/cache files (for example the Metal shader cache), violating the invariant that a sandboxed child should be confined to storage no other process can race it on.

The fix has the browser process create a unique, process-isolated subdirectory under each Darwin user directory at child-launch time (CreateProcessIsolatedDarwinUserDirs), passes the resulting name to the child through DIRHELPER_USER_DIR_SUFFIX, and tightens the seatbelt profile so the child can only touch that private subdirectory. Crucially, those subdirectories are created with ScopedTempDir::CreateDirectoryExclusive, whose base::File::Mkdir(path, S_IRWXU) fails atomically if the path already exists, so an attacker cannot pre-create the predictably named directory (or a symlink) to redirect or share the GPU process’s I/O.

The fix works because it converts shared, guessable, race-prone directories into per-process directories that are proven newly and exclusively created before the child is allowed to use them.

Key insight
The core mistake was confining the sandboxed GPU process to broad, shared, predictably named Darwin per-user directories, leaving its temp/cache I/O open to a race against other local processes; the fix isolates each child into its own subdirectory created atomically and exclusively (mkdir that fails on EEXIST) and restricts the seatbelt profile to only that subdirectory.

Attack Path

  1. Shared directory exposure The GPU process’s pre-patch seatbelt profile grants broad access to the shared Darwin user directories, so its temp/cache files live in /var/folders/.../{T,C,0}/ alongside files of every other process of the same user.
  2. Predictable target Because the paths are fixed and shared, a co-resident local process can locate and operate on the same directory the GPU process uses, without needing to win a name-guessing lottery.
  3. Race on file/directory state The attacker races the GPU process’s creation or use of a shared file (e.g., pre-creating a directory or planting a symlink where the GPU process expects to write/read), performing a TOCTOU substitution on that shared state (inferred, as the specific raced file is not shown in the diff).
  4. Influence over sandboxed process Winning the race lets the attacker redirect or corrupt the GPU process’s temp/cache I/O, undermining the sandbox’s filesystem isolation guarantee and potentially escalating to control over the GPU process (inferred).

Impact Assessment

An attacker with the ability to run code as the same macOS user (a local co-resident process, or a compromised lower-privileged Chrome child) can race the sandboxed GPU process on the shared Darwin per-user directories, breaking the intended filesystem isolation of the sandbox. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. Any resulting sandbox bypass or code execution in the GPU process is (inferred) and not demonstrated by the patch, and no exploited-in-the-wild status or severity is stated. Preconditions are local: attacker code executing under the victim’s user account with access to the shared /var/folders/... locations.

Changed Functions

FunctionChangeNotes
if
base/files/file_util_apple.mm
modified
switch
base/files/file_util_apple.mm
modified
if
content/browser/child_process_launcher_helper.cc
modified
LaunchResult
content/browser/child_process_launcher_helper.h
modified
CreateNamedPlatformChannelOnLauncherThread
content/browser/child_process_launcher_helper_mac.cc
modified
if
content/browser/child_process_launcher_helper_mac.cc
modified
for
content/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.txt
  • content/browser/sandbox_mac_unittest.mm
  • content/browser/sandboxed_process_launcher_delegate.cc
  • third_party/blink/web_tests/WebGPUExpectations
  • content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
  • base/files/file_util.h
  • third_party/blink/web_tests/platform/mac-mac26-arm64/external/wpt/webnn/conformance_tests/resample2d-gather-shape-divergence.https.any_npu-expected.txt
  • content/browser/sandboxed_process_launcher_delegate.h
  • content/browser/child_process_launcher_helper.h
  • content/browser/sandbox_parameters_mac.mm
  • content/browser/child_process_launcher_helper_mac.cc
  • sandbox/policy/mac/gpu.sb
  • base/test/launcher/test_launcher.cc
  • content/browser/gpu/gpu_process_host.cc
  • base/files/scoped_temp_dir.h
  • content/browser/child_process_launcher_helper.cc
  • base/environment.h
  • base/files/file_util_apple.mm
  • sandbox/policy/mac/common.sb
  • third_party/blink/web_tests/platform/mac-mac26-arm64/external/wpt/webnn/conformance_tests/resample2d-gather-shape-divergence.https.any_gpu-expected.txt
  • content/browser/sandbox_parameters_mac.h
  • content/browser/child_process_launcher_browsertest.cc
  • base/files/scoped_temp_dir.cc
  • base/files/scoped_temp_dir_unittest.cc

Audit Directions

  • Shared per-user OS directories in sandboxed processes
    Audit every sandboxed child (renderer, network, utility, plugin) for grants to shared, predictably named locations such as /var/folders/..., /tmp, or other confstr/NSTemporaryDirectory() paths, and check whether the same per-process isolation should apply.
  • Non-exclusive directory/file creation on shared paths
    Look for CreateDirectory/Set/CreateUniqueTempDir (versus exclusive mkdir/O_EXCL) used against attacker-reachable directories, where a pre-created path or planted symlink could be silently adopted.
  • Non-canonical paths versus sandbox grants
    Review filesystem permissions computed from paths that are not run through MakeAbsoluteFilePath, since symlinked prefixes like /var → /private/var can cause grants and the actual accessed path to diverge and be raced.
From 184ba1c1ecc60dc52f1b6480b14828b8311e67fb Mon Sep 17 00:00:00 2001
From: Bryan Oltman <bryanoltman@google.com>
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 <avi@chromium.org>
Commit-Queue: Bryan Oltman <bryanoltman@google.com>
Reviewed-by: Mark Mentovai <mark@chromium.org>
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)
Loading diff…

Regression Test / PoC

shipped with the fix
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)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.