Zero Browser — Architecture & Engine Specification
Version: 0.2 Last updated: 2026-09-19 Scope: System architecture, from-scratch rendering/JS/network stack, process and security model, Rust crate map, performance budget.
How to read this document. It describes Zero as built, and says so plainly where something is not built. Each section separates two things:
- As built — what the code in this repository does today. Verifiable by reading it; if it disagrees with the code, the document is wrong.
- Intended — where the section is going. Not a promise of date, and not a description of anything that exists.
Version 0.1 mixed the two, and the engine drifted from it for months without anyone noticing: it specified
wgpucompositing,tokioasync I/O and nineteen crates, none of which were built. The separation above is the fix. Section numbers are referenced from source comments (§3,§4and§10are cited incss.rs,html.rs,layout.rs,paint.rs,js/mod.rsandjs/interp.rs) — renumbering silently breaks those citations, so sections are rewritten in place, never reordered.
1. Design tenets
- Memory-safe by construction — Rust everywhere;
unsafeisolated, audited, minimized. - Process isolation — a compromised web page must not read another site's data or the OS.
- Layered and swappable — the engine knows nothing about windows, tabs or files; the shell is one embedder among possible others. This boundary is real today and is the one that matters.
- Measure before optimizing — the pipeline is CPU-rastered and single-threaded. GPU compositing is not a tenet, it is a candidate: at 1280x900 a real page takes 25 ms to render and 1.5 ms to composite, so moving the composite to the GPU cannot touch the number that dominates. It becomes worth doing when rasterization itself moves.
- Honest phasing — the engine implements a defined web subset, and says which.
- Every shortcut names its ceiling — deliberate simplifications carry a
ponytail:comment stating the limit and the upgrade path, so a shortcut is a tracked debt rather than a surprise.
2. Process model
As built
Two kinds of process:
┌──────────────────────────────────────────────────────────────┐
│ Browser process — trusted │
│ • Window, vertical tab rail, toolbar, menus, split view │
│ • Tab and session orchestration │
│ • Network (fetch, TLS, cookies, tracker blocking) │
│ • Storage (history, bookmarks, downloads, settings) │
│ • AI (extractive page summarizer) │
│ • Startup mitigations applied to itself │
└───────────────┬──────────────────────────┬───────────────────┘
│ pipe IPC (stdin/stdout) │
┌───────▼────────┐ ┌────────▼───────┐
│ Renderer proc │ ... │ Renderer proc │ (one per TAB)
│ • HTML/CSS │ │ │
│ • Style/Layout │ │ │
│ • Paint │ │ │
│ • JS │ │ │
│ NO network │ │ NO network │
└────────────────┘ └────────────────┘
- One renderer per tab. Page content — the most hostile input the browser handles — is parsed, run and painted in a child process that drops what privileges it can before reading a byte.
- The renderer has no network of its own. Every subresource it needs travels back up the pipe for the parent to fetch, which keeps cookies, the tracker list and the profile key on the trusted side.
- IPC is length-prefixed binary messages over the child's stdin/stdout
(
wire.rs). A reader thread feeds replies to a channel so a hung page cannot block the UI thread; a reply timeout bounds the wait, and a dead renderer is noticed and respawned rather than taking the window down. - A spare renderer is kept warm, so opening a tab does not wait for a process to start and read fonts.
- Navigation between two
zero://screens reuses the process — a settings toggle is a navigation, and it should not cost a process. - Startup mitigations (
sandbox.rs) apply to every process: no dynamic code, no injected libraries, strict handle checks. Windows only.
Not built
- Site isolation. Renderers are keyed by tab, not by site (eTLD+1).
- A filesystem jail. The renderer drops privileges on its token but is not confined by a restricted token, AppContainer, Seatbelt or seccomp-bpf. The mitigations make a memory-safety bug harder to escalate; they do not make one harmless.
- Separate service processes. Network, storage and AI are modules inside the browser process, not sandboxed services.
- Cross-site iframes in their own process —
<iframe>is not implemented at all.
Intended
Site isolation and a per-OS filesystem jail are the next real steps. Splitting network, storage or AI into service processes buys little until there is a second consumer or a jail to put them behind.
3. Rendering pipeline (the from-scratch engine)
The engine turns bytes into pixels. The stage numbers [1]–[7] below are cited
from source comments and are stable.
Network bytes
│
▼
[1] HTML tokenizer + tree builder ──► DOM tree
│ │
│ ▼
│ [3] Style engine
▼ (CSS parse + cascade + match)
[2] CSS parser ──► Stylesheets ─────────┘
│
▼ styled tree (computed values)
[4] Box tree construction
│
▼
[5] Layout (block, inline, flex, grid, table)
│ → geometry (x,y,w,h per box)
▼
[6] Paint → display list → CPU raster
│
▼
[7] Present (shell blits the canvas)
[1] HTML parsing (html.rs → dom.rs)
As built: a tolerant recursive-descent parser. Doctypes, comments, void
elements, self-closing tags, raw-text elements (<script>, <style>), unquoted
and boolean attributes, mismatched close tags — recovering rather than panicking.
Not built: WHATWG tree construction. No implied tags (no auto <tbody>, no
<p> auto-close), no adoption-agency error recovery, no full entity table, no
fragment parsing.
Intended: grow toward the WHATWG state machine, measured against a conformance
corpus rather than by inspection.
[2] CSS parsing (css.rs)
As built: selectors (tag, #id, .class, *, descendant, >, +, ~),
attribute selectors, structural and state pseudo-classes, declarations, @media
(type and width), @import, custom properties, calc() as a deferred expression
resolved at use, shorthand expansion, rgb()/hsl()/named colours, lengths in
px/%/em/rem, @font-face, the style attribute, and ::before/::after as
pseudo-elements whose declarations are kept aside for the box they generate.
Unknown rules and values are skipped, never fatal.
Not built: :has() — a rule carrying it is dropped whole rather than
misapplied. No @supports or @keyframes. The font shorthand is not
expanded, so a page that sets its face that way gets none of it.
[3] Style engine (style.rs)
As built: a RuleIndex buckets rules by tag, class and id, so matching tests
candidate rules rather than the whole stylesheet. Cascade with specificity and
source order, inheritance, and custom-property resolution.
Not built: invalidation. Any change restyles the whole document; there is no
dirty-subtree tracking.
[4]–[5] Layout (layout.rs)
As built: one LayoutBox tree built from the styled tree and mutated in place.
Block, inline, inline-block, flex (wrap, grow, justify, align), grid (repeat(),
fr, minmax(), spans, named areas), tables (colspan/rowspan), floats and
clear, intrinsic sizing, and positioning: relative, absolute against the
nearest positioned ancestor, fixed against the viewport, and sticky as a
post-pass once the tree is laid out (which is why Page::uses_sticky exists —
it tells the embedder that this page's band does not survive a scroll).
Not built: sticky honours top only and is not clamped to its containing
block, so it stays pinned past the end of the section it belongs to; fixed is
anchored to the viewport but does not ride the window as you scroll. Inline
layout is word-granular, splitting on ASCII whitespace.
[6] Paint (paint.rs)
As built: walk the box tree into a display list (solid and rounded rects,
gradients, shadows, borders, shaped text runs, images, clips), then rasterize on
the CPU into a canvas of RGBA pixels. Anti-aliased text, opacity, overflow
clipping and transform: translate/scale are applied as the list is built. A
text run carries its clip rather than being kept or dropped whole, so overflow
cuts words at the box edge — which is what makes a box collapsed to a pixel, the
way every large site hides a screen-reader heading, actually hide it.
Not built: the display list is flat — z-index orders siblings rather than
establishing stacking contexts, and opacity applies per command rather than to a
composited layer. Within one z-index, positioned boxes do at least paint after
non-positioned ones, which is the part of CSS's painting order overlays need.
[7] Present (zero-shell)
As built: the shell blits the finished canvas to the window via softbuffer.
There is no compositor, no layerization and no GPU path. Measured at 1280x900:
25 ms to render a real page, 1.5 ms to composite the frame.
Supporting modules: text.rs (shaping via rustybuzz, rasterizing via
fontdue, per-word font fallback across a priority chain, and font-family
matching by the name a face gives itself), woff2.rs (a page's own .woff2
typefaces unpacked back into an sfnt), svg.rs (Zero's own
SVG rasterizer — shapes, paths, fills, strokes, viewBox — used for both
<img src=".svg"> and inline <svg>), anim.rs (CSS transitions, holding the
previous value per element against a document clock), resource.rs (image decode).
Decisions this document never stated
Four properties of the pipeline arrived with the first engine commit as defaults rather than choices, and were never written down. They are recorded here because each carries a ceiling, and the cost of changing each grows with the layout code:
| Default | Ceiling it imposes | Cost to change |
|---|---|---|
Coordinates are f32 |
Accumulated float drift; positions that should compare equal do not. Makes pixel reftests flaky in ways that look like layout bugs | Low — mechanical, ~230 sites |
Geometry is physical only (left/right/top/bottom) |
direction: rtl and writing-mode: vertical-* are unreachable. Blocks Urdu and Japanese vertical text |
Rises fastest. ~220 sites in layout.rs today, plus threading a writing mode through every containing block |
| One mutable box tree, rebuilt per layout | No incremental layout and no parallel layout. Every layout is full-page | High — restructures layout. No payoff until incremental layout is real |
Computed values are HashMap<String, Value> |
Every property read hashes a string; no sharing between elements that computed identically | Moderate — 87 call sites across 55 distinct properties |
Intended: logical geometry is the one with a closing window and should land before the layout code grows further. Fixed-point coordinates and typed computed values are cheap whenever. The box/fragment-tree split waits for incremental layout to be worth having.
4. JavaScript engine (zero-engine/src/js/)
As built
Zero's own lexer, parser and tree-walking interpreter — no third-party JS engine. This is phase 1 of the plan below, delivered: correctness over speed.
Supported: closures, classes with super, try/catch/finally, regex
literals (own engine), setTimeout, JSON, Promise with then/catch/
finally/all, async/await, fetch, localStorage, console, DOM query
(getElementById, getElementsByTagName, getElementsByClassName,
querySelector, querySelectorAll), DOM mutation (innerHTML, innerText,
textContent, className, value), and event listeners.
Performance work has been measurement-led: the interpreter is 4–18x faster than
its first version, and profiling showed the cost was never the AST walk (see
crates/zero-engine/examples/jsbench.rs).
Not built
The gap is the standard library, not only speed. Absent: Math, Date,
Object.*, Array methods beyond push/pop/join, most String methods,
setInterval, createElement/appendChild, element.style, classList,
location, history, XMLHttpRequest, ES modules, getters and setters,
generators. Any script written against a modern baseline fails early. There is no
garbage collector — values are reference-counted, and cycles leak.
Intended
Unchanged in shape from v0.1, and still the right order:
- Phase 2 — bytecode VM and inline caches: resolved variable slots, shape-based property access. Worth doing when a real page's scripts are what is slow.
- Phase 3 — baseline JIT: hardened (W^X, guard pages). Years out.
- GC: precise mark-sweep before anything generational.
The standard library outranks all of the above: a VM that runs an incomplete language faster does not run more pages.
Reality note: a from-scratch JS engine will not match V8 for years. The strategy is to render Zero's own surfaces and a growing subset of the web on our engine, and hand the arbitrary long-tail web to the browser the system already has (§11) until the engine catches up.
5. Networking stack (zero-shell/src/net.rs)
As built
- Protocol: HTTP/1.1 over
ureq, which wrapsrustlsfor TLS. One pooled agent per process, so a page's subresources reuse an open TLS session instead of paying for a handshake each. - Identity: the user-agent names Zero honestly rather than impersonating another browser.
- Cookies (
cookies.rs): partitioned per top-level site and per space, so a profile is a real boundary. Persisted encrypted. - Content blocking (
blocker.rs): Adblock-syntax filtering at the fetch layer, so a blocked request never leaves the machine. - HTTPS-first: plain-HTTP targets are upgraded, and the URL that actually loaded is adopted if it changes.
- Transport encoding: gzip and deflate, via
ureq. - Runs in the browser process. The renderer asks it for bytes over the pipe.
Not built
- HTTP/2 and HTTP/3. HTTP/1.1 only.
- An HTTP cache. There is a per-page-load in-memory map, but no
Cache-Control,ETagorExpireshandling and nothing on disk — so every navigation refetches every subresource. - POST. A form with
method=postis not submitted at all, which means no logins anywhere. - A resolver of our own, and no DoH. HSTS. Mixed-content blocking.
A redirect policy of our own (delegated to
ureq). - Async I/O. Fetching is synchronous and blocks the caller.
Intended
POST first — it is the single largest gap between Zero and a browser someone can use daily. Then an RFC 9111 cache, then HSTS. HTTP/2 needs an async runtime and is a larger change than it looks.
6. Storage (zero-shell/src/storage.rs, spaces.rs, crypto.rs)
As built
- Per-space profile directories. A space is a profile: its own tabs, history,
cookies,
localStorage, downloads, settings and encryption key. - The format is tab-separated lines, not a database. It needs no dependency, is trivially inspectable, and a corrupt line can be skipped instead of failing the whole file.
- Encrypted at rest on all three platforms — DPAPI on Windows, AES-256-GCM under a Keychain or Secret Service key elsewhere.
- Sync is a file, not a service:
--exportseals a space under a fresh 32-byte code,--importopens it elsewhere. The code never leaves the user's hands. Import replaces the space's files rather than merging them.
Not built
No embedded database — rusqlite versus redb was a v0.1 open question, and flat
files turned out to be enough. No password storage, because there are no logins to
store (§5). No quota management or eviction.
7. AI (zero-shell/src/ai.rs)
As built
LocalAssistant is not a language model. It is a classic extractive
summarizer — word-frequency sentence ranking plus document statistics. It ships as
the default because it needs no network, no API key and no consent, which makes it
the only provider that satisfies the privacy invariant in
docs/04-SECURITY-PRIVACY.md §5.5 unconditionally.
The page context handed to it is deliberately text-only: URL, text, headings, tracker count, security state. No markup, no scripts, no cookies, no form values.
Not built
No model, on-device or cloud. No separate AI process — it is a module in the browser process. No agentic actions, so no confirmation gate exists yet.
Intended
A real model plugs in behind the Assistant trait and receives the same
PageContext. Anything leaving the device is gated on explicit, per-action
consent, and that gate belongs in the shell, never in the engine. Agentic actions
need the capability gate before they need a model.
8. UI shell and compositing (zero-shell)
As built
- Window and input via
winit; presenting viasoftbuffer— a CPU framebuffer blitted to the window. Nowgpu, no GPU path. - The chrome is drawn by Zero's own engine. The tab rail, toolbar, menus and
the
zero://pages are small HTML documents with inline<svg>icons, put through the same pipeline a website goes through. If the browser's own buttons look wrong, the engine has a bug worth fixing — which is the point, and it means the chrome is a conformance test that runs every frame. - The chrome is rebuilt from scratch each frame. The shell animates the rail width itself and asks for the next frame only while something is moving; an idle window draws nothing.
- Settings are links. Each control is an ordinary anchor carrying its new value
(
zero://settings?rail=icons), so changing a preference goes through the same navigation path as clicking any link on the web. - Headless modes for review and testing:
--pngrenders a page in a renderer process;--shotscreenshots the whole window, with poses that put the chrome into states a still image cannot otherwise reach.
Resolved
The v0.1 open question — own wgpu UI versus a mature Rust toolkit — was answered
by building a third option: the chrome is HTML rendered by our own engine, on the
CPU. It gave the design system and the engine one implementation instead of two.
9. Rust crate / workspace map
As built
Two crates.
zero/
├── crates/
│ ├── zero-engine # html, css, style, layout, paint, text, svg, anim, js
│ └── zero-shell # window, chrome, tabs, spaces, net, storage, crypto,
│ # blocker, cookies, renderer process, AI, i18n
├── docs/
├── examples/
└── Cargo.toml # workspace
The one boundary that is real is the important one: the engine knows nothing
about windows, tabs or files. It takes bytes and a ResourceLoader and returns a
canvas. The shell is one embedder among possible others.
A module that has never had a second consumer does not need to be a crate. The nineteen-crate map in v0.1 described boundaries that had not been earned; crates split off when a second consumer or a separate build appears, not before.
Dependencies, and why each is not "embedding an engine": winit and
softbuffer (windowing and presenting — shell concerns), ureq and rustls
(HTTP and TLS math), fontdue (TTF rasterizing), rustybuzz (shaping — required
for correct Indic rendering, since matra reordering and conjuncts are not
expressible as one glyph per character), image (decoding content images),
allsorts (WOFF2 decoding — the container is Brotli-compressed and its
glyf/loca tables are transformed and have to be rebuilt; only the decode is
borrowed, and packing the result back into an sfnt is ours), aes-gcm and
windows-sys (encryption at rest).
The line these imply, and the one worth holding: engine algorithms are ours; OS and protocol plumbing can be borrowed. A compression format is plumbing; a layout algorithm is not.
10. Web-platform support
As built
| Capability | State |
|---|---|
| HTML parsing | Tolerant, not WHATWG. No implied tags, no adoption agency, no <template> parsing — a template's children land in the DOM and are held back by the UA stylesheet rather than by the parser |
HTML hidden attribute |
Supported, via the UA stylesheet |
| CSS box model, colours, backgrounds, borders | Supported |
| CSS selectors: descendant, child, sibling, attribute, structural | Supported |
CSS ::before/::after |
Content generated; the box inherits the element's styling rather than taking the rule's own |
CSS :has() |
Not supported — the rule is dropped whole |
CSS inline style attribute |
Supported, last in the cascade (!important is not modelled) |
| CSS flexbox | Supported (wrap, grow, justify, align) |
| CSS grid | Supported (repeat(), fr, minmax(), spans, named areas) |
CSS tables, floats, clear, absolute and relative positioning |
Supported |
CSS position: sticky |
Supported, top only and unclamped. fixed anchors to the viewport but does not ride a scroll |
CSS transforms (translate/scale), transitions, opacity, z-index |
Supported. z-index orders siblings only |
CSS custom properties, calc(), @media, @import |
Supported |
CSS @font-face, font-family |
Supported, .woff2 included, plus the serif/monospace generics. The font shorthand is not expanded |
CSS text-overflow: ellipsis, white-space: nowrap |
Supported |
| Text: Latin and Indic shaping, font fallback | Supported; a run is clipped per glyph by overflow |
| Text: line breaking | ASCII whitespace only — CJK and Thai never wrap |
| Text: bidi (RTL) | Not supported — Urdu renders backwards |
| Text selection and copy | Supported |
| Images: PNG, JPEG, GIF, WebP, BMP, ICO | Supported |
| SVG | Own rasterizer: shapes, paths, fills, strokes, viewBox |
| JS: interpreter | Supported, minus most of the standard library (§4) |
| JS: bytecode VM, JIT | Not built |
Web APIs: fetch, DOM query and mutation, events, localStorage |
Supported |
Forms: GET submission, <select>, text input, IME |
Supported |
| Forms: POST | Not supported — no logins |
<iframe> |
Not supported |
<video> / <audio> |
Not supported |
| Accessibility tree | Not supported — invisible to a screen reader |
| Cross-site iframe process isolation | Not applicable until iframes exist |
Intended
The ordered work is tracked as GitHub milestones, not phases — they were set
by opening real sites and writing down what broke, so they lead with what makes
Zero unusable rather than what is furthest from spec. docs/03-ROADMAP.md holds
the strategy; the milestones hold the queue.
Each capability should graduate against a conformance corpus, not against inspection. See §14.
11. The compatibility bridge
As built
Not built, and being questioned rather than scheduled. Ctrl+Shift+O hands
the current page to whatever browser the system already has. That keeps the
promise that nobody is stuck on a page Zero renders poorly, and costs one
keystroke instead of shipping, sandboxing and updating a second engine.
It is a handoff, not a bridge: the page opens there, outside Zero's cookie jar and tracker blocking, and the user can see that it did.
Intended
If the handoff proves insufficient in real use, an embedded-engine bridge is the answer. Until then it is 200 MB of speculation, and the v0.1 plan to ship one behind a flag is withdrawn rather than deferred.
12. Performance
Measured
| What | Number | Notes |
|---|---|---|
| Render a real page at 1280x900 | 25 ms | CPU, single-threaded, whole document rather than viewport |
| Composite that frame | 1.5 ms | 2.6x faster once it stopped doing per-pixel coordinate arithmetic at zoom 1 |
| JS interpreter | 4–18x faster than v1 | examples/jsbench.rs |
| New-tab create | Does not wait on process start | A spare renderer is kept warm |
The 25 ms and the 1.5 ms together are why GPU compositing is not scheduled: moving the 1.5 ms to the GPU cannot touch the 25 ms. Rasterization has to move first.
Not measured
Cold start, input latency, idle RAM, memory under many tabs. There is no perf CI, so none of these is defended against regression. The v0.1 budgets — ≤1500 ms cold start, ≤100 ms new tab, ≤16.6 ms frame, ≤50 ms input latency — stand as targets with nothing enforcing them.
13. Security model
Summary; the full document is docs/04-SECURITY-PRIVACY.md.
As built
- Page content runs out of process, privilege-dropped, with no network of its own (§2).
- Startup mitigations on every process: no dynamic code, no injected libraries, strict handles. Windows only.
- Cookies and
localStoragepartitioned per site and per space. - Profile data encrypted at rest on all three platforms.
- HTTPS-first upgrade.
- Rust safety eliminates whole exploit classes in safe code.
Not built
Site isolation. A filesystem jail (restricted token, AppContainer, Seatbelt or
seccomp-bpf). HSTS. Mixed-content blocking. Signed updates and rollback protection
— there is no updater. An inventory of unsafe blocks.
14. Testing and CI
As built
251 unit tests across the two crates, plus an integration test for the renderer process. They are genuine — parser, cascade, layout, JS and wire-format behaviour, not smoke tests.
Not built
There is no CI. .github/ holds issue and PR templates and no workflows, so
nothing runs on push: not the test suite, not a build on the other two platforms,
not a lint. No WPT. No Test262. No fuzzing. No screenshot diffing. No perf CI.
This is the largest gap between this document and a project that can safely accept contributions, and it gates everything in §10 — "renders correctly" is currently an assertion, not a measurement.
Intended, in order
- CI that runs
cargo teston three platforms. Everything else here is speculative until this exists. - WPT reftests against
--png. WPT's CSS reftests render two documents and compare pixels, which the existing headless mode already does — so a meaningful conformance number is reachable without implementing WebDriver. Vendor only the subdirectories under test; the full suite is around 1.1 GB. - Unicode conformance data (
LineBreakTest.txt,BidiTest.txt) for the text work in §3 and §10. - Fuzzing the HTML, CSS and JS parsers — untrusted input is the top attack surface.
- Test262 subset for §4.
- Perf CI enforcing §12.
15. Key architectural risks
| Risk | Impact | Status |
|---|---|---|
| From-scratch engine timeline | High | Live. Mitigated by phasing and the §11 handoff |
| No CI, no conformance measurement | High | Live and unmitigated. See §14 |
| JS standard library gap blocks real pages | High | Live. Outranks JS performance work |
| Physical-only geometry blocks RTL and vertical text | Med | Live, and the cost rises with every layout change (§3) |
| No POST means no logins | High | Live. See §5 |
| Cross-platform sandbox parity | Med | Live. Mitigations are Windows-only |
| Indic text shaping correctness | Med | Mitigated — rustybuzz, and the chrome exercises it every frame |
| Team scale for a browser | High | Live. OSS contribution model, and §14 is what makes contribution safe |
16. Open decisions
The v0.1 list is resolved or withdrawn: the shell-renderer question was answered by building the chrome in our own engine (§8); the embedded-database question dissolved when flat files proved sufficient (§6); the compat bridge is withdrawn rather than deferred (§11). What remains open:
- Logical geometry — when, not whether. The cost rises with every layout change, and it gates RTL and vertical writing modes (§3).
- Licence boundary for borrowed code. Zero is Apache-2.0. Some ecosystem pieces that would save real time are MPL-2.0, which is file-level and perpetual. Depending on one is a clean, visible boundary; copying its source in is not. Worth deciding deliberately rather than drifting into.
- Accessibility: own implementation or OS bindings. A screen-reader tree means UIA, AT-SPI and NSAccessibility — three platforms' APIs, which sit on the plumbing side of the §9 line rather than the engine side.
- JS GC strategy. Reference counting leaks cycles today; mark-sweep is the next step, and the trigger for doing it is a real page that leaks.
- HTTP stack. An async runtime is the price of HTTP/2, worth paying when HTTP/1.1 is measurably the problem.