CVE-2026-79218
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forsandbox/linux/syscall_broker/broker_file_permission.cc |
modified | |
ifsandbox/linux/syscall_broker/broker_process_unittest.cc |
modified | |
TESTsandbox/linux/syscall_broker/broker_process_unittest.cc |
modified |
Files Changed
sandbox/linux/syscall_broker/broker_file_permission.ccsandbox/linux/syscall_broker/broker_process_unittest.cc
Patch
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();
Regression Test / PoC
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();
Original Bug Report
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.ccsandbox/linux/syscall_broker/broker_file_permission.ccservices/network/network_sandbox_hook_linux.ccservices/audio/audio_sandbox_hook_linux.cccontent/common/gpu_pre_sandbox_hook_linux.ccsandbox/policy/linux/bpf_network_policy_linux.ccsandbox/policy/linux/bpf_audio_policy_linux.ccsandbox/policy/linux/bpf_gpu_policy_linux.ccsandbox/policy/linux/bpf_speech_recognition_policy_linux.cccontent/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
-
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. -
Flawed Recursive Matching: For recursive permissions (which require a trailing slash in their definition, e.g.,
/tmp/),BrokerFilePermission::MatchPath()evaluates access usingbase::StartsWith(requested_filename, path_). A request for/tmp/.perfectly matches the/tmp/prefix (base::StartsWith("/tmp/.", "/tmp/")evaluates to true). -
Missing Directory Check in Broker: In
sandbox/linux/syscall_broker/broker_host.cc,OpenFileForIPC()passes the validated path tosys_open(file_to_open, flags). IfO_RDONLYis passed, the Linux kernel successfully opens the directory and returns a valid file descriptor. The broker then returns this descriptor to the sandboxed process viaSCM_RIGHTSwithout checking if it corresponds to a directory (e.g., viafstat). -
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()(forAF_UNIX) andconnect().
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.
- Initial Compromise: The attacker achieves remote code execution within the sandboxed Audio process.
- Craft Broker Request: The attacker sends a
COMMAND_OPENIPC message to the syscall broker, requesting the path/tmp/.with theO_RDONLYflag. - 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. - 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. - Recursive Discovery: The attacker parses the entries, finds subdirectories, and repeats the process (e.g., requesting
/tmp/ssh-XXXXXX/.) to map out the filesystem. - 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)andconnect()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
- Enforce File Type in Broker: In
sandbox/linux/syscall_broker/broker_host.cc, immediately after thesys_opencall inOpenFileForIPC, perform anfstat()on the resulting file descriptor. IfS_ISDIR(st_mode)is true, the broker should close the descriptor and return-EISDIRto the caller. - Harden Path Validation: Update
BrokerFilePermission::ValidatePathto 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.