Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proof of Concept Insights - (Apparent) Race condition on disconnecting animated nodes #9

Closed
bentobox19 opened this issue Aug 3, 2022 · 2 comments

Comments

@bentobox19
Copy link
Contributor

bentobox19 commented Aug 3, 2022

Context

The following exception is being thrown at random during E2E tests:

Exception thrown while executing UI block: 'parentNode' is a required parameter
__44-[RCTUIManager flushUIBlocksWithCompletion:]_block_invoke
    RCTUIManager.m:1201
__44-[RCTUIManager flushUIBlocksWithCompletion:]_block_invoke.498
__RCTExecuteOnMainQueue_block_invoke
____detox_sync_dispatch_wrapper_block_invoke
_dispatch_call_block_and_release
_dispatch_client_callout
_dispatch_main_queue_drain
_dispatch_main_queue_callback_4CF
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRunLoopRun
CFRunLoopRunSpecific
GSEventRunModal
-[UIApplication _run]
__detox_sync_UIApplication_run
UIApplicationMain
main
start_sim
0x0
0x0

Investigation

Building and running the E2E tests

detox build -c ios.sim.debug
detox test -c ios.sim.debug -l trace

Logging of the application

  • Command
/usr/bin/xcrun simctl spawn 7BC04173-BF0C-43FD-B02A-0D7DCF1B4893 log stream --level debug --style compact --predicate 'process == "MetaMask"'
  • We find the cause of the exception to be at RCTNativeAnimatedNodesManager.m:140.
2022-08-02 23:02:31.998 Df MetaMask[84277:10fd37] (Foundation) *** Assertion failure in -[RCTNativeAnimatedNodesManager disconnectAnimatedNodes:childTag:](), /Users/bentobox/Documents/dev/mm/mobile/node_modules/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m:140

2022-08-02 23:02:31.998 E  MetaMask[84277:10fd37] [com.facebook.react.log:native] Exception thrown while executing UI block: 'parentNode' is a required parameter
  • The code at that function is
- (void)disconnectAnimatedNodes:(nonnull NSNumber *)parentTag
                       childTag:(nonnull NSNumber *)childTag
{
  RCTAssertParam(parentTag);
  RCTAssertParam(childTag);

  RCTAnimatedNode *parentNode = _animationNodes[parentTag];
  RCTAnimatedNode *childNode = _animationNodes[childTag];

  RCTAssertParam(parentNode);
  RCTAssertParam(childNode);

  [parentNode removeChild:childNode];
  [childNode setNeedsUpdate];
}
  • The line RCTAssertParam(parentNode); is a failing assertion. We further explore by adding logs, and rebuilding the RN app.
#import <React/RCTLog.h>
- (void)disconnectAnimatedNodes:(nonnull NSNumber *)parentTag
                       childTag:(nonnull NSNumber *)childTag
{
  
  RCTLogInfo(@"disconnectAnimatedNodes (Tag) - %@ - %@ +", parentTag, childTag);

  RCTAssertParam(parentTag);
  RCTAssertParam(childTag);

  RCTAnimatedNode *parentNode = _animationNodes[parentTag];
  RCTAnimatedNode *childNode = _animationNodes[childTag];

  RCTLogInfo(@"disconnectAnimatedNodes (parentNode) - %@ - %@ +", parentNode, childNode);

  RCTAssertParam(parentNode);
  RCTAssertParam(childNode);

  [parentNode removeChild:childNode];
  [childNode setNeedsUpdate];
}
  • However there is not a lot of evidence for a race condition.
  • After some investigation, we find the caller of this function to be at ./node_modules/react-native/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m:disconnectAnimatedNodes.
  __removeChild(child: AnimatedNode): void {
    const index = this._children.indexOf(child);
    if (index === -1) {
      console.warn("Trying to remove a child that doesn't exist");
      return;
    }
    if (this.__isNative && child.__isNative) {
      NativeAnimatedHelper.API.disconnectAnimatedNodes(
        this.__getNativeTag(),
        child.__getNativeTag(),
      );
    }
    this._children.splice(index, 1);
    if (this._children.length === 0) {
      this.__detach();
    }
  }
  • After adding a log (yarn watch will just rebuild the bundle) and restart the test we find the start of a proof:
  • Notice the one that produces the exception
2022-08-03 03:06:04.656 I  MetaMask[63634:158457] [com.facebook.react.log:native] b. disconnectAnimatedNodes - (null) - (null) +
  • Let's see the error
2022-08-03 04:06:17.676 I  MetaMask[66774:1610a2] [com.facebook.react.log:native] disconnectAnimatedNodes (Tag) - 11502 - 11503 +
2022-08-03 04:06:17.677 I  MetaMask[66774:1610a2] [com.facebook.react.log:native] disconnectAnimatedNodes (Node) - (null) - (null) +
  • We want to find out when the parent was deleted
2022-08-03 04:06:17.666 I  MetaMask[66774:1610db] [com.facebook.react.log:javascript] '__removeChild', 10446, 11502

Early Conclusions

  • This call was issued before the call within RN. We believe, in parallel there is another call to get rid of the object, which produces the result to RCTAnimatedNode *parentNode = _animationNodes[parentTag]; to be null. More investigation is needed to find the real root cause. For now this is the issued fix
- (void)disconnectAnimatedNodes:(nonnull NSNumber *)parentTag
                       childTag:(nonnull NSNumber *)childTag
{
  RCTAssertParam(parentTag);
  RCTAssertParam(childTag);

  RCTAnimatedNode *parentNode = _animationNodes[parentTag];
  RCTAnimatedNode *childNode = _animationNodes[childTag];

  // Temporary patch!
  // There is a race condition after removing
  // the promise polifill and transform-regenerator.
  //
  // We'll set the investigation of the root cause
  // as technical debt.
  //
  // The caller is at node_modules/react-native/Libraries/
  //   /Animated/nodes/AnimatedWithChildren.js:__removeChild
  if (parentNode) {
    [parentNode removeChild:childNode];
  }

  if (childNode) {
    [childNode setNeedsUpdate];
  }
}

Further Facts

  • Different experiments were performed to determine the root cause of the exception.

Testing different patches over main

  • We took the main branch, and by only applying this patch into it we were able to obtain the error
diff --git a/node_modules/metro-react-native-babel-preset/src/configs/main.js b/node_modules/metro-react-native-babel-preset/src/configs/main.js
index caa45fe..8eeb742 100644
--- a/node_modules/metro-react-native-babel-preset/src/configs/main.js
+++ b/node_modules/metro-react-native-babel-preset/src/configs/main.js
@@ -174,16 +174,6 @@ const getPreset = (src, options) => {
     extraPlugins.push([require("@babel/plugin-transform-react-jsx-self")]);
   }

