Skip to content

Implement AIA fetching, for sites with broken cert chains#6

Merged
deepshekhardas merged 238 commits into
masterfrom
fix-10661
Jul 16, 2026
Merged

Implement AIA fetching, for sites with broken cert chains#6
deepshekhardas merged 238 commits into
masterfrom
fix-10661

Conversation

@deepshekhardas

Copy link
Copy Markdown
Owner

Cherry-pick from upstream PR LadybirdBrowser#10661 by sideshowbarker

awesomekling and others added 30 commits July 11, 2026 16:55
Create initial Qt tabs while constructing a window, then navigate them
after the completed window is shown. Move loading state ownership into
ViewImplementation so both frontends synchronize with loads requested
before their callbacks are attached.

Carry navigation IDs with loading notifications. Only accept terminal
notifications for the active navigation. This prevents a fresh process's
initial about:blank load from racing with a command-line URL.
layout_absolutely_positioned_element() used to reach into the used
values of the box's containing block and force its definite-size flags
on, so that sizes of the abspos box depending on the containing block
would resolve as definite. Since the ContainingBlockConstraints
refactor, every sizing path of an absolutely positioned box receives
its percentage bases explicitly, and its available space is built as
definite sizes from the AbsposContainingBlockInfo rect, so nothing
consults these flags on the containing block anymore.

This mutation was the last direct dependency of
layout_absolutely_positioned_element() on containing block used
values, which is a problem for running partial relayout of absolutely
positioned subtrees, where the containing block sits outside the laid
out subtree and has no used values at all.
The throwaway layout states used for intrinsic size measurement
pre-populated the used values of the measured box's containing block.
Nothing reads that entry anymore: intrinsic sizing runs receive their
percentage bases through ContainingBlockConstraints, abspos layout and
inline containing block resolution are skipped in intrinsic sizing
mode, and the remaining ancestor lookups go through try_get(), which
treats missing ancestors as indefinite. Any overlooked reader would
trip the VERIFY in LayoutState::get() rather than misbehave silently.

Dropping the copies removes the last containing block read that
happened under layout_absolutely_positioned_element(), which matters
for partial relayout of absolutely positioned subtrees: there the
containing block is outside the laid out subtree and has no used
values to copy from.
measure_automatic_content_height() pre-populated its throwaway layout
state with every ancestor that had used values, because flex and grid
static-position handling used to create used values for absolutely
positioned descendants through the lazy used-value creation mechanism,
which tripped the throwaway state's subtree VERIFY when a descendant's
containing block was an ancestor above the measured box.

Since used value creation became explicit, registering an abspos child
only records it on its containing formatting context root without
touching any used values, and a registration target above the measured
subtree root is never processed inside the measuring run, so nothing
reads those ancestor entries anymore. Any overlooked reader would trip
the VERIFY in LayoutState::get() rather than misbehave silently.
Its last callers were the throwaway layout states that copied
containing block or ancestor used values into intrinsic sizing and
content measurement runs, and those copies are gone now.
compute_inline_containing_block_rect() positioned atomic inline
fragments by subtracting two LayoutState::cumulative_offset() calls,
each of which recursively reads used values up the containing block
chain to the root. Since measurement layout states stopped copying
ancestor state, that up-walk reads past the measurement root when a
box establishing an independent formatting context with auto height
and a min-height contains an absolutely positioned element whose
containing block is an inline: the measuring BFC lays out the abspos
child inside the throwaway state, and resolving the inline containing
block rect crashed on the "no used values" VERIFY in LayoutState.

Only the difference between the two cumulative offsets ever mattered,
and it equals the running offset the fragment walk already threads
down: an atomic inline's fragment lives in the line boxes of its
containing block, which is exactly the node being walked when the
fragment is encountered. Handle atomic inline fragments directly in
the line box fragment loop with that offset, instead of reaching them
through the child walk and re-deriving their coordinate space from
the containing_line_box_fragment back-pointer. This removes the last
up-the-tree state reads from this function and makes it purely
top-down.

Adds a crash test that lays out an abspos element against an inline
containing block inside an automatic content height measurement.
Measurement layout states (measure_automatic_content_height and the
intrinsic sizing helpers) are throwaway: nothing in them is committed,
and out-of-flow boxes cannot affect the measured metrics. Yet the
measuring formatting contexts still laid out abspos children, and
resolving their insets against an inline containing block or an
anchor() function walks the containing block chain past the measurement
root, into ancestor state that throwaway states no longer contain. This
crashed on the "no used values" VERIFY;

Give LayoutState an explicit Purpose declared by its creator (Commit
for full layout and SVG partial relayout, Measurement for the seven
throwaway sites) and skip layout_absolutely_positioned_children in
measurement states. cumulative_offset() keeps its strict VERIFY.

Adds a crash test for anchor() inset resolution inside an automatic
content height measurement.
Table cells with percentage heights are laid out twice: once to measure
the content height that row sizing needs (the percentage resolves
against the table height, which isn't known yet), and once for real
after it is. Both layouts ran in the committing LayoutState, with
discard_used_values_for_descendants() erasing the first pass's
descendant state in between so the second pass could recreate it.

The first pass is a measurement in disguise: only the automatic content
height and the first baseline survive it. Run it in a throwaway
Measurement state instead, seeded with a full copy of the cell's used
values so collapsed-border overrides, definite-size flags, and size
constraints match the committing state exactly. The second pass becomes
the one and only inside layout of these cells in the committing state,
and discard_used_values_for_descendants() loses its only caller and is
removed.

With this, every box is created and placed at most once per committing
LayoutState, enforced by the existing VERIFYs in create() and
set_content_offset(). Absolutely positioned children of
percentage-height cells are also no longer laid out twice.
register_contained_abspos_child() tolerated re-registration of the same
box by replacing the stored static position instead of duplicating the
entry. That tolerance existed for the second inside layout of
percentage-height table cells; now that every box is laid out at most
once per layout state, no code path can encounter the same abspos box
twice: block flow, inline static position markers, and the
flex/grid/table notification walks each see a box exactly once per
state, and the table walk prunes at cells, whose contents register
during the cells' own inside layout.
When the navigate event canceled a navigation or intercepted it into a
same-document navigation, navigate() returned with the navigation ID
still stamped as the navigable ongoing navigation, and nothing ever
cleared it. The spec clears it in the queued same-document finalize,
but our synchronous same-document commit path deliberately preserves
navigation IDs it does not own, so the dead ID lingered forever.

Later same-document traversals then hit the stale-traversal guard in
apply the history step and treated themselves as superseded: on reddit,
going back from an SPA-navigated page moved the session history step
without committing anything, leaving the page, URL, and Navigation API
state stuck, and WebDriver waited forever for the dead navigation when
clicking links. Firing another traverse navigate event from that state
aborted the never-finished intercepted event from a traversal task with
no JavaScript on the stack and crashed WebContent, so give
abort_a_navigate_event an execution context as well.

The three new tests cover the intercepted push, canceled push, and
pending-handler abort cases; the first two used to time out and the
third crashed WebContent. Chromium and Firefox agree on the fixed
behavior end to end.
Inline list item boxes were treated as atomic inline boxes even though
their built-in marker children were skipped by the inline iterator. The
list item therefore got a singular paintable and its marker occupied no
space, so content started where the marker should have been.

Treat inline-flow ListItemBox nodes as fragmented inline containers,
emit their built-in marker as an atomic inline item, and share marker
sizing with block layout. Generalize per-line paintables to box-model
nodes so inline list items participate in fragment painting, containing
block selection, stacking, relative positioning, and vertical alignment
like other inline-flow containers.
Previously, a position:fixed element inside <foreignObject> used the
viewport as its containing block. During partial SVG relayout, the
viewport is outside the relayed-out subtree, so the fixed element was
registered as an abspos child of the viewport but never laid out.
LayoutState::commit() then cleared its paintable without rebuilding it,
making getBoundingClientRect() return an empty rect until the next full
layout.

With foreignObject establishing the fixed positioning containing block,
such elements are laid out by the foreignObject's formatting context,
which runs inside the partial SVG relayout pass. This also makes the
viewport pre-population in relayout_svg_root() (added in 9231b54 to
fix a crash in this same scenario) unnecessary, so remove it.
Retain the navigation fetch controller on its resulting document so an
abort can stop the main resource after response headers have committed.
Also stop every active subresource fetch associated with the document
and mark the document unsalvageable when cancellation was necessary.
Handle Escape in both frontends and ask WebContent to abort the active
top-level navigation. Update loading state immediately so the spinner
stops even if no terminal notification follows.

Remember the URL of a stopped load until its document commits. Reload
can then retry a load stopped while about:blank was still active.
Advance frame scheduling from the previous ideal deadline instead of the
actual timer dispatch time. This carries fractional milliseconds between
frames, allowing a 60 Hz target to alternate integer timer delays rather
than rounding every interval up to 17 ms.
Move the final async scroll race check into the compositor when it
receives a frame. WebContent no longer needs two synchronous compositor
round trips around every display list recording operation.
Replace the fixed store pair with a stateful collection. Publish stores
to the UI as vectors while retaining the existing two-buffer depth.

Reserve the initial front store until the UI presents another buffer.
Release older stores after the frontend finishes with them. This removes
front/back acknowledgement assumptions from the compositor protocol.
Compute conservative display-list and visual-context damage. Compare
commands by value and realign inserted or removed sequences. Compare
context ancestry instead of indices to avoid full-page damage.

Track damage per queued store so reused buffers repaint missed changes.
Clip Skia replay to the damage and add focused tests.
Replay partial display lists into a GPU surface matching the damage
rectangle, then copy that result into the shared backing store. This
keeps Skia layers and intermediate effects bounded by the damaged area
instead of the full high-DPI viewport.
Preserve the scheduled state when a pending frame is blocked by GPU work
or backing-store ownership. Rearm the display scheduler so the frame can
use the next vsync instead of restarting the loop after it becomes
unblocked and losing a presentation interval.
Carry each presented frame damage rectangle through compositor IPC and
LibWebView. Expose a frontend hook after accepting a backing store so
native presentation paths can update only the affected content.
The acceptable anchor element rules in css-anchor-position-1 require
an anchor's containing block chain to pass through the positioned
element's containing block: "laid out strictly before" is satisfied
either by sharing the containing block or, recursively, by the element
generating the anchor's containing block being an acceptable anchor
itself. We currently resolve anchor() against any registered element
with a matching name, so anchors outside the containing block resolve
instead of being rejected, the last registrant of a duplicate name
shadows an earlier acceptable one, and specified fallback values are
ignored. When the offset chain between such an anchor and the
containing block crosses a formatting context root that has not been
placed yet (a float, in cases 1 and 2), the resolved insets are
derived from unplaced offsets and are not even self-consistent.

The expectations capture this incorrect behavior; the following commit
implements the containing block scoping and rebaselines them to match
the spec and other engines.
resolve_anchor_insets() resolved anchor() against whatever element the
registry returned last for a name, ignoring the acceptable anchor
element rules entirely. Anchors outside the positioned element's
containing block resolved instead of being rejected, an unacceptable
last registrant shadowed earlier acceptable ones with the same name,
and specified fallback values were ignored. Computing such an anchor's
rect also subtracted two ICB-relative cumulative offsets whose chains
can cross formatting context roots (floats, atomic inlines) that are
not placed yet in the middle of layout, so the resolved insets were
derived from unplaced offsets read as (0, 0).

Implement the containing block scoping part of the rules: "laid out
strictly before" holds either between boxes sharing a containing block
or recursively through the element generating the anchor's containing
block, so the anchor's containing block chain must pass through the
positioned element's containing block. The registry lookup now walks
the candidates in reverse tree order with an acceptability predicate,
making the target anchor element the last acceptable element instead
of the last registered one.

With scoping in place the anchor rect only depends on boxes inside the
containing block's subtree, which are all placed by the time its
absolutely positioned children lay out. The rect is now accumulated
from the anchor up to the containing block, so boxes above it, which
may legitimately be unplaced, are never read.
Problem: The htmlcollection-cache-prune-after-gc.html test was timing
out relatively frequently on the Linux Sanitizer CI runners

Cause: The test forced a full GC after each of 800 child removals. Each
is slow enough on a Sanitizer job that 800 surpassed the 120s timeout.

Fix: Drop the count from 800 to just 100 — which actually reliably still
reproduces the freed-cell-prune crash the test exists to catch. Verified
locally on a Sanitizer build with the guard removed: 100/100 crashes at
100 and 0/50 at 25. So 100 sits well above the reliability floor of ~40,
while cutting the GC work by 8X — and finishing without timing out.
Problem: In CI, already-loaded-image-invalidates-style-and-layout.html
and other reftests that assign an img element’s src to an already-loaded
image intermittently captured the previous image — with the whole image
square painted the old color.

Cause: When an img adopts an image already in available-images list, the
cached-image path in “update the image data” sets the current request’s
image data synchronously — but defers the style and layout invalidation
into a queued element task. A rendering update that runs before that
task — such as the one the src attribute change itself schedules — then
paints the stale layout, because needs-layout hasn’t been set.

Fix: Do sync style and layout invalidation, alongside the sync image-
data update. Only the observably-async steps (restart the animation, set
the current URL, fire the load event) remain in the queued task.
Lexical-based editors such as Reddit's comment composer handle
Backspace on keydown by deleting a character themselves and cancelling
the event, and additionally handle beforeinput deletions the same way
if they occur. The expectations added here capture our current buggy
behavior: we dispatch beforeinput and run the delete editing action
even though the keydown was cancelled, so the editor deletes twice and
a single Backspace removes two characters.
Cancelling a Backspace or Delete keydown must suppress the entire
editing default action, including the beforeinput event. We instead
kept going: beforeinput was dispatched and, unless the page cancelled
that too, the deletion was performed with the input event suppressed.

Editors that handle deletion on keydown and also handle beforeinput
deletions, such as the Lexical editor used by Reddit's comment
composer, then deleted twice: once from their keydown handler and once
from their beforeinput handler, so a single Backspace press removed two
characters. Chrome, Firefox and Safari dispatch no beforeinput and
perform no editing action when the keydown is cancelled.

This effectively reverts the EventHandler part of commit 8f012f4
along with the now-unused DispatchInputEvent plumbing it introduced,
and rebaselines the tests: a cancelled Backspace keydown now leaves the
editor contents alone and dispatches no input events.
Focusing a contenteditable element used to move the cursor to the end
of the last text node in the editing host, or to the host itself when
it had no text descendants. Chrome and Firefox instead place the cursor
at the first valid caret position: the start of the first text node, or
before the first <br> when the host has no text.

The old behavior broke editor frameworks that only reconcile DOM
mutations occurring inside their rendered leaf elements. X's Draft.js
composer renders an empty block as <span data-offset-key><br></span>;
with the cursor anchored at the editing host itself, typed text was
inserted as a new text node directly under the host, where the editor
could not map it back to its model, leaving the model empty and
Backspace inoperative.

The test expectations match Chrome's behavior for text, nested inline,
empty <br> leaf, and multi-block editing hosts.
The expectations capture our current behavior: keyCode and which hold
the virtual key code during keypress events. Other browsers set both to
the character code (e.g. 104 for a keypress of "h"), and frameworks
depend on it: React synthesizes its onBeforeInput text from
String.fromCharCode(event.which) on keypress, so a wrong value corrupts
controlled editors such as X's Draft.js composer.
For keypress events that produce a character, all major browsers set
keyCode (and therefore which, which mirrors it) to the character code
rather than the virtual key code, so a keypress of "h" reports 104
instead of 72. React synthesizes onBeforeInput text on keypress from
String.fromCharCode(event.which), so reporting the virtual key code
gives frameworks the wrong (uppercased) character.

The keypress-legacy-key-attributes test expectations now match the
values reported by other browsers.
kalenikaliaksandr and others added 29 commits July 15, 2026 07:55
PaintableBox::reset_for_relayout() clears the paintable's scroll frame
references, but ViewportPaintable::assign_scroll_frames() only ran in
the full layout path. After a partial relayout, paintables in the
relaid subtree were left without scroll frames, so display list
recording lost scroll offsets and the clip/scroll hierarchy for scroll
containers inside the subtree; the find-in-page text blocks cache was
similarly only invalidated by the full path and went stale.

Reassign scroll frames from the partial relayout branch and invalidate
the text blocks cache the same way the full path does.
LayoutState::commit() detaches the subtree root's old paintable and
build_paint_tree() reattaches the rebuilt one with append_child(), so
a partially relaid subtree whose root was not the last paintable child
moved to the end of its parent's children. Paint and hit-test order
between siblings with equal stacking follow paintable tree order, so
the relaid subtree ended up covering later siblings that a full layout
would paint on top of it - for example, relaying out the first of two
overlapping absolutely positioned siblings put it above the second.

Capture the old paintable's next sibling before detaching it and
splice the rebuilt paint subtree back in at that position.
set_needs_layout_update() returns early when the node is already marked,
assuming the first mark already dirtied the whole ancestor chain. That
assumption breaks for outermost SVG roots registered for partial
relayout: dirt inside the svg marks the root itself but stops
propagating there, so a later invalidation on the root in the same
update cycle, such as a changed CSS size, hits the early return and
never reaches the ancestors. The update then stays confined to the svg
subtree, which relays out with a stale size even though the new size
must reflow the surrounding content.

Marks on an already-dirty outermost SVG root now fall through and
propagate to the ancestors, escalating such updates to a full layout.
Marks on other already-dirty nodes still return early, and the
fall-through walk stops at the first dirty ancestor, so repeated marks
stay cheap.
Partial and full layout each carried their own list of post-commit
refresh steps, and the lists had already drifted: only the full path
re-derived selection state, so a partial relayout inside a subtree
containing selected text dropped the selection highlight until the
next selection change or full layout, because the commit rebuilds the
paintable fragments that carry it.

Refresh every structure derived from committed layout results - scroll
frames, accumulated visual contexts, selection state, the text blocks
cache, the stacking context tree, the display list, and scroll state -
in a single Document::after_layout_commit() called by both paths, so a
layout path cannot forget one. Viewport-rect broadcast and collection
of content-visibility:auto boxes are gated on the layout tree having
changed, which the partial path today never does; upcoming partial
handling of confined layout tree updates will take that variant.
Layout indices key the per-pass used values store in LayoutState. They
used to be assigned only by the dense renumbering at the start of a
full layout pass, leaving nodes created by an incremental layout tree
update stuck at index 0 until the next pass renumbered them. Partial
relayout is about to run layout on freshly rebuilt subtrees without an
intervening full pass, at which point the colliding indices would make
those nodes share used values.

Hand out an index from a document counter at construction instead. The
full pass keeps renumbering the tree densely (the paged used values
store is only compact for the dense range) and resets the counter past
it, so between-pass indices never collide with the dense range.
create_independent_formatting_context_if_needed() and
create_independent_formatting_context() were member functions that
implicitly used `this` as the parent of the newly created context.
Partial relayout needs to create a formatting context for a subtree
root from Document::update_layout(), where there is no parent context
at all.

Make both functions static with an explicit parent parameter and pass
`this` at the existing call sites. Intrinsic sizing helpers can now
call the factory without casting away constness merely to select a
factory object, while still preserving the current context as the
parent.
Partial relayout is limited to SVG roots: the invalidation walk stops
at the first outermost-SVG-root ancestor and records it in an
SVG-typed dirty set on the document. Extending partial relayout to
other kinds of boxes - the goal is using the closest absolutely
positioned box as a boundary - needs the boundary check and the dirty
set to work in terms of generic boxes.

Introduce Box::is_partial_relayout_boundary(), which for now only
returns true for outermost SVG roots, and use it as the stop condition
in the ancestor walk. The SVG-typed dirty set and its registration
function become Document::PartialRelayoutInvalidation, a small type
that owns the attribution of pending updates to boundaries, so the
bookkeeping the dispatch relies on lives in one place. The dispatch in
Document::update_layout() still casts registered roots to SVGSVGBox
since nothing else can qualify yet. No behavior change intended.
The partial relayout dispatch consumed the registered dirty set
directly, trusting that every registered box was still a live, still
qualifying boundary. That only holds because the dispatch runs
exclusively when no layout tree update is pending, and it breaks the
moment partial relayout starts handling structural updates: an
in-place subtree rebuild can detach or replace a registered box
between the invalidation and the dispatch.

Make the dispatch resolve the live boundary set from the tree instead:
filter out roots that did not survive, re-check the boundary predicate
against the tree that is about to be laid out, and route the update to
the full layout path when a root no longer qualifies. Boundaries are
owned by the layout tree, not by the registration, so later structural
partial relayout can re-discover them from rebuilt subtrees instead of
anchoring boxes across the build.
The partial relayout pass hardcoded SVGFormattingContext and only
worked for SVG roots, while the upcoming absolutely positioned
boundaries are Block, Flex and Grid roots. Pick the formatting context
through the static factory instead, and explicitly notify the context
after the run so such roots lay out their contained absolutely
positioned children the same way a parent formatting context would
after dimensioning them in a full layout; that notification is a no-op
for SVGFormattingContext, so SVG behavior is unchanged.

The pass is also split into compute and commit halves, keeping the
subtree layout computation reusable independently of the paintable
tree mutation that publishes it.
layout_absolutely_positioned_element() receives everything it consumes
from outside the element's subtree as two plain values: the containing
block info and the fully resolved static position rect. Partial
relayout will need to save those values during a full layout pass and
replay them later to re-run absolutely positioned layout without
re-running the ancestor formatting context, which requires them to be
one persistable unit.

Bundle them into an AbsposLayoutInputs struct, and move the types it
is built from (Alignment, AbsposAxisMode, AbsposContainingBlockInfo,
StaticPositionRect) to a standalone header so LayoutState can hold the
struct without including FormattingContext.h. No functional change.
Replaying an absolutely positioned box's layout without running its
ancestor formatting context needs the inputs that context resolved for
the box. Record the AbsposLayoutInputs into the box's used values when
the absolutely positioned element algorithm runs in a Normal-mode
pass, and have LayoutState::commit() transfer them onto the
Layout::Box: throwaway measurement states never commit, so their
recordings are discarded, and a committing pass that no longer lays a
box out as absolutely positioned clears the stale copy.

An in-place layout tree update that replaces a box carries the saved
inputs over to the replacement: it represents the same element in the
same tree position, so the inputs remain those of the last committed
layout. Nothing consumes the saved inputs yet.
A box whose subtree contains an absolutely or fixed positioned
descendant with a containing block outside that subtree cannot become
a partial relayout boundary: the descendant is laid out by a
formatting context outside the subtree (the one rooted at the first
formatting-context-creating box on the containing block chain), so the
subtree cannot be laid out in isolation. The classic case is a
position:fixed descendant, whose containing block is the viewport,
escaping every box below it.

Record that fact as a flag on every box strictly between an
out-of-flow box and its containing block, derived in the same
pre-order walk that recomputes containing block pointers before a full
layout pass: the walk clears each box's flag on visit and out-of-flow
boxes then mark their ancestors, so the flags always describe exactly
the tree the pass is about to lay out. Boundary qualification consumes
the flag; for the SVG roots that qualify today nothing can escape
(foreignObject establishes both kinds of containing block), so this
becomes observable once absolutely positioned boxes become boundaries.
The escaped-by-abspos facts feeding partial relayout boundary
qualification are derived from containing block pointers, and those
are only recomputed by a full layout pass. A style change that makes
an element gain or lose a containing block for absolutely/fixed
positioned descendants (transform and friends, perspective,
transform-style, filter, backdrop-filter, contain, container-type,
will-change) moves where those descendants' layout happens without
updating either the pointers or the derived facts - most of these
properties do not even dirty layout by themselves.

Classify such value flips during style invalidation, mirroring how the
stacking-context bit only reacts to establishment-relevant changes so
that e.g. transform animations between two non-none values stay
unaffected, and record them in a sticky escape bit on the document's
partial relayout invalidation, routing the next layout to the full
path, which re-derives every fact and clears the bit. Both appliers of
computed invalidations honor the new bit: the style recalculation path
and the animated-style update path, which applies invalidations
without a full style recalculation.

Display, position and content-visibility changes already force an
in-place subtree rebuild, whose handling re-derives the facts for the
rebuilt subtree, so they need no classification here.
In-place layout tree updates can replace a node while its containing
block's committed inline paint data remains in use. The flat fragment
and inline-box-piece lists then retain weak references to the old
node, leaving dead entries whenever a subtree relayout skips the
containing block - which is exactly what a partial relayout of a
replaced inline-level boundary is going to do.

Repoint both kinds of inline paint data at the replacement during the
swap. The new node represents the same element at the same tree
position, so the committed geometry remains valid until the containing
block is rebuilt.
An incremental layout tree build replaces dirty subtrees in place, but
some of its work can mutate the tree outside the subtree being
replaced. Partial relayout of structural updates is only sound when
every mutation stayed inside a relayout boundary, so it needs to know
which subtrees were rebuilt and whether anything landed outside them.

Track the root of the in-place subtree replacement currently being
built, record every such root, and flag mutations outside the current
rebuild root: an inline/block mix wrapping an ancestor's inline
children into a fresh anonymous block, insertion parents above the
rebuild root (inline ancestors are skipped, and out-of-flow boxes can
join a trailing anonymous sibling), fresh subtrees that replace no box
in place and so belong to no rebuild root (a ShadowRoot direct child or
a top-layer element revealed from display:none marks itself), and the
two ::backdrop maintenance paths (the ::backdrop box is inserted as a
sibling of a replaced box, and its removal runs before the element's
own rebuild root is even established). The report is exposed on
TreeBuilder and not consumed by anything yet.
Geometry alone cannot tell which path handled an update: a partial
relayout is only correct if it reproduces exactly what a full layout
would have computed, so a silent fall-through to the full path is
invisible in layout results.

Count partial and full layout passes on the document and expose them
as internals.partialLayoutCount() and internals.fullLayoutCount(), so
the path that handled a given update becomes directly observable.
Each test mutates something in or around an absolutely positioned box
and records the resulting geometry together with the partial/full
layout pass counters. The expectations capture today's behavior: the
geometry is already correct, but nearly every update takes a full
layout pass, so tests asserting a partial pass currently end in FAIL.

The following commits extend partial relayout to absolutely positioned
boundaries case by case and rebaseline the tests they affect, so each
expectation diff shows exactly which updates that commit moves off the
full layout path. Tests asserting that an update must stay on the full
path (an escaping fixed-position descendant, top-layer changes, anchor
positioning in use anywhere in the document) pass as-is and stay
untouched throughout.
Let the invalidation walk stop at qualifying absolutely positioned
boxes, so a layout invalidation inside such a box re-lays out only its
subtree instead of the whole document. A box qualifies when it creates
a Block, Flex or Grid formatting context, no absolutely or fixed
positioned descendant escapes it, and the inputs of its own layout
were saved by the last committing pass. Unlike in-flow SVG roots,
whose geometry is frozen, the boundary re-resolves its own size and
position on every partial relayout by replaying the absolutely
positioned element algorithm from the saved inputs, hosted by a
context type that exists for exactly that (the algorithm is defined on
the FormattingContext base and consumes nothing from the concrete
context running it): the inputs pin everything the algorithm reads
from outside the subtree, so the replay reproduces exactly what the
ancestor formatting context computed, without laying out anything
outside. An absolutely positioned outermost SVG root qualifies through
this replay path like any other absolutely positioned box.

Because replay re-solves the boundary's own size, content-dependent
sizing (shrink-to-fit, intrinsic min/max constraints, aspect-ratio)
and visible overflow do not disqualify a boundary - leaked content
reaches ancestors only through their scrollable overflow, which is
recomputed after the subtree commit. This keeps the common overlay
patterns - dropdowns, tooltips, dialogs, positioned panels - on the
partial path when their contents change.

CSS anchor positioning is left out of scope: anchor names publish
geometry that anchor() functions on positioned boxes anywhere in the
document consume, and only a full layout pass re-resolves all of them.
The dispatch refuses partial relayout while any anchor name is
registered, an element whose registered names all vanish between
layouts sets the escape bit (the dispatch-time check reads the live
name maps, which no longer know them), and a box whose own inset
properties carry anchor() functions never qualifies as a boundary,
since only a full layout pass resolves those insets to plain values.
Structural DOM changes always forced a full layout because a layout
tree rebuild could restructure anything. In-place subtree rebuilds now
report their confinement, so insertions, removals and moves whose
effects stay inside a partial relayout boundary re-lay out only that
boundary's subtree.

The partial path runs the incremental tree build itself, walks each
rebuilt subtree to assign containing blocks to the new nodes and
re-derive the escaped-by-abspos facts (catching e.g. a freshly
inserted position:fixed box whose containing block is outside the
boundary), and collects the boundary set from the post-build tree: the
registered boundaries that survived the build plus the nearest
boundary containing each rebuilt subtree. Updates that cannot be
attributed to a boundary - a rebuilt subtree with no containing
boundary, a dirty DOM node whose layout node is detached, or anything
TreeBuilder reports as escaping its rebuild roots, which covers
top-layer relocation and ::backdrop maintenance - take the full layout
path. A container-relative length resolved against a container whose
box has no paintable yet registers it for evaluation even while a
layout pass is running, since a partial pass reports layout as up to
date; the follow-up pass then takes the full path and resolves the
container's size. Structural commits also refresh the cached
containing-block membership used by scrollable-overflow measurement,
since the rebuild can replace boxes it references. And because every
rebuilt subtree allocates fresh layout node indices while only a full
pass renumbers densely, repeated structural partial relayouts would
grow the per-pass used values store without bound; once between-pass
allocation outgrows the dense range, the next layout takes the full
path, which compacts the indices again.
A style change that needs relayout but no tree rebuild on an
absolutely positioned boundary used to dirty the root and force a full
layout, even though replay re-resolves the boundary's own size and
position from the saved layout inputs on every partial relayout
anyway. Register the boundary itself instead of walking its ancestors
and re-lay out only its subtree: this turns inset, size, margin and
padding mutations on boundary-eligible boxes - the classic left/top
animation pattern - into partial relayouts, with ancestor scrollable
overflow recomputed along the containing block chain since a moved or
resized boundary changes it.

Most saved inputs survive a boundary-self style change untouched: the
containing block rect and the static position rect derive from layout
outside the subtree, and the axis modes are recomputed from the live
insets at replay. Inputs whose parts were derived from the box's own
computed values record that fact where they are derived - grid
placement produces the containing block rect and alignments from
grid-row/column and self-alignment, and a flex container derives the
static position rect's alignment from the box's own align-self - and
such saved inputs keep the full layout path. A style change whose
relayout effect reaches the element's ::backdrop also keeps the full
path: the backdrop's box is a sibling of the element's box, outside
the subtree a boundary-self relayout covers.
Structural changes directly on a boundary element - appendChild or
removeChild on the boundary, or text insertion that churns its
anonymous wrappers - still took the full layout path, because
TreeBuilder replaces the dirty element's layout box and partial
relayout could not survive losing the box whose saved layout inputs
and paintable it works from. Register the boundary itself for such
whitelisted structural reasons without walking its ancestors: child
list mutations cannot change the boundary's computed values, so its
qualification and box kind are unaffected, and the in-place
replacement cannot require restructuring surrounding anonymous
siblings (which is also why the anonymous-parent escalation is skipped
for this case).

After the incremental build, the replaced boundary box is re-discovered
through the rebuilt subtree roots and accepted: the saved layout inputs
carried over to the replacement drive the replay, an SVG root's
geometry is materialized from the previous paintable (still referenced
by the DOM node until the next commit), and LayoutState::commit()
gains an overload taking the paintable to replace, so the fresh paint
subtree is spliced in at exactly its position - the box no longer owns
that paintable, which the plain overload relies on. A replaced
boundary applies the saved-input validity check unconditionally, since
the change that drove the replacement can no longer be classified.
Accumulate only visual offsets from sticky ancestors when positioning a
nested sticky box. Do not include the nearest scrolling ancestor because
its offset is already represented by the scrollport rectangle; including
it applies the prior scroll position twice on subsequent updates.

Mirror the calculation in the compositor and cover consecutive viewport
scroll updates with a reference test.
The old behavior was to fail `wpt` by providing it with the name of a
test that does not exist, which necessitated `|| true`. This hid venv
setup failures though, so that was less than ideal.

This new approach uses `--list-tests` which does not cause `wpt` to exit
with a non-zero status code, but still sets up the venv and allows setup
failures to propagate.

Remove `--skip-venv-setup` from the individual chunk runners since that
is still required to _activate_ the venv.
A browser launched by WebDriver must always run as its own process,
even if another instance of the browser owns the same profile.
Previously, each browser instance launched by WebDriver used its own
temporary profile. These profiles were leaked whenever a browser did
not exit cleanly.

Browsers launched by the same WebDriver process now share a single
profile, which is removed when the WebDriver process exits.
Profiles created during a WPT run are now grouped under a single
directory, which is removed when the run ends. Profile roots leaked by
unclean exits are removed at the start of the next run.
This implements TLS AIA-fetching support — by doing the following:

1. Install a cert-verification callback that, on a failure, records URLs
   from the AIA extension of any cert whose issuer is missing.
2. Make RequestServer fetch each missing intermediate on a fresh handle.
3. Parse each such intermediate, cache it, and then retry the request.

Otherwise, without this change, some sites reachable in other browsers
(which already do this) are unreachable in Ladybird — because we fail to
load those whose responses have missing intermediate CA certs, even when
a missing intermediate is pointed at by an AIA extension.

Note: On retry the fetched intermediates are only ever offered to chain-
building as untrusted certs. The completed chain must still validate to
a locally-trusted root; thus, this doesn’t introduce new/specious trust.

Fixes LadybirdBrowser#10520
Cover the two AIA-parsing primitives directly: extracting http caIssuers
URLs from a cert’s AIA extension (https URLs skipped, capped per-cert),
and parsing a fetched response body as a DER cert, a PKCS#7 certs-only
bundle, or PEM. The test compiles AIA.cpp into its own binary, and links
OpenSSL directly — rather than the whole RequestServer service.
Drive the full AIA-fetch pipeline against real TLS. The http-test-server
fixture now generates a root/intermediate/leaf certificate chain, and
serves HTTPS hosts that present a broken chain (leaf only); test-web
trusts the generated root via its request-server certificate option.
@deepshekhardas
deepshekhardas merged commit 9520563 into master Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants