[1] CSS @function substitution drops the CustomProperty backing its own tokens
A stylesheet and one getComputedStyle() call read freed token storage.
High. 일반 stylesheet와 getComputedStyle() 호출 한 번만으로 도달 가능한 style-resolution 경로에서, 해제된 heap으로부터 token payload를 읽어들입니다. Crash를 넘어서는 확장 여부는, resolver가 token을 다시 capture하기 전에 해제된 string slab을 공격자가 재점유할 수 있는지에 달려 있습니다.
CSS custom-property 값은 style 계산 과정에서 문자열이 아니라 token vector로 저장됩니다. 각 token은 다른 객체가 소유한 string storage에 대한 view만 갖습니다. Style resolution은 중첩된 substitution(var(), attr(), 그리고 CSS Mixins에서 새로 도입된 dashed @function 형태)을 하나의 평탄한 token buffer로 펼친 뒤, 이 buffer를 CSSVariableData::create()에 전달합니다. 이 함수는 token 데이터를 자신의 storage로 복사합니다. 이 구조가 안전하게 동작하려면, buffer에 token이 섞여 들어간 모든 객체가 이 최종 재capture 시점까지 살아있어야 한다는 invariant가 지켜져야 합니다.
관전 포인트: CSS @function을 정의하고 이를 이용해 property 값을 읽어오는 페이지는, 대상 property가 style resolution될 때마다 style resolver가 해제된 token storage를 읽게 만듭니다.
substitute()가 새로운CSSVariableData를 생성할 때까지 resolve된CustomProperty를m_intermediateCustomProperties에 보관하여 살려둡니다. 이는attr()tokenizer 출력에 이미 쓰이고 있던m_intermediateTokenStrings메커니즘과 동일한 방식입니다.
Source/WebCore/style/StyleSubstitutionResolver.cpp
Source/WebCore/style/StyleSubstitutionResolver.h
LayoutTests/fast/css/variables/dashed-function-result-token-lifetime.html
Patch Details
Header에는 기존 vector와 나란히 두 번째 lifetime-anchor vector가 추가되었습니다: Vector<RefPtr<const CustomProperty>> m_intermediateCustomProperties입니다. SubstitutionResolver::substituteDashedFunction에서는, tokens.appendVector(resolvedResult->tokens()) 직후에 지역 변수 RefPtr<const CustomProperty> resolvedResult를 함수 반환 시점에 소멸하도록 두지 않고 이 vector로 move하도록 변경되었습니다. SubstitutionResolver::substitute에서는, 기존에 m_intermediateTokenStrings를 clear하던 두 지점 모두에서 새 vector도 함께 clear됩니다. 하나는 substituteTokenRange가 값을 반환하지 못하는 실패 경로이고, 다른 하나는 CSSVariableData::create(*substitutedTokens, ...)가 반환된 이후의 성공 경로입니다. Regression LayoutTest는 @function --f(--a) { result: aaaa var(--a); }를 정의하고, 대상 요소에 --x: --f(bbbb)를 적용한 뒤 computed value를 다시 읽어들여 이 경로를 강제로 타게 합니다.
Re-entrancy 경계가 아니라 substitution 단계 사이에서, lifetime anchor가 누락되어 non-owning token view가 자신의 backing object보다 더 오래 살아남는 패턴입니다.
Background
이 코드가 있는 위치.
Style::SubstitutionResolver는 style 계산 중 임의의 substitution function을 resolve하는 역할을 담당합니다. CSSSubstitutionValue로부터 token range를 순회하면서 중첩된 var(), attr(), dashed-function 호출을 재귀적으로 펼치고, style builder가 소비할 최종 CSSVariableData를 만들어냅니다.
CSS Mixins와 dashed function.
CSS Mixins는 @function --name(--arg) { result: <tokens>; } 형태의 CSS 레벨 함수 정의인 @function을 도입합니다. 이 함수는 custom-property 값 안에서 dashed-function 참조로 호출되며, resolver는 함수 body를 평가한 뒤 그 결과 token을 호출자의 buffer에 섞어 넣는 방식으로 호출을 펼칩니다.
CSSParserToken과 그 backing storage.
CSSParserToken은 값 타입의 token으로, 외부에서 소유하는 backing string에 대한 non-owning view를 저장합니다. 이 backing string은 대체로 상위 CustomProperty나 parser가 할당한 buffer가 소유하는 StringImpl payload입니다. Style::CustomProperty는 파싱되고 tokenize된 custom-property 값을 담는 reference-counted holder입니다. CustomProperty::tokens()는 자신의 token들을 반환하지만, 그 token들이 가리키는 string storage의 소유권은 CustomProperty 자신에게 있습니다.
재capture 경계로서의 CSSVariableData::create.
CSSVariableData는 unparsed CSS 값을 담는 storage 객체입니다. Token vector로부터 이를 생성하는 과정에서 token 데이터가 새 객체 자신의 storage로 복사되며, 이 때문에 이 지점을 지나면 상위 owner들이 더 이상 살아있을 필요가 없어집니다.
m_intermediateTokenStrings.
Resolver는 이미 하나의 lifetime-anchor vector를 보유하고 있었습니다. m_intermediateTokenStrings는 attr() tokenization 과정에서 생성되는 임시 문자열을 보관하여, 최종 CSSVariableData::create가 데이터를 재capture할 때까지 해당 token backing을 살려둡니다. 이런 anchor idiom이 존재하는 이유는, resolver의 token buffer가 재귀적인 expansion 과정에서 여러 개의 독립적으로 소유된 소스로부터 view를 누적하기 때문입니다. 전체 expansion 범위를 자연스럽게 커버하는 단일 owner가 존재하지 않는 구조입니다.
Analysis
이 버그는 특정 expansion 경로에서 anchor가 누락되어 발생하는, StringImpl 기반 token payload의 heap use-after-free입니다.
substituteDashedFunction() substitute()
──────────────────────────── ────────────────────────────
resolvedResult = resolve(fn)
└─ CustomProperty (refcount 1)
└─ StringImpl backing
tokens.appendVector(
resolvedResult->tokens())
└─ views into that backing ──────┐
│
return → resolvedResult dtor │
└─ refcount 0 → backing freed │
│
└──► CSSVariableData::create(
*substitutedTokens)
reads freed backing ← UAF
위 흐름에서 substituteDashedFunction은 resolvedResult가 소유한 storage에 대한 view인 token들을 append한 뒤 반환합니다. 이 시점에 지역 변수인 RefPtr<const CustomProperty>가 소멸합니다. 만약 이것이 마지막 reference였다면, resolver의 커져가는 tokens buffer가 여전히 이 backing string들을 가리키고 있는 상태에서 해당 문자열들이 해제됩니다. 이렇게 dangling 상태가 된 view들은 이후 SubstitutionResolver::substitute 안의 CSSVariableData::create(*substitutedTokens, ...)로 전달되어 처리됩니다. 이 함수는 이미 존재하지 않는 storage로부터 token 데이터를 재capture하게 되는데, 바로 이것이 ASAN이 보고한 heap-use-after-free입니다.
Regression test는 동시에 가장 짧은 도달 가능성 증명이기도 합니다. Web content 관점에서 세 단계면 충분합니다.
var()참조를 포함하는 body를 가진@function --f(--a) { result: aaaa var(--a); }를 stylesheet에 정의합니다. Body 자체가 resolution을 필요로 하기 때문에, 새로운CustomProperty가 생성됩니다.- 대상 요소에서 dashed-function 호출을 통해 custom property를 설정합니다:
--x: --f(bbbb);. getComputedStyle(target).getPropertyValue('--x')로 computed value를 요청하여,SubstitutionResolver::substitute→substituteDashedFunction경로를 강제로 타게 합니다.
즉시 관찰되는 영향은 CSSVariableData::create가 token 데이터를 재capture하는 순간 발생하는 UAF read입니다. Instrumentation이 켜진 환경에서는 ASAN abort로, 그렇지 않은 환경에서는 항상 동일하게 재현되는 renderer crash로 나타납니다. Crash를 넘어선 weaponization을 시도한다면, 공격자는 resolvedResult가 drop되는 시점과 CSSVariableData::create 호출 사이의 window에서 해제된 string-backing slab을 재점유하려 할 것입니다. 예를 들어 같은 substitution chain 안에서 추가적인 CSS 문자열 할당(더 많은 중첩 @function이나 attr() 호출)을 발생시켜, 해제된 StringImpl buffer가 공격자가 제어하는 바이트로 재사용되도록 유도하는 방식입니다. 이 재사용이 성공한다면 CSSVariableData::create는 그 바이트들을 마치 원래의 token payload인 것처럼 읽어들이게 되고, 이는 substitute된 변수 값에 대한 controlled-content read로 이어집니다. 이후 결과로 만들어진 CSSVariableData에 대한 하위 파싱이 이 controlled 바이트와 어떻게 상호작용하는지에 따라, info-leak이나 재사용된 buffer 위에서의 confused-token parse primitive로까지 확장될 가능성이 있습니다.
발견 경위로는 ASAN-instrumented CSS fuzzing이 유력해 보입니다. Bug title 자체가 ASAN crash를 명시하고 있고, test case는 @function을 지원하는 grammar-aware CSS fuzzer라면 누구나 생성할 법한 최소 스니펫이며, token backing에 대한 heap-use-after-free는 ASAN이 안정적으로 잡아내는 유형의 버그이기 때문입니다. 앞서 m_intermediateTokenStrings를 도입했던 attr() lifetime fix로부터의 variant analysis 역시 마찬가지로 가능성이 있습니다. 일단 이 anchor 패턴이 알려지고 나면, dashed-function 경로에서 이것이 빠져 있음을 발견하는 일은 거의 기계적인 작업에 가깝기 때문입니다.
이 vulnerability는 WebContent renderer process 내부의 memory safety를 약화시킵니다. CSSParserToken payload가 substitution 지속 기간 동안 유효해야 한다는 lifetime invariant는, dashed @function이 만들어낸 결과가 외부 substitution에서 소비될 때마다 깨졌습니다. 따라서 @function 정의와 이를 사용하는 property를 담은 CSS를 배치할 수 있는 공격자라면, 해당 대상이 style resolve될 때마다 해제된 문자열 read를 유발할 수 있습니다.
Insight: SubstitutionResolver는 이와 정확히 동일한 위험에 대한 해법을 이미 갖고 있었습니다. attr() 경로를 위해 추가된 m_intermediateTokenStrings가 그것입니다. CSS Mixins 지원이 도입되면서 CustomProperty 소유의 token backing에 대해서도 같은 위험이 재발했지만, 이에 대응하는 anchor는 함께 추가되지 않았습니다. Non-owning view 타입(CSSParserToken, StringView, std::span)이 multi-stage resolver를 관통하도록 설계되어 있다면, view를 새로 기여하는 모든 code path는 최종 consumer가 재capture할 때까지 하위 owner의 lifetime을 함께 연장해야 합니다.
Audit directions
- Multi-stage substitution 경계를 넘나드는 non-owning token/view 타입.
Style::SubstitutionResolver에서 resolver-localtokensbuffer로 append하는 모든 경로 — 특히substituteAttrFunction,substituteVariableFunction,substituteInternalAutoBaseFunction, 그리고 향후 추가될 substitution helper들 — 를 점검하여, token이 append되는 각 owner가m_intermediateTokenStrings나m_intermediateCustomProperties(또는 이에 준하는 메커니즘)에CSSVariableData::create가 재capture할 때까지 anchor되어 있는지 확인해야 합니다.Source/WebCore/style/StyleSubstitutionResolver.cpp의substituteTokenRange부터 시작해서tokens.appendVector(...)를 수행하는 모든 callee를 따라가야 합니다. Code review에서는, 다른 객체의 token을appendVector하면서 바로 다음 줄에 대응하는 anchor append가 없는 패턴이 시각적으로 눈에 띄는 신호가 됩니다. - 더 짧게 살아있는 owner로부터의 token splicing.
CSSParserTokenpayload는 non-owning view이므로, 어떤CustomProperty/CSSVariableData의 token이 소스 객체보다 더 오래 살아남는 buffer로 섞여 들어가는 지점은 모두 UAF 후보입니다.Source/WebCore/css와Source/WebCore/style에서appendVector(.*->tokens()),tokens().subspan, 그리고 임시 객체로부터 복사하는CSSParserToken생성 구문을 검색하여, 각 호출 지점이 후속 token 소비 연산 동안 owner를 계속 살려두는지 확인해야 합니다. - 재capture 경계로서의
CSSVariableData::create. 이 호출 시점까지 lifetime을 맞추는 것이 표준 idiom입니다. Tree 전체에서CSSVariableData::create를 호출하는 모든 지점 — 특히 custom-function 평가,@property등록, shorthand expansion — 에 대해, "이미 scope를 벗어난 임시 owner로부터 가져온 token" 형태가 동일하게 존재하는지 확인해야 합니다. 구체적인 시작점으로는 같은 파일의resolveAndRegisterDashedFunctionArguments, 그리고 내부 resolver 호출이 반환한Vector<CSSParserToken>으로부터CSSVariableData를 생성하는 모든 helper가 있습니다. - Feature 추가 전반에 걸친 lifetime-anchor 일관성. 이미 명시적인 lifetime-anchor vector를 사용하는 subsystem이라면, 이후에 token을 생성하도록 추가되는 모든 code path는 이에 대응하는 anchor를 함께 추가해야 합니다.
StyleSubstitutionResolver와 CSS Mixins /@function구현에 대한 최근 commit들을 점검하여,m_intermediateTokenStrings도입 이후 추가된 다른 경로들에도 각각 대응하는 anchor가 있는지 확인해야 합니다.