[1] Nested DrawDisplayList replay falls back to the singleton ControlFactory
The per-thread isolation held at every level except the one that made levels.
High. Context마다 격리되어야 할 handle이 정확히 한 단계의 nesting boundary에서 전파를 멈추면서, GPU-process render thread 4개가 하나의 공유 AppKit cell set 위로 합쳐집니다. 이 확장이 성립하려면 attacker가 이미 WebContent에서 code execution을 확보한 상태여야 합니다. 그 지점부터는 singleton fallback 자체는 항상 동일하게 발생하고, 다만 interleaving만 달라집니다.
WebKit의 GPU-process rendering pipeline에서 cross-thread isolation은 각 rendering client가 그림을 그릴 때 사용하는 platform object의 사본을 개별적으로 갖도록 하는 방식으로 유지됩니다. 이렇게 하면 thread-safe하지 않은 native widget을 두 thread가 동시에 건드리는 상황이 발생하지 않습니다. RemoteRenderingBackend는 하나의 client가 사용하는 2D rendering resource를 소유하는 GPU 측 객체인데, 각각 자신만의 전용 work-queue thread에서 동작하며 기록된 drawing command를 그 thread 위에서 replay합니다. 이때 기록되는 command 중에는 native form control을 그리는 DrawControlPart item이 포함되어 있는데, 이 item은 replay마다 개별적으로 전달받은 ControlFactory를 통해 렌더링됩니다. 바로 이 구조 때문에 process 전체에서 공유되는 ControlFactory::singleton()이 render thread에서 건드려지는 일이 원천적으로 없어야 합니다.
관전 포인트: 이미 compromise된 renderer가 control-drawing command를 한 단계 더 감싸면, GPU-process thread 4개가 동일한 공유 AppKit cell object를 동시에 변경하게 만들 수 있습니다. commit message에서는 이를 concurrent use-after-free로 보고하고 있습니다.
Commit message는 mechanism과 reachability를 모두 명확히 서술하고 있습니다.
DisplayList::applyItem()only special-casesDrawControlPart, so a nestedDrawDisplayListitem falls through toitem.apply(context)and calls the single-argGraphicsContext::drawDisplayList, which substitutesControlFactory::singleton(). A compromised WCP can wrapDrawControlPartitems in a nestedDrawDisplayListand replay it on multipleRemoteRenderingBackends, racing the singletonControlFactoryMac's sharedNSCellstate across GPU-process work-queue threads.
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
이번 수정은 세 부분이 함께 이루어졌습니다. 먼저 DisplayList::applyItem()에 DrawDisplayList를 위한 새로운 switchOn 분기가 추가되어, replay 범위의 ControlFactory&를 전달합니다(item.apply(context, controlFactory)). 이는 기존에 있던 DrawControlPart(factory)와 SetCTM(base transform)의 특수 처리와 나란히 놓이는 case입니다. 이전에는 DrawDisplayList가 일반 fallback인 [&](const auto& item) { item.apply(context); }에 그대로 걸렸습니다. DrawDisplayList::apply()의 시그니처는 apply(GraphicsContext&) const에서 apply(GraphicsContext&, ControlFactory&) const로 변경되었고, 본문도 nested list에 replay 범위의 factory를 전혀 전달하지 않는 형태였던 context.drawDisplayList(m_displayList)에서 context.drawDisplayList(m_displayList, controlFactory)로 바뀌었습니다.
ControlPart::setOverrideControlFactory()는 header에 inline으로 있던 setter에서 ControlPart.cpp의 out-of-line WEBCORE_EXPORT 정의로 옮겨졌습니다. factory가 그대로면 즉시 반환하고, 실제로 바뀐 경우에는 캐시된 m_platformControl(std::unique_ptr<PlatformControl>)을 비워, 이전 factory가 만든 PlatformControl이 새 factory 아래에서 재사용되지 않도록 합니다. 부수적으로 IPC Testing API를 통해 GPU process를 직접 구동하는 새로운 regression test와 glib TestExpectations의 skip 항목이 함께 추가되었습니다.
재귀적인 dispatch table을 거치면서 context별 isolation handle 전파가 불완전하게 이루어져, nested case가 조용히 process 전역 singleton으로 대체되는 패턴입니다.
Background
Where this lives.
WebCore는 그리기 작업을 DisplayList에 기록합니다. DisplayList는 Vector<Item>이며, Item은 Save, Clip, DrawControlPart, DrawDisplayList 등 여러 작은 command 클래스의 variant입니다. Replay는 이 vector를 순회하며 DisplayList::applyItem()을 호출하는데, 이 함수는 WTF::switchOn을 이용해 각 variant를 해당하는 apply() 메서드로 dispatch합니다.
Nesting.
DrawDisplayList는 payload로 또 다른 Ref<const DisplayList>를 갖는 display-list item입니다. 그래서 display list는 중첩될 수 있고, replay도 재귀적으로 이루어집니다.
The control-drawing layer.
ControlPart는 button, checkbox, radio, menu list, search field 같은 native form control을 플랫폼과 무관하게 표현하는 클래스입니다. ControlFactory로부터 createPlatformControl()을 통해 실제 플랫폼에서 그리는 객체인 PlatformControl을 얻고, 이를 mutable std::unique_ptr<PlatformControl> m_platformControl에 캐시한 뒤 updateCellStates()와 draw()로 구동합니다. ControlPart::controlFactory()는 m_overrideControlFactory가 설정되어 있으면 이를 반환하고, 그렇지 않으면 process 전역 ControlFactory::singleton()을 반환합니다. macOS에서는 구체적인 factory가 AppKit NSCell 객체를 기반으로 하는 ControlFactoryMac입니다.
GPU-process rendering IPC.
WebContent는 rendering backend마다 stream connection 위에 RemoteRenderingBackend를 하나씩 생성합니다. RemoteRenderingBackend의 생성자는 자체 IPC::StreamConnectionWorkQueue를 만들고, 각 handler는 assertIsCurrent(workQueue())를 통해 검증하므로, backend마다 자신만의 전용 thread에서 메시지를 처리합니다. RemoteDisplayListRecorder는 수신한 RemoteGraphicsContext 메시지를 DisplayList::RecorderImpl에 기록하고, SinkDisplayListRecorderIntoDisplayList가 이를 replay 가능한 DisplayList로 확정하면, RemoteImageBuffer의 context가 이를 replay합니다.
ThreadSafeRefCounted.
atomic reference counting을 제공하는 WTF의 base class입니다. refcount 자체가 race 없이 안전하다는 점은 보장하지만, 객체가 가진 멤버 상태까지 thread-safe하게 만들어주지는 않습니다.
IPC Testing API.
layout test가 다른 process에 raw하게 직접 작성한 IPC 메시지를 보낼 수 있게 해주는 테스트 전용 WebKit 기능입니다(IPCTestingAPIEnabled=true). compromise된 WebContent process가 보낼 수 있는 메시지를 시뮬레이션하는 데 사용됩니다.
Analysis
이 버그는 재귀적인 dispatch table에서 isolation-context 전파가 실패하는 문제이며, 결과적으로 thread-safe하지 않은 플랫폼 상태에 대한 data race로 드러납니다.
Backend A thread Backend B thread shared singleton
──────────────── ──────────────── ────────────────
replay outerDL replay outerDL
factory = A_factory factory = B_factory
item: DrawDisplayList item: DrawDisplayList
└► generic arm └► generic arm
apply(context) apply(context) ControlFactoryMac
no factory ─────────────┬───────────────► m_buttonCell
│ m_checkboxCell
30x DrawControlPart │ m_radioCell
updateCellStates(20px) ──────┤ ▲
(200px) ───┘ concurrent RMW ─────┘
applyItem()이 replay마다 별도의 ControlFactory&를 전달하는 이유는, control-drawing 작업을 호출한 context 자신의 factory instance로만 격리시키기 위함입니다. 이 전달은 추가 인자가 필요한 item 타입, 즉 DrawControlPart와 SetCTM만을 열거하는 WTF::switchOn으로 구현되어 있고, 그 외 나머지는 모두 일반 item.apply(context) 분기로 넘어갑니다. 이 중 DrawDisplayList는 유일하게 재귀적인 item으로, nested DisplayList 전체에 대해 replay 로직에 다시 진입합니다. 그런데 이 타입이 별도로 열거되지 않았기 때문에 일반 분기로 흘러들어갔고, factory 인자 없이 GraphicsContext::drawDisplayList(m_displayList)를 호출하게 되었습니다. 그 결과 nested replay는 자신의 part들에 replay 범위의 factory가 전혀 설치되지 않은 채로 진행되었습니다. isolation invariant는 depth-0 item에서는 유지되었지만, nesting 경계를 넘을 때마다 조용히 무너졌습니다.
이후 단계를 보면, ControlPart::controlFactory()는 m_overrideControlFactory ? *m_overrideControlFactory : ControlFactory::singleton()을 반환합니다. 그래서 nested item에 override가 설치되지 않은 상태라면, nested list 안의 모든 DrawControlPart가 singleton으로 귀결됩니다. ControlPart::platformControl()은 그 factory에 PlatformControl을 요청하며 createPlatformControl()을 지연 호출하고, ControlPart::draw()는 이렇게 얻은 객체에 대해 updateCellStates(borderRect.rect(), style)를 호출한 뒤 draw(...)를 이어서 호출합니다. ControlFactory는 ThreadSafeRefCounted이므로 refcount 자체는 안전하지만, 그것이 내어주는 플랫폼 상태에 대해서는 아무것도 보장하지 않습니다. macOS에서는 이 singleton이 ControlFactoryMac인데, 새 테스트의 inline comment와 commit message에 따르면 이 factory가 지연 생성하는 NSCell 멤버와 공유 control view가 updateCellStates/draw에 의해 변경됩니다. 따라서 여러 work-queue thread에서의 동시 replay가 동일한 플랫폼 객체를 아무런 동기화 없이 읽고 쓰게 됩니다. (DrawControlPart::apply(GraphicsContext&, ControlFactory&)가 setOverrideControlFactory를 통해 전달받은 factory를 part에 설치한다는 부분은, ControlPart::controlFactory()와 두 번째 fix hunk로부터 도출한 합리적인 추론이며, 제공된 context에서 직접 본문이 확인되는 사항은 아닙니다.)
fix의 나머지 절반은 이와 관련된 staleness 문제를 막습니다. ControlPart는 자신의 PlatformControl을 mutable std::unique_ptr<PlatformControl> m_platformControl에 캐시해두는데, 기존의 inline setOverrideControlFactory는 이 캐시를 무효화하지 않은 채 factory만 교체했습니다. 그래서 override가 바뀐 뒤에도 part가 이전 factory가 만든 PlatformControl을, 그리고 그 뒤의 플랫폼 상태를 계속 사용할 수 있는 상태가 남아 있었습니다.
regression test 자체가 하나의 trigger 레시피이므로, 그 구조를 단계별로 살펴볼 필요가 있습니다.
CreateRenderingBackend를 네 번 호출해, 각각 자신의IPC::StreamConnectionWorkQueuethread를 갖는RemoteRenderingBackend네 개를 만듭니다.- backend마다
CreateDisplayListRecorder를 호출한 뒤,ButtonPart(Button/DefaultButton/PushButton),ToggleButtonPart(Checkbox/Radio), 그리고MenuListPart,SearchFieldPart를 순환하는DrawControlPart메시지를 30개 보냅니다. 테스트의 comment에 따르면 이들은 각각 macOS factory의 서로 다른 지연 생성 cell 멤버에 대응합니다.SinkDisplayListRecorderIntoDisplayList가 이를innerDL로 확정합니다. - backend마다 두 번째 recorder가
DrawDisplayList(innerDL)메시지를 하나 받아outerDL로 확정되는데, 이 단계가 바로 nesting에 해당합니다. - backend마다 CG 기반
ImageBuffer를 생성하고, 네 connection 각각에서 그RemoteGraphicsContext에DrawDisplayList(outerDL)을 0x40번 전달합니다.
outer replay는 backend 자신의 ControlFactory를 가지고 applyItem()에 진입합니다. 이때 단일 DrawDisplayList item은 일반 분기에 걸려 factory 없이 context.drawDisplayList(m_displayList)를 호출하고, 30개 DrawControlPart item의 nested replay는 ControlPart::controlFactory()의 singleton 분기를 거치게 됩니다. backend마다 서로 다른 control 크기(20, 200, 40, 120)를 전달하므로, ControlPart::draw()의 platformControl->updateCellStates(borderRect.rect(), style)가 네 thread에서 동시에 같은 공유 cell에 서로 충돌하는 geometry를 기록하게 됩니다.
exploitability 측면을 살펴보면, 이 경로는 오직 compromise된 WebContent process에서만 도달할 수 있습니다. trigger를 발생시키려면 RemoteRenderingBackend/RemoteGraphicsContext IPC를 직접 작성해서 보내야 하며, 정상적인 WebKit recording 코드라면 context별 factory를 올바르게 설치합니다. 이 위치에서 출발하면, nested-replay 경로는 독립된 work-queue thread들에 걸쳐 항상 동일하게 ControlFactory::singleton()으로 수렴합니다. 이때 즉시 관찰되는 영향은 공유된 control-drawing 상태에 대한 동기화되지 않은 concurrent read-modify-write이며, 여기에 factory가 캐시하는 멤버들의 동시 지연 초기화도 함께 일어납니다. 지연 생성되는 멤버의 초기화가 중간에 끊기거나 두 번 일어나면, 이전 instance가 유실되거나 두 번 해제될 가능성이 있습니다. 만약 race가 걸리는 상태 안에 retain/release가 concurrent 경로끼리 서로 얽히는 reference-counted Objective-C 객체가 포함되어 있다면, over-release로 인해 여전히 참조되고 있는 cell이나 control view가 해제될 수 있습니다. 이 경우 다른 thread의 다음 updateCellStates/draw 호출이 이미 해제된 메모리를 건드리게 되는데, commit message는 이 결과를 정확히 "Concurrent NSCell UAF"라고 명시하고 있습니다. 다만 공유 control 상태의 retain/release 경로 자체는 제공된 context에 포함되어 있지 않으므로, 이 부분은 근거를 밝힌 추정으로 제시합니다. 이를 controlled primitive로 발전시키려면 추가로 GPU-process heap을 grooming해, 해제된 AppKit allocation이 살아남은 thread가 그것을 dereference하기 전에 attacker가 원하는 데이터로 재할당되도록 만들어야 합니다. 제공된 context에는 해당 객체들의 allocation size class나 field layout에 대한 근거가 포함되어 있지 않습니다.
이 vulnerability는 GPU process 내부의 thread-isolation을 약화시킵니다. 이 thread-isolation은 compromise된 WebContent process가 IOSurface, media, GPU driver에 접근할 수 있는 더 높은 권한의 process를 손상시키지 못하도록 막는 trust boundary에 해당합니다. 여기서 걸려 있는 security model의 전제는, 각 RemoteRenderingBackend의 replay work-queue thread가 오직 자신만의 context별 ControlFactory만을 건드려서 thread-safe하지 않은 플랫폼 control 상태가 thread 간에 절대 공유되지 않는다는 것입니다. nested DrawDisplayList replay는 모든 backend를 singleton으로 수렴시킴으로써 이 전제를 깨뜨렸습니다. WebContent에서 code execution을 확보한 attacker는 이를 이용해 GPU process 안의 공유 플랫폼 객체에 대해 동기화되지 않은 concurrent mutation을 유발할 수 있습니다. 이는 sandbox가 보호해야 할 권한 수준에서의 memory corruption이며, 웹 페이지에서 직접 도달 가능한 버그라기보다는 sandbox-escape chain의 한 연결 고리 후보에 해당합니다.
Insight
이 버그의 본질은 개별 item의 로직이 아니라 dispatcher 자체의 구조에 있습니다. applyItem()은 언뜻 모든 경우를 다 다루는 것처럼 보이는 WTF::switchOn이지만, default 분기가 있어서 case를 빠뜨려도 컴파일 에러가 아니라 조용한 누락으로 처리됩니다. 그리고 하필 재귀적으로 replay에 다시 진입하는 item 타입이, 추가 context 전파가 가장 필요한 바로 그 타입이었습니다. catch-all 분기와 재귀적인 alternative를 함께 가진 dispatch table은 모두 이런 위험을 안고 있습니다. invariant가 새로운 레벨을 만들어내는 그 지점을 제외한 모든 레벨에서만 지켜지는 셈입니다. 구조적으로 더 안전한 형태를 만들려면, auto fallback을 없애 컴파일러가 새로운 item 타입마다 어떤 replay context가 필요한지 명시하도록 강제하거나, factory를 받지 않는 GraphicsContext::drawDisplayList 형태를 replay 코드에서 아예 쓸 수 없게 만들어 singleton fallback에 암묵적으로 도달하지 못하도록 막을 수 있습니다. 한 가지 더 짚을 점은, ControlFactory가 ThreadSafeRefCounted라는 사실이 여기서는 은근히 오해를 부른다는 것입니다. 이는 handle 자체를 여러 thread에서 공유해도 안전하게 만들 뿐, 그 뒤에 있는 플랫폼 상태까지 안전하게 만들지는 않습니다. 바로 이 지점이 context별 factory로 막으려 했던 혼동입니다.
발견 경위를 보면, GPU-process display-list IPC 표면을 겨냥한 수동 auditing에 강한 variant-analysis 성격이 더해진 형태로 읽힙니다. glib TestExpectations의 hunk에는 이미 존재하던 인접 테스트 ipc/remotedisplaylistrecorder-drawcontrolpart-slidertrackpart-crash.html이 보이는데, 이는 DrawControlPart replay가 이전에도 이미 조사 대상이었음을 뜻합니다. 자연스러운 후속 질문은 어떤 replay 경로가 context별 factory 없이 DrawControlPart에 도달하는지를 찾는 것입니다. 테스트에서 타임아웃을 늘려 직접 작성한 StreamConnection과 WebCore::PlatformColorSpace에 대한 CoreIPC.typeInfo override는, 기성 fuzzer가 아니라 손으로 직접 다듬은 IPC harness 작업이었음을 시사합니다.
Audit directions
중첩된 dispatch 테이블이 재귀 호출 시 per-invocation context를 놓치는 패턴. dispatcher는 열거된 case들에 context를 전달하지만, catch-all 분기는 명시적으로 나열되지 않은 항목에 대해 이 context를 누락시킵니다. 문제는 이 invariant가 대부분의 레벨에서는 유지되지만, 새로운 레벨을 만들어내는 재귀 지점에서만 깨진다는 점입니다. 좁게 보면, Source/WebCore/platform/graphics/displaylists/DisplayListItem.cpp의 DisplayList::applyItem()을 점검해서, replay나 drawing으로 암묵적 기본값을 들고 다시 진입하는 다른 item alternative가 있는지 확인해야 합니다. 아울러 factory 없는 형태의 GraphicsContext::drawDisplayList 호출부를 모두 열거해서, replay 경로에 남아 있는 사례가 없는지 확인할 필요가 있습니다. 단서는 apply()가 context parameter를 기본값 처리하거나 아예 생략한 함수로 이어지는 item입니다. 조금 더 넓게 보면, dispatcher가 context를 전달받는 동일한 패턴은 WebKit의 다른 visitor에서도 나타납니다. [&](const auto&) fallback을 가지면서도 다른 분기들은 추가 state를 받는 variant가 그 대상입니다. display-list, filter(FilterResults), serialization 계층의 다른 WTF::switchOn 지점들을 확인해야 하며, 같은 파일 안의 shouldDumpItem/dumpItem에서도 동일한 누락이 반복되는지 점검할 필요가 있습니다. 코드 검색 결과에서의 단서는, lambda들이 서로 다른 argument list를 갖는 switchOn입니다. 가장 넓은 범위에서 보면, 재사용 가능한 invariant는 dispatcher가 per-invocation context를 나르는 경우, 재귀적인 모든 alternative는 반드시 열거된 case여야 하며, default 분기가 context를 기본값 처리하는 API에 도달할 수 없어야 한다는 것입니다. 이 원칙은 default case와 nested/compound node type을 함께 가진 모든 interpreter나 IR walker에 적용됩니다. V8/SpiderMonkey의 bytecode visitor, default를 가진 LLVM pass의 switch-on-opcode, Block variant가 있는 AST에 대한 Rust의 match와 _ => 구문이 모두 해당됩니다. 코드베이스를 넘나들며 대응시킬 단서는, default/_/auto 분기가 self-recursive한 node kind, 그리고 호출별 context parameter와 공존하는 형태입니다.
-
Per-context isolation object with a process-wide singleton fallback. override 설치를 빠뜨리는 경로는 조용히 shared state로 저하되며, 실패로 드러나지 않습니다. 좁게 보면,
Source/WebCore/platform/graphics/controls/와 GPU-process graphics 코드에서ControlFactory::singleton()을 grep하고,ControlPart::setOverrideControlFactory의 모든 호출자를 확인해야 합니다. 그리고ControlPart::draw/sizeForBounds/rectForBounds호출 이전에 모든 replay/drawing entry point가 per-context factory를 설치하는지 검증할 필요가 있습니다. 단서는foo ? *foo : Foo::singleton()형태의 accessor인데, override가 일부 호출자에서만 설정되는 경우입니다. 조금 더 넓게 보면, 동일한 형태는 WebKit이 instance handle과::singleton()default를 짝지어, GPU-process work-queue thread에서 그 default에 도달할 수 있는 모든 곳에서 나타납니다.IOSurfacePool,FontCache, 그리고RemoteRenderingBackend의assertIsCurrent(workQueue())handler에서 도달 가능한RemoteSharedResourceCacheaccessor들을 대상으로, 동일한 optional-override-with-singleton-default 패턴이 있는지 점검해야 합니다. 가장 넓은 범위에서 보면, invariant는 override가 격리를 제공하기 위해 존재하는 경우, fallback default가 shared mutable object여서는 절대 안 된다는 것입니다. default는 per-invocation이거나 hard error여야 합니다. 이 원칙은 thread-local 대 global allocator, per-request 대 global DI container scope, 서버의 per-connection 대 process-global cache, 그리고 request context를 의도했던 자리에 쓰인 Go의context.Background()에도 동일하게 적용됩니다. 대응 단서는return override ? *override : Global::instance();형태의 모든 accessor입니다. -
Lazily-built cache keyed implicitly on a mutable owner. owner를 바꿔도 cache는 invalidate되지 않습니다. fix에서
ControlPart::setOverrideControlFactory에 추가된m_platformControl = nullptr이 이를 바로잡는 조치에 해당합니다. 좁게 보면,ControlPart의 하위 클래스들과PlatformControl계층을 점검해서,controlFactory()에서 파생된 다른 state 중 override swap 이후에도 그대로 남아있는 것이 없는지 확인해야 합니다. 아울러 IPC로부터 deserialize된ControlPart가 한 process 안에서 서로 다른 두 factory 아래 replay될 수 있는지도 확인할 필요가 있습니다. 단서는 public setter를 가진 멤버로부터constgetter 안에서 채워지는mutable std::unique_ptr<X> m_x;입니다. 조금 더 넓게 보면, 동일한 형태는 platform에서 파생된 resource를 caching하면서 그 producer가 런타임에 설정 가능한 모든 WebCore 객체에서 나타납니다.Source/WebCore/platform/graphics/에서, settable factory나 context, device handle을 통해 도달 가능한 creator를 가진mutablecached member를 찾고, 각 setter가 cache를 clear하는지 확인해야 합니다. 가장 넓은 범위에서 보면, invariant는 memoize된 field가 파생되어 나온 값에 대한 모든 setter는 반드시 그 field를 invalidate해야 한다는 것입니다. 선언된 dependency set 없는 memoization은 일반적인 실패 패턴이며, React의useMemodependency array, connection swap 이후에도 살아남는 ORM identity map, 재생성 가능한 device를 키로 삼는 GPU pipeline-state cache에서 동일하게 관찰됩니다. 대응 단서는, public mutator를 가진 멤버를 읽어서 파생되지만 그 mutator 안에서는 invalidation이 이루어지지 않는 memoized field입니다. -
Refcount-safe handle, non-thread-safe payload. thread-safe한 refcounting base를 상속한 타입을 보면, 독자는 객체의 내용물까지 공유해도 안전하다고 오해하기 쉽습니다. 좁게 보면,
ControlFactory(ThreadSafeRefCounted<ControlFactory>)와ControlPart(ThreadSafeRefCountedAndCanMakeThreadSafeWeakPtr<ControlPart>)의 구현을 살펴서, draw 도중 건드려지는 mutable platform state가 있는지 확인해야 합니다. 그리고 이들 중 어느 하나라도 둘 이상의RemoteRenderingBackendwork-queue thread에서 동시에 도달 가능한지도 점검할 필요가 있습니다. 단서는 non-atomic mutable member를 가지면서 lock이 없는ThreadSafe*RefCounted클래스입니다. 조금 더 넓게 보면, GPU-process replay에서 도달 가능한ThreadSafeRefCounted타입들(image buffer, font/glyph cache, gradient/pattern object, filter result)을 모두 열거해서, 각각에 대해 thread-safety가 refcount를 넘어서 실제로 확립된 적이 있는지 물어야 합니다. 코드 검색상의 형태는, member를 write하는 non-constmethod와 짝을 이룬ThreadSafeRefCountedbase입니다. 가장 넓은 범위에서 보면, invariant는 atomic reference counting은 concurrent access 상황에서 객체 내부 invariant에 대해 아무것도 증명하지 못한다는 것입니다. 동일한 혼동은Arc<Mutex<T>>가 필요한 자리에Arc<T>를 잘못 쓰는 경우,shared_ptr을 concurrency guarantee로 착각하는 경우, 그리고 handle이 volatile이라는 이유만으로 안전하다고 가정되는 Java 객체에서도 동일하게 나타납니다. 대응 단서는, concurrency에 대한 유일한 대비책이 refcounting base class에만 있으면서, mutating method는 존재하고 선언부 어디에도 synchronization primitive가 없는 타입입니다.