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.


0. Orientation

The 60-second version

One invariant generates the entire Chromium architecture:

A renderer process is assumed to be compromised. Every fact it reports to the browser process is treated as attacker-controlled input.

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.

From that one sentence follows a navigation heuristic better than memorising any list:

Anything a compromised renderer must not be able to do lives outside the renderer.

Cookies' authoritative store, permissions, navigation, the URL bar, disk access — all browser-side, for the same reason. Sockets and TLS are in the network service because they are shared and must not be per-origin-controlled. GL/Vulkan execution is in the GPU process because drivers are both fragile and a large attack surface.

The half that explains performance

The process diagram is the half people know. The thread diagram inside a renderer is the half that explains every performance conversation you will have:

RENDERER PROCESS
├── Main thread      parsing, DOM, style, layout, paint, JS — "the browser", as apps perceive it
├── Compositor       scroll input, property trees, tiling; produces frames from painted content
├── Raster pool      display items → pixels
├── IO thread        Mojo send/receive. Never do work here.
└── Workers          separate V8 isolates, no DOM

The compositor thread is why a page can keep scrolling while the main thread is frozen — and why that stops the moment anything needs the main thread.

What you should be able to do after this module

CapabilityEvidence you have it
Predict where a subsystem livesDerive it from the trust boundary, not from memory
Read a security decisionExplain what the browser must never accept from a renderer
Explain site isolation's costGive the memory and complexity price, and why it was paid anyway
Diagnose across processesKnow which process and thread a symptom must have originated in

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

ConcernProcessWhy there
DOM, CSS, layout, paint, JSRendereruntrusted content; must be sandboxed
Compositing (main-thread half)Rendererneeds layout output
Compositing (impl half), rasterRenderer + GPUmust survive a busy main thread
GL/Vulkan command executionGPUdrivers are fragile and a large attack surface
Sockets, TLS, HTTP cacheNetwork serviceshared across renderers; must not be per-origin-controlled
Cookie authorityBrowsera compromised renderer must not read another site's cookies
Permissions, navigation, URL barBrowserthe user-trust surface
Disk / profileBrowsersandboxed 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/touchstart listener — you have forced the compositor to consult a thread that is busy.
  • requestAnimationFrame is 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.com and https://b.example.com are 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:

  1. 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.
  2. 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.
  3. The request goes to the network service, not the renderer. The renderer never holds the socket.
  4. 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.
  5. The body is streamed to the chosen renderer, which begins parsing (bi-03) on the main thread while the preload scanner speculatively requests subresources.
  6. Blink builds DOM → style → layout → paint (bi-04–bi-09), producing display items and property trees rather than pixels.
  7. The compositor turns that into layers/tiles; raster produces pixels, potentially in the GPU process.
  8. 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.

ConceptClassWhat it answers
Security principalSiteInfoWhich data is this execution context allowed to access?
Principal instanceSiteInstanceWhich specific process should this document use?
Browsing context groupBrowsingInstanceWhich documents can find each other by name?
Process lockProcessLockWhich 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() without noopener keeps 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-Policy header, 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 a Receiver (receives). Replies flow back from Receiver to Remote.
  • A Receiver must 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:

  1. process untrustworthy inputs,
  2. in an unsafe language (C/C++),
  3. 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.

QuantityRough scaleWhy it matters
Renderer process baseline memorytens of MB before your pagewhy process-per-site has a real cost
Process startupmilliseconds, not microsecondswhy a spare renderer is kept warm
Mojo messagea task hop, not a function callwhy chatty IPC designs fail
Frame budget at 60 Hz16.7 ms total across all stagesyour script is one term
Frame budget at 120 Hz8.3 msraster 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:

  1. 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?
  2. 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.
  3. 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.
  4. 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.cc that 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.

Core terminology

TermDefinition
Browser processThe privileged one; owns UI, navigation, permissions, disk, process allocation
Renderer processSandboxed, untrusted; Blink + V8 + the main-thread half of cc
GPU processCommand execution; isolates fragile drivers and a large attack surface
Network serviceSockets, TLS, HTTP cache; shared, so not renderer-controlled
Utility processShort-lived, sandboxed helpers for parsing and decoding untrusted data
Site isolationOne renderer per site, so a compromise cannot reach another site's data
Site vs originSite is eTLD+1; origin is scheme+host+port. Isolation boundaries use both, differently
MojoThe IPC system: typed interfaces from .mojom, with generated validation
Message pipeThe bidirectional channel a Mojo interface is bound to
*Host / *ImplThe browser-side and renderer-side ends of one interface
Rule of TwoUntrusted input + no memory-safe language + no sandbox — pick at most two
SandboxThe OS-level restriction on what a process may do; renderers have no file access
Compositor (impl) threadHandles scroll and produces frames independently of the main thread
IO threadMojo send/receive only; doing work there stalls all IPC
IsolateA V8 instance; workers get their own, with no DOM
Frame tree / RenderFrameHostThe browser-side representation of a frame; the renderer's counterpart is untrusted

Mental models

