fix(💣): guard the picture swap in RNSkPictureRenderer - #4032
Conversation
|
Thank You for reporting this. do you have an example that would allow me to
reproduce the crash?
…On Fri, Aug 28, 2026 at 3:47 PM Afonso Jorge Ramos ***@***.***> wrote:
The crash
An app that replaces the drawn picture at a high rate (ours publishes ~30
pictures/second from an audio visualizer) intermittently dies with:
Abort message: 'Pure virtual function called!'
__cxa_pure_virtual
RNSkia::RNSkPictureRenderer::performDraw(...)::'lambda'(SkCanvas*)
RNSkia::RNSkOpenGLCanvasProvider::renderToCanvas(...)
RNSkia::RNSkPictureRenderer::performDraw(...)
RNSkia::RNSkView::requestRedraw()::'lambda'()
RNSkia::JniPlatformContext::notifyTaskReadyNative()
Reproduced on a Pixel 8 (RN 0.86, release build, v2.11.0): cycling between
different shader-based scenes while pictures stream in kills the process
within seconds.
The race
performDraw copies the picture to protect against concurrent replacement:
// Capture picture pointer to ensure thread safety - _picture can be// modified from the JS thread while we're drawing on the render thread
sk_sp<SkPicture> picture = _picture;
The copy itself is the race: copying an sk_sp is not atomic. When
setPicture on the JS thread replaces _picture at the same moment, the
render thread can read the old pointer while the replacement drops the last
ref, then ref() an already-destructed object. drawPicture on it fails in
__cxa_pure_virtual (or corrupts memory silently). #3588
<#3588> introduced this
copy and narrowed the window without closing it.
The fix
A std::mutex over the three places that touch _picture (setPicture,
getPicture, the copy in performDraw). The lock is held only for a pointer
copy or swap, so there is no contention worth measuring at any realistic
picture rate; drawing still happens outside the lock.
With this patch, the reproduction above survived 48 consecutive scene
switches plus mixed fullscreen transitions with the crash buffer empty,
where the unpatched build died within the first dozen.
------------------------------
You can view, comment on, or merge this pull request online at:
#4032
Commit Summary
- fbcea4d
<fbcea4d>
fix(💣): guard the picture swap in RNSkPictureRenderer
File Changes
(1 file <https://github.com/Shopify/react-native-skia/pull/4032/files>)
- *M* packages/skia/cpp/rnskia/RNSkPictureView.h
<https://github.com/Shopify/react-native-skia/pull/4032/files#diff-a6c51c178c8a086fe846bd1f34a3a34896af17db077c10209e9859e7da1d40be>
(24)
Patch Links:
- https://github.com/Shopify/react-native-skia/pull/4032.patch
- https://github.com/Shopify/react-native-skia/pull/4032.diff
—
Reply to this email directly, view it on GitHub
<#4032?email_source=notifications&email_token=AACKXVVJZENJ3XWG5JVRSPL5MGEPXA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DGOBVGIYTONJUG2THEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVRTG633UMVZF6Y3MNFRWW>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACKXVX7T2LRUH2Y5NRM7LD5MGEPXAVCNFSNUABFKJSXA33TNF2G64TZHM2DENJYGQ2TGMRVHNEXG43VMU5TKMRXHA4DINBRGIZ2C5QC>
.
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
|
|
I tried to hand you a single-file repro and have to report failure so far - three escalating attempts, none of which crash (yet!!):
So the trivial distillation doesn't carry it, and whatever the missing ingredient is I'd rather tell you that than hand you a repro that doesn't reproduce. My best guess is a much busier JS thread, or GC timing - the real app runs audio decode threads and a 30 Hz spectrum tap alongside. What does reproduce, reliably, is the app (private repo for now): ~30 The strongest evidence I can offer for the mechanism: with the two-hunk mutex from this PR applied locally, the exact cycling sequence that killed the process within four rounds survived eighteen plus mixed fullscreen stress, same pid throughout, crash buffer empty. Without it, dead in seconds. Happy to run any instrumented or diagnostic build you'd like against the reproducing setup. Full tombstone (v2.11.0, Pixel 8, Android 17) |

The crash
An app that replaces the drawn picture at a high rate (ours publishes ~30 pictures/second from an audio visualizer) intermittently dies with:
Reproduced on a Pixel 8 (RN 0.86, release build, v2.11.0): cycling between different shader-based scenes while pictures stream in kills the process within seconds.
The race
performDrawcopies the picture to protect against concurrent replacement:The copy itself is the race: copying an
sk_spis not atomic. WhensetPictureon the JS thread replaces_pictureat the same moment, the render thread can read the old pointer while the replacement drops the last ref, thenref()an already-destructed object.drawPictureon it fails in__cxa_pure_virtual(or corrupts memory silently). #3588 introduced this copy and narrowed the window without closing it.The fix
A
std::mutexover the three places that touch_picture(setPicture,getPicture, the copy inperformDraw). The lock is held only for a pointer copy or swap, so there is no contention worth measuring at any realistic picture rate; drawing still happens outside the lock.With this patch, the reproduction above survived 48 consecutive scene switches plus mixed fullscreen transitions with the crash buffer empty, where the unpatched build died within the first dozen.