Skip to content

Commit c1a0238

Browse files
myleshortonclaudejigar-f
authored
Fix macOS crash in GetAvailableServers (CGo write barrier) (#8541)
* Fix macOS crash in GetAvailableServers due to CGo write barrier panic GetAvailableServers was crashing with a runtime.throw in bulkBarrierPreWrite when called from Swift via gomobile. The Servers() return value contains pointer-rich types (Options with any interfaces) whose copy triggers GC write barriers on the CGo callback stack, which the GC heap bitmap doesn't cover. Move the Servers() + json.Marshal work to a dedicated goroutine so it runs on a proper Go stack where write barriers function correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix getAutoLocation in FFI and mobile for same CGo write barrier issue Apply the same goroutine-based fix to getAutoLocation() in both ffi/ffi.go and mobile/mobile.go. These functions marshal Server types containing pointer-rich Options interfaces, which can trigger GC write barrier panics on the CGo callback stack. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Extract RunOnGoStack helper, add panic recovery Address PR review feedback: - Extract a generic RunOnGoStack[T] helper in utils/gostack.go that runs a closure on a dedicated goroutine and returns the result via channel. Includes defer/recover so a panic in the closure returns an error instead of blocking the caller forever. - Refactor all three call sites (GetAvailableServers, getAutoLocation in FFI, GetAutoLocation in mobile) to use the shared helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Use radiance's CGo-safe JSON methods, remove local helper Move the CGo safety concern into radiance where the pointer-rich types originate. Callers now use ServersJSON() and GetServerByTagJSON() which marshal internally and return plain []byte. - Remove utils/gostack.go (moved to radiance common/gostack.go) - GetAvailableServers uses Manager.ServersJSON() - getAutoLocation (FFI) and GetAutoLocation (mobile) use Core.GetServerByTagJSON() which delegates to Manager.GetServerByTagJSON() - Upgrade radiance to 477c01d which adds the new methods Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Simplify: remove wasteful debug log, update radiance - Remove slog.Debug that converted potentially large []byte to string on every GetAvailableServers call regardless of log level - Update radiance to 405169d which extracts shared lookup logic and uses MarshalContext consistently Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Remove GetServerByTag from App interface All callers are internal to lantern-core and access serverManager directly. Removing the unnecessary interface method and delegation wrapper simplifies the API surface — external callers should use GetServerByTagJSON which is CGo-safe. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Run go mod tidy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add goroutine hop for gomobile-exported functions The previous fix (using ServersJSON/GetServerByTagJSON) moved the marshal inside radiance but didn't address the root cause: the crash is in the gomobile-generated CGo wrapper (_cgoexp_proxymobile__*), which copies Go pointer-containing return values to the C thread stack. The GC heap bitmap doesn't cover that memory, so bulkBarrierPreWrite panics. The only reliable fix is to run the entire function body on a real Go goroutine stack via runOffCgoStack. The goroutine result is sent back through a channel (which only involves copying the final []byte/string, not pointer-rich intermediate types). The FFI path (ffi.go) doesn't need this because its //export functions return *C.char (C-allocated memory, no GC pointers). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Use radiance's common.RunOffCgoStack, remove local copy Move the goroutine hop helper to radiance so any project consuming radiance through gomobile can use it. Replace the local runOffCgoStack in mobile.go with common.RunOffCgoStack. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Remove RunOffCgoStack wrappers from mobile.go ServersJSON and GetServerByTagJSON in radiance now handle the goroutine hop internally, so callers don't need to wrap anything. mobile.go goes back to straightforward delegation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Point to radiance main (bf610b5) after PR #361 merged Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * update radiance. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Jigar-f <jigar@getlantern.org>
1 parent a146b40 commit c1a0238

5 files changed

Lines changed: 33 additions & 25 deletions

File tree

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ require (
2525
github.com/alecthomas/assert/v2 v2.3.0
2626
github.com/getlantern/common v1.2.1-0.20260224184656-5aefb9c21c85
2727
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9
28-
github.com/getlantern/radiance v0.0.0-20260318170544-725ad379e90b
28+
github.com/getlantern/radiance v0.0.0-20260320012916-fcb0501214cd
2929
github.com/sagernet/sing-box v1.12.22
3030
golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0
3131
golang.org/x/sys v0.40.0
@@ -174,11 +174,11 @@ require (
174174
github.com/getlantern/amp v0.0.0-20260305201851-782bc8045e58 // indirect
175175
github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 // indirect
176176
github.com/getlantern/dnstt v0.0.0-20260112160750-05100563bd0d // indirect
177-
github.com/getlantern/fronted v0.0.0-20260316001628-14e5f21104b5 // indirect
177+
github.com/getlantern/fronted v0.0.0-20260319225233-cf2160f85053 // indirect
178178
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 // indirect
179179
github.com/getlantern/iptool v0.0.0-20230112135223-c00e863b2696 // indirect
180180
github.com/getlantern/keepcurrent v0.0.0-20260304213122-017d542145ae // indirect
181-
github.com/getlantern/kindling v0.0.0-20260305202005-ca73a10f13ac // indirect
181+
github.com/getlantern/kindling v0.0.0-20260319225424-4736208dd171 // indirect
182182
github.com/getlantern/lantern-box v0.0.42 // indirect
183183
github.com/getlantern/lantern-water v0.0.0-20260313142412-dfc64287c8c8 // indirect
184184
github.com/getlantern/mtime v0.0.0-20200417132445-23682092d1f7 // indirect

go.sum

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,13 @@ github.com/getlantern/dnstt v0.0.0-20260112160750-05100563bd0d/go.mod h1:LA7cwZQ
238238
github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
239239
github.com/getlantern/errors v1.0.4 h1:i2iR1M9GKj4WuingpNqJ+XQEw6i6dnAgKAmLj6ZB3X0=
240240
github.com/getlantern/errors v1.0.4/go.mod h1:/Foq8jtSDGP8GOXzAjeslsC4Ar/3kB+UiQH+WyV4pzY=
241+
github.com/getlantern/fdcount v0.0.0-20190912142506-f89afd7367c4 h1:JdD4XSaT6/j6InM7MT1E4WRvzR8gurxfq53A3ML3B/Q=
241242
github.com/getlantern/fdcount v0.0.0-20210503151800-5decd65b3731 h1:v+vJ3LgV4nW4xRPZo+xkADDflXLpRbG+Lv69XKWFjTQ=
242243
github.com/getlantern/fdcount v0.0.0-20210503151800-5decd65b3731/go.mod h1:XZwE+iIlAgr64OFbXKFNCllBwV4wEipPx8Hlo2gZdbM=
243244
github.com/getlantern/fronted v0.0.0-20260316001628-14e5f21104b5 h1:TEVUZpGVQ3+jAf+Oq/Ahx/TltaWHRT2Crj7upb7GZJo=
244245
github.com/getlantern/fronted v0.0.0-20260316001628-14e5f21104b5/go.mod h1:1a+iv1xzGxZWj/vCHzr8Z3dF9H1sNTuMSPHUqRsgbl0=
246+
github.com/getlantern/fronted v0.0.0-20260319225233-cf2160f85053 h1:bq5D3qSjLg2PVBPOqhLE5nQCJpfYHYu8MYavHfPJC78=
247+
github.com/getlantern/fronted v0.0.0-20260319225233-cf2160f85053/go.mod h1:2miS+mgq6csnGAUvW3w+VUODRAeo08rohkpkR99YGyM=
245248
github.com/getlantern/golog v0.0.0-20210606115803-bce9f9fe5a5f/go.mod h1:ZyIjgH/1wTCl+B+7yH1DqrWp6MPJqESmwmEQ89ZfhvA=
246249
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 h1:NlQedYmPI3pRAXJb+hLVVDGqfvvXGRPV8vp7XOjKAZ0=
247250
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65/go.mod h1:+ZU1h+iOVqWReBpky6d5Y2WL0sF2Llxu+QcxJFs2+OU=
@@ -257,6 +260,8 @@ github.com/getlantern/keepcurrent v0.0.0-20260304213122-017d542145ae h1:NMq3K7h3
257260
github.com/getlantern/keepcurrent v0.0.0-20260304213122-017d542145ae/go.mod h1:ag5g9aWUw2FJcX5RVRpJ9EBQBy5yJuy2WXDouIn/m4w=
258261
github.com/getlantern/kindling v0.0.0-20260305202005-ca73a10f13ac h1:ev9Y7AsmNVtUdH1bhSGFLU2CUbAMs83KhtbkHa8geAo=
259262
github.com/getlantern/kindling v0.0.0-20260305202005-ca73a10f13ac/go.mod h1:gKsfk+rBgfIEEF71fn/PHNHp5RaDD1avj2MODUpc2WA=
263+
github.com/getlantern/kindling v0.0.0-20260319225424-4736208dd171 h1:UEjX+Gg+T6oGVUbzHJ4JfLhlsIh8Wl8PmTXZYWGS43A=
264+
github.com/getlantern/kindling v0.0.0-20260319225424-4736208dd171/go.mod h1:c5cFjpNrqX8wQ0PUE2blHrO7knAlRCVx3j1/G6zaVlY=
260265
github.com/getlantern/lantern-box v0.0.42 h1:Cn8f41dNWC21RQBtljV1D2uNd3hm29OjELG2Stq1/7o=
261266
github.com/getlantern/lantern-box v0.0.42/go.mod h1:HhBd4LrHj0vIo8XFwvJVONoAySo8kz7jaqDQOzaZVRo=
262267
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9 h1:6seyD2f9tz2am0YQd/Qn+q7LFiiQgnmxgwWFnVceGZw=
@@ -277,8 +282,10 @@ github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175 h1:JWH5BB2o0e
277282
github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175/go.mod h1:h3S9LBmmzN/xM+lwYZHE4abzTtCTtidKtG+nxZcCZX0=
278283
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YLAuT8r51ApR5z0d8/qjhHu3TW+divQ2C98Ac=
279284
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q=
280-
github.com/getlantern/radiance v0.0.0-20260318170544-725ad379e90b h1:TYLWfqi5L3ijG0TdonpA4M0kgzWRKgndy7OIUqn8WMs=
281-
github.com/getlantern/radiance v0.0.0-20260318170544-725ad379e90b/go.mod h1:QG9fHG/GresYdPa3Tr5gYeg8yDkE+crdyvg6KXChQdQ=
285+
github.com/getlantern/radiance v0.0.0-20260318200641-bf610b5661c5 h1:2Ygz2U1eqTVPDSD/9coWmGwcr1bb2/Eor3Q9M2d/iDI=
286+
github.com/getlantern/radiance v0.0.0-20260318200641-bf610b5661c5/go.mod h1:QG9fHG/GresYdPa3Tr5gYeg8yDkE+crdyvg6KXChQdQ=
287+
github.com/getlantern/radiance v0.0.0-20260320012916-fcb0501214cd h1:C1C97PpN8zHX3xjrNJA2GqNIGyZJJccz0tGG/x5M9Sc=
288+
github.com/getlantern/radiance v0.0.0-20260320012916-fcb0501214cd/go.mod h1:54gfT+9fEFJAXLxSynr31iaeksM/M+uGhSvtJPoGEnE=
282289
github.com/getlantern/samizdat v0.0.3-0.20260310125445-325cf1bd1b60 h1:m9eXjDK9vllbVH467+QXbrxUFFM9Yp7YJ90wZLw4dwU=
283290
github.com/getlantern/samizdat v0.0.3-0.20260310125445-325cf1bd1b60/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0=
284291
github.com/getlantern/sing v0.7.18-lantern h1:QKGgIUA3LwmKYP/7JlQTRkxj9jnP4cX2Q/B+nd8XEjo=

lantern-core/core.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ type App interface {
6060
IsVPNRunning() (bool, error)
6161
GetAvailableServers() []byte
6262
MyDeviceId() string
63-
GetServerByTag(tag string) (servers.Server, bool)
63+
GetServerByTagJSON(tag string) ([]byte, bool, error)
6464
ReferralAttachment(referralCode string) (bool, error)
6565
UpdateLocale(locale string) error
6666
StartBackgroundListeners()
@@ -230,7 +230,7 @@ func (lc *LanternCore) initialize(opts *utils.Opts, eventEmitter utils.FlutterEv
230230
func (lc *LanternCore) listeningServerLocationChanges() {
231231
events.Subscribe(func(evt vpn.AutoSelectionsEvent) {
232232
tag := evt.Selections.Lantern
233-
servers, ok := lc.GetServerByTag(tag)
233+
servers, ok := lc.serverManager.GetServerByTag(tag)
234234
if !ok {
235235
slog.Error("no server found with tag", "tag", tag)
236236
return
@@ -347,9 +347,11 @@ func (lc *LanternCore) StopBackgroundListeners() {
347347
slog.Info("Background listeners stopped")
348348
}
349349

350-
func (lc *LanternCore) GetServerByTag(tag string) (servers.Server, bool) {
351-
return lc.serverManager.GetServerByTag(tag)
352-
350+
// GetServerByTagJSON returns the server for a given tag as pre-marshalled JSON.
351+
// This is safe to call from CGo callback stacks because the pointer-rich Server
352+
// types are marshalled here rather than being returned to the caller.
353+
func (lc *LanternCore) GetServerByTagJSON(tag string) ([]byte, bool, error) {
354+
return lc.serverManager.GetServerByTagJSON(tag)
353355
}
354356

355357
func (lc *LanternCore) VPNStatus() (vpn.Status, error) {
@@ -394,13 +396,13 @@ func (lc *LanternCore) AvailableFeatures() []byte {
394396
}
395397

396398
func (lc *LanternCore) GetAvailableServers() []byte {
397-
serversList := lc.rad.ServerManager().Servers()
398-
jsonBytes, err := json.Marshal(serversList)
399+
// Use ServersJSON which marshals under the lock, avoiding GC write barrier
400+
// panics when pointer-rich sing-box types are copied on a CGo callback stack.
401+
jsonBytes, err := lc.rad.ServerManager().ServersJSON()
399402
if err != nil {
400403
slog.Error("Error marshalling servers", "error", err)
401404
return nil
402405
}
403-
slog.Debug("Available servers JSON", "json", string(jsonBytes))
404406
return jsonBytes
405407
}
406408

lantern-core/ffi/ffi.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,14 +265,15 @@ func getAutoLocation() *C.char {
265265
return SendError(err)
266266
}
267267

268-
servers, ok := c.GetServerByTag(location.Lantern)
269-
if !ok {
270-
return SendError(fmt.Errorf("error finding server with tag: %s", location.Lantern))
271-
}
272-
jsonBytes, err := json.Marshal(servers)
268+
// Use GetServerByTagJSON which marshals internally, avoiding GC write
269+
// barrier panics when pointer-rich Server types are copied on the CGo stack.
270+
jsonBytes, ok, err := c.GetServerByTagJSON(location.Lantern)
273271
if err != nil {
274272
return SendError(fmt.Errorf("error marshalling server: %v", err))
275273
}
274+
if !ok {
275+
return SendError(fmt.Errorf("error finding server with tag: %s", location.Lantern))
276+
}
276277
return C.CString(string(jsonBytes))
277278
}
278279

lantern-core/mobile/mobile.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,7 @@ func StopAutoLocationListener() error {
217217
})
218218
}
219219

220-
// // GetAvailableServers returns the available servers in JSON format.
221-
// // This function retrieves the servers from lantern
220+
// GetAvailableServers returns the available servers in JSON format.
222221
func GetAvailableServers() ([]byte, error) {
223222
return withCoreR(func(c lanterncore.Core) ([]byte, error) { return c.GetAvailableServers(), nil })
224223
}
@@ -237,14 +236,13 @@ func GetAutoLocation() (string, error) {
237236
return "", err
238237
}
239238
return withCoreR(func(c lanterncore.Core) (string, error) {
240-
servers, ok := c.GetServerByTag(location.Lantern)
241-
if !ok {
242-
return "", fmt.Errorf("no server found with tag: %s", location.Lantern)
243-
}
244-
jsonBytes, err := json.Marshal(servers)
239+
jsonBytes, ok, err := c.GetServerByTagJSON(location.Lantern)
245240
if err != nil {
246241
return "", fmt.Errorf("error marshalling server: %v", err)
247242
}
243+
if !ok {
244+
return "", fmt.Errorf("no server found with tag: %s", location.Lantern)
245+
}
248246
slog.Debug("Auto location server:", "server", string(jsonBytes))
249247
return string(jsonBytes), nil
250248
})

0 commit comments

Comments
 (0)