[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.
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
Source/WebCore/style/StyleSubstitutionResolver.h
LayoutTests/fast/css/variables/dashed-function-result-token-lifetime.html
Patch Details
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.
Background
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.
Analysis
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.
Audit directions
-
Non-owning token/view types crossing a multi-stage substitution boundary outliving their backing owner. Audit all paths in
Style::SubstitutionResolverthat append into the resolver-localtokensbuffer — especiallysubstituteAttrFunction,substituteVariableFunction,substituteInternalAutoBaseFunction, and any future substitution helper — and verify that each owner whose tokens are appended is anchored either inm_intermediateTokenStringsorm_intermediateCustomProperties(or some equivalent) untilCSSVariableData::createre-captures. Start fromsubstituteTokenRangeinSource/WebCore/style/StyleSubstitutionResolver.cppand walk every callee that doestokens.appendVector(...). -
CSSParserTokenpayloads spliced into a buffer that outlives the source object. Anywhere tokens from oneCustomProperty/CSSVariableDataare spliced into a longer-lived buffer is a candidate UAF. GrepSource/WebCore/cssandSource/WebCore/styleforappendVector(.*->tokens()),tokens().subspan, andCSSParserTokenconstructions that copy from a temporary, then verify each call site keeps the owner alive across subsequent token-consuming operations. -
CSSVariableData::createis a re-capture boundary. Fixing the lifetime up to that point is the standard idiom. Check every caller ofCSSVariableData::createin the tree — particularly in custom-function evaluation,@propertyregistrations, and shorthand expansion — for the same "tokens drawn from a temporary owner that has already gone out of scope" shape. Concrete starting points:resolveAndRegisterDashedFunctionArgumentsin the same file, and any helper that constructsCSSVariableDatafromVector<CSSParserToken>returned by an inner resolver call. -
Lifetime-anchor parity across feature additions. When a subsystem already uses an explicit lifetime-anchor vector (here
m_intermediateTokenStrings), every subsequently added code path that produces tokens must add a parallel anchor. Audit recent commits toStyleSubstitutionResolverand the CSS Mixins /@functionimplementation for other paths added sincem_intermediateTokenStringswas introduced — and whether each has a parallel anchor.
Note: A few details (
CSSParserToken's view semantics, the established role ofm_intermediateTokenStringsforattr()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.