Skip to content

RiveNativePlugin crash during app termination — Metal command queue dealloc use-after-free #623

@bediaz

Description

@bediaz

Submission checklist

  • I have confirmed the issue is present in the latest version of the rive Flutter package
  • I have searched the documentation and forums and could not find an answer
  • I have searched existing issues and this is not a duplicate

Description

The RiveNativePlugin crashes during iOS app termination with a use-after-free in MTLResourceListPool.purge, triggered by the auto-generated .cxx_destruct releasing Metal ivars after the GPU context is partially torn down.

The crash occurs even when no Rive animation has been loaded or displayed — the plugin eagerly creates a
MTLDevice and MTLCommandQueue during registerWithRegistrar: → initWithTextures: (lines 211-212 of
rive_native_plugin.mm), so the Metal resources exist from app startup.

The issue: In dealloc (line 226), the plugin destroys the renderer context and clears GPU pointers. But
the Objective-C++ auto-generated .cxx_destruct runs after dealloc returns, releasing _metalDevice and
_metalCommandQueue ivars. By that point during system termination, the Metal device may already be
partially torn down, and the command queue's dealloc → MTLResourceListPool.purge accesses freed memory.

Expected behavior: App terminates cleanly without crash.

Reproduction steps / code

No special code needed — this crashes during normal app termination on iOS. The crash is intermittent and depends on system teardown ordering.

No Rive animation needs to be loaded.
The crash happens because RiveNativePlugin eagerly creates Metal resources on plugin registration (app startup), and the teardown order during app termination is unsafe.

rive_native_plugin.mm lines 211-212 (in initWithTextures:):
_metalDevice = MTLCreateSystemDefaultDevice();
_metalCommandQueue = [_metalDevice newCommandQueue];

.cxx_destruct releases these AFTER dealloc, when GPU may be torn down

Nil out _metalCommandQueue and _metalDevice explicitly in dealloc before the method
returns, so .cxx_destruct has nothing to release:

Upload your reproduction files / stack trace

e6e863006fd90fbcb933e97fce7a042c_crash_session_6d7dc9d4f0ed4509ba2b5f94cd667a39_DNE_0_v2_stacktrace.txt

Source .riv / .rev file

No response

Screenshots / video

No response

Rive Flutter package version

0.14.4

Flutter version

  • rive: 0.14.4
  • rive_native: 0.1.4 (transitive)
  • iOS 18.x, iPhone (AGXMetalG14 = A15/A16 GPU)
  • Flutter 3.35.7

Device

iPhone 13

OS version

iOS 18.6.2 and iOS 26.3.1

Additional context

Potential fix: Nil out _metalCommandQueue and _metalDevice explicitly in dealloc before the method
returns, so .cxx_destruct has nothing to release:

  • (void)dealloc
    {
    riveLock();
    if (_riveRendererContext != nullptr)
    {
    destroyRiveRendererContext(_riveRendererContext);
    _riveRendererContext = nullptr;
    }
    setGPU(nullptr, nullptr);
    riveUnlock();

    // Release Metal resources explicitly before .cxx_destruct runs,
    // to avoid use-after-free during system termination teardown.
    _metalCommandQueue = nil;
    _metalDevice = nil;
    }

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