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

Workaround for override mistake #146

Merged
merged 11 commits into from
Aug 27, 2019

Conversation

kumavis
Copy link
Contributor

@kumavis kumavis commented Aug 26, 2019

Fixes #12

introduces repairDataProperties originally from tc39/proposal-ses and adapts it to meet current SES.

A few notes for review:

  • need to adjust/update copyright info? file has been modified
  • currently only handling dynamic presence of Promise (there's a test for it), need to expand?
  • want a more minimal toBeRepaired set? can do, #12 has suggestions
  • defineProperty function was based off a comment I found in realms-shim/src/common, but doesn't seem to be true in node v10
  • mutability aspect of this feature is not tested
  • compare to SalesForce Aura repairDataProperties
  • in this comment @erights seems to propose doing this inside of harden

review requested @erights @michaelfig @jfparadis @bmeck

} = Object;
const { ownKeys } = Reflect;

// Object.defineProperty is allowed to fail silently,
Copy link
Contributor Author

@kumavis kumavis Aug 26, 2019

Choose a reason for hiding this comment

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

This was based off a comment I found in realms-shim/src/common, but doesn't seem to be true in at least node v10 REPL

Copy link
Member

Choose a reason for hiding this comment

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

Another obscure corner of JavaScript. The only case where this might happen is on browsers, where there's a distinction between the reified so-called "WindowProxy" object and the unreified so-called "Window" object. The issue is that a browser frame, when it is navigated from url1 to url2, gets the completely new root realm associated with url2, including global variable bindings, but keeps the same global "window" object (which is actually a WindowProxy) because that is the same object the parent can hold onto to designate this frame no matter what it is navigated to.

The way this is implemented is that every root realm that occupied the frame has its own hidden so-called "Window" object, whose properties are aliased to that root realm's global variables. The so-called "WindowProxy" object has a current hidden "Window" object, that it forwards all operations to. Done naively, this leads to a contradiction:

Say wp is such a WindowProxy object. While it is at url1, and therefore forwarding to hidden window hw1, someone does an

Object.defineProperty(wp, 'foo', {
  value: 88,
  writable: false,
  configurable: false
});

Let's say that this call reports success, however defineProperty is supposed to report success. Then, presumably, hw1 actually has a non-writable, non-configurable foo data property with value 88. By the object invariants, these conditions represent a commitment that this property on this object must always be a non-writable, non-configurable foo data property with value 88. Fine so far.

Now some navigates the frame to url2, associated with hidden window hw2 that has no foo property, and whose root realm therefore also has no foo global variable. What should

Object.getOwnPropertyDescriptor(wp, 'foo')

return? What it actually does across all browsers, and as mandated by the html spec, is to forward that getOwnPropertyDescriptor request to its current hidden window object, hw2, which returns undefined. However, were wp to do so, after the previous defineProperty had indicated success, it would violate the previous commitment.

Therefore, the previous call to defineProperty must not report success. Heroic work by Mozilla on Firefox at first had that defineProperty call throw an error, which was the only way that defineProperty could report that the operation failed. This broke some old code in some old version of some library that did not actually care whether the defineProperty succeeded, failed, or did anything at all. But if the call to defineProperty threw, the library broke.

Therefore, for this one horrible special case, we allow defineProperty to report failure by returning false rather than throwing. SES does not care about breaking that old version of some obscure library. SES does care about the threat to integrity if code proceeds normally under the assumption that some operation succeeded when it didn't. Thus, SES should restore the less kludgy behavior of defineProperty, that could only report failure by throwing.

Copy link
Contributor Author

@kumavis kumavis Aug 27, 2019

Choose a reason for hiding this comment

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

For now should I continue to use Object.defineProperties as workaround? or something else?

Copy link
Member

Choose a reason for hiding this comment

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

Attn @ljharb Do you know the status of the possibility that the failure masking behavior of Object.defineProperty be extended to Object.defineProperties? In your opinion, should we switch to another technique to unmask the failure masking, to stay safe in the face of this possible change?

Copy link

Choose a reason for hiding this comment

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

Based on what I'm reading in this thread, the only reason Object.defineProperty was forced to not throw was for legacy web compat - if Object.defineProperties doesn't already have that problem, then I can't see why we'd ever want to make it not throw when it fails?

Copy link
Member

Choose a reason for hiding this comment

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

Good. I agree with the conclusion. I was asking because I recall some tc39 talk of extending the failure masking behavior to Object.defineProperties for consistency. But I haven't heard much. If you haven't run across this, it is likely inactive. Whew. Thanks.

src/bundle/mutable.js Outdated Show resolved Hide resolved
@kumavis kumavis force-pushed the override-mistake-workaround branch from f5b35f6 to 25d1369 Compare August 26, 2019 08:28
src/bundle/mutable.js Outdated Show resolved Hide resolved
src/bundle/mutable.js Outdated Show resolved Hide resolved
@kumavis
Copy link
Contributor Author

kumavis commented Aug 26, 2019

Is the toBeRepaired set inclusion a trade-off of perf vs extensibility? I assume so. I'll likely be pushing for wider inclusion as I find more things in use in the wild.

Copy link
Member

@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.

Glad to see this!!

Mostly, I'd like to wait a reasonable amount of time to see what answers we get to the questions below. But if necessary, I'd rather see this PR proceed as is, and then get updated in light of those answers later.

} = Object;
const { ownKeys } = Reflect;

// Object.defineProperty is allowed to fail silently,
Copy link
Member

Choose a reason for hiding this comment

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

Another obscure corner of JavaScript. The only case where this might happen is on browsers, where there's a distinction between the reified so-called "WindowProxy" object and the unreified so-called "Window" object. The issue is that a browser frame, when it is navigated from url1 to url2, gets the completely new root realm associated with url2, including global variable bindings, but keeps the same global "window" object (which is actually a WindowProxy) because that is the same object the parent can hold onto to designate this frame no matter what it is navigated to.

The way this is implemented is that every root realm that occupied the frame has its own hidden so-called "Window" object, whose properties are aliased to that root realm's global variables. The so-called "WindowProxy" object has a current hidden "Window" object, that it forwards all operations to. Done naively, this leads to a contradiction:

Say wp is such a WindowProxy object. While it is at url1, and therefore forwarding to hidden window hw1, someone does an

Object.defineProperty(wp, 'foo', {
  value: 88,
  writable: false,
  configurable: false
});

Let's say that this call reports success, however defineProperty is supposed to report success. Then, presumably, hw1 actually has a non-writable, non-configurable foo data property with value 88. By the object invariants, these conditions represent a commitment that this property on this object must always be a non-writable, non-configurable foo data property with value 88. Fine so far.

Now some navigates the frame to url2, associated with hidden window hw2 that has no foo property, and whose root realm therefore also has no foo global variable. What should

Object.getOwnPropertyDescriptor(wp, 'foo')

return? What it actually does across all browsers, and as mandated by the html spec, is to forward that getOwnPropertyDescriptor request to its current hidden window object, hw2, which returns undefined. However, were wp to do so, after the previous defineProperty had indicated success, it would violate the previous commitment.

Therefore, the previous call to defineProperty must not report success. Heroic work by Mozilla on Firefox at first had that defineProperty call throw an error, which was the only way that defineProperty could report that the operation failed. This broke some old code in some old version of some library that did not actually care whether the defineProperty succeeded, failed, or did anything at all. But if the call to defineProperty threw, the library broke.

Therefore, for this one horrible special case, we allow defineProperty to report failure by returning false rather than throwing. SES does not care about breaking that old version of some obscure library. SES does care about the threat to integrity if code proceeds normally under the assumption that some operation succeeded when it didn't. Thus, SES should restore the less kludgy behavior of defineProperty, that could only report failure by throwing.

src/bundle/mutable.js Outdated Show resolved Hide resolved

// Re-attach the data property on the object so
// it can be found by the deep-freeze traversal process.
getter.value = value;
Copy link
Member

Choose a reason for hiding this comment

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

IIRC, this was one of the things that Salesforce changed, though I do not remember to what.

Copy link
Contributor Author

@kumavis kumavis Aug 27, 2019

Choose a reason for hiding this comment

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

its present in forcedotcom/aura, don't have access to their latest work for comparison.

also present in node's version

Copy link
Member

Choose a reason for hiding this comment

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

getter.value = value; is fine.

src/bundle/mutable.js Outdated Show resolved Hide resolved
src/bundle/mutable.js Outdated Show resolved Hide resolved
src/bundle/mutable.js Outdated Show resolved Hide resolved
src/bundle/mutable.js Outdated Show resolved Hide resolved
@erights
Copy link
Member

erights commented Aug 26, 2019

Is the toBeRepaired set inclusion a trade-off of perf vs extensibility?

Yes. Salesforce found they only needed to repair 5 primordial prototypes for the problem to practically disappear. OTOH Node did all the primordial prototypes. I am not aware of anyone running into problems with non-prototypes.

I assume so. I'll likely be pushing for wider inclusion as I find more things in use in the wild.

Good. After the core set are done, we should indeed wait till we see actual problems before extending the repair set further.

@kumavis
Copy link
Contributor Author

kumavis commented Aug 27, 2019

Addressed most primary concerns and added some basic tests. While this can be improved, I think it is ready for merge and can be improved in subsequent PRs.

@kumavis
Copy link
Contributor Author

kumavis commented Aug 27, 2019

I didn't significantly reduce the toBeRepaired list, but I restrained my desire to make it more complete 😅

@kumavis
Copy link
Contributor Author

kumavis commented Aug 27, 2019

Ok, took a swing at reducing the repairDataProperties list. This is enough to get metamask working with sesify/lavamoat.

Copy link
Member

@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!

@kumavis
Copy link
Contributor Author

kumavis commented Aug 27, 2019

this will allow me to remove some entries from https://github.com/Agoric/SES/issues/136

Copy link
Member

@michaelfig michaelfig left a comment

Choose a reason for hiding this comment

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

LGTM as well.

@michaelfig michaelfig merged commit a1a52e1 into Agoric:master Aug 27, 2019
@kumavis kumavis deleted the override-mistake-workaround branch August 27, 2019 23:55
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.

Repair some builtin prototypes in order to tolerate the "override mistake"
5 participants