Assume the renderer is compromised, and the architecture becomes derivable. Do not memorise which subsystem lives where — ask what an attacker with full control of the renderer could do if it lived there. That predicts the answer for cookies, permissions, navigation, file access, and the URL bar.

Validation is not politeness; it is the security boundary. Mojo generates validation because a compromised renderer sends whatever it likes. Every browser-side handler that trusts a renderer-supplied origin, URL, or frame id is a potential sandbox escape — which is why "the renderer already checked" is never an acceptable argument in review.

The Rule of Two is a design constraint you can apply outside Chromium. Untrusted input, unsafe language, no sandbox — choose at most two. It is a compact way to argue about any component processing hostile data, and it explains why parsing moved into utility processes.

Site isolation is expensive and was worth it. More processes means more memory and more IPC. It was adopted because Spectre made in-process boundaries unenforceable — the same security decision that coarsened performance.now() and gated SharedArrayBuffer behind cross-origin isolation (fe-01). One mitigation, visible in three unrelated-looking places.

Threads explain performance; processes explain security. Conflating them produces confused diagnosis. "Why is this slow?" is a thread question. "Why is this so indirect?" is a process question.

The compositor's independence is conditional. Scroll stays smooth on a busy main thread until something forces it back: a non-passive listener, a main-thread-scrolling reason, or content that has not been rastered. Knowing which of those it was is the whole diagnosis.


Common misconceptions

  1. "The renderer and the browser are just two halves of one program." They are a trust boundary. The browser treats every renderer message as hostile input.

  2. "Site isolation is one process per tab." It is one renderer per site, and a single tab with cross-site iframes spans several. That is also why memory grew when it shipped.

  3. "Mojo is just message passing." It is typed interfaces with generated validation, and the validation is the security-critical part.

  4. "Cookies live in the network service." The network service handles them on the wire; the authoritative store is browser-side, because a compromised renderer must not read another site's cookies.

  5. "The GPU process is for performance." It is substantially for isolation — drivers are fragile and a large attack surface, and a driver crash should not take the browser with it.

  6. "Work on the IO thread is fine if it's small." The IO thread is Mojo's send/receive path. Work there stalls IPC for everything.

  7. "Scrolling is always off the main thread." It is until something forces it back. Naming that something is the diagnosis, not "scroll is janky."

  8. "More processes is obviously worse." It is worse for memory and better for security and crash isolation, and after Spectre the second consideration stopped being optional.


Interview talking points

  • "One sentence generates the whole Chromium architecture: a renderer process is assumed to be compromised. Every 'why is this so indirect' question bottoms out there — if renderers could be trusted, most of the browser process and all of Mojo's validation would be unnecessary."
  • "That gives a navigation heuristic better than memorising subsystem locations: anything a compromised renderer must not be able to do lives outside the renderer. Cookie authority, permissions, navigation, disk — all browser-side, for one reason."
  • "The Rule of Two is the most portable idea in this module. Untrusted input, a memory-unsafe language, no sandbox — pick at most two. I use it to argue about any component that parses hostile data, not just browser code."
  • "Site isolation cost real memory and real complexity, and it was adopted because Spectre made in-process boundaries unenforceable. That's the same decision that coarsened performance.now() and put SharedArrayBuffer behind cross-origin isolation — one mitigation showing up in three places that look unrelated."
  • "Processes explain security; threads explain performance. Conflating them produces confused diagnosis. And the compositor's independence from the main thread is conditional — the useful question is never 'why is scroll janky' but 'what forced it back onto the main thread'."

Further Reading (primary sources first)

In-tree, read in your own checkout

  • docs/process_model_and_site_isolation.md
  • docs/security/compromised-renderers.md — read this one twice
  • docs/mojo_and_services.md, docs/mojo_ipc_conversion.md
  • docs/threading_and_tasks.md (+ _faq.md) — sequences vs threads
  • third_party/blink/renderer/README.md §"Threading model", §"Dependencies"
  • content/README.md and the DEPS files 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

  1. 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.

  2. 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?

  3. Explain to a senior engineer why an XSS on a.example.com is not automatically a total compromise of b.example.com, given both are the same site. Then explain the case where your reassurance is wrong.

  4. 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?

  5. 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.

  6. 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?

  7. 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.

  8. 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?

  9. 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.

  10. 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?


Connections to other modules

  • bi-01 — the paths that encode this process model; read the two together.
  • bi-05 — the bindings layer sits exactly on the trust boundary this module defines.
  • bi-06 — CHECK vs DCHECK is a security decision here: a CHECK survives in release because a violated invariant on the browser side may be an attack.
  • bi-10 — the compositor's independence, and what forces scroll back to the main thread.
  • bi-11 — the scheduler running inside the renderer this module maps.
  • bi-12 — cross-process tracing and reading a renderer's threads.
  • fe-01 — the same Spectre mitigations from outside: coarsened timers, COOP+COEP for SharedArrayBuffer, and why the compositor keeps animating while the page is dead.
  • fw-12 — why automation protocols inject input at the browser level rather than dispatching from script.