Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in CSS
DescriptionInappropriate implementation in CSS
ComponentCSS
Bug ClassLogic Error
Tracker484811719
Fix commit2e0a8f0ab03e (chromium/src) +43/-4
CISA KEVNot listed
CreditedSyn4pse
Disclosed2026-03-03

Background

`FindVariableName`
The Typed OM helper in css_unparsed_value.cc that reads the variable name identifier following a var( function token in a CSSUnparsedValue.
`CSSParserToken`
A compact CSS parser token whose value_length_ and value storage are only meaningful for token types (like kIdentToken) that actually carry a string value.
`CSSUnparsedValue`
A CSS Typed OM object that models an unparsed CSS value (including var() references) and can be constructed from author-supplied strings via JavaScript.
`kIdentToken`
The CSSParserTokenType enum value marking an identifier token, the only token type for which a var() variable name Value() is valid.

Root Cause Analysis

When parsing a CSSUnparsedValue containing a var( function, FindVariableName consumed the token after var( and immediately called stream.Consume().Value().ToString() without verifying the token was actually an identifier. The invariant violated is that CSSParserToken::Value() may only be called on tokens whose type carries value storage; for a token like , (as in var(,)), value_length_ and the value pointer were never initialized, so Value() read uninitialized memory. The primary fix guards the read with a stream.Peek().GetType() == CSSParserTokenType::kIdentToken check and returns an empty String otherwise, so Value() is only reached on a token that legitimately holds a string. Defense in depth also hardens CSSParserToken itself: value_length_ is now explicitly zero-initialized in the constructor, a has_value_ bit plus a DCHECK in Value() catch invalid calls in debug builds, and callers up the chain (VariableReferenceValue, ParserTokenStreamToTokens) now propagate a null result instead of dereferencing it. Together these ensure a malformed var() name aborts parsing cleanly rather than consuming garbage token data.
Key insight
The single core mistake was assuming the token immediately after var( is always an identifier and blindly calling Value() on it, when it can be any token type (e.g. a comma) with no value storage. The fix adds an explicit kIdentToken type check before the Value() read and zero-initializes value_length_ so even an errant read yields an empty token rather than uninitialized data.

Attack Path

  1. Craft malformed input An attacker uses JavaScript to build new CSSUnparsedValue(['var(,)']), a var() reference whose first inner token is a comma instead of an identifier.
  2. Force reparse via Typed OM They assign it through div.attributeStyleMap.set('--x', bad) and read it back with .get('--x'), driving the value through FindVariableName.
  3. Trigger the invalid read FindVariableName consumes the comma token and calls Value() on it, reading value_length_ and value bytes that were never initialized for a comma token.
  4. Amplify the effect Repeating the operation thousands of times (as in the crash test) reliably surfaces the uninitialized read, producing a crash or leaking whatever stack/heap bytes happened to back the token.

Impact Assessment

An attacker gains an out-of-bounds/uninitialized memory read within the renderer process by supplying a crafted CSSUnparsedValue from ordinary page JavaScript, with no special privileges required beyond running script. Depending on memory contents this yields a renderer crash (denial of service) or potential disclosure of adjacent memory contents interpreted as a string. The only precondition is the ability to execute JavaScript that constructs and round-trips a malformed var() value through the Typed OM attributeStyleMap.

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
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/core/css/cssom/css_unparsed_value.cc
  • third_party/blink/renderer/core/css/parser/css_parser_token.h
  • third_party/blink/web_tests/external/wpt/css/css-typed-om/missing-variable-in-unparsed-value-crash.html

Audit Directions

  • Unchecked `Value()` on parser tokens
    Search other CSS/Typed OM parser paths that call CSSParserToken::Value() after Consume()/Peek() without first asserting the token type carries a value.
  • Assumed token shape after function tokens
    Review code that consumes tokens following a known function (var(, env(, attr(, etc.) and presumes the next token is an identifier without validating GetType().
  • Uninitialized bitfield/length members
    Audit compact token and value structs for _length or pointer fields left uninitialized for token types that do not use them, especially where the same accessor is shared across valued and valueless types.
From 2e0a8f0ab03e48d823d391d9f77e1e96d68b7796 Mon Sep 17 00:00:00 2001
From: Steinar H. Gunderson <sesse@chromium.org>
Date: Thu, 19 Feb 2026 05:43:25 -0800
Subject: [PATCH] Fix reading of invalid data in Typed OM.

After seeing var(, we would not properly check that the next token
was an ident, and would just call Value(), potentially reading
uninitialized data. Our fix has a little defense in depth:

 1. For this specific case, we check the token type. (The spec is not
    exactly clear on exactly _what_ we should do, but right now,
    we abort the parsing. We should probably throw an exception,
    but getting an ExceptionState all the way down here is not trivial.)
 2. For CSSParserTokens without a value, we add a DCHECK if someone
    calls Value().
 3. In addition, we explicitly initialize data_length_ to zero,
    so that if someone calls Value() in a non-DCHECK build, we'll
    get an empty token instead of uninitialized data.

Style perftest parsing performance is neutral.

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

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 3d43306..486e9d1 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
@@ -19,7 +19,11 @@
 
 String FindVariableName(CSSParserTokenStream& stream) {
   stream.ConsumeWhitespace();
-  return stream.Consume().Value().ToString();
+  if (stream.Peek().GetType() == CSSParserTokenType::kIdentToken) {
+    return stream.Consume().Value().ToString();
+  } else {
+    return {};
+  }
 }
 
 V8CSSUnparsedSegment* VariableReferenceValue(
@@ -35,6 +39,11 @@
   CSSStyleVariableReferenceValue* variable_reference =
       CSSStyleVariableReferenceValue::Create(variable_name.ToString(),
                                              unparsed_value);
+  if (!variable_reference) {
+    // TODO(sesse): Plumb the ExceptionState here so that we can use
+    // the Create() variant that properly throws an exception.
+    return nullptr;
+  }
   return MakeGarbageCollected<V8CSSUnparsedSegment>(variable_reference);
 }
 
@@ -57,8 +66,12 @@
       if (stream.Peek().GetType() == CSSParserTokenType::kCommaToken) {
         stream.Consume();
       }
-      tokens.push_back(VariableReferenceValue(
-          variable_name, ParserTokenStreamToTokens(stream)));
+      V8CSSUnparsedSegment* ref = VariableReferenceValue(
+          variable_name, ParserTokenStreamToTokens(stream));
+      if (!ref) {
+        break;
+      }
+      tokens.push_back(ref);
     } else {
       if (stream.Peek().GetBlockType() == CSSParserToken::kBlockStart) {
         ++nesting_level;
diff --git a/third_party/blink/renderer/core/css/parser/css_parser_token.h b/third_party/blink/renderer/core/css/parser/css_parser_token.h
index 0f8efb7..b294c9dc3 100644
--- a/third_party/blink/renderer/core/css/parser/css_parser_token.h
+++ b/third_party/blink/renderer/core/css/parser/css_parser_token.h
@@ -93,7 +93,8 @@
         unit_(0),                // Don't care.
         value_is_inline_(false),
         value_is_8bit_(false),  // Don't care.
-        padding_(0)             // Don't care.
+        padding_(0),            // Don't care.
+        value_length_(0)        // For security.
   {}
 
   // The resulting CSSParserToken may hold a reference to the data in value.
@@ -128,6 +129,9 @@
     return static_cast<CSSParserTokenType>(type_);
   }
   StringView Value() const {
+#if DCHECK_IS_ON()
+    DCHECK(has_value_);
+#endif
     return value_is_8bit_ ? StringView(Span8()) : StringView(Span16());
   }
 
@@ -221,6 +225,9 @@
       value_data_char_raw_ = string.Bytes();
       value_is_inline_ = false;
     }
+#if DCHECK_IS_ON()
+    has_value_ = true;
+#endif
   }
   bool ValueDataCharRawEqual(const CSSParserToken& other) const;
   const void* ValueDataCharRaw() const {
@@ -268,8 +275,15 @@
   // tightly with the rest of this object for a smaller object size.
   unsigned value_is_8bit_ : 1;
 
+#if DCHECK_IS_ON()
+  unsigned has_value_ : 1 = false;
+
+  // These are free bits. You may take from them if you need.
+  [[maybe_unused]] unsigned padding_ : 11;
+#else
   // These are free bits. You may take from them if you need.
   [[maybe_unused]] unsigned padding_ : 12;
+#endif
 
   unsigned value_length_;
   union {
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
new file mode 100644
index 0000000..b92bd62d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-typed-om/missing-variable-in-unparsed-value-crash.html
@@ -0,0 +1,12 @@
+<!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>
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
new file mode 100644
index 0000000..b92bd62d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-typed-om/missing-variable-in-unparsed-value-crash.html
@@ -0,0 +1,12 @@
+<!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>
Loading diff…

Original Bug Report

reported by he...@gmail.com

Heap-buffer-overflow in CSSUnparsedValue::FindVariableName

Summary

CSSUnparsedValue’s var()/env() tokenization path assumes the first argument is always string-backed, and calls FindVariableName() as stream.Consume().Value().ToString() without validating the token type. When the first token inside var(...) is a comma (e.g. var(,)), the consumed token is kCommaToken (not string-backed), so CSSParserToken::Value() constructs a StringView from uninitialized value_length_/value_data_char_raw_.Therefore, OOB happens when we convert that StringView to a String copies an attacker-influenced size from an invalid pointer.

Details

When converting a serialized custom property back into Typed OM segments, ParserTokenStreamToTokens() treats any var()/env() function token as a variable-reference and immediately parses a “variable name” token.

In css_unparsed_value.cc, the variable-name extraction does not check token type or whether the token is string-backed:

String FindVariableName(CSSParserTokenStream& stream) {
  stream.ConsumeWhitespace();
  return stream.Consume().Value().ToString();
}

For malformed input like var(,), after consuming whitespace the next token is a comma (kCommaToken). Comma tokens are not string-backed (see CSSParserToken::HasStringBacking()), but FindVariableName() still calls Value().

In css_parser_token.h, CSSParserToken::Value() returns a StringView built from value_length_ and value_data_char_raw_:

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

In css_parser_token.h, non-string-backed tokens constructed via CSSParserToken(CSSParserTokenType, BlockType) do not initialize the “value” fields, so value_length_ and value_data_char_raw_ contain indeterminate data:

explicit CSSParserToken(CSSParserTokenType type,
                        BlockType block_type = kNotBlock)
    : type_(type),
      block_type_(block_type),
      numeric_value_type_(0),
      numeric_sign_(0),
      unit_(0),
      value_is_inline_(false),
      value_is_8bit_(false),
      padding_(0) {}

As a result, Value().ToString() can attempt to copy an attacker-controlled large length from an attacker-controlled pointer, leading to the OOB during the copy inside StringView::ToString().

Bisection

This issue was introduced in commit fb6b1c467f2705a7d8c607d512cacefd17f1488d (“Convert CSSUnparsedValue parsing to the streaming parser”) by Steinar H. Gunderson on 2024-08-27 (CL 5803173).

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

You should ASAN crash which shown in the asan.txt

View on issue tracker