Skip to content

A GPUDevice shared between JS runtimes crashes within seconds (iOS and Android) #455

Description

@AlirezaHadjar

What happens

Render a canvas from the Reanimated UI runtime, then touch the same GPUDevice from the React JS runtime, and the app dies in 4 to 12 seconds. No exotic API needed. queue.writeBuffer, queue.submit, device.createBuffer, buffer.mapAsync and context.present each do it.

Five signatures so far:

  • iOS, Metal assert in -[_MTLCommandEncoder dealloc]
  • iOS, Metal assert encodeSignalEvent:value: during Submit
  • iOS, Metal assert in -[_MTLCommandBuffer commit], reached from Instance::ProcessEvents
  • iOS, AddressSanitizer: heap-use-after-free in Dawn's RingBufferAllocator
  • Android, SIGSEGV on the JS thread inside BufferBase::MapAtCreation

I chased this on iOS first and assumed it was a Metal quirk. It is not. Android/Vulkan crashes too, and so does a Release build with no sanitizer linked, so the Metal asserts are not just validation noise.

Repro

Branch: https://github.com/AlirezaHadjar/react-native-webgpu/tree/repro/cross-runtime-device
It sits on main at 244ad53 and only touches apps/example.

It adds a screen under Diagnostics -> "Cross-Runtime Device Stress". The UI runtime renders a canvas through runOnUI while the React JS runtime hammers the same device from a setTimeout pump. Each mode picks which API family both sides hit.

  1. yarn build:ios or yarn build:android in apps/example
  2. Home, scroll to the bottom, tap Tests, then Cross-Runtime Device Stress
  3. It starts in write mode and dies in about 10 seconds. Tap submit, map or present for the other signatures.

On iOS you can pick the mode at launch:

xcrun simctl launch <udid> <bundle-id> -RNWebGPUCrossRuntimeMode map

Watch the ui: and js: counters under the title. If either stops climbing, that runtime died and nothing is racing any more, so "it survived" means nothing.

What I measured

iPhone 17 Pro simulator, iOS 26.5, Debug with ASan, 30 seconds per mode:

mode result crash
write crash 12s -[_MTLCommandEncoder dealloc] <- EnsureBlit <- QueueBase::WriteBuffer
submit crash 6s ASan + encodeSignalEvent <- Submit
create survived
map crash 4s -[_MTLCommandBuffer commit] <- SubmitPendingCommandBuffer <- DeviceBase::Tick <- Instance::ProcessEvents
present crash 4s ASan + encodeSignalEvent <- Submit
churn survived
destroy survived
all crash 6s ASan + Submit

Release build for the simulator, no sanitizer in the image list: write crashes in about 10 seconds with the same -[_MTLCommandEncoder dealloc] stack.

Android emulator, API 36, arm64, Debug: submit crashes with SIGSEGV on the React JS thread inside createBuffer.

Stacks

iOS, both runtimes sitting in rnwgpu::GPUQueue::writeBuffer at the same time. Which thread aborts changes run to run:

abort -> __assert_rtn -> MTLReportFailure
-[_MTLCommandEncoder dealloc]
-[MTLSerializerBlitCommandEncoder dealloc]
dawn::native::metal::CommandRecordingContext::EnsureBlit()
dawn::native::metal::Device::CopyFromStagingToBuffer(...)
dawn::native::BufferBase::UploadData(...)
dawn::native::QueueBase::WriteBuffer(...)
wgpu::Queue::WriteBuffer(...)
rnwgpu::GPUQueue::writeBuffer(...)

iOS, the uploader's ring buffer torn up by one thread while another walks it:

AddressSanitizer: heap-use-after-free on address 0x0001047986c0
#1 dawn::SerialQueue<..., RingBufferAllocator::Request>::Enqueue(...)
#2 dawn::native::RingBufferAllocator::Allocate(...)
#3 dawn::native::DynamicUploader::Reserve(...)
   -> BufferBase::UploadData -> QueueBase::WriteBuffer -> rnwgpu::GPUQueue::writeBuffer
freed by thread T7 here: same Enqueue frame
previously allocated by thread T7 here: same Enqueue frame
T7 created by -[RCTJSThreadManager startJSThread] (RCTJSThreadManager.mm:69)

iOS map mode. The JS thread aborts inside the event pump while the main thread encodes:

thread com.facebook.react.runtime.JavaScript (triggered)
  -[_MTLCommandBuffer commit] <- metal::Queue::SubmitPendingCommandBuffer
  <- metal::Device::TickImpl <- DeviceBase::Tick <- InstanceBase::ProcessEvents
  <- rnwgpu::async::RuntimeContext::tick()
main thread
  -[MTLSerializerCommandEncoder endEncoding]

Android submit mode, tombstone on the React JS thread:

signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x76954777a8
tid: 10325, name: mqt_v_js
  #01 libwebgpu_dawn.so  dawn::native::BufferBase::MapAtCreation()+408
  #02 libwebgpu_dawn.so  dawn::native::DeviceBase::CreateBuffer(...)+368
  #03 libwebgpu_dawn.so  dawn::native::BufferBase::MapAtCreationInternal()+196
  #05 libwebgpu_dawn.so  dawn::native::DeviceBase::APICreateBuffer(...)+864
  #07 libreact-native-webgpu.so  rnwgpu::GPUDevice::createBuffer(...)+260

Environment

  • react-native-webgpu main at 244ad53
  • react-native 0.81.4, react-native-reanimated 4.3.1
  • iPhone 17 Pro simulator, iOS 26.5, Debug with ASan and Release without
  • Android emulator, API 36, arm64, Debug

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions