/>

101 Chrome Exploitation — Part 1: Architecture

Назад

Modern web browsers have evolved from simple document viewers into sophisticated platforms capable of running complex web applications. At the heart of this transformation lies a carefully orchestrated architecture built around three fundamental components: the rendering engine that transforms markup into visual content, the JavaScript engine that executes dynamic code, and the network stack that manages all web communication.

This chapter explores how these core components work together within Chrome/Chromium's architecture, which has become the industry standard influencing all major browsers. While modern browsers employ multi-process architectures for security and stability (which we'll explore in detail in the next chapter), understanding these fundamental building blocks is essential for grasping how browsers transform HTML, CSS, Images and others into interactive pixels.

Overview and Process Model

Chromium hardens the web platform with a multi-process architecture: privilege separation, sandboxing, and scoped IPC interfaces bound by Mojo. The typical desktop configuration consists of:

  • Browser process (UI, navigation, policy, storage partitioning): full privileges, no untrusted web content.
  • Renderer processes (Blink + V8): sandboxed, untrusted content execution per site instance; can host out-of-process iframes (OOPIFs) for cross-site subtrees.
  • GPU process: privileged access to OS graphics APIs; executes command buffer streams from untrusted clients.
  • Network Service: a service wrapper around //net; runs inside the browser process (in-process) on Android by default or in a dedicated utility process (out-of-process) on desktop by default; provides network::mojom::* Mojo APIs to other processes.

Chromium's architecture is built on several key principles that ensure security, performance, and reliability. Site Isolation and Out-of-Process Iframes (OOPIFs) form the foundation of cross-site security by separating documents from different sites into distinct renderer processes, with the browser process acting as a mediator for cross-process interactions including input handling, hit testing, focus management, and navigation. All inter-process communication relies on Mojo, Chromium's IPC framework, which enables renderers to communicate with privileged services through carefully designed narrow interfaces such as URLLoaderFactory for network requests, Storage APIs for data persistence, and Clipboard access for system integration.

The rendering pipeline is designed for optimal performance through careful separation of concerns and parallelization. HTML parsing, CSS processing, and JavaScript execution occur on the renderer's main thread, feeding into a sophisticated compositing pipeline that can continue handling animations and scrolling operations independently, even when JavaScript execution might block the main thread. Security is enforced through strict sandboxing mechanisms that are tailored to each platform: Windows implementations use AppContainer isolation combined with Win32k lockdown to prevent direct system API access, while Android deployments rely on process sandboxes reinforced by SELinux policies. Across all platforms, renderer processes are deliberately restricted from direct filesystem or network access, forcing all such operations to go through controlled, privileged service interfaces.

Rendering Engine (Blink)

Blink serves as Chromium's rendering engine, originally forked from WebKit in 2013 and subsequently redesigned through the RenderingNG initiative. As stated in Chromium's documentation, "Blink is a rendering engine of the web platform" that implements everything rendering content inside a browser tab, from implementing web platform specifications to embedding V8 for JavaScript execution. A high-level view of Blink's path from markup to pixels:

  • Parsing and trees
    • - HTML parser builds the DOM tree while the CSS parser builds CSSOM. Incremental parsing and speculative preloaders reduce latency.
    • - The style engine computes ComputedStyle for each element, applying cascade, inheritance, and the style invalidation system.
  • Layout and fragmentation
    • - Blink converts styled DOM into layout objects; modern layout NG performs block/inline/fragmentation per spec. Flow is fragmented across columns/pages when needed.
  • Paint and display lists
    • - The paint phase records DisplayItems into a paint/display list (Skia-backed), not pixels. This is a key security and performance seam: data remains declarative until raster.
  • Layerization and compositing
    • - Some DOM regions become composited layers based on heuristics and CSS triggers (transforms, opacity animations, video, WebGL). Layers form a tree parallel to DOM/layout.
    • - Property trees (transform, clip, effect) drive compositing math. Blink hands a layer tree and display lists to the compositor (cc) which produces frames.
  • Raster and display
    • - Skia raster workers (CPU or GPU) tile and raster content. The compositor submits a CompositorFrame to Viz (the out-of-process display compositor), which draws to the screen or to platform surfaces.

