PROT_EXEC mmap/mprotect are killed), Landlock, per-tab
namespaces, and an undumpable worker so secrets cannot leak via core dump or
ptrace. The syscall allowlist is deny-by-default, so io_uring is blocked
in the worker — it is a seccomp-bypass primitive, so it never gets in.--dump-video to download and pipe video streamsThree engine-wide corrections, each isolated in a probe measured against Firefox headless before any code changed. Structural divergence over the parity corpus fell from 154.06 to 126.04.
max(previous bottom margin, a theme constant), and only once a line of
text had been painted. A page whose content is boxes — a separator, a spacer, an
icon tile, an empty card — never paints one, so no block on it received any margin
at all: three 20px divs with margin-bottom:30px stacked at 0/20/40 where Firefox
puts 0/50/100. The rule lives in a new pure module, block_flow (bf_), and is the
normative one — max(0, positives) + min(0, negatives), which stops agreeing with
max(a, b) the moment a margin is negative, and margin-top:-1px to overlap borders
is everyday markup. Parent/child collapsing works in both directions: a first child’s
top margin moves the parent, while a last child’s bottom margin stays inside a
box that has bottom padding.body{font-size:16px}
— or its font: shorthand, which is on essentially every real page — used to render
h1, h2 and p at exactly the same size, flattening the document’s hierarchy. An
absolute font-size replaces the user-agent heading scale only when the author
declared it on the heading; on an ancestor it is the heading’s inherited value and
h1{font-size:2em} still doubles it.grid-template-areas places items by name. Grid items were placed round-robin at
index % columns, so every named layout — how essentially every modern portal is
built — came out with its regions in source order. Named areas resolve to rectangles
(spec/grid_areas.md) and explicit placement runs before auto-placement, per CSS Grid
§8.3/§8.5. The grid-template / grid shorthands parse too.Also fixed on the way: ::before content is inline content and no longer takes a row of
its own (it was doubling the height of Wikipedia’s reference list); multi-column
fragmentation no longer reads row positions its own earlier pass had already rewritten;
and a flex/grid item with both height and padding is no longer exactly its vertical
padding too short.
The two structural render breaks are closed, both measured against Firefox headless:
header{display:flex} holding a flex
<nav><ul> — the navbar of nearly every modern site — used to split into two
stacked rows with the inner items’ boxes drawn at the wrong size. A run carries only
its innermost container, and laying out the outer one needs the outer container’s
parameters, which in a grid-of-flex-cards no run carries at all. So containers
now have a descriptor table (pv_cont_def, indexed by cont_id) holding
parent_id/parent_item plus the container parameters; it crosses the IPC as its
own array. resolve_context describes the whole ancestor chain instead of stopping
at the innermost, the layout loop groups by the root container, and
layout_container recurses into items that are themselves containers, sizing
them by the nested container’s max-content width.display:inline-block flows inside its line. A badge, pill or chip in the
middle of a sentence used to break it into three: text, badge alone on a full-width
row, rest of the text. Registering a box was the problem — opening a box is a block
operation that flushes the line. An inline-level box is now tracked off the box
stack and materialised when the line flushes, from the extent of its own fragments.
An inline-block that opens a line keeps the old block treatment (shrink-wrapped and
placed by text-align), which is what a standalone centred call-to-action needs.Both are locked by pixel regression tests. See spec/page_view.md.
Still open, and now the loudest remaining gap: table auto width (shrink-to-fit)
plus colspan/rowspan — every table still spans the full page and ignores spans.
Four defects that made ordinary pages look broken, all measured against Firefox
headless on the same HTML (firefox --headless --screenshot versus
freedom --download-png):
px, pt, rem, viewport units and the size
keywords resolve against the 16px root and replace the user-agent heading scale;
em/% stay relative and chain outward until an absolute size anchors them
(.card{font-size:14px} .card h3{font-size:1.2em} → 16.8px). Before this the percent
was multiplied onto the UA size, so every author-styled <h1> painted at double
its declared size. See spec/css.md, “font-size: absolute vs relative”.px/rem against, so the two cannot drift — the CSS 2.1 heading scale
(2.0 / 1.5 / 1.17 / 1.0 / 0.83 / 0.67), line-height 1.2, and headings that inherit
their colour instead of taking a theme accent. A heading’s bold now rides the resolved
font weight, so h1{font-weight:normal} finally works. See spec/box_style.md §4b.<strong>bold</strong>, renders as bold,
and no longer as bold ,. The word flow inserted a space before every word that did not
open a line, which put a stray gap before every comma, bracket and full stop following an
inline element — on every page with emphasis, links or <code>.<span> registers a box and honours right/bottom instead of landing in the flow at
full width. As a side effect the PNG export is now sized by the lowest box rather than by
the text flow alone: a page made only of boxes used to export as a 0px image.All four are locked by pixel regression tests in tests/test_freedom.c.
(Nested flex and inline-block flow, listed as open when this section was written, are closed above.)
Inline <svg> renders: shapes (rect/circle/ellipse/line/polyline/polygon),
full <path> data (including elliptical arcs), <g> groups with inherited paint and
composed transform, fill="currentColor" icons that take the surrounding text colour,
viewBox scaling, and <text>.
It renders unconditionally — unlike <img>, which stays behind the images toggle —
because the grammar the parser accepts has no URL form at all: <image>, <use>,
<foreignObject>, <script> and <style> are dropped together with their subtrees, so
an inline SVG structurally cannot fetch. The parser (svg_render, sv_) is pure, bounded
and fuzzed (make fuzz-svg); the Cairo half (svg_paint, svp_) only draws already
validated geometry. See spec/svg_render.md.
The same push closed five render gaps measured against Firefox
(firefox --headless --screenshot versus freedom --download-png on the same file):
justify-content: space-between paints one
band across the header instead of one rectangle per item with the page showing through.display: inline-block shrink-wraps to its content and is placed by the parent’s
text-align — a centred call-to-action button was a full-page-width bar.<img>, <svg>) are placed by text-align and honour their
declared width/height, and a max-width; margin: 0 auto block keeps its width
cap inside a decorated ancestor.A modernization push across eight phases delivered a rendering engine comparable
to modern browsers for trusted hosts (hosts present in both allow.conf and
js.conf):
Animations & Transitions (R1): pure interpolation engine (interp module)
supporting CSS easing functions (linear, ease, ease-in/out, steps, cubic-bezier),
scalar and color value interpolation, keyframe interpolation, and a full animation
state machine handling delays, direction, fill modes, and iteration counts.
animation-duration CSS property parsed and flows through the full pipeline
(CSS -> page_view -> IPC -> paint). @keyframes at-rules are parsed and stored;
animation-name, animation-iteration-count, animation-direction,
animation-fill-mode, animation-timing-function, and animation-delay are
all parsed through the CSS cascade. In the GUI, boxes with animations
automatically form stacking contexts and composite with animated opacity blended
from the interpolation engine against wall-clock time.
Advanced Selectors (R2): :first-of-type, :last-of-type, :only-of-type,
:nth-of-type(An+B), :nth-last-of-type(An+B), :empty, :target, and
:lang(ident) pseudo-classes fully parsed and matched. The existing :not(),
:is(), and :where() functional pseudo-classes were already implemented.
The DOM element chain (css_chain.c) was extended with per-type sibling counting
and child count for accurate matching. :empty correctly detects elements with
zero child nodes. :lang() performs case-insensitive prefix matching on the
lang attribute. Pseudo-elements are correctly distinguished: ::before and
::after match in the CSS cascade (for style resolution) but never match in
DOM queries like querySelector (matching real browser behavior).
CSS Filters (R3): filter: blur(Npx) and filter: grayscale(N%) parsed
through the full pipeline. In the GUI, boxes with filters form stacking contexts.
The grayscale filter is applied via Cairo pixel manipulation: the box’s offscreen
group is extracted as an image surface, each pixel is converted to grayscale
(weighted luminance: 0.299R + 0.587G + 0.114B), and the modified surface is
blended back with partial grayscale factor. Anti-DoS bounded at 4096px.
Layout & Positioning (R4): position: sticky implemented — boxes pin to the
viewport top when scrolled past their in-flow position, common for fixed
navigation bars. right and bottom CSS insets now honored for absolute/fixed
positioned boxes: when right is set and left is auto, the box anchors to
the right edge of its containing block (and similarly for bottom). These were
already parsed but previously ignored in the box-tree position resolver.
Firefox-parity pass on modern layout (2026-07-27): the render was diffed against Firefox 140 ESR headless on the same HTML, and the gaps that broke modern pages were closed rather than guessed at.
flex-basis is sized to its
measured content instead of an equal share of the row, so justify-content
(space-between/center/space-around) finally has leftover space to
distribute and a three-link nav bar shrink-wraps instead of spanning the page.
Item margin counts as main-axis space. Every flex container goes through the
flex engine (previously only those whose items declared a flex property).display:flex/display:grid container
now paints its background/border/radius and lays its items out inside its
padding; nesting is preserved, so a flex row inside a
max-width; margin: 0 auto wrapper stays inside the wrapper.display:inline-block flows horizontally. A parent whose element children
are all inline-block lays out as an anonymous flex row — nav bars, badge
strips and button groups no longer stack one per line.position:absolute with width:auto sizes
to its content (a right-anchored badge lands at the right edge instead of
off-screen), and a position:relative wrapper holding only positioned
children generates its own box, so it paints, reserves its height and acts as
their containing block.td { border: 1px solid } paints (<th>
centered by UA default); inline backgrounds (<code>, <mark>) paint
behind their own glyphs instead of smearing across the line; text-align
inside a grid/flex cell centers within that cell; and <li> no longer
inherits a paragraph’s 1em margin, so lists are tight like a browser’s.Still v1: text does not wrap beside a lone float, align-items centers
against the tallest item rather than an explicitly taller container, and
width:auto is not stretched between left+right.
Out-of-flow subtree + line-box pass (2026-07-30, Stage 2d): measured against Firefox on a live Wikipedia article; four chained pipeline bugs closed.
position:absolute/fixed subtree leaves the flow. Blocks
living in child boxes of an absolute box (a dropdown menu’s <li> items)
used to lay out in flow, reserving huge blank bands before the article; a
flex/grid container inside an absolute subtree slipped past the check
entirely. Pure bt_oof_anchor/bt_oof_root classify each block’s box chain
(cycle-bounded, fail-open); the positioned pass measures and paints the whole
subtree stacked. Wikipedia’s header shed ~2700px of reserved blanks.clip: rect() and the explicit-height flag.
Both were resolved by the worker but never serialized, so the trusted side got
a valid-looking 0×0 clip rect and clipped every positioned box to nothing.block_id/node_id for input runs, so a checkbox-hack control
(position:absolute; opacity:0) painted as a visible ghost checkbox instead
of entering the positioned pipeline.line-height used to win: Wikipedia’s .mw-editsection{line-height:0}
after every heading collapsed the heading’s line and the next paragraph
painted through the glyphs. The line now takes the max leading over its
fragments.Background Enhancements (R5): background-position parsed with keyword
(left/center/right/top/bottom) and pixel value support. Applied as
a Cairo translate offset in the box decoration painter, with center
auto-resolving to (box_w - img_w) / 2. Multi-layer background-image with
comma-separated URLs (url(a.png), url(b.png)) — second layer painted behind
the first with independent sizing, repeat, and position. radial-gradient()
parsed and painted via cairo_pattern_create_radial with center at box center.
Gradient stop positions (e.g. red 20%, blue 80%) now consumed instead of
always-even spacing.
Nested Compositor Tree (R6): stacking-context boxes now recursively compose
their entire subtree as a single offscreen Cairo group. A parent with opacity
properly blends its children (including nested stacking contexts) as one unit —
fixing the previous limitation where child boxes with their own opacity were
composited independently on top of the parent’s group. Implemented via a
recursive paint_nested_children helper that traverses the box tree.
JavaScript Modernization (R7): <script defer> attribute parsed in the HTML
parser and honored via two-pass script execution: sync/external scripts run first,
deferred scripts run after, all sharing the same per-page JS time budget.
customElements.define() now stores constructors in a live registry and fires
connectedCallback during __fireDeferred. Binary arrayBuffer response type
in XHR/fetch: __hostFetch returns raw binary as an ArrayBuffer, and
responseType='arraybuffer' in XHR as well as fetch().arrayBuffer() return
real binary data. DOM events keydown, input, change, blur, and focus
were already fully wired (discovered during the audit).
Pseudo-elements (R8): ::before and ::after parsed in the selector engine
and matched in the CSS cascade (rules like div::before { color: red; } apply
their styles). The content property is parsed (quoted strings) and stored in
css_style. DOM queries correctly reject pseudo-elements (matching browser
behavior). Full content injection into the display list during the page_view
walk is deferred to a follow-up.
allow.conf and js.conf) automatically gets author CSS, images, and
full JS — no per-session toggles needed.<link rel=stylesheet> and <script src>, downloading
them in parallel (up to 4 threads) through the same policy-gated fetcher while
the worker parses. Total wait tends to the maximum latency instead of the sum.gap/justify-content/flex-wrap multi-line/
align-items/align-self, CSS Grid with sized tracks (fr weights, fixed px,
repeat()/minmax(), grid-column: span N), and recursive box-tree layout
with margin collapsing.j/k, gg/G, etc.),
window controls (maximize, minimize, resize, titlebar drag), sepia/light/dark
themes, hover link preview, loading indicator, multi-tab with shortcuts.User-Agent and Accept-Language shared
by all Freedom users — a common Firefox/Linux identity.https:// added, http:// upgraded) or
anything else for a DuckDuckGo HTML (no-JS) search. Dangerous schemes like
javascript: are searched, never executed.js.conf allowlist. Inline
scripts execute in the sandboxed worker against a memory-safe writable DOM..onion routing via SOCKS5h proxy, .i2p via HTTP proxy, both
fail-closed (never leak to clearnet).setTimeout/setInterval with actual delays on a
virtual clock the trusted parent drives (the worker cannot wake itself).Ctrl+P for vector PDF, Ctrl+Shift+P for full-height PNG.Ctrl+S with fail-closed filenames, atomic 0600 writes.Ctrl++/Ctrl+-/Ctrl+0 with 50-300% ladder.margin/padding/width/max-width with box-sizing:
border-box, border-radius, box-shadow (outset and inset), outline,
visibility:hidden, cursor:pointer, text-overflow:ellipsis, word-break,
overflow:hidden clipping.min()/max()/clamp() in lengths, calc() with
dimensional checking, var()/custom properties with depth-capped lookups.margin-inline, padding-block, inset-inline,
inline-size/block-size with physical LTR mapping.@media (prefers-color-scheme: dark) applies
automatically when the dark theme is on.translate(), scale(), rotate(), skew()/skewX()/
skewY(), and matrix() (QR-decomposed at parse time) via real Cairo affine
matrix, pivoted at transform-origin (keywords + percents; default center).
Single function per declaration; matrix() covers the composed case.vw/vh/vmin/vmax resolve against the normalized
1920x1080 desktop (the same fixed identity @media width queries and the
anti-fingerprint screen report), inside calc()/min()/max()/clamp()
and font-size too. min-height:100vh heroes and calc(100vh - 80px)
layouts now work with zero real-window leakage.backdrop-filter: blur(Npx) (+ -webkit- alias) samples
and blurs the already-painted backdrop under the box before its translucent
background composites on top; rgba()/hsla() background alpha is honored
in the box fill (background: rgba(255,255,255,0.25) panels).matchMedia() really evaluates
width/height/orientation queries against the same normalized 1920x1080
identity (identity signals pinned: light scheme, fine pointer, hover);
IntersectionObserver delivers one synthetic isIntersecting:true entry per
observe() in the deferred phase — AOS-style scroll-reveal content that
used to stay stuck at opacity:0 now renders. All values are synthetic
constants: zero real geometry, scroll order, or timing leaks.data: images. All decoded inside the sandboxed
worker.md_pacer brain; the
consumer regulates the whole pipeline through pipe backpressure — no sleeps).
The decoder process sends microsecond PTS with per-segment time bases, the
first HLS segment’s frames are no longer lost to the probe, codecs drain only
at end-of-stream (v1 drained per segment, killing every later segment), and
audio writes to aplay are non-blocking best-effort (the UI thread can never
freeze on a full audio pipe). Verified end-to-end: 3 HLS segments in, 150/150
frames out, monotonic PTS, clean EOS.aplay pipe carries
a ~1 MiB cushion so catch-up bursts are re-timed, not dropped.<video>/<audio> element surface: multiple <source> children
are selected by type (HLS/MPEG-TS/MP4 preferred), fallback content inside
media elements is suppressed (the element renders instead), and page JS gets
an identity-safe HTMLMediaElement facade — play()/pause()/
canPlayType(), attribute-reflected muted/loop/controls, buffered
TimeRanges, new Audio(src) — so player scripts run without throwing and
without touching the network.px/pt/rem sizes that replace (not multiply)
the user-agent size, and headings that inherit their colour<strong>/<em>/<a>/<code>)<svg>: shapes, <path> (arcs included), <g> transforms, currentColor,
viewBox scaling, <text> — always rendered, structurally unable to fetchdata: images with zero network requestbackground-image with linear-gradient(), radial-gradient(),
conic-gradient() (128-sector fan, from <deg>, stop positions in %/deg,
two-position stops = hard edges for pies/stripes), and
url(...) (fetched/decoded under same policy as <img>)background-clip: text / -webkit-background-clip: text +
-webkit-text-fill-color: transparent fills glyphs with the element gradientfilter: drop-shadow(dx dy blur color) — shadow of the group’s real alpha
silhouette (rounded cards, PNG transparency), not the box rectbackground-position, background-size, background-repeat, multi-layerborder-radius on backgrounds, borders, shadows, and outlinesbox-sizing: border-box honoredfilter: blur() and filter: grayscale() parsed, grayscale paintedanimation-duration and @keyframes parsing:nth-of-type(), :first-of-type, :empty, :lang(),
:not()/:is()/:where()::before/::after parsed and matched in CSS cascadeposition: sticky (pins to viewport on scroll)right/bottom insets honored for absolute/fixed positioning, including on
inline tags (position:absolute makes an element a block box, CSS 2.2 §9.7)<script defer> two-pass executioncustomElements.define() with connectedCallbackarrayBuffer in XHR/fetchinnerHTML getter and setterCtrl+P) or PNG (Ctrl+Shift+P)Ctrl+S, fail-closed filenames, 0600)Ctrl++/Ctrl+-/Ctrl+0) and reload (Ctrl+R/F5)!important) — never phones homemargin/padding/width/max-width), text presentation
(font-family, text-transform, letter-spacing, etc.)visibility, cursor, text-overflow, word-break, overflow:hiddenvar()/custom properties@media (prefers-color-scheme: dark))translate(), scale(), rotate() via Cairo affine matrix--dump-console, --dump-dom, --dump-layout , --dump-video.onion/.i2p + torify)Ctrl+D)