bi-02 — Chromium Architecture & the Process Model
Phase 1 · Foundations · Spec areas §2 (architecture map), §18 (multiprocess), §19 (security architecture). Prerequisite: bi-01.
Cross-track hook: frontend-principal-engineering.md §2 (browser architecture), §13
(security). This module supplies the enforcement mechanism behind claims that track makes
about origins and isolation.
Why a Principal Engineer needs this
1. It is the layer at which "whose bug is it" is decided. A rendering artifact could be Blink, the compositor, or the GPU process. A hanging page could be the renderer main thread, a blocked Mojo call, or the network service. You cannot triage across those without a process model, and triage across team boundaries is your job, not your team's.
2. Every web security guarantee you rely on is a process boundary or a check inside
one. "Same-origin policy protects us" is not a statement about JavaScript. It is a
statement about which process holds which data and which checks run in the privileged one.
Engineers who learned SOP as a JS-level rule consistently mis-reason about postMessage,
about SharedArrayBuffer, and about what an XSS actually gets an attacker.
3. The performance model is a process/thread model. "Move it off the main thread" is
meaningless until you can say which main thread, and what the target thread is allowed to
touch. Compositor-driven scroll, off-main-thread raster, and the reason
getBoundingClientRect is expensive are all consequences of this diagram.
4. It bounds what is even implementable. When a product asks for a Web API that needs privileged OS access, the design question is which part goes in Blink, which crosses Mojo, and what the browser process must independently verify. That is §18's design exercise, and it is a real interview and real design-review question at this level.
Mental Model
The processes
┌──────────────────────────────────────────────────────────────┐
│ BROWSER PROCESS privileged · trusts nobody │
│ UI, tabs, navigation, permissions, profile/disk, process │
│ allocation, the security decisions that actually matter │
└──────┬──────────────┬───────────────┬──────────────┬─────────┘
│ Mojo │ Mojo │ Mojo │ Mojo
┌──────▼──────┐ ┌─────▼───────┐ ┌─────▼──────┐ ┌─────▼────────┐
│ RENDERER A │ │ RENDERER B │ │ GPU │ │ NETWORK │
│ sandboxed │ │ sandboxed │ │ sandboxed │ │ SERVICE │
│ site a.com │ │ site b.com │ │ │ │ │
│ ├ Blink │ │ │ │ raster, │ │ sockets, │
│ ├ V8 │ │ │ │ GL/Vulkan, │ │ HTTP cache, │
│ └ cc(main) │ │ │ │ display │ │ cookies* │
└─────────────┘ └─────────────┘ └────────────┘ └──────────────┘
(* the authoritative cookie
store is browser-side)
The invariant that generates the whole design:
A renderer process is assumed to be compromised. Every decision it reports to the browser process is treated as attacker-controlled input.
Read that again, because almost every "why is this so complicated" question in Chromium bottoms out there. If a renderer could be trusted, half of Mojo's validation, all of site isolation, and most of the browser process would be unnecessary.
Who owns what
| Concern | Process | Why there |
|---|---|---|
| DOM, CSS, layout, paint, JS | Renderer | untrusted content; must be sandboxed |
| Compositing (main-thread half) | Renderer | needs layout output |
| Compositing (impl half), raster | Renderer + GPU | must survive a busy main thread |
| GL/Vulkan command execution | GPU | drivers are fragile and a large attack surface |
| Sockets, TLS, HTTP cache | Network service | shared across renderers; must not be per-origin-controlled |
| Cookie authority | Browser | a compromised renderer must not read another site's cookies |
| Permissions, navigation, URL bar | Browser | the user-trust surface |
| Disk / profile | Browser | sandboxed processes have no file access |
The pattern: anything a compromised renderer must not be able to do lives outside the renderer. That single rule predicts the location of most subsystems, and it is a far better navigation heuristic than memorising the list.
Threads inside a renderer
The process diagram is the half people know. This is the half that explains performance.
RENDERER PROCESS
├── Main thread ("renderer main")
│ Blink: parsing, DOM, style, layout, paint, JS (V8), rAF, most Web APIs
│ ← everything your app code perceives as "the browser"
├── Compositor thread ("impl")
│ input handling for scroll, layer property trees, tiling decisions,
│ produces frames from already-painted content
├── Raster / worker pool
│ turns display items into pixels; may hand work to the GPU process
├── IO thread
│ Mojo message send/receive. Never do work here.
└── Web Worker / Worklet threads
separate V8 isolates, no DOM
Two consequences that matter constantly:
- Compositor-driven scroll works while the main thread is blocked. That is not an
optimisation detail; it is the reason a janky page still scrolls. It also explains why
scroll becomes janky only when you add a non-passive
wheel/touchstartlistener — you have forced the compositor to consult a thread that is busy. requestAnimationFrameis main thread. So "use rAF for smoothness" does nothing for a main thread that is already saturated. This is the mechanical basis for the sibling track's M01 conclusions.
Sequences, not threads
Chromium's task abstraction is a sequence: an ordered series of tasks that never run concurrently with each other, but which need not be pinned to one physical thread. In-tree guidance is explicit that you should prefer sequences to physical threads.
Why this matters to a reader of the code: when you see scoped_refptr<base::SequencedTaskRunner>
you should read it as "this work is ordered relative to itself, but you may not assume
which thread." SequenceChecker in a class is the executable statement of that contract,
and — like DCHECK — it is documentation you can trust because it is enforced.
Blink adds its own rule on top: cross-thread communication is message passing, not shared
memory. renderer/README.md states that mutexes and atomics are "strongly discouraged,"
citing use-after-free security bugs and stability problems as the historical reason, and
notes that existing shared-memory patterns in the tree should not be cargo-culted. This is a
rare case where the codebase tells you directly that some of its own code is not exemplary
— worth remembering when you find a mutex and assume it is the house style.
Mojo, in one paragraph
Mojo is Chromium's IPC system. You declare an interface in a .mojom file; the build
generates both ends in C++ (and other languages). A message pipe carries messages
between them; endpoints can be passed through pipes, which is how capabilities are
delegated. The important property is not the syntax — it is that the receiving side must
validate everything, because on the browser side the sender may be a compromised
renderer. When you read a browser-process Mojo implementation and it looks paranoid, that
is the design working.
Site isolation, precisely
Each site (scheme + eTLD+1 — not origin, and not URL) gets its own renderer process, including cross-site iframes, which are rendered out-of-process and composited together.
Two follow-on facts that are constantly misremembered:
- Site ≠ origin.
https://a.example.comandhttps://b.example.comare the same site. Origin-keyed isolation is available but is not the default. - Site isolation is a defence against a compromised renderer, not only against script. Same-origin policy already stops script from reading cross-origin data. Site isolation is what stops a renderer whose sandbox has been escaped-into (or a Spectre-style side channel) from having that data in its address space at all. Spectre is precisely why "the data was never in that process" became a necessary guarantee, rather than "the checks stop it."
That last distinction is the single most common gap in otherwise-strong frontend engineers'
security models, and it is where COOP/COEP and SharedArrayBuffer gating come from.
Under the Hood: a navigation, end to end
Trace this once and the diagram stops being abstract. Roughly:
- Browser process receives the navigation intent (omnibox, link click forwarded from a renderer, redirect). The renderer asks; the browser decides — a renderer cannot navigate itself anywhere it likes.
- Browser consults the process model: which site is this, is there an existing suitable process, must a new one be created? A "spare" renderer often exists precisely to hide process-startup latency.
- The request goes to the network service, not the renderer. The renderer never holds the socket.
- Response headers come back. Security decisions — CSP, COOP/COEP,
X-Frame-Options, MIME sniffing, whether this response may even be delivered to this renderer (cross-origin read blocking) — are made where they cannot be lied about. - The body is streamed to the chosen renderer, which begins parsing (bi-03) on the main thread while the preload scanner speculatively requests subresources.
- Blink builds DOM → style → layout → paint (bi-04–bi-09), producing display items and property trees rather than pixels.
- The compositor turns that into layers/tiles; raster produces pixels, potentially in the GPU process.
- The GPU process composites the final frame and presents it at vsync.
Every arrow between numbered steps is a Mojo boundary or a thread hop, and each is a place where you can observe the system with tracing. That is the practical payoff: this list is a checklist for reading a trace.
Where to look, without being told
Apply bi-01's path-to-process table. //content/browser/ for step 1–2 and 4,
//services/network/ for 3, //third_party/blink/renderer/core/ for 5–6, //cc/ for 7,
//gpu/ + //components/viz/ for 8. In-tree docs worth reading in your own checkout:
docs/process_model_and_site_isolation.md, docs/mojo_and_services.md,
docs/threading_and_tasks.md, and docs/security/compromised-renderers.md — the last is
the clearest statement anywhere of what the browser assumes an attacker already controls.
Deep dive: the process-model vocabulary
Chromium's process-allocation logic is not ad hoc. It is built on four named abstractions, and
almost every confusing process-model question dissolves once you can name which one is in play.
These are quoted from docs/process_model_and_site_isolation.md in the tree.
| Concept | Class | What it answers |
|---|---|---|
| Security principal | SiteInfo | Which data is this execution context allowed to access? |
| Principal instance | SiteInstance | Which specific process should this document use? |
| Browsing context group | BrowsingInstance | Which documents can find each other by name? |
| Process lock | ProcessLock | Which sites is this process permitted to host? |
Why the principal is a site, not an origin
This is the fact everyone gets wrong. The principal is scheme + eTLD+1 because document.domain
lets same-site documents reach into each other synchronously. If a.example.com and
b.example.com could be in different processes while still able to script each other, you would
have cross-process synchronous DOM access — which is not a performance problem, it is a data race
on two heaps.
So the rule is not "origins are the security boundary, and Chromium is sloppy." The rule is:
Any two documents with the same principal in the same browsing context group MUST live in the same process, because they have synchronous access to each other's content — cross-document scripting and shared memory (
SharedArrayBuffer).
Put the other way round: process separation is only available where synchronous access is
already impossible. That is why origin-keyed isolation requires opting out of document.domain
(via Origin-Agent-Cluster), and why crossOriginIsolated (COOP+COEP) is a precondition for
SharedArrayBuffer — you must prove nothing cross-origin shares your agent cluster before you are
handed shared memory.
The agent cluster correspondence
SiteInstance corresponds roughly to the HTML spec's agent cluster, and the in-tree docs are
careful to say the match is not exact:
- multiple agent clusters may share one principal instance (e.g.
data:URLs stay with their creator), - principals track factors the spec's agent-cluster key does not (e.g. StoragePartition).
This gap is worth internalising as a general lesson. Implementations track more state than specs, because specs define observable behaviour while implementations also carry policy, storage partitioning, and product requirements. When implementation and spec vocabulary diverge, it is usually because the implementation is deciding something the spec left to the UA.
BrowsingInstance: why popups are special
A browsing context group is the set of tabs and frames that hold references to each other —
frames in a page, popups with window.opener, named targets. Any two documents in the group can
find each other by name, so same-principal documents in the group must share a process.
Consequences you can observe:
window.open()withoutnoopenerkeeps you in the same browsing context group, which constrains process allocation for the popup.rel="noopener"is therefore not merely a tidiness measure — it lets the browser put the new page in a different browsing context group, which unlocks a different process and severs the synchronous relationship.- A tab can change browsing context group on navigation: a
Cross-Origin-Opener-Policyheader, or a browser-initiated cross-site navigation, both do this. That is exactly what COOP is for.
If you have ever wondered why
rel="noopener"shows up in security and performance advice, this is why: it is the same mechanism seen from two sides.
ProcessLock: the enforcement, not the intention
A ProcessLock is attached to a RenderProcessHost (the browser-side object representing a
renderer) and restricts which sites may load there and which data the process may access. Locks
have varying granularity:
- a single site (
https://example.com), - a single origin (
https://accounts.example.com), - an entire scheme (
file://), - or a special "allow-any-site" value for processes permitted to host multiple sites.
Processes start unlocked and acquire a lock once content is loaded. chrome:// URLs are never
allowed to share a process with other sites, on every platform, regardless of isolation mode.
The distinction that matters: SiteInstance expresses intent ("this document belongs to this
principal instance"); ProcessLock is the enforcement point the browser consults when a renderer
asks for data. A compromised renderer can lie about who it is — the lock is what makes the lie
useless.
Deep dive: Mojo, precisely
From docs/mojo_and_services.md:
- A message pipe is a pair of endpoints. Writing at one endpoint enqueues on the peer. Pipes are bidirectional.
- A mojom file describes interfaces: strongly-typed collections of messages (roughly analogous to protobuf messages).
- Given an interface and a pipe, one endpoint is designated a
Remote(sends) and the other aReceiver(receives). Replies flow back fromReceivertoRemote. - A
Receivermust be bound to an implementation. A received message is dispatched as a scheduled task invoking the corresponding method.
That last point is the one with real consequences and it is easy to skim past:
Mojo message dispatch is a task, not a call. It is posted to a sequence and runs when that sequence gets to it.
So a Mojo "call" is asynchronous by default, ordering is guaranteed per pipe, and two messages on two different pipes have no ordering relationship at all. A large class of subtle Chromium bugs is exactly this: code that assumed two IPCs would arrive in the order they were sent, when they travelled on different pipes. (Associated interfaces exist precisely to share a pipe and recover ordering — that is what they are for.)
Capability passing
Endpoints can be sent through pipes. That is how privilege is delegated: instead of the renderer asking "may I do X?" and the browser answering, the browser hands the renderer an endpoint that is the ability to do X, scoped to what it was created for. This is a capability model, and it is why you should read a browser-side Mojo implementation as "what am I willing to hand out, and scoped to what?" rather than "what checks do I run?"
Deep dive: what the browser must never trust
docs/security/compromised-renderers.md is the single most useful security document in the tree
because it is organised as an inventory of things a compromised renderer must not be able to
obtain. Its section list is effectively a threat model:
Site Isolation foundations · Cross-Origin HTTP resources · Contents of cross-site frames
Cookies · Passwords · Security-sensitive UI (Omnibox) · Permissions · Web storage
Messaging · JavaScript code cache · Cross-Origin-Resource-Policy · frame-ancestors CSP
and X-Frame-Options · HTTP request headers · SameSite cookies · User gestures/activations
Web Accessible Resources of extensions · Non-Web resources · Android-specific gaps
Renderer processes hosting the DevTools frontend
Two entries deserve special attention because they surprise application engineers:
frame-ancestors CSP and X-Frame-Options are enforced in the browser process. They must be,
because the renderer being framed is the one that would otherwise decide, and it may be
compromised. This is why those headers are meaningfully stronger than a JavaScript framebusting
check — the check is on the other side of a trust boundary.
The JavaScript code cache is in the list. A shared compilation cache is a cross-origin information channel if you are not careful; it is partitioned for that reason. Caches are side channels by default, and every partitioned cache in the browser (HTTP cache, code cache, connection pools) is there because someone demonstrated the leak.
Cross-origin data: the read-blocking layer
Same-origin policy classically stopped a page from reading a cross-origin response. Site isolation raises the bar: the response should never reach the renderer's address space at all, because a compromised renderer can read its own memory regardless of what checks it is supposed to run. That is the job of cross-origin read blocking (and its successor work): decide, in the network service or browser, whether this response may even be delivered to this process.
This is the concrete architectural consequence of Spectre. Before speculative-execution attacks, "the renderer has the bytes but the checks stop it" was an acceptable design. After, it was not — because a side channel can read what the checks forbid. The move from check to never deliver is one of the largest architecture changes in the browser's history, and it is why site isolation shipped despite its memory cost.
Deep dive: the Rule of Two
docs/security/rule-of-2.md states a constraint worth carrying into your own systems. Code must
not do all three of the following at once:
- process untrustworthy inputs,
- in an unsafe language (C/C++),
- without a sandbox.
Pick at most two. This single rule explains a startling amount of Chromium's architecture:
- parsers for untrusted formats (images, fonts, audio) get moved into sandboxed utility processes — dropping requirement 3;
- new code is increasingly written in Rust or in memory-safe wrappers — dropping requirement 2;
- data that can be validated to a trusted schema before reaching unsafe code — dropping 1.
When you see a whole process that seems to exist for one small job, the Rule of Two is usually the reason.
Numbers worth carrying
These are order-of-magnitude anchors, not benchmarks — verify on your own hardware before quoting.
| Quantity | Rough scale | Why it matters |
|---|---|---|
| Renderer process baseline memory | tens of MB before your page | why process-per-site has a real cost |
| Process startup | milliseconds, not microseconds | why a spare renderer is kept warm |
| Mojo message | a task hop, not a function call | why chatty IPC designs fail |
| Frame budget at 60 Hz | 16.7 ms total across all stages | your script is one term |
| Frame budget at 120 Hz | 8.3 ms | raster costs do not halve |
The spare-renderer trick is worth knowing about specifically: the browser keeps an unused renderer warm so that a navigation does not pay process startup on the critical path. It is a latency optimisation that only makes sense once you know process creation is expensive — and it is a good example of the browser spending memory to buy responsiveness, the same trade you will be asked to make in application architecture.
Anti-Patterns
"The renderer" as a single thing. Renderer ≠ Blink ≠ main thread. Most confused performance conversations collapse these three.
Assuming SOP is enforced in JavaScript. It is enforced by a combination of Blink checks and the browser process refusing to deliver data. If your model has only the first, you cannot explain cross-origin read blocking or why Spectre changed the architecture.
Reasoning about cookies from the renderer's perspective. document.cookie is a
restricted view. The authority is browser-side, which is why HttpOnly is meaningful at
all.
Treating --single-process results as evidence. It collapses the boundaries this
module is about, is not maintained to production quality, and will happily "prove" things
that are false in the shipping configuration.
Believing a diagram (including this one) over a trace. Process and thread assignments
change between releases and platforms — Android differs from desktop in process limits and
reuse. thread backtrace all in lldb, or a Perfetto trace, tells you what is true today on
your machine.
Trade-offs
Process-per-site costs memory. Site isolation measurably increases RAM, which is why it shipped on desktop before mobile and why Android uses process-reuse heuristics. If you ever argue that a browser "should just isolate everything," you are arguing for a product that loses on low-end devices — which is most devices.
IPC costs latency and complexity. Every Mojo hop is serialisation, a thread hop, and validation code that must be written and reviewed. The alternative — trusting the renderer — is what the pre-2008 single-process browsers did, and its failure mode was "one bad page takes down the browser, and one exploit takes the machine."
Out-of-process iframes complicate everything. Input routing, focus, hit testing, compositing, and the accessibility tree all become cross-process problems. This is the clearest example in the codebase of a security requirement imposing large architectural cost, and it is the best §46 notebook entry in this module.
Lab — the process/thread lab, process and thread archaeology
See bi-02-architecture-process-model/docs/execution.md.
Debugging Exercise
On your local content_shell build:
- Launch it, then enumerate the actual processes and their types on your machine. Which process types appear for a trivial page? Which appear only after you load something that paints, or something cross-origin?
- Attach lldb to a renderer and run
thread backtrace all. Identify main, compositor, raster, and IO threads by name. Write the names down — you will grep for them in traces for the rest of the track. - Load a page with a cross-site iframe. Confirm from process inspection that a second renderer exists. Then confirm the same-site iframe case does not create one.
- Block the main thread with a long synchronous loop, and scroll. Explain what still works and why, in terms of threads rather than in terms of "the compositor is fast."
Testing & QA Considerations
- Find a
*_browsertest.ccthat exercises a cross-process behaviour. What can it assert that a unit test structurally cannot? - Find a Mojo interface with validation code in its browser-side implementation. Find the test that feeds it invalid input. That test is the compromised-renderer model expressed as code — the most concrete form of the invariant in this module.
- Determine how Chromium tests site isolation itself, and what a regression there would look like in CI.
Further Reading (primary sources first)
In-tree, read in your own checkout
docs/process_model_and_site_isolation.mddocs/security/compromised-renderers.md— read this one twicedocs/mojo_and_services.md,docs/mojo_ipc_conversion.mddocs/threading_and_tasks.md(+_faq.md) — sequences vs threadsthird_party/blink/renderer/README.md§"Threading model", §"Dependencies"content/README.mdand theDEPSfiles around//content/public
External
- Life of a Pixel — the standard rendering-pipeline orientation.
- The Chromium security team's site-isolation writeups (and the Spectre-era rationale).
- W3C/WHATWG: HTML origin and site definitions — confirm for yourself that "site" is scheme + eTLD+1, because everyone misquotes it.
Principal Engineer Review
-
A page hangs: the tab is unresponsive but the browser UI is fine, and the spinner keeps spinning. Enumerate the possible locations of the hang across processes and threads, and give the single cheapest discriminating observation for each.
-
Your company wants to embed third-party widgets. Compare
<iframe>, Shadow DOM, and a same-origin script include, purely in terms of process and trust boundaries. What does each actually protect against, and what does each definitely not? -
Explain to a senior engineer why an XSS on
a.example.comis not automatically a total compromise ofb.example.com, given both are the same site. Then explain the case where your reassurance is wrong. -
Site isolation costs significant memory. Construct the strongest argument for shipping a browser configuration without it, then rebut it. What device class makes this a genuine product decision rather than a security-vs-laziness one?
-
A Web API needs privileged OS access (say, a hardware sensor). Specify the split: what lives in Blink, what crosses Mojo, what the browser process must independently verify, and what it must not trust from the renderer. Name a concrete way a naive split leaks authority.
-
Chromium prefers sequences to physical threads, and Blink prefers message passing to shared memory. Both trade performance for safety. Under what conditions is that trade wrong, and how would you recognise you are in one of those conditions?
-
A teammate proposes moving an expensive computation "off the main thread" using a Web Worker. What must be true about the data for this to be a win? Give a realistic case where it is a loss, and quantify what dominates.
-
Out-of-process iframes made input routing, focus, hit testing, and accessibility substantially harder. Argue that this complexity is essential; then argue it is the price of a security model chosen for other reasons. Which do you actually believe, and what evidence would change your mind?
-
You are told a rendering bug reproduces only with GPU acceleration enabled. Describe how you would narrow it to Blink, the compositor, or the GPU process — and say which of your steps changes the security model and therefore cannot be used to draw final conclusions.
-
The browser process is a single point of failure and a single point of trust. Design a hypothetical Chromium in which it is decomposed further. What breaks, what improves, and why do you think the actual architecture has not gone that way?