In August 2022, CVE-2022-3075 was disclosed: a heap corruption vulnerability in Chrome’s Mojo IPC that enabled sandbox escape. Google rated it Critical. It had been exploited in the wild.
The bug was simple: Mojo’s nullptr element validator skipped type validation in a specific deserialization path. An attacker with renderer code execution could send a crafted IPC message to the browser process, and the missing validation caused heap corruption in a privileged context.
Chrome patched it in version 105. The fix was instance-specific: add validation to that one code path. The architectural root cause — that Mojo deserialization trusts DCHECK as a security boundary — was not addressed.
The 2026 Dismissal
In June 2026, I submitted VRP-001 to Chrome’s Vulnerability Reward Program: a handle type confusion in the same file (native_struct_serialization.cc), caused by the same root cause (nullptr element validator skipping validation), producing the same impact (heap corruption in browser process → sandbox escape).
Chrome’s response: “The only interesting one is the NOTREACHED()… and it’s not a security bug.”
The issue was downgraded from Vulnerability to Bug. Priority P4. Severity S4. The same vulnerability class that was Critical with a $100,000+ bounty in 2022 was “not a security bug” in 2026.
What changed? Not the code. The code was identical. What changed was Chrome’s policy: they decided that code paths guarded only by DCHECK (which compiles out in release builds) are “trusted” and therefore not security-relevant.
What I Found in the Mojo Codebase
I analyzed Chrome’s Mojo IPC codebase and found:
- 296 DCHECK instances across
mojo/core/andmojo/public/cpp/bindings/lib/ - At least 15 guard security-relevant conditions (bounds checks, offset validation, handle state)
- Zero of these 15 are protected by CHECK or if-return in release builds
- Every single one is reachable from attacker-controlled wire data sent over Mojo IPC
The framework follows a consistent anti-pattern:
// Step 1: Validate() — runs at deserialization, uses proper error returns
bool Validate(const void* data, ValidationContext* context) {
if (!IsAligned(data)) { ReportError(); return false; }
if (header->num_elements > kMax) { ReportError(); return false; }
return true;
}
// Step 2: at() — called during message dispatch, DCHECK ONLY
Ref at(size_t offset) {
DCHECK(offset < static_cast<size_t>(header_.num_elements));
return storage[offset]; // release build: no bounds check
}
The num_elements parameter is passed to ToRef() — which ignores it entirely (array_internal.h:60-62):
static Ref ToRef(StorageType* storage, size_t offset, uint32_t num_elements) {
return storage[offset]; // num_elements? never heard of it
}
In release builds: at(9999999) on a 4-element array = heap buffer overflow.
The Smoking Gun
If you need proof that the “DCHECK = trusted path” heuristic is arbitrary, look no further than message.cc:382-383:
CHECK(base::IsValueInRangeForNumericType<uint32_t>(payload.size()));
DCHECK(base::IsValueInRangeForNumericType<uint32_t>(handles.size()));
Same function. Same operation (range check). Same data source (IPC message payload). One gets CHECK, the other gets DCHECK. There is no architectural reason. It’s whatever the developer happened to type that day.
This is not a security boundary. It’s a style choice.
The Error Principle Classification
My framework classifies this as Omission Debt with Dₑ = 0.95 — the highest severity class. The pattern:
- A check exists (DCHECK)
- The check is needed (the condition can fail — that’s why the DCHECK exists)
- The check is absent in release builds (DCHECK compiles out)
- The result: attacker-controlled index → raw memory access → exploitation
The debt isn’t in individual lines of code. It’s in the policy that treats DCHECK as a security mechanism. The debt is architectural, not instance-level. That’s why fixing individual instances doesn’t help — new ones keep appearing.
The Pattern Repeats
| Year | Bug | Pattern | Disposition |
|---|---|---|---|
| 2022 | CVE-2022-3075 | nullptr validator in Mojo deser. | Critical, exploited in wild |
| 2025 | CVE-2025-2783 | Ipcz handle validation | Critical, exploited in wild |
| 2025 | CVE-2025-4609 | Ipcz broker type confusion | High, public PoC |
| 2026 | CVE-2026-13281 | Mojo integer overflow | High (8.3 CVSS) |
| 2026 | VRP-001 | nullptr validator (same as 3075) | “Not a security bug” |
| 2026 | VRP-005 | DataPipe DCHECK OOB | Dismissed (pending) |
Same root cause. Different instances. The debt migrates; it never gets paid.
What Would Fix This?
One policy change:
“Data crossing the renderer→browser IPC boundary may not be guarded by DCHECK alone.”
Every DCHECK in Mojo deserialization that guards attacker-supplied data should be either:
- Promoted to
CHECK(crash on failure — safe, kills the renderer) - Replaced with an
if (!condition) return errorpattern (graceful rejection)
This is not difficult. It’s not expensive. It’s one audit plus one cleanup change list.
Existing Mitigations Don’t Help
| Chrome Hardening | Blocks this? | Reason |
|---|---|---|
| MiraclePtr (BRP) | ❌ NO | OOB access writes to adjacent live memory, not freed |
| CFG | ❌ NO | DCHECK is not an indirect call |
| ACG | ❌ NO | OOB write doesn’t allocate or execute |
| ASLR | ❌ NO | OOB is relative to base, absolute address not needed |
| Site Isolation | ❌ NO | Bug is in IPC deserialization, not cross-site navigation |
No existing mitigation blocks this class of bug.
Timeline
- June 2026: VRP-001 submitted → dismissed as “not a security bug”
- July 2026: Architectural analysis submitted to Chrome Security
- Response: VRP-005 dismissed as “Won’t Fix (Infeasible)” in 6 minutes
Related research: Geometric Vulnerability Analysis →