DioxusLabs / DioxusLabs/dioxus
macOS: closing a window frees the process-global menu bar → use-after-free on the next menu click
- Dominant language
- Rust
- Stars
- 39.1k
- Forks
- 1.9k
- Avg merge
- 4d 10h
- Merged PRs (30d)
- 4
Description
## Summary
`WebviewInstance` owns the `muda::Menu` for the window's lifetime:
```rust
// src/webview.rs:532
_menu: menu,
```
On macOS, `init_menu_bar` installs that same menu as the **process** menu bar:
```rust
// src/menubar.rs
#[cfg(target_os = "macos")]
{
menu.init_for_nsapp();
}
```
`init_for_nsapp` assigns `NSApp.mainMenu`, which is global and outlives any
window. When the owning window closes, `WebviewInstance` drops, dropping the
last `Rc` inside `muda::Menu` and freeing every `MenuChild`. `NSApp.mainMenu`
still points at the corresponding `NSMenuItem`s, which hold the child as a raw
pointer ivar. The next click on **any** menu item dereferences freed memory.
The menu's lifetime is scoped to a window; its installation is scoped to the
process. On macOS those disagree, and closing a secondary window is enough to
leave the app's own menu bar dangling.
## Reproduction
Any multi-window app with a custom menu (`Config::with_menu`):
1. Open a second window.
2. Close it (⌘W).
3. Click any menu-bar item.
This aborts on the first cycle for us. It is a use-after-free, so it depends on
allocator reuse — a single open/close/click sometimes survives. Clicking three
items right after the close made it reliable (died on cycle 1 of 15).
Tested on macOS 26.0 and 27.0, Apple silicon, `dioxus-desktop` 0.7.10, release
profile.
## Observed failure
Usually not a segfault. The freed `MenuChild` reads back as a plausible
`Option` of 0x0, which muda's icon validation accepts, and its PNG
encoder then rejects:
```
thread 'main' panicked at muda-0.17.2/src/platform_impl/macos/icon.rs:34:53:
called `Result::unwrap()` on an `Err` value: Format(FormatError { inner: ZeroWidth })
```
macOS crash report, trimmed — note only two frames between AppKit's dispatch
and the panic machinery:
```
Exception Type: EXC_CRASH (SIGABRT)
Thread 0 Crashed:: main Dispatch queue: com.apple.main-thread
2 libsystem_c.dylib abort + 148
...
13 app 0x105432f14 <- muda menu action handler
14 app 0x1054365bc
15 AppKit -[NSApplication(NSResponder) sendAction:to:from:] + 540
16 AppKit -[NSMenuItem _corePerformAction:] + 540
18 AppKit -[NSMenu performActionForItemAtIndex:] + 208
25 AppKit +[NSMenu(MenuBarAgent) _triggerActionForItemID:source:completionHandler:]
```
Because the payload is arbitrary freed memory, the symptom is not stable; a
different reuse pattern will fault or corrupt elsewhere.
## Relationship to the upstream muda bug
The raw-pointer ivar this relies on is tauri-apps/muda#328, fixed by
tauri-apps/muda#361 ("replace raw MenuChild pointer with Rc", merged
2026-07-30).
**That fix is not in any published muda.** At tag `muda-v0.19.3`, the latest
release, `src/platform_impl/macos/mod.rs` still reads:
```rust
// FIXME: Use `Rc` or something else to access the MenuChild.
#[ivars = Cell<*const MenuChild>]
```
So bumping muda does not currently resolve this, and `dioxus-desktop` 0.7.10
requires `muda = "0.17.0"` in any case, which cannot resolve past 0.17.2.
Once muda ships #361 the stale `NSMenuItem` will hold a strong reference and
the use-after-free disappears on its own. Until then it needs handling here.
## Suggested fix
Do not let a window own a menu that is installed process-wide. In preference
order:
1. On macOS, keep a process-lifetime clone of whichever menu was passed to
`init_for_nsapp` (e.g. in the shared desktop context). `muda::Menu` is
`#[derive(Clone)]` over `Rc`, so retaining one clone pins the children at
negligible cost, and it is correct with or without muda#361.
2. Re-install a surviving window's menu on `NSApp.mainMenu` when the owning
window closes, and clear it when the last window goes.
3. At minimum, document that `Config::with_menu` on macOS hands the menu to the
process and that callers must keep it alive.
(1) is the smallest change and does not alter observable behaviour.
## Workaround for app authors
Retain one clone of the menu yourself:
```rust
let menu = Menu::new();
// ... build submenus ...
// `Menu` is Rc-backed; one leaked clone pins the children for the life of the
// process, regardless of which window currently owns the menu bar.
std::mem::forget(menu.clone());
menu
```
15/15 open/close/click cycles clean after this, versus dying on cycle 1 before.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the menu ownership at src/webview.rs:532 and the macOS path in src/menubar.rs, especially init_menu_bar and init_for_nsapp. Reproduce the multi-window open/close/click cycle on macOS, then verify that retaining a process-lifetime menu clone prevents the failure while preserving existing menu behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100