← All issues

[1] CSS @function dashed-substitution use-after-free

Severity: High | Component: WebCore Style — Style::SubstitutionResolver | 4b97014

Rated High because the diff fixes a heap use-after-free where CSSVariableData::create re-captures token data from a freed CustomProperty's string backing; the path is reachable from web content via a one-line @function snippet, and heap reuse before the re-capture would convert the freed-read into a controlled-content read primitive over the recycled buffer.

The commit keeps the resolved CustomProperty alive in m_intermediateCustomProperties until substitute() has constructed the new CSSVariableData, mirroring the existing m_intermediateTokenStrings mechanism used for attr() tokenizer output. The regression test installs @function --f(--a) { result: aaaa var(--a); }, applies --x: --f(bbbb) on a target, and reads getComputedStyle(target).getPropertyValue('--x') to drive the substitution path through the freed-token consumer.

Source/WebCore/style/StyleSubstitutionResolver.cpp

@@ substituteDashedFunction
if (guard.isCyclicContext())
return false;
 
+ // Tokens reference resolvedResult's string backing; keep it alive until CSSVariableData re-captures.
tokens.appendVector(resolvedResult->tokens());
+ m_intermediateCustomProperties.append(WTF::move(resolvedResult));
return true;
}
 
@@ substitute
auto substitutedTokens = substituteTokenRange(value.m_data->tokenRange(), context);
if (!substitutedTokens) {
m_intermediateTokenStrings.clear();
+ m_intermediateCustomProperties.clear();
return nullptr;
}
 
auto data = CSSVariableData::create(*substitutedTokens, m_isAttrTainted ? IsAttrTainted::Yes : IsAttrTainted::No, context);
m_intermediateTokenStrings.clear();
+ m_intermediateCustomProperties.clear();
return data;

Source/WebCore/style/StyleSubstitutionResolver.h

Vector<String> m_intermediateTokenStrings;
+ Vector<RefPtr<const CustomProperty>> m_intermediateCustomProperties;

LayoutTests/fast/css/variables/dashed-function-result-token-lifetime.html

+@function --f(--a) {
+ result: aaaa var(--a);
+}
+#target { --x: --f(bbbb); }
+var v = getComputedStyle(target).getPropertyValue('--x');

The patch introduces a new member Vector<RefPtr<const CustomProperty>> m_intermediateCustomProperties on SubstitutionResolver. In substituteDashedFunction, immediately after tokens.appendVector(resolvedResult->tokens()), the local RefPtr is moved into the new vector. In substitute, the new vector is cleared on both the failure and success paths, paired with the existing m_intermediateTokenStrings.clear(). No existing logic is restructured — the change is purely the addition of a parallel lifetime anchor.

Non-owning token views outliving their backing object due to a missing lifetime anchor across substitution stages.

CSS Mixins introduces @function, a CSS-level function definition (@function --name(--arg) { result: <tokens>; }) that can be invoked as a dashed-function reference inside a custom property value. Style::SubstitutionResolver resolves these references during style computation by recursively expanding nested var() / attr() / dashed-function calls into a single token vector, then constructing a CSSVariableData (via CSSVariableData::create) from that vector. CSSVariableData is the storage object for an unparsed CSS value, and CSSParserToken is a value-type token that stores non-owning views into externally owned backing strings — typically StringImpl payloads owned by an upstream CustomProperty or by parser-allocated buffers. Style::CustomProperty is a reference-counted holder of a parsed/tokenized custom-property value; calling CustomProperty::tokens() returns its tokens, but those tokens reference string storage owned by the CustomProperty itself.

The pre-existing m_intermediateTokenStrings field already exists as a lifetime anchor for transient strings produced during attr() tokenization, exactly to keep token backing alive until the final CSSVariableData::create re-captures the data.

Before the fix, substituteDashedFunction called tokens.appendVector(resolvedResult->tokens()) and then returned, dropping the local RefPtr<const CustomProperty> resolvedResult at end-of-scope. Once resolvedResult was destroyed, the backing StringImpls it kept alive could be freed even though the appended token views (now in the resolver's growing tokens buffer) still pointed at that storage. The dangling tokens were subsequently consumed by CSSVariableData::create(*substitutedTokens, ...) in substitute(), which re-captures token data — by that time the original backing was already destroyed, producing the ASAN heap-use-after-free reported in the bug.

This is the classic "tokens view externally owned strings" lifetime hazard, and SubstitutionResolver already had a working solution for it on the attr() path. When CSS Mixins / dashed-function support landed, the same hazard recurred for CustomProperty-owned token backings but the corresponding lifetime anchor was not added — variant analysis from the prior attr() lifetime fix lands the missing anchor mechanically.

🔒

The lifetime model behind CSS variable token resolution and what happens when a substitution stage drops its backing object too early.

Subscribe to read more

🔒

Several reusable lifetime-anchor audit patterns identified across the CSS substitution pipeline, with concrete starting points for variant discovery.

Subscribe to read more