← All reports

[4] WebCore::Color: destructor leaves a usable out-of-line pointer in freed storage

LowWebCore graphicsUAF

d474526

Severity가 Low로 평가되는 이유는, 이 자체만으로는 도달 가능한 경로가 없기 때문입니다. 이 commit이 닫는 trigger는 없습니다. 대신 제거되는 것은 증폭 효과입니다. 패치 이전에는 Color에 대한 독립적인 UAF가 발생할 경우 freed slot에서 식별 가능한 heap pointer를 얻을 수 있었습니다. 패치 이후에는 같은 UAF에서 0을 얻게 됩니다.

하드웨어 memory tagging은 dangling pointer를 정상적으로 resolve되지 않고 fault를 일으키도록 만드는 것이 목적입니다. 다만 이 보장은 allocator가 메모리를 tagging하는 방식에 의존하는 속성이며, non-canonical 형태로 저장된 pointer는 이 보호 범위 밖에 놓입니다. WebCore::Color는 CSS parsing, painting, canvas, SVG 전반에서 사용되는 value type으로, 단일 word인 m_colorAndFlags로 구성되어 있습니다. 이 word는 inline sRGBA color와 flag를 함께 packing하거나, wide-gamut/float color data를 담은 ref-counted OutOfLineComponents를 가리키는 pointer를 저장합니다. Hardened allocator가 전제하는 기대치는, 객체가 destroy된 이후에는 그 storage에 usable한 capability가 남아있지 않아야 한다는 것입니다.

관전 포인트: defensive value 관점에서 의미가 있습니다. 이 commit 자체가 어떤 trigger를 열거나 닫지는 않습니다. 다만 Color에 대한 독립적인 use-after-free와 결합되었을 때, 남아있던 heap pointer를 0 word로 바꿔주는 역할을 합니다.

From the commit message:

WebCore::Color is made of a single field that contains flags in the upper bits and can eventually contain a pointer to an OutOfLineComponents as well.

This compact pointer is untagged by libpas, so we should manually clear it to prevent any security issues if the Color object is Use After Free'd.

Annotate secureZeroBytes and secureZeroSpan with NODELETE so the safer-cpp static analyzer can prove that calling them from ~Color() does not run any destructor or free memory.

Source/WebCore/platform/graphics/Color.h

inline Color::~Color()
{
if (isOutOfLine())
asOutOfLine().deref();
+ secureZeroBytes(m_colorAndFlags);
}

Source/WTF/wtf/StdLibExtras.h

template<typename T, std::size_t Extent>
-void secureZeroSpan(std::span<T, Extent> destination)
+void NODELETE secureZeroSpan(std::span<T, Extent> destination)
...
-template<typename T> void secureZeroBytes(T& object)
+template<typename T> void NODELETE secureZeroBytes(T& object)

두 가지 변경이 함께 동작합니다. 먼저 Color::~Color()에 기존 asOutOfLine().deref() 호출 뒤 secureZeroBytes(m_colorAndFlags) 호출이 추가되었습니다. 이를 통해 객체의 storage가 해제되기 전에 compact pointer/flags word가 덮어써지게 됩니다. 한편 StdLibExtras.h에서는 secureZeroSpansecureZeroBytesNODELETE annotation이 추가되었습니다. 이 annotation은 WebKit의 safer-cpp static analyzer가 이 함수들을 destructor에서 호출해도 다른 객체를 transitively destruct하거나 메모리를 해제하지 않는다는 사실을 증명할 수 있게 해줍니다. 이 annotation이 없으면 analyzer는 hardened destructor context에서의 해당 호출을 unsafe로 판단하여 거부합니다.

destructor에서 pointer를 담고 있는 필드를 scrub하지 않고 남겨두어, freed된 객체가 use-after-free로 재접근될 경우 exploitable한 잔재가 남는 패턴.

Memory tagging. MTE(Memory Tagging Extension)는 ARMv8.5의 하드웨어 기능입니다. 각 allocation은 4-bit tag를 가지며, pointer도 대응하는 tag를 가집니다. 두 tag가 일치하지 않으면 dereference 시 trap이 발생합니다. libpas는 WebKit의 userspace allocator로, MTE와 통합되어 free 시 메모리를 retag합니다. 이로 인해 대부분의 dangling pointer는 재사용된 메모리로 정상 resolve되지 않고 access 시점에 fault를 일으키게 됩니다.

Compact pointers. "Compact pointer"란 pointer를 다른 bit들과 함께 하나의 word에 저장하는 방식을 말합니다. 여기서는 m_colorAndFlags가 상위 bit에 pointer를, 그 아래에 flag bit들을 함께 담고 있습니다. 이 값은 일반적인 tagged pointer 형태로 저장되지 않기 때문에, 포함하는 객체가 해제될 때 allocator level의 retagging 로직이 자동으로 이를 clear하거나 retag하지 않습니다. 바로 이 지점이 compact pointer와 일반 member pointer가 MTE 하에서 갈라지는 부분입니다.

Color의 레이아웃. WebCore::Color는 단일 word로 이루어진 value type입니다. flag에 따라 inline sRGBA color를 packing하거나, wide-gamut/float color data를 담은 ref-counted OutOfLineComponents를 가리키는 pointer를 저장합니다. Color는 WebKit의 type-segregated heap인 TZone-allocated memory(WTF_MAKE_TZONE_ALLOCATED(Color))를 사용합니다.

secureZeroBytesNODELETE. secureZeroBytes는 write가 compiler에 의해 최적화로 제거되지 않도록 보장하는 zeroing primitive입니다. dying object에 대한 memset-to-zero의 고질적인 문제는, compiler가 이 write를 observable하지 않다고 판단해 삭제해버릴 수 있다는 점입니다. NODELETE는 WebKit safer-cpp의 annotation으로, 해당 함수가 어떤 destructor도 transitively 실행하지 않고 메모리도 해제하지 않는다는 것을 선언합니다. 이 annotation이 존재하는 이유는, allocator 코드로 재진입해서는 안 되는 context — 대표적으로 hardened type의 destructor — 에서도 static analyzer가 이런 함수 호출을 허용할 수 있도록 하기 위함입니다. teardown 도중 allocator로 재진입하는 destructor야말로 hardened destructor contract가 원천적으로 금지하려는 대상이기 때문입니다.

패치 이전 ~Color()는 out-of-line인 경우 OutOfLineComponents에 대한 reference를 해제한 뒤, 죽은 객체의 storage에 m_colorAndFlags를 그대로 남겨두었습니다. out-of-line color의 경우 이 word는 방금 reference가 끊긴 그 객체를 가리키는 pointer 그 자체입니다. 이 pointer가 canonical 형태가 아니라 flag bit와 함께 packing되어 있기 때문에, libpas의 MTE retagging은 이를 건드리지 않습니다. 결과적으로 freed slot에는 tagging hardware 상에서도 식별 가능하고 올바른 형태의 heap pointer가 그대로 남게 됩니다.

이 자체는 corruption을 만들어내는 logic bug가 아닙니다. 이 diff 안의 어떤 code path도 이 잔재를 읽지 않습니다. 정확히는, 다른 버그가 Color에 대한 UAF를 만들어냈을 때 그 residual capability를 무력화시켜줄 defensive scrub이 빠져 있었다는 문제입니다.

이런 결합 시나리오에서는 이 잔재가 두 가지 방식으로 힘을 발휘합니다. 먼저, attacker가 freed된 Color의 storage를 첫 word가 data로 읽히는 다른 type으로 reclaim하는 경우를 가정할 수 있습니다. 이때 남아있는 word는 OutOfLineComponents 객체의 주소를 노출시키며, 이는 heap layout disclosure에 해당합니다. 반대로 그 word가 살아있는 OutOfLineComponents*로 dereference되는 경우도 가정할 수 있습니다. 이 경우 freed된 ref-counted 객체에 대한 연산이 가능해지는데, 여기에는 deref() 경로와 reference-count 상태 조작이 포함됩니다. 즉 Color 자체의 결함이 아니라, upstream UAF 위에 쌓이는 type confusion입니다. 패치 이후에는 같은 reuse가 0을 읽게 되므로, dereference는 read/write primitive가 아니라 항상 동일하게 발생하는 null fault로 귀결됩니다.

이 commit 자체가 단독으로 여는 exploit path나 닫는 exploit path는 없습니다. 이 hardening은 Color에 대한 독립적인 UAF와 결합했을 때만 의미를 갖습니다. Color는 WebContent process(CSS, canvas, painting)와 GPU process(rendering) 양쪽에서 사용되므로, 이 hardening은 두 renderer-adjacent sandbox 모두에 적용되지만 그 자체로 어떤 boundary를 넘나드는 역할을 하지는 않습니다.

이 변경은 도달 가능한 trigger에 대한 fix라기보다 defense-in-depth 성격에 가깝습니다. Color 주변의 memory-safety boundary를 강화하는 변경으로, 이전까지는 "destroy된 ColorOutOfLineComponents에 대한 usable pointer를 남기지 않는다"는 invariant가 강제되지 않았습니다. 이 변경이 attacker의 capability를 확장시키지는 않습니다. 오히려 Color value를 보유하는 임의의 코드에서 발생 가능한 가상의 upstream UAF에 대한 증폭 surface를 줄여주는 역할을 합니다.

Insight: MTE 도입 과정에서 WebKit 전반에 걸친 hardening gap의 한 유형이 드러나고 있습니다. Compact pointer — pointer bit가 non-pointer flag bit와 하나의 word에 함께 packing된 형태 — 는 allocator level의 retagging에는 보이지 않기 때문에, tagging hardware 상에서도 free 이후 usable한 형태로 살아남습니다. 이와 동일한 패턴은 WebKit이 tagged-pointer 기법을 사용하는 모든 곳에서 나타날 수 있습니다. PackedPtr, tagged JSValue 계열 encoding, CompactPointerTuple, CompactRefPtr 등이 destructor에서 deref만 수행하고 scrub은 하지 않는 short-lived value type 안에 있다면, 모두 같은 처리가 필요한 후보에 해당합니다. NODELETE plumbing 자체도 인프라로서 주목할 만합니다. static analyzer가 검증 가능한 "safe destructor" contract를 WebKit이 공식화하고 있다는 신호로 볼 수 있습니다.