fix: fixes for sandbox communication (shared origins and permissions)#36
Open
ryan-karn wants to merge 2 commits into
Open
fix: fixes for sandbox communication (shared origins and permissions)#36ryan-karn wants to merge 2 commits into
ryan-karn wants to merge 2 commits into
Conversation
…allstackincubator#30, callstackincubator#28) - Add iOS support for shared-origin factory pooling (same-origin sandboxes reuse a single RCTReactNativeFactory) - Add idleTTL prop to defer ReactHost/factory cleanup after last surface unmounts, enabling warm starts for same-origin remounts - Add origin-pooling demo app demonstrating both features on iOS and Android Ref: callstackincubator#28 Ref: callstackincubator#30
da093f5 to
5eb1133
Compare
…ittedFrom callstackincubator#35) Fixing issues for handling messages and errors when sandboxes use the same origin. Namely, this approach defines and passes an id that is passed to the sandbox, and provides a hook that allows sandboxes to use this for messaging. This is to distinguish views on the same origin that share the same runtime and globals. This also addresses an issue there allowOrigin checks where checking sender side allows rather than receiver side. ref: callstackincubator#29 ref: callstackincubator#35
5eb1133 to
13a98dd
Compare
Contributor
Author
|
Detailing some the of the behaviour seen prior to this fix; (1) Sandbox → Host messagingiss29_bug1.mp4
(2) Sandbox→Sandbox messagingiss29_bug3.mp4
(3) Error Callbacksiss29_bug5.mp4
|
Contributor
Author
|
Demonstration of post-fix behaviour: (1) Sandbox → Host messagingiss29_fix1.mp4
(2) Sandbox→Sandbox messagingiss29_fix2.mp4
(3) Error Callbacksiss29_fix3.mp4
(4) Origin permission checksiss29_fix4.mp4
|
Contributor
Author
|
Also, demonstration of the p2p counter demo post this change - Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-05-14.at.14.53.12.mov |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note this PR is based on changes proposed in #34
Summary
This change fixes cross-origin messaging, error broadcasting, and access control for sandboxes sharing a Hermes VM via origin pooling.
#29
In order to achieve this behaviour, additional changes to the API/approach for sending and handling messages within a bundle were needed. (See details below).
These changes are:
useSurfaceMessaginghookWhy
When multiple sandboxes share an origin (and therefore a single Hermes VM), there is only one
globalThis.postMessageand oneglobalThis.setOnMessagefor the entire runtime. The native side can install these globals, but it cannot know which surface a JS call originates from without cooperation from the JS layer.The fundamental problem:
postMessage({type: 'hello'})arrives at the native host function, but the native side has no way to determine which of the N surfaces sharing this VM made the call. Similarly, when a message arrives from the host, the native side can invoke the JS callback, but if all surfaces registered via the samesetOnMessage, only the last one wins.The solution requires the JS side to include a routing hint (
__sandboxDelegateId) in outgoing messages and to register listeners keyed by delegate ID. This is inherently a JS-side concern because:initialProperty— it's only available to the JS component as a prop.postMessagehost function receives a genericjsi::Value— it can't inspect the JS call stack to determine which React component invoked it.setOnMessageso the native side can maintain a map of callbacks.The hook (or the equivalent convention-based pattern) bridges this gap by attaching the surface identity to messages at the JS layer, enabling the native layer to route correctly.
Break down of changes included
iOS Fixes (
SandboxReactNativeDelegate.mm)1.
setOnMessageargument countThe native
setOnMessageJSI function strictly required 1 argument, but the JSuseSurfaceMessaginghook passes an optionaldelegateIdas the second argument. Updated to accept 1 or 2 arguments.2. JSI global re-installation on warm start
When a same-origin sandbox warm-starts, it needs to swap
postMessage/setOnMessageto point to the new delegate. The original code useddefineReadOnlyGlobalwithconfigurable: false, making the properties permanently locked. Replaced withObject.definePropertyusingconfigurable: trueso warm-start re-installations succeed.3. Per-surface
setOnMessagedispatchPreviously,
_onMessageSandboxwas a single shared callback (last-writer-wins). Added_surfaceMessageCallbacksmap keyed by delegate ID so each surface gets its own listener, matching Android behavior.4. Cross-origin message routing
Changed
routeMessage:toSandbox:to usefindAll()(deliver to all delegates for the target origin) and reordered checks: existence first, then permissions. This ensuresSandboxRoutingErrorfires when the target doesn't exist, andAccessDeniedErroronly fires when the target exists but the sender lacks permission.5. Error broadcasting
Updated
setupErrorHandlerto broadcast errors to all delegates registered for the origin viaregistry.findAll(), matching Android. The error handler capturesoriginby value and uses a weak reference toselfto prevent crashes if the delegate is deallocated.6. Error handler idempotency
setupErrorHandleris now idempotent — it sets a flag (__sandboxErrorHandlerInstalled) onErrorUtilsand skips re-installation on warm start. The cold-start handler remains active and broadcasts via the registry.7. Self-targeting removal
Removed the guard that blocked a sandbox from sending messages to its own origin, matching Android behavior.
8. Registry unregistration fix
Changed
deallocandsetOriginto useunregisterDelegateinstead ofunregister, which was nuking all delegates for the entire origin when only one should be removed.Android Fixes (
SandboxJSIInstaller.cpp,SandboxReactNativeDelegate.kt,SandboxReactNativeViewManager.kt)1. Object mutation in postMessage
The
__sandboxDelegateIdproperty was stripped from the caller's message object without being restored. Now the property is restored after serialization to avoid surprising the caller.2. Thread-safe delegate map
delegateByIdchanged frommutableMapOf(non-thread-safe) toConcurrentHashMapsince it's accessed from both the JS and UI threads.3. Atomic delegate ID generation
nextDelegateIdchanged from a plainLongtoAtomicLongto prevent duplicate IDs under concurrent access.4.
allowedOriginsregistrationAndroid was always registering sandboxes with empty
allowedOriginsin the C++SandboxRegistry. Both registration paths (installSandboxJSIBindingsandnativeRegisterDelegate) now read theallowedOriginsfield from the Kotlin delegate via JNI at registration time. AddednativeUpdateAllowedOriginsJNI method and call it from the view manager when the prop changes.5. Cross-origin routing order
Aligned with iOS: check target existence first (
SandboxRoutingError), then permissions (AccessDeniedError). Previously Android checked permissions first, which produced misleading errors when the target didn't exist.Shared C++ (
SandboxRegistry.cpp,SandboxRegistry.h)#35
1.
registerSandboxalways updatesallowedOriginsThe duplicate-delegate check previously returned early without updating
allowedOrigins_[origin]. Now it skips adding the delegate but always updates the allowed origins map.2.
isPermittedFromsemanticsUpdated to check the target's
allowedOrigins(receiver-side access control): "does the target accept messages from this source?"3.
updateAllowedOriginsmethodNew method that updates allowed origins for an origin without requiring a delegate reference. Used by Android's JNI bridge.
Cleanup function
setOnMessagenow returns a cleanup function that replaces the callback with a no-op. Consumers should use it inuseEffectcleanup to prevent stale callbacks when a surface unmounts while the VM stays alive.Demo App (
apps/origin-pooling)Convention-based messaging (
SandboxAppConvention.tsx)New component demonstrating messaging without importing
@callstack/react-native-sandbox. UsesglobalThis.postMessage/setOnMessagedirectly with the__sandboxDelegateIdconvention.Dual-approach wiring
SandboxAppConvention(convention-based, no library import)SandboxApp(library-based,useSurfaceMessaging)Access control demo
isolated-1AccessDeniedErrorwhen trying to reach alpha/betaDemo App (
apps/p2p-counter)