01 — Browser Multi-Process Architecture
Overview
Modern browsers are among the most complex pieces of software running on consumer hardware. They execute untrusted code from arbitrary websites, render pixel-perfect graphics, play video, run cryptographic operations, and do all of this simultaneously across dozens of tabs — while keeping each tab isolated from every other. The multi-process architecture that underpins this capability is a product of painful lessons learned from the single-process era, a decade of incremental engineering, and a security crisis that arrived in 2018 in the form of Spectre.
Historical Context: The Single-Process Era
Before 2008, every major browser — Internet Explorer 6, Firefox 2, Opera 9 — ran as a single OS process. This meant a single address space, a single set of file descriptors, and a single kernel thread scheduler context for all tabs.
The consequences were severe and well-known to users:
- One tab hangs → all tabs hang. A JavaScript-heavy page or a misbehaving plugin would trigger the "spinning beach ball" or "not responding" state for the entire browser window.
- One tab crashes → all tabs crash. A segfault in the JPEG decoder, a buffer overflow in the Flash plugin, or a corrupted heap brought down every open page.
- No memory isolation. A tab could theoretically read the DOM, cookies, and stored credentials of any other tab in the same process.
- Plugin privilege. The Netscape Plugin API (NPAPI) gave Flash, Java, Acrobat, and QuickTime full process-level privileges. A compromise in any plugin was a full OS compromise.
The IE6 era is remembered not just for poor security but for the architectural impossibility of fixing that security without rebuilding the browser from scratch. Microsoft attempted incremental hardening but the monolithic model made meaningful isolation impossible.
The Chrome Revolution (2008)
Google Chrome launched in September 2008 alongside a comic book by Scott McCloud explaining its multi-process architecture. The key insight was borrowed from the OS world: use process boundaries the same way the kernel uses privilege rings — as the strongest available isolation primitive.
The original Chrome model assigned one renderer process per tab. If a tab crashed, only that renderer died. The browser UI process — which owned the address bar, tabs, and back/forward navigation — remained alive and could spawn a new renderer.
This was a profound user-experience improvement, but the deeper value was security. Renderer processes were sandboxed: they could not open files, could not make direct system calls, and communicated with the outside world only through narrow, audited IPC channels. A compromised renderer gained an attacker only the contents of that one tab, not the user's entire system.
The Chrome Process Model
┌──────────────────────────────────────────────────────────────────────┐
│ CHROME BROWSER │
│ │
│ ┌─────────────────────┐ ┌──────────────────────────────────────┐ │
│ │ Browser Process │ │ Renderer Processes │ │
│ │ │ │ │ │
│ │ ┌───────────────┐ │ │ ┌────────────┐ ┌────────────────┐ │ │
│ │ │ UI Thread │ │ │ │ Renderer A │ │ Renderer B │ │ │
│ │ │ (address bar,│ │◄──►│ │ (site A) │ │ (site B) │ │ │
│ │ │ tab strip) │ │ │ │ Blink+V8 │ │ Blink+V8 │ │ │
│ │ ├───────────────┤ │ │ └────────────┘ └────────────────┘ │ │
│ │ │ IO Thread │ │ │ │ │
│ │ │ (IPC pump) │ │ │ ┌────────────┐ ┌────────────────┐ │ │
│ │ └───────────────┘ │ │ │ Renderer C │ │ Renderer D │ │ │
│ └─────────────────────┘ │ │ (site C) │ │ (isolated │ │ │
│ │ └────────────┘ │ iframe) │ │ │
│ ┌─────────────────────┐ │ └────────────────┘ │ │
│ │ GPU Process │ └──────────────────────────────────────┘ │
│ │ (compositing, │ │
│ │ WebGL, video │ ┌──────────────────────────────────────┐ │
│ │ decode) │ │ Network Service Process │ │
│ └─────────────────────┘ │ (HTTP/2, QUIC, DNS, sockets, │ │
│ │ cache — isolated from renderers) │ │
│ ┌─────────────────────┐ └──────────────────────────────────────┘ │
│ │ Utility Processes │ │
│ │ (audio, printing, │ ┌──────────────────────────────────────┐ │
│ │ storage, etc.) │ │ Extension Processes │ │
│ └─────────────────────┘ │ (one per extension, sandboxed) │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
Browser Process
The browser process is the supervisor. It:
- Owns the native window, title bar, tab strip, address bar, and menus.
- Manages the lifecycle of all other processes (spawn, monitor, reap).
- Owns the persistent profile data: bookmarks, history, saved passwords.
- Runs the UI thread (main thread) and a pool of I/O threads.
The browser process itself is not sandboxed — it runs with the user's full privileges. This is unavoidable: it must be able to write bookmarks to disk, update the binary, and launch child processes. The design principle is to minimize the code surface area of the browser process and push everything else into sandboxed children.
Renderer Processes
Each renderer process hosts the Blink layout engine and the V8 JavaScript engine. A renderer is responsible for the full rendering pipeline: parsing HTML/CSS, executing JavaScript, laying out the document, painting, and communicating the resulting compositor frame back to the GPU process.
Renderers are sandboxed and cannot:
- Read or write arbitrary files.
- Open network sockets directly.
- Make system calls that haven't been explicitly allowlisted.
All requests to the outside world go through the browser process or the network service process via Mojo IPC.
GPU Process
The GPU process is responsible for all GPU work: compositing renderer layer trees into the final frame, accelerated 2D canvas, WebGL, and hardware video decoding. It is a single process shared by all renderer processes. Keeping GPU work in a separate process prevents a WebGL bug from crashing the browser and isolates the privileged GPU driver from the untrusted renderer.
Network Service Process
Originally the browser process handled all networking. After the Spectre disclosure in 2018, Chrome moved networking into its own sandboxed process. This prevents a compromised renderer from reading cached credentials or cookies directly from the browser process's memory.
The network service process manages HTTP/2 and HTTP/3 (QUIC) connections, the DNS cache, the HTTP cache on disk, and TLS termination.
Site Isolation (Post-Spectre, 2018)
Pre-2018 Chrome assigned one renderer per tab, not per site. This meant a tab navigating
between a.com and b.com would reuse the same renderer process. Cross-origin iframes within
a page also shared the renderer with the embedder.
Spectre and Meltdown changed this calculus permanently.
Spectre demonstrated that a JavaScript timing attack could read arbitrary memory within the
same process across a speculative execution side channel. If evil.com and bank.com shared
a renderer, the attacker could potentially read the bank's DOM, cookies in memory, and
autofilled passwords by running a Spectre gadget in a tight loop.
The fix — Site Isolation — ensures that each site gets its own renderer process. A site is
defined as scheme + eTLD+1 (effective top-level domain plus one label). So app.example.com
and cdn.example.com are the same site; example.com and attacker.com are not.
Before Site Isolation:
┌──────────────────────────────────────────┐
│ Renderer Process │
│ main frame: bank.com │
│ iframe: evil-tracker.com ← DANGER │
│ (shared address space!) │
└──────────────────────────────────────────┘
After Site Isolation:
┌──────────────────────────┐ ┌─────────────────────────┐
│ Renderer A │ │ Renderer B │
│ main frame: bank.com │ │ iframe: evil-tracker │
│ (isolated) │ │ (isolated) │
└──────────────────────────┘ └─────────────────────────┘
IPC via Mojo only — no shared memory
Site isolation carries a real cost: more renderer processes mean more RAM. Chromium's process count management subsystem caps the number of live renderers based on available RAM, potentially reusing processes for sites that haven't been recently visited.
Process Sandboxing
The sandbox is the second layer of defense after process isolation. Even if an attacker achieves Remote Code Execution (RCE) in a renderer, the sandbox constrains what they can do with that execution.
Linux — seccomp-BPF:
The renderer process uses seccomp (secure computing mode) with a BPF (Berkeley Packet Filter)
program to allowlist permitted system calls. The BPF program runs in the kernel on every syscall
attempt. Calls to open(), socket(), execve(), and hundreds of others are blocked. A
compromised renderer that tries to read /etc/passwd gets EPERM.
Windows — Job Objects + DACL: Chrome uses Windows Job Objects to restrict what the process can do. The sandbox configuration denies: access to the filesystem, registry writes, creation of named pipes, spawning new processes, and loading unsigned DLLs. Desktop and window station are also restricted to prevent keylogging via SetWindowsHookEx.
macOS — Sandbox Profiles:
Chrome uses macOS's Seatbelt sandbox (built on TrustedBSD MAC). The renderer process loads a
.sb sandbox profile that uses an allowlist of Mach services and file paths. The GPU process
has a less restrictive profile to allow GPU driver access.
Mojo IPC System
Mojo is Chromium's IPC (Inter-Process Communication) system, introduced to replace the older
IPC::Channel system. Mojo provides:
- Message pipes: Bidirectional channels between processes, each endpoint a handle.
- Interfaces: Generated from
.mojomIDL files — strongly typed method dispatch. - Remote/Receiver: The
mojo::Remote<T>is the caller-side handle;mojo::Receiver<T>is the implementation-side handle. - Data pipes: Efficient bulk data transfer (e.g., streaming a large response body).
- Shared buffers: Memory-mapped regions shared between processes for high-bandwidth data.
Renderer Process Browser Process
───────────────── ──────────────────────
mojo::Remote<NavigationHost> mojo::Receiver<NavigationHost>
│ │
│ Navigate(url, params) │
├─────────────────────────────►│
│ │ validate URL, check permissions
│ OnNavigationStarted(id) │ spawn network request
│◄─────────────────────────────┤
│ │
Mojom interfaces are audited carefully because they define the attack surface from a compromised renderer into the rest of the system.
Renderer Process Warmup
Starting a renderer process takes 50–200ms due to dynamic linking, sandbox setup, and V8 initialization. Chrome mitigates this with spare renderer processes: after startup Chrome pre-launches one empty renderer process. When the user navigates to a new page, the spare renderer is available immediately and a new spare is spawned in the background.
For predictive navigation (hovering a link) Chrome may speculatively start a renderer for the target site.
Production Examples and Debugging Notes
Viewing processes: In Chrome, Shift+Esc opens the built-in Task Manager. Each row
represents a process: renderer processes show the site they host, memory usage, and CPU usage.
chrome://process-internals provides even more detail including the process ID and isolation
status of each frame.
Debugging renderer crashes: When a renderer crashes, Chrome shows the "Aw, Snap!" page.
The crash is logged with a stack trace (if crash reporting is enabled) and can be captured via
chrome://crashes. On macOS, crash reports appear in ~/Library/Logs/DiagnosticReports/.
Process limit thrashing: On low-memory devices Chrome may reuse renderer processes for
multiple sites (breaking isolation). Watch for this in chrome://process-internals. The flag
--site-per-process forces full isolation regardless of RAM.
Security Implications
- Sandbox escape: The actual threat model Chrome defends against is a two-stage exploit: first RCE in the renderer (via a Blink/V8 bug), then a sandbox escape (via a Mojo interface bug or kernel syscall bug) to reach the browser process. Both stages must succeed for a full compromise.
- Mojo attack surface: Every Mojom interface reachable from a renderer is a potential sandbox escape vector. Chrome has a dedicated security team that reviews all interface changes.
- UXSS (Universal Cross-Site Scripting): Bugs in the browser process itself can sometimes inject script into arbitrary renderer origins, bypassing SOP entirely.
Performance Implications
- Memory overhead: Each renderer process has its own V8 heap, Blink state, and OS process overhead (~50–100MB baseline). With 20 tabs this is 1–2GB. Chrome aggressively discards renderer processes for backgrounded tabs to reclaim memory.
- IPC cost: Every cross-process call adds serialization and a kernel context switch. For latency-critical paths (e.g., user input) Chrome uses shared memory to pass compositor frames rather than serializing them through Mojo.
- GPU process bottleneck: A single GPU process handles all compositing. On systems with slow GPU drivers this can become a bottleneck. Chrome's Vulkan and Metal backends aim to reduce driver-induced stalls.
Failure Modes
| Failure | Symptom | Recovery |
|---|---|---|
| Renderer crash | "Aw, Snap!" page | Tab can be reloaded; others unaffected |
| GPU process crash | Blank/glitchy frame | Chrome restarts GPU process transparently |
| Browser process crash | Entire window closes | Session restore on next launch |
| Network service crash | Network requests fail briefly | Chrome restarts and retries |
| Sandbox escape + RCE | Silent full compromise | Requires both a renderer RCE and sandbox escape |
Modern Usage and Future Directions
WebContents in Electron: Electron embeds Chromium's multi-process model. Each BrowserWindow
gets its own renderer process. Electron applications that disable the sandbox (nodeIntegration:
true) give renderers full Node.js capabilities — effectively defeating the security model.
Chrome's future — Blink in Workers: Chrome is experimenting with moving more rendering work into Worker threads within renderer processes to reduce main-thread jank without adding more processes.
WebDriver BiDi: The new bidirectional Chrome DevTools Protocol is being designed to work correctly with the multi-process model, providing process-aware debugging.
Exercises
- Open
chrome://process-internalsin Chrome and identify how many renderer processes are running. Note which sites share a process (if any). Explain why they share. - Write a simple HTML page with a cross-origin iframe. Confirm in the Task Manager that the iframe is in a different process than the main frame.
- Research the
COOP(Cross-Origin Opener Policy) header. Explain how it interacts with the site isolation mechanism described here. - Read the Chromium source code for the
RenderProcessHostImpl::Initfunction and trace the sequence of syscalls made to create a sandboxed renderer process on Linux. - Design a minimal Mojom interface for passing a URL from a renderer to the browser for navigation. What security checks must the browser-side implementation perform?
References
- Barth, A. et al. "The Security Architecture of the Chromium Browser." Technical Report, 2008.
- Reis, C., Moshchuk, A., Oskov, N. "Site Isolation: Process Separation for Web Sites within the Browser." USENIX Security 2019.
- Kocher, P. et al. "Spectre Attacks: Exploiting Speculative Execution." IEEE S&P 2019.
- Chromium Design Documents:
https://www.chromium.org/developers/design-documents/ - Mojo Documentation:
https://chromium.googlesource.com/chromium/src/+/main/mojo/README.md - Chromium Multi-Process Architecture:
https://www.chromium.org/developers/design-documents/multi-process-architecture/