-
Notifications
You must be signed in to change notification settings - Fork 80
fix(pass-style): Relax passable error criteria in hardenTaming unsafe mode. #2990
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
Conversation
packages/pass-style/src/error.js
Outdated
| // Error.prototype.stack is own accessor in V8, but nowhere else to our | ||
| // knowledge (2025). | ||
| // We relax validation for "stack" only when harden.isFake. | ||
| if (!(harden.isFake && propName === 'stack')) { |
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 fail to see how unsafe harden is related to error stacks. I think what we possibly need to do is allow known "platform own stack accessors" in pass-style. It would be nice to only allow these if there was no lockdown, or if lockdown didn't tame errors.
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 think more next week about sensing the effects of error taming variants. I’ll probe your mind next week about “platform own stack accessors” because my understanding is that expectations about the identity or uniqueness of error own get and set may be evolving in a way that will make it harder to sense whether they are the platform’s intrinsic get and set. I fear we may truly need to leave a pinhole for stack in pass-style, generally, and simply be careful to discriminate them with isError and time we destructure all properties of an object.
8f28d6a to
cd71d7d
Compare
87d00cd to
342ac49
Compare
|
We’ve resolved to update this change such that |
df3ba90 to
2b0119f
Compare
cd71d7d to
afa9989
Compare
afa9989 to
0fd4738
Compare
|
|
||
| const { getOwnPropertyDescriptors, getPrototypeOf } = Object; | ||
|
|
||
| // @ts-expect-error isFake is a secret. |
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.
WAT? If it is a secret, it is a very poorly kept secret!
(Please avoid ever seeming to make a security claim that we're not actually making.)
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 is a pattern copied from elsewhere. It is not a secret in this sense. It is a secret in the sense that we do not make its existence evident in the type, in order to avoid burdening common users with undue knowledge of its existence. Recommendation welcome.
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.
How about, “Existence of harden.isFake is not communicated by the types.”?
| makeExo, | ||
| } from '../index.js'; | ||
|
|
||
| // @ts-expect-error isFake is a secret property. |
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.
again
| return obj; | ||
| }, | ||
| tortuous(hardA, softB, hardC, optHardD, optSoftE = {}) { | ||
| // Recall that isFrozen lies with hardenTaming: unsafe |
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.
Aren't you fixing that? We should at least vehemently deprecate the existing unsafe-fast.
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.
Yes, I expect this to change to accommodate the new mode. We may need to expose the real isFrozen somewhere to make this test work in every mode.
| // We do not trust isFrozen because lockdown with unsafe hardenTaming replaces | ||
| // isFrozen with a version that is in cahoots with fake harden. |
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 is the unsafe-fast case that we're at least deprecating and hopefully removing soon, yes?
packages/pass-style/src/error.js
Outdated
| } | ||
|
|
||
| const er1StackDesc = getOwnPropertyDescriptor(Error('er1'), 'stack'); | ||
| const er2StackDesc = getOwnPropertyDescriptor(TypeError('er2'), 'stack'); |
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.
Just curious: Do you have a particular case in mind that you're defending against with the "redundant" check?
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 don’t. This is copied from SES.
| if ( | ||
| er1StackDesc === undefined || | ||
| er2StackDesc === undefined || | ||
| er1StackDesc.get === undefined | ||
| ) { |
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 fact that you wrote || means I'm confused what the purpose of the "redundant" check is. I expected something more like
| if ( | |
| er1StackDesc === undefined || | |
| er2StackDesc === undefined || | |
| er1StackDesc.get === undefined | |
| ) { | |
| if ( | |
| (er1StackDesc === undefined && | |
| er2StackDesc === undefined) || | |
| er1StackDesc.get === undefined | |
| ) { |
packages/pass-style/src/error.js
Outdated
| return undefined; | ||
| } | ||
|
|
||
| // We should only encounter this case on v8 because of its problematic |
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.
And the JS engine MetaMask is using?
packages/pass-style/src/error.js
Outdated
| // In the v8 case as we understand it, all errors have an own stack | ||
| // accessor property, but within the same realm, all these accessor | ||
| // properties have the same getter and have the same setter. | ||
| // This is therefore the case that we repair. |
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.
Should also mention somewhere that the upcoming Error.captureStackTrace proposal may result in more own stack accessors, on more platforms, that may or may not use the same getter and setter. If we don't think those are a problem, we should explain somewhere why not. But not necessarily here.
packages/pass-style/src/error.js
Outdated
| // properties have the same getter and have the same setter. | ||
| // This is therefore the case that we repair. | ||
| typeof er1StackDesc.get !== 'function' || | ||
| er1StackDesc.get !== er2StackDesc.get || |
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.
Aha! Now I understand
packages/pass-style/src/error.js
Outdated
| // Otherwise, we have own stack accessor properties that are outside | ||
| // our expectations, that therefore need to be understood better | ||
| // before we know how to repair them. |
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.
Should this comment be moved to immediately above the above throw?
erights
left a 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.
I did not review most of the tests. But I reviewed error.js carefully which LGTM.
Thanks!
| if (repairError !== undefined) { | ||
| repairError(candidate); | ||
| } |
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.
Comment here would be good about why we're ok with this special case side effect in what should be a pure query function.
0fd4738 to
9cbb853
Compare
mhofman
left a 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.
Looks good, but would love to see an explicit test for the can't patch error case (either by harden or by passStyleOf).
Also wondering about how lazy the makeRepairError should be.
| }, | ||
| rawOut(obj) { | ||
| t.is(Object.isFrozen(obj), true); | ||
| t.true(hardenIsFake || Object.isFrozen(obj)); |
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.
Curious why this is the only Object.isFrozen case where we have to condition on hardenIsFake
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 count 9 total. 5 affirmative. 4 negative. Perhaps you mean something else?
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 see some Object.freeze calls above in the same test that don't get the same treatment, and arguably I didn't look closely at what the test does, I just saw those lines modified, but not get an extra hardenIsFake condition, and was just wondering why.
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.
Your intuition was onto something, anyway. I went through each of these cases again and found 4 that did not require the relaxation for harden.isFake, left over from when I relaxed these tests to accommodate the unsafe mode of @endo/harden where isFrozen is not rigged. Will have to revisit if we go so far.
| ); | ||
| } | ||
| if (repairError !== undefined) { | ||
| // This point is unreachable unless the candidate is mutable and the |
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.
How do we know the object is mutable here? I mean it's possible that fake harden didn't freeze but that someone actually explicitly froze it.
9cbb853 to
392e030
Compare
kriskowal
left a 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.
Pushed a commit to explicitly test frozen-not-hardened errors are unfixable on pathological-V8 (but fine on prior V8 versions we still test).
| }, | ||
| rawOut(obj) { | ||
| t.is(Object.isFrozen(obj), true); | ||
| t.true(hardenIsFake || Object.isFrozen(obj)); |
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 count 9 total. 5 affirmative. 4 negative. Perhaps you mean something else?
946b89b to
5221365
Compare
|
I believe I’ve addressed all feedback. Commits added, none changed, for incremental review. |
5221365 to
de4795c
Compare
| const { ownKeys } = Reflect; | ||
| const { isFrozen, getOwnPropertyDescriptors, values } = Object; | ||
|
|
||
| const repairError = makeRepairError(); |
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.
So this still causes makeRepairError to still run at import of @endo/pass-style, but from what I gather, for the use of the toPassableError export, which unlike passStyleOf, is always exported anew.
I think the pass-style package has grown into a weird monster of instantiable and instantiated exports, both exposed on the same entry-point. Because of this I'm tempted to ask to revert my previous request for a lazy repairError, as it's pointless with the current structure of this package. Sorry about the back and forth, I forgot how unwieldy this whole thing is.
71c7d0f to
6feeca5
Compare
6feeca5 to
95c8156
Compare
mhofman
left a 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.
LGTM
Feel free to further revert the repairError change so that it's fully contained within error.js and no changes are required of passStyleOf.js.
95c8156 to
8c55a42
Compare
8c55a42 to
39f8386
Compare
| const mutable = {}; | ||
| t.is(greeter.sayHello(mutable), 'hello', `passableGreeter can sayHello`); | ||
| t.is(Object.isFrozen(mutable), true, `mutable is frozen`); | ||
| t.true(Object.isFrozen(mutable), `mutable is frozen`); |
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 case relies on harden and isFrozen to be in cahoots. This passes with unsafe harden taming because isFrozen is lying. The safe harden taming passes because harden, called as a side-effect of passing mutable to an Exo, is effective. This will continue to work without modification when we introduce surface hardening. If we obviate unsafe harden taming by making an unsafe mode of @endo/harden which doesn’t collude with isFrozen, this test will need to be adjusted to take that into account.
| }, | ||
| rawIn(obj) { | ||
| t.is(Object.isFrozen(obj), false); | ||
| t.false(Object.isFrozen(obj)); |
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 test passes because the M.raw guard is bypassing the harden otherwise implied. It’s always passed an unfrozen/unhardened object in the tests.
| }, | ||
| rawOut(obj) { | ||
| t.is(Object.isFrozen(obj), true); | ||
| t.true(hardenIsFake || Object.isFrozen(obj)); |
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.
Your intuition was onto something, anyway. I went through each of these cases again and found 4 that did not require the relaxation for harden.isFake, left over from when I relaxed these tests to accommodate the unsafe mode of @endo/harden where isFrozen is not rigged. Will have to revisit if we go so far.
| t.false(Object.isFrozen(obj)); | ||
| return obj; | ||
| }, | ||
| rawOut(obj) { |
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.
rawOut has an M.any() guard, so the argument is implicitly hardened, albeit fake hardened. The relaxation is unnecessary in this case and I will restore the case.
| t.is(Object.isFrozen(optSoftE), false); | ||
| t.true(Object.isFrozen(hardC)); | ||
| t.true(Object.isFrozen(optHardD)); | ||
| t.false(!hardenIsFake && Object.isFrozen(optSoftE)); |
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 case requires a relaxation because isFrozen is misreporting a soft object has having been frozen. It’s either a soft object that was passed through a raw optional arg, or defaulted to a soft object.
Refs: #2990 ## Description This change synchronizes the error repair mechanism in SES with changes made in #2990 to pass-style to gratutiously improve the resilience of that mechanism in the face of scripts that ran before SES. ### Security Considerations This reduces SES vulnerability to corruption from code that runs before SES. Replacing the TypeError constructor cannot confuse SES with regard to whether the platform produces type errors with own stack properties. ### Scaling Considerations None. ### Documentation Considerations None. ### Testing Considerations We cover versions of Node.js with and without own stack properties in CI and existing tests should be sufficient. ### Compatibility Considerations Programs that previously deceived SES may now fail when initializing SES. We consider such programs compromised and that incompatibility is a feature. ### Upgrade Considerations None.
This change increases test coverage over lockdown with hardenTaming unsafe. The increase in test coverage revealed the need to relax certain cases, but moreof adjust
passStyleOfto repair unfrozen V8 error objects inunsafehardenTamingmode.