Web APIs and Codecs (images, fonts, audio/video)

Blink implements web-exposed APIs in third_party/blink/renderer/core/ (for standards like DOM, HTML, CSSOM) and third_party/blink/renderer/modules/ (feature-gated specs such as WebCodecs, WebAudio, WebRTC, WebGPU). Bindings are generated to V8 via the Blink bindings layer so JavaScript can call into engine implementations. A few relevant subsystems:

  • Image decoding: Blink's image decoders live under platform/image-decoders/ and feed decoded pixels or frames into the paint pipeline. Decoders select by MIME/type sniff and integrate with Skia surfaces and the loader/fetch stack. Decoding may be incremental for progressive formats and is driven on demand by layout/paint.
  • Fonts and text: The text stack in platform/fonts/ handles font selection, shaping, and painting. It resolves CSS font-family via web fonts and system fonts, uses HarfBuzz for shaping, caches word shapes, and performs system fallback when glyphs are missing.
  • Encodings: The Encoding Standard API bridges to WTF::TextCodec and ICU for character encoding/decoding.
  • Media codecs: Web-facing APIs like WebCodecs (modules/webcodecs/) provide low-level encode/decode access but delegate actual codec work to Chromium's media stack (//media), including media::DecoderFactory, VideoEncodeAccelerator, and VideoFrame. Higher-level media playback (HTMLMediaElement) and WebAudio use the same underlying media primitives.

Notes for researchers:

  • Web APIs are split between core/ and modules/; modules often depend on runtime-enabled features and may be origin-trial or behind flags. The platform/loader/fetch/ code integrates Fetch with the Network Service while keeping Blink responsive during page freeze via specific loader components.
  • Parsing/decoding tends to be pull-driven by layout/paint or the consuming API; heavy decode work is off-thread where possible, but the main thread orchestrates API entry points and DOM integration.

Memory Management Architecture

Blink relies on two complementary memory allocators. PartitionAlloc serves as Chromium's hardened general-purpose allocator used broadly across processes. It employs partitioned heaps and bucketized slot spans to reduce cross-type corruption and large overflows. Optional anti-UAF features, such as BackupRefPtr and pointer scanning/quarantine where enabled, make dangling-pointer exploitation significantly harder. The system includes a thread cache for small objects, keeps metadata out-of-line to avoid in-band overwrite, and enforces strict invariants that are checked in debug and sanitized builds.

The second system is Oilpan (Blink GC), which functions as Blink's tracing garbage collector for engine/DOM objects allocated as GarbageCollected<T>. It uses typed handles (Member, WeakMember, Persistent) instead of raw pointers and integrates with V8 via wrapper tracing to ensure DOM wrappers and JS objects stay alive together. Oilpan features incremental and concurrent marking and sweeping to minimize main-thread pauses, implements per-thread heaps with strict cross-thread ownership rules to reduce UAF risk, and encourages message passing rather than shared mutable state, requiring special handles for cross-thread references.

Threading Model and Task Scheduling

Blink operates primarily on a single main thread where almost all important operations occur. According to the Blink documentation, "All JavaScript (except workers), DOM, CSS, style and layout calculations run on the main thread. Blink is highly optimized to maximize the performance of the main thread, assuming the mostly single-threaded architecture.". We can list the main tasks and your responsabilities as:

  • Main thread: DOM, style, layout, paint list recording, JS execution.
  • Compositor (impl) thread: input routing, animation, tile prioritization, draws; can continue scrolling/animating while main thread is busy.
  • Raster worker threads: perform rasterization and image decode (often via Skia) off main thread.
  • Blink Scheduler: Frame/Page/Worker schedulers prioritizes input, rAF, layout, and internal work; tasks are attributed to frames and task types (e.g., input vs default) to enable throttling/freezing.
Rendering pipeline. Source: https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGA_WF53k96imRH8Mp34Y/

Side-note (Gecko/WebRender). Gecko uses a Rust-based pipeline: Stylo (Servo's style system) and WebRender, a GPU-first renderer that consumes a retained display list and re-rasterizes via a dedicated GPU scene graph. This front-loads display list building and may simplify impl-thread vs main-thread roles compared to cc/Viz.

Side-note (WebKit/WebCore). WebKit's WebCore pipeline maps similarly (DOM -> render tree -> layers). The compositor is integrated with Core Animation on Apple platforms. Terminology differs (RenderObject/RenderLayer), but the stages are comparable.

JavaScript Engine (V8)

V8 is embedded inside the renderer process. Blink exposes DOM and Web APIs via auto-generated bindings to V8 objects, with careful world isolation (main world vs isolated worlds for extensions) and lifetime policies. V8 runs JS and WebAssembly in Isolates; each renderer main thread and each worker has its own v8::Isolate. Understanding V8's architecture requires grasping three fundamental concepts: Isolates, Contexts, and Worlds.

Isolate represents an isolated instance of the V8 engine that maps to a physical thread in a 1:1 relationship. Each isolate contains its own heap and garbage collector, ensuring complete separation between different JavaScript execution environments. The main thread has one isolate, and each worker thread maintains its own separate isolate, preventing interference between concurrent JavaScript operations.

Context corresponds to a global object and provides the fundamental unit for JavaScript environment isolation. Each frame's window object exists as a separate context, which enables strict isolation between different JavaScript environments running within the same process. This separation is critical for maintaining security boundaries, ensuring that code from different origins cannot directly access each other's global variables and objects.

World is a concept specifically designed to support Chrome extension content scripts by providing an additional layer of isolation beyond contexts. The main world hosts web page JavaScript, while isolated worlds are created for each content script that needs to run on the page. Although these worlds share DOM access, allowing content scripts to interact with page elements, they maintain completely separate JavaScript heaps, preventing conflicts between extension code and web page scripts.

Compilation Pipeline

V8's compilation strategy balances startup performance with peak execution speed through a sophisticated multi-tier approach. The process begins with parsing and AST generation, where the scanner performs lexical analysis with sophisticated lookahead capabilities. The parser then builds Abstract Syntax Trees with several key optimizations: lazy parsing for unused functions to reduce initial overhead, parallel parsing on background threads to improve throughput, scope analysis for variable resolution, and early error detection to catch issues before execution.

The second phase involves bytecode generation with Ignition, which generates compact bytecode that executes in a register-based virtual machine. This bytecode collects type feedback through inline caches, supports debugging and profiling capabilities, and enables tier-up decisions for further optimization. The bytecode design follows several key principles: maintaining a compact representation that is often smaller than the original source code, enabling fast interpretation with computed gotos, providing rich type feedback collection, and ensuring direct mapping to JavaScript semantics.

V8 employs multiple optimization tiers to maximize performance. Maglev serves as the mid-tier compiler, targeting "warm" functions that have been invoked some few times. It operates as a single-pass compiler designed for fast compilation speed and uses type feedback from Ignition to generate decent code quickly. Maglev's optimizations include inline caching for property access, simple function inlining, basic arithmetic optimizations, and type-guided devirtualization.

For the most performance-critical code, TurboFan acts as the optimizing compiler, targeting "hot" functions that have been invoked 1000+ times. It uses a sea-of-nodes intermediate representation and implements a sophisticated optimization pipeline to generate highly optimized machine code. TurboFan's advanced optimizations include escape analysis and scalar replacement to eliminate unnecessary allocations, loop-invariant code motion to improve loop performance, global value numbering for redundancy elimination, and speculative optimizations with deoptimization fallbacks to handle dynamic type changes.

Memory Architecture

V8's heap is organized into several distinct spaces, each optimized for different types of objects and usage patterns. The New Space (Young Generation) employs a semi-space design that enables fast allocation for short-lived objects. This space uses a scavenger collector with parallel evacuation capabilities and typically maintains a size between 1-8MB. The design is based on the generational hypothesis that most objects die young, making this space highly efficient for temporary allocations.

The Old Space (Old Generation) handles long-lived objects that have survived multiple garbage collection cycles. It utilizes a mark-compact collector with incremental and concurrent marking capabilities to minimize collection pauses. This space uses page-based allocation strategies and is designed to efficiently manage objects that persist throughout the application's lifetime.

Source: v8.dev
Source: v8.dev

For exceptionally large objects that exceed the standard page size, V8 maintains a separate Large Object Space. Objects in this space, such as large arrays and strings, are never moved during garbage collection cycles and receive special handling to optimize memory usage and access patterns.

The Code Space represents a specialized memory region dedicated to executable memory for compiled JavaScript and WebAssembly code. This space operates under special security permissions and is managed separately from regular object spaces to maintain the security boundaries required for executable code.

V8 Heap Sandbox

Additional optimizations include pointer compression on 64-bit systems, which shrinks pointers to 32 bits within a 4GB isolate heap window, improving cache locality and reducing potential attack surfaces. This optimization became after some work in the V8 Sandbox (Heap Sandbox), which further enhances security by isolating internal pointers behind a software sandbox with bounds checks and metadata, effectively constraining the exploitation impact of memory corruption vulnerabilities within V8.

V8's in-process heap sandbox confines V8 code and data to a reserved address space region (the "sandbox") and replaces raw pointers with sandboxed pointers or table indices. It is software-only (with optional future hardware assist) and is enabled by default on supported 64-bit configurations.

The threat model assumes an attacker can arbitrarily and concurrently modify any memory within the sandbox, with the goal being to prevent corruption of memory outside the sandbox. While reads outside may still be observable through side channels, writes outside are treated as sandbox violations. The address space consists of a large virtual region reserved for V8, where pointers are either offsets from the sandbox base (caged/sandboxed pointers) or indices into indirection tables.

The system employs several pointer kinds and indirection mechanisms. Sandboxed (or compressed) pointers are offsets relative to a cage base with bounds masking and checks on dereference, guaranteed to point inside the sandbox. The external pointer table protects references to non-V8 ("external") objects such as embedder resources and ArrayBuffer backings via index-to-host address mapping with tagging and clearing on garbage collection and teardown. A code pointer table provides similar table indirection for executable code pointers and JIT metadata to prevent direct corruption of control-flow targets. Additionally, a trusted space allows select V8 objects like bytecode containers and JIT metadata to live outside the sandbox in a "trusted" heap reachable only via validated indirection.

Side-note (SpiderMonkey). SpiderMonkey's tiers: Interpreter, Baseline JIT, and IonMonkey (optimizing). The newer Warp pipeline streamlines compilation with improved type information and caching. For Wasm, SpiderMonkey uses a baseline compiler (Cranelift) and an optimizing compiler (Ion/Baseline Wasm backends). Memory model and GC differ but goals (fast warm-up, strong steady-state) align.

Side-note (JavaScriptCore). JSC tiers: LLInt (interpreter), Baseline JIT, DFG JIT (SSA graph), and FTL (using the B3/AIR backend). JSC employs aggressive W^X enforcement and on Apple platforms integrates with platform code-signing and memory protections.

Network Stack (Life of a URL request)

Chromium's network stack lives in //net and is exposed to other processes through the Network Service (//services/network). The service wraps //net and provides Mojo APIs in network::mojom (notably URLLoaderFactory and URLLoader). The browser launches the service and decides placement: out-of-process by default for isolation and stability, or in-process on Android by default; in both cases the service runs on a dedicated thread. NetworkService and NetworkContext are trusted and not exposed to renderers; less-privileged code receives remotes to URLLoaderFactory.

At construction time, NetworkService creates one or more NetworkContexts. Each NetworkContext owns an independent net::URLRequestContext (cookies, cache, socket pools, etc.). Chrome keeps distinct contexts for system use, per-profile (including Incognito), and sometimes per-app. Within //net, URLRequestContext is the top-level object used to create net::URLRequests and references subsystems like the cache, cookie store, and host resolver. Transport concerns live under HttpNetworkSession, which owns the HttpStreamFactory, socket pools, and HTTP/2/QUIC session pools.

Life of a basic HTTP request

  1. Request setup (privilege separation)
    • - In the browser, a NetworkContext creates a network::mojom::URLLoaderFactory. A consumer (renderer, navigation code, or helper like network::SimpleURLLoader) sends a network::ResourceRequest and a network::mojom::URLLoaderClient pipe to the factory, which creates a network::URLLoader.
    • - Privacy/security parameters are carried by net::IsolationInfo: the Network Isolation Key (NIK) partitions caches and sockets; SiteForCookies and redirect policies govern SameSite cookie attachment.
  2. Create the net::URLRequest
    • - network::URLLoader uses its URLRequestContext to create and start a net::URLRequest. The ResourceScheduler may delay dispatch based on priority and tab activity.
  3. Cache and transaction selection
    • - URLRequest asks URLRequestJobFactory for a URLRequestHttpJob, which attaches cookies as allowed by SiteForCookies and the request initiator.
    • - The job asks HttpCache for an HttpCache::Transaction. The cache looks up an entry by URL and NIK; on miss it wraps an HttpNetworkTransaction transparently.
  4. Connect and obtain an HTTP stream
    • - HttpNetworkTransaction requests an HttpStream from HttpStreamFactory. A Job acquires a ClientSocketHandle through the ClientSocketPoolManager, which selects the right pool (direct, SSL, proxy) and kicks off DNS and connect via HostResolverImpl if needed.
    • - For TLS, SSLClientSocketPool layers on top of the transport pool and negotiates ALPN; HTTP/2 may be negotiated during the TLS handshake. QUIC may be raced alongside TCP for origins known to support it, and successful sessions are pooled per origin.
  5. Send request, receive headers
    • - HttpNetworkTransaction passes headers to the HttpStream (HttpBasicStream, SpdyHttpStream, or QUIC stream), which formats and sends the request. Response headers flow up through the cache and job to URLRequest, then to network::URLLoader, and across Mojo to the URLLoaderClient.
  6. Stream the body
    • - network::URLLoader creates a raw Mojo data pipe, gives the read end to the client, and repeatedly writes chunks (typically 64KB) with backpressure. Content-Encodings are decoded by a pull-based source-stream chain (gzip/deflate, brotli) at the URLRequestJob layer; the cache stores bytes pre-decompression.
  7. Completion, redirects, cancellation
    • - On completion the loader notifies the client and tears down Mojo endpoints. On redirects, the client is informed and may choose to follow; a new URLRequestJob is created for the new URL if continued.
    • - On cancel or teardown, the transaction decides socket reuse; if possible, remaining body bytes may be drained before returning the socket to the appropriate pool.

This explanation might seem rushed, but this stack is very complex and would require an entire post just to explain these details. I strongly recommend that you read the official documentation on the "Life of a URLRequest": https://chromium.googlesource.com/chromium/src/+/HEAD/net/docs/life-of-a-url-request.md

What the Network Service does (and does not) include

The Network Service is intentionally narrowly scoped to the lowest layers of fetching and transport. For new researchers, two mental models help: first, think "service boundary first" where untrusted clients cannot set high-risk fields, as the browser configures NetworkContext and URLLoaderFactoryParams with trusted defaults (e.g., NIK, cookie policies), then hands out only a factory pipe, with all responses crossing back via Mojo after service-side policy checks; second, think "two major layers" where above the service, navigation/renderer code drives Fetch and policy, while inside the service, URLRequest plus cache/transactions and socket/session pools perform the actual I/O, and understanding which concerns live where makes debugging and design changes tractable. Its responsibilities include:

  • Implementing client HTTP semantics and transport: URL loading over HTTP/HTTPS, HTTP caching, cookies, authentication, redirects, content encodings, and streaming response bodies over Mojo data pipes.
  • Managing connection lifecycle and reuse: host resolution, TCP/TLS connections, socket pool limits and prioritization, and session pools for multiplexed protocols; deciding when to drain or reuse connections on completion/cancel.
  • Enforcing privacy and security policies at the service boundary: honoring net::IsolationInfo (Network Isolation Key and SiteForCookies) for partitioned caches/sockets and SameSite cookie attachment; applying cross-origin protections required to return bytes to less-privileged clients (e.g., CORS handling, CORB/ORB filtering) before data is exposed to renderers.
  • Providing stable, versioned Mojo interfaces (network::mojom::*) to privileged browser code and unprivileged clients via URLLoaderFactory, with careful accounting of thread hops and IPC to minimize latency.

I believe it's crucial to explicitly state that the network service is highly privileged, also to mitigate the risk of memory corruptions resulting in UXSS, which would make the criticality and impact of an RCE within the renderer process much greater.

Equally important is what lives outside of the service. Higher-level browser features and web platform integrations are implemented in other components and call into the service rather than being embedded in it:

  • Browser features like Safe Browsing, extensions, and DevTools operate above the Network Service. Hooks exist only where strictly necessary (e.g., traffic shaping for DevTools) to avoid coupling and to keep the core networking path lean.
  • Web platform orchestration (e.g., the Fetch algorithm, Service Worker interception, storage partitioning policy) is driven by content/browser layers. These decide which NetworkContext/URLLoaderFactory to use, set trusted parameters (like IsolationInfo), and consume results via Mojo callbacks.
  • Non-network URL schemes (file:, blob:, data:, chrome:) are handled by other factories outside //services/network; only networked schemes (http:, https:, ws:, wss:) flow through the service.

Notes

Cross-browser architecture varies, but there are recurring themes. Gecko (Firefox) implements multi-process isolation ("Fission") analogous to Chromium's Site Isolation and renders via WebRender's GPU scene graph; Stylo merges Servo's Rust style system into Gecko, and SpiderMonkey's tiers (Baseline/Ion with Warp) differ from V8's pipeline. WebKit (Safari) follows a similar DOM => render tree => layers pipeline, using JavaScriptCore (LLInt/Baseline/DFG/FTL) and Core Animation/Metal for compositing. A security-relevant difference is that Chromium centralizes display in the Viz service and brokers GPU access through a command buffer process, while Firefox integrates WebRender closely with Gecko threads and Safari leans on platform frameworks. Despite naming and IPC differences (Mojo vs IPDL), the goals around OOPIF/Fission and cross-origin mitigations are broadly aligned.

Platform constraints shape hardening strategies. On Windows, renderer processes run in AppContainer with Win32k lockdown and additional mitigations (ACG/CIG/CFG/CET). On Android, renderers are confined with seccomp-bpf and SELinux; some services like the Network Service may run in-process for memory/performance trade-offs, while respecting the same privilege boundaries. JIT availability can vary by device policy and channel, and on iOS, third-party browsers must use WebKit for rendering and JS, so security posture follows WebKit and platform rules even if other Chromium subsystems are present.

Hands-on Exercises

Here is a small, realistic exercise that walks through adding a tiny web-exposed API to Blink. The goal is to practice the Blink bindings workflow without touching privileged code. We will add navigator.exampleEcho(input) that simply returns the string you pass in, guarded by a runtime feature flag so you can enable or disable it from the command line. After the walkthrough, you'll find a short proposal for a more advanced follow-up exercise on implementing a toy image codec.

Create a new module folder and add these files. The IDL extends Navigator via a partial interface; the C++ supplement implements the method and is garbage-collected per Blink conventions. The feature is gated behind a runtime flag named ExampleAPI.

third_party/blink/renderer/modules/example/navigator_example.idl

// Exposed on Navigator; guarded by a runtime flag so it ships disabled by default.
[ImplementedAs=NavigatorExample, RuntimeEnabled=ExampleAPI]
partial interface Navigator {
  // Trivial demo: returns the same string that was passed in.
  [CallWith=ScriptState] DOMString exampleEcho(DOMString input);
};

third_party/blink/renderer/modules/example/navigator_example.h

// Minimal Navigator supplement used by the bindings generated from the IDL.
#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_EXAMPLE_NAVIGATOR_EXAMPLE_H_
#define THIRD_PARTY_BLINK_RENDERER_MODULES_EXAMPLE_NAVIGATOR_EXAMPLE_H_

#include "third_party/blink/renderer/bindings/core/v8/v8_typedefs.h"
#include "third_party/blink/renderer/core/frame/navigator.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/supplementable.h"

namespace blink {

class ScriptState;

// A per-Navigator attachable object (Supplement) that implements exampleEcho.
class NavigatorExample final : public GarbageCollected<NavigatorExample>,
                               public Supplement<Navigator> {
 public:
  static const char kSupplementName[];
  // Fetches or creates the supplement attached to a given Navigator.
  static NavigatorExample& From(Navigator&);

  explicit NavigatorExample(Navigator&);
  ~NavigatorExample() override = default;

  // Static entrypoint called by generated bindings for Navigator.exampleEcho().
  static String exampleEcho(ScriptState*, Navigator&, const String& input);

  void Trace(Visitor* visitor) const override { Supplement<Navigator>::Trace(visitor); }
};

}  // namespace blink

#endif  // THIRD_PARTY_BLINK_RENDERER_MODULES_EXAMPLE_NAVIGATOR_EXAMPLE_H_

third_party/blink/renderer/modules/example/navigator_example.cc

// Simple supplement implementation: obtains/creates the per-Navigator instance
// and exposes a trivial echo method.
#include "third_party/blink/renderer/modules/example/navigator_example.h"

namespace blink {

const char NavigatorExample::kSupplementName[] = "NavigatorExample";

// Ensures a single NavigatorExample is attached to this Navigator and returns it.
// - Lookup: Supplement<Navigator>::From<NavigatorExample>(navigator)
// - Create+attach if missing: MakeGarbageCollected<NavigatorExample>(navigator);
//   ProvideTo(navigator, supplement)
// - Why: host-scoped state (no globals), Oilpan GC-managed lifetime via Trace,
//   idempotent access for generated bindings/static entry points.
NavigatorExample& NavigatorExample::From(Navigator& navigator) {
  auto* supplement = Supplement<Navigator>::From<NavigatorExample>(navigator);
  if (!supplement) {
    supplement = MakeGarbageCollected<NavigatorExample>(navigator);
    ProvideTo(navigator, supplement);
  }
  return *supplement;
}

NavigatorExample::NavigatorExample(Navigator& navigator)
    : Supplement<Navigator>(navigator) {}

String NavigatorExample::exampleEcho(ScriptState*,
                                     Navigator&,
                                     const String& input) {
  // Return the input unchanged.
  return input;
}

}  // namespace blink

Add a small GN target so the files build and are linked into the modules library. Then wire the module into the parent modules/BUILD.gn and register the IDL with Blink's bindings pipeline.

third_party/blink/renderer/modules/example/BUILD.gn

# GN target for our example module. Use blink_modules_sources so the right
# configs/deps are applied. Do not list the .idl here; it is registered below.
import("//third_party/blink/renderer/modules/modules.gni")

blink_modules_sources("example") {
  sources = [
    "navigator_example.h",
    "navigator_example.cc",
  ]
}

Expose the new module in third_party/blink/renderer/modules/BUILD.gn by adding //third_party/blink/renderer/modules/example to the sub_modules list (near other module entries) so it is built and linked into blink_modules:

# third_party/blink/renderer/modules/BUILD.gn
  sub_modules = [
    ":modules_generated",
    "//third_party/blink/renderer/bindings/modules/v8",
...
    "//third_party/blink/renderer/modules/example",
...

Register the IDL so the bindings generator can see it. Add the path to third_party/blink/renderer/bindings/idl_in_modules.gni inside static_idl_files_in_modules:

# Statically-defined (not build-time-generated) IDL files in 'modules' component
# for production.
static_idl_files_in_modules = [
  "//third_party/blink/renderer/modules/example/navigator_example.idl",
...

Finally, gate the feature at runtime. Add an entry named ExampleAPI to the Blink runtime-enabled features configuration (the exact JSON5 location varies by branch; search for existing feature entries such as WebShare and mirror that pattern).

Build and run. Any time you change GN files or IDL registration, re-run the GN generation step before building so Ninja files pick up the changes:

gn gen out/Default
autoninja -C out/Default chrome
out/Default/chrome --no-sandbox --enable-blink-features=ExampleAPI --user-data-dir=/tmp/t

Now you can add in the new tab data:text/html,<script>document.write('navigator.exampleEcho("hello"): ' + navigator.exampleEcho('hello'));</script>.

Follow-up proposal: a toy image codec

If you want a deeper systems exercise, implement a tiny image decoder under platform/image-decoders/ that recognizes a custom header (for example TOY0) and decodes to a solid color. Register it with the decoder factory so <img> can load it, and add a web test that feeds a minimal data URL and checks that a colored rectangle is painted. This touches sniffing, decode lifecycle, and integration with the loader/paint pipeline without the complexity of a production codec.

References

  • Blink architecture: third_party/blink/renderer/README.md
  • How Blink Works (2018, public): bit.ly/how-blink-works (Google Doc)
  • Blink scheduling: https://chromium.googlesource.com/chromium/src/+/HEAD/third_party/blink/renderer/platform/scheduler/TaskSchedulingInBlink.md
  • Compositor thread architecture: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/website/site/developers/design-documents/compositor-thread-architecture/index.md
  • GPU accelerated compositing in Chrome: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/website/site/developers/design-documents/gpu-accelerated-compositing-in-chrome/index.md
  • Viz overview: https://chromium.googlesource.com/chromium/src/+/HEAD/services/viz/README.md
  • Multi-process architecture and Site Isolation: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/process_model_and_site_isolation.md; https://chromium.googlesource.com/chromium/src/+/HEAD/docs/website/site/developers/design-documents/site-isolation/index.md
  • Cross-Origin Isolation: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/cross_origin_isolation.md
  • Network stack overview: https://chromium.googlesource.com/chromium/src/+/HEAD/net/docs/life-of-a-url-request.md; https://chromium.googlesource.com/chromium/src/+/HEAD/services/network/README.md
  • Command buffer and GPU process: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/website/site/developers/design-documents/gpu-command-buffer/index.md
  • V8:
  • Ignition/TurboFan overview: https://v8.dev/blog/launching-ignition-and-turbofan
  • Sparkplug: https://v8.dev/blog/sparkplug
  • Liftoff (Wasm baseline): https://v8.dev/blog/liftoff
  • Pointer Compression: https://v8.dev/blog/pointer-compression
  • V8 Sandbox: https://v8.dev/blog/sandbox


Author: 3074e822993c84cdf216428e1f1e8f570c9626544872f2ed4c291befc72770e3