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

vm: regression for "in" operator on global between 22.4.1 and 22.5.0; breaks jsdom #54436

Closed
domenic opened this issue Aug 18, 2024 · 10 comments
Closed
Labels
confirmed-bug Issues with confirmed bugs. v22.x v22.x Issues that can be reproduced on v22.x or PRs targeting the v22.x-staging branch. vm Issues and PRs related to the vm subsystem.

Comments

@domenic
Copy link
Contributor

domenic commented Aug 18, 2024

Version

v22.5.0

Platform

Microsoft Windows NT 10.0.22631.0 x64

Subsystem

vm

What steps will reproduce the bug?

"use strict";

const vm = require("vm");

class EventTarget {
  addEventListener() {}
}

const windowConstructor = function () {};
Object.setPrototypeOf(windowConstructor, EventTarget);
const windowPrototype = Object.create(EventTarget.prototype);

function Window() {
  vm.createContext(this);
  this._globalProxy = vm.runInContext("this", this);

  Object.setPrototypeOf(this, windowPrototype);

  const window = this;
  Object.defineProperty(this, "window", {
    get() {
      return window._globalProxy;
    },
    enumerable: true,
    configurable: true
  });
}

const window = new Window();

console.log(vm.runInContext(`"addEventListener" in window`, window));

How often does it reproduce? Is there a required condition?

Every time

What is the expected behavior? Why is that the expected behavior?

Outputs true. (That occurs on v22.4.1.)

What do you see instead?

Outputs false

Additional information

No response

@RedYetiDev
Copy link
Member

Can you strip your example to just the basics? Something very small that will reproduce this?

@RedYetiDev RedYetiDev added vm Issues and PRs related to the vm subsystem. v22.x v22.x Issues that can be reproduced on v22.x or PRs targeting the v22.x-staging branch. labels Aug 18, 2024
@domenic
Copy link
Contributor Author

domenic commented Aug 19, 2024

I've already done so. It's 30 lines with no external dependencies. The issue is specifically about class hierarchies, inherited methods, and subclassing so it's not like it can get any smaller. Maybe I could remove enumerable: true, configurable: true to save two lines if that'd help you out. Or rename the variables from descriptive names like Window and EventTarget into generic ones like Subclass and Superclass.

@RedYetiDev
Copy link
Member

RedYetiDev commented Aug 19, 2024

I've already done so. It's 30 lines with no external dependencies. The issue is specifically about class hierarchies, inherited methods, and subclassing so it's not like it can get any smaller. Maybe I could remove enumerable: true, configurable: true to save two lines if that'd help you out. Or rename the variables from descriptive names like Window and EventTarget into generic ones like Subclass and Superclass.

You are correct, I was trying to oversimplify the code, but that's not needed. Sorry for the mistake. When I get a chance I'll check this for repro-exists.

@RedYetiDev
Copy link
Member

FWIW the only vm subsystem PR for that version is #53517

@marco-ippolito
Copy link
Member

Maybe c0962dc @legendecas ?

@legendecas legendecas added the confirmed-bug Issues with confirmed bugs. label Aug 19, 2024
@legendecas
Copy link
Member

I can confirm the issue. Will prepare a fix soon.

