High firefox Sandbox Escape 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionSandbox escape in the Security: Process Sandboxing component
ComponentSecurity/NSS
Bug ClassSandbox Escape
Tracker2040515
Fix commit7538e5fa889a (firefox) +19/-85
CISA KEVNot listed
CreditedYaqoub Aldurayhim
Disclosed2026-06-16

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 SandboxPolicyCommon handler (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 Operation enum 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.

Key insight
The single mistake was brokering symlink creation while only authorizing the link’s own path and never constraining its arbitrary target, giving sandboxed code a link-planting primitive to defeat path-based confinement; the fix eliminates the primitive by denying symlink/symlinkat outright with EPERM.

Attack Path

  1. Compromise the content process An attacker gains code execution in a sandboxed content (renderer) process through a separate content-process bug.
  2. Invoke `symlink`/`symlinkat` The attacker calls symlink() on a link path that passes AllowOperation(W_OK | X_OK, perms), with a target pointing outside the sandbox’s allowed filesystem region.
  3. Broker creates the link SymlinkTrap forwards the request and the broker executes symlink() in the parent context, never validating the target.
  4. Dereference via brokered ops The attacker issues follow-up brokered operations that resolve the planted symlink, reaching privileged or out-of-policy files.
  5. Escape confinement Path-based sandbox restrictions are bypassed, giving filesystem access beyond the intended policy.

Impact Assessment

An attacker who already controls a sandboxed content process gains a symlink-based path-confusion primitive to read or modify filesystem locations outside the sandbox’s path policy, weakening or escaping process confinement on Linux. The precondition is prior code execution in the content process; the primitive operates through the trusted broker in the parent, which is why it constitutes a sandbox-escape rather than an ordinary content-process capability.

Changed Functions

FunctionChangeNotes
if
security/sandbox/linux/SandboxFilter.cpp
modified
if
security/sandbox/linux/broker/SandboxBroker.cpp
modified
TEST_F
security/sandbox/linux/gtest/TestBroker.cpp
modified

Files Changed

  • security/sandbox/common/test/SandboxTestingChildTests.h
  • security/sandbox/linux/SandboxBrokerClient.cpp
  • security/sandbox/linux/SandboxBrokerClient.h
  • security/sandbox/linux/SandboxFilter.cpp
  • security/sandbox/linux/broker/SandboxBroker.cpp
  • security/sandbox/linux/broker/SandboxBrokerCommon.cpp
  • security/sandbox/linux/broker/SandboxBrokerCommon.h
  • security/sandbox/linux/gtest/TestBroker.cpp
  • security/sandbox/test/browser_content_sandbox_fs_tests.js

Audit Directions

  • Broker operations with unvalidated arguments
    Review every SANDBOX_FILE_*/Operation handler for arguments (targets, secondary paths, modes) that bypass AllowOperation checks the way symlink targets did.
  • Symlink-resolving brokered calls
    Audit brokered open, rename, stat, and readlink paths for TOCTOU or symlink-following that could reach out-of-policy files even without a create primitive.
  • Unnecessary syscalls forwarded to the broker
    Confirm each syscall trapped to the broker is genuinely required by content processes, and deny with Error(EPERM) any that merely add attack surface.
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"
     );
   }
Loading diff…

Regression Test / PoC

shipped with the fix
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"
     );
   }
Loading diff…