Skip to content

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

Description

@iamdiwakar

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

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions