CVE-2026-113343
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsandbox/linux/seccomp-bpf-helpers/baseline_policy.cc |
modified | |
ifsandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc |
modified | |
switchsandbox/linux/seccomp-bpf-helpers/syscall_sets.cc |
modified | |
switchsandbox/policy/linux/bpf_speech_recognition_policy_linux.cc |
modified | |
BPF_TEST_Csandbox/linux/seccomp-bpf-helpers/baseline_policy_unittest.cc |
modified | |
ifsandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc |
modified |
Files Changed
sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.hsandbox/policy/linux/bpf_gpu_policy_linux.ccsandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.ccsandbox/linux/seccomp-bpf-helpers/syscall_sets.hsandbox/linux/seccomp-bpf-helpers/syscall_sets.ccsandbox/linux/seccomp-bpf-helpers/baseline_policy.ccsandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.ccsandbox/linux/seccomp-bpf-helpers/baseline_policy_unittest.ccsandbox/policy/linux/bpf_speech_recognition_policy_linux.cc
Patch
From 258d9c2bd051202056371589143d0051baac0640 Mon Sep 17 00:00:00 2001 From: Matthew Denton <[email protected]> Date: Mon, 21 Sep 2026 13:58:18 -0700 Subject: [PATCH] linux sandbox: Reject sys_old_mmap on i386 in baseline policy On i386, __NR_mmap (90) is the legacy sys_old_mmap, which takes a single pointer to an argument struct rather than passing flags in a register. Because seccomp cannot inspect the struct, RestrictMmapFlags() was checking an unrelated register and the intended flag restrictions did not apply. Return ENOSYS for __NR_mmap on i386 instead, mirroring the existing treatment of clone3. This is safe because glibc on i386 uses __NR_mmap2, which passes flags in a register and remains filtered by RestrictMmapFlags(). This resolves and removes the TODO(crbug.com/40528912) for this block. Fixed: 502376419 Change-Id: I72aa30fa6400d5dd1085f759725a7e58037bf860 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8416553 Commit-Queue: Matthew Denton <[email protected]> Reviewed-by: Elly <[email protected]> Cr-Commit-Position: refs/heads/main@{#1702035} --- diff --git a/sandbox/linux/seccomp-bpf-helpers/baseline_policy.cc b/sandbox/linux/seccomp-bpf-helpers/baseline_policy.cc index 4a7d0ac..85f14c9 100644 --- a/sandbox/linux/seccomp-bpf-helpers/baseline_policy.cc +++ b/sandbox/linux/seccomp-bpf-helpers/baseline_policy.cc @@ -258,17 +258,9 @@ .Else(Error(EPERM)); } - // TODO(crbug.com/40528912): should i386 really be in this list? -#if defined(__i386__) || defined(__x86_64__) || defined(__mips__) || \ - defined(__aarch64__) - if (sysno == __NR_mmap) - return RestrictMmapFlags(); -#endif - -#if !defined(__LP64__) - if (sysno == __NR_mmap2) - return RestrictMmapFlags(); -#endif + if (SyscallSets::IsMmap(sysno)) { + return RestrictMmapFlags(sysno); + } if (sysno == __NR_mprotect || sysno == __NR_pkey_mprotect) { // pkey_mprotect is identical to mprotect except for the additional (last) diff --git a/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc b/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc index 71be492..88e6b30 100644 --- a/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc +++ b/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc @@ -23,6 +23,7 @@ #include <unistd.h> #include "base/allocator/partition_alloc_features.h" +#include "base/check.h" #include "base/feature_list.h" #include "base/features.h" #include "base/notreached.h" @@ -31,6 +32,7 @@ #include "sandbox/linux/bpf_dsl/bpf_dsl.h" #include "sandbox/linux/bpf_dsl/seccomp_macros.h" #include "sandbox/linux/seccomp-bpf-helpers/sigsys_handlers.h" +#include "sandbox/linux/seccomp-bpf-helpers/syscall_sets.h" #include "sandbox/linux/seccomp-bpf/sandbox_bpf.h" #include "sandbox/linux/system_headers/linux_futex.h" #include "sandbox/linux/system_headers/linux_memfd.h" @@ -236,7 +238,16 @@ CrashSIGSYSIoctl()); } -ResultExpr RestrictMmapFlags(uint64_t extra_allowed_mask) { +ResultExpr RestrictMmapFlags(int sysno, uint64_t extra_allowed_mask) { +#if defined(__i386__) + // On i386, __NR_mmap is the legacy sys_old_mmap which takes a single pointer + // to an argument struct that we cannot examine. Return ENOSYS so callers + // fall back to __NR_mmap2, which is filtered below. + if (sysno == __NR_mmap) { + return Error(ENOSYS); + } +#endif + CHECK(SyscallSets::IsMmap(sysno)); #if BUILDFLAG(IS_ANDROID) && defined(__x86_64__) const uint64_t kArchSpecificAllowedMask = MAP_32BIT; #else diff --git a/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.h b/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.h index 901c24b..0d58ffa8 100644 --- a/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.h +++ b/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.h @@ -40,6 +40,7 @@ // MAP_STACK | MAP_NORESERVE | MAP_FIXED | MAP_DENYWRITE. // Crash if any other flag is used. SANDBOX_EXPORT bpf_dsl::ResultExpr RestrictMmapFlags( + int sysno, uint64_t extra_allowed_mask = 0); // Restrict the flags argument in mremap(2). diff --git a/sandbox/linux/seccomp-bpf-helpers/syscall_sets.cc b/sandbox/linux/seccomp-bpf-helpers/syscall_sets.cc index cbc2dfc..77b1169a 100644 --- a/sandbox/linux/seccomp-bpf-helpers/syscall_sets.cc +++ b/sandbox/linux/seccomp-bpf-helpers/syscall_sets.cc @@ -580,6 +580,21 @@ } } +bool SyscallSets::IsMmap(int sysno) { + switch (sysno) { +#if defined(__i386__) || defined(__x86_64__) || defined(__mips__) || \ + defined(__aarch64__) + case __NR_mmap: +#endif +#if !defined(__LP64__) + case __NR_mmap2: +#endif + return true; + default: + return false; + } +} + bool SyscallSets::IsAllowedGeneralIo(int sysno) { switch (sysno) { case __NR_lseek: diff --git a/sandbox/linux/seccomp-bpf-helpers/syscall_sets.h b/sandbox/linux/seccomp-bpf-helpers/syscall_sets.h index fa49942..6550055 100644 --- a/sandbox/linux/seccomp-bpf-helpers/syscall_sets.h +++ b/sandbox/linux/seccomp-bpf-helpers/syscall_sets.h @@ -57,6 +57,7 @@ #endif static bool IsAllowedAddressSpaceAccess(int sysno); + static bool IsMmap(int sysno); static bool IsAllowedGeneralIo(int sysno); static bool IsSockSendOneMsg(int sysno); static bool IsPrctl(int sysno); diff --git a/sandbox/policy/linux/bpf_gpu_policy_linux.cc b/sandbox/policy/linux/bpf_gpu_policy_linux.cc index 5725da2..74333b9 100644 --- a/sandbox/policy/linux/bpf_gpu_policy_linux.cc +++ b/sandbox/policy/linux/bpf_gpu_policy_linux.cc @@ -83,6 +83,8 @@ #if defined(__i386__) || defined(__x86_64__) || defined(__mips__) // The Nvidia driver uses flags not in the baseline policy // (MAP_LOCKED | MAP_EXECUTABLE | MAP_32BIT) + // TODO(crbug.com/502376419): Use SyscallSets::IsMmap() and + // RestrictMmapFlags() with extra_allowed_mask instead of Allow(). case __NR_mmap: return Allow(); #endif diff --git a/sandbox/policy/linux/bpf_speech_recognition_policy_linux.cc b/sandbox/policy/linux/bpf_speech_recognition_policy_linux.cc index 0e27ada..39624b50 100644 --- a/sandbox/policy/linux/bpf_speech_recognition_policy_linux.cc +++ b/sandbox/policy/linux/bpf_speech_recognition_policy_linux.cc @@ -8,6 +8,7 @@ #include "sandbox/linux/bpf_dsl/bpf_dsl.h" #include "sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.h" +#include "sandbox/linux/seccomp-bpf-helpers/syscall_sets.h" #include "sandbox/linux/syscall_broker/broker_process.h" #include "sandbox/linux/system_headers/linux_prctl.h" #include "sandbox/linux/system_headers/linux_syscalls.h" @@ -29,12 +30,6 @@ ResultExpr SpeechRecognitionProcessPolicy::EvaluateSyscall( int system_call_number) const { switch (system_call_number) { -#if defined(__NR_mmap) - case __NR_mmap: - // The speech recognition sandbox requires the MAP_POPULATE flag in - // addition to the default flags. - return RestrictMmapFlags(MAP_POPULATE); -#endif // Required by the Speech On-Device API (SODA) binary to find the // appropriate configuration file to use within a language pack directory. #if defined(__NR_getdents64) @@ -57,6 +52,12 @@ } #endif default: + if (SyscallSets::IsMmap(system_call_number)) { + // The speech recognition sandbox requires the MAP_POPULATE flag in + // addition to the default flags. + return RestrictMmapFlags(system_call_number, MAP_POPULATE); + } + auto* sandbox_linux = SandboxLinux::GetInstance(); if (sandbox_linux->ShouldBrokerHandleSyscall(system_call_number)) return sandbox_linux->HandleViaBroker(system_call_number); diff --git a/sandbox/linux/seccomp-bpf-helpers/baseline_policy_unittest.cc b/sandbox/linux/seccomp-bpf-helpers/baseline_policy_unittest.cc index 9d27487..a5a3b19 100644 --- a/sandbox/linux/seccomp-bpf-helpers/baseline_policy_unittest.cc +++ b/sandbox/linux/seccomp-bpf-helpers/baseline_policy_unittest.cc @@ -631,6 +631,34 @@ MFD_CLOEXEC | MFD_HUGETLB); } +#if defined(__i386__) +// On i386, __NR_mmap is the legacy sys_old_mmap, which takes a single pointer +// to an argument struct whose contents cannot be filtered. The policy rejects +// the syscall with ENOSYS so callers use __NR_mmap2 instead. +BPF_TEST_C(BaselinePolicy, OldMmapEnosys, BaselinePolicy) { + struct { + unsigned long addr; + unsigned long len; + unsigned long prot; + unsigned long flags; + unsigned long fd; + unsigned long offset; + } args = {0, + 4096, + PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_PRIVATE | MAP_POPULATE, + static_cast<unsigned long>(-1), + 0}; + errno = 0; + // The fourth register is not part of this syscall's ABI; load it with a + // value that RestrictMmapFlags() would accept to ensure the rejection is + // unconditional. + long ret = syscall(__NR_mmap, &args, 0, 0, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0); + BPF_ASSERT_EQ(-1, ret); + BPF_ASSERT_EQ(ENOSYS, errno); +} +#endif // defined(__i386__) + } // namespace } // namespace sandbox diff --git a/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc b/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc index 36041d2..f9666ef 100644 --- a/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc +++ b/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc @@ -28,6 +28,7 @@ #include "sandbox/linux/bpf_dsl/policy.h" #include "sandbox/linux/seccomp-bpf-helpers/sigsys_handlers.h" #include "sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.h" +#include "sandbox/linux/seccomp-bpf-helpers/syscall_sets.h" #include "sandbox/linux/seccomp-bpf/bpf_tests.h" #include "sandbox/linux/system_headers/linux_syscalls.h" #include "sandbox/linux/tests/unit_tests.h" @@ -70,6 +71,12 @@ // program for __NR_mmap is no longer an unconditional Allow(). EXPECT_FALSE(mmap_expr->IsAllow()); +#if !defined(__LP64__) && defined(__NR_mmap2) + ResultExpr mmap2_expr = speech_policy.EvaluateSyscall(__NR_mmap2); + ASSERT_TRUE(mmap2_expr); + EXPECT_FALSE(mmap2_expr->IsAllow()); +#endif + // Control 1: the else-branch the author *intended* to fall through to. // BPFBasePolicy::EvaluateSyscall(__NR_mmap) chains to RestrictMmapFlags(), // which is an If/Else node — NOT an unconditional Allow. @@ -102,7 +109,7 @@ ~SpeechRecognitionMmapPolicyWrapper() override = default; ResultExpr EvaluateSyscall(int sysno) const override { - if (sysno == __NR_mmap) { + if (SyscallSets::IsMmap(sysno)) { // Exercise the production code path. This is the same ResultExpr that // ships in the kSpeechRecognition seccomp filter. return real_policy_.EvaluateSyscall(sysno); @@ -120,9 +127,9 @@ ~BaselineMmapPolicyWrapper() override = default; ResultExpr EvaluateSyscall(int sysno) const override { - if (sysno == __NR_mmap) { + if (SyscallSets::IsMmap(sysno)) { // The else-branch the author intended. - return RestrictMmapFlags(); + return RestrictMmapFlags(sysno); } return Allow(); }
Regression Test / PoC
diff --git a/sandbox/linux/seccomp-bpf-helpers/baseline_policy_unittest.cc b/sandbox/linux/seccomp-bpf-helpers/baseline_policy_unittest.cc
index 9d27487..a5a3b19 100644
--- a/sandbox/linux/seccomp-bpf-helpers/baseline_policy_unittest.cc
+++ b/sandbox/linux/seccomp-bpf-helpers/baseline_policy_unittest.cc
@@ -631,6 +631,34 @@
MFD_CLOEXEC | MFD_HUGETLB);
}
+#if defined(__i386__)
+// On i386, __NR_mmap is the legacy sys_old_mmap, which takes a single pointer
+// to an argument struct whose contents cannot be filtered. The policy rejects
+// the syscall with ENOSYS so callers use __NR_mmap2 instead.
+BPF_TEST_C(BaselinePolicy, OldMmapEnosys, BaselinePolicy) {
+ struct {
+ unsigned long addr;
+ unsigned long len;
+ unsigned long prot;
+ unsigned long flags;
+ unsigned long fd;
+ unsigned long offset;
+ } args = {0,
+ 4096,
+ PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE | MAP_POPULATE,
+ static_cast<unsigned long>(-1),
+ 0};
+ errno = 0;
+ // The fourth register is not part of this syscall's ABI; load it with a
+ // value that RestrictMmapFlags() would accept to ensure the rejection is
+ // unconditional.
+ long ret = syscall(__NR_mmap, &args, 0, 0, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
+ BPF_ASSERT_EQ(-1, ret);
+ BPF_ASSERT_EQ(ENOSYS, errno);
+}
+#endif // defined(__i386__)
+
} // namespace
} // namespace sandbox
diff --git a/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc b/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc
index 36041d2..f9666ef 100644
--- a/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc
+++ b/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc
@@ -28,6 +28,7 @@
#include "sandbox/linux/bpf_dsl/policy.h"
#include "sandbox/linux/seccomp-bpf-helpers/sigsys_handlers.h"
#include "sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.h"
+#include "sandbox/linux/seccomp-bpf-helpers/syscall_sets.h"
#include "sandbox/linux/seccomp-bpf/bpf_tests.h"
#include "sandbox/linux/system_headers/linux_syscalls.h"
#include "sandbox/linux/tests/unit_tests.h"
@@ -70,6 +71,12 @@
// program for __NR_mmap is no longer an unconditional Allow().
EXPECT_FALSE(mmap_expr->IsAllow());
+#if !defined(__LP64__) && defined(__NR_mmap2)
+ ResultExpr mmap2_expr = speech_policy.EvaluateSyscall(__NR_mmap2);
+ ASSERT_TRUE(mmap2_expr);
+ EXPECT_FALSE(mmap2_expr->IsAllow());
+#endif
+
// Control 1: the else-branch the author *intended* to fall through to.
// BPFBasePolicy::EvaluateSyscall(__NR_mmap) chains to RestrictMmapFlags(),
// which is an If/Else node — NOT an unconditional Allow.
@@ -102,7 +109,7 @@
~SpeechRecognitionMmapPolicyWrapper() override = default;
ResultExpr EvaluateSyscall(int sysno) const override {
- if (sysno == __NR_mmap) {
+ if (SyscallSets::IsMmap(sysno)) {
// Exercise the production code path. This is the same ResultExpr that
// ships in the kSpeechRecognition seccomp filter.
return real_policy_.EvaluateSyscall(sysno);
@@ -120,9 +127,9 @@
~BaselineMmapPolicyWrapper() override = default;
ResultExpr EvaluateSyscall(int sysno) const override {
- if (sysno == __NR_mmap) {
+ if (SyscallSets::IsMmap(sysno)) {
// The else-branch the author intended.
- return RestrictMmapFlags();
+ return RestrictMmapFlags(sysno);
}
return Allow();
}
Original Bug Report
i386 sys_old_mmap ABI mismatch allows bypass of mmap flag restrictions
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 Chrome Security team.
Overview: The __NR_mmap syscall (90) on i386 Linux uses a legacy ABI that takes a pointer to an argument struct, but the Chromium seccomp policy incorrectly filters it using register-based checks. This allows a compromised renderer to bypass mmap flag restrictions by providing safe flags in the checked register while hiding restricted flags like MAP_HUGETLB in the memory struct.
Affected files:
sandbox/linux/seccomp-bpf-helpers/baseline_policy.ccsandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc
Estimated timestamp from git blame: 2014-09-23
Background
On i386 Linux, the __NR_mmap syscall (syscall number 90) invokes the legacy sys_old_mmap kernel handler. Unlike modern system calls that pass arguments directly in CPU registers, sys_old_mmap takes a single argument in the %ebx register: a user-space pointer to a struct mmap_arg_struct. The kernel copies this structure from user memory to extract the actual mmap parameters (addr, len, prot, flags, fd, offset).
The Vulnerability
Chromium’s Linux seccomp-BPF policy attempts to restrict the flags passed to mmap to reduce the kernel attack surface exposed to sandboxed processes. In sandbox/linux/seccomp-bpf-helpers/baseline_policy.cc, the policy for __NR_mmap on i386 calls RestrictMmapFlags().
// sandbox/linux/seccomp-bpf-helpers/baseline_policy.cc
#if defined(__i386__) || defined(__x86_64__) || defined(__mips__) || \
defined(__aarch64__)
if (sysno == __NR_mmap)
return RestrictMmapFlags();
#endif
However, RestrictMmapFlags() (defined in sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc) incorrectly assumes the standard 6-register calling convention used by the newer mmap2 syscall:
// sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc
const Arg<int> flags(3);
return If((flags & ~kAllowedMask) == 0, Allow()).Else(CrashSIGSYS());
On i386, Arg<int>(3) instructs the BPF filter to inspect the 4th argument register, which is %esi. Because the kernel’s sys_old_mmap handler ignores %esi and instead reads the flags from the memory structure pointed to by %ebx, the seccomp filter checks the wrong location.
Impact
This ABI mismatch allows a compromised sandboxed process to bypass intended restrictions on mmap flags. Specifically, an attacker can access flags like MAP_HUGETLB, which exposes the hugetlbfs kernel subsystem. This subsystem has historically been a source of kernel vulnerabilities (e.g., Use-After-Free or race conditions) and is explicitly blocked by the sandbox policy to prevent privilege escalation.
Potential Exploitation Steps
An attacker with arbitrary code execution in the renderer process on an i386 system could exploit this bypass by following these steps:
- Allocate a buffer in memory to hold a crafted
mmap_arg_struct. - Populate the struct, setting the
flagsfield to include restricted flags, such asMAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB. - Execute assembly to trigger the system call:
- Set
%eax = 90(__NR_mmap). - Set
%ebxto the address of the crafted struct. - Set
%esi = 0x22(MAP_PRIVATE | MAP_ANONYMOUS), which satisfies the sandbox’skAllowedMask. - Execute
int 0x80.
- Set
- The seccomp filter inspects
%esi, finds the allowed flags, and permits the system call. - The kernel executes
sys_old_mmap, reads the struct from memory, and applies the restrictedMAP_HUGETLBflag, bypassing the sandbox.
Recommended Fix
Remove __i386__ from the #if block applying RestrictMmapFlags() to __NR_mmap in baseline_policy.cc.
Modern glibc-based systems use __NR_mmap2 (192) for memory mapping, which uses the standard register-based ABI and is correctly filtered by the existing policy. The legacy __NR_mmap (90) syscall should ideally be blocked outright on i386 (or explicitly fall through to a default deny) to prevent such structural bypasses.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
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.