CVE-2026-13838
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/css/css_font_feature_values_rule.cc |
modified |
Files Changed
third_party/blink/renderer/core/css/css_font_feature_values_map.hthird_party/blink/renderer/core/css/css_font_feature_values_rule.ccthird_party/blink/renderer/core/css/css_font_feature_values_rule.hthird_party/blink/renderer/core/css/css_font_feature_values_rule.idlthird_party/blink/web_tests/external/wpt/css/css-fonts/font-feature-values-map-live.html
Patch
From ca29db2f56d3ccefd0820157a1bbd5b4d35fca77 Mon Sep 17 00:00:00 2001
From: Rune Lillesveen <futhark@chromium.org>
Date: Wed, 27 May 2026 03:24:10 -0700
Subject: [PATCH] Make readonly attributes on CSSFontFeatureValuesRule [SameObject]
This is based on the proposal in [1] and makes it straightforward to
keep the CSSOM wrappers work for setting/getting including wrapper
reattachments that happen when shared stylesheets are decoupled on
modifications.
[1] https://github.com/w3c/csswg-drafts/issues/13953
Bug: 514445398, 515494291
Change-Id: I5436e4feeff3935f1999630d775ad34a8a26194f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7865910
Reviewed-by: Dominik Röttsches <drott@chromium.org>
Commit-Queue: Rune Lillesveen <futhark@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1636821}
---
diff --git a/third_party/blink/renderer/core/css/css_font_feature_values_map.h b/third_party/blink/renderer/core/css/css_font_feature_values_map.h
index fa15bd43..ea55b11bb 100644
--- a/third_party/blink/renderer/core/css/css_font_feature_values_map.h
+++ b/third_party/blink/renderer/core/css/css_font_feature_values_map.h
@@ -37,6 +37,12 @@
CSSFontFeatureValuesMap(const CSSFontFeatureValuesMap&) = delete;
CSSFontFeatureValuesMap& operator=(const CSSFontFeatureValuesMap&) = delete;
+ void Reattach(StyleRuleFontFeatureValues* style_rule_font_feature_values,
+ FontFeatureAliases* aliases) {
+ backing_style_rule_ = style_rule_font_feature_values;
+ aliases_ = aliases;
+ }
+
// IDL attributes / methods
uint32_t size() const;
diff --git a/third_party/blink/renderer/core/css/css_font_feature_values_rule.cc b/third_party/blink/renderer/core/css/css_font_feature_values_rule.cc
index 099503b..0611419 100644
--- a/third_party/blink/renderer/core/css/css_font_feature_values_rule.cc
+++ b/third_party/blink/renderer/core/css/css_font_feature_values_rule.cc
@@ -42,33 +42,51 @@
}
CSSFontFeatureValuesMap* CSSFontFeatureValuesRule::annotation() {
- return MakeGarbageCollected<CSSFontFeatureValuesMap>(
- this, font_feature_values_rule_,
- font_feature_values_rule_->GetAnnotation());
+ if (!annotation_) {
+ annotation_ = MakeGarbageCollected<CSSFontFeatureValuesMap>(
+ this, font_feature_values_rule_,
+ font_feature_values_rule_->GetAnnotation());
+ }
+ return annotation_.Get();
}
CSSFontFeatureValuesMap* CSSFontFeatureValuesRule::ornaments() {
- return MakeGarbageCollected<CSSFontFeatureValuesMap>(
- this, font_feature_values_rule_,
- font_feature_values_rule_->GetOrnaments());
+ if (!ornaments_) {
+ ornaments_ = MakeGarbageCollected<CSSFontFeatureValuesMap>(
+ this, font_feature_values_rule_,
+ font_feature_values_rule_->GetOrnaments());
+ }
+ return ornaments_.Get();
}
CSSFontFeatureValuesMap* CSSFontFeatureValuesRule::stylistic() {
- return MakeGarbageCollected<CSSFontFeatureValuesMap>(
- this, font_feature_values_rule_,
- font_feature_values_rule_->GetStylistic());
+ if (!stylistic_) {
+ stylistic_ = MakeGarbageCollected<CSSFontFeatureValuesMap>(
+ this, font_feature_values_rule_,
+ font_feature_values_rule_->GetStylistic());
+ }
+ return stylistic_.Get();
}
CSSFontFeatureValuesMap* CSSFontFeatureValuesRule::swash() {
- return MakeGarbageCollected<CSSFontFeatureValuesMap>(
- this, font_feature_values_rule_, font_feature_values_rule_->GetSwash());
+ if (!swash_) {
+ swash_ = MakeGarbageCollected<CSSFontFeatureValuesMap>(
+ this, font_feature_values_rule_, font_feature_values_rule_->GetSwash());
+ }
+ return swash_.Get();
}
CSSFontFeatureValuesMap* CSSFontFeatureValuesRule::characterVariant() {
- return MakeGarbageCollected<CSSFontFeatureValuesMap>(
- this, font_feature_values_rule_,
- font_feature_values_rule_->GetCharacterVariant());
+ if (!character_variant_) {
+ character_variant_ = MakeGarbageCollected<CSSFontFeatureValuesMap>(
+ this, font_feature_values_rule_,
+ font_feature_values_rule_->GetCharacterVariant());
+ }
+ return character_variant_.Get();
}
CSSFontFeatureValuesMap* CSSFontFeatureValuesRule::styleset() {
- return MakeGarbageCollected<CSSFontFeatureValuesMap>(
- this, font_feature_values_rule_,
- font_feature_values_rule_->GetStyleset());
+ if (!styleset_) {
+ styleset_ = MakeGarbageCollected<CSSFontFeatureValuesMap>(
+ this, font_feature_values_rule_,
+ font_feature_values_rule_->GetStyleset());
+ }
+ return styleset_.Get();
}
String CSSFontFeatureValuesRule::cssText() const {
@@ -115,10 +133,41 @@
void CSSFontFeatureValuesRule::Reattach(StyleRuleBase* rule) {
DCHECK(rule);
font_feature_values_rule_ = To<StyleRuleFontFeatureValues>(rule);
+ if (annotation_) {
+ annotation_->Reattach(font_feature_values_rule_.Get(),
+ font_feature_values_rule_->GetAnnotation());
+ }
+ if (ornaments_) {
+ ornaments_->Reattach(font_feature_values_rule_.Get(),
+ font_feature_values_rule_->GetOrnaments());
+ }
+ if (stylistic_) {
+ stylistic_->Reattach(font_feature_values_rule_.Get(),
+ font_feature_values_rule_->GetStylistic());
+ }
+ if (swash_) {
+ swash_->Reattach(font_feature_values_rule_.Get(),
+ font_feature_values_rule_->GetSwash());
+ }
+ if (character_variant_) {
+ character_variant_->Reattach(
+ font_feature_values_rule_.Get(),
+ font_feature_values_rule_->GetCharacterVariant());
+ }
+ if (styleset_) {
+ styleset_->Reattach(font_feature_values_rule_.Get(),
+ font_feature_values_rule_->GetStyleset());
+ }
}
void CSSFontFeatureValuesRule::Trace(blink::Visitor* visitor) const {
visitor->Trace(font_feature_values_rule_);
+ visitor->Trace(annotation_);
+ visitor->Trace(ornaments_);
+ visitor->Trace(stylistic_);
+ visitor->Trace(swash_);
+ visitor->Trace(character_variant_);
+ visitor->Trace(styleset_);
CSSRule::Trace(visitor);
}
diff --git a/third_party/blink/renderer/core/css/css_font_feature_values_rule.h b/third_party/blink/renderer/core/css/css_font_feature_values_rule.h
index ae054f3..9cf308e 100644
--- a/third_party/blink/renderer/core/css/css_font_feature_values_rule.h
+++ b/third_party/blink/renderer/core/css/css_font_feature_values_rule.h
@@ -43,6 +43,12 @@
CSSRule::Type GetType() const override { return kFontFeatureValuesRule; }
Member<StyleRuleFontFeatureValues> font_feature_values_rule_;
+ Member<CSSFontFeatureValuesMap> annotation_;
+ Member<CSSFontFeatureValuesMap> ornaments_;
+ Member<CSSFontFeatureValuesMap> stylistic_;
+ Member<CSSFontFeatureValuesMap> swash_;
+ Member<CSSFontFeatureValuesMap> character_variant_;
+ Member<CSSFontFeatureValuesMap> styleset_;
};
template <>
diff --git a/third_party/blink/renderer/core/css/css_font_feature_values_rule.idl b/third_party/blink/renderer/core/css/css_font_feature_values_rule.idl
index 436f78d..4ecacc2 100644
--- a/third_party/blink/renderer/core/css/css_font_feature_values_rule.idl
+++ b/third_party/blink/renderer/core/css/css_font_feature_values_rule.idl
@@ -7,12 +7,12 @@
interface CSSFontFeatureValuesRule : CSSRule {
attribute CSSOMString fontFamily;
- readonly attribute CSSFontFeatureValuesMap annotation;
- readonly attribute CSSFontFeatureValuesMap ornaments;
- readonly attribute CSSFontFeatureValuesMap stylistic;
- readonly attribute CSSFontFeatureValuesMap swash;
- readonly attribute CSSFontFeatureValuesMap characterVariant;
- readonly attribute CSSFontFeatureValuesMap styleset;
+ [SameObject] readonly attribute CSSFontFeatureValuesMap annotation;
+ [SameObject] readonly attribute CSSFontFeatureValuesMap ornaments;
+ [SameObject] readonly attribute CSSFontFeatureValuesMap stylistic;
+ [SameObject] readonly attribute CSSFontFeatureValuesMap swash;
+ [SameObject] readonly attribute CSSFontFeatureValuesMap characterVariant;
+ [SameObject] readonly attribute CSSFontFeatureValuesMap styleset;
};
interface CSSFontFeatureValuesMap {
diff --git a/third_party/blink/web_tests/external/wpt/css/css-fonts/font-feature-values-map-live.html b/third_party/blink/web_tests/external/wpt/css/css-fonts/font-feature-values-map-live.html
new file mode 100644
index 0000000..ed73676
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-fonts/font-feature-values-map-live.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>CSS Fonts Test: CSSFontFeatureValuesMap is live and reflect changes</title>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/css/css-fonts/font-feature-values-map-live.html b/third_party/blink/web_tests/external/wpt/css/css-fonts/font-feature-values-map-live.html
new file mode 100644
index 0000000..ed73676
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-fonts/font-feature-values-map-live.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>CSS Fonts Test: CSSFontFeatureValuesMap is live and reflect changes</title>
+<link rel="help" href="https://drafts.csswg.org/css-fonts/#cssfontfeaturevaluesmap">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @font-feature-values MyFamily {
+ @annotation { black-boxed: 3 }
+ }
+</style>
+<!-- Two identical stylesheets exposes a stylesheet sharing issue in Chrome -->
+<style>
+ @font-feature-values MyFamily {
+ @annotation { circled: 1; black-boxed: 3; }
+ }
+</style>
+<script>
+ test(() => {
+ const a1 = document.styleSheets[0].cssRules[0].annotation;
+ a1.set("black-boxed", 42);
+ const a2 = document.styleSheets[0].cssRules[0].annotation;
+ assert_array_equals(a1.get("black-boxed"), [42]);
+ assert_array_equals(a2.get("black-boxed"), [42]);
+ }, "Two retreived maps for CSSFontFeatureValuesRule.annotation should reflect the same values");
+
+ test(() => {
+ assert_array_equals(document.styleSheets[1].cssRules[0].annotation.get("black-boxed"), [3]);
+ }, "The second stylesheet should not be affected by modifications in the first");
+
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-fonts/font-feature-values-sameobject.tentative.html b/third_party/blink/web_tests/external/wpt/css/css-fonts/font-feature-values-sameobject.tentative.html
new file mode 100644
index 0000000..375f955
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-fonts/font-feature-values-sameobject.tentative.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>CSS Fonts Test: CSSFontFeatureValuesRule SameObject test</title>
+<link rel="help" href="https://drafts.csswg.org/css-fonts/#om-fontfeaturevalues">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/13953">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style id="style">
+ @font-feature-values TestFont {
+ @annotation { a: 1; }
+ @ornaments { b: 2; }
+ @stylistic { c: 3; }
+ @swash { d: 4; }
+ @character-variant { e: 5; }
+ @styleset { f: 6; }
+ }
+</style>
+<script>
+ test(() => {
+ const style = document.getElementById('style');
+ const rule = style.sheet.cssRules[0];
+ assert_true(rule instanceof CSSFontFeatureValuesRule, "Should be CSSFontFeatureValuesRule");
+
+ const attributes = [
+ 'annotation',
+ 'ornaments',
+ 'stylistic',
+ 'swash',
+ 'characterVariant',
+ 'styleset'
+ ];
+
+ for (const attr of attributes) {
+ const map1 = rule[attr];
+ const map2 = rule[attr];
+ assert_equals(map1, map2, `${attr} should return the same object`);
+ }
+ }, "CSSFontFeatureValuesMap attributes in CSSFontFeatureValuesRule should be [SameObject]");
+
+</script>
+
Original Bug Report
Potential same-process CSS poisoning via broken Copy-On-Write in CSSPositionTryRule and others
Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: Multiple CSS rule types fail to properly reattach their CSSOM wrappers during Copy-On-Write (COW) forks and rely on shallow copying during rule cloning. This can allow an attacker to bypass immutability and poison shared, cached stylesheets in the MemoryCache. Consequently, an attacker could potentially inject malicious styles into cross-origin documents sharing the same renderer process.
Affected files:
third_party/blink/renderer/core/css/css_position_try_rule.ccthird_party/blink/renderer/core/css/css_font_feature_values_rule.ccthird_party/blink/renderer/core/css/css_property_rule.ccthird_party/blink/renderer/core/css/css_counter_style_rule.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Summary
There is a logic flaw in Blink’s CSS Copy-On-Write (COW) mechanism affecting several CSS rule types, most notably @position-try (CSSPositionTryRule) and @font-feature-values (CSSFontFeatureValuesRule). When a shared, cached stylesheet is mutated, Blink forks the stylesheet to keep the cached version immutable. However, due to shallow copying of internal rule properties and a failure to reattach CSSOM wrappers to the newly cloned rules, an attacker can maintain a reference to the shared internal property sets and arbitrarily mutate them. This leads to same-process cross-origin CSS poisoning.
Root Cause Analysis
When a script mutates a CORS-enabled shared stylesheet, a COW fork is initiated via CSSStyleSheet::WillMutateRules() -> StyleSheetContents::Copy().
There are two combined flaws that enable this vulnerability:
-
Shallow Copying during Rule Cloning: During
StyleSheetContents::Copy(), internal rules are cloned viaStyleRuleBase::Clone(). For rule types likeStyleRulePositionTry, the implicit default copy constructor is used (StyleRulePositionTry(const StyleRulePositionTry&) = default;). If an attacker previously accessed the.styleproperty,MutableProperties()is called, making the underlyingproperties_set mutable. The default copy constructor performs a shallow copy of theMember<CSSPropertyValueSet> properties_pointer. As a result, both the original rule in theMemoryCacheand the new cloned rule point to the exact sameMutableCSSPropertyValueSet. -
Missing CSSOM Wrapper Reattachment: After cloning,
CSSStyleSheet::ReattachChildRuleCSSOMWrappers()delegates to each rule’sReattach()method to update its internal pointers to the newly cloned rule. While safe rules likeCSSStyleRulecorrectly reattach their child properties (properties_cssom_wrapper_->Reattach(...)),CSSPositionTryRule::Reattach()and others completely omit this step. Therefore, theCSSPositionTryDescriptorsCSSOM wrapper retains a staleproperty_set_pointer referencing the shared, now-mutableMutableCSSPropertyValueSet.
Subsequent mutations through the attacker’s CSSOM wrapper bypass the COW protections and directly modify the shared MutableCSSPropertyValueSet. Any other document in the same renderer process (e.g., same-site origins, or any origin on Android where site isolation is relaxed) loading the same cached stylesheet will receive the attacker’s injected styles.
Potential Exploitation Steps
(Note: These are suggested steps; our tooling agent does not yet have the ability to run code to confirm a working Proof of Concept).
- An attacker page embeds a common, cross-origin CORS-enabled stylesheet (e.g., a popular CSS framework from a CDN).
- Using JavaScript, the attacker retrieves the
CSSPositionTryRulefrom the stylesheet’scssRuleslist. - The attacker accesses the
.styleproperty (let s = rule.style;), which instantiates the CSSOM wrapper and forces the underlying property set to become mutable. - The attacker mutates the style (e.g.,
s.marginTop = "100px";). - This mutation triggers a COW fork. Because of the shallow copy, the
MemoryCache’s stylesheet and the attacker’s stylesheet share the mutable property set. - Because the CSSOM wrapper fails to reattach, subsequent modifications via
sdirectly poison the shared property set in theMemoryCache. - A victim navigating to a different origin in the same renderer process that relies on the same framework will have the poisoned CSS applied, potentially leading to UI spoofing or data exfiltration (e.g., using background-image requests to steal typed passwords).
Suggested Fix
- Fix Reattachment: Update
CSSPositionTryRule::Reattach,CSSFontFeatureValuesRule::Reattach,CSSPropertyRule::Reattach, andCSSCounterStyleRule::Reattachto properly callReattach()on their respective inner CSSOM wrappers (e.g.,properties_cssom_wrapper_->Reattach(...)). - Fix Shallow Copying: In
StyleRuleBase::Clone, update the instantiation of affected rule types to safely handle properties, similar toStyleRule::Create. Instead of using default copy constructors, useProperties().ImmutableCopyIfNeeded()to ensure that the cloned rule does not inadvertently share a mutable property set with the cached original.
Evaluated with Chrome root at commit: b7d0c4d810da1b31400f198c70d9720fc8f0e5a0
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.