fix(v3/darwin): copy screen id/name out of autoreleased UTF8String buffers#5565
Conversation
…ffers processScreen stored the raw result of -[NSString UTF8String] in the C Screen struct's id and name fields. UTF8String returns a pointer into an autoreleased buffer whose lifetime ends when the enclosing autorelease pool drains, which can happen before Go reads the strings with C.GoString in cScreenToScreen — a use-after-free that intermittently aborts the process with 'fatal error: invalid pointer found on stack' when Wails' internal ApplicationDidChangeScreenParameters handler refreshes the screen cache (display sleep/wake, monitor connect or disconnect, resolution change). Because it is a runtime memory-safety abort inside an internal handler, applications cannot recover from it. The strings are now strdup'd into malloc'd memory owned by the struct and freed by cScreenToScreen after conversion. name is also initialised to NULL so the pre-10.15 path no longer leaves an uninitialised pointer. getAllScreens and getScreenForWindow now run inside an explicit autorelease pool: previously, calls from Go goroutine threads (which have no ambient pool) leaked every autoreleased object created during enumeration. The pool also makes the bug deterministic instead of timing-dependent, which the new regression test exploits — run with MallocScribble=1 it segfaults without the fix and passes with it. Fixes #5556
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughReplaces autoreleased NSString UTF8String pointers with strdup'd C strings, wraps Objective‑C enumeration in autorelease pools, converts and frees C buffers in Go, adds allScreens() helper, a Darwin regression test, and a changelog entry. ChangesmacOS Screen String Ownership Fix
Sequence DiagramsequenceDiagram
participant ObjC as Objective-C
participant C as C layer
participant Go as Go runtime
ObjC->>C: create NSStrings for screen id/name
C->>C: strdupOrNull(UTF8String) -> malloc'd char*
C->>Go: return array of C Screens (malloc'd) + count
Go->>Go: allScreens()/cScreenToScreen iterate
Go->>C: C.GoString(char*) to make Go string
Go->>C: C.free(char*) to release malloc'd memory
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
Fixes a macOS memory-safety issue in v3 screen enumeration where -[NSString UTF8String] pointers were stored into C structs and later read by Go after the autorelease pool drained, causing intermittent use-after-free crashes during display-change events.
Changes:
- Copy screen
id/nameinto malloc-owned buffers (strdup) and free them afterC.GoStringconversion. - Wrap
getAllScreens/getScreenForWindowin explicit@autoreleasepoolblocks to avoid leaking autoreleased objects when called from Go goroutine threads. - Add a Darwin regression test and update the unreleased changelog.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| v3/UNRELEASED_CHANGELOG.md | Documents the macOS screen-string use-after-free crash fix and autorelease pool changes. |
| v3/pkg/application/screen_darwin.go | Implements strdup-based ownership for screen strings, adds autorelease pools, and adjusts screen enumeration/conversion flow. |
| v3/pkg/application/screen_darwin_test.go | Adds a regression test intended to catch invalid screen string lifetimes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| NSArray<NSScreen *> *screens = [NSScreen screens]; | ||
| Screen* returnScreens = malloc(sizeof(Screen) * screens.count); | ||
| for (int i = 0; i < screens.count; i++) { |
There was a problem hiding this comment.
Resolved in commit 826a787c6. getAllScreens now takes an int* outCount and fills it from the same [NSScreen screens] snapshot used to size the malloc, and allScreens() reads that count instead of calling GetNumScreens() separately — so allocation and iteration use one consistent snapshot, closing the TOCTOU/over-read (and the tail-entry strdup leak on shrink). Verified with the MallocScribble=1 regression test plus the full pkg/application suite on macOS.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In `@v3/pkg/application/screen_darwin.go`:
- Around line 110-123: getAllScreens() currently returns only a pointer while
allScreens() calls C.GetNumScreens() separately, creating a TOCTOU risk; change
getAllScreens() to return both the allocated Screen* and its count (e.g., a
small struct or out-param) so the same NSScreen snapshot/alloc is used, then
update allScreens() to use the returned count instead of calling
C.GetNumScreens(); ensure cScreens is sized from that returned count, pass that
same count into cScreenToScreen conversions, and keep the `@autoreleasepool`
around the NSScreen enumeration to avoid leaks from processScreen()/strdup'd
id/name.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 603f78bb-66bd-4572-92ef-6422c098d06e
📒 Files selected for processing (3)
v3/UNRELEASED_CHANGELOG.mdv3/pkg/application/screen_darwin.gov3/pkg/application/screen_darwin_test.go
Review follow-up: allScreens() sized its slice from a separate GetNumScreens() call. If the display configuration changed between the two calls, Go could read past the malloc'd array (freeing garbage id/name pointers) or, when shrinking, leak the strdup'd strings in unvisited tail entries. getAllScreens now returns the count from the same [NSScreen screens] snapshot via an out-parameter.
|
Addressed in the latest commit: |
Description
processScreenstored the raw result of-[NSString UTF8String]in the CScreenstruct'sidandnamefields.UTF8Stringreturns a pointer into an autoreleased buffer whose lifetime ends when the enclosing autorelease pool drains — which can happen before Go reads the strings withC.GoStringincScreenToScreen. The result is a use-after-free that intermittently aborts the process withfatal error: invalid pointer found on stackwhen Wails' internalApplicationDidChangeScreenParametershandler refreshes the screen cache (display sleep/wake, monitor connect/disconnect, resolution change). Because it's a runtime memory-safety abort inside an internal handler, applications cannotrecover()from it.Changes
idandnameare nowstrdup'd into malloc'd memory owned by the struct, and freed exactly once bycScreenToScreenafter conversion.nameis initialised toNULL, fixing a second latent bug: on macOS < 10.15 the field was left uninitialised (garbage pointer fed toGoString).getAllScreens/getScreenForWindownow run inside an explicit@autoreleasepool. Previously, calls from Go goroutine threads (which have no ambient pool) leaked every autoreleased object created during enumeration — and this is the common path, since application event handlers run on goroutines.allScreens()so the regression test can exercise the ownership handover (cgo isn't available in test files).Test (proves the issue)
TestScreenStringsSurviveAutoreleasePoolenumerates screens — with the explicit pool, the autoreleased buffers are deterministically dead by the time Go reads them, so the test reproduces the report's timing-dependent crash reliably:MallocScribble=1): segfaults in the string read — the exact crash signature from macOS: fatal error: invalid pointer found on stack in cScreenToScreen on display change (UTF8String use-after-free) #5556.ID="1" Name="Built-in Retina Display",ID="5" Name="LS49A950U").Full
pkg/applicationsuite passes on macOS (Apple Silicon).Relationship to #5516
#5516 addresses the concurrent off-main-thread enumeration crash and the screen-count TOCTOU. This PR is orthogonal: it fixes the dangling string pointers (and the pre-10.15 uninitialised
name), which #5516 does not touch. The reporter'sruntime.gostring(0x3)stack is this bug. Both can land independently; whichever lands second has a trivial conflict inprocessScreen/processAndCacheScreens.Fixes #5556
Type of change
Checklist:
v3/UNRELEASED_CHANGELOG.mdSummary by CodeRabbit
Bug Fixes
Tests