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

Make SET_FLAG_IF more paranoid #3740

Merged
merged 5 commits into from Apr 9, 2023
Merged

Conversation

hoodmane
Copy link
Member

@hoodmane hoodmane commented Apr 5, 2023

This fixes a regression introduced in #3283. To be properly paranoid we should assume that each property access can raise an error.

The bug also depends on this branch here:
https://github.com/pyodide/pyodide/blob/main/src/core/python2js.c#L833-L834
which is a bit suspect...

This fixes a regression introduced in pyodide#3283.

The bug also depends on this branch here:
https://github.com/pyodide/pyodide/blob/main/src/core/python2js.c#L833-L834
which is a bit suspect...
@hoodmane hoodmane added this to the 0.23.1 milestone Apr 6, 2023
Copy link
Member

@ryanking13 ryanking13 left a comment

Choose a reason for hiding this comment

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

Thanks!

if (cond) { \
type_flags |= flag; \
} \
} catch (e) { \
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we propagate the error, or maybe at least show a warning message? If we ignore the error, then the object will have invalid type flags I think.

Copy link
Member Author

@hoodmane hoodmane Apr 7, 2023

Choose a reason for hiding this comment

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

We can't throw an error here because then the JavaScript object becomes completely inaccessible from Python (as previously was the case with objects produced viaObject.create(null)). It's possible to work around missing type flags via slightly non idiomatic code. It's not possible to work around "touching this object in any way throws an error".

But note also that this is extremely rare. Basically the main thing we are concerned about is stuff like:

py_func({ get length() { throw new Error(); } });

It's actually pretty funny that hypothesis managed to find a code path that is equivalent to this.

But you are right that ideally this should produce a JsProxy such that len(x) throws a JsException, not one that throws TypeError: object of type 'type' has no len(). Particularly for extremely pathological stuff like:

py_func({ 
    get length() { 
        if(firstTimeCalled){
            throw new Error(); 
        }
        return 7;
    } 
});

To get something that behaves correctly here we should do:

function hasProperty(obj, prop) {
  while (obj) {
    if (Object.getOwnPropertyDescriptor(obj, prop)) {
      return true;
    }
    obj = Object.getPrototypeOf(obj);
  }
  return false;
}

I think we should still catch and throw away errors, but we should set fail_test to true and when compiled in debug mode we should log them.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well currently we have:

$ len(run_js("({ get length() { return 'a'; } })"))
TypeError: object of type 'pyodide.ffi.JsProxy' has no len()

But if we changed it to not execute the getter, we would get:

$ len(run_js("({ get length() { return 'a'; } })"))
TypeError: 'str' object cannot be interpreted as an integer

which is I guess fine. But then what about:

len(run_js("({ length: 'a' })"))

or:

len(run_js("({ length: undefined })"))

I guess in Python you can say:

class A:
   def __len__(self):
      return None

and the expected behavior is that len(A()) raises

TypeError: 'NoneType' object cannot be interpreted as an integer

Copy link
Member Author

Choose a reason for hiding this comment

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

But I think the most important thing is that JsProxy_create should never fail.

@hoodmane hoodmane merged commit 873ca14 into pyodide:main Apr 9, 2023
18 of 19 checks passed
@hoodmane hoodmane deleted the paranoid-set-flag-if branch April 9, 2023 00:14
hoodmane added a commit to hoodmane/pyodide that referenced this pull request Apr 9, 2023
hoodmane added a commit that referenced this pull request Apr 11, 2023
This is a followup to #3740 which rearranges the logic a bit more on further consideration.

I found out that if you create a revocable Proxy and then revoke it, a huge number of browser
internal operations raise TypeErrors. Such an object is thus an excellent stress test for our
handling of explosive objects. Overall we do quite well, `isPyProxy` and `compute_type_flags`
are actually the only spots where we run into trouble (and `PyProxy_Check` is the only spot
that is raising a fatal error).

This changes it so that an object with a `length` getter that throws an error or returns
not an integer will have a `__len__` in Python that always throws an error. It can't check
the type anymore because it avoids invoking getters so if the result is a return value from
a getter it won't know the type. The same is true for the `Error.name`, `Error.message`, and
`Error.stack` fields.

To check for methods, I still use `typeof x[method] === "function"`, invoking the getter.
Usually functions don't have getters anyways and I think it is weirder to have false positives
in this case. If there is a getter and it raises an error, we catch it and throw it away.
hoodmane added a commit to hoodmane/pyodide that referenced this pull request Jul 7, 2023
This fixes a regression introduced in pyodide#3283.
To be properly paranoid we should assume that each property access can raise an error.
If an error is raised, throw it away and don't set the flag.
This could be further improved to avoid some getter calls but this a start.
hoodmane added a commit to hoodmane/pyodide that referenced this pull request Jul 7, 2023
This is a followup to pyodide#3740 which rearranges the logic a bit more on further consideration.

I found out that if you create a revocable Proxy and then revoke it, a huge number of browser
internal operations raise TypeErrors. Such an object is thus an excellent stress test for our
handling of explosive objects. Overall we do quite well, `isPyProxy` and `compute_type_flags`
are actually the only spots where we run into trouble (and `PyProxy_Check` is the only spot
that is raising a fatal error).

This changes it so that an object with a `length` getter that throws an error or returns
not an integer will have a `__len__` in Python that always throws an error. It can't check
the type anymore because it avoids invoking getters so if the result is a return value from
a getter it won't know the type. The same is true for the `Error.name`, `Error.message`, and
`Error.stack` fields.

To check for methods, I still use `typeof x[method] === "function"`, invoking the getter.
Usually functions don't have getters anyways and I think it is weirder to have false positives
in this case. If there is a getter and it raises an error, we catch it and throw it away.
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.

None yet

2 participants