rive-app / rive-app/rive-flutter

nativeTexture() crashes with SIGSEGV on freed MetalTextureRenderer* — missing nullptr guard

Open
#607 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Dart
Stars
1.5k
Forks
240
PR merge metrics
No merged PRs in 30d

Description

Description
The exported nativeTexture() function in pls_binding.mm is the only renderer API that does not guard against a nullptr renderer argument. When called with a freed or null pointer, it crashes with EXC_BAD_ACCESS (SIGSEGV) / KERN_INVALID_ADDRESS.

Every other exported function in the same file includes a null check:

// ✅ Has null check
EXPORT bool clear(MetalTextureRenderer* renderer, bool clear, uint32_t color) {
    if (renderer == nullptr) { return false; }
    renderer->begin(clear, color);
    return true;
}

// ✅ Has null check
EXPORT rive::Renderer* makeRenderer(MetalTextureRenderer* renderer) {
    if (renderer == nullptr) { return nullptr; }
    return renderer->renderer();
}

// ❌ Missing null check — crashes
EXPORT void* nativeTexture(MetalTextureRenderer* renderer) {
    return (__bridge void*)renderer->currentTargetTexture();
}

Crash Report

Process:               XXXX - [71482]
Code Type:             ARM-64 (Native)
OS Version:            macOS 15.7.3 (24G419)

Crashed Thread:        65  Dispatch queue: com.airplaystudio.mixer.render

Exception Type:        EXC_BAD_ACCESS (SIGSEGV)
Exception Codes:       KERN_INVALID_ADDRESS at 0x00006001e1d78e90

Thread 65 Crashed:
0   rive_native     nativeTexture + 8
1   our_plugin      [RiveTextureBridge registerRendererPointer:asSlot:]_block_invoke + 172

Reproduction Context
This crash occurs when nativeTexture() is called from a render loop on a background dispatch queue while destroyRiveRenderer() runs on the platform thread. The 1-second batched _disposeTexture() Timer in _NativeRenderTexture creates a window where the old MetalTextureRenderer* is freed but external callers may still hold the pointer.

Race window:

  1. performLayout() triggers makeRenderTexture() with new dimensions
  2. Old renderer is queued for destruction via 1-second Timer
  3. External render loop calls nativeTexture(oldRenderer) before onTextureChanged callback fires
  4. destroyRiveRenderer() frees the old renderer on the platform thread
  5. nativeTexture() dereferences the freed pointer → SIGSEGV

Suggested Fix

 EXPORT void* nativeTexture(MetalTextureRenderer* renderer) {
+    if (renderer == nullptr) { return nullptr; }
     return (__bridge void*)renderer->currentTargetTexture();
 }

Environment
Package: rive_native 0.1.2
Platform: macOS 15.7.3 (ARM64)
Flutter: 3.x
File:
native/src/pls_binding.mm , line 246–249

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 in native/src/pls_binding.mm around lines 246–249 and compare nativeTexture() with the other exported functions’ null checks. Trace the destroyRiveRenderer() and _disposeTexture() lifecycle described in the issue, then reproduce the render-loop and disposal timing if possible. Done means the reported nativeTexture() crash is prevented for the described invalid-renderer calls.

Written by the indexing model from the issue text.

Assessment

Tech stack
objective-c
Domain
computer-graphics, desktop-dev
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.