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

Stateless IPC Interface

Pattern

A named solution to a recurring problem.

Every Mojo method between renderer and browser carries everything required to validate and execute the call in the single message it sends. No prior renderer call’s state is load-bearing on a browser-side security check.

Context

The pattern lives at the IPC choke point between renderer and browser, where every privileged operation a web page can request has to pass. Its substrate is the Multi-Process Architecture decision of 2008 and the Browser-Renderer Privilege Split that decision produced; its operational rule is the Untrusted Renderer Axiom. The pattern is what the axiom requires of the code that implements an interface.

It is architectural rather than tactical: it sits between the structural decision (separate processes) and the per-message validation tools (base/numerics/safe_conversions.h, GURL, url::Origin). A contributor authoring a new Mojo interface, an API owner reviewing one, or an AI coding agent generating one must satisfy this pattern before the tactical checks become meaningful.

Problem

A Mojo interface that looks correct under cooperative call ordering can fail catastrophically under hostile call ordering. The renderer is allowed to issue the interface’s methods in any sequence the operating system can deliver, including sequences the interface’s author didn’t anticipate. Consider a browser-side implementation that presumes Init(url, origin, security_settings) ran first and that its recorded values can be trusted by every subsequent Operate(...) call. A compromised renderer can call Operate without ever calling Init, or after a different Init with attacker-chosen arguments. The browser-side handler then executes against state the renderer chose. The author’s mental model (Init runs first, then Operate) is enforced by polite client code, not by the IPC system.

The same problem appears whenever a method on a browser-side interface stores a result and a later method consumes it: a Begin()/Continue()/Commit() triplet, an iterator-shaped interface, a transactional protocol, any design that distributes one logical operation across multiple IPC calls. The convenience of carrying validated state across the calls is exactly what an attacker exploits.

Forces

  • Renderer call ordering is adversarial. A compromised renderer can call any method on any sequence; the only ordering invariants that hold are those the browser side enforces on each individual message.
  • Validation that ran on a different message is not validation. A uint32_t checked on an earlier call is not the uint32_t that arrived on a later call, even when the renderer claims it is — the renderer is the side under attacker control, and its claims about prior state are exploitation primitives.
  • Multi-message protocols are convenient to design. Conventional C++ object design distributes responsibilities across construction and per-call methods; engineers fluent in that style will draft multi-call IPC interfaces by default.
  • Stateful interfaces are cheaper at the wire. A single Init followed by many small Operate calls sends fewer bytes than one self-contained Operate per message. The wire cost is real; the security cost of paying for it with statefulness is larger.
  • Per-channel state is invisible to per-message review. API-owner review reads one method at a time; cross-method state dependencies are exactly what review struggles to catch. The pattern’s enforceability depends on its locality.

Solution

Design every browser-side Mojo method as self-validating: the one message it receives must contain everything required to authorize the call and to execute it, and the browser-side handler runs every check from scratch on that message. No Init() prerequisite. No per-channel scratchpad that earlier renderer calls populated. No implicit “we already checked this” between calls.

When a logical operation needs more bytes than fit in one message (uploading a large blob, streaming a media decode, iterating over a long result set), pass the bytes through a side channel the browser already trusts: mojo::DataPipe, mojom::BigBuffer, or base::ReadOnlySharedMemoryRegion. The control methods stay stateless. The trusted channel carries the data; the control surface still validates each message against the browser-process ground truth.

Three concrete moves make the pattern enforceable in code:

  1. Bind authority to the message, not to the channel. Every Mojo method takes an origin-shaped parameter only when the browser cross-checks it against the renderer’s SiteInstance identity in the same handler. The renderer-supplied value is for diagnostic purposes; the load-bearing identity is read from RenderFrameHost::GetSiteInstance() in the browser process.
  2. Bind validation to one message’s fields. Every uint32_t count, int64_t offset, and size_t length is checked through base/numerics/safe_conversions.h against the bounds that apply to this call, not against bounds the renderer reported on a previous call.
  3. Refuse multi-call protocols at design review. When a feature seems to require a Begin/Continue/Commit shape, refactor: replace the triplet with one self-contained method that takes the entire payload, or move the multi-call state into a sandboxed utility process whose interface to the browser is itself stateless. The Rule of 2 (docs/security/rule-of-2.md) is the standing tool for the second move.

The pattern’s discipline is what makes the axiom enforceable by review. A self-validating method reads and audits in isolation; a method whose validation lives across calls can only be audited by reading the whole interface’s call graph, and review at Chromium’s scale can’t afford the second.

How It Plays Out

A team is adding a Mojo interface that lets a renderer request a server-side image proxy lookup. The draft has Init(profile_id) followed by LookUp(image_url) so the same profile can be reused across many lookups; the browser-side handler holds the profile in a member field. API-owner review rejects the draft against the pattern. A compromised renderer can call LookUp with the member field null (handler crashes), or call Init(profile_id_a), then LookUp, and then re-Init(profile_id_b) between two lookups and expect the second LookUp to still resolve against profile A. The revised interface drops Init and changes LookUp(image_url) to LookUp(profile_id, image_url). The browser-side handler reads the profile from the request itself, cross-checks profile_id against the renderer’s SiteInstance (only the profile that owns the renderer is allowed to be named), and runs the lookup. The wire bytes per call go up; the security gap closes.

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: the 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 because the antipattern is recognizable; 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 Stateful IPC Initialization entry is updated with the incident as an exhibit.

