High patch-now ✔︎ verified CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in CSS
DescriptionHeap buffer overflow in CSS
ComponentCSS
Bug ClassOOB
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Validation-gap mechanism is clearly shown. But commit message cites only DCHECKs and the diff adds validation plus a safe CHECK, not the OOB site; corruption rests on the advisory label, which the analysis acknowledges.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker484751092
Fix commit5efc7a0127a6 (chromium/src) +89/-18
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedSyn4pse
Reported2026-02-16 (per vendor advisory)
Fix landed2026-02-25 (public git)
Disclosed2026-03-18
Patch gap21 days — fix public in source before the advisory

Background

CSS Typed OM `CSSUnparsedValue`
a JS-constructible object (new CSSUnparsedValue([...])) that represents raw token text, possibly with substitution functions. It can be assigned to properties with attributeStyleMap.set().
`CSSUnparsedDeclarationValue` / `CSSVariableData`
the internal style-engine representation of an unparsed declaration. CSSUnparsedValue::ToCSSValue() builds one from the serialized string.
Argument grammar of substitution functions
var(), env(), attr(), if() and custom functions (--f()) have required argument shapes. CSSVariableParser::ConsumeUnparsedDeclaration checks these when parsing a normal stylesheet.
Property acceptance check (`cssom_types.cc.tmpl`)
generated code that decides whether a given Typed OM value can be assigned to a property before it is converted and stored.

Root Cause Analysis

Before the patch, the generated acceptance check in cssom_types.cc.tmpl returned true for any value with GetType() == CSSStyleValue::kUnparsedType. The hunk does not name the enclosing generated function. CSSUnparsedValue::ToCSSValue() then wrapped the raw string in CSSVariableData::Create(...) and a CSSUnparsedDeclarationValue without parsing it. Script could therefore insert declaration values that the CSS parser would never produce, such as var(,), env(), attr(0), if() or --f(,). The commit message says this caused “DCHECKs later in the pipeline”. The advisory classifies the consequence in release builds as a heap buffer overflow in CSS, but the diff does not show where the out-of-bounds access happens. Later substitution code that assumes well-formed arguments is the likely place (inferred).

The fix adds CSSUnparsedValue::IsValidDeclarationValue(), which runs CSSVariableParser::ConsumeUnparsedDeclaration on the serialized string. The acceptance check now returns that result, so invalid values are rejected at assignment. ToCSSValue() also adds a CHECK(IsValidDeclarationValue(unparsed_string)), so any other path that reaches it with an invalid value crashes safely instead of building a malformed object.

Key insight
The Typed OM path accepted CSSUnparsedValue contents without validation, bypassing the parser checks that normally guarantee CSSUnparsedDeclarationValue holds a valid <declaration-value>. The fix runs ConsumeUnparsedDeclaration both at assignment time and, as a hard CHECK, at conversion time.

Attack Path

  1. Construct malformed value Page script creates new CSSUnparsedValue(['var(,)']) (or env(), attr(0), if(), --f() and similar, as listed in set-invalid-untyped-value-crash.html).
  2. Assign via Typed OM element.attributeStyleMap.set('width', value) (or a custom property like --x). Before the fix, the acceptance check returned true for any unparsed value.
  3. Malformed internal value created CSSUnparsedValue::ToCSSValue() builds a CSSUnparsedDeclarationValue from the unvalidated string.
  4. Style resolution consumes it attributeStyleMap.get() or a forced style/layout (offsetTop) processes the malformed substitution function. The commit message reports DCHECK failures there, and the advisory reports a heap buffer overflow. The exact sink and any resulting memory-corruption primitive are (inferred).

Impact Assessment

The vendor rates this high (“Heap buffer overflow in CSS”), and the provided advisory data does not report in-the-wild exploitation. Ordinary web content in a stock build can reach it with a few lines of script using the standard CSS Typed OM API. No flags, extensions or user gesture are needed. Memory corruption happens in the sandboxed renderer process. Turning the overflow into code execution inside the renderer is (inferred) and not shown by the patch.

Changed Functions

FunctionChangeNotes
switch
third_party/blink/renderer/build/scripts/core/css/templates/cssom_types.cc.tmpl
modified
for
third_party/blink/web_tests/external/wpt/css/css-typed-om/missing-variable-in-unparsed-value-crash.html
modified

Files Changed

  • third_party/blink/renderer/build/scripts/core/css/templates/cssom_types.cc.tmpl
  • third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
  • third_party/blink/renderer/core/css/cssom/css_unparsed_value.h
  • third_party/blink/web_tests/external/wpt/css/css-typed-om/missing-variable-in-unparsed-value-crash.html
  • third_party/blink/web_tests/external/wpt/css/css-typed-om/set-invalid-untyped-value-crash.html

Audit Directions

  • Typed OM to internal value conversions
    Check every CSSStyleValue::ToCSSValue() override (CSSUnsupportedStyleValue, math values, transform/keyword values) for internal values that the parser would never produce.
  • Destination-property context
    The patch’s TODO notes that restricted_value and other property-specific constraints are still ignored. Check whether a value that is a valid generic <declaration-value> but invalid for the target property (e.g. registered custom properties, !important) can still break later substitution code.
  • Substitution-function consumers
    Review var()/env()/attr()/if()/custom-function resolution code for places that index into token ranges assuming a well-formed argument list. Replace those DCHECKs with real bounds checks so that any other way of injecting malformed data fails safely.
From 5efc7a0127a6a735e252e67cecaced918d5bf42a Mon Sep 17 00:00:00 2001
From: Anders Hartvoll Ruud <andruud@chromium.org>
Date: Wed, 25 Feb 2026 06:21:21 -0800
Subject: [PATCH] Validate CSSUnparsedValues upon assignment

CSS Typed OM has a concept of a value "matching a grammar" (or not)
upon assignment to a property [1]. For CSSUnparsedValues, we currently
don't perform any significant validation, and as a consequence
we allow "invalid" CSSUnparsedDeclarationValues to be created
(causing DCHECKs later in the pipeline).

This CL makes sure values can be parsed using CSSVariableParser::
ConsumeUnparsedDeclaration before assignment.

We're still not handling the value in the context of the destination
property, which we probably should. This is also a problem with
current state of things, however, so for now the goal is primarily
to avoid the DCHECKs in Issue 484751092.

Finally, I opened an issue against the specification [2], which
currently doesn't define any of this.

[1] https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation
[2] https://github.com/w3c/csswg-drafts/issues/13547

Fixed: 484751092
Change-Id: Id7f888a6df8c02ade24910900f5d01909cb2dfad
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7595347
Reviewed-by: Steinar H Gunderson <sesse@chromium.org>
Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1590110}
---

diff --git a/third_party/blink/renderer/build/scripts/core/css/templates/cssom_types.cc.tmpl b/third_party/blink/renderer/build/scripts/core/css/templates/cssom_types.cc.tmpl
index edfa73a5..4442ba08 100644
--- a/third_party/blink/renderer/build/scripts/core/css/templates/cssom_types.cc.tmpl
+++ b/third_party/blink/renderer/build/scripts/core/css/templates/cssom_types.cc.tmpl
@@ -11,6 +11,7 @@
 #include "third_party/blink/renderer/core/css/cssom/css_keyword_value.h"
 #include "third_party/blink/renderer/core/css/cssom/css_numeric_value.h"
 #include "third_party/blink/renderer/core/css/cssom/css_style_value.h"
+#include "third_party/blink/renderer/core/css/cssom/css_unparsed_value.h"
 #include "third_party/blink/renderer/core/css/cssom/css_unsupported_style_value.h"
 #include "third_party/blink/renderer/core/css/cssom/cssom_keywords.h"
 #include "third_party/blink/renderer/core/css/properties/css_property.h"
@@ -105,8 +106,8 @@
                     : CSSPropertyName(id);
     return unsupported_style_value->IsValidFor(name);
   }
-  if (value.GetType() == CSSStyleValue::kUnparsedType) {
-    return true;
+  if (auto* unparsed_value = DynamicTo<CSSUnparsedValue>(value)) {
+    return unparsed_value->IsValidDeclarationValue();
   }
 
   switch (id) {
diff --git a/third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc b/third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
index 12d70ed0..5f9d6a3 100644
--- a/third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
+++ b/third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
@@ -4,11 +4,13 @@
 
 #include "third_party/blink/renderer/core/css/cssom/css_unparsed_value.h"
 
+#include "css_style_value.h"
 #include "third_party/blink/renderer/core/css/css_unparsed_declaration_value.h"
 #include "third_party/blink/renderer/core/css/css_variable_data.h"
 #include "third_party/blink/renderer/core/css/cssom/css_style_variable_reference_value.h"
 #include "third_party/blink/renderer/core/css/parser/css_parser_token_stream.h"
 #include "third_party/blink/renderer/core/css/parser/css_tokenizer.h"
+#include "third_party/blink/renderer/core/css/parser/css_variable_parser.h"
 #include "third_party/blink/renderer/core/css_value_keywords.h"
 #include "third_party/blink/renderer/platform/bindings/exception_messages.h"
 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
@@ -136,6 +138,10 @@
   return IndexedPropertySetterResult::kIntercepted;
 }
 
+bool CSSUnparsedValue::IsValidDeclarationValue() const {
+  return IsValidDeclarationValue(ToStringInternal());
+}
+
 const CSSValue* CSSUnparsedValue::ToCSSValue() const {
   String unparsed_string = ToStringInternal();
 
@@ -144,12 +150,40 @@
         MakeGarbageCollected<CSSVariableData>());
   }
 
+  CHECK(IsValidDeclarationValue(unparsed_string));
+  // The call to IsValidDeclarationValue() above also creates a CSSVariableData
+  // to carry out its check. It would be nice to use that here, but WPTs
+  // expect leading whitespace to be preserved, even though it's not possible
+  // to create such declaration values normally.
+  CSSVariableData* variable_data =
+      CSSVariableData::Create(unparsed_string,
+                              /*is_animation_tainted=*/false,
+                              /*is_attr_tainted=*/false,
+                              /*needs_variable_resolution=*/false);
+
   // TODO(crbug.com/985028): We should probably propagate the CSSParserContext
   // to here.
-  return MakeGarbageCollected<CSSUnparsedDeclarationValue>(
-      CSSVariableData::Create(unparsed_string, false /* is_animation_tainted */,
-                              false /* is_attr_tainted */,
-                              false /* needs_variable_resolution */));
+  return MakeGarbageCollected<CSSUnparsedDeclarationValue>(variable_data);
+}
+
+bool CSSUnparsedValue::IsValidDeclarationValue(const String& string) {
+  CSSParserTokenStream stream(string);
+  bool important_unused;
+  // This checks that the value does not violate the "argument grammar" [1]
+  // of any substitution functions, and that it is a valid <declaration-value>
+  // otherwise.
+  //
+  // [1] https://drafts.csswg.org/css-values-5/#argument-grammar
+  //
+  // TODO(andruud): 'restricted_value' depends on the destination property.
+  return CSSVariableParser::ConsumeUnparsedDeclaration(
+      stream,
+      /*allow_important_annotation=*/false,
+      /*is_animation_tainted=*/false,
+      /*must_contain_variable_reference=*/false,
+      /*restricted_value=*/false,
+      /*comma_ends_declaration=*/false, important_unused,
+      *StrictCSSParserContext(SecureContextMode::kInsecureContext));
 }
 
 String CSSUnparsedValue::ToStringInternal() const {
diff --git a/third_party/blink/renderer/core/css/cssom/css_unparsed_value.h b/third_party/blink/renderer/core/css/cssom/css_unparsed_value.h
index ec7e3ed..7fd66ae 100644
--- a/third_party/blink/renderer/core/css/cssom/css_unparsed_value.h
+++ b/third_party/blink/renderer/core/css/cssom/css_unparsed_value.h
@@ -48,6 +48,14 @@
   CSSUnparsedValue(const CSSUnparsedValue&) = delete;
   CSSUnparsedValue& operator=(const CSSUnparsedValue&) = delete;
 
+  // True if this CSSUnparsedValue can be converted into
+  // a CSSUnparsedDeclarationValue.
+  //
+  // We may want to ban some invalid values earlier, see:
+  // https://github.com/w3c/csswg-drafts/issues/13547
+  bool IsValidDeclarationValue() const;
+
+  // Requires IsValidDeclarationValue()==true.
   const CSSValue* ToCSSValue() const override;
 
   StyleValueType GetType() const override { return kUnparsedType; }
@@ -68,6 +76,7 @@
   }
 
  private:
+  static bool IsValidDeclarationValue(const String&);
   String ToStringInternal() const;
   String SerializeSegments() const;
   // Return 'false' if there is a cycle in the serialization.
diff --git a/third_party/blink/web_tests/external/wpt/css/css-typed-om/missing-variable-in-unparsed-value-crash.html b/third_party/blink/web_tests/external/wpt/css/css-typed-om/missing-variable-in-unparsed-value-crash.html
deleted file mode 100644
index b92bd62d..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-typed-om/missing-variable-in-unparsed-value-crash.html
+++ /dev/null
@@ -1,12 +0,0 @@
-<!DOCTYPE html>
-<title>Crash Test: Missing variable name in CSSUnparsedValue</title>
-<link rel="help" href="https://issues.chromium.org/issues/484811719">
-<div id="div"></div>
-<script>
-  for (let i = 0; i < 5000; ++i) {
-    const bad = new CSSUnparsedValue(['var(,)']);
-    div.attributeStyleMap.set('--x', bad);
-    div.attributeStyleMap.get('--x');
-  }
-</script>
-<p>PASS if no crash</p>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-typed-om/set-invalid-untyped-value-crash.html b/third_party/blink/web_tests/external/wpt/css/css-typed-om/set-invalid-untyped-value-crash.html
new file mode 100644
index 0000000..ce618bf
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-typed-om/set-invalid-untyped-value-crash.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Crash when setting invalid CSSUnparsedValue</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/13547">
+<div id=target></div>
+<script>
+  let examples = [
+    'var()',
+    'var(,)',
+    'var(0)',
+    'env()',
+    'env(,)',
+    'env(0)',
+    'attr()',
+    'attr(,)',
+    'attr(0)',
+    'if()',
+    'if(,)',
+    'if(0)',
+    '--f()',
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/css/css-typed-om/missing-variable-in-unparsed-value-crash.html b/third_party/blink/web_tests/external/wpt/css/css-typed-om/missing-variable-in-unparsed-value-crash.html
deleted file mode 100644
index b92bd62d..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-typed-om/missing-variable-in-unparsed-value-crash.html
+++ /dev/null
@@ -1,12 +0,0 @@
-<!DOCTYPE html>
-<title>Crash Test: Missing variable name in CSSUnparsedValue</title>
-<link rel="help" href="https://issues.chromium.org/issues/484811719">
-<div id="div"></div>
-<script>
-  for (let i = 0; i < 5000; ++i) {
-    const bad = new CSSUnparsedValue(['var(,)']);
-    div.attributeStyleMap.set('--x', bad);
-    div.attributeStyleMap.get('--x');
-  }
-</script>
-<p>PASS if no crash</p>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-typed-om/set-invalid-untyped-value-crash.html b/third_party/blink/web_tests/external/wpt/css/css-typed-om/set-invalid-untyped-value-crash.html
new file mode 100644
index 0000000..ce618bf
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-typed-om/set-invalid-untyped-value-crash.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Crash when setting invalid CSSUnparsedValue</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/13547">
+<div id=target></div>
+<script>
+  let examples = [
+    'var()',
+    'var(,)',
+    'var(0)',
+    'env()',
+    'env(,)',
+    'env(0)',
+    'attr()',
+    'attr(,)',
+    'attr(0)',
+    'if()',
+    'if(,)',
+    'if(0)',
+    '--f()',
+    '--f(,)',
+    '--f(0)',
+    'thing!!!',
+    'var(--x) !important',
+  ];
+  // Some of the above cases may be valid. That's fine; just don't crash.
+
+  for (let e of examples) {
+    try {
+      let value = new CSSUnparsedValue([e]);
+      target.attributeStyleMap.set('width', value);
+      // One of the two above statements should likely throw an exception.
+      // If they don't, then we should at least not crash on get():
+      target.attributeStyleMap.get('width');
+    } catch (e) {
+      // Intentionally empty.
+    }
+    target.offsetTop;
+  }
+</script>
Loading diff…

Original Bug Report

reported by he...@gmail.com

Heap-buffer-overflow in StyleCascade::ConsumeVariableName

Summary

StyleCascade resolves env()/attr() substitutions by reading the function’s first argument as an ident via ConsumeVariableName, but it only DCHECKs the token type and then calls CSSParserToken::Value(). In release builds, CSS Typed OM can inject syntactically-invalid env(0) into an unparsed declaration (via attributeStyleMap.set()), so the resolver ends up calling Value() on a non-string-backed token (e.g. kNumberToken), producing a StringView with uninitialized pointer/length and cause OOB in ToAtomicString().

Details

During variable/substitution resolution, StyleCascade::ResolveTokensInto detects env() and calls StyleCascade::ResolveEnvInto, which consumes the environment variable name using ConsumeVariableName.

The key issue is that ConsumeVariableName relies on a debug-only type assertion before calling Value():

ConsumeVariableName:

AtomicString ConsumeVariableName(CSSParserTokenStream& stream) {
  stream.ConsumeWhitespace();
  CSSParserToken ident_token = stream.ConsumeIncludingWhitespaceRaw();
  DCHECK_EQ(ident_token.GetType(), kIdentToken);
  return ident_token.Value().ToAtomicString();
}

In a normal stylesheet parse pipeline, invalid env() syntax is expected to be rejected before this point. However, CSS Typed OM can store CSSUnparsedValue content as a CSSUnparsedDeclarationValue without enforcing env()’s argument grammar, so a value like env(0) can reach the substitution resolver.

For env(0), the token after the ( is a kNumberToken, which is not string-backed. CSSParserToken::Value() does not check GetType()/HasStringBacking(); it constructs a StringView from the token’s internal value_* fields:

CSSParserToken::Value:

StringView Value() const {
  return value_is_8bit_ ? StringView(Span8()) : StringView(Span16());
}

For non-string-backed tokens (such as kNumberToken), the value_length_ / value_data_char_raw_ fields are not initialized as a meaningful string payload. Calling Value() on such a token therefore yields a StringView with a garbage pointer/length. The subsequent ToAtomicString() tries to hash/copy from that span, leading to an out-of-bounds memory access.

Bisection

This issue was introduced in commit 27932c039882c41ae25df71df3078a5bbfb38795 (“StyleCascade, Phase 1”) by Anders Hartvoll Ruud on 2019-07-23 (CL 1605418).

Reproduction

Using https://storage.googleapis.com/chromium-browser-asan/linux-release/asan-linux-release-1585188.zip

Run:

./chrome --no-sandbox --user-data-dir=/tmp/xx poc.html

This would trigger ASAN crash shown in asan.txt

Suggested Fix

Treat non-ident first arguments in ConsumeVariableName as untrusted.

View on issue tracker