domenic pushed a commit to jsdom/jsdom that referenced this issue Aug 19, 2024
Due to a Node.js v22 vm issue (nodejs/node#54436), some subtests are failing that should pass, and some passing that should fail. Instead of trying to add per-subtest per-Node-version disable/enable functionality, let's just mark the whole test as failing for now, leaving the intended subtests in commented out.
@legendecas
Copy link
Member

legendecas commented Aug 19, 2024

I think the best course of this issue would be setting the prototype of the inner globalThis in the vm context. The issue is caused by the class hierarchy with the globalThis:

Outer Context    |    Vm Context
Object           |    Object
  ^              |      ^
  | prototype    |      | prototype
EventTarget      |    Global
  ^              |      ^
  | prototype    |      | prototype
Window           |      |
  ^              |      |
  | prototype    |      |
window (sandbox) <--- globalThis
		intercepts property access

If I understand correctly, the expected class hierarchy should be:

Outer Context    |    Vm Context
Object           |    Object
  ^              |      ^
  | prototype    |      | prototype
EventTarget      |    EventTarget
  ^              |      ^
  | prototype    |      | prototype
Window           |    Window
  ^              |      ^
  | prototype    |      | prototype
window (sandbox) <--- globalThis
		intercepts property access

With the fix in #53517, the inner globalThis correctly queries the own properties of the sandbox object with Object.hasOwn, however, it can not distinguish in operator with Object.hasOwn with current V8 APIs.

To fix the issue and preserving the correct Object.hasOwn behavior, we have two solutions:

  1. Delegate prototype lookup to the sandbox object, so that in operator will lookup the property on the outer prototype instead. However, this may unconditionally leaks vm outside Object.prototype properties to the inner vm context. I believe this may already be the case for jsdom since API objects like EventTarget are inherited from the outside Object.
  2. Manually setting the inner globalThis prototype to be the Window.prototype in userland before (1) gets implemented. This works in Node.js versions before and after v22.5.0. Moreover, in v22.5.0 and later, Object.hasOwn will return correct value.

Since Object.hasOwn behavior is not fixable in user land, I am wondering if it would make sense to you to set the inner global prototype like the following snippet:

"use strict";

const vm = require("vm");

class EventTarget {
  addEventListener() {}
}

const windowConstructor = function () {};
Object.setPrototypeOf(windowConstructor, EventTarget);
const windowPrototype = Object.create(EventTarget.prototype);

function Window() {
  vm.createContext(this);
  this._globalProxy = vm.runInContext("this", this);

  // Set up the inner global prototype
  Object.setPrototypeOf(this._globalProxy, windowPrototype);

  const window = this;
  Object.defineProperty(this, "window", {
    get() {
      return window._globalProxy;
    },
    enumerable: true,
    configurable: true
  });
}

const window = new Window();

console.log(vm.runInContext(`"addEventListener" in window`, window));

In this way, the following snippet will output the expected results:

const jsdom = require('jsdom');
const vm = require('vm');
const { JSDOM } = jsdom;
const dom = new JSDOM(``, {
  runScripts: 'outside-only',
});

const vmContext = dom.getInternalVMContext();
// Uncomment the following line to see the difference.
// vm.runInContext(`Object.setPrototypeOf(window, Window.prototype)`, vmContext); 
console.log(vm.runInContext(`window instanceof Object`, vmContext)); // => always `false` because the window was created outside the context
console.log(vm.runInContext(`window instanceof EventTarget`, vmContext));
console.log(vm.runInContext(`'addEventListener' in window`, vmContext));
console.log(vm.runInContext(`Object.hasOwn(window, 'addEventListener')`, vmContext));
console.log(vm.runInContext(`Object.getOwnPropertyDescriptor(window, 'addEventListener')`, vmContext));

The output should be in Node.js v22.5.0:

false
true
true
false
undefined

@domenic
Copy link
Contributor Author

domenic commented Aug 20, 2024

I can try such things, but from past experience anything other than our current (admittedly convoluted) setup has caused lots of tests to break. I would expect that anything that causes a regression like this to be reverted...

@domenic
Copy link
Contributor Author

domenic commented Aug 25, 2024

Given how many other things this appears to fix, I'm happy to close this. In fact, I would wonder if it's possible to back-port c0962dc to v20, so that I could land jsdom/jsdom#3765 without having to wait for v22 to become the earliest LTS...

@domenic domenic closed this as completed Aug 25, 2024
@legendecas
Copy link
Member

I removed https://github.com/nodejs/node/labels/dont-land-on-v20.x on the original PR and it can be backported in the next release.

nodejs-github-bot pushed a commit that referenced this issue Sep 4, 2024
Add more test coverage on vm prototype properties lookup with
`in` operator and property access.

PR-URL: #54606
Refs: #54436
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this issue Sep 12, 2024
Add more test coverage on vm prototype properties lookup with
`in` operator and property access.

PR-URL: #54606
Refs: #54436
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
louwers pushed a commit to louwers/node that referenced this issue Nov 2, 2024
Add more test coverage on vm prototype properties lookup with
`in` operator and property access.

PR-URL: nodejs#54606
Refs: nodejs#54436
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
tpoisseau pushed a commit to tpoisseau/node that referenced this issue Nov 21, 2024
Add more test coverage on vm prototype properties lookup with
`in` operator and property access.

PR-URL: nodejs#54606
Refs: nodejs#54436
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed-bug Issues with confirmed bugs. v22.x v22.x Issues that can be reproduced on v22.x or PRs targeting the v22.x-staging branch. vm Issues and PRs related to the vm subsystem.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants
@domenic @legendecas @marco-ippolito @RedYetiDev and others