8b599dffcda562194faa12e1a95289b7d0640a34 Adjust scoping of interleaved-recalc and detach-layout-tree flags
Triage note: Reorders flag resets (std::optional AutoReset) so the inner-most flag clears before invalidation runs. Fixes crashes (470164324/378189500) reachable via SVG + container/position-try; likely a stale-resource UAF but exploitability unclear from the diff.
The bug at a glance
This is a reentrancy/ordering fix in Blink’s style engine that governs when SVG resource invalidation runs after interleaved container-query / position-try recalc and after layout-tree detach. When the guarding boolean was left set during invalidation, a nested invalidation triggered by the outer one could be deferred rather than performed immediately, leaving stale SVG resource client relationships in place. Stale resource client pointers surviving a detach are the classic precondition for a use-after-free of SVGResource clients or LayoutSVG* objects. It is scored medium rather than high because reaching the deferred-invalidation path requires a specific nesting (invalidation-inside-invalidation during CQ/position-try/detach), the upstream tracker marks it not-known-exploitable, and the concrete failure is a stale-state window rather than a demonstrated controllable free-and-reuse. Two Fixed: IDs (470164324, 378189500) indicate it closes real reported issues, not a speculative refactor.
Blink uses process-wide-ish boolean guards (in_container_query_style_recalc_, in_position_try_style_recalc_, in_detach_scope_) to decide whether SVG resource invalidations should run now or be queued for later. The bug is that these guards were held via base::AutoReset for the entire lifetime of the scope object, so they were still true at the moment the scope’s tail work (PostInterleavedRecalcUpdate / InvalidateSVGResourcesAfterDetach) actually invalidated resources. An invalidation that itself schedules further invalidations then saw the flag set and deferred, so some resource state never got refreshed within the frame it needed to. The angle for an analyst: the vulnerable window is not the invalidation call itself but the fact that the flag outlives the point where invalidation begins.
Root cause
base::AutoReset resets its target only when it is destroyed, i.e. at the end of the enclosing scope, AFTER the trailing invalidation call executes. In ApplyRuleSetAfterContainerRecalc-style code and in the position-try recalc path, the guard was declared as base::AutoReset<bool> cq_recalc(&in_container_query_style_recalc_, true); / pt_recalc(...) and remained true through PostInterleavedRecalcUpdate(container/element). In DetachLayoutTreeScope, in_detach_scope_ was an AutoReset member, so it stayed true through the destructor body which calls InvalidateSVGResourcesAfterDetach(). The patch converts each guard to std::optional<base::AutoReset> constructed in-place, then explicitly calls .reset() to clear the flag immediately before the invalidation entry point. Once the innermost flag is cleared first, a nested invalidation triggered during the tail work observes the flag as false and performs the invalidation immediately instead of deferring it, so stale SVG resource client state is not left behind.
Key code
Reset the innermost guard flag before invoking the invalidation, so nested invalidations run immediately instead of being deferred
- base::AutoReset<bool> cq_recalc(&in_container_query_style_recalc_, true);
+ std::optional<base::AutoReset<bool>> cq_recalc(
+ std::in_place, &in_container_query_style_recalc_, true);
...
+ cq_recalc.reset();
PostInterleavedRecalcUpdate(container);
...
~DetachLayoutTreeScope() {
engine_.MarkForLayoutTreeChangesAfterDetach();
+ in_detach_scope_.reset();
engine_.InvalidateSVGResourcesAfterDetach();
}
Patch walkthrough
third_party/blink/renderer/core/css/style_engine.cc— Two guards are converted from stack AutoReset to std::optional: in_container_query_style_recalc_ in the interleaved container-query recalc path, and in_position_try_style_recalc_ in the position-try recalc path. In each function the optional is reset() right before the trailing PostInterleavedRecalcUpdate(…) call. This ensures the flag corresponding to the current recalc is already cleared when PostInterleavedRecalcUpdate performs SVG resource invalidation, so any invalidation nested inside it is executed rather than queued. The rest of each function is unchanged; the change is purely about the moment the flag drops from true to false.third_party/blink/renderer/core/css/style_engine.h— DetachLayoutTreeScope’s in_detach_scope_ member is changed from base::AutoReset to std::optional<base::AutoReset> constructed with std::in_place. The destructor now calls in_detach_scope_.reset() between MarkForLayoutTreeChangesAfterDetach() and InvalidateSVGResourcesAfterDetach(), so the detach flag is cleared before the SVG invalidation runs. Previously the AutoReset member would only reset after the destructor body finished, i.e. after InvalidateSVGResourcesAfterDetach had already run under the still-set flag.
Background
base::AutoReset — An RAII helper that sets a variable to a new value on construction and restores the old value on destruction. Because restoration is tied to scope/object lifetime, any code that runs before the destructor sees the temporary value. Wrapping it in std::optional lets the author trigger restoration early via reset().
Interleaved recalc — Container queries and CSS anchor positioning (position-try) require Blink to run additional style/layout passes inside an ongoing lifecycle update. These are ‘interleaved’ recalcs guarded by flags so the engine can distinguish nested work from top-level lifecycle work and defer certain follow-up work.
SVG resource invalidation — SVG paints/filters/clips/masks register client relationships (LocalSVGResource clients). When style or the layout tree changes, these must be invalidated so stale client references to detached or restyled objects are dropped. Deferring an invalidation that should happen now can leave a client pointing at an object that is about to be freed.
DetachLayoutTreeScope — An RAII scope in StyleEngine that brackets detachment of layout objects and performs post-detach cleanup (MarkForLayoutTreeChangesAfterDetach, InvalidateSVGResourcesAfterDetach) in its destructor.
Vulnerability window
- introduction — Introduced when the interleaved-recalc and detach guards were implemented with plain AutoReset held for the full scope, coupling flag lifetime to scope lifetime rather than to the invalidation call boundary.
- report — Two internal issues (470164324, 378189500) were filed, indicating observed misbehavior tied to deferred/stale SVG resource invalidation during CQ/position-try/detach.
- fix — 2026-01-08, Fredrik Soderquist landed the scoping change (CL 7368065), resetting the innermost flag before invoking the invalidation function.
Proof of concept
No test was shipped in this commit and no exploit is demonstrated. A trigger recipe consistent with the diff: build an SVG resource (e.g. a filter, clipPath, or mask) referenced by an element that participates in a container query or CSS anchor position-try, then mutate styles so that the interleaved recalc (or a detach) performs an SVG resource invalidation which itself schedules a further invalidation of a related resource client. On the unpatched code the nested invalidation is deferred because the guard is still set, leaving a stale client. Whether that stale state is reachable as a controllable UAF depends on subsequent object teardown ordering; the upstream marks it not-known-exploitable, so this remains a stale-state hardening/correctness fix rather than a proven exploit primitive.
Exploitation
- setup — Attacker page constructs SVG paint servers/filters with client relationships and attaches them to elements under container-query or anchor-positioning layout.
- trigger — Style mutation drives an interleaved recalc or layout-tree detach whose invalidation nests a second invalidation while the guard flag is still true, causing the inner invalidation to be deferred.
- primitive — A stale SVG resource client survives into a state where its referent may be freed; reuse of the stale client would be the UAF. Not demonstrated to be controllable here.
Detection & hunting
For defenders and detection engineers:
- ASAN use-after-free in LocalSVGResource / SVGResourceClient during style recalc — Crashes with SVG resource client stacks under StyleEngine::PostInterleavedRecalcUpdate or InvalidateSVGResourcesAfterDetach on pre-fix builds are candidate matches.
- Rendering glitches with SVG resources under container queries / position-try — Stale filter/clip/mask output after dynamic style changes can be a benign symptom of the same deferred-invalidation defect.
Audit directions
- Other AutoReset-held reentrancy guards in Blink — Grep for base::AutoReset guards whose scope encloses a trailing call that can re-enter the same subsystem; any where the guard must be cleared before the tail call has the same defect shape. Prefer std::optional + early reset().
- SVG resource invalidation deferral paths — Audit every place that checks in_container_query_style_recalc_, in_position_try_style_recalc_, in_detach_scope_ to decide defer-vs-perform, and confirm the flag semantics now match the corrected reset ordering.