High watch CVSS 8.3 chrome Sandbox Escape 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Sandbox
DescriptionIncorrect authorization in Sandbox
ComponentSandbox
Sub-componentSandbox/linux
Bug ClassSandbox Escape
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.
Tracker500311587
Fix commit4298968d02fa (chromium/src) +75/-13
CWECWE-863
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-04-07 (per vendor advisory)
Fix landed2026-07-28 (public git)
Disclosed2026-08-25
Patch gap28 days — fix public in source before the advisory

Background

Linux syscall broker
a Chromium sandbox mechanism where a sandboxed process asks a trusted broker process to perform file operations (e.g. open) on its behalf, governed by a policy of BrokerFilePermission entries.
`BrokerFilePermission`
a policy entry describing a permitted path (single file or a recursive prefix) and its allowed operations; it validates and matches requested paths.
`ValidatePath()`
the broker routine that rejects malformed or unsafe request paths (empty, non-absolute, trailing slash, or containing parent/self references) before matching.
Recursive permission / `MatchPath()`
a ReadWriteCreateRecursive("/tmp/")-style entry that grants any path under a prefix by a base::StartsWith() check in MatchPath().

Root Cause Analysis

The old ContainsParentReference() helper only rejected .. parent references: a trailing /.. and any embedded /../ sequence. It did not reject the current-directory self-reference /., so a request path like /tmp/. passed ValidatePath(). That path then satisfied the base::StartsWith() check in MatchPath() for a /tmp/ recursive permission, so the broker opened /tmp/. — the directory itself — and returned a dirfd for the recursive root, granting broader access than the policy intended (the “root capability” dirfd).

The fix renames the helper to ContainsParentOrSelfReference() and, using std::string_view, rejects paths that end with /.. or /. and paths that contain /../ or /./, so self-references are now filtered out before matching. The regression test confirms that a normal file and a subdirectory under the recursive prefix still open, while the recursive root reached via a /. suffix is now rejected with the fake errno sentinel.

Key insight
The path validator guarded against parent-directory (..) references but omitted current-directory (.) self-references, letting /tmp/. slip through ValidatePath() and still match a recursive prefix; the fix adds /. and /./ to the rejected set in the renamed ContainsParentOrSelfReference().

Attack Path

  1. Hold a recursive permission The sandbox policy grants a recursive prefix such as /tmp/ (e.g. ReadWriteCreateRecursive), which authorizes files beneath the prefix but is not meant to hand out a directory fd for the root itself.
  2. Request the root via self-reference The sandboxed process asks the broker to open("/tmp/."), appending the /. self-reference to the recursive root.
  3. Bypass `ValidatePath()` The pre-patch ContainsParentReference() does not reject /., so validation passes.
  4. Match the recursive prefix MatchPath()’s base::StartsWith() accepts /tmp/. against the /tmp/ permission, so the broker opens the directory and returns a dirfd to the recursive root, an authorization broader than intended.
  5. Leverage the root dirfd With a directory descriptor for the recursive root, the process gains directory-level access to the prefix beyond the file-level access the policy meant to allow (further escalation to a full sandbox escape is (inferred)).

Impact Assessment

A compromised sandboxed renderer/process could obtain a directory file descriptor for the root of a recursive broker permission by requesting a /.-suffixed path, weakening the sandbox’s file-access confinement (a directory-traversal / incorrect-authorization flaw). The advisory classifies the bug class as SandboxEscape with “Incorrect authorization in Sandbox”; whether the dirfd is sufficient for a complete sandbox escape is (inferred) and not demonstrated by the patch. No exploited-in-the-wild status is stated.

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

Audit Directions

  • Incomplete path-normalization filters
    Audit any custom path sanitizer for coverage of the full set of relative components: not just .. but . and their trailing, leading, and embedded forms (/./, /., ./).
  • Validate-then-match prefix matching
    Wherever a StartsWith()/prefix match grants recursive access, verify the validation stage canonicalizes or rejects paths so that syntactically-different but equivalent paths (/x/., /x//, /x/./) cannot resolve to a broader target than intended.
  • String-length vs. string_view rewrites
    Review other broker/sandbox parsers still using manual index arithmetic (path[len-1] etc.) for off-by-one and missing-case bugs that a std::string_view ends_with/find rewrite would surface.
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