Shopify / Shopify/react-native-skia

use-after-free in RNSkPictureRenderer::performDraw / SkPicture ref-counting (SIGSEGV drawPicture + SIGTRAP SkRefCntBase::ref)

Open
#3,925 2 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
8.6k
Forks
647
Avg merge
1d 17h
Merged PRs (30d)
35

Description

Description

We are seeing sporadic native crashes in the picture rendering path. There
are two distinct signatures, both on the main thread inside
RNSkPictureRenderer::performDraw, and both consistent with the renderer
touching an SkPicture whose refcount has already dropped to zero (i.e. the
JS side replaced/released the picture while a redraw was in flight):

  1. SIGTRAPsk_abort_no_print fired from SkRefCntBase::ref() while
    performDraw copies its sk_sp<SkPicture> (resurrecting a dead object).
  2. SIGSEGV / EXC_BAD_ACCESSSkCanvas::drawPicture dereferencing a
    garbage pointer (one report flags "possible pointer authentication
    failure", consistent with a freed object).

We have collected 8 crash reports since 2026-07-01 with these signatures.
The crashes are sporadic and include occurrences while the app is idle on a
screen with a continuously animating canvas.

Environment

  • @shopify/react-native-skia: 2.6.4 (also inspected 2.6.5–2.6.9
    changelogs/commits — no picture-lifecycle changes found in that range)
  • React Native: 0.86.0, New Architecture enabled (Fabric), Hermes
  • iOS — reports below are from the iOS Simulator (Xcode 26.3 / 17C529,
    macOS 26.1 25B78, arm64); the app is also exercised on physical devices
  • Rendering: default Metal canvas provider (RNSkMetalCanvasProvider)
  • react-native-reanimated: not installed (JS-driven rendering only)

Stack signature 1 — SIGTRAP, refcount assert on a dead SkPicture

AayuPlus-2026-07-01-213531.ips (EXC_BREAKPOINT / SIGTRAP, main thread):

0  sk_abort_no_print()
1  SkRefCntBase::ref() const::'lambda'()::operator()() const
2  SkRefCntBase::ref() const
3  SkPicture* SkSafeRef<SkPicture>(SkPicture*)
4  sk_sp<SkPicture>::sk_sp(sk_sp<SkPicture> const&)
5  sk_sp<SkPicture>::sk_sp(sk_sp<SkPicture> const&)
6  RNSkia::RNSkPictureRenderer::performDraw(std::shared_ptr<RNSkia::RNSkCanvasProvider>)
7  RNSkia::RNSkPictureRenderer::renderImmediate(std::shared_ptr<RNSkia::RNSkCanvasProvider>)
8  RNSkia::RNSkView::requestRedraw()::'lambda'()::operator()() const
   … (std::function plumbing)

Reading of this stack: requestRedrawrenderImmediateperformDraw
copies the current picture sk_sp — and the copy constructor's SkSafeRef
hits Skia's "ref on 0-refcount object" abort, i.e. the picture was already
destroyed when the redraw ran.

Stack signature 2 — SIGSEGV inside drawPicture

AayuPlus-2026-07-08-121149.ips (EXC_BAD_ACCESS / SIGSEGV,
KERN_INVALID_ADDRESS at 0x0000beadd8b87e08 — possible pointer authentication failure, main thread):

0  SkCanvas::drawPicture(SkPicture const*, SkMatrix const*, SkPaint const*)
1  SkCanvas::drawPicture(SkPicture const*)
2  SkCanvas::drawPicture(sk_sp<SkPicture> const&)
3  RNSkia::RNSkPictureRenderer::performDraw(...)::'lambda'(SkCanvas*)::operator()(SkCanvas*) const
   … (std::function plumbing)
11 RNSkMetalCanvasProvider::renderToCanvas(std::function<void (SkCanvas*)> const&)
12 RNSkia::RNSkPictureRenderer::performDraw(std::shared_ptr<RNSkia::RNSkCanvasProvider>)
13 RNSkia::RNSkPictureRenderer::renderImmediate(std::shared_ptr<RNSkia::RNSkCanvasProvider>)

Same entry path, but here the dead picture survives the copy and crashes
inside drawPicture when its contents are dereferenced.

Reproduction context

We don't have a minimal reproducer yet (it's a sporadic race), but the app
context is:

  • Multiple <Canvas> components on screen, each rendering an imperative
    SkPicture: createPicture(cb) is called every frame from a
    requestAnimationFramesetState loop (~60fps), and the result is
    rendered through a single <Picture picture={p} /> element per canvas.
    Each frame therefore replaces the previous picture, which becomes
    garbage-collectable immediately.
  • Crashes occur sporadically, including while the app idles on the screen
    with the animating canvases (nothing else happening).
  • Frequency increased when we added a canvas with higher picture churn
    (more draws per picture, still one picture/frame) — but the SIGTRAP
    signature predates that change on the same 2.6.4, when our only Skia
    usage was declarative JSX canvases re-rendered via requestAnimationFrame
    • state (so the picture in question was the renderer's internal one).
  • 8 reports between 2026-07-01 and 2026-07-08 across normal dev usage.

Mitigations we applied (reduce frequency — may help locate the window)

  1. Two-frame picture retention on the JS side: keeping the last two
    SkPicture objects referenced in a ref so GC cannot finalize a picture
    that a not-yet-completed redraw may still read. This points at the
    window being between the JS-side release/GC of the previous picture and
    performDraw consuming it.
  2. Pausing the rAF loop when the screen is not focused (less churn,
    fewer chances to race).

Both reduce, not eliminate, the exposure — the race looks like it needs a
fix at the RNSkPictureRenderer level (e.g. taking a strong ref under the
same lock/thread that swaps the picture, or deferring picture destruction
to the render thread).

Possibly related open issues

  • #3589 — "iOS - EXC_BAD_ACCESS - Attempted to dereference garbage pointer"
  • #3744 — "fix: prevent EXC_BAD_ACCESS in Promise destructor after runtime
    teardown"

Crash reports

The two full (redacted) .ips crash reports exceed the issue body size
limit, so they are attached as a gist:
https://gist.github.com/iamdiwakar/69bbcef50c7ceb0f62f78493b4c76d6f

  • AayuPlus-2026-07-01-213531.ips — SIGTRAP signature
  • AayuPlus-2026-07-08-121149.ips — SIGSEGV signature

Redactions are limited to incident_id/crashReporterKey/modelCode;
all thread stacks, binary images (incl. UUIDs), and exception details
are intact. Happy to test candidate patches.

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 at RNSkPictureRenderer::performDraw and follow the requestRedraw → renderImmediate path, then inspect how the current SkPicture is swapped and retained; no source files or tests are named. Reproduce with multiple Canvas components creating a picture every animation frame through RNSkMetalCanvasProvider, and consider the work done when the reported SIGTRAP and SIGSEGV no longer occur during sustained churn.

Written by the indexing model from the issue text.

Assessment

Tech stack
ios, react-native
Domain
computer-graphics, mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.