Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Stateful IPC Initialization

Antipattern

A recurring trap that causes harm — learn to recognize and escape it.

A Mojo interface requires sequential method calls. State established on one call (typically an Init()) is presumed by later operational calls. A compromised renderer reorders the sequence and the browser-side handler runs against state the renderer chose.

Symptoms

  • A Mojo interface implementation class on the browser side carries member fields populated by an Init, Begin, or Open method and consulted by subsequent operational methods.
  • The interface’s documentation says “call Init before any other method,” and the browser-side code reads as if that ordering were guaranteed.
  • A handler reads a url::Origin, GURL, profile identifier, or buffer size from an instance field rather than from the message it is currently processing.
  • API-owner review comments on the interface ask “what happens if Op() is called before Init()?” or “what happens if Init() is called twice with different arguments?”, and the answers are not in the design document.
  • A mojom file shows a Begin/Continue/Commit triplet, a SetX / UseX split, or an iterator-shaped surface where one method produces a handle the next method dereferences.
  • A handler comment reads “the renderer has already validated this” or “Init checked the origin, so we can trust it here.”
  • The post-mortem on a High- or Critical-severity browser-process bug attributes the root cause to “method called out of order” or “uninitialized member field dereference under hostile call ordering.”

Why It Happens

Stateful object design is the C++ default. Conventional C++ teaches that an object is constructed once, populates its invariants, and then exposes operations that depend on those invariants. A Mojo interface implementation is also an object on the browser side, and the gravity of the language idiom pulls every author toward distributing responsibility across a constructor-shaped Init and operation-shaped methods. The pattern feels natural; the prohibition is Chromium-specific.

The cost of doing the right thing is real. A stateless protocol sends more bytes per call: the identity, origin, and size fields ride on every operational message rather than being shared via a one-time Init. The bindings code is slightly more verbose. Some interfaces feel awkward when the natural shape of the operation is multi-step (streaming uploads, long-running media decodes, iterator-shaped result sets). Authors who weigh the cost without weighing the security gain reach for statefulness.

The bindings don’t enforce ordering. Mojo guarantees in-order delivery on a single message pipe, but it doesn’t require any specific sequence of methods to be called. A compromised renderer can issue the interface’s methods in any sequence the operating system can deliver, including sequences the author didn’t consider. The author’s mental model (Init runs first; subsequent calls inherit its validation) holds only as long as the renderer cooperates, and the Untrusted Renderer Axiom says the renderer does not cooperate.

Per-channel state is invisible to per-message review. API-owner review reads one method at a time. A method that looks safe in isolation because it consults a member field can only be audited as unsafe by reading the whole interface’s call graph. Review at Chromium’s scale can’t afford to read call graphs interface by interface; a stateful design hides the bug from the gate that should have caught it.

Refactor cost compounds. Once an interface ships with downstream consumers (a feature in Stable, a downstream Chromium-based product, a developer-facing API), changing it requires coordinated migration. The longer the antipattern lives, the more expensive its removal becomes. Projects end up with stateful surfaces everyone agrees should be refactored but nobody has the budget to retire.

The Harm

A compromised renderer can call operational methods before the Init. Member fields are zero-initialized or uninitialized; the handler dereferences them, indexes into them, or treats them as authenticated identifiers. The outcomes range from a null-pointer crash (a denial-of-service bug, Low severity) to a use-of-uninitialized-memory primitive (a High-severity browser-process memory-corruption bug whose exploitation primitive is the middle link of a sandbox-escape chain).

A compromised renderer can call Init with attacker-chosen arguments, run the operational method, then call Init again with different arguments. The browser-side handler, processing the second Init, may free or replace state the first Init allocated while a callback or async continuation from the first operational call still holds references. A use-after-free in the browser process is the canonical primitive for the chain’s middle link.

A compromised renderer can omit a check the Init was responsible for performing. Suppose Init(origin, settings) validated that the renderer was authorized to use the interface with that origin and stored the result on the channel, and Op(payload) consulted only the stored result. The renderer can construct a sequence where Init ran for an allowed origin earlier in the channel’s life and Op is invoked under a different security context. The handler cannot detect the shift; the browser process loses sight of which origin the operation actually belongs to.

