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

IPC Integer Type Discipline

Pattern

A named solution to a recurring problem.

Every size, count, and offset that crosses a Mojo IPC trust boundary is carried in an explicitly-sized unsigned integer type, and every arithmetic operation on those values runs through base/numerics/safe_conversions.h, so a hostile renderer cannot weaponize the browser-side handler’s integer math.

Context

The pattern lives at the same boundary as Stateless IPC Interface: the choke point between a renderer process and the browser process, where every Mojo method’s parameters arrive under the Untrusted Renderer Axiom. Where statelessness governs which checks a method must perform, integer-type discipline governs how the count, offset, and size checks are written so the arithmetic itself can’t be turned into the bug.

Mojo’s wire format and the in-tree numerics library are performance-adjacent code, but the consequences of getting their integer rules wrong are security-critical. A contributor wiring up a new Mojo interface, an API owner reviewing one, or an AI coding agent generating one applies the discipline on every numeric field; no higher-level check is meaningful until it holds.

Problem

Conventional C++ defaults are dangerous at an IPC trust boundary. int is signed, so a renderer-supplied negative value compares less than any plausible upper bound but indexes into memory as a large unsigned offset. size_t is platform-dependent: 32 bits on a 32-bit Android build, 64 bits on desktop. A value the renderer sends as a 64-bit number is silently truncated when the browser handler reads it into a size_t on a 32-bit target, and the truncated value passes a bounds check that the original would have failed. Arithmetic on either type wraps silently on overflow. A length + offset check that is safe in the small-value regime becomes a buffer-overflow primitive when both values approach the type’s maximum and the addition rolls to a tiny positive number.

The same primitives recur in renderer-side reports of “how much data I am about to send,” “where in this region I am reading,” and “how many records this batch contains.” A handler that accepts the renderer’s claim and indexes into a fixed allocation has skipped the check that mattered. Default integer types are convenient; that convenience is the bug. The type system says the code is correct; the trust boundary says the values are not.

Forces

  • Renderer integers are attacker-controlled. A compromised renderer can send any 32-bit or 64-bit pattern through any Mojo field; “negative” and “very large” aren’t input errors, they’re exploitation primitives.
  • Default C++ integer types are wrong at this boundary. int is signed; size_t is platform-dependent; both wrap silently on overflow. None of the three properties is acceptable when the value originates in an untrusted process.
  • Safe-arithmetic libraries impose a small but real ergonomic cost. base::CheckedNumeric<size_t> is more typing than size_t, and the call sites need to handle the failure branch. Authors who don’t know the discipline draft code without it.
  • Bounds checks done wrong look exactly like bounds checks done right. A if (offset + length > buffer_size) return false; check reads correct on inspection but is a vulnerability when the addition overflows. Review can’t catch the bug without explicitly running the overflow case in its head.
  • Linting and codegen can enforce the rule. Chromium’s clang plugins and the IncludeWhatYouUse (IWYU) integration flag the wrong types and the missing safe_conversions.h include; the discipline is enforceable mechanically when authors opt in.

Solution

Apply three rules to every Mojo interface parameter and every browser-side handler that uses one:

  1. Explicitly-sized unsigned integers only. Sizes, counts, and offsets that cross a Mojo boundary use uint32_t or uint64_t, declared exactly that way in the .mojom file. Never int, never int32_t, never size_t, never long. The wire type is the source of truth; the C++ type on the handler side matches it byte for byte.
  2. Checked arithmetic through base/numerics/safe_conversions.h. Every operation on a renderer-supplied integer runs through base::CheckedNumeric<T>, base::CheckMul, base::CheckAdd, or base::checked_cast<T>. The result is consumed only after .IsValid() or the explicit .ValueOrDie() discipline; an unguarded .ValueOrDie() on attacker-controlled input is a deliberate browser-process crash, not a silent miscalculation.
  3. Cross-cast at the boundary, not deep in the handler. When the renderer sends a uint64_t byte count that the browser will eventually use as a size_t to index a buffer, the conversion happens immediately on receipt via base::checked_cast<size_t>, and the conversion’s failure terminates the request. The antipattern is to carry the wider type deep into the handler and convert late; every later operation that uses the value pays the same overflow cost again.

The three rules close the family of bugs the CWE catalog files under CWE-190 (integer overflow), CWE-191 (integer underflow), and CWE-681 (sign conversion). The compiler can’t catch them because the types are valid in non-IPC contexts; only the discipline names the contextual rule.

How It Plays Out

A team is adding a Mojo interface that lets a renderer report a buffer it has prepared for upload. The draft declares void Report(uint32_t offset, uint32_t length, mojo_base.mojom.BigBuffer payload), and the browser-side handler checks if (offset + length > buffer_size) return false; before reading the slice. API-owner review rejects the check. With both offset and length near UINT32_MAX, the addition wraps to a small positive value that passes the comparison, and the handler reads off the end of buffer_size. The revision uses base::CheckedNumeric<uint32_t> end = base::CheckAdd(offset, length); if (!end.IsValid() || end.ValueOrDie() > buffer_size) return false;. The wrap case now produces an invalid CheckedNumeric that fails the validity test on the same line as the addition. The handler refuses the message, and the renderer’s attempt to address out-of-bounds memory dies at the boundary.

A contributor reviewing an existing browser-side handler notices a field declared int count reading from a Mojo message. The contributor walks the call graph. count is multiplied by sizeof(Record) to compute an allocation size, and the multiplication wraps for any count above approximately INT_MAX / sizeof(Record). The bug is a heap-overflow primitive: a renderer that sends a crafted large count allocates a small buffer and writes far past it. The fix changes the .mojom declaration to uint64_t count, replaces the multiplication with base::CheckMul(count, sizeof(Record)), and rejects the message when the multiplication overflows. The CVE is filed under the Sandbox Escape Chain writeup as the type of middle-link bug that turns a renderer compromise into something more.

A downstream-vendor maintainer adds a custom IPC for their enterprise telemetry collector. The interface takes a size_t record count from the renderer because that’s what the maintainer’s local handler eventually uses. Security review flags two problems: the size_t is 32 bits on the vendor’s 32-bit Android build but 64 bits on their desktop build, so the same renderer message is parsed differently on different targets; and size_t is unsigned but the maintainer’s handler still subtracts from it without checked arithmetic. The vendor refactors the .mojom to uint64_t and runs every arithmetic step through the safe_conversions.h templates. The next upstream audit cites the vendor’s interface as a worked example of the discipline applied outside the Chromium tree.

Consequences

Benefits.

  • The compiler-enforced type and the checked-arithmetic library together close the integer-overflow family on every reviewed interface. A bug that survives the rule is a bug that survives explicit review, not one that hid behind a default.
  • API-owner review can audit one method’s integer parameters in isolation. The standing review question becomes “is every renderer-supplied number a uint32_t or uint64_t declared in the .mojom, and does every arithmetic step on it use a base::Checked* helper?” — a question a reviewer can answer in seconds.
  • The discipline survives author turnover. The .mojom declaration is the wire-level contract; a new contributor who reads the interface sees the types and inherits the discipline mechanically.
  • Cross-platform builds behave identically. Replacing size_t and int with explicit widths at the boundary removes the family of bugs where the same renderer message is parsed differently on 32-bit and 64-bit targets.
  • Downstream vendors who follow the rule on their custom IPCs inherit the upstream invariant for free. A vendor who breaks it owns the resulting CVE; the Supply-Chain Vulnerability Lag entry catalogs the cost.

Liabilities.

  • The call-site code is noisier. A base::CheckedNumeric<uint32_t> end = base::CheckAdd(offset, length); is three more tokens than uint32_t end = offset + length;; new contributors won’t write the longer form unless review or codegen requires it.
  • The failure path on every arithmetic operation has to be handled. A handler that ignores .IsValid() and calls .ValueOrDie() converts every overflow into a browser-process crash; that’s still a denial-of-service vector, even if it isn’t a memory-corruption one. The team has to decide per call site which failure mode is preferable.
  • Retrofitting an existing interface is expensive. Live Mojo interfaces with downstream consumers can’t change wire types without a coordinated migration; the pattern is cheapest to apply at design time and most costly to apply after the interface has shipped.

Notes for Agent Context

When writing or modifying a Mojo interface (.mojom file) that takes a size, count, or offset from a renderer, declare the field as uint32_t or uint64_t only. Never use int, int32_t, int64_t, size_t, or long for renderer-supplied numeric fields; the signed types and the platform-dependent size_t are the bug. On the browser-side handler, run every arithmetic operation on the renderer-supplied integer through base/numerics/safe_conversions.h: use base::CheckedNumeric<T>, base::CheckAdd, base::CheckMul, base::CheckSub, and base::checked_cast<T> instead of raw +, *, -, or C-style casts. Always inspect .IsValid() before consuming a CheckedNumeric result; an unguarded .ValueOrDie() on attacker-controlled input is a deliberate crash, not a check. Perform the cast from the wire type (uint64_t) to any narrower in-process type (size_t for indexing) immediately on message receipt with base::checked_cast<size_t>, and fail the request when the cast fails; do not carry the wider value deep into the handler. If asked to write if (offset + length > buffer_size), refuse: replace with base::CheckedNumeric<uint64_t> end = base::CheckAdd(offset, length); if (!end.IsValid() || end.ValueOrDie() > buffer_size) return false; so the wrap case fails on the same line as the addition.

Sources

The canonical primary source is the Chromium project’s base/numerics/README.md, which states the rule directly and walks through the CheckedNumeric template and its companions. Contributors read it when they encounter a safe_conversions.h review comment. The docs/security/mojo.md document supplies the higher-level frame: every Mojo handler treats its inputs as attacker-controlled, and integer-type discipline is the type-system half of the requirement that statelessness covers structurally. The Mojo bindings documentation under mojo/public/cpp/bindings/README.md defines the wire types the discipline maps onto; authors consult it when choosing between uint32_t and uint64_t for a field.

The vulnerability taxonomy behind the rule comes from MITRE’s CWE catalog: CWE-190 (Integer Overflow or Wraparound), CWE-191 (Integer Underflow), and CWE-681 (Incorrect Conversion between Numeric Types). The discipline is engineered to refuse each at the boundary. Chrome Security blog post-mortems of historical IPC integer bugs name the discipline as the standing fix; Project Zero writeups of full sandbox-escape chains routinely identify a missing checked-arithmetic step as the proximate cause of the middle link — the implicit citation every time.

Technical Drill-Down

  • base/numerics/README.md — the canonical reference for CheckedNumeric, ClampedNumeric, and the safe-cast helpers; the file every reviewer cites when asking for integer-type discipline.
  • base/numerics/safe_conversions.h — the header that defines base::checked_cast, base::saturated_cast, and the supporting templates; every browser-side handler that touches a renderer-supplied integer includes it.
  • base/numerics/checked_math.h — the arithmetic side of the library; CheckAdd, CheckMul, CheckSub and the CheckedNumeric template that wraps them.
  • docs/security/mojo.md — the project’s standing operational rules for Mojo interface authors; the integer rules sit alongside the statelessness rules in the same checklist.
  • mojo/public/cpp/bindings/README.md — the Mojo C++ bindings reference; the wire-type mapping that determines which .mojom declaration the discipline applies to.
  • CWE-190: Integer Overflow or Wraparound — the MITRE taxonomy entry that names the bug family the discipline closes at the trust boundary.