← All issues

[1] Unsafe argument capture in EXTDisjointTimerQueryWebGL2::queryCounterEXT()

Severity: High | Component: WebGL — EXTDisjointTimerQueryWebGL2 | 84075f4

WebGL 2 context를 가진 임의의 웹 페이지에서 WebGLQuery 객체에 대한 use-after-free가 발생한다는 점이 High로 평가된 근거입니다. Lambda 생성 시점부터 GC에 의한 파괴 시점까지의 타이밍 window는 공격자가 제어할 수 있습니다. Controlled UAF primitive로의 확장은 heap grooming 가능성에 달려 있으나, 이 부분은 검증되지 않았습니다. Dangling reference 경로 자체는 diff를 통해 신뢰도 0.95로 확인됩니다.

queueMicrotask에 전달된 lambda가 WebGLQuery& query 파라미터를 참조로 캡처했습니다. microtask는 비동기로 실행되기 때문에, 실행 전에 query 객체가 파괴될 가능성이 있었습니다. 수정 방법은 캡처 대상을 Ref smart pointer로 감싸는 것이었습니다.

Source/WebCore/html/canvas/EXTDisjointTimerQueryWebGL2.cpp

- protect(protect(context->scriptExecutionContext())->eventLoop())->queueMicrotask(protect(context->scriptExecutionContext())->vm(), [&] {
- query.makeResultAvailable();
+ protect(protect(context->scriptExecutionContext())->eventLoop())->queueMicrotask(protect(context->scriptExecutionContext())->vm(), [query = Ref { query }] {
+ query->makeResultAvailable();
});

이번 패치는 EXTDisjointTimerQueryWebGL2::queryCounterEXT() 내의 단일 호출 지점을 수정합니다. Lambda의 캡처 목록이 [&](모두 참조로 캡처)에서 [query = Ref { query }](reference-counted smart pointer로 캡처)로 변경되었습니다. Lambda 본문에서는 query.makeResultAvailable()이 smart pointer 역참조 문법에 맞게 query->makeResultAvailable()로 바뀌었습니다. 그 외 로직 변경은 없으며, 수정 내용은 전적으로 캡처 방식에 한정됩니다.

비동기 task에 전달된 ref-counted 객체를 참조로 캡처한 lambda에서 발생한 dangling reference.

EXT_disjoint_timer_query_webgl2 extension은 WebGL 2 성능 측정을 위한 GPU timestamp query 기능을 제공합니다. JavaScript에서 queryCounterEXT()를 호출하면, 현재 GPU 작업이 완료된 이후 query 결과를 available로 표시하기 위한 microtask가 예약됩니다.

queueMicrotask는 현재 JavaScript 실행이 완료된 직후, event loop로 제어가 돌아가기 전에 lambda를 비동기적으로 실행하도록 예약합니다. 이로 인해 lambda가 생성되는 시점과 실제로 실행되는 시점 사이에 시간적 간격이 생깁니다.

WebGLQuery는 GPU query를 나타내는 ref-counted WebGL 객체입니다. JavaScript 측에서 모든 참조를 제거하고 garbage collection이 실행되면 파괴될 수 있습니다. 한편 Ref는 WebKit의 intrusive reference-counting smart pointer입니다. 참조로부터 Ref를 생성하면 refcount가 증가하고, Ref가 유지되는 동안은 객체가 파괴되지 않습니다. queryCounterEXT의 주변 코드에서는 execution context, event loop, GL 객체에 대해 이미 protect() wrapper를 사용하고 있었습니다. 그러나 query 파라미터 자체는 일반 C++ 참조로 캡처되고 있었습니다.

Root cause는 동기적 scope를 넘어 실행되는 lambda 안에서 ref-counted 객체를 참조로 캡처한 것입니다. 수정 전에는 queryCounterEXTWebGLQuery& query를 파라미터로 받고, queueMicrotask에 전달된 lambda 내에서 [&]를 통해 이를 캡처했습니다. microtask는 비동기로 실행되기 때문에, lambda는 WebGLQuery 객체의 reference count를 유지하지 않습니다. queryCounterEXT 호출과 microtask 실행 사이에 JavaScript가 query 객체에 대한 모든 참조를 제거하고 garbage collection이 트리거되면, 해당 객체는 파괴됩니다. 이후 microtask가 실행되면 해제된 메모리에 대해 query.makeResultAvailable()이 호출됩니다. 이것이 use-after-free입니다.

이 취약점은 코드 리뷰를 통해 발견할 수 있습니다. 비동기 API에 전달된 lambda에서의 [&] 캡처 패턴은 잘 알려진 C++ antipattern으로, async scheduling 호출 지점의 캡처 목록을 검사하면 기계적으로 찾아낼 수 있습니다.

🔒

Explores the exploitation window and heap reclamation feasibility for this asynchronous lifetime bug

더 확인하려면 구독해 주세요

🔒

Multiple audit patterns identified for similar async capture bugs across WebGL and WebCore scheduling APIs

더 확인하려면 구독해 주세요