The antipattern is the recurring middle link of the Sandbox Escape Chain. The chain has three structural links: a renderer-process compromise (a V8 type confusion, a Blink object-lifetime bug, a parser memory-corruption), a privilege boundary crossing (an IPC handler that mishandles a renderer-controlled call), and a browser-process exploitation primitive. Project Zero’s writeups, the Chrome Security blog’s post-mortems, and the project’s own docs/security/mojo.md all single out stateful initialization as the most common shape of the second link. A renderer compromise that finds a stateless interface dies at the boundary; one that finds a stateful interface walks through.

Downstream Chromium-based products inherit the antipattern’s surface area. A vendor that ships a Chromium fork, an Electron application with a custom IPC handler, or a WebView2 integration that exposes its own Mojo interfaces inherits the boundary along with the responsibility for defending it. A stateful handler the vendor wrote becomes a vendor-owned CVE. The 2025 enterprise-browser-vendor disclosures cited in Supply-Chain Vulnerability Lag include cases where the vendor-introduced Mojo interface failed exactly this rule.

The Way Out

Stateless IPC Interface is the direct corrective pattern. Every Mojo method between renderer and browser process carries, in the single message it sends, all data required to authorize and execute the call. The browser-side handler validates each message in isolation. No prior call’s state is load-bearing on a security check.

Three concrete refactoring moves convert a stateful interface to a stateless one.

Fold the Init arguments into the operational call. Replace Init(profile_id, origin) followed by LookUp(image_url) with LookUp(profile_id, origin, image_url). The browser-side handler reads the authority parameters from the request itself and cross-checks them against the renderer’s SiteInstance identity on every call. The wire bytes per operation go up; the security gap closes.

Replace Begin/Continue/Commit triplets with a single self-contained method. Most multi-call protocols collapse to one method when the author asks “does this operation logically need to be split across messages, or is the split a convenience?” Often the split is convenience. If the operation logically needs to stream data (uploading a large blob, decoding a media file, iterating over a long result set), pass the bytes through a side channel the browser already trusts: mojo::DataPipe, mojom::BigBuffer, or base::ReadOnlySharedMemoryRegion. Keep the control methods stateless. The trusted channel carries the data; the control surface stays self-validating.

Move multi-call state into a sandboxed utility process. When the protocol genuinely cannot be flattened (a long-running compiler in V8, a media decoder, a font shaper), the project’s standing answer is to host the multi-call state in a separate sandboxed utility process whose Mojo surface to the browser is itself stateless. The Rule of 2 (docs/security/rule-of-2.md) names this move as the standing response for any interface that would need to parse complex input in the browser process at C++ scale. The utility process holds the per-channel state internally; the browser-process interface to the utility process exchanges fully-formed control messages with no cross-call dependence.

A refactor of an existing stateful interface follows a four-step sequence. Identify every member field on the implementation class whose value is populated by one method and consumed by another. Add the equivalent parameter to each consuming method’s mojom definition. Rewrite the browser-side handler to read from the message rather than the member field. Delete the member field and the Init-shaped method. The Gerrit reviewer reads each before/after method pair against the Untrusted Renderer Axiom and confirms the new version validates as if no prior call ran.

How It Plays Out

A team is adding a Mojo interface that lets a renderer request a server-side image proxy lookup. The first draft has Init(profile_id) followed by LookUp(image_url); the browser-side handler stores profile_id on the implementation class. API-owner review rejects the draft against the antipattern. A compromised renderer can call LookUp without Init (member is null, handler crashes), or can call Init(profile_id_a) once and LookUp many times expecting the same profile to apply, when in fact the renderer can call Init(profile_id_b) between any two LookUp calls. The revised interface drops Init and changes LookUp to LookUp(profile_id, image_url); the handler reads the profile from the message, cross-checks it against the renderer’s SiteInstance on every call, and runs the lookup. The bug is closed at design time. The original draft’s wire-cost objection was real; the security cost of paying it with statefulness was larger.

A contributor reviewing an existing WebTransport Mojo interface notices that SetReceiveBuffer(size) and ReceiveMessage() share a buffer field on the channel. The contributor draws the call graph: a renderer can call ReceiveMessage immediately after a small SetReceiveBuffer, before the buffer is committed, and a race in the handler indexes into the prior-call buffer with the new size. Triage classifies the bug as High-severity. The fix folds the size into the receive call so the browser allocates a one-message buffer at the moment of read and the channel carries no buffer state across calls. The interface ships in the next milestone with the antipattern removed; the bug becomes the canonical exhibit cited in subsequent design reviews. The Chrome Security blog publishes the post-mortem with the antipattern named explicitly.

A downstream enterprise-browser vendor’s security audit finds a custom IPC the vendor added for telemetry: BeginSession(user_id), Report(event), EndSession(). The vendor’s IPC followed the upstream Mojo conventions for surface shape but not for statelessness. An internal red-team exercise produced a renderer-compromise scenario in which the renderer called Report after EndSession was supposed to have torn down the session. The browser-side handler logged the renderer’s event under the previous user’s identity. The vendor refactors. Report(user_id, event) carries the session identity on every call, the handler verifies user_id against the renderer’s authenticated frame state on each report, and BeginSession/EndSession are removed entirely. The session lifecycle is reconstructed on the browser side from the stream of reports. The vendor’s audit write-up is shared with the upstream docs/security/ team as a worked example of a downstream-vendor refactor away from stateful initialization.

Notes for Agent Context

Do not propose a Mojo interface whose mojom definition includes an Init, Begin, Open, or Connect method followed by operational methods that depend on the Init arguments. Refuse the shape and refactor into self-contained methods. Never store renderer-supplied data on the per-channel implementation object’s member fields and consult it on a later call; every check runs on the current message’s fields, and every authority parameter (origin, profile identifier, session identifier) is re-verified against the renderer’s SiteInstance identity in the handler. When a logical operation appears to need multi-call structure (a streaming upload, a long-running compile, an iterator-shaped result set), route the data through mojo::DataPipe, mojom::BigBuffer, or base::ReadOnlySharedMemoryRegion and keep the control surface stateless; do not introduce a SetX/UseX split on the control interface. When refactoring an existing stateful interface, fold every member field whose value crosses calls into a parameter on each consuming mojom method, then delete the member field; a remaining cross-call member is a remaining bug.

Sources

The Chromium project’s docs/security/mojo.md names statefulness as the standing failure mode of Mojo interface design and prescribes the stateless rule directly. The docs/security/rule-of-2.md document supplies the architectural form of the larger response: when an interface would require complex parsing or multi-step state in the browser process, push it into a sandboxed utility process whose interface back to the browser is itself stateless. The Chrome Security blog’s running coverage of post-mortems names stateful initialization repeatedly as the proximate cause of browser-process memory-corruption bugs traced to renderer compromise. Project Zero’s analyses of historical Chromium sandbox escapes (Ned Williamson’s IPC-bug writeups and the V8-escape-chain documentation) single out the antipattern as the recurring middle link of the chain. The Mojo bindings reference under mojo/public/cpp/bindings/README.md is the operational source for what the bindings do and do not enforce; it documents that the system does not guarantee any particular method-call ordering, which is the substrate on which the antipattern’s harm rests.

Technical Drill-Down

  • docs/security/mojo.md — the project’s canonical operational rule for Mojo interface authors; the stateless requirement appears in the opening section and is referenced throughout.
  • docs/security/rule-of-2.md — the standing heuristic that pushes complex parsing into sandboxed utility processes; the structural answer when a stateful surface cannot be flattened.
  • mojo/public/cpp/bindings/README.md — the Mojo C++ bindings reference; documents the call-ordering and lifetime guarantees the bindings do and do not provide.
  • base/numerics/safe_conversions.h — the checked-arithmetic library used in stateless handlers to re-validate every count, offset, and size on the current message rather than trusting a stored value.
  • content/browser/ — the directory that hosts browser-side Mojo interface implementations; the in-tree examples of refactored stateless interfaces live under its subdirectories.
  • Chrome Security blog — the public-facing series in which post-mortems of stateful-IPC bugs and the underlying review rules are explained for an outside audience.