Skip to content

fix(runtime-wry): Fix mobile crash: RefCell already borrowed - #15502

Merged
Legend-Master merged 3 commits into
tauri-apps:devfrom
bclarke123:fix/mobile-resume-suspend-deadlock
Jun 10, 2026
Merged

fix(runtime-wry): Fix mobile crash: RefCell already borrowed#15502
Legend-Master merged 3 commits into
tauri-apps:devfrom
bclarke123:fix/mobile-resume-suspend-deadlock

Conversation

@bclarke123

@bclarke123 bclarke123 commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

On mobile, the wry event loop's Event::Resumed | Event::Suspended branch held a windows.0 RefCell borrow across both the per-window event handlers and the user RunEvent callback. If any of those mutates the window set — i.e. takes windows.0.borrow_mut() (creating or closing a window) — it panics with already borrowed: BorrowMutError. These events fire on every app foreground/background, so e.g. creating a window from a resume/suspend handler hard-crashes the app.

Creating/closing a window from a window/lifecycle handler is a supported pattern on desktop, so this is effectively a desktop→mobile parity crash.

MRE repo available here: https://github.com/bclarke123/tauri-resume-mre - install as normal, bun run tauri ios init or bun run tauri android init, run on device, follow repro instructions below

Root cause

#[cfg(mobile)]
e @ Event::Resumed | e @ Event::Suspended => {
let windows_ref = windows.0.borrow();            // borrow held...
windows_ref.values().for_each(|window| {
  ...
  for handler in listeners.values() { handler(&event); }            // ...across handlers
  callback(RunEvent::WindowEvent { label, event: event.clone() });  // ...and the callback
});
drop(windows_ref);
}

The window-mutating dispatch runs inline on the main thread (send_user_message short-circuits to handle_user_message when already on the UI thread), so it re-enters windows.0.borrow_mut() while the borrow above is still held → panic.

The three desktop WindowEvent branches (UserEvent synthesized window events, SynthesizedWindowEvent, and Event::WindowEvent) all drop the windows borrow before invoking the callback/handlers — precisely so they can mutate windows. This mobile branch was the exception; that asymmetry is the bug.

Fix

Collect the per-window listener handles, release the windows borrow, then dispatch - matching the desktop branches. Dispatch order (handlers then callback) is unchanged.

Reproduction

Minimal app: create a window from a resume/suspend handler.

app.run(|app, event| {
  #[cfg(mobile)]
  if let tauri::RunEvent::WindowEvent { event: tauri::WindowEvent::Resumed, .. } = &event {
    let _ = tauri::WebviewWindowBuilder::new(app, "child", tauri::WebviewUrl::default()).build();
  }
});

Background the app, then foreground it. On unpatched Tauri this crashes:

thread '' panicked at .../tauri-runtime-wry-2.11.2/src/lib.rs:4086:19: RefCell already borrowed

iOS backtrace (abridged) shows the re-entry clearly: will_resign_active → handle_event_loop → for_each (windows.0.borrow) → RunEvent callback → WebviewWindowBuilder::build → create_window → send_user_message (inline) → handle_user_message → RefCell::borrow_mut (lib.rs:4086).

Reproduced on a physical device on both iOS and Android (identical panic at lib.rs:4086, RefCell already borrowed). With this fix applied, both platforms create the window cleanly with no crash through repeated background/foreground cycles.

Testing

No automated test: the branch is #[cfg(mobile)] and reproducing requires the live tao/wry event loop emitting Resumed/Suspended plus a window-mutating handler — which the desktop-based test suite can't exercise. Verified to compile on the affected target:

cargo check -p tauri-runtime-wry --target aarch64-apple-ios

plus the manual on-device red→green above (iOS + Android).

Notes

  • Single file: crates/tauri-runtime-wry/src/lib.rs.
  • No public API change; not breaking.

@bclarke123
bclarke123 requested a review from a team as a code owner June 7, 2026 15:54
@sftse

sftse commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

This is less of a review than a shooting-from-the-hip initial remark, but while this fixed the crash it isn't super obvious from the description if the original implementation assumed the data under the RefCell is not modified.

If the callback tries to borrow_mut the RefCell it is mutating it in some way, is this change now still behavior-preserving? (I can probably answer that myself when doing a real review)

@bclarke123

bclarke123 commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

while this fixed the crash it isn't super obvious from the description if the original implementation assumed the data under the RefCell is not modified.

It's a good point to raise - on some level the original did assume that the data under the RefCell wasn't modified, but the method of enforcing it (panicking) wasn't ideal. With this change, any code that didn't previously panic should work exactly the same, but behind the scenes, the borrow on windows.0 is released earlier. For code that previously panicked (attempts to mutate the window set), the new behaviour is more well defined, delivering a snapshot of windows that existed when the event fired (which I believe is how the desktop WindowEvent code works already - eg this code - this is bringing mobile into parity with that).

@sftse

sftse commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Yes, a rephrasing of my imprecise question would be what invariants are expected to hold across the loop.

@Legend-Master Legend-Master left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks

(Didn't really test the possibilities of such panics, but since it aligns with the other similar code, the consistency is a win anyways)

@github-actions

Copy link
Copy Markdown
Contributor

Package Changes Through bce3221

There are 6 changes which include tauri-utils with patch, tauri-runtime-wry with patch, tauri with patch, tauri-bundler with patch, tauri-cli with patch, @tauri-apps/cli with patch

Planned Package Versions

The following package releases are the planned based on the context of changes in this pull request.

package current next
tauri-utils 2.9.2 2.9.3
tauri-bundler 2.9.2 2.9.3
tauri-runtime 2.11.2 2.11.3
tauri-runtime-wry 2.11.2 2.11.3
tauri-codegen 2.6.2 2.6.3
tauri-macros 2.6.2 2.6.3
tauri-plugin 2.6.2 2.6.3
tauri-build 2.6.2 2.6.3
tauri 2.11.2 2.11.3
@tauri-apps/cli 2.11.2 2.11.3
tauri-cli 2.11.2 2.11.3

Add another change file through the GitHub UI by following this link.


Read about change files or the docs at github.com/jbolda/covector

@Legend-Master
Legend-Master merged commit 056069a into tauri-apps:dev Jun 10, 2026
19 checks passed
Proksima pushed a commit to Proksima/tauri that referenced this pull request Aug 5, 2026
…pps#15502)

* fix(runtime-wry): RefCell borrow held across callbacks on mobile Resumed/Suspended

* Add .changes

* Rename mobile-resume-suspend-deadlock to mobile-resume-suspend-deadlock.md
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.

3 participants