-
Notifications
You must be signed in to change notification settings - Fork 74
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
feat(ses): redefine SharedSymbol to bypass Hermes prototype bug on obj literal short-hand methods #2206
Conversation
There was a problem hiding this 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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 beundefined
but it's currently''
).
Excluded From Support
Symbol.species
and its interactions with JS library functionsSymbol.unscopables
(Hermes does not supportwith
)
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)
There was a problem hiding this comment.
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
it's solving SES compatibility for React Native running on Hermes ^ when calling otherwise the output on Android emulators is after calling the original 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 |
None of this tells me which non-configurable property exists on To be more clear, what are the own property descriptors of |
{
"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 |
sharedSymbolDescs[name] && | ||
sharedSymbolDescs[name].configurable === false |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
)
There was a problem hiding this comment.
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
There was a problem hiding this 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.
There was a problem hiding this 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.
There was a problem hiding this 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.
(rerunning failed CI jobs) |
passing ^ looks good to approve then merge |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks!
This reverts commit 22b2c17.
Co-authored-by: Mathieu Hofman <[email protected]>
587119c
to
f74eee9
Compare
sry @kriskowal looks like i added an extra mandatory approval required by you ^ |
I am honestly unsure what the unmet merge requirement is. Will update the branch just in case |
- includes endojs/endo#2206 - includes #8786 `curl -O https://npmfs.com/download/ses/1.5.0/dist/ses.cjs`
## **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 <[email protected]>
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
*BREAKING*:
in the commit message with migration instructions for any breaking change.NEWS.md
for user-facing changes.