Low chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in CSS
DescriptionType Confusion in CSS
ComponentCSS
Bug ClassType Confusion
Tracker490023239
Fix commitdff61fda8560 (chromium/src) +77/-1
CISA KEVNot listed
CreditedSyn4pse
Disclosed2026-04-07

Files Changed

  • third_party/blink/renderer/core/css/css_scope_rule.cc
  • third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach-expected.txt
  • third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach.js
From dff61fda8560b68af87c699fa2ef4345b0ed2cce Mon Sep 17 00:00:00 2001
From: Steinar H. Gunderson <[email protected]>
Date: Fri, 06 Mar 2026 04:42:22 -0800
Subject: [PATCH] Fix CSSOM wrapper desync on setScopeText().

If setScopeText() was called on a @scope rule, especially a nested
scope rule, we'd only update the CSSOM wrapper, not the actual
StyleRule in the style sheet. This could cause type confusion when
modifying the StyleRule further. (This was hardened to a CHECK
failure in the previous CL related to this bug, changing to To<>
instead of static_cast.)

setScopeText() is only available to devtools (including extensions
with devtools permissions).

Fixed: 490023239
Change-Id: I3c64ede444c89144126acb75d854b9ac3b23d0ed
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7641210
Commit-Queue: Steinar H Gunderson <[email protected]>
Reviewed-by: Anders Hartvoll Ruud <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1595323}
---

diff --git a/third_party/blink/renderer/core/css/css_scope_rule.cc b/third_party/blink/renderer/core/css/css_scope_rule.cc
index 87e0758..2a99fc5 100644
--- a/third_party/blink/renderer/core/css/css_scope_rule.cc
+++ b/third_party/blink/renderer/core/css/css_scope_rule.cc
@@ -9,6 +9,7 @@
 #include "third_party/blink/renderer/core/css/css_style_sheet.h"
 #include "third_party/blink/renderer/core/css/parser/css_parser_context.h"
 #include "third_party/blink/renderer/core/css/style_rule.h"
+#include "third_party/blink/renderer/core/css/style_sheet_contents.h"
 #include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
 
 namespace blink {
@@ -113,8 +114,16 @@
         child_rule->Clone(new_style_scope->RuleForNesting(),
                           /*mixin_parameter_bindings=*/nullptr));
   }
