Skip to content

fix(darwin): copy screen id/name C strings and snapshot screen count atomically - #5584

Closed
x-haose wants to merge 2 commits into
wailsapp:masterfrom
x-haose:fix-darwin-screen-cstring
Closed

x-haose wants to merge 2 commits into
wailsapp:masterfrom
x-haose:fix-darwin-screen-cstring

Conversation

@x-haose

@x-haose x-haose commented Jun 12, 2026 •

Copy link
Copy Markdown

Description

Fixes a hard crash (SIGSEGV in C.GoString) that occurs during display-reconfiguration event storms — typically when a Mac wakes from sleep, connects/disconnects a monitor, or changes resolution while a v3 app is running.

Crash observed (v3.0.0-alpha.97, macOS 26.5, arm64)

fatal error: fault
[signal SIGSEGV: segmentation violation code=0x2 addr=0x100000000 pc=...]
runtime.gostring(0x10000001b)
github.com/wailsapp/wails/v3/pkg/application._Cfunc_GoString
github.com/wailsapp/wails/v3/pkg/application.cScreenToScreen        screen_darwin.go:185
github.com/wailsapp/wails/v3/pkg/application.(*macosApp).processAndCacheScreens
github.com/wailsapp/wails/v3/pkg/application.(*macosApp).run.func3
github.com/wailsapp/wails/v3/pkg/application.(*EventManager).handleApplicationEvent.func1

The app had been running ~12.5h and crashed on wake.

Root causes (two memory-safety issues in screen_darwin.go)

  1. Dangling C strings. processScreen stores the raw result of -[NSString UTF8String] into the Screen struct:

    returnScreen.id   = [[NSString stringWithFormat:@"%d", displayID] UTF8String];
    returnScreen.name = [screen.localizedName UTF8String];

    That pointer is owned by the (autoreleased) NSString. Go reads it later via C.GoString — once the string is deallocated, the pointer dangles. 0x10000001b above is exactly such a garbage pointer.

  2. Count/allocation race. getAllScreens() mallocs the array from one [NSScreen screens] snapshot, while processAndCacheScreens fetches the element count via a separate GetNumScreens() call. If the display configuration changes between the two calls (precisely when these code paths run), the loop over-reads the allocation.

Fix

  • copyCString helper: strdup the UTF-8 bytes; the Go side frees them after conversion in cScreenToScreen, keeping every processScreen caller leak-free.
  • getAllScreens(int* outCount): return the count from the same snapshot as the allocation.
  • Zero-initialize the Screen struct and guarantee id/name are always owned, non-NULL pointers (empty string when localizedName is unavailable or the NSScreen is nil) — this also removes a latent C.GoString(NULL) hazard via the getScreenForWindow / getScreenForSystemTray paths (e.g. window not currently on any screen).

No public API changes; getAllScreens is internal to the file.

Testing

  • go build ./pkg/application on macOS 26.5 / arm64 / go1.25.
  • Downstream app (x-haose/tg-checkin, long-running menubar-resident app) now runs with this patch via go.mod replace; previously crashed on wake, will report back after soak.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Summary by CodeRabbit

  • Bug Fixes

    • Improved screen detection reliability by preventing race conditions during display reconfiguration.
  • Refactor

    • Enhanced memory safety and management of internal screen data structures.

…atomically

Fixes a SIGSEGV in C.GoString during display-reconfiguration event storms
(typically on sleep/wake), caused by two memory-safety issues in
screen_darwin.go:

1. Dangling C strings: processScreen stored the raw result of
   -[NSString UTF8String] (a pointer owned by the autoreleased NSString)
   into the Screen struct, which Go reads later via C.GoString. Once the
   NSString is deallocated the pointer dangles. Fixed by strdup'ing the
   UTF-8 bytes (copyCString) and freeing them on the Go side after
   conversion in cScreenToScreen, keeping every caller leak-free.

2. Count/allocation race: getAllScreens malloc'd the array from one
   [NSScreen screens] snapshot while processAndCacheScreens fetched the
   element count via a separate GetNumScreens call. If the display
   configuration changed between the two calls (exactly when these code
   paths run), the loop over-read the allocation. Fixed by returning the
   count from the same snapshot through an out-parameter.

Also zero-initializes the Screen struct and guarantees name/id are always
owned, non-NULL pointers (empty string when localizedName is unavailable
or the NSScreen is nil), removing a C.GoString(NULL) hazard via the
getScreenForWindow / getScreenForSystemTray paths.

Observed crash (v3.0.0-alpha.97, macOS 26.5, arm64):
  runtime.gostring(0x10000001b)
  application._Cfunc_GoString
  application.cScreenToScreen        screen_darwin.go:185
  application.(*macosApp).processAndCacheScreens
  application.(*EventManager).handleApplicationEvent.func1
@coderabbitai

coderabbitai Bot commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 35225834-8313-4d05-a7c3-7920377200d3

📥 Commits

Reviewing files that changed from the base of the PR and between 2633f81 and 43569fe.

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

Walkthrough

Screen enumeration on macOS now uses explicit C heap ownership for string fields and atomic screen-count acquisition. A new copyCString helper duplicates NSString UTF-8 bytes onto the C heap for Screen.id and Screen.name. The getAllScreens function captures screen count atomically within a single snapshot. Go-side cleanup adds deferred free calls to release the heap allocations after converting to Go strings.

Changes

Darwin Screen Enumeration Memory Safety

Layer / File(s) Summary
Heap-allocated string fields in processScreen
v3/pkg/application/screen_darwin.go
Introduces copyCString C helper to duplicate NSString UTF-8 data onto the C heap. Updates processScreen to allocate Screen.id and Screen.name via copyCString, replacing unsafe direct UTF8String pointers with owned heap allocations.
Atomic screen count and Go-side memory management
v3/pkg/application/screen_darwin.go
Refactors getAllScreens to snapshot NSScreen screens once and capture count atomically. Adds deferred free calls in cScreenToScreen to release heap-allocated strings after Go conversion. Updates processAndCacheScreens to obtain screen count from the same snapshot used to allocate the array, eliminating a separate count fetch.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Suggested labels

Bug, go, MacOS, size:M, awaiting review 🕐

Suggested reviewers

  • leaanthony
  • atterpac
  • flofreud

Poem

🐰 A rabbit hops through Darwin's screens,
With strings that now know what they mean—
No dangling pointers left behind,
Just heap-owned data, safely signed!
One snapshot counts them all at once,
No races now—our memory won't.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Description check ❓ Inconclusive The description covers the crash context, root causes, fixes, and testing. However, it does not reference an issue number using 'Fixes #' format and some template sections are incomplete or unchecked. Add 'Fixes #' to link the PR to the related issue, and ensure all checklist items are properly addressed (testing platforms, wails doctor output, etc.).
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the two main fixes: copying C strings for screen id/name and atomically snapshotting screen count.
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.

@taliesin-ai

Copy link
Copy Markdown
Collaborator

Thanks for this, @x-haose — you correctly spotted that macOS screen enumeration could hand back pointers into Objective-C strings that get freed, and reading the screen count separately from the list was a real race. Solid catch.

The same fix landed just ahead of this via #5565, which added strdupOrNull for the id/name and a single getAllScreens(int* outCount) that returns the list and count together — the same approach as yours. So I'm closing this as already-resolved, purely on timing. You'll be credited in the release notes for independently finding and fixing it. 🙏

(Note for posterity: #5516 tackles the same area from a different angle — marshalling enumeration onto the main thread — so that protection is being tracked separately.)

leaanthony added a commit that referenced this pull request Jun 28, 2026
…PRs (#5683)

* docs(changelog): credit @puneetdixit200 for GTK4 menu fix (#5539)

* docs(changelog): credit @x-haose for macOS screen-enumeration fix (#5584)

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
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.

3 participants