wcandillon / wcandillon/react-native-webgpu

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

Open
#455 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
1.2k
Forks
70
Avg merge
1d 6h
Merged PRs (30d)
23

Description

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

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the Cross-Runtime Device Stress screen in apps/example from the referenced repro branch, using the write, submit, map, and present modes on iOS and Android. Read the reported Dawn and rnwgpu stack traces while watching the ui: and js: counters. Done means the shared GPUDevice no longer crashes and both runtime counters continue across the modes and platforms.

Written by the indexing model from the issue text.

Assessment

Tech stack
react-native
Domain
computer-graphics, mobile
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.