-  group_rule_ = MakeGarbageCollected<StyleRuleScope>(
+  StyleRuleScope* new_group_rule = MakeGarbageCollected<StyleRuleScope>(
       *new_style_scope, std::move(new_child_rules));
+
+  if (parentStyleSheet()) {
+    parentStyleSheet()->Contents()->ReplaceRuleIfExists(group_rule_,
+                                                        new_group_rule,
+                                                        /*position_hint=*/0);
+  }
+
+  Reattach(new_group_rule);
 }
 
 StyleRuleScope& CSSScopeRule::GetStyleRuleScope() {
diff --git a/third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach-expected.txt b/third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach-expected.txt
new file mode 100644
index 0000000..829cb80
--- /dev/null
+++ b/third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach-expected.txt
@@ -0,0 +1,12 @@
+Tests CSS.setScopeText with scope nested in a rule.
+
+Running test: testNestedEdit
+==== Style sheet text ====
+
+  .outer {
+    @scope (.b) {
+      .inner { color: red; }
+    }
+  }
+
+
diff --git a/third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach.js b/third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach.js
new file mode 100644
index 0000000..3c75080f
--- /dev/null
+++ b/third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach.js
@@ -0,0 +1,55 @@
+/* https://crbug.com/490023239 */
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+  const {session, dp} = await testRunner.startHTML(`
+<!DOCTYPE html>
+<style>
+  .outer {
+    @scope (.a) {
+      .inner { color: red; }
+    }
+  }
+</style>
+<div class="outer"><div class="inner" id="t">x</div></div>`, 'Tests CSS.setScopeText with scope nested in a rule.');
+
+  const CSSHelper = await testRunner.loadScript('../resources/css-helper.js');
+  const cssHelper = new CSSHelper(testRunner, dp);
+
+  dp.DOM.enable();
+  dp.CSS.enable();
+
+  const event = await dp.CSS.onceStyleSheetAdded();
+  const styleSheetId = event.params.header.styleSheetId;
+  const setScopeText = cssHelper.setScopeText.bind(cssHelper, styleSheetId, false);
+
+  testRunner.runTestSuite([
+    async function testNestedEdit() {
+      await setScopeText({
+        range: {
+          startLine: 2,
+          startColumn: 11,
+          endLine: 2,
+          endColumn: 15,
+        },
+        text: '(.b)',
+      });
+
+      await session.evaluateAsync(`
+(() => {
+  const sheet = document.styleSheets[0];
+  const outer = sheet.cssRules[0];
+  const scope = outer.cssRules[0];
+
+  scope.deleteRule(0);
+  scope.insertRule('@media all {}', 0);
+  const media = scope.cssRules[0];
+
+  outer.selectorText = '.outer2';
+  media.insertRule('.boom {}', 0);
+
+  getComputedStyle(document.getElementById('t')).color;
+  document.body.offsetTop;
+})()
+`);
+    },
+  ]);
+})
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach-expected.txt b/third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach-expected.txt
new file mode 100644
index 0000000..829cb80
--- /dev/null
+++ b/third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach-expected.txt
@@ -0,0 +1,12 @@
+Tests CSS.setScopeText with scope nested in a rule.
+
+Running test: testNestedEdit
+==== Style sheet text ====
+
+  .outer {
+    @scope (.b) {
+      .inner { color: red; }
+    }
+  }
+
+
diff --git a/third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach.js b/third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach.js
new file mode 100644
index 0000000..3c75080f
--- /dev/null
+++ b/third_party/blink/web_tests/inspector-protocol/css/css-set-scope-text-reattach.js
@@ -0,0 +1,55 @@
+/* https://crbug.com/490023239 */
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+  const {session, dp} = await testRunner.startHTML(`
+<!DOCTYPE html>
+<style>
+  .outer {
+    @scope (.a) {
+      .inner { color: red; }
+    }
+  }
+</style>
+<div class="outer"><div class="inner" id="t">x</div></div>`, 'Tests CSS.setScopeText with scope nested in a rule.');
+
+  const CSSHelper = await testRunner.loadScript('../resources/css-helper.js');
+  const cssHelper = new CSSHelper(testRunner, dp);
+
+  dp.DOM.enable();
+  dp.CSS.enable();
+
+  const event = await dp.CSS.onceStyleSheetAdded();
+  const styleSheetId = event.params.header.styleSheetId;
+  const setScopeText = cssHelper.setScopeText.bind(cssHelper, styleSheetId, false);
+
+  testRunner.runTestSuite([
+    async function testNestedEdit() {
+      await setScopeText({
+        range: {
+          startLine: 2,
+          startColumn: 11,
+          endLine: 2,
+          endColumn: 15,
+        },
+        text: '(.b)',
+      });
+
+      await session.evaluateAsync(`
+(() => {
+  const sheet = document.styleSheets[0];
+  const outer = sheet.cssRules[0];
+  const scope = outer.cssRules[0];
+
+  scope.deleteRule(0);
+  scope.insertRule('@media all {}', 0);
+  const media = scope.cssRules[0];
+
+  outer.selectorText = '.outer2';
+  media.insertRule('.boom {}', 0);
+
+  getComputedStyle(document.getElementById('t')).color;
+  document.body.offsetTop;
+})()
+`);
+    },
+  ]);
+})
Loading diff…

Original Bug Report

reported by [email protected]

Type confusion in InspectorCSSAgent::setScopeText

Summary

CSS.setScopeText (via InspectorCSSAgent::setScopeText) desynchronizes nested @scope wrappers: CSSScopeRule::SetPreludeText mutates only wrapper-local group_rule_, and later index-based reattach calls CSSGroupingRule::Reattach with a non-group StyleRule*, leading to type confusion and the memory corruption.

> NOTE: this is a resubmission of the report 488270255, since the previous issue is repurposed.

Details

CSS.setScopeText edits the prelude of an existing @scope rule by calling InspectorStyleSheet::SetScopeRuleText, which locates the existing CSSScopeRule wrapper and invokes CSSScopeRule::SetPreludeText.

The key problem is that CSSScopeRule::SetPreludeText replaces the wrapper’s internal group_rule_ pointer with a newly allocated StyleRuleScope, but does not replace the corresponding StyleRuleScope inside the stylesheet’s rule graph. This means the wrapper becomes detached from the real sheet contents, yet still participates in subsequent CSSOM calls and later Reattach() cascades (which assume wrapper/rule index+type stability).

In CSSScopeRule::SetPreludeText, the mutation ends by assigning a new rule object only to the wrapper field:

  HeapVector<Member<StyleRuleBase>> new_child_rules;
  new_child_rules.ReserveInitialCapacity(
      GetStyleRuleScope().ChildRules().size());
  for (StyleRuleBase* child_rule : GetStyleRuleScope().ChildRules()) {
    new_child_rules.push_back(
        child_rule->Clone(new_style_scope->RuleForNesting(),
                          /*mixin_parameter_bindings=*/nullptr));
  }
  group_rule_ = MakeGarbageCollected<StyleRuleScope>(
      *new_style_scope, std::move(new_child_rules));

After this, page JS can mutate the stale CSSScopeRule wrapper (e.g. deleteRule() then insertRule('@media all {}', 0)) such that the wrapper’s child_rule_cssom_wrappers_ at index 0 becomes a CSSMediaRule, while the stylesheet-backed @scope still has a style rule at index 0.

Later, a parent rule mutation that performs a replacement+reattach (e.g. CSSStyleRule::setSelectorText) reattaches nested wrappers by index. During that reattach, the stale scope wrapper forwards ChildRules()[0] (a StyleRule*) into the CSSMediaRule wrapper’s Reattach(). CSSMediaRule inherits CSSGroupingRule::Reattach, which performs an unchecked cast:

In CSSGroupingRule::Reattach, the downcast is a static_cast without runtime validation:

void CSSGroupingRule::Reattach(StyleRuleBase* rule) {
  DCHECK(rule);
  group_rule_ = static_cast<StyleRuleGroup*>(rule);
  for (unsigned i = 0; i < child_rule_cssom_wrappers_.size(); ++i) {
    if (child_rule_cssom_wrappers_[i]) {
      child_rule_cssom_wrappers_[i]->Reattach(
          group_rule_->ChildRules()[i].Get());
    }
  }
}

Once the wrapper’s group_rule_ is miscast to a StyleRuleGroup*, subsequent CSSOM operations on that wrapper (notably insertRule) can write through the misinterpreted layout, corrupting the real StyleRule object fields. A reliable symptom is that later style resolution (RuleSet construction) crashes in RuleSet::AddStyleRule/AddChildRules while iterating nested rules, consistent with a corrupted StyleRule::ChildRules() pointer.

Bisection

This issue is introduced by the commit: ed46557e198a8fce6c1ef52b38e5c17734c99ba9 [css-nesting] Implement CSSScopeRule::SetPreludeText by rule replacement.

Reproduction

Download the chrome from https://storage.googleapis.com/chromium-browser-asan/linux-release/asan-linux-release-1591355.zip

Run the chrome with the attached extension:

./chrome --load-extension=/path/to/ext --no-sandbox

You would observe the ASAN stack trace shown in the asan.txt

Suggested Fix

Harden CSSGroupingRule::Reattach against type mismatches by validating rule is a StyleRuleGroup before assigning it to group_rule_ (e.g. DynamicTo<StyleRuleGroup>).

View on issue tracker