Skip to content

fix(v3): prevent SIGSEGV enumerating screens on display change (macOS)#5516

Open
flofreud wants to merge 3 commits into
wailsapp:masterfrom
flofreud:fix/v3-macos-screen-params-crash
Open

fix(v3): prevent SIGSEGV enumerating screens on display change (macOS)#5516
flofreud wants to merge 3 commits into
wailsapp:masterfrom
flofreud:fix/v3-macos-screen-params-crash

Conversation

@flofreud
Copy link
Copy Markdown

@flofreud flofreud commented May 29, 2026

Description

On macOS, a v3 app crashes with a native SIGSEGV often when the display
configuration changes — sleep/wake, lid open/close, monitor connect/disconnect,
resolution or arrangement change.

After building with this fix the issue seems to be resolved.

Root cause

ApplicationDidChangeScreenParameters is dispatched on a background goroutine
(event_manager.go runs every application-event listener in its own go func),
and macOS emits the event several times in quick succession during a single
reconfiguration. The handler calls macosApp.processAndCacheScreens, which calls
[NSScreen screens] and messages each NSScreen. AppKit is main-thread-only, so
a burst of events runs getAllScreens concurrently, off the main thread,
while the display graph is itself being reconfigured → use-after-free / message to
a freed object → SIGSEGV. Because it is a native crash during cgo execution, the
dispatcher's defer handlePanic() cannot recover it and the process dies.

Observed at crash time: three goroutines simultaneously inside
processAndCacheScreens, each spawned by a separate handleApplicationEvent.

SIGSEGV: segmentation violation
signal arrived during cgo execution
_Cfunc_getAllScreens()
(*macosApp).processAndCacheScreens()      screen_darwin.go
(*macosApp).run.func3()                   application_darwin.go  // ApplicationDidChangeScreenParameters
(*EventManager).handleApplicationEvent.func1()

The per-screen [[NSScreen screens] firstObject] lookup added to processScreen
in alpha.91 (#5117) multiplied the off-main-thread AppKit access in this path and
made the crash much easier to hit.

Fix

  • Marshal screen enumeration onto the main thread via InvokeSync, guarding
    against a deadlock when already on the main thread. The main run loop also
    serialises the burst of events, so [NSScreen screens] is never accessed
    concurrently.
  • Resolve the primary-screen height once per refresh instead of
    re-enumerating [NSScreen screens] inside processScreen for every screen.
  • Return the screen count from getAllScreens so the Go side no longer reads
    the count separately via GetNumScreens — closing a TOCTOU where a display
    added between the two calls could make the loop over-read the malloc'd buffer.

How to test

  1. Run a v3 app on macOS with a window/system tray, ideally with an external monitor.
  2. Repeatedly connect/disconnect the monitor, or sleep/wake with a clamshell.
  3. Before: the app dies with the SIGSEGV above. After: screens re-cache cleanly.

go build ./pkg/application/ and go vet ./pkg/application/ pass.

Refs #5117

Summary by CodeRabbit

  • Bug Fixes
    • Fixed a crash that occurred when display configuration changed (sleep/wake, monitor connect/disconnect).
    • Made screen enumeration run on the main thread to avoid concurrent access issues that could trigger crashes.
    • Resolved primary-screen height once per refresh to ensure consistent coordinate handling.
    • Returned an explicit screen count to prevent race conditions when sizing screen buffers.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

Walkthrough

Refactors macOS screen handling to compute primary screen height once, change C getAllScreens to return a counted Screen* snapshot, and marshal AppKit screen enumeration onto the main thread to prevent SIGSEGV during display configuration changes.

Changes

Darwin Screen Enumeration SIGSEGV Fix

Layer / File(s) Summary
Primary screen height resolution
v3/pkg/application/screen_darwin.go
Adds primaryScreenHeight() and updates processScreen() to accept the resolved primary height so primary-screen frame height is computed once rather than per-screen.
Screen enumeration API and callsite updates
v3/pkg/application/screen_darwin.go
getAllScreens() now accepts an outCount pointer and returns a Screen* array sized from a single [NSScreen screens] snapshot; callers (GetPrimaryScreen, getScreenForWindow, getScreenForSystemTray) pass primaryScreenHeight() into processScreen().
Main thread enumeration marshalling
v3/pkg/application/screen_darwin.go
processAndCacheScreens() calls C.getAllScreens(&count), marshals and frees the returned C buffer, and ensures enumeration runs on the main thread (direct or via InvokeSync).
Changelog documentation
v3/UNRELEASED_CHANGELOG.md
Adds a Fixed entry summarizing the SIGSEGV crash fix, main-thread marshalling, once-per-refresh height resolution, and TOCTOU mitigation via returned count.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • wailsapp/wails#5304: Modifies Darwin Y-coordinate conversion and primary-screen baseline logic related to primaryScreenHeight().

Suggested labels

Bug, MacOS, awaiting review 🕝

Suggested reviewers

  • leaanthony

Poem

🐰 I hopped through screens where shadows flop,
A SIGSEGV lurked when displays would swap.
Heights now found once, counts snapped neat and tight,
Main thread keeps AppKit safe through day and night.
The rabbit cheers—no more crash in sight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: fixing a SIGSEGV crash on macOS triggered by screen enumeration during display configuration changes.
Description check ✅ Passed The PR description comprehensively covers the issue, root cause analysis, fix details, and testing instructions as required by the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@flofreud flofreud marked this pull request as ready for review May 29, 2026 18:24
`ApplicationDidChangeScreenParameters` is dispatched on background
goroutines (event_manager.go) and can fire several times in quick
succession during a single display reconfiguration. `macosApp.
processAndCacheScreens` enumerated `[NSScreen screens]` and messaged each
`NSScreen` off the main thread, so a burst of events ran `getAllScreens`
concurrently and crashed with a native SIGSEGV during cgo execution. As a
hard signal it is not catchable by the dispatcher's `defer handlePanic()`.

- Marshal the screen enumeration onto the main thread via `InvokeSync`,
  guarding against a deadlock when already on the main thread. Running on
  the main run loop also serialises the event burst so `[NSScreen screens]`
  is never touched concurrently.
- Resolve the primary-screen height once per refresh instead of
  re-enumerating `[NSScreen screens]` inside `processScreen` for every
  screen (introduced in alpha.91, which multiplied the off-main-thread
  AppKit access in this path).
- Return the screen count from `getAllScreens` so the Go side no longer
  reads the count separately via `GetNumScreens`, closing a TOCTOU that
  could over-read the malloc'd buffer when a display is added mid-refresh.

Refs wailsapp#5117

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@flofreud flofreud force-pushed the fix/v3-macos-screen-params-crash branch from 4b8327d to 7cad7ad Compare May 29, 2026 18:24
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
v3/pkg/application/screen_darwin.go (1)

113-126: ⚡ Quick win

Reuse the captured snapshot for primary height instead of re-enumerating.

getAllScreens snapshots [NSScreen screens] at Line 114 to size the allocation, but Line 119 calls primaryScreenHeight(), which enumerates [NSScreen screens] a second time. This both contradicts the "single snapshot" intent of the fix and means the primary height can come from a different snapshot than the count/loop. Since screens[0] is the primary screen (matching isPrimary = (i == 0)), derive the height from the already-captured array.

♻️ Derive primary height from the existing snapshot
 	if (outCount != NULL) {
 		*outCount = (int)count;
 	}
-	CGFloat primaryHeight = primaryScreenHeight();
+	// Reuse the snapshot above instead of re-enumerating [NSScreen screens];
+	// screens[0] is the primary screen (matches isPrimary = (i == 0) below).
+	CGFloat primaryHeight = count > 0 ? [[screens objectAtIndex:0] frame].size.height : 0;
 	Screen* returnScreens = malloc(sizeof(Screen) * count);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/screen_darwin.go` around lines 113 - 126, getAllScreens
currently snapshots NSArray *screens but then calls primaryScreenHeight(), which
re-enumerates NSScreen; instead derive the primary height from the
already-captured snapshot: after capturing screens (NSArray<NSScreen *>
*screens), compute primaryHeight using the first element ([screens
objectAtIndex:0])'s frame/visibleFrame height and pass that into processScreen
for all items so the primary height is consistent with the snapshot; update
getAllScreens to stop calling primaryScreenHeight() and use the height extracted
from screens[0] (keep the existing processScreen and isPrimary = (i == 0)
logic).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@v3/pkg/application/screen_darwin.go`:
- Around line 113-126: getAllScreens currently snapshots NSArray *screens but
then calls primaryScreenHeight(), which re-enumerates NSScreen; instead derive
the primary height from the already-captured snapshot: after capturing screens
(NSArray<NSScreen *> *screens), compute primaryHeight using the first element
([screens objectAtIndex:0])'s frame/visibleFrame height and pass that into
processScreen for all items so the primary height is consistent with the
snapshot; update getAllScreens to stop calling primaryScreenHeight() and use the
height extracted from screens[0] (keep the existing processScreen and isPrimary
= (i == 0) logic).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b7fc19a9-fd30-4730-9d6b-336c88e84d4f

📥 Commits

Reviewing files that changed from the base of the PR and between e34a2c4 and 7cad7ad.

📒 Files selected for processing (2)
  • v3/UNRELEASED_CHANGELOG.md
  • v3/pkg/application/screen_darwin.go

leaanthony and others added 2 commits May 30, 2026 15:08
getAllScreens snapshotted [NSScreen screens] to size the allocation but
then called primaryScreenHeight(), which enumerated [NSScreen screens] a
second time — contradicting the single-snapshot intent and risking a
primary height from a different snapshot than the count/loop. Derive it
from screens[0] (already treated as primary via isPrimary = i == 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
v3/pkg/application/screen_darwin.go (1)

261-272: 💤 Low value

Consider main-thread marshalling for remaining AppKit accessors.

getScreenForWindow and getScreenForSystray call C functions that access [NSScreen screens] (via primaryScreenHeight()) without main-thread marshalling. If invoked from a background goroutine during a display reconfiguration, they could hit the same SIGSEGV the PR fixes for processAndCacheScreens. These paths are lower risk since they're typically called in response to user actions rather than system events, but you may want to add similar protection in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/screen_darwin.go` around lines 261 - 272,
getScreenForWindow and getScreenForSystray call C functions that touch AppKit
([NSScreen screens]) and must be called on the main thread to avoid SIGSEGV;
wrap the calls to C.getScreenForWindow and C.getWindowForSystray in the same
main-thread marshalling used elsewhere (e.g., a runOnMain / runMainSync helper
or the pattern used in processAndCacheScreens) so the C.get* calls and
subsequent cScreenToScreen conversion run on the main thread, propagate any nil
cScreen as an error, and return the Screen only after the main-thread call
completes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@v3/pkg/application/screen_darwin.go`:
- Around line 261-272: getScreenForWindow and getScreenForSystray call C
functions that touch AppKit ([NSScreen screens]) and must be called on the main
thread to avoid SIGSEGV; wrap the calls to C.getScreenForWindow and
C.getWindowForSystray in the same main-thread marshalling used elsewhere (e.g.,
a runOnMain / runMainSync helper or the pattern used in processAndCacheScreens)
so the C.get* calls and subsequent cScreenToScreen conversion run on the main
thread, propagate any nil cScreen as an error, and return the Screen only after
the main-thread call completes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a2c54d8d-1a7b-4bd3-ba6b-cbc4204ea9f8

📥 Commits

Reviewing files that changed from the base of the PR and between 7cad7ad and fa5c77b.

📒 Files selected for processing (1)
  • v3/pkg/application/screen_darwin.go

@flofreud
Copy link
Copy Markdown
Author

@leaanthony I addressed the first CodeRabbit nit-pick but the one about Wails generally not considering main-thread marshalling observed by Claude and CodeRabbit should probably be a more focused effort later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants