DioxusLabs / DioxusLabs/dioxus

macOS: closing a window frees the process-global menu bar → use-after-free on the next menu click

Open
#5,753 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.