Skip to content

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

@unspokenlanguage

Description

@unspokenlanguage

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

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