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

feat(ses): Shim compatible with Hermes compiler #2334

Open
wants to merge 112 commits into
base: master
Choose a base branch
from

Conversation

leotm
Copy link
Contributor

@leotm leotm commented Jun 25, 2024

Description

Add lockdown shim compatible with Hermes
Make current shim compatible with Hermes
for building React Native (RN) prod apps with SES

Generating the release AAB (Android App Bundle)
i.e. npx react-native build-android --mode=release via RN CLI
calls Gradle's bundleRelease task under the hood (bundle build task on release variant)
which calls RNGP (React Native Gradle Plugin) React task createBundleReleaseJsAndAssets and fails
after Metro finishes writing the release bundle (bundle, sourcemaps, assets)

Gradle emits these Hermes errors

(at runtime we can see both are SyntaxErrors)
Resulting in vague java.lang.StackOverflowError (no error message)

The try/catch approach testing language ft support via a new fn works
since RNGP no longer emits the Hermes errors in the task after Metro bundles
and we're conditionally using parts of SES compatible with the JS engine

The initial approach involved building a new shim via an env var
which involved a lot of duplicates /src files

Nb: clean before bundling to see changes reflected
i.e. ./gradlew clean :app:bundleRelease

Nb: async function* a() {}; alone won't emit an error
but using/referencing it and beyond const b = a; will

Nb: eventually we hit RNGP BundleHermesCTask.kt > run > detectedHermesCommand > detectOSAwareHermesCommand from PathUtils.kt, which calls the Hermes compiler default command hermesc - the path of the binary file, to create the optimised bytecode bundle to load/exec at runtime

TODO

  • use standard async functions without arrows (module-load.js)
  • remove async generators from whitelist (permits.js)
  • remove async generators from intrinsics (get-anonymous-intrinsics-hermes.js)
  • remove repairing async generators during taming (tame-function-constructors.js)
  • remove async generator tests (ses, harden, tame-function-unit)
  • ensure 379 ses tests passing
  • test new shim in React Native repro
    • test android bundle via gradle: ./gradlew :app:createBundleReleaseJsAndAssets
    • test android build/runtime via gradle: ./gradlew :app:installRelease -PreactNativeArchitectures=arm64-v8a
    • test android/ios build/runtime via RN CLI: yarn <android/ios> --mode release
  • add new entry points to hook into get-anonymous-intrinsics-hermes.js
    • original: bundle.js > index.js > lockdown-shim.js > lockdown.js
    • hermes: bundle.js > index-hermes.js > lockdown-shim-hermes.js > lockdown-hermes.js
  • support env var to bundle new ses/lockdown shim bundle/terse variants (bundle.js)
  • bundle new shims in prepare npm lifecycle script (package.json)
  • add new shim exports entry points (package.json)
  • conditionally get intrinsics (get-anonymous-intrinsics.js)
  • conditionally repair async generators (tame-function-constructors.js)
  • conditionally add async generators to whitelist (permits.js)
  • revert new publishing changes
  • revert new entry points
  • revert new bundling with env var changes
  • ensure 379 ses tests still passing
  • restore original tests
  • suggested change
  • revert permits.js changes
  • asyncTrampoline non-arrow async function
  • console info msg x2
  • improved catch block
    • do nothing only on the SyntaxError
    • otherwise re-throw
  • add to commons.js and use
    • AsyncGeneratorFunctionInstance
    • AsyncArrowFunctionInstance?
  • CI (new PR, stacked)

Follow-up, CI testing options discussed

  • ubuntu: RN app + SES, cd android && ./gradlew :app:bundleRelease
    • add Java CI docs if included
  • react-native-community/docker-android is available to use
  • CI(macos): RN app test + SES, Xcode release
    • not needed, Xcode does not emit Hermes errors like Gradle
  • test262:hermes script (like test262:xs)
  • Hermes maintains test262 tests (testsuite.py, testsuite_skiplist.py)
  • (eshost) > Hermes CLI v0.12.0 (few years old, newer being discussed)
  • or build and run Hermes (and/or Static Hermes) as a standalone compiler and VM
    • add docs or at least link to Hermes docs

Security Considerations

Does this change introduce new assumptions or dependencies that, if violated, could introduce security vulnerabilities? How does this PR change the boundaries between mutually-suspicious components? What new authorities are introduced by this change, perhaps by new API calls?

Scaling Considerations

Does this change require or encourage significant increase in consumption of CPU cycles, RAM, on-chain storage, message exchanges, or other scarce resources? If so, can that be prevented or mitigated?

Documentation Considerations

Give our docs folks some hints about what needs to be described to downstream users. Backwards compatibility: what happens to existing data or deployments when this code is shipped? Do we need to instruct users to do something to upgrade their saved data? If there is no upgrade path possible, how bad will that be for users?

Testing Considerations

Every PR should of course come with tests of its own functionality. What additional tests are still needed beyond those unit tests? How does this affect CI, other test automation, or the testnet?

Compatibility Considerations

Does this change break any prior usage patterns? Does this change allow usage patterns to evolve?

Upgrade Considerations

What aspects of this PR are relevant to upgrading live production systems, and how should they be addressed?

Include *BREAKING*: in the commit message with migration instructions for any breaking change.

Update NEWS.md for user-facing changes.

Delete guidance from pull request description before merge (including this!)

packages/ses/src/permits.js Outdated Show resolved Hide resolved
packages/ses/index-hermes.js Outdated Show resolved Hide resolved
packages/ses/lockdown-hermes.js Outdated Show resolved Hide resolved
packages/ses/src/module-load.js Outdated Show resolved Hide resolved
packages/ses/src/module-load.js Outdated Show resolved Hide resolved
packages/ses/src/permits.js Outdated Show resolved Hide resolved
packages/ses/src/permits.js Outdated Show resolved Hide resolved
packages/ses/src/permits.js Outdated Show resolved Hide resolved
packages/ses/src/permits.js Outdated Show resolved Hide resolved
@leotm leotm marked this pull request as ready for review July 3, 2024 12:11
@leotm leotm marked this pull request as draft July 3, 2024 12:49
@leotm leotm changed the title feat(ses): Hermes compatible shim variant feat(ses): Shim compatible with Hermes Jul 3, 2024
@leotm leotm marked this pull request as ready for review July 10, 2024 11:56
@leotm
Copy link
Contributor Author

leotm commented Jul 10, 2024

ready for review ^ tackling the testing strategy separately in a follow-up PR, to keep this change small

Copy link
Member

@kriskowal kriskowal left a comment

Choose a reason for hiding this comment

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

This is indeed far more surgical than I expected.

I only request that we capture the async generator function instance in a single try{eval}catch in commons.js so we can just use if blocks to decide whether to include async generators in the various points you’re currently using try/catch.

I would like to make sure @erights looks at these changes as well.

packages/ses/src/get-anonymous-intrinsics.js Outdated Show resolved Hide resolved
packages/ses/src/get-anonymous-intrinsics.js Outdated Show resolved Hide resolved
packages/ses/src/tame-function-constructors.js Outdated Show resolved Hide resolved
@kriskowal
Copy link
Member

ready for review ^ tackling the testing strategy separately in a follow-up PR, to keep this change small

Are you familiar with stacked PRs? You can propose the test changes in a PR based on this branch so they can be reviewed together. I would hesitate to approve code that doesn’t come with tests in the same merge, though I’m fine with reviewing them separately. Much depends on your workflow.

One workflow that I like is individually reviewable commits in a single PR. That does require more commit grooming, and it looks like you intend to squash the 21 commits here when you merge.

Another workflow is to create “stacked” PRs for each review artifact, then merge them top to bottom, so that ultimately the feature and its tests arrive in master with a single merge commit.

@erights
Copy link
Contributor

erights commented Jul 10, 2024

Glad to see this!

I would like to make sure @erights looks at these changes as well.

Hopefully tomorrow.

@erights erights self-requested a review July 14, 2024 08:40
packages/ses/src/module-load.js Outdated Show resolved Hide resolved
.github/workflows/ci.yml Outdated Show resolved Hide resolved
packages/ses/package.json Outdated Show resolved Hide resolved
Copy link
Contributor

@boneskull boneskull left a comment

Choose a reason for hiding this comment

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

No blocking suggestions.

.github/workflows/ci.yml Outdated Show resolved Hide resolved
.github/workflows/ci.yml Show resolved Hide resolved
"./tools.js": "./tools.js",
"./assert-shim.js": "./assert-shim.js",
"./lockdown-shim.js": "./lockdown-shim.js",
"./compartment-shim.js": "./compartment-shim.js",
"./package.json": "./package.json"
},
"scripts": {
"build": "node scripts/bundle.js",
"build:vanilla": "node scripts/bundle.js",
"build:hermes": "SES_BUILD_TYPE=hermes node scripts/bundle.js",
Copy link
Contributor

Choose a reason for hiding this comment

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

this will fail in powershell (if it matters)

consider util.parseArgs() instead of env vars

Comment on lines 84 to 85
"test:hermes": "./scripts/hermes.sh",
"test:create-hermes-bin-symlinks": "./scripts/hermes-bin-symlinks.sh",
Copy link
Contributor

Choose a reason for hiding this comment

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

also not particularly windows-friendly 😄

packages/ses/scripts/bundle.js Show resolved Hide resolved
packages/ses/scripts/hermesTransforms.js Show resolved Hide resolved
packages/ses/scripts/hermesTransforms.js Outdated Show resolved Hide resolved
@@ -1,10 +1,14 @@
/** @import {ModuleTransforms} from '../../compartment-mapper/types.js' */
Copy link
Contributor

Choose a reason for hiding this comment

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

note: conflict with #2310; this would want to be SyncModuleTransforms instead.

also, if you're reaching in to compartment-mapper from ses (which it appears creates a cyclic dependency), it seems like compartment-mapper (or somewhere else) might want to be where this script lives.

packages/ses/src/commons.js Show resolved Hide resolved
packages/ses/test/hermes-smoke.js Outdated Show resolved Hide resolved
Comment on lines +381 to +409

// Test for async generator function syntax support.
let AsyncGeneratorNewFunctionInstance;
try {
// Wrapping one in an new Function lets the `hermesc` binary file
// parse the Metro js bundle without SyntaxError, to generate the
// optimised Hermes bytecode bundle, when `gradlew` is called to
// assemble the release build APK for React Native prod Android apps.
// Delaying the error until runtime lets us customise lockdown behaviour.
AsyncGeneratorNewFunctionInstance = new FERAL_FUNCTION(
'return (async function* AsyncGeneratorFunctionInstance() {})',
)();
} catch (e) {
// @ts-expect-error ts(2339) Property 'jsEngine' does not exist on type 'Error'. However it exists on Hermes.
if (Error.prototype.jsEngine === 'hermes' && e.name === 'SyntaxError') {
// Swallows Hermes error `async generators are unsupported` at runtime.
// @ts-expect-error ts(2554) Expected 0 arguments, but got 1. It refers to the Web API Window object, but on Hermes we expect 1 argument.
// eslint-disable-next-line no-undef
print('SES: Skipping async generators, unsupported on Hermes');
// Note: `console` is not a JS built-in, so Hermes engine throws:
// Uncaught ReferenceError: Property 'console' doesn't exist
// See: https://github.com/facebook/hermes/issues/675
// However React Native provides a `console` implementation:
// https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/InitializeCore.js
} else {
throw e;
}
}
export const AsyncGeneratorFunctionInstance = AsyncGeneratorNewFunctionInstance;

Choose a reason for hiding this comment

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

Would this work?

Suggested change
// Test for async generator function syntax support.
let AsyncGeneratorNewFunctionInstance;
try {
// Wrapping one in an new Function lets the `hermesc` binary file
// parse the Metro js bundle without SyntaxError, to generate the
// optimised Hermes bytecode bundle, when `gradlew` is called to
// assemble the release build APK for React Native prod Android apps.
// Delaying the error until runtime lets us customise lockdown behaviour.
AsyncGeneratorNewFunctionInstance = new FERAL_FUNCTION(
'return (async function* AsyncGeneratorFunctionInstance() {})',
)();
} catch (e) {
// @ts-expect-error ts(2339) Property 'jsEngine' does not exist on type 'Error'. However it exists on Hermes.
if (Error.prototype.jsEngine === 'hermes' && e.name === 'SyntaxError') {
// Swallows Hermes error `async generators are unsupported` at runtime.
// @ts-expect-error ts(2554) Expected 0 arguments, but got 1. It refers to the Web API Window object, but on Hermes we expect 1 argument.
// eslint-disable-next-line no-undef
print('SES: Skipping async generators, unsupported on Hermes');
// Note: `console` is not a JS built-in, so Hermes engine throws:
// Uncaught ReferenceError: Property 'console' doesn't exist
// See: https://github.com/facebook/hermes/issues/675
// However React Native provides a `console` implementation:
// https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/InitializeCore.js
} else {
throw e;
}
}
export const AsyncGeneratorFunctionInstance = AsyncGeneratorNewFunctionInstance;
const getAsyncGeneratorFunctionInstance = () => {
// Test for async generator function syntax support.
try {
// Wrapping one in an new Function lets the `hermesc` binary file
// parse the Metro js bundle without SyntaxError, to generate the
// optimised Hermes bytecode bundle, when `gradlew` is called to
// assemble the release build APK for React Native prod Android apps.
// Delaying the error until runtime lets us customise lockdown behaviour.
return new FERAL_FUNCTION(
'return (async function* AsyncGeneratorFunctionInstance() {})',
)();
} catch (e) {
// @ts-expect-error ts(2339) Property 'jsEngine' does not exist on type 'Error'. However it exists on Hermes.
if (Error.prototype.jsEngine === 'hermes' && e.name === 'SyntaxError') {
// Swallows Hermes error `async generators are unsupported` at runtime.
// @ts-expect-error ts(2554) Expected 0 arguments, but got 1. It refers to the Web API Window object, but on Hermes we expect 1 argument.
// eslint-disable-next-line no-undef
print('SES: Skipping async generators, unsupported on Hermes');
// Note: `console` is not a JS built-in, so Hermes engine throws:
// Uncaught ReferenceError: Property 'console' doesn't exist
// See: https://github.com/facebook/hermes/issues/675
// However React Native provides a `console` implementation:
// https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/InitializeCore.js
} else {
throw e;
}
}
export const AsyncGeneratorFunctionInstance = getAsyncGeneratorFunctionInstance();

packages/ses/package.json Outdated Show resolved Hide resolved
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.

6 participants