-  if (!options || options.enableBabelRuntime !== false) {
-    extraPlugins.push([
-      require("@babel/plugin-transform-runtime"),
-      {
-        helpers: true,
-        regenerator: !isHermes
-      }
-    ]);
-  }
-
   return {
     comments: false,
     compact: true,
@@ -196,7 +186,6 @@ const getPreset = (src, options) => {
       {
         plugins: [
           ...defaultPluginsBeforeRegenerator,
-          isHermes ? null : require("@babel/plugin-transform-regenerator"),
           ...defaultPluginsAfterRegenerator
         ].filter(Boolean)
       },
  • Further investigation including the patch react-native+0.66.3.patch, which ,among other things, comments the promise polifill gave the following results.

    • Commenting only the promise polifill makes the exception appear.
    • Removal only of @babel/plugin-transform-runtime won't make the exception appear.
    • Removal only of @babel/plugin-transform-regenerator makes the exception appear.
  • It is important to stress the fact that these are tests over a patched main branch. These three patches are fundamental to the working of ses in the react native contect.

Stack trace of __removeChild

  • Obtainable with console.log(new Error().stack);

  • Needs further study.

Error@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:7802:32
__removeChild@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:75008:32
__detach@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:78559:32
__removeChild@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:75017:24
__detach@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:78342:32
_attachProps@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:78062:38
UNSAFE_componentWillReceiveProps@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:78127:28
callComponentWillReceiveProps@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:20553:52
updateClassInstance@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:20725:42
updateClassComponent@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:24627:45
beginWork$1@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:29655:29
performUnitOfWork@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:29066:29
workLoopSync@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:29005:28
renderRootSync@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:28981:25
performSyncWorkOnRoot@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:28742:40
performSyncWorkOnRoot@[native code]
flushSyncCallbacks@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:19068:36
flushSyncCallbacksOnlyInLegacyMode@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:19049:29
batchedUpdates$1@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:28790:47
batchedUpdates@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:16630:36
notify@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:603478:14
notifyNestedSubs@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:603549:28
handleChangeWrapper@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:603554:27
handleChangeWrapper@[native code]
dispatch@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:550882:17
update_bg_state_cb@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:553331:32
@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:376859:19
forEach@[native code]
notify@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:376858:39
update@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:376881:20
@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:432894:22
generatorResume@[native code]
fulfilled@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=io.metamask.MetaMask:432776:30
promiseReactionJob@[native code]
@leotm
Copy link
Collaborator

leotm commented Sep 14, 2023

prev/old screenshots for reference (and sim rec)

e2e fail Not a value node smoke addressbook tests fail Error - Couldn't find a navigation context Screenshot 2023-09-14 at 17 10 12

@leotm
Copy link
Collaborator

leotm commented Sep 14, 2023

closing as related e2e tests fixed since/currently in

Screenshot 2023-09-14 at 16 31 27

@leotm leotm closed this as completed Sep 14, 2023
leotm added a commit to MetaMask/metamask-mobile that referenced this issue Nov 10, 2023
## **Description**

Problem being solved: prototype pollution/poisoning

SES lockdown (shim v0.18.8) on iOS JSC, baked early into RN core before
RN initialisation for the simplest minimal solution
as opposed to previous approach of shim'ing at the beginning of our
entry file requiring further complex lib patches

with SES lockdown on Android Hermes (introduced earlier in our [RN v0.71.6 upgrade](#6220))
being followed up separately
currently bundling successfully, but runtime not yet functional

_SES lockdown on Android JSC was also passing smoke tests after some work prior to Hermes_
_so a backup engine worth keeping on ice being followed up separately_ 

_Previous patches no longer required:
[`eth-keyring-controller`](https://github.com/MetaMask/metamask-mobile/pull/3794/files#diff-19aae36749eec9908c74557591fade5cc596e9a40422d88594c0fba456870389),
`ethjs-contract` (one not [two](https://github.com/MetaMask/metamask-mobile/pull/3794/files#diff-1970015453fca9583682c37a44695a3d26645329e586bb70ff6f05b74f936802)), [`web3-core-methods`](LavaMoat/docs#8), [`metro-react-native-babel-preset`](LavaMoat/docs#4), Sentry [config](LavaMoat/docs#6) (see previous PR: #3794

_Nb: `@babel/plugin-transform-regenerator` removed from
`metro-react-native-babel-preset` since initial investigation_
_Nb: `@babel/plugin-transform-runtime` config opt `regenerator: true`
previously caused iOS animated node assertion failures_
_Nb: default `@babel/plugin-transform-runtime` via
`metro-react-native-babel-preset` causes additional 4 SES warnings_

<details>
<summary>Nb: Current behaviour (not SES)</summary>

```
main, jsc
- import wallet via SRP
  - tap form field: disables cmd+D (app must be closed, no Metro restart)

main, v8
- (fresh) import wallet via SRP
  - if cmd+v paste ok, bot Import btn tappable when filled, but spinner hang (no error/warn), ~20s Metro dc
  - restart, enter pw, tap Unlock btn, spinner hang, still ~20s Metro dc
  - tap Reset Wallet, hang
- import (continued)
  - partial responsiveness
    - tap form field: dev menu disabled (cmd+d, app must be closed)
    - unresponsive: tap Back, tap Show, cannot tap Import, cmd+v disabled
    - responsive: cycle/input fields
    - still ~20s Metro dc
```

</details>

<details>
<summary>Previous SES warnings when locking down at entry file (not RN
InitializeCore)</summary>

https://www.diffchecker.com/fjj1iObp

```console
# JSC (26)
Removing intrinsics.Object.setPrototypeOf.default
Removing intrinsics.Object.setPrototypeOf.__esModule
Removing intrinsics.Object.assign.default
Removing intrinsics.Object.assign.__esModule
Removing intrinsics.Reflect.construct.default
Removing intrinsics.Reflect.construct.__esModule
Removing intrinsics.Reflect.decorate
Removing intrinsics.Reflect.metadata
Removing intrinsics.Reflect.defineMetadata
Removing intrinsics.Reflect.hasMetadata
Removing intrinsics.Reflect.hasOwnMetadata
Removing intrinsics.Reflect.getMetadata
Removing intrinsics.Reflect.getOwnMetadata
Removing intrinsics.Reflect.getMetadataKeys
Removing intrinsics.Reflect.getOwnMetadataKeys
Removing intrinsics.Reflect.deleteMetadata
Removing intrinsics.%ArrayPrototype%.toReversed
Removing intrinsics.%ArrayPrototype%.toSorted
Removing intrinsics.%ArrayPrototype%.toSpliced
Removing intrinsics.%ArrayPrototype%.with
Removing intrinsics.%ArrayPrototype%.@@unscopables.toReversed
Removing intrinsics.%ArrayPrototype%.@@unscopables.toSorted
Removing intrinsics.%ArrayPrototype%.@@unscopables.toSpliced
Removing intrinsics.%TypedArrayPrototype%.toReversed
Removing intrinsics.%TypedArrayPrototype%.toSorted
Removing intrinsics.%TypedArrayPrototype%.with
```

```console
# V8 (33)
Removing intrinsics.Object.assign.default
Removing intrinsics.Object.assign.__esModule
Removing intrinsics.Object.setPrototypeOf.default
Removing intrinsics.Object.setPrototypeOf.__esModule
Removing intrinsics.JSON.rawJSON
Removing intrinsics.JSON.isRawJSON
Removing intrinsics.Reflect.construct.default
Removing intrinsics.Reflect.construct.__esModule
Removing intrinsics.Reflect.decorate
Removing intrinsics.Reflect.metadata
Removing intrinsics.Reflect.defineMetadata
Removing intrinsics.Reflect.hasMetadata
Removing intrinsics.Reflect.hasOwnMetadata
Removing intrinsics.Reflect.getMetadata
Removing intrinsics.Reflect.getOwnMetadata
Removing intrinsics.Reflect.getMetadataKeys
Removing intrinsics.Reflect.getOwnMetadataKeys
Removing intrinsics.Reflect.deleteMetadata
Removing intrinsics.%ArrayPrototype%.toReversed
Removing intrinsics.%ArrayPrototype%.toSorted
Removing intrinsics.%ArrayPrototype%.toSpliced
Removing intrinsics.%ArrayPrototype%.with
Removing intrinsics.%ArrayPrototype%.@@unscopables.toReversed
Removing intrinsics.%ArrayPrototype%.@@unscopables.toSorted
Removing intrinsics.%ArrayPrototype%.@@unscopables.toSpliced
Removing intrinsics.%ArrayBufferPrototype%.transferToFixedLength
Removing intrinsics.%ArrayBufferPrototype%.detached
Removing intrinsics.%StringPrototype%.isWellFormed
Removing intrinsics.%StringPrototype%.toWellFormed
Removing intrinsics.%RegExpPrototype%.unicodeSets
Removing intrinsics.%TypedArrayPrototype%.toReversed
Removing intrinsics.%TypedArrayPrototype%.toSorted
Removing intrinsics.%TypedArrayPrototype%.with
```

</details>

<details>
<summary>Notes on patch creation</summary>

- `--exclude 'nothing'` to include `package.json` changes, then trim
patch
- `react-native` requires trimming majority of patch after initial diffs
- upon failure on symlinks, `git clean -fdx` and re-create

</details>

## **Related issues**

Fixes
- LavaMoat/docs#1 various issues
  - #3794 previous pr
- LavaMoat/docs#12 various issues

Worthy read for everyone on adding/upgrading libraries
- endojs/endo#1855

## **Manual testing steps**

App functions normally

## **Screenshots/Recordings**

### **Before**

Previously failing iOS (JSC) E2E tests have now been fixed

- LavaMoat/docs#9
  - https://github.com/MetaMask/metamask-mobile/assets/1881059/3cac630f-6ec8-4975-a273-8a115a4e8fe9
  - https://github.com/MetaMask/metamask-mobile/assets/1881059/67221672-d1a3-4346-a783-5f93b53dbbe9
  - https://github.com/MetaMask/metamask-mobile/assets/1881059/baa6e512-e307-4f0f-ae2f-77bfb4a29baf
  - https://github.com/MetaMask/metamask-mobile/assets/1881059/c21ffb0b-c9df-4223-ba73-5383dc8627f5

And more screenshots in related issues linked above

### **After**

App functions normally

---------

Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants