← 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

CSSVariableData::create가 이미 해제된 CustomProperty의 string backing에서 token 데이터를 재참조하는 heap use-after-free를 수정합니다. 단 한 줄의 @function snippet만으로 web content에서 이 경로에 도달할 수 있습니다. 재참조 전에 heap 재사용이 발생하면, freed-read가 recycled buffer에 대한 controlled-content read primitive로 전환됩니다. 이러한 이유로 Severity를 High로 평가합니다.

substitute()가 새로운 CSSVariableData를 생성할 때까지, resolve된 CustomPropertym_intermediateCustomProperties에 유지하도록 수정되었습니다. attr() tokenizer 출력에 사용되는 기존의 m_intermediateTokenStrings 방식과 동일한 패턴입니다. regression test는 @function --f(--a) { result: aaaa var(--a); }를 정의하고, target에 --x: --f(bbbb)를 적용한 뒤 getComputedStyle(target).getPropertyValue('--x')를 호출하여, substitution 경로가 해제된 token consumer를 통과하도록 유도합니다.

Source/WebCore/style/StyleSubstitutionResolver.cpp

@@ substituteDashedFunction
if (guard.isCyclicContext())
return false;
 
+ // Tokens가 resolvedResult의 string backing을 참조합니다. CSSVariableData가 재참조할 때까지 살아있어야 합니다.
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');

patch는 SubstitutionResolver에 새로운 멤버 Vector<RefPtr<const CustomProperty>> m_intermediateCustomProperties를 추가합니다. substituteDashedFunction에서는 tokens.appendVector(resolvedResult->tokens()) 직후, local RefPtr를 새로운 vector로 이동시킵니다. substitute에서는 기존의 m_intermediateTokenStrings.clear()와 쌍을 이루어, 실패 경로와 성공 경로 모두에서 새로운 vector를 비웁니다. 기존 로직의 구조 변경은 없으며, parallel lifetime anchor가 추가된 것이 변경의 전부입니다.

substitution 단계를 가로지르는 lifetime anchor가 누락되어, backing object보다 오래 살아남는 non-owning token view.

CSS Mixins는 @function을 도입하여 CSS 수준의 함수 정의(@function --name(--arg) { result: <tokens>; })를 가능하게 합니다. 이 함수는 custom property 값 안에서 dashed-function reference로 호출됩니다. Style::SubstitutionResolver는 style computation 도중 이러한 참조를 재귀적으로 확장합니다. 중첩된 var() / attr() / dashed-function 호출을 단일 token vector로 펼친 뒤, CSSVariableData::create를 통해 CSSVariableData를 생성합니다.

CSSVariableData는 파싱되지 않은 CSS 값의 storage 객체입니다. CSSParserToken은 외부 소유의 backing string에 대한 non-owning view를 저장하는 value-type token으로, backing string은 보통 upstream CustomProperty나 parser-allocated buffer가 소유하는 StringImpl payload입니다. Style::CustomProperty는 파싱/토크나이즈된 custom-property 값을 담는 reference-counted holder입니다. CustomProperty::tokens()를 호출하면 token이 반환되지만, 해당 token은 CustomProperty 자체가 소유하는 string storage를 참조합니다.

기존에 이미 m_intermediateTokenStrings 필드가 lifetime anchor로 존재했습니다. attr() tokenization 도중 생성되는 일시적인 string의 backing을 유지하여, 최종 CSSVariableData::create가 데이터를 재참조할 때까지 살아있게 하는 역할을 합니다.

패치 이전에는, substituteDashedFunctiontokens.appendVector(resolvedResult->tokens())를 호출한 뒤 반환될 때 local RefPtr<const CustomProperty> resolvedResult가 scope 종료 시점에 drop되었습니다. resolvedResult가 소멸되면, 해당 객체가 유지하던 StringImpl이 해제될 수 있었습니다. 이때 resolver의 tokens buffer에 이미 추가된 token view들은 여전히 그 storage를 가리키는 상태였습니다. 이후 substitute()CSSVariableData::create(*substitutedTokens, ...)가 dangling token을 참조하게 되고, 이 시점에는 이미 original backing이 소멸된 상태이므로 ASAN이 heap-use-after-free를 보고합니다.

이것은 "tokens가 외부 소유의 string을 view한다"는 전형적인 lifetime 문제입니다. SubstitutionResolver에는 attr() 경로에서 이미 동일한 문제의 해법이 적용되어 있었습니다. CSS Mixins / dashed-function 지원이 추가될 때 CustomProperty 소유의 token backing에 대해 동일한 문제가 재발했지만, 대응하는 lifetime anchor가 함께 추가되지 않았습니다. 기존 attr() lifetime fix에서 variant analysis를 수행하면 누락된 anchor를 기계적으로 도출할 수 있습니다.

최소 trigger는 다음과 같습니다. @function --f(--a) { result: aaaa var(--a); }를 정의하고, element에 --x: --f(bbbb)를 적용한 뒤 getComputedStyle().getPropertyValue('--x')로 resolution을 강제합니다. 패치 이전 코드 경로는 함수 body를 CustomProperty로 resolve하고, 해당 token을 local tokens buffer에 추가한 뒤, body에 대한 유일한 RefPtr를 해제합니다. 이후 CSSVariableData::create에 도달할 때 이미 stale한 token view를 참조하게 됩니다.

이 취약점을 ASAN crash 이상으로 무기화하려면, 공격자는 resolvedResult가 drop되는 시점과 CSSVariableData::create 호출 사이에 해제된 StringImpl slab을 재사용해야 합니다. 동일한 substitution chain 안에서 추가 CSS string allocation을 발생시키면 — 더 중첩된 @function / attr() 호출 등 — 해제된 buffer가 공격자가 제어하는 바이트로 채워질 가능성이 있습니다. 재사용이 성공하면 CSSVariableData::create는 원래의 token payload 대신 공격자가 선택한 바이트를 읽게 됩니다. 결과적으로 치환된 변수 값에 대한 controlled-content read가 이론적으로 가능합니다. 이후 CSSVariableData를 처리하는 downstream parsing이 제어된 바이트와 어떻게 상호작용하느냐에 따라, recycled buffer에 대한 confused-token parse primitive로 이어질 가능성도 있습니다.

이 취약점은 WebContent renderer process 내부의 memory safety를 약화시킵니다. dashed @function이 결과를 생성하고 그것이 외부 substitution에서 참조될 때마다, CSSParserToken payload가 substitution 과정 전반에 걸쳐 유효하게 유지되어야 한다는 lifetime 불변 조건이 위반되었습니다. @function 정의와 해당 함수를 사용하는 property를 포함하는 CSS를 제공할 수 있는 공격자라면, 대상 element의 style resolution이 발생할 때마다 freed-string read를 유발할 수 있습니다.

Note: 일부 세부 사항(CSSParserToken의 view semantics, attr() tokenization에 대한 m_intermediateTokenStrings의 역할, free 이후의 heap 재사용 경로)은 patch shape과 주변 코드로부터 추론된 것으로, diff에서 직접 확인되는 내용은 아닙니다. 핵심 UAF 메커니즘과 fix가 parallel lifetime anchor 역할을 한다는 사실은 patch에 의해 충분히 뒷받침됩니다.