High chrome Sandbox Escape 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Sandbox
DescriptionIncorrect authorization in Sandbox
ComponentSandbox
Bug ClassSandbox Escape
Tracker500311587
Fix commit4298968d02fa (chromium/src) +75/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
for
sandbox/linux/syscall_broker/broker_file_permission.cc
modified
if
sandbox/linux/syscall_broker/broker_process_unittest.cc
modified
TEST
sandbox/linux/syscall_broker/broker_process_unittest.cc
modified

Files Changed

  • sandbox/linux/syscall_broker/broker_file_permission.cc
  • sandbox/linux/syscall_broker/broker_process_unittest.cc
From 4298968d02fa7a24dccc65b03071af84c5418c38 Mon Sep 17 00:00:00 2001
From: Greg Thompson <grt@chromium.org>
Date: Tue, 28 Jul 2026 08:24:54 -0700
Subject: [PATCH] [sandbox/linux] Fix directory traversal vulnerability

BrokerFilePermission::ValidatePath failed to reject paths ending with
the `/.` (current directory) self-reference. This allowed an attacker to
request `/tmp/.`, which bypassed ValidatePath() but still successfully
matched the base::StartsWith() check for the `/tmp/` recursive
permission in MatchPath(), improperly granting a dirfd to the root
capability. BrokerFilePermission now rejects paths containing `/../` or
`/./`, as well as trailing `/..` and `/.` references.

Fixed: 500311587
Change-Id: If6beb2361a8a69c6f6a3955aa38c6585d102f3cb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8160841
Reviewed-by: Elly <ellyjones@chromium.org>
Auto-Submit: Greg Thompson <grt@chromium.org>
Commit-Queue: Elly <ellyjones@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1669516}
---

diff --git a/sandbox/linux/syscall_broker/broker_file_permission.cc b/sandbox/linux/syscall_broker/broker_file_permission.cc
index e39e207..dd65fac 100644
--- a/sandbox/linux/syscall_broker/broker_file_permission.cc
+++ b/sandbox/linux/syscall_broker/broker_file_permission.cc
@@ -12,6 +12,7 @@
 
 #include <ostream>
 #include <string>
+#include <string_view>
 
 #include "base/check.h"
 #include "base/compiler_specific.h"
