wcandillon / wcandillon/react-native-webgpu
A GPUDevice shared between JS runtimes crashes within seconds (iOS and Android)
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:duringSubmit - iOS, Metal assert in
-[_MTLCommandBuffer commit], reached fromInstance::ProcessEvents - iOS,
AddressSanitizer: heap-use-after-freein Dawn'sRingBufferAllocator - Android,
SIGSEGVon the JS thread insideBufferBase::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.
yarn build:iosoryarn build:androidinapps/example- Home, scroll to the bottom, tap Tests, then Cross-Runtime Device Stress
- It starts in
writemode and dies in about 10 seconds. Tapsubmit,maporpresentfor 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
mainat 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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