Description
On macOS, processScreen (in pkg/application/screen_darwin.go) stores the result of -[NSString UTF8String] directly into the C Screen struct's id and name fields:
// screen_darwin.go (CGO preamble)
returnScreen.id = [[NSString stringWithFormat:@"%d", displayID] UTF8String]; // line ~71
...
returnScreen.name = [screen.localizedName UTF8String]; // line ~85
-[NSString UTF8String] returns a pointer to an autoreleased buffer whose lifetime is bound to the current autorelease pool. The Screen structs are malloc'd in getAllScreens() and returned to Go, which reads the strings later in cScreenToScreen:
ID: C.GoString(screen.id),
Name: C.GoString(screen.name), // line ~186
By the time Go calls C.GoString, the autorelease pool has typically drained and the underlying UTF-8 buffer has been freed. screen.name/screen.id are then dangling pointers. Most of the time the freed memory still happens to hold the old bytes, so it appears to work; intermittently it points at reused/garbage memory and GoString dereferences an invalid address.
This is a use-after-free. There is also a secondary latent bug in processAndCacheScreens: getAllScreens() sizes its malloc from [[NSScreen screens] count], while numScreens := int(C.GetNumScreens()) queries [[NSScreen screens] count] again in a separate call. If the display configuration changes between the two calls, the loop can index past the allocated array.
Crash
Triggered by Wails' own internal handler for events.Mac.ApplicationDidChangeScreenParameters (registered in application_darwin.go), which fires on display sleep/wake, resolution changes, and monitor connect/disconnect:
fatal error: invalid pointer found on stack
runtime.gostring(0x3)
internal/bytealg.IndexByteString()
runtime.findnull(...)
github.com/wailsapp/wails/v3/pkg/application._Cfunc_GoString(...)
github.com/wailsapp/wails/v3/pkg/application.cScreenToScreen(...) screen_darwin.go:186
github.com/wailsapp/wails/v3/pkg/application.(*macosApp).processAndCacheScreens(...) screen_darwin.go:199
github.com/wailsapp/wails/v3/pkg/application.(*macosApp).run.func3(...) application_darwin.go:327
github.com/wailsapp/wails/v3/pkg/application.(*EventManager).handleApplicationEvent.func1() event_manager.go:171
Because it is a fatal error (a runtime memory-safety abort, not a Go panic), it cannot be recovered with recover(), and the triggering event handler is internal to Wails so applications cannot intercept it.
To reproduce
- Run any Wails v3 app on macOS that stays open for an extended period.
- Let the display sleep/wake a few times, or connect/disconnect an external monitor (anything that emits
NSApplicationDidChangeScreenParametersNotification).
- Intermittently, the app aborts with
fatal error: invalid pointer found on stack in cScreenToScreen.
Observed once over an ~11h session on macOS (Apple Silicon), Go 1.26.1, Wails v3.0.0-alpha.98.
Suggested fix
Don't store the autoreleased UTF8String pointer. Either:
strdup() the result in processScreen and free() it after C.GoString (e.g. in cScreenToScreen / after processAndCacheScreens), or
- copy into fixed-size
char[] fields inside the Screen struct.
Separately, make processAndCacheScreens use a single screen snapshot — e.g. have getAllScreens() return its element count (or a NULL terminator) rather than calling GetNumScreens() a second time.
Environment
- Wails CLI / library:
v3.0.0-alpha.98
- Platform: macOS (Apple Silicon)
- Go: 1.26.1
Description
On macOS,
processScreen(inpkg/application/screen_darwin.go) stores the result of-[NSString UTF8String]directly into the CScreenstruct'sidandnamefields:-[NSString UTF8String]returns a pointer to an autoreleased buffer whose lifetime is bound to the current autorelease pool. TheScreenstructs aremalloc'd ingetAllScreens()and returned to Go, which reads the strings later incScreenToScreen:By the time Go calls
C.GoString, the autorelease pool has typically drained and the underlying UTF-8 buffer has been freed.screen.name/screen.idare then dangling pointers. Most of the time the freed memory still happens to hold the old bytes, so it appears to work; intermittently it points at reused/garbage memory andGoStringdereferences an invalid address.This is a use-after-free. There is also a secondary latent bug in
processAndCacheScreens:getAllScreens()sizes itsmallocfrom[[NSScreen screens] count], whilenumScreens := int(C.GetNumScreens())queries[[NSScreen screens] count]again in a separate call. If the display configuration changes between the two calls, the loop can index past the allocated array.Crash
Triggered by Wails' own internal handler for
events.Mac.ApplicationDidChangeScreenParameters(registered inapplication_darwin.go), which fires on display sleep/wake, resolution changes, and monitor connect/disconnect:Because it is a
fatal error(a runtime memory-safety abort, not a Go panic), it cannot be recovered withrecover(), and the triggering event handler is internal to Wails so applications cannot intercept it.To reproduce
NSApplicationDidChangeScreenParametersNotification).fatal error: invalid pointer found on stackincScreenToScreen.Observed once over an ~11h session on macOS (Apple Silicon), Go 1.26.1, Wails
v3.0.0-alpha.98.Suggested fix
Don't store the autoreleased
UTF8Stringpointer. Either:strdup()the result inprocessScreenandfree()it afterC.GoString(e.g. incScreenToScreen/ afterprocessAndCacheScreens), orchar[]fields inside theScreenstruct.Separately, make
processAndCacheScreensuse a single screen snapshot — e.g. havegetAllScreens()return its element count (or a NULL terminator) rather than callingGetNumScreens()a second time.Environment
v3.0.0-alpha.98