← All issues

VMTraps Deadlock Fix in Suspended-Thread Lambda

82fbab5

Source/JavaScriptCore/runtime/VMTraps.cpp

+ auto expectedUID = optionalOwnerThread.value()->uid();
ThreadSuspendLocker locker;
sendMessage(locker, *optionalOwnerThread.value().get(), [&] (PlatformRegisters& registers) -> void {
auto signalContext = SignalContext::tryCreate(registers);
if (!signalContext)
return;
 
- auto ownerThread = vm.apiLock().ownerThread();
// We can't mess with a thread unless it's the one we suspended.
- if (!ownerThread || ownerThread != optionalOwnerThread)
+ // Use ownerThreadUID() instead of ownerThread() to avoid creating a temporary
+ // RefPtr<Thread> copy, which would acquire the Thread control block WordLock.
+ // If the suspended thread was frozen mid-unlock of that same WordLock,
+ // calling ownerThread() here would deadlock.
+ auto currentUID = vm.ownerThreadUID();
+ if (!currentUID || *currentUID != expectedUID)
return;
 
- Thread& thread = *ownerThread->get();
+ Thread& thread = *optionalOwnerThread->get();

VMTraps는 debugger, GC stop-the-world, watchdog timer 등에서 실행 중인 JavaScript VM을 비동기적으로 중단시킬 때 사용하는 JSC의 메커니즘입니다. SignalSender::work()mach thread_suspend()로 대상 VM thread를 정지시킨 뒤, frozen 상태인 동안 lambda를 실행하여 trap breakpoint를 설치하거나 소유권을 확인합니다. 이 lambda 내부의 모든 작업은 사실상 async-signal-safe여야 합니다. 즉, 정지된 thread가 실행 중 임의의 시점에 보유하고 있을 수 있는 lock을 획득하려 시도해서는 안 됩니다. RefPtr<Thread> 기반의 thread reference counting은 WordLock으로 보호되는 ThreadSafeWeakPtrControlBlock을 거치기 때문에, 무해해 보이는 ownerThread() 호출조차 잠재적인 deadlock 원인이 됩니다.

T5 (VM thread)                T2 (SignalSender)
  │                               │
  ├─ strongDeref()                │
  │   └─ acquire WordLock ──►  LOCK HELD
  │       [suspended here] ◄── thread_suspend(T5)
  │                               │
  │                               ├─ ownerThread()
  │                               │   └─ strongRef()
  │                               │       └─ acquire WordLock
  │                               │           └─ DEADLOCK

이 commit은 ownerThread() 호출을 ownerThreadUID()로 대체했습니다. ownerThreadUID()는 stored pointer에서 thread UID를 직접 읽어오므로, RefPtr copy를 생성하지 않으며 WordLock도 획득하지 않습니다. expectedUID는 thread 정지 전에 미리 캡처해두고, lambda 내부에서 비교하는 방식으로 소유권을 확인합니다.

arm64 debug build 환경의 JSC 테스트 인프라에서 전체적인 cascade hang을 유발하고 있었습니다. serial WorkQueue 위에서 deadlock 상태의 SignalSender 뒤에 대기 중이던 다른 VM들은 완료되지 못했고, 각각의 ~VM() destructor가 waitForCompletion()에서 무기한 블로킹되었습니다. 다만 이 패턴이 드러내는 위험은 더 일반적인 문제이기도 합니다. thread를 정지시킨 상태에서 해당 thread가 이미 보유하고 있는 lock을 다시 획득하려 시도하는 구조 자체가, mach thread_suspend() callback 내부에서 실행되는 모든 코드에 공통적으로 적용되는 hazard입니다.

🔒

The new ownership-check pattern and the broader sendMessage lambda surface have audit-worthy edge cases worth examining.

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