Pass a top-level JS null TurboModule arg to ObjC as nil, not NSNull (#58190) - #58190
Open
christophpurrer wants to merge 1 commit into
Open
Pass a top-level JS null TurboModule arg to ObjC as nil, not NSNull (#58190)#58190christophpurrer wants to merge 1 commit into
christophpurrer wants to merge 1 commit into
Conversation
|
@christophpurrer has exported this pull request. If you are a Meta employee, you can view the originating Diff in D117960577. |
christophpurrer
added a commit
to christophpurrer/react-native-macos
that referenced
this pull request
Aug 29, 2026
…eact#58190) Summary: Changelog: [iOS][Fixed] - Pass a top-level JS `null` TurboModule argument to Objective-C as `nil` instead of `NSNull` when `enableModuleArgumentNSNullConversionIOS` is enabled When `enableModuleArgumentNSNullConversionIOS` is on, `convertJSIValueToObjCObject` maps a JS `null` to `(id)kCFNull`. That is the intended behaviour for nulls *nested* inside arrays and dictionaries, but a `null` in **argument position** must still reach Objective-C as `nil` — `NSNull` is truthy and does not respond to the selectors the receiver expects, so leaking it crashes the callee. The guard that enforced this lived three branches deep in `ObjCTurboModule::setInvocationArg`, reachable only when all of the following held: - `objCArgType == encode(id)`, and - `getArgumentTypeName(...)` returned non-nil, and - `RCTConvert` responded to a selector named after that type. `getArgumentTypeName` resolves the argument type by scanning for `__rct_export__`-prefixed selectors, which the compiler only emits for methods declared with `RCT_EXPORT_METHOD`. Any TurboModule method without that macro — or with an `id`-typed argument, since `[RCTConvert respondsToSelector:selector(id:)]` is `NO` — silently skipped the guard and received `NSNull`. This diff hoists the check to immediately after the conversion, so it applies to every argument regardless of the method's `__rct_export__` metadata, its ObjC type encoding, or whether an `RCTConvert` converter exists. Returning without calling `setArgument:` leaves the `NSInvocation` slot zeroed, i.e. `nil` — identical to what the old guard did. Behaviour is unchanged when the flag is off: the check short-circuits on the flag. Nested `NSNull` inside arrays and dictionaries is untouched, as asserted by the new `testInvokeTurboModuleKeepsNestedNullAsNSNullWhenFlagEnabled` case. (The flag-enabled branch of the pre-existing `testInvokeTurboModuleWithNull` case never executes while the flag defaults to `false`, so it did not cover this.) The pre-existing check inside the `RCTConvert` branch is left in place. It is now effectively unreachable — `objCArg == kCFNull` is the only way `convertedObjCArg` can be `kCFNull`, because every `RCTConvert` converter either returns a non-`kCFNull` input unchanged or builds a new object — but it costs nothing and keeps the diff narrow. Differential Revision: D117960577
christophpurrer
force-pushed
the
export-D117960577
branch
from
August 29, 2026 04:39
e5a43bd to
c2af8ff
比较
christophpurrer
added a commit
to christophpurrer/react-native-macos
that referenced
this pull request
Aug 29, 2026
…eact#58190) Summary: Changelog: [iOS][Fixed] - Pass a top-level JS `null` TurboModule argument to Objective-C as `nil` instead of `NSNull` when `enableModuleArgumentNSNullConversionIOS` is enabled When `enableModuleArgumentNSNullConversionIOS` is on, `convertJSIValueToObjCObject` maps a JS `null` to `(id)kCFNull`. That is the intended behaviour for nulls *nested* inside arrays and dictionaries, but a `null` in **argument position** must still reach Objective-C as `nil` — `NSNull` is truthy and does not respond to the selectors the receiver expects, so leaking it crashes the callee. The guard that enforced this lived three branches deep in `ObjCTurboModule::setInvocationArg`, reachable only when all of the following held: - `objCArgType == encode(id)`, and - `getArgumentTypeName(...)` returned non-nil, and - `RCTConvert` responded to a selector named after that type. `getArgumentTypeName` resolves the argument type by scanning for `__rct_export__`-prefixed selectors, which the compiler only emits for methods declared with `RCT_EXPORT_METHOD`. Any TurboModule method without that macro — or with an `id`-typed argument, since `[RCTConvert respondsToSelector:selector(id:)]` is `NO` — silently skipped the guard and received `NSNull`. This diff hoists the check to immediately after the conversion, so it applies to every argument regardless of the method's `__rct_export__` metadata, its ObjC type encoding, or whether an `RCTConvert` converter exists. Returning without calling `setArgument:` leaves the `NSInvocation` slot zeroed, i.e. `nil` — identical to what the old guard did. Behaviour is unchanged when the flag is off: the check short-circuits on the flag. Nested `NSNull` inside arrays and dictionaries is untouched, as asserted by the new `testInvokeTurboModuleKeepsNestedNullAsNSNullWhenFlagEnabled` case. (The flag-enabled branch of the pre-existing `testInvokeTurboModuleWithNull` case never executes while the flag defaults to `false`, so it did not cover this.) The pre-existing check inside the `RCTConvert` branch is left in place. It is now effectively unreachable — `objCArg == kCFNull` is the only way `convertedObjCArg` can be `kCFNull`, because every `RCTConvert` converter either returns a non-`kCFNull` input unchanged or builds a new object — but it costs nothing and keeps the diff narrow. Differential Revision: D117960577
christophpurrer
force-pushed
the
export-D117960577
branch
from
August 29, 2026 04:44
c2af8ff to
0fffca4
比较
…eact#58190) Summary: Changelog: [iOS][Fixed] - Pass a top-level JS `null` TurboModule argument to Objective-C as `nil` instead of `NSNull` when `enableModuleArgumentNSNullConversionIOS` is enabled When `enableModuleArgumentNSNullConversionIOS` is on, `convertJSIValueToObjCObject` maps a JS `null` to `(id)kCFNull`. That is the intended behaviour for nulls *nested* inside arrays and dictionaries, but a `null` in **argument position** must still reach Objective-C as `nil` — `NSNull` is truthy and does not respond to the selectors the receiver expects, so leaking it crashes the callee. The guard that enforced this lived three branches deep in `ObjCTurboModule::setInvocationArg`, reachable only when all of the following held: - `objCArgType == encode(id)`, and - `getArgumentTypeName(...)` returned non-nil, and - `RCTConvert` responded to a selector named after that type. `getArgumentTypeName` resolves the argument type by scanning for `__rct_export__`-prefixed selectors, which the compiler only emits for methods declared with `RCT_EXPORT_METHOD`. Any TurboModule method without that macro — or with an `id`-typed argument, since `[RCTConvert respondsToSelector:selector(id:)]` is `NO` — silently skipped the guard and received `NSNull`. This diff hoists the check to immediately after the conversion, so it applies to every argument regardless of the method's `__rct_export__` metadata, its ObjC type encoding, or whether an `RCTConvert` converter exists. Returning without calling `setArgument:` leaves the `NSInvocation` slot zeroed, i.e. `nil` — identical to what the old guard did. Behaviour is unchanged when the flag is off: the check short-circuits on the flag. Nested `NSNull` inside arrays and dictionaries is untouched, as asserted by the new `testInvokeTurboModuleKeepsNestedNullAsNSNullWhenFlagEnabled` case. (The flag-enabled branch of the pre-existing `testInvokeTurboModuleWithNull` case never executes while the flag defaults to `false`, so it did not cover this.) The pre-existing check inside the `RCTConvert` branch is left in place. It is now effectively unreachable — `objCArg == kCFNull` is the only way `convertedObjCArg` can be `kCFNull`, because every `RCTConvert` converter either returns a non-`kCFNull` input unchanged or builds a new object — but it costs nothing and keeps the diff narrow. Differential Revision: D117960577
christophpurrer
force-pushed
the
export-D117960577
branch
from
August 29, 2026 05:53
0fffca4 to
d2000e9
比较
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
注册 for free
to join this conversation on GitHub.
Already have an account?
登录 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.
Summary:
Changelog: [iOS][Fixed] - Pass a top-level JS
nullTurboModule argument to Objective-C asnilinstead ofNSNullwhenenableModuleArgumentNSNullConversionIOSis enabledWhen
enableModuleArgumentNSNullConversionIOSis on,convertJSIValueToObjCObjectmaps a JS
nullto(id)kCFNull. That is the intended behaviour for nulls nestedinside arrays and dictionaries, but a
nullin argument position must still reachObjective-C as
nil—NSNullis truthy and does not respond to the selectors thereceiver expects, so leaking it crashes the callee.
The guard that enforced this lived three branches deep in
ObjCTurboModule::setInvocationArg, reachable only when all of the following held:objCArgType == encode(id), andgetArgumentTypeName(...)returned non-nil, andRCTConvertresponded to a selector named after that type.getArgumentTypeNameresolves the argument type by scanning for__rct_export__-prefixedselectors, which the compiler only emits for methods declared with
RCT_EXPORT_METHOD.Any TurboModule method without that macro — or with an
id-typed argument, since[RCTConvert respondsToSelector:selector(id:)]isNO— silently skipped the guard andreceived
NSNull.This diff hoists the check to immediately after the conversion, so it applies to every
argument regardless of the method's
__rct_export__metadata, its ObjC type encoding, orwhether an
RCTConvertconverter exists. Returning without callingsetArgument:leavesthe
NSInvocationslot zeroed, i.e.nil— identical to what the old guard did.Behaviour is unchanged when the flag is off: the check short-circuits on the flag. Nested
NSNullinside arrays and dictionaries is untouched, as asserted by the newtestInvokeTurboModuleKeepsNestedNullAsNSNullWhenFlagEnabledcase. (The flag-enabledbranch of the pre-existing
testInvokeTurboModuleWithNullcase never executes while theflag defaults to
false, so it did not cover this.)The pre-existing check inside the
RCTConvertbranch is left in place. It is noweffectively unreachable —
objCArg == kCFNullis the only wayconvertedObjCArgcan bekCFNull, because everyRCTConvertconverter either returns a non-kCFNullinputunchanged or builds a new object — but it costs nothing and keeps the diff narrow.
Differential Revision: D117960577