Skip to content

Protect against race conditions accessing the same RCTDevSettings object #743

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

Merged
merged 16 commits into from
Mar 9, 2021

Conversation

HeyImChris
Copy link

@HeyImChris HeyImChris commented Mar 8, 2021

Please select one of the following

  • I am removing an existing difference between facebook/react-native and microsoft/react-native-macos 👍
  • I am cherry-picking a change from Facebook's react-native into microsoft/react-native-macos 👍
  • I am making a fix / change for the macOS implementation of react-native
  • I am making a change required for Microsoft usage of react-native

Summary

It's possible to get into a scenario where the main thread and a background thread are trying to edit the same object at the same time, causing a deadlock on initialization. The general deadlock happens like this:

  1. Main thread: creates the React-Native instance
  2. Main thread: instance creation triggers creating an RCTCxxBridge
  3. Main thread: RCTCxxBridge creation kicks off an async setup routine
  4. Background thread: async setup routine triggers the creation of an RCTDevSettings module
  5. Main thread: checks various props in RCTDevSettings (e.g. isDevModeEnabled and some others) on another module's init
  6. Main thread: Takes lock to access RCTDevSettings
  7. Background thread: Holding RCTDevSettings waiting for lock to modify it
  8. Deadlock

It's not totally clear to me why the background thread is stopping the main thread's access to RCTDevSettings, but from searching around it appears that we aren't the first ones to hit multi-threading issues using Apple's NSUserDefaults object.

The simplest fix with the smallest churn between our fork and upstream that also stops the deadlocking from happening is to kick off the main thread's call to happen async so we don't block on setting user defaults.

An alternative fix here can be to move all the NSUserDefaults props to be ivars on the class, but that has a couple issues.

  1. Introduces a bigger diff with upstream
  2. Breaks backwards compatibility (this was documented 4 years ago so not sure how relevant it is anymore)
  3. Allows developers to persist settings and change some other configs on the fly

Changelog

[Apple] [Bug] - Protect against race conditions deadlocking on init

Test Plan

Ran this in a downstream test app and we consistently deadlock here with another thread holding a mutex on the same object. When we dispatch async we don't block the thread on data manipulation and won't deadlock anymore.

Microsoft Reviewers: Open in CodeFlow

@HeyImChris HeyImChris self-assigned this Mar 8, 2021
@HeyImChris HeyImChris requested a review from alloy as a code owner March 8, 2021 22:51
@pull-bot
Copy link

pull-bot commented Mar 8, 2021

Messages
📖

📋 Verify Changelog Format - A changelog entry has the following format: [CATEGORY] [TYPE] - Message.

CATEGORY may be:
  • General
  • macOS
  • iOS
  • Android
  • JavaScript
  • Internal (for changes that do not need to be called out in the release notes)

TYPE may be:

  • Added, for new features.
  • Changed, for changes in existing functionality.
  • Deprecated, for soon-to-be removed features.
  • Removed, for now removed features.
  • Fixed, for any bug fixes.
  • Security, in case of vulnerabilities.

MESSAGE may answer "what and why" on a feature level. Use this to briefly tell React Native users about notable changes.

Generated by 🚫 dangerJS against 763d32b

Comment on lines 124 to 126
dispatch_async(dispatch_get_main_queue(), ^{
[self->_userDefaults setObject:self->_settings forKey:kRCTDevSettingsUserDefaultsKey];
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this cause an issue because this method is called on init, and someone might potentially use this object before all the default values are set because it's asynchronous?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe we should only dispatch async to main thread when the current running thread isn't main thread.... or do you know already it will always be in background thread?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the utility next to it that does a dispatch_sync is explicitly named unsafe because of deadlocks exactly like this one :D

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice yea I think it makes sense to use RN's explicit method to check the thread. I don't think we can guarantee this is ever called on a given thread

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this cause an issue because this method is called on init, and someone might potentially use this object before all the default values are set because it's asynchronous?

Don't think we need to worry about that. The _settings var is determined synchronously before we call this and the _userDefaults value is set up synchronously before this in init.

@HeyImChris HeyImChris merged commit 11fa4fd into microsoft:master Mar 9, 2021
HeyImChris added a commit to HeyImChris/react-native-macos that referenced this pull request Mar 9, 2021
…ect (microsoft#743)

* Update RCTCxxBridge.mm

* Update RCTCxxBridge.mm

* protect against race conditions

* use built in main queue execution
HeyImChris added a commit that referenced this pull request Mar 9, 2021
* Add nullability checks (#704)

* Update RCTCxxBridge.mm

* add nullability checks

* Protect against race conditions accessing the same RCTDevSettings object (#743)

* Update RCTCxxBridge.mm

* Update RCTCxxBridge.mm

* protect against race conditions

* use built in main queue execution

* pod update
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants