CVE-2026-11282
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsandbox/policy/BUILD.gn |
modified | |
switchsandbox/policy/linux/bpf_speech_recognition_policy_linux.cc |
modified |
Files Changed
sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.ccsandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.hsandbox/policy/BUILD.gnsandbox/policy/linux/bpf_speech_recognition_policy_linux.ccsandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc
Patch
From e38655623b2ca9ddb59df5a54e9ba549f6ca8bcb Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Wed, 29 Apr 2026 12:36:37 -0700
Subject: [PATCH] Reland "Fix mmap seccomp policy in speech recognition sandbox"
This is a reland of commit d732652538cc407ab189c0dbd4804793b13b510a.
The speech recognition policy test uses __NR_mmap, which is not
available on some architectures (e.g., ARM, which uses __NR_mmap2
instead). This caused build failures on the chromeos-arm-generic-dbg
builder.
This CL guards the entire test file with #if defined(__NR_mmap) to
ensure it only compiles on architectures where the syscall is available.
This matches the pattern used in the production policy implementation.
Original change's description:
> Fix mmap seccomp policy in speech recognition sandbox
>
> The Linux speech recognition sandbox policy incorrectly used a C++
> ternary operator with a BPF DSL expression. This caused the expression
> to be evaluated as a boolean during policy generation, which
> unconditionally evaluated to true. As a result, mmap was always allowed,
> bypassing baseline security restrictions.
>
> This CL fixes the logic by refactoring the baseline RestrictMmapFlags()
> helper to support an optional extra_allowed_mask. The speech recognition
> policy now uses this helper with MAP_POPULATE. This eliminates
> duplication of the baseline flag mask and ensures the speech recognition
> sandbox stays in sync with baseline security updates, while correctly
> allowing the required MAP_POPULATE flag.
>
> Fixed: 502023400
> Change-Id: Id4db89418b21d11aa5590115c84ad64c8e838a00
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7800739
> Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
> Reviewed-by: Ben Scarlato <akhna@google.com>
> Reviewed-by: Will Harris <wfh@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1622456}
Fixed: 502023400
Change-Id: I253ed2b15127dde71489e6219364cfa183896d0c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7800978
Reviewed-by: Ben Scarlato <akhna@google.com>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Reviewed-by: Will Harris <wfh@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1622647}
---
diff --git a/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc b/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc
index 301cc43..71be492 100644
--- a/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc
+++ b/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc
@@ -236,7 +236,7 @@
CrashSIGSYSIoctl());
}
-ResultExpr RestrictMmapFlags() {
+ResultExpr RestrictMmapFlags(uint64_t extra_allowed_mask) {
#if BUILDFLAG(IS_ANDROID) && defined(__x86_64__)
const uint64_t kArchSpecificAllowedMask = MAP_32BIT;
#else
@@ -251,7 +251,7 @@
const uint64_t kAllowedMask = MAP_SHARED | MAP_PRIVATE | MAP_ANONYMOUS |
MAP_STACK | MAP_NORESERVE | MAP_FIXED |
MAP_DENYWRITE | MAP_LOCKED | MAP_DROPPABLE |
- kArchSpecificAllowedMask;
+ kArchSpecificAllowedMask | extra_allowed_mask;
const Arg<int> flags(3);
return If((flags & ~kAllowedMask) == 0, Allow()).Else(CrashSIGSYS());
}
diff --git a/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.h b/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.h
index ed0f61c..901c24b 100644
--- a/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.h
+++ b/sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.h
@@ -5,6 +5,7 @@
#ifndef SANDBOX_LINUX_SECCOMP_BPF_HELPERS_SYSCALL_PARAMETERS_RESTRICTIONS_H_
#define SANDBOX_LINUX_SECCOMP_BPF_HELPERS_SYSCALL_PARAMETERS_RESTRICTIONS_H_
+#include <stdint.h>
#include <unistd.h>
#include "build/build_config.h"
@@ -38,7 +39,8 @@
// Only allow: MAP_SHARED | MAP_PRIVATE | MAP_ANONYMOUS |
// MAP_STACK | MAP_NORESERVE | MAP_FIXED | MAP_DENYWRITE.
// Crash if any other flag is used.
-SANDBOX_EXPORT bpf_dsl::ResultExpr RestrictMmapFlags();
+SANDBOX_EXPORT bpf_dsl::ResultExpr RestrictMmapFlags(
+ uint64_t extra_allowed_mask = 0);
// Restrict the flags argument in mremap(2).
// Crash if any flags are used.
diff --git a/sandbox/policy/BUILD.gn b/sandbox/policy/BUILD.gn
index 1a3c7c6c..b883ef3 100644
--- a/sandbox/policy/BUILD.gn
+++ b/sandbox/policy/BUILD.gn
@@ -227,6 +227,15 @@
"//testing/gtest",
]
+ if (is_linux || is_chromeos) {
+ sources += [ "linux/bpf_speech_recognition_policy_linux_unittest.cc" ]
+ deps += [
+ "//sandbox/linux:sandbox_linux_test_utils",
+ "//sandbox/linux:sandbox_services_headers",
+ "//sandbox/linux:seccomp_bpf",
+ ]
+ }
+
if (is_win) {
sources += [
"win/mf_cdm_sandbox_type_unittest.cc",
diff --git a/sandbox/policy/linux/bpf_speech_recognition_policy_linux.cc b/sandbox/policy/linux/bpf_speech_recognition_policy_linux.cc
index 781f400d..0e27ada 100644
--- a/sandbox/policy/linux/bpf_speech_recognition_policy_linux.cc
+++ b/sandbox/policy/linux/bpf_speech_recognition_policy_linux.cc
@@ -30,15 +30,10 @@
int system_call_number) const {
switch (system_call_number) {
#if defined(__NR_mmap)
- case __NR_mmap: {
+ case __NR_mmap:
// The speech recognition sandbox requires the MAP_POPULATE flag in
// addition to the default flags.
- const uint64_t kAllowedMask = MAP_POPULATE;
- const bpf_dsl::Arg<int> flags(3);
- return (flags & ~kAllowedMask) == 0
- ? Allow()
- : BPFBasePolicy::EvaluateSyscall(system_call_number);
- }
+ 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.
diff --git a/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc b/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc
new file mode 100644
index 0000000..36041d2
--- /dev/null
+++ b/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc
@@ -0,0 +1,186 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Regression test for: BoolExpr-in-C++-ternary always-true bug in
+// SpeechRecognitionProcessPolicy. The C++ conditional operator at
+// bpf_speech_recognition_policy_linux.cc:38-40 used a bpf_dsl::BoolExpr
+// (== std::shared_ptr<const internal::BoolExprImpl>, which is never null)
+// as the ternary condition. shared_ptr's contextual conversion to bool is
+// therefore always true, so EvaluateSyscall(__NR_mmap) returned Allow()
+// unconditionally and the compiled BPF filter for mmap was
+// SECCOMP_RET_ALLOW with no inspection of arg[3]. This bypassed
+// RestrictMmapFlags() and let a compromised speech-recognition utility
+// process call mmap() with normally-forbidden flags such as MAP_GROWSDOWN
+// and MAP_HUGETLB.
+//
+// See crbug.com/502023400 for details.
+
+#include "sandbox/policy/linux/bpf_speech_recognition_policy_linux.h"
+
+#include <errno.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+
+#include "build/build_config.h"
+#include "sandbox/linux/bpf_dsl/bpf_dsl.h"
+#include "sandbox/linux/bpf_dsl/bpf_dsl_impl.h"
+#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/bpf_tests.h"
+#include "sandbox/linux/system_headers/linux_syscalls.h"
+#include "sandbox/linux/tests/unit_tests.h"
+#include "sandbox/policy/linux/bpf_base_policy_linux.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if defined(__NR_mmap)
+
+#ifndef MAP_GROWSDOWN
+#define MAP_GROWSDOWN 0x0100
+#endif
+#ifndef MAP_HUGETLB
+#define MAP_HUGETLB 0x40000
+#endif
+
+namespace sandbox::policy {
+namespace {
+
+using bpf_dsl::Allow;
+using bpf_dsl::ResultExpr;
+
+// -----------------------------------------------------------------------------
+// Part 1 — Static analysis of the production policy AST.
+//
+// SpeechRecognitionProcessPolicy::EvaluateSyscall(__NR_mmap) builds and
+// returns a ResultExpr at C++ compile/run time *before* the BPF program is
+// emitted. If the policy were correctly written with bpf_dsl::If().Else(),
+// the returned node would be an IfThenResultExprImpl whose IsAllow() is
+// false.
Regression Test / PoC
diff --git a/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc b/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc
new file mode 100644
index 0000000..36041d2
--- /dev/null
+++ b/sandbox/policy/linux/bpf_speech_recognition_policy_linux_unittest.cc
@@ -0,0 +1,186 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Regression test for: BoolExpr-in-C++-ternary always-true bug in
+// SpeechRecognitionProcessPolicy. The C++ conditional operator at
+// bpf_speech_recognition_policy_linux.cc:38-40 used a bpf_dsl::BoolExpr
+// (== std::shared_ptr<const internal::BoolExprImpl>, which is never null)
+// as the ternary condition. shared_ptr's contextual conversion to bool is
+// therefore always true, so EvaluateSyscall(__NR_mmap) returned Allow()
+// unconditionally and the compiled BPF filter for mmap was
+// SECCOMP_RET_ALLOW with no inspection of arg[3]. This bypassed
+// RestrictMmapFlags() and let a compromised speech-recognition utility
+// process call mmap() with normally-forbidden flags such as MAP_GROWSDOWN
+// and MAP_HUGETLB.
+//
+// See crbug.com/502023400 for details.
+
+#include "sandbox/policy/linux/bpf_speech_recognition_policy_linux.h"
+
+#include <errno.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+
+#include "build/build_config.h"
+#include "sandbox/linux/bpf_dsl/bpf_dsl.h"
+#include "sandbox/linux/bpf_dsl/bpf_dsl_impl.h"
+#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/bpf_tests.h"
+#include "sandbox/linux/system_headers/linux_syscalls.h"
+#include "sandbox/linux/tests/unit_tests.h"
+#include "sandbox/policy/linux/bpf_base_policy_linux.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if defined(__NR_mmap)
+
+#ifndef MAP_GROWSDOWN
+#define MAP_GROWSDOWN 0x0100
+#endif
+#ifndef MAP_HUGETLB
+#define MAP_HUGETLB 0x40000
+#endif
+
+namespace sandbox::policy {
+namespace {
+
+using bpf_dsl::Allow;
+using bpf_dsl::ResultExpr;
+
+// -----------------------------------------------------------------------------
+// Part 1 — Static analysis of the production policy AST.
+//
+// SpeechRecognitionProcessPolicy::EvaluateSyscall(__NR_mmap) builds and
+// returns a ResultExpr at C++ compile/run time *before* the BPF program is
+// emitted. If the policy were correctly written with bpf_dsl::If().Else(),
+// the returned node would be an IfThenResultExprImpl whose IsAllow() is
+// false.
+// -----------------------------------------------------------------------------
+TEST(SpeechRecognitionPolicyMmapBypass, PolicyReturnsConditionalForMmap) {
+ SpeechRecognitionProcessPolicy speech_policy;
+
+ ResultExpr mmap_expr = speech_policy.EvaluateSyscall(__NR_mmap);
+ ASSERT_TRUE(mmap_expr);
+
+ // ********** FIXED **********
+ // IsAllow() is only true for the Allow() leaf node; an If/Else conditional
+ // node returns false. The fact that this is false proves that the BPF
+ // program for __NR_mmap is no longer an unconditional Allow().
+ EXPECT_FALSE(mmap_expr->IsAllow());
+
+ // 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.
+ BPFBasePolicy base_policy;
+ ResultExpr base_expr = base_policy.EvaluateSyscall(__NR_mmap);
+ ASSERT_TRUE(base_expr);
+ EXPECT_FALSE(base_expr->IsAllow());
+
+ // Control 2: same file, correctly-written prctl handler uses If().Else()
+ // and is NOT an unconditional Allow.
+ ResultExpr prctl_expr = speech_policy.EvaluateSyscall(__NR_prctl);
+ ASSERT_TRUE(prctl_expr);
+ EXPECT_FALSE(prctl_expr->IsAllow());
+}
+
+// -----------------------------------------------------------------------------
+// Part 2 — Live kernel verification under the production policy expression.
+//
+// We cannot apply the full SpeechRecognitionProcessPolicy in a unit test
+// because its `default:` branch reaches into SandboxLinux::GetInstance()
+// (broker not initialised here). Instead we wrap a trivial Policy that
+// delegates *only* __NR_mmap to the real production policy and allows
+// everything else. This means the seccomp program installed in the forked
+// test process for __NR_mmap is *byte-identical* to the one Chrome installs
+// in the speech-recognition utility process.
+// -----------------------------------------------------------------------------
+class SpeechRecognitionMmapPolicyWrapper : public bpf_dsl::Policy {
+ public:
+ SpeechRecognitionMmapPolicyWrapper() = default;
+ ~SpeechRecognitionMmapPolicyWrapper() override = default;
+
+ ResultExpr EvaluateSyscall(int sysno) const override {
+ if (sysno == __NR_mmap) {
+ // Exercise the production code path. This is the same ResultExpr that
+ // ships in the kSpeechRecognition seccomp filter.
+ return real_policy_.EvaluateSyscall(sysno);
+ }
+ return Allow();
+ }
+
+ private:
+ SpeechRecognitionProcessPolicy real_policy_;
+};
+
+class BaselineMmapPolicyWrapper : public bpf_dsl::Policy {
+ public:
+ BaselineMmapPolicyWrapper() = default;
+ ~BaselineMmapPolicyWrapper() override = default;
+
+ ResultExpr EvaluateSyscall(int sysno) const override {
+ if (sysno == __NR_mmap) {
+ // The else-branch the author intended.
+ return RestrictMmapFlags();
+ }
+ return Allow();
+ }
+};
+
+// REGRESSION TEST: under the fixed speech-recognition policy
+// expression, mmap with MAP_GROWSDOWN and MAP_HUGETLB is now correctly blocked.
+BPF_DEATH_TEST_C(SpeechRecognitionPolicyMmapBypass,
+ ForbiddenGrowsdownBlocked,
+ DEATH_SEGV_MESSAGE(GetErrorMessageContentForTests()),
+ SpeechRecognitionMmapPolicyWrapper) {
+ mmap(nullptr, 4096, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN, -1, 0);
+}
+
+BPF_DEATH_TEST_C(SpeechRecognitionPolicyMmapBypass,
+ ForbiddenHugetlbBlocked,
+ DEATH_SEGV_MESSAGE(GetErrorMessageContentForTests()),
+ SpeechRecognitionMmapPolicyWrapper) {
+ mmap(nullptr, 2 * 1024 * 1024, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);
+}
+
+BPF_DEATH_TEST_C(SpeechRecognitionPolicyMmapBypass,
+ ForbiddenGrowsdownWithPopulateBlocked,
+ DEATH_SEGV_MESSAGE(GetErrorMessageContentForTests()),
+ SpeechRecognitionMmapPolicyWrapper) {
+ // MAP_POPULATE | MAP_GROWSDOWN: even with the required MAP_POPULATE flag,
+ // forbidden flags like MAP_GROWSDOWN must still be blocked.
+ mmap(nullptr, 4096, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE | MAP_GROWSDOWN, -1, 0);
+}
+
+// Ensure that normal mmap with MAP_POPULATE still works (this was the goal
+// of the original code).
+BPF_TEST_C(SpeechRecognitionPolicyMmapBypass,
+ MapPopulateAllowed,
+ SpeechRecognitionMmapPolicyWrapper) {
+ errno = 0;
+ void* p = mmap(nullptr, 4096, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
+ BPF_ASSERT_NE(MAP_FAILED, p);
+ munmap(p, 4096);
+}
+
+// CONTROL: under the *intended* baseline restriction, the very same
+// MAP_GROWSDOWN call is caught by seccomp and the process dies with the
+// canonical "**CRASHING**:seccomp-bpf failure in syscall" SIGSYS message.
+BPF_DEATH_TEST_C(SpeechRecognitionPolicyMmapBypass,
+ BaselineBlocksGrowsdown,
+ DEATH_SEGV_MESSAGE(GetErrorMessageContentForTests()),
+ BaselineMmapPolicyWrapper) {
+ mmap(nullptr, 4096, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN, -1, 0);
+ // Not reached — CrashSIGSYS() runs first.
+}
+
+} // namespace
+} // namespace sandbox::policy
+
+#endif // defined(__NR_mmap)
Original Bug Report
Potential seccomp bypass in Speech Recognition sandbox due to BPF DSL ternary logic error
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 Linux speech recognition sandbox policy incorrectly uses a C++ ternary operator with a BPF DSL expression, which unconditionally evaluates to true at policy generation time. This causes the seccomp-bpf filter to unconditionally allow the mmap syscall, bypassing intended baseline security restrictions. A compromised utility process could potentially use these unrestricted flags to assist in a sandbox escape.
Affected files:
sandbox/policy/linux/bpf_speech_recognition_policy_linux.cc
Estimated timestamp from git blame: 2022-10-29
Description
A potential vulnerability exists in the Linux speech recognition sandbox policy (sandbox/policy/linux/bpf_speech_recognition_policy_linux.cc) where the mmap system call is unconditionally allowed due to a logic error involving the Chromium BPF Domain Specific Language (DSL).
In SpeechRecognitionProcessPolicy::EvaluateSyscall, the code attempts to conditionally allow the MAP_POPULATE flag while falling back to the baseline policy for other cases:
case __NR_mmap: {
const uint64_t kAllowedMask = MAP_POPULATE;
const bpf_dsl::Arg<int> flags(3);
return (flags & ~kAllowedMask) == 0
? Allow()
: BPFBasePolicy::EvaluateSyscall(system_call_number);
}
The issue arises from using the C++ ternary operator (? :). The BPF DSL expression (flags & ~kAllowedMask) == 0 evaluates to a bpf_dsl::BoolExpr, which is an alias for std::shared_ptr<const internal::BoolExprImpl>. To satisfy the ternary operator’s condition, C++ invokes std::shared_ptr::operator bool(), which simply checks if the underlying pointer is non-null. Because the BPF AST node is always successfully allocated during policy generation, the pointer is never null.
Consequently, the condition evaluates as unconditionally true during the C++ execution phase. The policy generated for __NR_mmap becomes an unconditional Allow(), and the BPFBasePolicy::EvaluateSyscall branch is entirely omitted from the final seccomp-bpf filter.
Impact
This defense-in-depth failure bypasses the restrictions normally applied by RestrictMmapFlags() in the baseline policy. An attacker who has already compromised the speech recognition utility process can use sensitive mmap flags that are otherwise heavily restricted (such as MAP_GROWSDOWN, MAP_HUGETLB, or MAP_FIXED_NOREPLACE). Access to these flags broadens the kernel attack surface and can facilitate kernel-level sandbox escapes (e.g., via “Stack Clash” techniques).
Notably, this bug masks a secondary logic error: if bpf_dsl::If().Else() had been used correctly, the kAllowedMask logic (which only permits MAP_POPULATE) would have caused standard mmap calls (which require MAP_PRIVATE or MAP_SHARED) to fail, likely crashing the utility process on startup.
Potential Attacker Steps
Note: Our tooling does not yet have the ability to run code, so these are suggested theoretical steps an attacker would follow to trigger the vulnerability:
- Gain Remote Code Execution (RCE) within the sandboxed Speech Recognition utility process (for example, by exploiting a memory corruption bug in the closed-source audio processing library).
- Execute shellcode that calls the
mmapsyscall with sensitive flags, such asMAP_GROWSDOWN, to manipulate virtual memory area adjacency as part of a kernel exploit chain. - The kernel’s seccomp-bpf engine intercepts the
mmapsyscall but unconditionally allows it due to the policy logic error, enabling the attacker to proceed with the sandbox escape.
Suggested Fix
- Replace the C++ ternary operator (
cond ? then : els) with the BPF DSL’s nativeIf(cond, then).Else(els)construct. - Correct the
kAllowedMasklogic formmap. The current mask of solelyMAP_POPULATEis too restrictive and would block normal execution. Ensure the logic properly factors in the baseline allowed flags (fromRestrictMmapFlags()) alongsideMAP_POPULATEbefore enforcing the restriction.
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.