Skip to content

Base red: the app no longer crashes at launch on macOS 14 (WebKit navigation with no source frame) - #36290

Merged
lawrencecchen merged 2 commits into
feat-cmux-nextfrom
fix/base-red-mac14-webkit-sourceframe
Nov 5, 2026
Merged

lawrencecchen merged 2 commits into
feat-cmux-nextfrom
fix/base-red-mac14-webkit-sourceframe

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Base red: the cmux-next app ends at launch on macOS 14.8.7, its oldest supported OS. 15.7.7 and 26.6.2 pass the same smoke.

  • feat tip 66339d3, fleet build 83e04e0d8b97c62c796fc5aa, Tart smoke step 48cb9eceb51bb6143325b239: SMOKE FAIL os=14.8.7 ... crashes=1. The app died after the chromium step; every later step found no socket.
  • Crash report, main thread, EXC_BREAKPOINT (SIGTRAP):
    Foundation  static URLRequest._unconditionallyBridgeFromObjectiveC(_:)
    cmux DEV    @objc closure #1 in PageWebView.webView(_:decidePolicyFor:)
    ...         libswift_Concurrency completeTaskWithClosure
    

Cause

PageWebView.webView(_:decidePolicyFor:) reads action.sourceFrame.request.url. #26286 (1e27057) added this line. WebKit declares sourceFrame nonnull, but macOS 14's WebKit gives nil for some navigations. In Swift, the nil frame then gives a nil NSURLRequest, and the bridge to URLRequest traps. The trapping frame is exactly that bridge, called directly from the delegate closure. The iOS app already hit the same WebKit behavior (BrowserTab.swift: "sourceFrame is nil for some navigations despite its Swift type").

AgentPaneNavigation reads the same property the same way, so it has the same hazard. The browser's WKNavigationAction.sourcePage used sourceFrame as WKFrameInfo?. That cast does not prove safety, because the optimizer may fold the nonnull value's .some.

Fix

The new CmuxNextDesign/WKNavigationAction+Frames.swift adds sourceFrameIfPresent, sourceFrameURL, targetFrameURL and WKFrameInfo.requestURLIfPresent. They call the Objective-C getters through perform(_:), which returns an optional. Thus no code assumes that the frame or its request is nonnull. All three call sites now use them: PageWebView, AgentPaneNavigation and sourcePage. When the frame exists, the URL is the same as before. When there is no frame, the policy treats the source as unknown (nil), which is a path it already handles. A nil source never grants the artifact or srcdoc allowances, so it fails closed.

Other WebKit delegate reads in the app are safe: targetFrame is declared optional, WKScriptMessage.frameInfo and the initiatedByFrame: parameters are not navigation actions, and no other code reads sourceFrame.

Regression test

FakeNavigationAction (CmuxNextPagesTestSupport) is a WKNavigationAction subclass. Its sourceFrame getter returns nil through the Objective-C runtime, as on macOS 14. The tests call the real delegate methods with it:

  • PageNavigationNoSourceFrameTests: PageWebView allows its own document, opens a clicked outside link externally, and cancels an unclicked one.
  • AgentPaneNavigationNoSourceFrameTests: AgentPaneNavigation allows the pane's document and cancels another main-frame document.

Command: cmux-ci run --class light --script scripts/ci/package-test-lane.sh --ref <sha> --arg=suite --arg=Packages/macOS/CmuxNext --arg=PageNavigationNoSourceFrameTests,AgentPaneNavigationNoSourceFrameTests

  • Red, at 1b35c93 (tests only): step 065f1feb855193591217c6b6: both tests start, then the test process ends with unexpected signal code 5 (SIGTRAP, the trap in the crash report).
  • Green, at b53b9f8 (fix): step c65b70d79776acd6f5b697ee (Blacksmith overflow, run manaflow-ai/cmuxterm-hq actions 41987489687): ✔ aNavigationWithNoSourceFrameGetsThePolicyNotATrap() and ✔ aNavigationWithNoSourceFrameGetsTheDecisionNotATrap() pass.

macOS matrix (Tart, scripts/cmux-next/os-matrix.sh --suite smoke)

Build of b53b9f8: fleet job 7f4ba96af2200a8fcfb1b297 (tag base-red-mac14-b53b9f860-v1). Smoke step 57a668a56b1a7651b1834b67:

macOS base 66339d3 (step 48cb9eceb51bb6143325b239) this PR
14.8.7 FAIL, crashes=1 (this trap) PASS, crashes=0, hangs=0, faults=0
15.7.7 PASS PASS, crashes=0, hangs=0, faults=0

Observed / Not run

Observed:

  • The base crash report (main thread, the stack above, EXC_BREAKPOINT) and its smoke log: the app died after the chromium step.
  • The red and green fleet test steps above, at 1b35c93 and b53b9f8.
  • The macOS 14 and 15 Tart smoke at b53b9f8: both PASS with no crash.
  • python3 scripts/cmux-next/prepush-gate.py: no new violations.

Not run:

  • macOS 26 smoke on this head. The base already passed on 26.6.2, and the fix does not change behavior when a frame is present.
  • The browser sourcePage change has no test of its own. It reads through the same helper that the two tests cover.
  • I did not find the exact WebKit navigation that has no source frame on macOS 14. The test uses an Objective-C double that returns nil, as the iOS code already notes for the same WebKit behavior.

Changelog

  • Fixed: cmux NEXT no longer crashes at launch on macOS 14.

No capture: there is no UI change. This fixes a crash in a WebKit navigation delegate.


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes a launch crash on macOS 14 where a WebKit navigation with no source frame trapped the app.

  • macOS 14's WebKit returns nil for WKNavigationAction.sourceFrame on some navigations even though it is declared nonnull; reading sourceFrame.request.url bridged a nil NSURLRequest to URLRequest and trapped, killing the app at launch on the oldest supported OS (14.8.7). macOS 15 and 26 passed the same smoke.
  • New CmuxNextDesign helpers (sourceFrameIfPresent, sourceFrameURL, targetFrameURL, requestURLIfPresent) read the frames through the Objective-C runtime so a missing frame becomes nil instead of a trap. PageWebView, AgentPaneNavigation, and the browser's sourcePage now use them; with a frame present the URL is unchanged, and with none the policy treats the source as unknown, which the existing paths already handle and which fails closed on artifact and srcdoc allowances.
  • Regression tests use a new FakeNavigationAction double that returns nil for sourceFrame through the runtime; PageNavigationNoSourceFrameTests and AgentPaneNavigationNoSourceFrameTests trap at the base commit and pass with this fix.

Tart smoke on macOS 14.8.7 goes from FAIL (crashes=1) to PASS (crashes=0, hangs=0, faults=0); 15.7.7 stays PASS.

Written for commit b53b9f8. Summary will update on new commits.

View guided diff Turn on auto-fix

…ot a trap (red)

macOS 14's WebKit gives a nil WKNavigationAction.sourceFrame for some
navigations although WebKit declares it nonnull. PageWebView and the agent
pane read sourceFrame.request.url through the Swift type, so the nil
NSURLRequest bridges to URLRequest and traps: the cmux-next app ended at
launch on macOS 14.8.7 (Tart smoke of 66339d3).

FakeNavigationAction (CmuxNextPagesTestSupport) answers the sourceFrame
getter with nil through the Objective-C runtime, as that WebKit does. Both
delegates trap on it at this commit.
…S 14

macOS 14's WebKit gives a nil WKNavigationAction.sourceFrame for some
navigations, although the header declares it nonnull. PageWebView (since
#26286, 1e27057) and AgentPaneNavigation read sourceFrame.request.url
through the Swift type: the nil frame answers a nil NSURLRequest, and its
bridge to URLRequest traps (URLRequest._unconditionallyBridgeFromObjectiveC).
The cmux-next app ended at launch on macOS 14.8.7 in
PageWebView.webView(_:decidePolicyFor:) (fleet build 83e04e0d8b97c62c796fc5aa,
Tart smoke 48cb9eceb51bb6143325b239, crashes=1); 15 and 26 pass.

CmuxNextDesign's WKNavigationAction.sourceFrameIfPresent / sourceFrameURL /
targetFrameURL and WKFrameInfo.requestURLIfPresent ask the Objective-C
runtime for the getters, whose answer is typed optional, so the optimizer
cannot fold a nil check away (as it may for `sourceFrame as WKFrameInfo?`,
which the browser's sourcePage used). Both navigation delegates and
sourcePage read through them. With a frame present, the URL is the same as
before; with none, the policy treats the source as unknown, as it already
does for a nil URL (the agent pane refuses an artifact frame it did not make).

Regression: PageNavigationNoSourceFrameTests and
AgentPaneNavigationNoSourceFrameTests (previous commit) trap at the base and
pass here.
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review b53b9f8: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr36290 -f ref=fix/base-red-mac14-webkit-sourceframe -f platform=macos -f backend_mode=local

Shipped in:

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

For the PageWebView and agent-pane navigation area: #26286 (1e27057, artifact isolation on the shared WK page host) added the action.sourceFrame.request.url read in PageWebView.webView(_:decidePolicyFor:). On macOS 14, WebKit gives a nil sourceFrame for some navigations, so this read trapped at launch. AgentPaneNavigation had the same read. This PR moves both to WKNavigationAction.sourceFrameURL / targetFrameURL (CmuxNextDesign), which read the frames as optional. In new WebKit delegate code, please use those helpers and not sourceFrame.request.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent subagent): SHIP at b53b9f8

Correctness first. I read the diff, the policy code it feeds (PageNavigation.policy, PageArtifactFrame, AgentPaneNavigation.decision, AgentPaneSource.isTrusted, PageDescriptor.owns) and Package.swift. I built and ran nothing myself.

1. The helper (WKNavigationAction+Frames.swift)

  • perform(_:) sends the real Objective-C getter and returns Unmanaged<AnyObject>!. A nil frame gives .none, so nothing is bridged and nothing traps. Both getters return +0, so takeUnretainedValue() is the correct ownership: Swift retains the result, with no leak and no over-release.
  • request is also read through perform and as? NSURLRequest. .url is optional, so this read has no bridge trap either. targetFrame was already optional; it now goes through the same safe request read.
  • When a frame exists, the result is the same object and the same URL as the old sourceFrame.request.url and targetFrame?.request.url.
  • Actor isolation: the extension members take the type's main-actor isolation, perform is nonisolated, and all three call sites are main-actor delegate methods.
  • No trap path is left at these call sites. action.request.url stays as it was; WebKit always fills in the request.

2. Fail-closed

  • Every allowance that depends on the source needs a positive match, and nil never matches: initialSrcdoc (isAgentPageURL(nil) is false), the passive srcdoc allowance (page.owns(nil) is false) and the agent-pane artifact frame (isTrusted(nil) is false). No page hook reads PageNavigation.sourceFrameURL.
  • Non-blocking note: a nil source also skips the denials keyed on the source (source is srcdoc, source is artifact, and contextRestricted(parent:)). The checks on the target frame still apply. One theoretical case is more permissive than either known source: on the agent page, a subframe navigation with target about:srcdoc, a nil source and a page-owned URL now gets .allow. With source = agent page it gets .cancel. WebKit gives a nil source for loads that the UI process starts, not for loads that page content starts. The case is limited to macOS 14, and the old code crashed there. So I do not block on it. If you want it fully closed in a follow-up, cancel non-main-frame navigations that have no source frame; the launch load that crashed is a main-frame load.

3. Test double

  • class_replaceMethod installs the nil getter on FakeNavigationAction only. WKNavigationAction and the other tests do not change. The "@@:" encoding and the block signature (self first, object return) are correct. The static let makes the install run once and thread-safe.
  • The base code reaches the replaced IMP through objc_msgSend, gets nil, sends request to nil and traps in the URLRequest bridge. This agrees with the red step (signal 5) and with the crash report. The overridden request, navigationType, targetFrame, modifierFlags and buttonNumber cover every other property the two delegates read.
  • The red step, the green step and the Tart smoke (macOS 14 and 15) come from the PR body. I did not run them again.

4. Other bridged reads of nonnull WebKit properties

  • In CmuxNext/Sources, no other code reads sourceFrame. The other delegate reads are action.request.url, navigationResponse.response.url, targetFrame (optional) and securityOrigin of the initiatedByFrame: parameters. They are not this hazard.
  • Residual, non-blocking: message.frameInfo.request.url in AgentPaneBridge.swift (lines 26, 78, 99) and in WebKitPageHostBridge.swift:59 still bridge WKFrameInfo.request through its Swift type. No report shows nil there for script messages. requestURLIfPresent is a cheap hardening for these reads if you want it.

5. Repo rules

  • No blocking call on the main thread and no try!. The tests use try #require.
  • CmuxNextAgentPane, CmuxNextBrowser and CmuxNextPages already list CmuxNextDesign in Package.swift.
  • CmuxNextPagesTestSupport imports only system frameworks. CmuxNextAgentPaneTests and CmuxNextPagesTests already depend on it.
  • No new module edges. The extensions expose instance members, so the namespace lint has nothing to flag.

CI on this head: all checks are green or skipped, except cmux-next swift test, which is still pending.

Review (independent subagent): SHIP at b53b9f8

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 5, 2026
@lawrencecchen
lawrencecchen merged commit f8d9177 into feat-cmux-next Nov 5, 2026
246 of 264 checks passed
@lawrencecchen
lawrencecchen deleted the fix/base-red-mac14-webkit-sourceframe branch November 5, 2026 08:47
lawrencecchen added a commit that referenced this pull request Nov 5, 2026
macOS 14's WebKit gives no source frame for some navigations (#36290). In
PageNavigation.policy a nil source URL skipped the source checks: a srcdoc
frame of the agent page sent to another document of the page was cancelled
from a known source but allowed with none.

On the agent page, a load into a frame inside the page from an unknown
source is now cancelled first, as the agent pane's rules deny an unknown
source. The main frame is not affected, so the launch load of the page's
own document still loads with no source frame (the load that crashed on
macOS 14 before #36290). Loads with a known source take the same path as
before. Chromium calls the policy for the main frame only.

Regression: AgentPageNoSourceSubframeTests (previous commits) fails 4
expectations at 2781f61 (step 99ca5fd136639b8811d1fcb6) and passes here.
lawrencecchen added a commit that referenced this pull request Nov 5, 2026
lawrencecchen added a commit that referenced this pull request Nov 5, 2026
lawrencecchen added a commit that referenced this pull request Nov 5, 2026

This branch was previously deployed

1 inactive deployment
ci-routing-agent — b53b9f86 Deployed Nov 5, 2026 by lawrencecchen via namespace autofix #15022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant