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): redefine SharedSymbol to bypass Hermes prototype bug on obj literal short-hand methods #2206

Merged
merged 7 commits into from
May 7, 2024

Conversation

leotm
Copy link
Contributor

@leotm leotm commented Apr 10, 2024

closes: #XXXX
refs: #1891

Description

Fix SES compat with Hermes when calling addIntrinsics(tameSymbolConstructor());

Follow-up to

Security Considerations

Scaling Considerations

Documentation Considerations

Testing Considerations

Compatibility Considerations

Upgrade Considerations

  • Includes *BREAKING*: in the commit message with migration instructions for any breaking change.
  • Updates NEWS.md for user-facing changes.

@leotm leotm marked this pull request as ready for review April 10, 2024 17:00
Copy link
Contributor

@mhofman mhofman left a comment

Choose a reason for hiding this comment

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

I'm confused as to what the problem is this is trying to solve?

How is the just created SharedSymbol function born with own non-configurable properties, conflicting with the original Symbol properties?

@@ -50,14 +50,20 @@ export const tameSymbolConstructor = () => {
},
});

const sharedSymbolDescs = getOwnPropertyDescriptors(SharedSymbol);
const originalDescsEntries = entries(
getOwnPropertyDescriptors(OriginalSymbol),
);
const descs = fromEntries(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

consider adding a comment like
// Hermes has many non-configurable Symbol properties. We should support such platforms.

Copy link
Contributor

Choose a reason for hiding this comment

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

The JS spec says that well known symbols are non-configurable properties of the Symbol global. How is hermes different exactly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh right i see your point, the differences listed are

Supported

  • Symbols (including most well-known Symbols)

In Progress

  • Symbol.prototype.description (it's not fully spec-conformant yet. Symbol().description should be undefined but it's currently '').

Excluded From Support

  • Symbol.species and its interactions with JS library functions
  • Symbol.unscopables (Hermes does not support with)

source: https://github.com/facebook/hermes/blob/main/doc/Features.md#excluded-from-support

and what i've discovered from logging originalDescsEntries and Hermes Debugger in #2206 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

looking further at the source

...
/// Represents a JS Symbol (es6).  Movable, not copyable.
/// TODO T40778724: this is a limited implementation sufficient for
/// the debugger not to crash when a Symbol is a property in an Object
/// or element in an array.  Complete support for creating will come
/// later.
class JSI_EXPORT Symbol : public Pointer {
...

https://github.com/facebook/hermes/blob/main/API/jsi/jsi/jsi.h#L515-L540

https://github.com/facebook/hermes/blob/main/include/hermes/VM/PrimitiveBox.h#L310-L347

@leotm
Copy link
Contributor Author

leotm commented Apr 10, 2024

I'm confused as to what the problem is this is trying to solve?

How is the just created SharedSymbol function born with own non-configurable properties, conflicting with the original Symbol properties?

it's solving SES compatibility for React Native running on Hermes ^ when calling addIntrinsics(tameSymbolConstructor()); in the shim

otherwise the output on Android emulators is property is not configurable, no stack

after calling defineProperties(SharedSymbol, descs);

the original originalDescsEntries on Hermes logged out are

 LOG  length {"configurable": true, "enumerable": false, "value": 0, "writable": false}
 LOG  name {"configurable": true, "enumerable": false, "value": "Symbol", "writable": false}
 LOG  for {"configurable": true, "enumerable": false, "value": [Function for], "writable": true}
 LOG  keyFor {"configurable": true, "enumerable": false, "value": [Function keyFor], "writable": true}
 LOG  hasInstance {"configurable": false, "enumerable": false, "value": Symbol(Symbol.hasInstance), "writable": false}
 LOG  iterator {"configurable": false, "enumerable": false, "value": Symbol(Symbol.iterator), "writable": false}
 LOG  isConcatSpreadable {"configurable": false, "enumerable": false, "value": Symbol(Symbol.isConcatSpreadable), "writable": false}
 LOG  toPrimitive {"configurable": false, "enumerable": false, "value": Symbol(Symbol.toPrimitive), "writable": false}
 LOG  toStringTag {"configurable": false, "enumerable": false, "value": Symbol(Symbol.toStringTag), "writable": false}
 LOG  match {"configurable": false, "enumerable": false, "value": Symbol(Symbol.match), "writable": false}
 LOG  matchAll {"configurable": false, "enumerable": false, "value": Symbol(Symbol.matchAll), "writable": false}
 LOG  search {"configurable": false, "enumerable": false, "value": Symbol(Symbol.search), "writable": false}
 LOG  replace {"configurable": false, "enumerable": false, "value": Symbol(Symbol.replace), "writable": false}
 LOG  split {"configurable": false, "enumerable": false, "value": Symbol(Symbol.split), "writable": false}

so the conflict seems to be attempting to define 11 non-configurable properties to configurable

@mhofman
Copy link
Contributor

mhofman commented Apr 10, 2024

otherwise the output on Android emulators is property is not configurable, no stack

after calling defineProperties(SharedSymbol, descs);

the original originalDescsEntries on Hermes logged out are

None of this tells me which non-configurable property exists on SharedSymbol that cannot be redefined. What is the actual trigger?

To be more clear, what are the own property descriptors of SharedSymbol right after construction?

@leotm
Copy link
Contributor Author

leotm commented Apr 10, 2024

otherwise the output on Android emulators is property is not configurable, no stack

after calling defineProperties(SharedSymbol, descs);

the original originalDescsEntries on Hermes logged out are

None of this tells me which non-configurable property exists on SharedSymbol that cannot be redefined. What is the actual trigger?

To be more clear, what are the own property descriptors of SharedSymbol right after construction?

getOwnPropertyDescriptors(SharedSymbol) appears to conform to the JS spec

{
   "arguments":{
      "configurable":false,
      "enumerable":false,
      "get":[
         "Function anonymous"
      ],
      "set":[
         "Function anonymous"
      ]
   },
   "caller":{
      "configurable":false,
      "enumerable":false,
      "get":[
         "Function anonymous"
      ],
      "set":[
         "Function anonymous"
      ]
   },
   "length":{
      "configurable":true,
      "enumerable":false,
      "value":1,
      "writable":false
   },
   "name":{
      "configurable":true,
      "enumerable":false,
      "value":"Symbol",
      "writable":false
   },
   "prototype":{
      "configurable":false,
      "enumerable":false,
      "value":{},
      "writable":true
   }
}

logging

// ...
  const descs=  fromEntries(
    arrayMap(originalDescsEntries, ([name, desc])=>  {
      console.log(name, desc);
      console.log(name, sharedSymbolDescs[name]);
// ...

we get

length {"configurable": true, "enumerable": false, "value": 0, "writable": false}
length {"configurable": true, "enumerable": false, "value": 1, "writable": false}

name {"configurable": true, "enumerable": false, "value": "Symbol", "writable": false}
name {"configurable": true, "enumerable": false, "value": "Symbol", "writable": false}

prototype {"configurable": false, "enumerable": false, "value": {}, "writable": true}

for {"configurable": true, "enumerable": false, "value": [Function for], "writable": true}
for undefined

keyFor {"configurable": true, "enumerable": false, "value": [Function keyFor], "writable": true}
keyFor undefined

hasInstance {"configurable": false, "enumerable": false, "value": Symbol(Symbol.hasInstance), "writable": false}
hasInstance undefined

iterator {"configurable": false, "enumerable": false, "value": Symbol(Symbol.iterator), "writable": false}
iterator undefined

isConcatSpreadable {"configurable": false, "enumerable": false, "value": Symbol(Symbol.isConcatSpreadable), "writable": false}
isConcatSpreadable undefined

toPrimitive {"configurable": false, "enumerable": false, "value": Symbol(Symbol.toPrimitive), "writable": false}
toPrimitive undefined

toStringTag {"configurable": false, "enumerable": false, "value": Symbol(Symbol.toStringTag), "writable": false}
toStringTag undefined

match {"configurable": false, "enumerable": false, "value": Symbol(Symbol.match), "writable": false}
match undefined

matchAll {"configurable": false, "enumerable": false, "value": Symbol(Symbol.matchAll), "writable": false}
matchAll undefined

search {"configurable": false, "enumerable": false, "value": Symbol(Symbol.search), "writable": false}
search undefined

replace {"configurable": false, "enumerable": false, "value": Symbol(Symbol.replace), "writable": false}
replace undefined

split {"configurable": false, "enumerable": false, "value": Symbol(Symbol.split), "writable": false}
split undefined

the issue appears to be the prototype

replacing the condition
if (sharedSymbolDescs[name] && sharedSymbolDescs[name].configurable === false) {
with
if (name === 'prototype') {
also works

Comment on lines 60 to 61
sharedSymbolDescs[name] &&
sharedSymbolDescs[name].configurable === false
Copy link
Contributor Author

Choose a reason for hiding this comment

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

nb: condition name === 'prototype' is also sufficient on Hermes

Copy link
Contributor

@mhofman mhofman Apr 10, 2024

Choose a reason for hiding this comment

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

Alright it looks like the problem is a spec non compliance from Hermes. Short-hand methods are not supposed to have a .prototype property. This is actually a fairly big problem for Symbol taming, as it would break Object(Symbol()) instanceof Symbol

Copy link
Contributor

Choose a reason for hiding this comment

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

@erights any ideas on how to best handle this non-compliance? It will likely bite us in other places.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've never run Hermes before. I'm on a Mac. What's the best way for me to reproduce this? Possible to reproduce under a debugger, as I saw in your screenshots?

Copy link
Contributor

Choose a reason for hiding this comment

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

Short-hand methods are not supposed to have a .prototype property.

Current hypothesis. The correct fix is likely to make SharedSymbol a function function rather than a concise method. This probably would have been somewhat better all along anyway, since the original Symbol does, in most ways, act more like a function function than a concise method.

Copy link
Contributor Author

@leotm leotm Apr 12, 2024

Choose a reason for hiding this comment

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

since the https://github.com/facebook/hermes/releases/tag/v0.12.0 CLI is shy of couple yrs old
i requested to help get an updated version (being discussed with team)

so i tested the commit fix by building and running Hermes, remembering to git checkout the static_h branch after cloning (before building) on Mac (debug build was fine), which lgtm (expected/conformed behaviour)

nb: the CLI version displays 0.12.0, hopefully latest commit will be shown too for clarity soon

Copy link
Contributor Author

@leotm leotm Apr 12, 2024

Choose a reason for hiding this comment

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

i'll get back to you @erights with repro's on RN 71+ and running them with Flipper and Hermes Debugger on Mac
debugging works well for me on RN 71, but not above versions (if the same for you i'll file an upstream issue)

Copy link
Contributor

Choose a reason for hiding this comment

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

will proceed with changes
perhaps with a comment like this?

The changes I suggested are actually an improvement over what's currently there, and should be kept regardless of Hermes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated ^ and tested working on RN 71 (containing Hermes bug) on Android
with SES shim built from current/suggested changes
(dist/ses.cjs generated from yarn clean && yarn build)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've never run Hermes before. I'm on a Mac. What's the best way for me to reproduce this? Possible to reproduce under a debugger, as I saw in your screenshots?

i'll get back to you @erights with repro's on RN 71+ and running them with Flipper and Hermes Debugger on Mac debugging works well for me on RN 71, but not above versions (if the same for you i'll file an upstream issue)

@erights here's the repro with @mhofman's fix applied in a commit
https://github.com/leotm/RN07117SES

and i've added a ReadMe instructions to run it on Mac, debug with Flipper and repro the issues tracked in

Copy link
Contributor

@mhofman mhofman left a comment

Choose a reason for hiding this comment

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

We should bypass the Hermes bug, and increase spec fidelity by only doing the following:

  const SharedSymbol = functionBind(Symbol, undefined);

Where in common.js:

export const functionBind = uncurryThis(bind);

The above will maintain the behavior of the original Symbol "constructor", including its native throwing behavior. It works because the receiver is not used by the [[Call]] of the primordial %Symbol%.

The name property will be "fixed" by the later copy of own properties from OriginalSymbol, and the toString() will be censored like for the original. Special casing of own properties should no longer be necessary for Hermes.

@leotm leotm changed the title feat(ses): tolerate non-configurable Symbol props feat(ses): tolerate prototype on object literal short-hand methods Apr 12, 2024
@leotm leotm changed the title feat(ses): tolerate prototype on object literal short-hand methods feat(ses): redefine SharedSymbol to bypass Hermes prototype bug on obj literal short-hand methods Apr 15, 2024
@leotm leotm requested review from mhofman and erights April 15, 2024 10:47
leotm added a commit to leotm/RN07117SES that referenced this pull request Apr 15, 2024
Copy link
Contributor

@mhofman mhofman left a comment

Choose a reason for hiding this comment

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

Looks good, but since I proposed the change, I'd like @erights to approve.

Also suggestion on the comment, since we don't currently use bind anywhere for this purpose.

packages/ses/src/tame-symbol-constructor.js Outdated Show resolved Hide resolved
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 also looks good to me and I defer to @erights for the final stamp.

@erights
Copy link
Contributor

erights commented Apr 16, 2024

(rerunning failed CI jobs)

@leotm leotm requested a review from kriskowal April 16, 2024 12:03
@leotm
Copy link
Contributor Author

leotm commented Apr 16, 2024

passing ^ looks good to approve then merge

Copy link
Contributor

@erights erights left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

@naugtur naugtur force-pushed the tolerate-non-configurable-symbol-props branch from 587119c to f74eee9 Compare April 30, 2024 09:39
@naugtur naugtur enabled auto-merge (rebase) April 30, 2024 09:40
@naugtur naugtur disabled auto-merge April 30, 2024 09:40
@naugtur naugtur enabled auto-merge (squash) April 30, 2024 09:42
@leotm
Copy link
Contributor Author

leotm commented May 7, 2024

sry @kriskowal looks like i added an extra mandatory approval required by you ^
i don't have merge auth so Zb's enabled auto-merge

@mhofman mhofman disabled auto-merge May 7, 2024 18:45
@mhofman
Copy link
Contributor

mhofman commented May 7, 2024

I am honestly unsure what the unmet merge requirement is. Will update the branch just in case

@mhofman mhofman enabled auto-merge (squash) May 7, 2024 18:47
@mhofman mhofman merged commit 59bb9ba into endojs:master May 7, 2024
17 checks passed
leotm added a commit to MetaMask/metamask-mobile that referenced this pull request May 15, 2024
leotm added a commit to MetaMask/metamask-mobile that referenced this pull request May 29, 2024
## **Description**

- bump SES to
[v1.5.0](https://github.com/endojs/endo/releases/tag/ses%401.5.0)
- includes Hermes fix in preparation for Hermes SES shim

## **Related issues**

- includes endojs/endo#2206
- includes #8786

## **Manual testing steps**

Run: `curl -O https://npmfs.com/download/ses/1.5.0/dist/ses.cjs`
Result: no change/diff

## **Screenshots/Recordings**

### **Before**

### **After**

## **Pre-merge author checklist**

- [x] I’ve followed [MetaMask Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

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
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants