[6] PathCG should use thread-safe scratchContext
Two locks, one CoreGraphics context, and they never excluded each other.
Medium — 이전 fix에서 추가된 두 개의 lock은 서로를 배제하지 못했습니다. 그 결과 공유된 CoreGraphics context가 web content로부터 계속 동시에 변경 가능한 상태로 남아 있었습니다. 확인된 영향은 안정적으로 재현 가능한 renderer crash이며, 더 강력한 primitive로 이어지는지는 race 대상이 되는 graphics-state 필드가 heap 기반의 dash array인지에 달려 있습니다. 다만 closed-source인 CG 내부 구현으로는 이 부분이 확인되지 않습니다.
Stroke된 path에 대한 기하학적 질문에 답하려면 — 특정 point가 stroke 위에 있는지, stroke의 bounds가 무엇인지 — stroke parameter를 담고 있는 CoreGraphics context가 필요합니다. 그래서 WebKit은 화면에 그려지지 않는 일회용 "scratch" context를 유지합니다. 원래 이 context는 process 전역 singleton이었습니다. Main thread만이 stroke geometry를 계산했기 때문에 이 방식은 안전했습니다. 그런데 OffscreenCanvas를 Worker로 transfer하는 기능이 도입되면서 상황이 달라졌습니다. isPointInStroke()가 이제 worker thread에서 실행되는 반면, main thread는 paint를 위해 SVG stroke bounding box를 계산합니다. 이 경우, 한 번에 한 thread만 scratch context를 변경한다는 invariant가 반드시 성립해야 합니다.
관전 포인트: 하나의 page가 두 개의 thread를 동시에 동기화되지 않은 상태로 하나의 공유 CoreGraphics context에 접근하게 만들 수 있습니다. 그 결과 renderer가 이후 읽어들이는 graphics-state 구조체가 손상됩니다.
Bug 313935의 fix는 불완전했습니다. 해당 fix는 PathCG::strokeContains()와 PathCG::strokeBoundingRect()에 각각 static Lock을 추가하여 두 함수를 독립적으로 thread-safe하게 만들었습니다. 하지만 strokeContains()와 strokeBoundingRect()는 서로 다른 두 thread에서 동시에 호출될 수 있습니다. 이번 fix는 scratchContext()가 thread별 graphics context를 반환하도록 변경하여, 두 thread가 같은 context에 동시에 접근하지 못하도록 합니다.
Source/WebCore/platform/graphics/cg/PathCG.cpp
LayoutTests/fast/canvas/offscreen-isPointInStroke-svg-stroke-bounds-race-crash.html
Patch Details
WebCore::scratchContext()는 scratch CGContextRef가 process 전역이 아니라 thread별로 존재하도록 재작성되었습니다. 기존 구현은 createScratchContext()로 한 번만 초기화되는 static NeverDestroyed<RetainPtr<CGContextRef>> 하나를 유지했습니다. 새 구현은 static NeverDestroyed<ThreadSpecific<RetainPtr<CGContextRef>>>를 유지하며, 각 thread가 처음 접근할 때 context를 지연 생성합니다 (auto& result = *context.get(); if (!result) result = createScratchContext();).
공유 context가 사라지면서, 이전에 strokeContains()와 strokeBoundingRect()에 각각 추가되었던 함수별 static Lock scratchContextLock / Locker locker { scratchContextLock } 쌍이 삭제되었습니다. 이제 두 함수 모두 scratchContext()를 호출한 뒤 곧바로 CGContextSaveGState(context)로 진행합니다. 이번 commit에는 regression test도 함께 추가되었습니다. 이 test는 8개의 worker를 생성해 OffscreenCanvas.isPointInStroke()를 dash pattern을 바꿔가며 빡빡한 루프로 호출하는 동시에, main thread에서는 clip-path: inset(0) stroke-box가 적용된 20개의 SVG <path> element를 반복적으로 변경하여 매 paint마다 strokeBoundingBox() 재계산을 강제합니다.
하나의 공유된 mutable resource에 대한 접근을 호출 지점별로 분리된 lock으로 직렬화하려 했으나, 그 결과 서로를 배제하지 못하는 독립된 critical section이 만들어진 패턴.
Background
CGContextRef와 graphics state stack.
CoreGraphics context는 line width, line dash pattern, cap, join, CTM 등을 담은 graphics state stack을 유지하며, 이는 CGContextSaveGState / CGContextRestoreGState로 조작됩니다. 여기에 더해 mutable한 current path도 함께 갖습니다. CGContextSetLineDash는 호출자가 전달한 dash array를 현재 graphics state로 복사해 넣습니다. 즉 gstate는 호출자가 지정한 dash 개수에 따라 크기가 달라지는 heap 기반 buffer를 갖게 됩니다.
Scratch context pattern.
PathCG는 순수하게 기하학적인 질문에 답하기 위해 CGContext가 필요합니다. CGContextReplacePathWithStrokedPath는 현재 graphics state의 stroke 설정에 따라 current path를 stroke된 outline으로 변환합니다. 이런 목적이기 때문에 화면에 표시되지 않는 일회용 context를 유지하는 것입니다. createScratchContext()가 이 context를 생성하고, scratchContext()가 이를 반환합니다.
NeverDestroyed<T>와 ThreadSpecific<T>.
NeverDestroyed<T>는 destructor가 절대 호출되지 않는 함수 지역 static을 위한 WTF wrapper로, 의도적으로 leak시키는 방식의 process 수명 singleton을 만듭니다. ThreadSpecific<T>는 thread-local storage를 기반으로 thread마다 하나씩의 T instance를 제공합니다. *threadSpecific은 호출한 thread 자신의 instance를 반환하며, 처음 접근 시 default-construct되고 thread 종료 시 소멸됩니다.
함수 지역 static Lock.
함수 body 내부에 선언된 Lock은 그 함수의 모든 호출에 걸쳐 공유되는 단일 object입니다. 하지만 다른 함수 내부에 선언된 별개의 Lock은 완전히 별도의 object가 됩니다.
Worker 안의 OffscreenCanvas.
canvas.transferControlToOffscreen()은 OffscreenCanvas를 생성하며, 이를 transfer와 함께 Worker로 postMessage할 수 있습니다. 이후에는 worker thread가 2D context를 소유하게 되고, isPointInStroke()를 포함한 해당 API 호출은 main thread가 아닌 worker thread에서 실행됩니다.
stroke-box reference box.
Reference box가 stroke-box인 clip-path는 해당 element의 stroke bounding box를 기준으로 해석됩니다. 따라서 renderer는 paint 시점에 해당 element의 strokeBoundingBox()를 계산해야 합니다. d와 stroke-dasharray attribute를 변경하면 캐시된 값이 무효화되어 다음 frame에서 재계산이 강제됩니다.
Analysis
이 vulnerability는 공유된 mutable state에 대한 data race이며, CoreGraphics 자체의 allocation 내부에 memory-unsafety 결과를 초래합니다.
Worker thread Main/rendering thread
───────────── ─────────────────────
strokeContains() strokeBoundingRect()
Locker { lockA } ◄── distinct ──► Locker { lockB }
ctx = scratchContext() ─┐ ┌─ ctx = scratchContext()
├── SAME ───┤
CGContextSaveGState(ctx)│ CGContextRef │ CGContextSaveGState(ctx)
CGContextSetLineDash(...) CGContextSetLineDash(...)
CGContextReplacePathWithStrokedPath CGContextReplacePathWithStrokedPath
CGContextRestoreGState(ctx) CGContextRestoreGState(ctx)
└──── interleaved gstate push/pop and dash-buffer writes ────┘
두 함수 모두 context를 광범위하게 변경합니다. CGContextSaveGState, stroke-style applier(line width, CGContextSetLineDash를 통한 dash, join, cap), path 구성, 그리고 CGContextReplacePathWithStrokedPath가 여기에 해당합니다. Bug 313935에 대한 이전 fix는 이를 직렬화하려는 시도로 두 함수 각각의 내부에 static Lock scratchContextLock을 선언했습니다. 함수 지역 static은 각각 별개의 Lock object이기 때문에, 두 critical section은 서로 배타적인 관계가 아니었습니다. strokeContains()가 lock A를 쥐고 있는 동안 strokeBoundingRect()는 lock B를 쥐었고, 어느 쪽도 다른 쪽을 배제하지 못했습니다. 원래 성립해야 했던 invariant — 한 번에 한 thread만 공유된 scratch context를 변경한다 — 는 각 함수 내부에서만 pairwise로 강제되었을 뿐, resource 전체에 대해서는 강제되지 않았습니다.
CGContext의 graphics-state stack과 current path는 non-atomic한 mutable 구조체입니다. Interleave된 push/pop 쌍은 다른 thread가 push한 gstate를 pop해버릴 수 있고, heap allocation 기반의 gstate 필드 — 가장 눈에 띄는 예로 CGContextSetLineDash가 복사해 넣는 line-dash array — 는 한 thread가 여전히 참조하고 있는 동안 다른 thread에 의해 덮어써지거나 해제될 수 있습니다. Regression test가 dash array의 길이와 내용을 양쪽에서 의도적으로 다르게 변화시키는 것도 바로 이 지점을 겨냥한 것으로 보입니다. 이번 fix는 lock의 범위를 넓히는 대신 공유 resource 자체를 제거하는 방식을 택했습니다. 각 thread가 ThreadSpecific을 통해 자신만의 context를 갖게 되므로, disjoint critical section 문제 자체가 해소되고 lock은 더 이상 필요하지 않게 됩니다.
Regression test 자체가 trigger 방법을 그대로 보여주며, 특별한 권한 없이 web에서 완전히 도달 가능합니다.
createOffscreenCanvasWorkers()는 8개의 canvas를 만들고 각각에transferControlToOffscreen()을 호출한 뒤, 그 handle을 새로 생성한Worker로postMessage합니다. 각 worker는 종료 조건이 없는while (true)루프에 진입하여 15개 segment로 이루어진 bezier path를 구성하고, 2에서 7 사이를 순환하며 반복마다 길이가 달라지는 dash count로ctx.setLineDash(dashes)를 호출한 뒤ctx.isPointInStroke(64, 64)를 호출합니다.- 각
isPointInStroke호출은PathCG::strokeContains()로 이어집니다. 기존 코드에서는 이 함수가 자체 함수 지역 lock을 획득하고, 공유된CGContextRef를 가져온 뒤CGContextSaveGState와 stroke-style applier를 실행합니다. 이 과정에서 worker의 dash array가 공유 gstate로 밀어넣어집니다. - Main thread에서는
createSVGPaths()가 각각clip-path: inset(0) stroke-box스타일이 적용된 20개의<path>element를 생성합니다. 이로 인해 매 paint마다strokeBoundingBox()계산이 강제됩니다. animationLoop()는requestAnimationFrame마다 모든 path의d와stroke-dasharray를 다시 씁니다. 이렇게 하면 캐시된 stroke bounds가 무효화되고, 다음 paint에서PathCG::strokeBoundingRect()가 호출됩니다. 이 함수는 다른, 서로 무관한 lock을 획득한 뒤 같은 공유 context를 변경합니다.
두 lock이 서로 다른 object이기 때문에, 2단계와 4단계는 context에 대한 상호 배제 없이 동시에 실행됩니다. Test에 달린 주석 — "CGContextSetLineDash가 서로 다른 array를 allocate하도록 dash pattern을 다양하게 변경" — 은 작성자가 이 race를 무해한 scalar 필드가 아니라 graphics state의 heap 기반 dash-array 필드 쪽으로 유도하고자 했음을 시사합니다.
직접 확인 가능한 primitive는 높은 신뢰도로 재현 가능한, 통제된 형태의 rendering process crash입니다. 이 이상으로 확장될 가능성도 존재합니다. Race를 벌이는 두 호출 지점 모두 공유 context에 대해 CGContextSaveGState(context)를 실행하므로, interleave된 push/pop 시퀀스가 graphics-state stack을 desynchronize시킬 가능성이 있습니다. 또한 양쪽에서 공격자가 임의로 지정한 dash count로 CGContextSetLineDash가 호출되면, 한 thread의 gstate dash buffer가 다른 thread가 여전히 참조하는 도중 교체되거나 해제될 가능성도 있습니다. 이 torn-gstate 조건이 성립한다면, 공격자가 dash-array 길이를 통해 크기를 조절할 수 있는 CoreGraphics 내부 allocation에 대한 use-after-free 또는 double-free에 해당할 수 있습니다. 다만 이를 실제로 성립시키려면 좁은 timing window를 반복적으로 이겨내야 하고, CG allocator를 grooming하는 과정도 필요합니다. CoreGraphics는 closed-source이고 graphics-state 및 dash-buffer allocation 동작을 이 변경사항만으로는 관찰할 수 없기 때문에, torn graphics state에서 실제 사용 가능한 heap primitive로 이어지는 단계는 하나의 projection에 해당합니다. Diff와 regression test가 실제로 exercise하는 CGContextSetLineDash / CGContextSaveGState 호출에 근거를 두고 있지만, 어느 쪽도 이를 직접 입증하지는 않습니다.
두 함수 모두 layout 및 canvas state와 함께 WebCore에서 실행되는 기하학적 query, 즉 WebContent process 내부에서 실행됩니다. Race를 벌이는 두 thread는 WebContent의 worker thread와 WebContent의 main/rendering thread입니다. Sandbox 경계는 넘지 않으므로, 공격자가 WebContent를 벗어나려면 별도의 escape가 여전히 필요합니다.
이 vulnerability는 rendering process 내부의 memory safety를 약화시킵니다. 공유된 CoreGraphics scratch context가 한 번에 한 thread에 의해서만 변경된다는 thread-confinement 가정이 깨지기 때문입니다. 여기서 위협받는 security-model 가정은, main rendering thread와 worker thread 양쪽에서 접근하는 process 전역 mutable object가 완전히 직렬화되어야 한다는 것입니다. Fix 이전에는 이 직렬화가 부분적으로만 이루어지고 있었습니다. Worker에서 isPointInStroke()를 구동하는 동시에 main thread에서 stroke-bounding-box 재계산을 강제하는 script를 공격자가 실행하면, 손상된 CoreGraphics 내부 state에 도달할 수 있습니다. 최소한 이는 rendering process에 대해 안정적으로 재현 가능한 crash를 허용하며, 손상된 state에 line-dash array와 같은 heap 기반 gstate 필드가 포함된다면 더 강력한 memory-corruption primitive로 이어질 가능성도 있습니다.
Insight
이번 사례는 불완전한 fix의 후속 조치를 보여주는 전형적인 예시이며, 그 실패 양상은 시사하는 바가 큽니다. 원래 patch는 lock을 resource 옆이 아니라 호출 지점에 배치했습니다. 함수 지역 static Lock object 두 개는 얼핏 보면 "scratch context가 lock으로 보호되고 있다"는 인상을 줍니다. 하지만 실제로는 하나의 object에 대한 두 개의 독립된 mutex일 뿐입니다. 이는 리뷰어가 Locker 라인을 보고 안심한 채 넘어가기 쉬운 전형적인 패턴입니다. 최종적으로 채택된 fix는 더 견고한 방식입니다. Lock의 범위를 resource 전체로 넓히는 대신, ThreadSpecific을 통해 공유 자체를 제거함으로써 synchronization 문제를 confinement 문제로 전환했습니다. 이 tradeoff는 "공유된 scratch object" 패턴이 multi-threaded reachability와 만날 때마다 기억해둘 만한 가치가 있습니다. 이 사례가 보여주는 더 넓은 흐름도 있습니다. OffscreenCanvas-in-workers와 같은 기능은 오랫동안 main-thread 전용이었던 전역 상태를 소급적으로 multi-threaded reachability로 끌어올립니다. 그리고 이런 전역 상태는 점검되기 전까지는 잠재적인 버그로 남아 있습니다.
Audit directions
-
데이터 옆이 아니라 사용 지점에 선언된 lock이 문제를 만듭니다. 하나의 공유 리소스에 대해 서로 분리된 여러 critical section이 생기기 때문입니다. 지켜야 할 invariant는 가변 리소스는 정확히 하나의 lock으로 보호되어야 하고, 모든 mutation 지점이 동일한 lock을 잡아야 한다는 것입니다. 이 원칙을 지키기 어려운 이유는, 함수 내부의
static Lock이 그 함수만 놓고 보면 올바르게 읽히지만 전역적으로는 아무 의미가 없기 때문입니다. Narrow 범위에서는Source/WebCore/platform/graphics/안에서 함수 본문 내부에 선언된static Lock을 검색하고, 그 lock이 보호하는 객체가 다른 함수에서도 도달 가능한 함수-localstatic/NeverDestroyedsingleton인지 확인해야 합니다. 다른 path backend(PathCairo.cpp,PathSkia.cpp)와 동일한 scratch-object idiom을 쓰는FontCascade/glyph-cache helper부터 살펴보는 것이 좋습니다. Wider 범위에서는 같은 클래스가 다른 메커니즘을 통해서도 나타날 수 있습니다.std::once_flag로 보호되는 초기화 코드에 이후 unguarded mutation이 붙어 있거나, non-atomic composite 구조를 atomic flag로 보호하거나, 동일한 owner의 서로 다른 lock member를 각각 이름 붙인 두 개의Lockerscope가 존재하는 경우입니다. 코드 검색 결과에서 눈여겨볼 특징은, lock 객체가 그것이 가리키는 데이터보다 더 좁은 scope에 선언된 함수입니다. Widest 범위의 원칙은 lock granularity는 caller가 아니라 resource에 맞춰 정해져야 한다는 것이며, process-global mutable singleton을 가진 모든 코드베이스로 확장해서 적용할 수 있습니다. 예를 들어 Chromium의base::NoDestructor<T>와 별도로 선언된base::Lock조합, Rust의static Mutex<()>가 별도의static mut을 보호하는 패턴, 또는 공유 static field를 mutate하는 Javasynchronizedinstance method 등이 해당됩니다. 모든 단계에서 공통되는 match tell은 동일한 리소스를 mutate할 때 held될 수 있는 서로 다른 lock 객체의 개수를 세는 것입니다. 그 개수가 하나보다 많으면 hit으로 판단합니다. 코드 리뷰에서는 함수 본문 내부에 선언된static Lock을 발견하면, 그것이 보호하는 리소스와 동일한 lock을 잡는 다른 모든 함수를 명시하는 comment가 필요합니다. -
단일 스레드 실행 모델에서는 안전했지만 새로운 API surface로 인해 추가 스레드에서 도달 가능해지면서 안전하지 않게 된 global 변수가 두 번째 패턴입니다. Invariant는 file-scope 또는 function-local의 모든 mutable static은, worker-thread entry point가 그것에 도달할 수 있게 되는 순간 immutable하거나, confined되거나, 완전히 serialize되어야 한다는 것입니다. Narrow 범위에서는
Source/WebCore/platform/graphics/와Source/WebCore/html/canvas/를 점검하여, canvas 2D geometry entry point —isPointInStroke,isPointInPath,measureText, 그리고 stroke/fill bounding-box 경로 — 에서 도달 가능한 mutable platform object(context, buffer, cache)를 담은 function-localstatic NeverDestroyed<...>를 찾고, 각각이ThreadSpecific이거나 단일 resource-scoped lock으로 커버되는지 확인해야 합니다. Wider 범위에서는 최근 worker-thread reachability를 얻게 된 entry point들, 즉OffscreenCanvas(2D 및 WebGL),createImageBitmap, CSS Painting API worklet, WebCodecs 등을 나열하고, 각 call graph를 따라가며 처음 접촉하는 mutable process-global을 찾아야 합니다. 여기서 눈여겨볼 형태는 파일 전체에 locking이 전혀 없는scratch*,shared*,*Cache라는 이름의 helper입니다. Widest 범위의 원칙은 reachability가 확장되면 이전의 thread-safety 증명은 무효화된다는 것이며, Gecko의 OffscreenCanvas 작업이나 legacy singleton 위에 thread pool을 추가하는 라이브러리, request handler를 single-worker에서 multi-worker 배포로 옮기는 서비스에도 적용할 가치가 있습니다. Match tell은 유일한 안전성 근거가 "main thread only"라고 문서화되어 있으면서도, 현재 caller 목록에 non-main-thread frame이 포함된 mutable static입니다. -
Incomplete remediation, 즉 위반된 invariant 자체가 아니라 crash가 보고된 지점에만 fix를 적용해 sibling call site가 그대로 무방비 상태로 남는 패턴입니다. Invariant는 fix는 crash report에 나온 한 경로가 아니라, 깨진 상태(broken state)에 도달할 수 있는 모든 경로를 커버해야 한다는 것입니다. Narrow 범위에서는
PathCG에 한정해서,PathCG.cpp와GraphicsContextCG.cpp에 남아 있는 나머지 모든 함수를 다시 훑어보고 process-wide CoreGraphics object를 얻는 곳이 있는지, 그리고 sharing이 재도입되지 않았는지 확인해야 합니다. 이어서 다른 backend들의strokeContains/strokeBoundingRect대응 함수들도 동일한 조치를 받았는지 점검해야 합니다. Wider 범위에서는 diff 전체가 "한 함수에Locker를 추가"하는 것으로 끝나는 WebKit security fix들을 리뷰하면서, 각각에 대해 동일하게 guard된 객체를 mutate하는 다른 함수가 몇 개나 더 있는지 물어야 합니다. 코드 검색에서 눈여겨볼 형태는 정확히 하나의 함수 본문과 lock 선언만 건드리고 리소스의 선언 자체는 바꾸지 않은 commit입니다. 반대로 리소스의 ownership을ThreadSpecific, per-object, 혹은 immutable 형태로 바꾸는 fix는 더 강한 형태의 해법에 해당합니다. Widest 범위의 원칙은 이 fix가 reachable state space 자체를 바꾸었는가, 아니면 그중 한 경로만 막았는가라는 질문이며, 공유 helper의 한 caller에만 bounds check가 추가되고 helper 자체에는 추가되지 않은 CVE follow-up을 포함해 어떤 코드베이스의 post-mortem 문화에도 적용할 수 있습니다. Match tell은 patch된 함수가 동일한 객체에 write하는 여러 함수 중 하나이면서, patch가 그 객체의 선언에는 손대지 않는 경우입니다. -
새로 도입된
ThreadSpecific<RetainPtr<CGContextRef>>의 lifetime semantics를 점검할 필요가 있습니다. 이는 reference-counted이거나 framework가 소유하는 객체를 담은 thread-local storage로, destruction이 thread exit 시점, 즉 autorelease pool이 없거나 runtime이 부분적으로 torn-down된 상태 같은 이례적인 상황에서 실행되는 teardown 경로에서 일어납니다. Narrow 범위에서는 worker-thread exit 시RetainPtr<CGContextRef>의 release가 안전한지, 그리고NeverDestroyed<ThreadSpecific<...>>의 초기화 순서가 process shutdown 시 dangling TLS key를 남기지 않는지 확인해야 합니다. 여기서 tell은 TLS destructor에서 도달되는CFRelease/-release호출입니다. Wider 범위에서는 WebCore 안의 다른ThreadSpecific<>instantiation들을 검색해, element type이 단순 POD가 아니라 CoreFoundation, Objective-C, 혹은 GPU handle을 소유하는 경우를 찾아 각각 동일한 teardown 가정을 갖고 있는지 확인해야 합니다. Widest 범위의 원칙은 thread-local destructor는 저하된(degraded) context에서 실행되며 ambient runtime state에 의존해서는 안 된다는 것으로, POSIXpthread_key_createdestructor,Dropimpl을 가진 Rust의thread_local!, pooled-thread 환경의 JavaThreadLocal에도 동일하게 적용됩니다. Match tell은 단순히 메모리를 해제하는 데 그치지 않고 framework 안으로 호출을 이어가는, non-trivial destructor를 가진 TLS-held type입니다. 이 부분의 검증은 간단하지 않습니다. 정적 검사보다는 leak/ASan build 위에서 worker-churn stress test를 돌려야 확인 가능할 가능성이 높습니다. -
공유 상태를 per-thread 상태로 전환하면서 부수적으로 발생하는 메모리 증가도 점검 대상입니다. 이제 stroke geometry를 생성하는 각 스레드가
createScratchContext()를 통해 자신만의CGContext를 할당하게 되었으므로, 페이지가 다수의Worker가 각각isPointInStroke를 건드리도록 유도해 무제한적인 thread 생성을 강제할 수 있는지, 그리고 그것이 살아있는 CoreGraphics context 개수에 비례하는 결과로 이어지는지 살펴볼 필요가 있습니다. Narrow 범위에서는PathCG.cpp에 있는createScratchContext()의 allocation 크기를 확인하고, 현실적인 worker 상한값을 곱해 보아야 합니다. Wider 범위에서 눈여겨볼 형태는, 최근 shared 상태에서ThreadSpecific으로 전환되었으면서 per-thread 객체가 크고 thread 개수가 attacker에 의해 영향받을 수 있는 다른 사례들입니다. Widest 범위의 원칙은 confinement는 contention을 per-actor memory로 맞바꾸며, actor 개수가 attacker-controlled인 상황에서는 per-actor memory가 곧 availability surface가 된다는 것이며, 서버의 per-connection buffer, per-goroutine arena, thread-local allocator cache 전반에 일반적으로 적용됩니다. Match tell은 element가 non-trivial하게 allocate되면서, 그 owning thread가 untrusted input으로부터 spawn 가능한ThreadSpecific/thread_local입니다.