Repository navigation
macOS: fix popup over-releases (adopted tab close, native-window close) and free user-closed popup windows - #75
Merged
Conversation
…b no longer crashes (Canvas 18) impl_create_web_view stored the WKWebViewConfiguration WebKit passes to webView:createWebViewWithConfiguration:forNavigationAction:windowFeatures: in child_e->config without retaining it. WebKit only lends that object for the call and the child WKWebView keeps its own copy, so it is freed soon after. cocoa_destroy_engine releases e->config, so destroying an adopted tab (closing it, or its session or window) released a freed object: objc_msgSend on a garbage isa on the AppKit main thread, EXC_BAD_ACCESS at isa + 0x10. Seen in the field after an Okta dashboard tile opened Jira as an adopted tab. - impl_create_web_view retains the passed configuration for both ADOPT and NATIVE_WINDOW children. - cocoa_discard_popup and the engine-owned impl_web_view_did_close path release it, so the retain does not leak. The java_owned path leaves it to cocoa_destroy_engine, which releases it once. - Canvas 18 gains the safeguard "An engine owns every object it releases" and Operation 8 items 2 and 4 say so. Not compiled here (no macOS toolchain): build on macOS and exercise adopt -> close tab, and adopt -> window.close(), ideally under Zombies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZMcbkZ4JwWKt4vBrjZcLY
…f (Canvas 18 Op 10.3) The demo had no way to destroy an adopted tab, so the teardown fixed in the previous commit could not be tested by hand. Removing a heavyweight component from its parent does not destroy its native peer; dispose() does. - A "Close tab" button removes the selected adopted tab and disposes it (never the opener). - A fourth opener case opens a popup that calls window.close(), so the adopted child's webViewDidClose: path runs before its tab is closed. - The demo README lists both steps and the NSZombieEnabled=YES hint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZMcbkZ4JwWKt4vBrjZcLY
…-releases its window (Canvas 15) impl_create_web_view allocs the popup NSWindow (+1, held in popup_window) but left releasedWhenClosed at its default YES. impl_web_view_did_close calls -close, which then releases the window, and later releases popup_window again. Found on-device with the new self-closing popup demo case in Native window mode, with NSZombieEnabled=YES: -[NSKVONotifying_NSWindow release]: message sent to deallocated instance The window is now created with releasedWhenClosed = NO, so the teardown's single release balances the alloc. Canvas 15 gains the safeguard "The library owns the popup window (macOS)" and its createWebView step says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZMcbkZ4JwWKt4vBrjZcLY
…popup down (Canvas 15) With releasedWhenClosed = NO (previous commit), a native-window popup closed with its own close button was never freed: only a page's window.close() reached the teardown, so the window, its web view, delegate, configuration, Java global refs and Engine stayed alive, and Java never heard onPopupClosed. - The popup's UI-delegate object also adopts NSWindowDelegate and is the popup window's delegate. Its windowWillClose: schedules the webViewDidClose: teardown on the main queue (dispatch_async_f: a window must not be released inside its own windowWillClose:, and cocoa_run_on_main_async would run it inline), retaining the delegate and web view across the hop. - webViewDidClose: clears the window's delegate before -close, so its own close does not re-enter. The teardown clears the "eng" association first, so whichever close path runs second does nothing. - Canvas 15 gains the safeguard "A native popup is torn down once, however it closes" and the macOS operation describing the close-button path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZMcbkZ4JwWKt4vBrjZcLY
shannah
marked this pull request as ready for review
November 2, 2026 14:17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The crash
An app on agentic-app-framework v0.21.5 (webview 1.10.2) crashed on macOS after the user signed in to Okta and clicked the Jira tile, which opens Jira as an adopted popup tab. The crash report shows a message sent to a freed Objective-C object on the AppKit main thread:
EXC_BAD_ACCESS KERN_INVALID_ADDRESS at 0x003b9d8b7e0ddce8x14 = 0xbdbb9d8b7e0ddcda,x15 = 0x003b9d8b7e0ddcd8, which isx14with its pointer-authentication bits stripped. The fault address isx15 + 0x10, the method-cache readobjc_msgSenddoes through a garbage isa.webview-popup-adopt-gracewas alive, so a popup had been adopted.Fix 1: an adopted tab over-released its configuration (Canvas 18)
impl_create_web_viewstored theWKWebViewConfigurationWebKit passes towebView:createWebViewWithConfiguration:forNavigationAction:windowFeatures:inchild_e->configwithout retaining it. WebKit only lends that object for the call (+0), and the childWKWebViewkeeps its own copy, so the passed object is freed soon after.cocoa_destroy_enginereleasese->configfor every engine, so destroying an adopted tab released a freed object.impl_create_web_viewretains the passed configuration for both ADOPT and NATIVE_WINDOW children.cocoa_discard_popupand the engine-ownedimpl_web_view_did_closepath release it. Thejava_ownedpath leaves it tococoa_destroy_engine, which releases it once.Fix 2: a native-window popup over-released its window on
window.close()(Canvas 15)Found while testing fix 1 on a Mac. In Native window mode, a popup that calls
window.close()stopped with zombies on:-[NSKVONotifying_NSWindow release]: message sent to deallocated instance. The popupNSWindowkeptreleasedWhenClosedat its default YES, so-closereleased it and the teardown released it again. It's now created withreleasedWhenClosed = NO. This bug predates this PR. Canvas 15 safeguard: "The library owns the popup window (macOS)".Fix 3: a native popup window closed with its close button is freed (Canvas 15)
Only a page's
window.close()reached the teardown. A window the user closed with its own close button was never freed (before fix 2, AppKit freed the window object behind the library's back and everything else leaked), and Java never heardonPopupClosed.NSWindowDelegateand is the popup window's delegate. ItswindowWillClose:schedules thewebViewDidClose:teardown on the main queue, retaining the delegate and web view across the hop. It usesdispatch_async_fbecause a window must not be released inside its ownwindowWillClose:, andcocoa_run_on_main_asyncwould run it inline.webViewDidClose:clears the window's delegate before-close, so its own close doesn't re-enter. The teardown clears the"eng"association first, so whichever close path runs second does nothing.Demo (
WebViewAdoptPopupDemo)dispose()on it, which is what reachescocoa_destroy_engine. It leaves the opener tab alone.window.close()button.Testing
CI compiles the native library for macOS (arm64 and x86_64), Linux and Windows; all green on 18ddc01.
On a Mac (macOS 15.4.1, arm64), with
NSZombieEnabled=YES ./run-mac-adopt-popup-demo.sh, on 18ddc01 — all passed, no crashes, no zombie messages:After merge
The fix reaches apps only after a swingwebview release, a webview-run release that shades it, and a matching bump of both pins in agentic-app-framework's
pom.xml(they move in lockstep).🤖 Generated with Claude Code
https://claude.ai/code/session_01SZMcbkZ4JwWKt4vBrjZcLY