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

Handle proxy object prototypes more correctly #2406

Merged
merged 1 commit into from
Jul 22, 2024

Conversation

jasnell
Copy link
Member

@jasnell jasnell commented Jul 17, 2024

Refs: #2291

auto ret = JsValue(check(fn->Call(js.v8Context(), jsHandler.inner, 1, args)));
JSG_REQUIRE(ret.isObject() || ret.isNull(), TypeError,
"Proxy getPrototypeOf trap did not return an object or null");
// TODO(maybe): V8 performs additional checks on the returned value to
Copy link
Member

Choose a reason for hiding this comment

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

Can you give an example (JavaScript code) where this makes a difference? I don't really understand from the description.

Copy link
Member Author

@jasnell jasnell Jul 17, 2024

Choose a reason for hiding this comment

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

class Foo {}
Object.freeze(Foo)
Object.isFrozen(Foo);  // true... Foo is non-extensible
const P = new Proxy(Foo, {getPrototypeOf() { return Array }}) // try changing the prototype
Object.getPrototypeOf(P); // throws!
TypeError: 'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype
    at Function.getPrototypeOf (<anonymous>)

We are not performing the additional check so our code would return Array rather than throwing in this case

Copy link
Member Author

Choose a reason for hiding this comment

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

The motivation here, as far as I understand it, is that if the target is sealed or frozen (aka non-extensible) then the Proxy trap is not really supposed to be able to change the prototype that it returns as that could change the behavior of the proxied object in a way that violates the seal. For our purposes, however, it's not entirely certain we need to worry about that check, and implementing the check at the c++ layer would require patching v8, which I'm trying to avoid here. If we need it, then so be it... In that case I'll just patch v8 to expose v8's JSProxy::GetPrototypeOf implementation as a new method on v8::Proxy

Copy link
Member

Choose a reason for hiding this comment

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

I dunno, we might need it. Keep in mind we follow prototype chains manually in JSRPC, and then invoke the method we find. Will we potentially be violating the seal when we do that?

I don't really understand this explanation though. Isn't your code strictly doing things that someone could do in pure JS? It shouldn't be possible to violate a seal or freeze from pure JS, should it? Again, example code would really help in understanding this.

If we can't be really sure we understand this then I would say we'd better go the route of patching V8 with a GetPrototypeJS() that actually runs V8's logic.

Copy link
Member Author

Choose a reason for hiding this comment

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

Isn't your code strictly doing things that someone could do in pure JS? It shouldn't be possible to violate a seal or freeze from pure JS, should it?

That's the point. Reporting a different prototype when the object is sealed violates the seal and is why v8 has this additional check. The example I showed is the error as it exists in browsers and node.js. What I don't have a good sense of is just how critical this check is. It's certainly an acceptable argument that If We Don't Know, err on the side of caution. It'll likely have to wait until I'm back from PTO tho unless you'd like to take a stab at the v8 patch

Copy link
Member

Choose a reason for hiding this comment

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

Wait sorry, when I looked before I missed your first comment somehow and only saw the second.

Copy link
Member

Choose a reason for hiding this comment

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

OK, looking at your code example helps. But it confuses me because in my opinion the V8 behavior seems wrong to me.

In my mind a Proxy is just a convenient way to define a wrapper around some other object that intercepts and redirects some or all functionality. The wrapper is a different object from the target. The reason a Proxy has a target at all is mostly convenience -- so that functionality you don't explicitly override can fall back to operating on the target.

In that model, it makes no sense to me that because the target happens to be frozen, the proxy would be restricted in what it's allowed to override.

But I am probably not understanding something about proxies.

Still not totally sure what to do here though. I worry that if we implement this "wrong" it could come back to bite us later and will be hard to fix.

Copy link
Member Author

Choose a reason for hiding this comment

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

Still not totally sure what to do here though. I worry that if we implement this "wrong" it could come back to bite us later and will be hard to fix.

yep, I share this concern. I'm just not sure how likely the issue is to come up in a practical sense. I think I will just go ahead and do the v8 patch but it will have to be when I'm back.

Copy link
Member

Choose a reason for hiding this comment

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

Put another way, I don't understand why creating a Proxy that overrides the prototype is being treated as equivalent to re-assigning the prototype on the underlying object. The Proxy is not the same object, and it should be allowed to have whatever prototype it wants IMO.

Maybe we just go ahead with this.

@kentonv
Copy link
Member

kentonv commented Jul 17, 2024

Can you link to V8's code for this, for comparison?

@jasnell
Copy link
Member Author

jasnell commented Jul 17, 2024

@jasnell jasnell force-pushed the jsnell/handle-proxy-prototypes branch from e35ef7e to 3bb9dbe Compare July 17, 2024 16:50
@jasnell jasnell requested a review from kentonv July 17, 2024 17:36
if (trap.isUndefined()) {
JSG_REQUIRE(target->IsObject(), TypeError, "Proxy target is not an object");
// Run this through getPrototype to handle the case where the target is also a proxy.
return JsObject(target.As<v8::Object>()).getPrototype(js);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it possible to construct a cyclic object which could cause this to infinitely recurse?

Copy link
Member Author

Choose a reason for hiding this comment

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

It might be but I would think that our existing cpu limit mitigations would handle that case. That said, I can't think of a way off hand to create a cyclic proxy on the target.

Copy link
Member

@kentonv kentonv 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 going to go ahead and approve this.

I'm still a little worried about the difference in behavior when the target is frozen, but I actually question whether the V8 behavior is even correct. In any case it doesn't seem like it creates any kind of problem that we can't solve with a compat flag later.

@jasnell
Copy link
Member Author

jasnell commented Jul 19, 2024

How about this... for now, we can emit a periodic warning log if the proxy is returning a different prototype. This will at least give us a sense of whether anyone is potentially hitting this. I agree we can fix this up with a compat flag later if absolutely necessary

@jasnell jasnell force-pushed the jsnell/handle-proxy-prototypes branch 2 times, most recently from 010511c to b2041c2 Compare July 19, 2024 19:33
src/workerd/jsg/jsvalue.c++ Outdated Show resolved Hide resolved
@jasnell jasnell force-pushed the jsnell/handle-proxy-prototypes branch from b2041c2 to a1026f6 Compare July 21, 2024 18:21
@jasnell jasnell requested a review from kentonv July 21, 2024 18:22
@kentonv kentonv merged commit e22d66e into main Jul 22, 2024
9 checks passed
@kentonv kentonv deleted the jsnell/handle-proxy-prototypes branch July 22, 2024 16:22
@kentonv
Copy link
Member

kentonv commented Jul 22, 2024

(Clicked merge since James is on vacation.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants