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

assert.deepEqual and assert.deepStrictEqual incorrectly compare Proxied arrays when using ownKeys trap #41714

Closed
itaylor opened this issue Jan 27, 2022 · 27 comments · Fixed by #47209
Labels
assert Issues and PRs related to the assert subsystem. confirmed-bug Issues with confirmed bugs. util Issues and PRs related to the built-in util module. v8 engine Issues and PRs related to the V8 dependency.

Comments

@itaylor
Copy link

itaylor commented Jan 27, 2022

Version

v17.4.0

Platform

Darwin macbook-pro-5.lan 21.2.0 Darwin Kernel Version 21.2.0: Sun Nov 28 20:28:54 PST 2021; root:xnu-8019.61.5~1/RELEASE_X86_64 x86_64

Subsystem

assert

What steps will reproduce the bug?

assert.deepEqual(new Proxy(['foo'], {}), ['foo']); // works properly
assert.deepEqual(new Proxy(['foo'], { ownKeys: (target) => Reflect.ownKeys(target) }), ['foo']) // throws

assert.deepStrictEqual(new Proxy(['foo'], {}), ['foo']); // works properly
assert.deepStrictEqual(new Proxy(['foo'], { ownKeys: (target) => Reflect.ownKeys(target) }), ['foo']) // throws

In the above, Reflect.ownKeys(target) should be the same as not setting an ownKeys trap, but it isn't. The trap works fine elsewhere

Indeed,

Reflect.ownKeys(['foo'])  // [ '0', 'length' ]
assert.deepEqual(new Proxy(['foo'], { ownKeys: (target) => ['0', 'length'] }), ['foo'])  // throws

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

Every time

What is the expected behavior?

Using a ownKeys trap shouldn't cause comparison errors.

The ownKeys trap seems to be working fine elsewhere. I traced the code to:

const keys1 = getOwnNonIndexProperties(val1, filter);

Where the GetOwnNonIndexProperties function is called. Perhaps it is incorrectly filtering or the filtering is broken for proxies somehow?

What do you see instead?

Comparison errors from assert.deepEquals and assert.deepStrictEquals when the Arrays compared have the same content, but are using Proxies with ownKeys traps.

Additional information

I tried this on node 14, 16, and 17, with the same results.

@Mesteery Mesteery added the assert Issues and PRs related to the assert subsystem. label Jan 27, 2022
@Trott
Copy link
Member

Trott commented Jan 27, 2022

@nodejs/assert

@benjamingr

This comment was marked as off-topic.

@benjamingr benjamingr added the feature request Issues that request new features to be added to Node.js. label Jan 27, 2022
@benjamingr
Copy link
Member

benjamingr commented Jan 27, 2022

Hmm

> require('internal/test/binding').internalBinding('util').getOwnNonIndexProperties(new Proxy(['foo'], { ownKeys: (target) => Reflect.ownKeys(target) }), 0)
[ 0, 'length' ]

So the issue is that 0 is considered an index property.

So the issue is that

object->GetPropertyNames(
        context, KeyCollectionMode::kOwnOnly,
        filter,
        IndexFilter::kSkipIndices)
          .ToLocal(&properties)

Does not ignore index properties with a proxy trap?

@ljharb

This comment was marked as off-topic.

@targos

This comment was marked as off-topic.

@ljharb

This comment was marked as off-topic.

@benjamingr
Copy link
Member

benjamingr commented Jan 27, 2022

@ljharb it wouldn't matter in this case:

> require('internal/test/binding').internalBinding('util').getOwnNonIndexProperties(new Proxy(['foo'], { ownKeys: Reflect.ownKeys }), 0)
[ 0, 'length' ]
> (node:70102) internal/test/binding: These APIs are for internal testing only. Do not use them.
(Use `node --trace-warnings ...` to show where the warning was created)

This is a "bug" either in v8 or how we use v8 which returns index properties from the call above. (I saw bug because I don't believe we ever discussed what proxy behavior should be)

@itaylor
Copy link
Author

itaylor commented Jan 27, 2022

@ljharb, The problem still occurs with passing {ownKeys: Reflect.ownKeys}

assert.deepEqual(new Proxy(['foo'], { ownKeys: Reflect.ownKeys }), ['foo']) // throws

I see in the spec that you're right, it should be passing a target argument, but that argument isn't listed in the MDN documentation of Reflect.ownKeys.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys

@ljharb

This comment was marked as off-topic.

@benjamingr
Copy link
Member

A fix can be something like this:

diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js
index c126bd6346..6a529c100f 100644
--- a/lib/internal/util/comparisons.js
+++ b/lib/internal/util/comparisons.js
@@ -36,6 +36,7 @@ const {
   isRegExp,
   isSet,
   isNativeError,
+  isProxy,
   isBoxedPrimitive,
   isNumberObject,
   isStringObject,
@@ -68,6 +69,14 @@ function areSimilarRegExps(a, b) {
          a.lastIndex === b.lastIndex;
 }
 
+function getOwnNonIndexPropertiesHandleProxies(target, filter) {
+  const res = getOwnNonIndexProperties(target, filter);
+  if (isProxy(target)) {
+    return res.filter((x) => typeof x !== 'number');
+  }
+  return res;
+}
+
 function areSimilarFloatArrays(a, b) {
   if (a.byteLength !== b.byteLength) {
     return false;
@@ -176,8 +185,8 @@ function innerDeepEqual(val1, val2, strict, memos) {
       return false;
     }
     const filter = strict ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS;
-    const keys1 = getOwnNonIndexProperties(val1, filter);
-    const keys2 = getOwnNonIndexProperties(val2, filter);
+    const keys1 = getOwnNonIndexPropertiesHandleProxies(val1, filter);
+    const keys2 = getOwnNonIndexPropertiesHandleProxies(val2, filter);
     if (keys1.length !== keys2.length) {
       return false;
     }
@@ -217,8 +226,8 @@ function innerDeepEqual(val1, val2, strict, memos) {
     // only contain numeric keys, we don't need to exam further than checking
     // the symbols.
     const filter = strict ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS;
-    const keys1 = getOwnNonIndexProperties(val1, filter);
-    const keys2 = getOwnNonIndexProperties(val2, filter);
+    const keys1 = getOwnNonIndexPropertiesHandleProxies(val1, filter);
+    const keys2 = getOwnNonIndexPropertiesHandleProxies(val2, filter);
     if (keys1.length !== keys2.length) {
       return false;
     }

We should also probably file a v8 bug?

@ljharb
Copy link
Member

ljharb commented Jan 27, 2022

Wouldn’t that fix potentially interfere with proxies of arrays, or proxies of objects with numeric string keys?

@benjamingr
Copy link
Member

@ljharb

Wouldn’t that fix potentially interfere with proxies of arrays, or proxies of objects with numeric string keys?

It would as in today they don't work and following this patch they do and return the same keys as the non-proxy version

@devsnek
Copy link
Member

devsnek commented Jan 27, 2022

0 is an index regardless of what kind of object it is pulled off of. If this is coming from IndexFilter::kSkipIndices I would suggest fixing it there.

@hinell

This comment was marked as off-topic.

@itaylor

This comment was marked as off-topic.

@ljharb

This comment was marked as off-topic.

@benjamingr
Copy link
Member

@ljharb the issue is much simpler than that I think? no one updated the assert code to support proxies and there are (likely) a bunch of bugs in the V8 APIs used like Gus said.

So I think this feature request is definitely worth doing but am waiting for someone with more regular commits to V8 to either fix it there or tell us it's intended so we work around it similar to #41714 (comment)

Note node is not distinguishing something based on whether or not it's a proxy in that workaround - it just works around the underlying v8 bug that does.

@ljharb
Copy link
Member

ljharb commented Feb 12, 2022

@benjamingr sure, there's surely something to fix here.

What I meant is, node's inspection output in the thrown assertion error clearly marks the proxy as a Proxy; that's the part i was saying is a violation of the language's intent.

@benjamingr
Copy link
Member

benjamingr commented Feb 12, 2022

@ljharb

What I meant is, node's inspection output in the thrown assertion error clearly marks the proxy as a Proxy; that's the part i was saying is a violation of the language's intent.

Yes though that's not intentional it's just since assert was never made to work with proxies. This is a v8 bug not something intentional Node chose to do.

@hinell

This comment was marked as off-topic.

@BridgeAR
Copy link
Member

I had trouble seeing the comments that were directly related to the issue, so I just hid some comments as off-topic.

@nodejs/v8 the issue here clearly looks like a V8 bug. Would it be possible to fix getPropertyNames using IndexFilter::kSkipIndices to filter indices even if there is a proxy trap? See #41714 (comment)

@BridgeAR BridgeAR added util Issues and PRs related to the built-in util module. v8 engine Issues and PRs related to the V8 dependency. labels Jul 16, 2022
@BridgeAR BridgeAR removed the feature request Issues that request new features to be added to Node.js. label Jul 16, 2022
@BridgeAR BridgeAR added the confirmed-bug Issues with confirmed bugs. label Jul 16, 2022
@BridgeAR
Copy link
Member

There is actually an explicit comment about the behavior in V8:

// This is a helper class for JSReceiver::GetKeys which collects and sorts keys.
// GetKeys needs to sort keys per prototype level, first showing the integer
// indices from elements then the strings from the properties. However, this
// does not apply to proxies which are in full control of how the keys are
// sorted.

We could either implement the behavior in FilterProxyKeys or have to check for proxies on our own and do the filtering manually as @benjamingr suggested.

MaybeHandle<FixedArray> FilterProxyKeys(KeyAccumulator* accumulator,
Handle<JSProxy> owner,
Handle<FixedArray> keys,
PropertyFilter filter) {
if (filter == ALL_PROPERTIES) {
// Nothing to do.
return keys;
}
Isolate* isolate = accumulator->isolate();
int store_position = 0;
for (int i = 0; i < keys->length(); ++i) {
Handle<Name> key(Name::cast(keys->get(i)), isolate);
if (key->FilterKey(filter)) continue; // Skip this key.
if (filter & ONLY_ENUMERABLE) {
PropertyDescriptor desc;
Maybe<bool> found =
JSProxy::GetOwnPropertyDescriptor(isolate, owner, key, &desc);
MAYBE_RETURN(found, MaybeHandle<FixedArray>());
if (!found.FromJust()) continue;
if (!desc.enumerable()) {
accumulator->AddShadowingKey(key);
continue;
}
}
// Keep this key.
if (store_position != i) {
keys->set(store_position, *key);
}
store_position++;
}
return FixedArray::ShrinkOrEmpty(isolate, keys, store_position);
}

@BridgeAR
Copy link
Member

It is actually somewhat confusing for me that there's a separate IndexFilter vs. PropertyFilter.

@targos
Copy link
Member

targos commented Nov 8, 2022

What should we do with this?

@BridgeAR
Copy link
Member

BridgeAR commented Feb 9, 2023

I opened https://bugs.chromium.org/p/v8/issues/detail?id=13728. Let's see if this can be fixed in V8.

@debadree25
Copy link
Member

I tried a solution something like

diff --git a/deps/v8/src/objects/keys.cc b/deps/v8/src/objects/keys.cc
index a0796864f1..4f84cd9094 100644
--- a/deps/v8/src/objects/keys.cc
+++ b/deps/v8/src/objects/keys.cc
@@ -182,8 +182,9 @@ ExceptionStatus KeyAccumulator::AddKeys(Handle<JSObject> array_like,
 MaybeHandle<FixedArray> FilterProxyKeys(KeyAccumulator* accumulator,
                                         Handle<JSProxy> owner,
                                         Handle<FixedArray> keys,
-                                        PropertyFilter filter) {
-  if (filter == ALL_PROPERTIES) {
+                                        PropertyFilter filter,
+                                        bool skip_indices) {
+  if (filter == ALL_PROPERTIES  && !skip_indices) {
     // Nothing to do.
     return keys;
   }
@@ -191,7 +192,7 @@ MaybeHandle<FixedArray> FilterProxyKeys(KeyAccumulator* accumulator,
   int store_position = 0;
   for (int i = 0; i < keys->length(); ++i) {
     Handle<Name> key(Name::cast(keys->get(i)), isolate);
-    if (key->FilterKey(filter)) continue;  // Skip this key.
+    if (key->FilterKey(filter) || (skip_indices && key->IsNumber())) continue;  // Skip this key.
     if (filter & ONLY_ENUMERABLE) {
       PropertyDescriptor desc;
       Maybe<bool> found =
@@ -218,7 +219,7 @@ Maybe<bool> KeyAccumulator::AddKeysFromJSProxy(Handle<JSProxy> proxy,
   // Postpone the enumerable check for for-in to the ForInFilter step.
   if (!is_for_in_) {
     ASSIGN_RETURN_ON_EXCEPTION_VALUE(
-        isolate_, keys, FilterProxyKeys(this, proxy, keys, filter_),
+        isolate_, keys, FilterProxyKeys(this, proxy, keys, filter_, skip_indices_),
         Nothing<bool>());
   }
   // https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-ownpropertykeys

On v8 but doesn't seem to make a change 😕

@debadree25
Copy link
Member

Hey! was able to get this fixed in v8 @ https://chromium.googlesource.com/v8/v8/+/975ff4dbfd1be3a7395e26d412774bc955b47341 so the next update of v8 in node should fix it or do we have to patch the commit here?

cc @benjamingr @BridgeAR

nodejs-github-bot pushed a commit that referenced this issue Mar 24, 2023
Original commit message:

    fix GetPropertyNames for proxys with ownKeys trap

    Added checks to FilterProxyKeys function for when skip_indices is
    enabled.

    Bug: v8:13728
    Change-Id: Id096e32ef8e6c2344be9682e8222aea8790bd66d
    Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4333698
    Reviewed-by: Camillo Bruni <cbruni@chromium.org>
    Commit-Queue: Jakob Kummerow <jkummerow@chromium.org>
    Cr-Commit-Position: refs/heads/main@{#86548}

Refs: v8/v8@975ff4d
PR-URL: #47209
Fixes: #41714
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Jiawen Geng <technicalcute@gmail.com>
Reviewed-By: Richard Lau <rlau@redhat.com>
Reviewed-By: Erick Wendel <erick.workspace@gmail.com>
RafaelGSS pushed a commit that referenced this issue Apr 5, 2023
Original commit message:

    fix GetPropertyNames for proxys with ownKeys trap

    Added checks to FilterProxyKeys function for when skip_indices is
    enabled.

    Bug: v8:13728
    Change-Id: Id096e32ef8e6c2344be9682e8222aea8790bd66d
    Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4333698
    Reviewed-by: Camillo Bruni <cbruni@chromium.org>
    Commit-Queue: Jakob Kummerow <jkummerow@chromium.org>
    Cr-Commit-Position: refs/heads/main@{#86548}

Refs: v8/v8@975ff4d
PR-URL: #47209
Fixes: #41714
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Jiawen Geng <technicalcute@gmail.com>
Reviewed-By: Richard Lau <rlau@redhat.com>
Reviewed-By: Erick Wendel <erick.workspace@gmail.com>
RafaelGSS pushed a commit that referenced this issue Apr 6, 2023
Original commit message:

    fix GetPropertyNames for proxys with ownKeys trap

    Added checks to FilterProxyKeys function for when skip_indices is
    enabled.

    Bug: v8:13728
    Change-Id: Id096e32ef8e6c2344be9682e8222aea8790bd66d
    Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4333698
    Reviewed-by: Camillo Bruni <cbruni@chromium.org>
    Commit-Queue: Jakob Kummerow <jkummerow@chromium.org>
    Cr-Commit-Position: refs/heads/main@{#86548}

Refs: v8/v8@975ff4d
PR-URL: #47209
Fixes: #41714
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Jiawen Geng <technicalcute@gmail.com>
Reviewed-By: Richard Lau <rlau@redhat.com>
Reviewed-By: Erick Wendel <erick.workspace@gmail.com>
RafaelGSS pushed a commit that referenced this issue Apr 7, 2023
Original commit message:

    fix GetPropertyNames for proxys with ownKeys trap

    Added checks to FilterProxyKeys function for when skip_indices is
    enabled.

    Bug: v8:13728
    Change-Id: Id096e32ef8e6c2344be9682e8222aea8790bd66d
    Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4333698
    Reviewed-by: Camillo Bruni <cbruni@chromium.org>
    Commit-Queue: Jakob Kummerow <jkummerow@chromium.org>
    Cr-Commit-Position: refs/heads/main@{#86548}

Refs: v8/v8@975ff4d
PR-URL: #47209
Fixes: #41714
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Jiawen Geng <technicalcute@gmail.com>
Reviewed-By: Richard Lau <rlau@redhat.com>
Reviewed-By: Erick Wendel <erick.workspace@gmail.com>
danielleadams pushed a commit that referenced this issue Jul 6, 2023
Original commit message:

    fix GetPropertyNames for proxys with ownKeys trap

    Added checks to FilterProxyKeys function for when skip_indices is
    enabled.

    Bug: v8:13728
    Change-Id: Id096e32ef8e6c2344be9682e8222aea8790bd66d
    Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4333698
    Reviewed-by: Camillo Bruni <cbruni@chromium.org>
    Commit-Queue: Jakob Kummerow <jkummerow@chromium.org>
    Cr-Commit-Position: refs/heads/main@{#86548}

Refs: v8/v8@975ff4d
PR-URL: #47209
Fixes: #41714
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Jiawen Geng <technicalcute@gmail.com>
Reviewed-By: Richard Lau <rlau@redhat.com>
Reviewed-By: Erick Wendel <erick.workspace@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
assert Issues and PRs related to the assert subsystem. confirmed-bug Issues with confirmed bugs. util Issues and PRs related to the built-in util module. v8 engine Issues and PRs related to the V8 dependency.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants