CVE-2026-79218
Overview
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 ofBrokerFilePermissionentries. - `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 abase::StartsWith()check inMatchPath().
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.
..) 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
- 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. - Request the root via self-reference
The sandboxed process asks the broker to
open("/tmp/."), appending the/.self-reference to the recursive root. - Bypass `ValidatePath()`
The pre-patch
ContainsParentReference()does not reject/., so validation passes. - Match the recursive prefix
MatchPath()’sbase::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. - 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
/.-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
| 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
Audit Directions
- Incomplete path-normalization filtersAudit 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 matchingWherever 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 rewritesReview other broker/sandbox parsers still using manual index arithmetic (
path[len-1]etc.) for off-by-one and missing-case bugs that astd::string_viewends_with/findrewrite would surface.
Patch
From 4298968d02fa7a24dccc65b03071af84c5418c38 Mon Sep 17 00:00:00 2001 From: Greg Thompson <[email protected]> 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 <[email protected]> Auto-Submit: Greg Thompson <[email protected]> Commit-Queue: Elly <[email protected]> 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.