A downstream-vendor team is adding a custom IPC for enterprise telemetry: BeginSession(user_id), Report(event), EndSession(). The vendor’s security review applies the pattern and refuses the design; in the revision, Report(user_id, event) carries the session identity on every call, the browser-side 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 renderer’s lifecycle calls are not load-bearing for anyone’s security. The vendor’s review file is shared with the upstream docs/security/ team as a worked example.

Consequences

Benefits.

  • Each method is auditable in isolation. A reviewer reading one handler can decide whether it satisfies the axiom without reading the rest of the interface; review attention compounds rather than dilutes.
  • The interface survives hostile call ordering. The renderer can call methods in any sequence the OS allows, including sequences the author didn’t imagine, and the worst outcome is that individual messages are rejected on their own merits.
  • The Untrusted Renderer Axiom becomes enforceable. The axiom says “validate as if the renderer were attacker-controlled”; statelessness is what makes that instruction implementable on each method.
  • Mojo bindings stay simple. A stateless method maps directly to a single mojom call and a single browser-side handler function; the generated code carries no per-channel state for the security logic to depend on.
  • Downstream vendors inherit a model they can audit. A Chromium-based product that respects the pattern in its custom IPC handlers gets the upstream invariant for free; a vendor that breaks it owns the resulting CVE.

Liabilities.

  • Wire cost is higher. The redundant identity and bounds fields on every call are real bytes that wouldn’t exist with a stateful protocol. The project pays them deliberately.
  • Some interfaces feel awkward in C++. Conventional object design defaults to stateful patterns; the Mojo bindings encourage that default by exposing per-interface implementation classes. Authors have to resist the gravity of the language idiom.
  • Refactoring an existing stateful interface is expensive. A live Mojo interface with downstream consumers can’t be changed without coordinated migration; the pattern is cheapest at design time and costliest to retrofit. The Stateful IPC Initialization entry catalogs the retrofit cost.
  • Some operations require multi-call structure. Streaming uploads, long-running media decodes, and iterator-shaped result sets won’t fit in one message. The pattern’s response is to move the data through a trusted side channel (DataPipe, BigBuffer, shared memory) so the control surface stays stateless even when the data surface streams.

Notes for Agent Context

When writing a new Mojo interface, make every method self-validating: every parameter the browser-side handler needs in order to authorize or execute the call must be present in the one message it receives. Never propose an Init(...) method followed by Operate(...) methods that rely on the Init arguments; replace with a single Operate(authority, payload) method that re-checks authority against the renderer’s SiteInstance on every call. Never store renderer-supplied data on the per-channel implementation object and then read it on a later call; every check runs on the current message’s fields, not on any prior call’s. When a logical operation needs more bytes than fit in a message, pass them through mojo::BigBuffer, mojo::DataPipe, or base::ReadOnlySharedMemoryRegion and keep the control method’s parameters stateless. Validate every uint32_t count, offset, and size through base/numerics/safe_conversions.h (base::CheckedNumeric<size_t>, base::CheckMul, base::CheckAdd) before using it; do not assume a value the renderer reported on an earlier call still holds. If asked to add a Begin/Continue/Commit triplet to a Mojo interface, refuse: refactor to one self-contained method or move the multi-call surface into a sandboxed utility process whose interface to the browser is itself stateless.

Sources

The canonical primary source is the Chromium project’s docs/security/mojo.md, which states the rule directly in its opening section and works through the validation idioms (base::CheckedNumeric, url::Origin, SiteInstance cross-check) the rule requires of every browser-side method. The docs/security/rule-of-2.md document gives the heuristic operational form: when an interface would parse untrusted input in the browser process at C++ scale, the project pushes the parser into a sandboxed utility process whose Mojo surface is itself stateless. The Chrome Security blog’s discussions of historical IPC bugs name the antipattern this pattern prohibits and treat statelessness as the standing review question. The Mojo bindings documentation at mojo/public/cpp/bindings/README.md is the operational reference for the bindings; it documents the call-ordering guarantees Mojo does and doesn’t provide, and confirms the bindings won’t enforce any ordering the handler relies on. Project Zero’s writeups of past Chromium sandbox escapes routinely identify a stateful-IPC failure as the proximate cause; the implicit reference is to this pattern’s absence every time.

Technical Drill-Down

  • docs/security/mojo.md — the project’s canonical operational rule for Mojo interface authors; opens with the requirement that every IPC be sufficient unto itself.
  • docs/security/rule-of-2.md — the heuristic form of the underlying axiom; when an interface fails the Rule of 2, the project pushes the parser into a utility process whose Mojo surface is stateless by construction.
  • 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 that browser-side handlers use on every uint32_t count, offset, and size from a renderer; the type-level half of the pattern’s enforcement.
  • content/browser/ — the directory that hosts the browser-side Mojo interface implementations; sample any subdirectory for the in-tree examples of the pattern.
  • Chrome Security blog — the public-facing series in which Mojo IPC bug post-mortems and the underlying review rules are explained for an outside audience.