@@ -32,19 +33,14 @@
 BrokerFilePermission::~BrokerFilePermission() = default;
 
 namespace {
-bool ContainsParentReference(const char* path, size_t len) {
-  // No trailing /..
-  if (len >= 3 && UNSAFE_TODO(path[len - 3]) == '/' &&
-      UNSAFE_TODO(path[len - 2]) == '.' && UNSAFE_TODO(path[len - 1]) == '.') {
+bool ContainsParentOrSelfReference(std::string_view path) {
+  // No trailing /.. or /.
+  if (path.ends_with("/..") || path.ends_with("/.")) {
     return true;
   }
-  for (size_t i = 0; i < len; i++) {
-    if (UNSAFE_TODO(path[i]) == '/' && (len - i) > 3) {
-      if (UNSAFE_TODO(path[i + 1]) == '.' && UNSAFE_TODO(path[i + 2]) == '.' &&
-          UNSAFE_TODO(path[i + 3]) == '/') {
-        return true;
-      }
-    }
+  if (path.find("/../") != std::string_view::npos ||
+      path.find("/./") != std::string_view::npos) {
+    return true;
   }
   return false;
 }
@@ -68,7 +64,7 @@
   if (len > 1 && UNSAFE_TODO(path[len - 1]) == '/') {
     return false;
   }
-  if (ContainsParentReference(path, len)) {
+  if (ContainsParentOrSelfReference(std::string_view(path, len))) {
     return false;
   }
   return true;
@@ -304,7 +300,7 @@
   else
     CHECK(last_char != '/') << GetErrorMessageForTests();
 
-  CHECK(!ContainsParentReference(path_.c_str(), path_.length()));
+  CHECK(!ContainsParentOrSelfReference(path_));
 }
 
 BrokerFilePermission::BrokerFilePermission(std::string path, uint64_t flags)
diff --git a/sandbox/linux/syscall_broker/broker_process_unittest.cc b/sandbox/linux/syscall_broker/broker_process_unittest.cc
index ba53cd3..b84898eb 100644
--- a/sandbox/linux/syscall_broker/broker_process_unittest.cc
+++ b/sandbox/linux/syscall_broker/broker_process_unittest.cc
@@ -455,6 +455,72 @@
   // expected.
 }
 
+void TestOpenDirectory(bool fast_check_in_client) {
+  base::ScopedTempDir temp_dir;
+  ASSERT_TRUE(
+      temp_dir.CreateUniqueTempDirUnderPath(base::FilePath(kTempDirForTests)));
+  const std::string temp_dir_str = temp_dir.GetPath().MaybeAsASCII();
+  ASSERT_FALSE(temp_dir_str.empty());
+
+  base::FilePath sub_dir_path = temp_dir.GetPath().AppendASCII("sub");
+  ASSERT_TRUE(base::CreateDirectory(sub_dir_path));
+  const std::string sub_dir_str = sub_dir_path.MaybeAsASCII();
+  ASSERT_FALSE(sub_dir_str.empty());
+
+  base::FilePath file_path = temp_dir.GetPath().AppendASCII("file");
+  ASSERT_TRUE(base::WriteFile(file_path, "data"));
+  const std::string file_str = file_path.MaybeAsASCII();
+  ASSERT_FALSE(file_str.empty());
+
+  BrokerCommandSet command_set = MakeBrokerCommandSet({COMMAND_OPEN});
+  std::vector<BrokerFilePermission> permissions = {
+      BrokerFilePermission::ReadWriteCreateRecursive(temp_dir_str + "/")};
+  auto policy = std::make_optional<BrokerSandboxConfig>(
+      command_set, permissions, kFakeErrnoSentinel);
+  BrokerProcess open_broker(std::move(policy), BrokerType::SIGNAL_BASED,
+                            fast_check_in_client);
+  ASSERT_TRUE(open_broker.Fork(base::BindOnce(&NoOpCallback)));
+
+  // Regular files under the recursive prefix open normally.
+  int fd =
+      open_broker.GetBrokerClientSignalBased()->Open(file_str.c_str(), O_RDWR);
+  EXPECT_GE(fd, 0);
+  if (fd >= 0) {
+    EXPECT_EQ(0, IGNORE_EINTR(close(fd)));
+  }
+
+  // Opening a directory under the recursive prefix is allowed.
+  fd = open_broker.GetBrokerClientSignalBased()->Open(sub_dir_str.c_str(),
+                                                      O_RDONLY);
+  EXPECT_GE(fd, 0);
+  if (fd >= 0) {
+    EXPECT_EQ(0, IGNORE_EINTR(close(fd)));
+  }
+  fd = open_broker.GetBrokerClientSignalBased()->Open(sub_dir_str.c_str(),
+                                                      O_RDONLY | O_DIRECTORY);
+  EXPECT_GE(fd, 0);
+  if (fd >= 0) {
+    EXPECT_EQ(0, IGNORE_EINTR(close(fd)));
+  }
+
+  // The recursive root itself, reached via a "/." suffix, is rejected.
+  fd = open_broker.GetBrokerClientSignalBased()->Open(
+      (temp_dir_str + "/.").c_str(), O_RDONLY);
+  EXPECT_EQ(fd, -kFakeErrnoSentinel);
+}
+
+TEST(BrokerProcess, OpenDirectoryClient) {
+  TestOpenDirectory(true /* fast_check_in_client */);
+  // Don't do anything here, so that ASSERT works in the subfunction as
+  // expected.
+}
+
+TEST(BrokerProcess, OpenDirectoryHost) {
+  TestOpenDirectory(false /* fast_check_in_client */);
+  // Don't do anything here, so that ASSERT works in the subfunction as
+  // expected.
+}
+
 TEST(BrokerProcess, OpenFileRW) {
   ScopedTemporaryFile tempfile;
   const char* tempfile_name = tempfile.full_file_name();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/sandbox/linux/syscall_broker/broker_process_unittest.cc b/sandbox/linux/syscall_broker/broker_process_unittest.cc
index ba53cd3..b84898eb 100644
--- a/sandbox/linux/syscall_broker/broker_process_unittest.cc
+++ b/sandbox/linux/syscall_broker/broker_process_unittest.cc
@@ -455,6 +455,72 @@
   // expected.
 }
 
+void TestOpenDirectory(bool fast_check_in_client) {
+  base::ScopedTempDir temp_dir;
+  ASSERT_TRUE(
+      temp_dir.CreateUniqueTempDirUnderPath(base::FilePath(kTempDirForTests)));
+  const std::string temp_dir_str = temp_dir.GetPath().MaybeAsASCII();
+  ASSERT_FALSE(temp_dir_str.empty());
+
+  base::FilePath sub_dir_path = temp_dir.GetPath().AppendASCII("sub");
+  ASSERT_TRUE(base::CreateDirectory(sub_dir_path));
+  const std::string sub_dir_str = sub_dir_path.MaybeAsASCII();
+  ASSERT_FALSE(sub_dir_str.empty());
+
+  base::FilePath file_path = temp_dir.GetPath().AppendASCII("file");
+  ASSERT_TRUE(base::WriteFile(file_path, "data"));
+  const std::string file_str = file_path.MaybeAsASCII();
+  ASSERT_FALSE(file_str.empty());
+
+  BrokerCommandSet command_set = MakeBrokerCommandSet({COMMAND_OPEN});
+  std::vector<BrokerFilePermission> permissions = {
+      BrokerFilePermission::ReadWriteCreateRecursive(temp_dir_str + "/")};
+  auto policy = std::make_optional<BrokerSandboxConfig>(
+      command_set, permissions, kFakeErrnoSentinel);
+  BrokerProcess open_broker(std::move(policy), BrokerType::SIGNAL_BASED,
+                            fast_check_in_client);
+  ASSERT_TRUE(open_broker.Fork(base::BindOnce(&NoOpCallback)));
+
+  // Regular files under the recursive prefix open normally.
+  int fd =
+      open_broker.GetBrokerClientSignalBased()->Open(file_str.c_str(), O_RDWR);
+  EXPECT_GE(fd, 0);
+  if (fd >= 0) {
+    EXPECT_EQ(0, IGNORE_EINTR(close(fd)));
+  }
+
+  // Opening a directory under the recursive prefix is allowed.
+  fd = open_broker.GetBrokerClientSignalBased()->Open(sub_dir_str.c_str(),
+                                                      O_RDONLY);
+  EXPECT_GE(fd, 0);
+  if (fd >= 0) {
+    EXPECT_EQ(0, IGNORE_EINTR(close(fd)));
+  }
+  fd = open_broker.GetBrokerClientSignalBased()->Open(sub_dir_str.c_str(),
+                                                      O_RDONLY | O_DIRECTORY);
+  EXPECT_GE(fd, 0);
+  if (fd >= 0) {
+    EXPECT_EQ(0, IGNORE_EINTR(close(fd)));
+  }
+
+  // The recursive root itself, reached via a "/." suffix, is rejected.
+  fd = open_broker.GetBrokerClientSignalBased()->Open(
+      (temp_dir_str + "/.").c_str(), O_RDONLY);
+  EXPECT_EQ(fd, -kFakeErrnoSentinel);
+}
+
+TEST(BrokerProcess, OpenDirectoryClient) {
+  TestOpenDirectory(true /* fast_check_in_client */);
+  // Don't do anything here, so that ASSERT works in the subfunction as
+  // expected.
+}
+
+TEST(BrokerProcess, OpenDirectoryHost) {
+  TestOpenDirectory(false /* fast_check_in_client */);
+  // Don't do anything here, so that ASSERT works in the subfunction as
+  // expected.
+}
+
 TEST(BrokerProcess, OpenFileRW) {
   ScopedTemporaryFile tempfile;
   const char* tempfile_name = tempfile.full_file_name();
Loading diff…

Original Bug Report

reported by vm...@google.com

Directory enumeration and sandbox escape in Linux syscall broker

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 without the security team.

Overview: The Linux syscall broker improperly validates file paths, allowing requests ending in ‘/.’ to bypass checks and return directory file descriptors. When exploited from unchrooted processes like Audio or Network, this grants the ability to enumerate the host filesystem using the allowed getdents64 syscall. By discovering and connecting to sensitive host sockets, an attacker could potentially achieve a full sandbox escape.

Affected files:

  • sandbox/linux/syscall_broker/broker_host.cc
  • sandbox/linux/syscall_broker/broker_file_permission.cc
  • services/network/network_sandbox_hook_linux.cc
  • services/audio/audio_sandbox_hook_linux.cc
  • content/common/gpu_pre_sandbox_hook_linux.cc
  • sandbox/policy/linux/bpf_network_policy_linux.cc
  • sandbox/policy/linux/bpf_audio_policy_linux.cc
  • sandbox/policy/linux/bpf_gpu_policy_linux.cc
  • sandbox/policy/linux/bpf_speech_recognition_policy_linux.cc
  • content/utility/speech/speech_recognition_sandbox_hook_linux.cc

Estimated timestamp from git blame: 2025-07-22

Summary

A potential vulnerability exists in the Linux syscall_broker that allows a compromised sandboxed process to obtain directory file descriptors for the host filesystem. This is caused by a logic flaw where paths ending in self-references (e.g., /.) bypass trailing-slash path validation and successfully match against recursive directory permissions.

Because several utility processes (such as Audio and Network) are intentionally not restricted by a namespace sandbox (chroot) and have the getdents64 syscall explicitly permitted in their seccomp-bpf policies, an attacker can use the broker-provided directory descriptor to recursively enumerate the host filesystem. In the case of the Audio process, this can be escalated to a full sandbox escape by discovering and connecting to host Unix sockets.

Technical Details

  1. Path Validation Bypass: In sandbox/linux/syscall_broker/broker_file_permission.cc, ValidatePath() explicitly rejects paths with a trailing slash (/) and parent references (..). However, it fails to reject self-references (.). Thus, a requested path like /tmp/. is considered valid.

  2. Flawed Recursive Matching: For recursive permissions (which require a trailing slash in their definition, e.g., /tmp/), BrokerFilePermission::MatchPath() evaluates access using base::StartsWith(requested_filename, path_). A request for /tmp/. perfectly matches the /tmp/ prefix (base::StartsWith("/tmp/.", "/tmp/") evaluates to true).

  3. Missing Directory Check in Broker: In sandbox/linux/syscall_broker/broker_host.cc, OpenFileForIPC() passes the validated path to sys_open(file_to_open, flags). If O_RDONLY is passed, the Linux kernel successfully opens the directory and returns a valid file descriptor. The broker then returns this descriptor to the sandboxed process via SCM_RIGHTS without checking if it corresponds to a directory (e.g., via fstat).

  4. Process Configuration Context:

    • Audio and Network Processes: These processes are spawned without engaging the namespace sandbox (chroot). This means they execute within the host’s filesystem namespace.
    • Seccomp Policy: Their seccomp-bpf policies explicitly allow __NR_getdents64.
    • Socket Permissions: The Audio process policy explicitly allows socket() (for AF_UNIX) and connect().

Potential Attack Steps

Note: Our tooling agent cannot run code, so the following represents the theoretical steps an attacker would take to trigger the vulnerability based on static analysis.

  1. Initial Compromise: The attacker achieves remote code execution within the sandboxed Audio process.
  2. Craft Broker Request: The attacker sends a COMMAND_OPEN IPC message to the syscall broker, requesting the path /tmp/. with the O_RDONLY flag.
  3. Obtain Directory FD: The broker validates the path, incorrectly determines it is a file inside the permitted /tmp/ directory, opens it, and returns the directory file descriptor to the Audio process.
  4. Enumerate Filesystem: The attacker invokes syscall(__NR_getdents64, dirfd, buf, size), which is permitted by the seccomp filter. This reads the host’s /tmp/ directory contents.
  5. Recursive Discovery: The attacker parses the entries, finds subdirectories, and repeats the process (e.g., requesting /tmp/ssh-XXXXXX/.) to map out the filesystem.
  6. Sandbox Escape: Upon discovering a sensitive Unix domain socket (such as an SSH agent socket or X11 socket), the attacker calls socket(AF_UNIX, SOCK_STREAM, 0) and connect()s directly to it. Because there is no chroot, the connection succeeds, granting the attacker access to a highly privileged host service and effectively escaping the sandbox.

(Note: If exploited from the Network process, the attacker could similarly enumerate DIR_USER_DATA to steal unguessable extension IDs, profiles, and localized storage data.)

Suggested Fix

  1. Enforce File Type in Broker: In sandbox/linux/syscall_broker/broker_host.cc, immediately after the sys_open call in OpenFileForIPC, perform an fstat() on the resulting file descriptor. If S_ISDIR(st_mode) is true, the broker should close the descriptor and return -EISDIR to the caller.
  2. Harden Path Validation: Update BrokerFilePermission::ValidatePath to reject self-references (paths containing /. or ending in /.).

Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker