Skip to content

setApplicationMenu: dangling pointer in dispatch_async causes silent JSON parse failure (menu never created) #160

@Palanikannan1437

Description

@Palanikannan1437

Bug

ApplicationMenu.setApplicationMenu() silently fails to create any menu. The native menu bar only shows the app name with no custom menus, and keyboard accelerators never fire.

Root Cause

toCString() in dist/api/bun/proc/native.ts creates a Buffer and returns a raw pointer via ptr(buf). The Buffer itself is not stored anywhere — it's a local temporary eligible for GC immediately after the FFI call returns.

The native setApplicationMenu in nativeWrapper.mm does this:

extern "C" void setApplicationMenu(const char *jsonString, ...) {
    NSLog(@"Setting application menu from JSON in objc");
    dispatch_async(dispatch_get_main_queue(), ^{
        NSData *jsonData = [NSData dataWithBytes:jsonString length:strlen(jsonString)];
        // ^ jsonString pointer is already dangling here
    });
}

The dispatch_async block captures jsonString by pointer, not by value. By the time the block executes on the main thread, Bun's GC has already freed the Buffer backing the pointer. strlen() on freed memory returns 0, NSData gets 0 bytes, and NSJSONSerialization fails with:

Failed to parse JSON: Error Domain=NSCocoaErrorDomain Code=3840 "Unable to parse empty data."

The menu is silently never created. No crash, no other error.

Diagnosis Trail

The only visible symptoms are:

  1. Menu bar shows only the app name (no custom menus)
  2. This log line appears ~20ms after "Setting application menu from JSON in objc":
    Failed to parse JSON: Error Domain=NSCocoaErrorDomain Code=3840 "Unable to parse empty data."
  3. The application-menu-clicked event never fires for any action or accelerator

Fix

Keep a reference to the Buffer alive until the main thread has consumed it. Minimal JS-side fix in dist/api/bun/proc/native.ts:

 setApplicationMenu: (params: { menuConfig: string }): void => {
     const { menuConfig } = params;
+    // Prevent GC from freeing the buffer before dispatch_async runs on main thread
+    const buf = Buffer.from(menuConfig + "\0", "utf8");
+    (globalThis as any).__electrobun_menuConfigBuf = buf;
     native.symbols.setApplicationMenu(
-        toCString(menuConfig),
+        ptr(buf),
         applicationMenuHandler,
     );
 },

The proper native-side fix would be to copy the JSON string into an NSString before dispatch_async:

extern "C" void setApplicationMenu(const char *jsonString, ZigStatusItemHandler zigTrayItemHandler) {
    NSLog(@"Setting application menu from JSON in objc");
    NSString *jsonCopy = [NSString stringWithUTF8String:jsonString]; // copy while pointer is valid
    dispatch_async(dispatch_get_main_queue(), ^{
        NSData *jsonData = [jsonCopy dataUsingEncoding:NSUTF8StringEncoding];
        // ... rest unchanged
    });
}

The same pattern likely affects showContextMenu and setTrayMenuFromJSON but may be less reproducible due to timing differences.

Environment

  • electrobun: 1.12.3
  • macOS: 15.5 arm64
  • Bun: (via electrobun bundled runtime)

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