Skip to content

[#424] Fix: app.rive.runtime.kotlin.core.errors.RiveException: Accessing disposed C++ object RiveArtboardRenderer.#429

Open
dabrosch wants to merge 8 commits into
rive-app:masterfrom
dabrosch:dabrosch/issue-424
Open

[#424] Fix: app.rive.runtime.kotlin.core.errors.RiveException: Accessing disposed C++ object RiveArtboardRenderer.#429
dabrosch wants to merge 8 commits into
rive-app:masterfrom
dabrosch:dabrosch/issue-424

Conversation

@dabrosch

@dabrosch dabrosch commented Dec 8, 2025

Copy link
Copy Markdown

[#424] Fix: app.rive.runtime.kotlin.core.errors.RiveException: Accessing disposed C++ object RiveArtboardRenderer.

The crash is a TOCTOU race on the render worker: draw() can call resizeArtboard() (which reads renderer width/height) while another thread is deleting the renderer, so the C++ pointer is disposed between the hasCppObject check and the dimension read.

Master partially addressed this in #11831 by splitting resizeArtboard() into separate frameLock/fileLock sections, but resize still runs outside draw()'s frameLock. delete() can still win the race once requireArtboardResize is cleared and resizeArtboard() begins.

This PR keeps resize and draw under the same lock scope: acquire frameLock, then fileLock (matching controller.advance()), run resize if needed, then draw. resizeArtboard() is private and assumes its caller already holds both locks. The disposeDependencies() fileLock override is removed; with the new ordering, it would invert lock acquisition relative to draw() and deadlock.

RiveFileController.isActive is marked @Volatile so the worker thread sees UI-thread deactivation promptly inside draw().

Tests

The old dispose-during-draw tests used RiveFileController without a File, so draw() and activeArtboard fell back to synchronizing on different objects. They passed even when fileLock was removed from draw(). Tests now load R.raw.empty so both paths share File.fileLock, matching production RiveAnimationView usage.

Lock-order validation: removing fileLock from draw() fails the file-lock tests; removing frameLock fails the frame-lock / #424 tests.

Issue fixed: #424

…: Accessing disposed C++ object RiveArtboardRenderer.

[rive-app#424] Fix: app.rive.runtime.kotlin.core.errors.RiveException: Accessing disposed C++ object RiveArtboardRenderer.

- The crash is caused by calling resizeArtboard which attempts to get the width from the disposed C++ object, so to protect against this we move the call to inside of the frame lock.

- This nesting of the file lock inside of the frame lock looks to be correctly supported by the disposeDependencies function first releasing the file lock and then releasing the frame lock.
@dabrosch

dabrosch commented Dec 8, 2025

Copy link
Copy Markdown
Author

I haven't been able to repro the crash locally yet, we have only seen it in the field at a very low % (~0.025%) but will try the method the other user has described to confirm the fix works.

@dabrosch

dabrosch commented Dec 9, 2025

Copy link
Copy Markdown
Author

Here is my attempt at reproducing the crash, but it doesn't seem to repro: https://github.com/dabrosch/rive-android/tree/dabrosch/10.4.4-with-bug-demo

dabrosch referenced this pull request Dec 9, 2025
* fix(android): crash on artboard resizing

* test: add test to verify the crash

Co-authored-by: Gordon <pggordonhayes@gmail.com>
- Switched both Draw and resizeArtBoard to be under both the file lock and the frame lock.
- Removed test for resizeArtboard and made it private because we are already testing that the CppObject cannot be changed during draw.
- Updated some comments, I am still wondering if we really need the file lock before disposingDependencies, because the objects it is releasing look like they could be mutated even when the file or even frame lock is held.
- Tests were not even able to be ran without the Rive.init, so I added that.
val afterDeleteLatch = CountDownLatch(1)

// The renderer needs a valid artboard to proceed to accessing the cppPointer
val timeout = 1000L

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can try with lower numbers for the timeout, I just wanted to be safe.

Comment on lines -36 to -37
open fun resizeArtboard() {
if (!hasCppObject) return

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was not protected from race conditions because it didn't have the frameLock.

@HayesGordon

Copy link
Copy Markdown
Contributor

Thanks for the contribution @dabrosch. I'll tag @ErikUggeldahl to review this

Comment on lines +106 to +108
synchronized(controller.file?.lock ?: this) {
super.disposeDependencies()
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized this could cause a deadlock with the above code. I am not even fully convinced that we really do need a File Lock on this, but if we do keep it, the order of lock obtainment needs to match the order in draw, so Frame Lock then File Lock.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now have removed it.

… not also be synchronized on the file lock.

If left as is we would get a deadlock with the above draw function which gets the framelock and then gets the file lock.
@dabrosch

Copy link
Copy Markdown
Author

Thanks for the contribution @dabrosch. I'll tag @ErikUggeldahl to review this

It would be nice to stop having this crash :-) I just merged it with main.

@rostyslav-prokopenko-liven

Copy link
Copy Markdown

bump

dabrosch added 2 commits June 15, 2026 22:35
…lock ordering fix.

Keep resize and draw under frameLock then fileLock, make resizeArtboard
private with no internal locking, and drop disposeDependencies fileLock
override to avoid lock-order inversion with draw().
…ive volatile

- The prior dispose-during-draw tests used RiveFileController without a File, so draw() and activeArtboard synchronized on different fallback locks and would pass even when fileLock was removed from draw().
- Rewrote the suite against a real empty .riv file with shared fixtures, added a rive-app#424 resize/delete regression test, and deduped the file-lock mutation scenarios.
- Marked controller.isActive @volatile so draw()'s cross-thread early-out checks are
reliable.
@dabrosch

dabrosch commented Jun 21, 2026

Copy link
Copy Markdown
Author

Merged with main again, but noticed that the changes pushed since last time still did not actually fully fix the deadlock issue, so this PR is still needed (I just updated the PR description again).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash! app.rive.runtime.kotlin.core.errors.RiveException: Accessing disposed C++ object RiveArtboardRenderer.

3 participants