← All issues

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

CSS Mixins added a dashed-function path beside `attr()` — but skipped the lifetime anchor that keeps token strings alive across substitution.

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 test case is the minimum trigger: define @function --f(--a) { result: aaaa var(--a); }, apply --x: --f(bbbb) to an element, and force resolution via getComputedStyle().getPropertyValue('--x'). The pre-fix code path resolves the function body to a CustomProperty, appends its tokens into the local tokens buffer, releases the only RefPtr to the body, then later reaches CSSVariableData::create — which consumes the now-stale token views.

To weaponize beyond an ASAN crash, an attacker would attempt to reclaim the freed StringImpl slab between the drop of resolvedResult and the call to CSSVariableData::create. Issuing additional CSS string allocations during the same substitution chain — more nested @function/attr() calls — would cause the freed buffer to be reused with attacker-controlled bytes. If reuse succeeds, CSSVariableData::create would read attacker-chosen bytes as if they were the original token payload, yielding a controlled-content read of the substituted variable's value and, depending on how downstream parsing of the resulting CSSVariableData interacts with the controlled bytes, a confused-token parse primitive over the recycled buffer.

This vulnerability weakens memory safety inside the WebContent renderer process. The lifetime invariant that CSSParserToken payloads remain valid for the duration of substitution was violated whenever a dashed @function produced a result that was then consumed by an outer substitution. An attacker who can place CSS containing a @function definition and a property using it can trigger the freed-string read on every style resolution of the affected target.

Note: A few details (CSSParserToken's view semantics, the established role of m_intermediateTokenStrings for attr() tokenization, and the post-free heap-reuse path) are inferred from the patch shape and surrounding code rather than directly visible in the diff. The core UAF mechanism and the fix's role as a parallel lifetime anchor are fully supported by the patch.