mobile: return string instead of []byte + update Swift callers#8663
Conversation
The gomobile wrapper copies Go pointer-containing return values to the C thread stack using runtime.wbMove. When a GC cycle runs during the copy, bulkBarrierPreWrite panics because the destination isn't GC-tracked. Returning string avoids this — gomobile marshals strings via C heap allocation rather than leaving them as Go slice headers. See getlantern/engineering#3175 for the full crash analysis (from Freshdesk #172640 — Derek reporting "Lantern Crash" on macOS 26.3.1). Go changes: AvailableFeatures, UserData, FetchUserData, GetAvailableServers, GetSelectedServerJSON, OAuthLoginCallback, AcknowledgeGooglePurchase, AcknowledgeApplePurchase, Login, Logout, DeleteAccount Swift changes (macos + ios): preserve Flutter contract by converting the string back to Data for methods whose Dart side reads `bytes` via utf8.decode (getUserData, fetchUserData, oauthLoginCallback, login, logout, deleteAccount, acknowledgeInAppPurchase). For methods whose Dart side expects String (featureFlags, getLanternAvailableServers, getSelectedServerJSON), just pass the gomobile string directly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR mitigates a gomobile GC/write-barrier crash by changing exported Go mobile APIs to return string (instead of []byte) and updating the iOS/macOS Swift method-channel handlers to preserve existing Flutter/Dart contracts (bytes vs string expectations).
Changes:
- Update 11 gomobile-exported functions in
lantern-core/mobile/mobile.gofrom[]byte/([]byte, error)tostring/(string, error). - Update iOS/macOS
MethodHandler.swiftcall sites to either (a) convert returned JSONStringback toDatafor Dart paths that expect bytes, or (b) pass throughStringdirectly where Dart expects a string. - Normalize Swift-side fallback behavior for empty results (e.g.,
"{}"/"[]") for feature flags and server JSON methods.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
lantern-core/mobile/mobile.go |
Switch exported gomobile return types to string to avoid pointer-bearing return values on cgo callback stacks. |
ios/Runner/Handlers/MethodHandler.swift |
Update Swift callers to match new gomobile signatures while preserving Dart decoding expectations. |
macos/Runner/Handlers/MethodHandler.swift |
Same as iOS: adapt to string returns and maintain Dart-side contract (bytes vs string). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
It sounds like you have another solution for this @garmr-ulfr is that right? Should I close this? |
I'm still working on it. It's fine to merge this in for now. |
The gomobile-exported funcs in lantern-core/mobile/mobile.go now return
string instead of []byte. The generated Android binding will therefore
return String where it used to return ByteArray.
For each affected method, match what the iOS handler does so the Flutter
platform-channel contract stays stable:
* Methods whose Dart callers expect bytes (Uint8List) — login,
logout, deleteAccount, userData, fetchUserData, oauthLoginCallback,
acknowledgeGooglePurchase — convert the String result via
`.toByteArray(Charsets.UTF_8)` before calling success() (mirrors
Swift's `.data(using: .utf8)`).
* Methods whose Dart callers expect a String — availableFeatures,
getAvailableServers, getSelectedServerJSON — drop the
`String(byteArray)` constructor and use the return value directly,
with the same "{}" / "[]" empty-default that iOS uses.
Addresses Copilot review on PR #8663.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
OK let's see if this solves the issue for @Derekf5. That will at least give us another data point to work with. |
Summary
Gomobile-exported functions returning `([]byte, error)` can crash with a Go runtime write-barrier panic during GC. This PR converts them to return `(string, error)` and updates the macOS + iOS Swift callers accordingly.
Root Cause
The gomobile wrapper runs on the cgo callback goroutine (locked to the C thread). After the Go function returns, the wrapper copies the return value into a C struct on the OS thread stack using `runtime.wbMove`. Because `[]byte` contains a pointer (slice data), `wbMove` calls `bulkBarrierPreWrite` to track the write. With an active GC cycle, `bulkBarrierPreWrite` verifies the destination is GC-tracked — the C thread stack is not — and calls `runtime.throw`.
Full analysis: getlantern/engineering#3175 (from Freshdesk #172640 — Derek's crash on macOS 26.3.1).
Note: `radiance/common.RunOffCgoStack` (the `withCoreR` wrapper) was intended to fix this class of bug, but it only moves the function body to a worker goroutine — the return-value copy still happens on the cgo callback goroutine, so it's still vulnerable for pointer-bearing return types.
Go changes (`lantern-core/mobile/mobile.go`)
11 functions converted from `([]byte, error)` or `[]byte` to `(string, error)` or `string`:
Swift changes (`macos/` + `ios/` MethodHandler)
The Dart side reads some method results via `jsonDecode(utf8.decode(bytes))` and others via `jsonDecode(result)` (string). To avoid touching the Dart side:
Test Plan
Target branch
Targeting `garmr/radiance-daemon-refactor` per discussion in Slack — the same fix landed on `radiance` main as getlantern/radiance#416 (lock + telemetry), and this is the client-side half.
🤖 Generated with Claude Code