CVE-2026-12296
Overview
Background
- Broker (`SandboxBroker`)
- a trusted parent-process helper that performs privileged filesystem operations on behalf of a sandboxed child after checking them against a path policy.
- Seccomp trap
- a
SandboxPolicyCommonhandler (e.g.SymlinkTrap) that intercepts a blocked syscall in the content process and forwards it to the broker over IPC. - `SANDBOX_FILE_SYMLINK`
- the broker
Operationenum value that requested creation of a symbolic link inside the sandboxed process’s allowed paths. - `AllowOperation(W_OK | X_OK, perms)`
- the broker permission check that gated write/traverse operations such as link and symlink creation on a policy path.
Root Cause Analysis
The content-process seccomp filter routed __NR_symlink and __NR_symlinkat to SymlinkTrap/SymlinkAtTrap, which called SandboxBrokerClient::Symlink and thereby asked the trusted broker to execute symlink() via DoLink under a SANDBOX_FILE_SYMLINK request. The broker only checked that the link path satisfied AllowOperation(W_OK | X_OK, perms), but a symlink’s target is an arbitrary attacker-controlled string that the broker never validated, letting a compromised child plant a link whose target escapes the path policy. Because subsequent brokered operations (open, rename, stat, etc.) resolve symlinks, this created a path-confusion primitive by which the sandbox’s path whitelist could be bypassed to reach files outside the allowed set.
The fix removes symlink support from the broker entirely and makes the filter return Error(EPERM) directly for both syscalls, so no attacker-controlled symlink can ever be created. This works because content processes have no legitimate need to create symlinks, so denying the primitive closes the escape without breaking functionality.
symlink/symlinkat outright with EPERM.Attack Path
- Compromise the content process An attacker gains code execution in a sandboxed content (renderer) process through a separate content-process bug.
- Invoke `symlink`/`symlinkat`
The attacker calls
symlink()on a link path that passesAllowOperation(W_OK | X_OK, perms), with a target pointing outside the sandbox’s allowed filesystem region. - Broker creates the link
SymlinkTrapforwards the request and the broker executessymlink()in the parent context, never validating the target. - Dereference via brokered ops The attacker issues follow-up brokered operations that resolve the planted symlink, reaching privileged or out-of-policy files.
- Escape confinement Path-based sandbox restrictions are bypassed, giving filesystem access beyond the intended policy.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsecurity/sandbox/linux/SandboxFilter.cpp |
modified | |
ifsecurity/sandbox/linux/broker/SandboxBroker.cpp |
modified | |
TEST_Fsecurity/sandbox/linux/gtest/TestBroker.cpp |
modified |
Files Changed
security/sandbox/common/test/SandboxTestingChildTests.hsecurity/sandbox/linux/SandboxBrokerClient.cppsecurity/sandbox/linux/SandboxBrokerClient.hsecurity/sandbox/linux/SandboxFilter.cppsecurity/sandbox/linux/broker/SandboxBroker.cppsecurity/sandbox/linux/broker/SandboxBrokerCommon.cppsecurity/sandbox/linux/broker/SandboxBrokerCommon.hsecurity/sandbox/linux/gtest/TestBroker.cppsecurity/sandbox/test/browser_content_sandbox_fs_tests.js
Audit Directions
- Broker operations with unvalidated argumentsReview every
SANDBOX_FILE_*/Operationhandler for arguments (targets, secondary paths, modes) that bypassAllowOperationchecks the way symlink targets did. - Symlink-resolving brokered callsAudit brokered
open,rename,stat, andreadlinkpaths for TOCTOU or symlink-following that could reach out-of-policy files even without a create primitive. - Unnecessary syscalls forwarded to the brokerConfirm each syscall trapped to the broker is genuinely required by content processes, and deny with
Error(EPERM)any that merely add attack surface.
Patch
diff --git a/security/sandbox/linux/SandboxBrokerClient.cpp b/security/sandbox/linux/SandboxBrokerClient.cpp
index d8091602974..44672eff5b8 100644
--- a/security/sandbox/linux/SandboxBrokerClient.cpp
+++ b/security/sandbox/linux/SandboxBrokerClient.cpp
@@ -205,11 +205,6 @@ int SandboxBrokerClient::Link(const char* aOldPath, const char* aNewPath) {
return DoCall(&req, aOldPath, aNewPath, nullptr, false);
}
-int SandboxBrokerClient::Symlink(const char* aOldPath, const char* aNewPath) {
- Request req = MakeRequest(SANDBOX_FILE_SYMLINK, 0, 0);
- return DoCall(&req, aOldPath, aNewPath, nullptr, false);
-}
-
int SandboxBrokerClient::Rename(const char* aOldPath, const char* aNewPath) {
Request req = MakeRequest(SANDBOX_FILE_RENAME, 0, 0);
return DoCall(&req, aOldPath, aNewPath, nullptr, false);
diff --git a/security/sandbox/linux/SandboxBrokerClient.h b/security/sandbox/linux/SandboxBrokerClient.h
index cc5e96437dd..2f5c0952618 100644
--- a/security/sandbox/linux/SandboxBrokerClient.h
+++ b/security/sandbox/linux/SandboxBrokerClient.h
@@ -34,7 +34,6 @@ class SandboxBrokerClient final : private SandboxBrokerCommon {
int Chmod(const char* aPath, int aMode);
int Link(const char* aPath, const char* aPath2);
int Mkdir(const char* aPath, int aMode);
- int Symlink(const char* aOldPath, const char* aNewPath);
int Rename(const char* aOldPath, const char* aNewPath);
int Unlink(const char* aPath);
int Rmdir(const char* aPath);
diff --git a/security/sandbox/linux/SandboxFilter.cpp b/security/sandbox/linux/SandboxFilter.cpp
index 04f7cf4507e..004d2314786 100644
--- a/security/sandbox/linux/SandboxFilter.cpp
+++ b/security/sandbox/linux/SandboxFilter.cpp
@@ -299,13 +299,6 @@ class SandboxPolicyCommon : public SandboxPolicyBase {
return broker->Link(path, path2);
}
- static intptr_t SymlinkTrap(ArgsRef aArgs, void* aux) {
- auto broker = static_cast<SandboxBrokerClient*>(aux);
- auto path = reinterpret_cast<const char*>(aArgs.args[0]);
- auto path2 = reinterpret_cast<const char*>(aArgs.args[1]);
- return broker->Symlink(path, path2);
- }
-
static intptr_t RenameTrap(ArgsRef aArgs, void* aux) {
auto broker = static_cast<SandboxBrokerClient*>(aux);
auto path = reinterpret_cast<const char*>(aArgs.args[0]);
@@ -472,19 +465,6 @@ class SandboxPolicyCommon : public SandboxPolicyBase {
return broker->Link(path, path2);
}
- static intptr_t SymlinkAtTrap(ArgsRef aArgs, void* aux) {
- auto broker = static_cast<SandboxBrokerClient*>(aux);
- auto path = reinterpret_cast<const char*>(aArgs.args[0]);
- auto fd2 = static_cast<int>(aArgs.args[1]);
- auto path2 = reinterpret_cast<const char*>(aArgs.args[2]);
- if (fd2 != AT_FDCWD && path2[0] != '/') {
- SANDBOX_LOG("unsupported fd-relative symlinkat(\"%s\", %d, \"%s\")", path,
- fd2, path2);
- return BlockedSyscallTrap(aArgs, nullptr);
- }
- return broker->Symlink(path, path2);
- }
-
static intptr_t RenameAtTrap(ArgsRef aArgs, void* aux) {
auto broker = static_cast<SandboxBrokerClient*>(aux);
auto fd = static_cast<int>(aArgs.args[0]);
@@ -966,7 +946,7 @@ class SandboxPolicyCommon : public SandboxPolicyBase {
case __NR_mkdir:
return Trap(MkdirTrap, mBroker);
case __NR_symlink:
- return Trap(SymlinkTrap, mBroker);
+ return Error(EPERM);
case __NR_rename:
return Trap(RenameTrap, mBroker);
case __NR_rmdir:
@@ -995,7 +975,7 @@ class SandboxPolicyCommon : public SandboxPolicyBase {
case __NR_mkdirat:
return Trap(MkdirAtTrap, mBroker);
case __NR_symlinkat:
- return Trap(SymlinkAtTrap, mBroker);
+ return Error(EPERM);
case __NR_renameat:
return Trap(RenameAtTrap, mBroker);
case __NR_unlinkat:
diff --git a/security/sandbox/linux/broker/SandboxBroker.cpp b/security/sandbox/linux/broker/SandboxBroker.cpp
index 261ee725152..886a815b507 100644
--- a/security/sandbox/linux/broker/SandboxBroker.cpp
+++ b/security/sandbox/linux/broker/SandboxBroker.cpp
@@ -529,17 +529,6 @@ static int DoStat(const char* aPath, statstruct* aBuff, int aFlags) {
return statsyscall(aPath, aBuff);
}
-static int DoLink(const char* aPath, const char* aPath2,
- SandboxBrokerCommon::Operation aOper) {
- if (aOper == SandboxBrokerCommon::Operation::SANDBOX_FILE_LINK) {
- return link(aPath, aPath2);
- }
- if (aOper == SandboxBrokerCommon::Operation::SANDBOX_FILE_SYMLINK) {
- return symlink(aPath, aPath2);
- }
- MOZ_CRASH("SandboxBroker: Unknown link operation");
-}
-
static int DoConnect(const char* aPath, size_t aLen, int aType,
bool aIsAbstract) {
// Deny SOCK_DGRAM for the same reason it's denied for socketpair.
@@ -910,9 +899,8 @@ void SandboxBroker::ThreadMain(void) {
break;
case SANDBOX_FILE_LINK:
- case SANDBOX_FILE_SYMLINK:
if (permissive || AllowOperation(W_OK | X_OK, perms)) {
- if (DoLink(pathBuf, pathBuf2, req.mOp) == 0) {
+ if (link(pathBuf, pathBuf2) == 0) {
resp.mError = 0;
} else {
resp.mError = -errno;
diff --git a/security/sandbox/linux/broker/SandboxBrokerCommon.cpp b/security/sandbox/linux/broker/SandboxBrokerCommon.cpp
index 4762be3e19a..396a1c53284 100644
--- a/security/sandbox/linux/broker/SandboxBrokerCommon.cpp
+++ b/security/sandbox/linux/broker/SandboxBrokerCommon.cpp
@@ -46,19 +46,8 @@ unsigned SandboxBrokerCommon::OperationToInt(Operation aOp) {
// static
const char* SandboxBrokerCommon::OperationDescription(Operation aOp) {
static constexpr const char* kNames[] = {
- "open",
- "access",
- "stat",
- "chmod",
- "link",
- "symlink",
- "mkdir",
- "rename",
- "rmdir",
- "unlink",
- "readlink",
- "connect",
- "connect-abstract",
+ "open", "access", "stat", "chmod", "link", "mkdir",
+ "rename", "rmdir", "unlink", "readlink", "connect", "connect-abstract",
};
static_assert(
diff --git a/security/sandbox/linux/broker/SandboxBrokerCommon.h b/security/sandbox/linux/broker/SandboxBrokerCommon.h
index 599431f0ce4..1bdae7a3c38 100644
--- a/security/sandbox/linux/broker/SandboxBrokerCommon.h
+++ b/security/sandbox/linux/broker/SandboxBrokerCommon.h
@@ -32,7 +32,6 @@ class SandboxBrokerCommon {
SANDBOX_FILE_STAT,
SANDBOX_FILE_CHMOD,
SANDBOX_FILE_LINK,
- SANDBOX_FILE_SYMLINK,
SANDBOX_FILE_MKDIR,
SANDBOX_FILE_RENAME,
SANDBOX_FILE_RMDIR,
@@ -44,11 +43,11 @@ class SandboxBrokerCommon {
};
MOZ_USING_ENUM_STATIC(Operation, SANDBOX_FILE_OPEN, SANDBOX_FILE_ACCESS,
SANDBOX_FILE_STAT, SANDBOX_FILE_CHMOD,
- SANDBOX_FILE_LINK, SANDBOX_FILE_SYMLINK,
- SANDBOX_FILE_MKDIR, SANDBOX_FILE_RENAME,
- SANDBOX_FILE_RMDIR, SANDBOX_FILE_UNLINK,
- SANDBOX_FILE_READLINK, SANDBOX_SOCKET_CONNECT,
- SANDBOX_SOCKET_CONNECT_ABSTRACT, SANDBOX_OP_MAX_VALUE);
+ SANDBOX_FILE_LINK, SANDBOX_FILE_MKDIR,
+ SANDBOX_FILE_RENAME, SANDBOX_FILE_RMDIR,
+ SANDBOX_FILE_UNLINK, SANDBOX_FILE_READLINK,
+ SANDBOX_SOCKET_CONNECT, SANDBOX_SOCKET_CONNECT_ABSTRACT,
+ SANDBOX_OP_MAX_VALUE);
static bool OperationIsValid(Operation aOp) {
return static_cast<unsigned>(aOp) <=
diff --git a/security/sandbox/linux/gtest/TestBroker.cpp b/security/sandbox/linux/gtest/TestBroker.cpp
index 1b7b84178d2..ea5f22e14da 100644
--- a/security/sandbox/linux/gtest/TestBroker.cpp
+++ b/security/sandbox/linux/gtest/TestBroker.cpp
@@ -68,9 +68,6 @@ class SandboxBrokerTest : public ::testing::Test {
int Mkdir(const char* aPath, int aMode) {
return mClient->Mkdir(aPath, aMode);
}
- int Symlink(const char* aPath, const char* bPath) {
- return mClient->Symlink(aPath, bPath);
- }
int Rename(const char* aPath, const char* bPath) {
return mClient->Rename(aPath, bPath);
}
@@ -306,25 +303,6 @@ TEST_F(SandboxBrokerTest, Link) {
PrePostTestCleanup();
}
-TEST_F(SandboxBrokerTest, Symlink) {
- PrePostTestCleanup();
-
- int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT);
- ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed.";
- close(fd);
- ASSERT_EQ(0, Symlink("/tmp/blublu", "/tmp/blublublu"));
- EXPECT_EQ(0, Access("/tmp/blublublu", F_OK));
- statstruct aStat;
- ASSERT_EQ(0, lstatsyscall("/tmp/blublublu", &aStat));
- EXPECT_EQ((mode_t)S_IFLNK, aStat.st_mode & S_IFMT);
- // Not whitelisted target path
- EXPECT_EQ(-EACCES, Symlink("/tmp/blublu", "/tmp/nope"));
- EXPECT_EQ(0, unlink("/tmp/blublublu"));
- EXPECT_EQ(0, unlink("/tmp/blublu"));
-
- PrePostTestCleanup();
-}
-
TEST_F(SandboxBrokerTest, Mkdir) {
PrePostTestCleanup();
@@ -400,7 +378,8 @@ TEST_F(SandboxBrokerTest, Readlink) {
int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT);
ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed.";
close(fd);
- ASSERT_EQ(0, Symlink("/tmp/blublu", "/tmp/blublublu"));
+ // This is the real symlink() now that there's no broker support.
+ ASSERT_EQ(0, symlink("/tmp/blublu", "/tmp/blublublu"));
EXPECT_EQ(0, Access("/tmp/blublublu", F_OK));
char linkBuff[256];
EXPECT_EQ(11, Readlink("/tmp/blublublu", linkBuff, sizeof(linkBuff)));
diff --git a/security/sandbox/common/test/SandboxTestingChildTests.h b/security/sandbox/common/test/SandboxTestingChildTests.h
index a42eefea587..9dae8d40761 100644
--- a/security/sandbox/common/test/SandboxTestingChildTests.h
+++ b/security/sandbox/common/test/SandboxTestingChildTests.h
@@ -627,6 +627,12 @@ void RunTestsContent(SandboxTestingChild* child) {
return fd;
});
+ child->ErrnoValueTest("symlink"_ns, EPERM,
+ [] { return symlink("something", "/tmp/testlink"); });
+ child->ErrnoValueTest("symlinkat"_ns, EPERM, [] {
+ return symlinkat("something", AT_FDCWD, "/tmp/testlink");
+ });
+
# endif // XP_LINUX
# ifdef XP_MACOSX
diff --git a/security/sandbox/test/browser_content_sandbox_fs_tests.js b/security/sandbox/test/browser_content_sandbox_fs_tests.js
index 9e2232f40ce..cc56dc62ca6 100644
--- a/security/sandbox/test/browser_content_sandbox_fs_tests.js
+++ b/security/sandbox/test/browser_content_sandbox_fs_tests.js
@@ -75,10 +75,9 @@ async function createTempFile() {
createSymlink
);
ok(!symlinkCreated.ok, "created a symlink in temp failed");
- const expectedError = isLinux() ? lazy.LIBC.EACCES : lazy.LIBC.EPERM;
is(
symlinkCreated.code,
- expectedError,
+ lazy.LIBC.EPERM,
"created a symlink in temp failed with access denied"
);
}
Regression Test / PoC
diff --git a/security/sandbox/common/test/SandboxTestingChildTests.h b/security/sandbox/common/test/SandboxTestingChildTests.h
index a42eefea587..9dae8d40761 100644
--- a/security/sandbox/common/test/SandboxTestingChildTests.h
+++ b/security/sandbox/common/test/SandboxTestingChildTests.h
@@ -627,6 +627,12 @@ void RunTestsContent(SandboxTestingChild* child) {
return fd;
});
+ child->ErrnoValueTest("symlink"_ns, EPERM,
+ [] { return symlink("something", "/tmp/testlink"); });
+ child->ErrnoValueTest("symlinkat"_ns, EPERM, [] {
+ return symlinkat("something", AT_FDCWD, "/tmp/testlink");
+ });
+
# endif // XP_LINUX
# ifdef XP_MACOSX
diff --git a/security/sandbox/test/browser_content_sandbox_fs_tests.js b/security/sandbox/test/browser_content_sandbox_fs_tests.js
index 9e2232f40ce..cc56dc62ca6 100644
--- a/security/sandbox/test/browser_content_sandbox_fs_tests.js
+++ b/security/sandbox/test/browser_content_sandbox_fs_tests.js
@@ -75,10 +75,9 @@ async function createTempFile() {
createSymlink
);
ok(!symlinkCreated.ok, "created a symlink in temp failed");
- const expectedError = isLinux() ? lazy.LIBC.EACCES : lazy.LIBC.EPERM;
is(
symlinkCreated.code,
- expectedError,
+ lazy.LIBC.EPERM,
"created a symlink in temp failed with access denied"
);
}