[4] Nested DrawDisplayList replay ControlFactory confinement
Per-context isolation held everywhere — except one nested replay edge.
Severity를 High로 평가한 이유는, 침해된 WebContent process가 serialized display-list stream을 완전히 제어할 수 있고, 이를 통해 여러 RemoteRenderingBackend가 GPU process 안에서 공유되는 NSCell/WebControlView 객체를 동시에 변경하고 회수하도록 유도할 수 있기 때문입니다. 결과적으로 sandbox 경계를 넘나드는 race-conditioned use-after-free로 이어질 가능성이 있습니다. 다만 이 concurrency가 실제로 진짜 UAF로 이어지는지, 아니면 무해한 data race에 그치는지는 diff만으로는 확인되지 않으며, 이 점이 confidence를 제한하는 요인입니다. 다만 GPU process까지 영향이 미친다는 점을 고려하면 severity 자체는 낮아지지 않습니다.
DisplayList::applyItem()은 DrawControlPart만 별도로 처리하고 있어서, nested된 DrawDisplayList item은 일반 분기인 item.apply(context)로 떨어지게 되고 결국 single-arg 버전의 GraphicsContext::drawDisplayList가 호출됩니다. 이 single-arg 버전은 ControlFactory::singleton()으로 대체되는 경로입니다. 침해된 WebContent process는 DrawControlPart item들을 nested DrawDisplayList 안에 감싼 뒤, 이를 여러 RemoteRenderingBackend에서 replay시킬 수 있습니다. 이 과정에서 singleton인 ControlFactoryMac이 공유하는 NSCell 상태를 두고 GPU-process work-queue 스레드들 사이에서 race가 발생하게 됩니다. Fix는 ControlFactory를 DrawDisplayList::apply()까지 전달하도록 하고, applyItem()에서 DrawDisplayList를 별도로 처리하는 분기를 추가하여 nested replay가 per-context factory를 사용하도록 만듭니다.
Source/WebCore/platform/graphics/displaylists/DisplayListItem.cpp
Source/WebCore/platform/graphics/displaylists/DisplayListItems.cpp
Source/WebCore/platform/graphics/controls/ControlPart.cpp
LayoutTests/ipc/nested-display-list-draw-control-part-crash.html
Patch Details
세 가지 변경이 함께 맞물려, per-context ControlFactory&가 nested replay 경로까지 전달되도록 만듭니다. 먼저 DisplayList::applyItem()에 switchOn 분기가 추가되어, DrawDisplayList item에 대해서는 item.apply(context, controlFactory)가 호출됩니다. 이전처럼 일반 분기인 [&](const auto& item){ item.apply(context); }로 떨어지지 않도록 막는 변경입니다. 다음으로 DrawDisplayList::apply에 ControlFactory& 매개변수가 추가되어, ControlFactory::singleton()으로 대체되던 single-arg overload 대신 two-arg 버전인 context.drawDisplayList(m_displayList, controlFactory)를 호출하게 됩니다. 마지막으로 ControlPart::setOverrideControlFactory가 out-of-line으로 분리되고 export되며, override factory가 바뀔 때 m_platformControl = nullptr로 초기화하도록 강화되었습니다. 이를 통해 이전 factory로부터 만들어진 caching된 PlatformControl이 재사용되는 상황을 막습니다. Regression test는 DrawControlPart item들을 DrawDisplayList 안에 nested시킨 뒤, 이를 네 개의 RemoteRenderingBackend에서 동시에 replay합니다.
Nested/recursive dispatch 경로가 기본 handler로 떨어지면서 공유되는 process-wide singleton으로 대체될 때, per-context object confinement가 깨지는 패턴.
Background
WebKit은 그리기 연산을 Item variant들로 구성된 serializable한 DisplayList에 기록합니다. DisplayList::applyItem()은 WTF::switchOn을 통해 각 item을 dispatch하고, item의 apply()를 호출합니다. WebContent sandbox를 native graphics API로부터 격리하기 위해, 그리기 작업은 CoreIPC를 거쳐 GPU process의 RemoteRenderingBackend/RemoteGraphicsContext로 proxy됩니다. 이때 각 backend는 자신만의 work-queue 스레드에서 replay를 수행합니다. Native form control(버튼, 체크박스, 메뉴 등)은 ControlPart 서브클래스를 통해 그려지며, 이 서브클래스들은 ControlFactory로부터 platform cell을 받아옵니다. macOS에서는 ControlFactoryMac이 공유되는 NSCell/WebControlView 객체를 lazy하게 생성하고 caching합니다. ControlFactory::singleton()은 per-context override가 설정되지 않았을 때 사용되는 process 전역의 단일 factory이며, ControlPart::controlFactory()가 m_overrideControlFactory ? *m_overrideControlFactory : ControlFactory::singleton()을 반환하는 방식으로 결정됩니다. DrawDisplayList item은 또 다른 DisplayList 전체를 inline으로 replay하면서, replay 로직 자체로 재귀적으로 되돌아갑니다. IPCTestingAPIEnabled를 사용하면 JavaScript에서 raw CoreIPC 메시지를 직접 조작하여 전송할 수 있는데, 이는 침해된 WebContent process를 모델링하는 수단입니다.
Analysis
이 취약점은 공유되는 AppKit NSCell/WebControlView 객체를 대상으로 한 data race이며, 결국 동시성 use-after-free로 이어집니다. Fix 이전에는 applyItem()이 추가 context 인자가 필요한 item으로 DrawControlPart와 SetCTM만 별도 처리하고 있었습니다. DrawDisplayList를 포함한 나머지 모든 item은 일반 분기로 떨어져 single-argument 버전인 DrawDisplayList::apply(GraphicsContext&)가 호출되었고, 이는 다시 single-arg GraphicsContext::drawDisplayList(m_displayList)를 호출했습니다. 이 overload는 GPU process가 해당 backend를 위해 생성한 per-context ControlFactory를 전달받지 않습니다. 대신 nested replay 안의 DrawControlPart item들은 ControlPart::controlFactory()를 통해 factory를 결정하게 되는데, override가 없는 상태이므로 결과적으로 ControlFactory::singleton()을 반환하게 됩니다. GPU process는 각 RemoteRenderingBackend를 독립된 work-queue 스레드에서 실행하지만, ControlFactoryMac::singleton()은 단일한 공유 인스턴스이며 그 안의 lazy하게 생성되는 NSCell 멤버와 공유 WebControlView는 매 draw마다 변경되는 mutable state에 해당합니다. 원래 per-context factory를 전달하는 구조는 각 backend 스레드를 자신의 factory 안에 가두어 두기 위한 핵심 메커니즘이었습니다. 그런데 nested DrawDisplayList 경로가 이 confinement를 조용히 벗어나면서, 공유 singleton이 다시 개입하게 된 것입니다.
Backend A (WQ thread) Backend B (WQ thread)
───────────────────── ─────────────────────
nested DrawDisplayList nested DrawDisplayList
└─► DrawControlPart └─► DrawControlPart
resolves to ──┐ ┌── resolves to
ControlFactory::singleton() (shared)
│ │
updateCellStates/draw on the SAME NSCell / WebControlView
└────┴──► lazy (re)alloc + teardown race → UAF
Test가 모델링하는 exploit 방향은 다음과 같습니다. 먼저 여러 개의 RemoteRenderingBackend를 생성하고, 각각에 서로 다른 cell 멤버(ButtonPart/ToggleButtonPart/MenuListPart/SearchFieldPart)에 걸친 다수의 DrawControlPart item으로 구성된 inner list를 만듭니다. 그다음 각 inner list를 DrawDisplayList로 감싸 outer list를 구성하고, 이 outer list들을 빠른 루프 안에서 동시에 replay합니다. 각 nested replay는 control part를 ControlFactory::singleton()으로 resolve하게 되므로, work-queue 스레드들이 동시에 updateCellStates/draw를 호출하면서 같은 공유 객체를 lazy하게 재생성하게 되고, 이 과정에서 allocation과 destruction 사이에 race가 발생합니다. 만약 두 스레드가 공유 cell의 lazy 재생성이나 해제 시점을 두고 race를 일으키면, dangling된 AppKit 객체 pointer가 dereference되면서 GPU process 안에서 attacker가 timing을 조절할 수 있는 use-after-free로 이어질 가능성이 있습니다. Attacker는 part 종류, 크기, 반복 횟수, 동시성 정도를 제어할 수 있으며, 이는 heap layout과 timing에는 영향을 줄 수 있지만 해제된 객체의 내용 자체를 직접 제어하지는 못합니다.
이 취약점은 GPU process 내부의 memory-safety 및 thread-confinement 경계를 약화시킵니다. 원래의 security model은 각 backend가 per-context ControlFactory를 기준으로 replay를 수행하여 NSCell state가 여러 스레드 사이에서 공유되지 않도록 보장하는 것을 전제로 합니다. Nested DrawDisplayList replay는 이 invariant를 위반했습니다. 취약한 코드가 더 높은 권한을 가진 GPU process에서 실행되고, attacker가 이미 WebContent에서 raw IPC를 직접 구동할 수 있다고 가정하는 만큼, 여기서의 성공적인 UAF는 단순한 동일 process 내 버그를 넘어 sandbox escape로 이어지는 의미 있는 단계에 해당합니다.
applyItem()의 dispatch table 구조 자체도 유지보수 측면의 위험 요소입니다. 추가 ControlFactory(또는 baseTransform) context가 필요한 item 타입은 매번 명시적으로 열거되어야 하며, catch-all 분기는 누락된 item에 대해 정확성을 조용히 저하시킵니다. 어떤 subsystem이 global singleton에서 per-context instance로 이전할 때는, 모든 recursion/nesting/fallback 경로를 빠짐없이 점검해야 합니다. 단 하나의 경로라도 누락되면 전체 isolation 보장이 무너지기 때문입니다.
Note: drawDisplayList overload의 semantics, ControlFactoryMac singleton이 공유하는 mutable 멤버, GPU-process work-queue threading, 그리고 이 race가 무해한 data race가 아니라 실제 object-lifetime UAF에 해당한다는 점은 commit message와 주변 design으로부터 추론한 내용이며, 제공된 source에서 완전히 확인되는 사항은 아닙니다. Fix가 바로잡은 dispatch-table 누락 자체는 직접적으로 확인됩니다.
Audit directions
- 정확성이나 isolation을 위해 필요한 추가 context 인자를 누락시키는, catch-all 분기를 가진
WTF::switchOn(또는 visitor) dispatch.DisplayList::applyItem()과 유사한 variant dispatcher들을 점검하여, per-context state를 받아야 하는데도(sub-content를 재귀하거나 replay하는 모든 item) single-argapply(context)로 떨어지는 item 타입이 또 있는지 확인해야 합니다.Source/WebCore/platform/graphics/displaylists/에서switchOn을 검색하고, 각 item의applyoverload를applyItem에서 전달되는 인자와 비교해야 합니다. - Subsystem이 per-context instance로 이전한 이후, nested/recursive 경로에서 다시 등장하는 singleton fallback. GPU-process display-list replay 도중 도달 가능한 모든
::singleton()호출을 점검하여, per-context factory가 존재하는 상황에서 singleton이 호출되는 경우가 없는지 확인해야 합니다.Source/WebCore/platform/graphics/controls/에서ControlFactory::singleton()과controlFactory()를 검색하고, override가 설정되지 않은 채로 이들에 도달하는 replay 경로를 추적해야 합니다. - 여러 GPU work-queue 스레드에서 접근되는 공유 mutable native(AppKit/Objective-C) 객체.
ControlFactoryMac의 lazy하게 생성되는NSCell/WebControlView멤버와 유사한 caching된 platform 객체들이 per-context로 분리되어 있는지, 또는 동시 replay로부터 보호되고 있는지 확인해야 합니다.RemoteRenderingBackend의 work-queue threading을 조사하여, 다른 display-list item이 동시 replay 도중 공유 platform singleton을 resolve하는 경우가 없는지도 확인해야 합니다. - 다른
GraphicsContextdual-overload API도 점검해야 합니다 (drawDisplayList처럼 single-arg vs context-carrying 형태로 나뉘는 API). 한쪽 variant가 조용히 default global을 대입하는 overload 쌍이 있는지 검색하고, 이런 singleton-substituting single-arg 형태를 실수로 선택하는 호출부가 있는지 확인해야 합니다.