Skip to content

Instantly share code, notes, and snippets.

@jorendorff
Last active October 7, 2023 23:20
Show Gist options
  • Select an option

  • Save jorendorff/0a88e49ef9c297001317a373c35d957d to your computer and use it in GitHub Desktop.

Select an option

Save jorendorff/0a88e49ef9c297001317a373c35d957d to your computer and use it in GitHub Desktop.

Same-compartment realms are bug 1357862.

Plan

Each heading here is a separate bug to be filed, eventually.

Change JSAPI to distinguish realms and compartments

https://bugzilla.mozilla.org/show_bug.cgi?id=1363200

The first step in supporting same-compartment realms.

Without any change in behavior, we'll make the following superficial JSAPI changes:

  • Define JS::Realm as a public typedef of JSCompartment

  • Move several APIs from jsapi.h to public/Realm.h and rename them

  • Rename JSAutoCompartment -> JS::AutoRealm. (Look for cases where the object passed to the ctor might be a CCW, since we want to ban that later.)

  • Finally, change JS::Realm so that it is not the same type as JSCompartment, to ensure that API users outside js/src do not conflate the two. (JS::Realm can be an opaque struct type, for now, and inside the JSAPI we can just cast it to JSCompartment immediately on entry.)

Add "realm" link to ObjectGroup

https://bugzilla.mozilla.org/show_bug.cgi?id=1363206

With same-compartment realms, all non-ccw objects need a link to their realm/global (for document.domain, but mainly for other reasons unrelated to security, like when a random object is passed the JS::AutoRealm constructor).

Make the Location binding a proxy

https://bugzilla.mozilla.org/show_bug.cgi?id=1363211

It needs to be a Proxy because we need to implement security checks in the object's internal methods (because of the interaction between same-compartment realms and document.domain). See next bug.

Add security checking to WindowProxy and Location objects

https://bugzilla.mozilla.org/show_bug.cgi?id=1363208

With same-compartment realms, we need a new way to enforce security checks during cross-realm access.

This is a matter of following the spec:

<bz> jorendorff: well, we have a spec.
<bz> jorendorff: lemme pull it up
<bz> jorendorff: https://html.spec.whatwg.org/multipage/browsers.html#location-getprototypeof
     and following
<bz> jorendorff: some of those internal methods just are, but some do sPlatformObjectSameOrigin(
<bz> jorendorff: We'd just do that
<bz> jorendorff: So in [[GetPrototypeOf]], [[GetOwnProperty]], [[DefineOwnProperty]],
     [[Get]], [[Set]], [[Delete]], [[OwnPropertyKeys]]
<bz> jorendorff: (in terms of our code, in the corresponding proxy handler bits
<bz> jorendorff: The windowproxy bits are at
     https://html.spec.whatwg.org/multipage/browsers.html#windowproxy-getprototypeof
     and following
<bz> jorendorff: but it's the same list

Rename JSCompartment -> JS::Realm, JSAutoCompartment -> JS::AutoRealm

https://bugzilla.mozilla.org/show_bug.cgi?id=1363212

Superficial change, part of same-compartment realms.

Separate from other bugs because it touches everything in the world.

Not totally trivial, because there are several other things to rename at the same time, like JSContext::compartment_; yet CompartmentChecker isn't affected.

Split JS::Compartment from JS::Realm

https://bugzilla.mozilla.org/show_bug.cgi?id=1363214

To support same-compartment realms, we factor out a Compartment class so that Compartment and Realm are no longer identical inside js/src. However, initially there will be no way to create multiple Realms in the same Compartment, so they'll still be one-to-one.

Each Realm has a pointer to its freshly factored-out Compartment. All Wrapper stuff moves from JS::Realm to the new JS::Compartment.

Initially, JS::Compartment also contains a pointer to its sole Realm. (It will be eliminated later.)

To make way for JS::Compartment, js::wasm::Compartment will have to be renamed to js::wasm::WasmCompartment.

Add cx->realm_ and use it for "the current global"

Behavior changes caused by this bug should be limited to fairly obscure cases, all having to do with "the current global". Before this bug, that's based on cx->compartment_, and it's updated when crossing compartment boundaries. In this bug we switch over to cx->realm_, which is updated in exactly those places required by the ECMAScript spec.

We need to update cx->realm_ when entering and leaving scripts:

  • calling and executing
  • "inline" interpreter function calls and returns
  • unwinding for an exception
  • jit calls: initially broken. So actually using same-compartment realms will have to preffed off until this is fixed (see the next bug).

Otherwise, I don't think we have to change cx->realm_ at JSAPI boundaries.

With this change, AutoRealm updates the current realm, not just the current compartment.

Update cx->realm_ when entering/leaving JIT calls

Jan agreed to take this.

  • when calling an unknown function jit->jit, update cx->realm_
  • also at inlined jit calls
  • adjust optimization where we innerize the windowproxy (per bz)

Remove JSContext::compartment_

Use cx->realm_->compartment_ instead. Separate bug because this is a subtle change in behavior, in corner cases only, and I have no real expectation as to what it might break.

Remove JS::Compartment::realm

Not sure what this will entail until I get here.

New JS API for creating same-compartment realms

Actually support N:1 relationship between realms and compartments.

We can test the new API this in the shell before using it in the browser. This will likely involve some work on the Debugger and places where we currently iterate over compartments. Not a big deal though.

Intended design: Make this an option in CompartmentOptions: "don't create a new compartment; instead bundle with X existing compartment".

Alternative design: Keep a HashMap<JSPrincipals*, Compartment*> in each Zone. When creating a global, check for an existing compartment with the same principals. Always bundle same-principals globals into the same compartments whenever possible.

I like the former because it's one less HashMap and it seems like it'll work out about the same in practice.

Implement same-compartment realms for chrome, sandbox, and add-on globals

Behind a pref, off by default. (Content is not included yet because as long as all content remains CPG, we don't have any problems with document.domain.)

This eliminates CCWs between JSMs, for example.

Sandboxes will still get their own compartment. Add-ons, TBD.

Turn pref for same-compartment chrome realms on by default

Requires JIT work to be done.

Implement same-compartment realms for content, preffed off

Change Gecko to put multiple same-tab-group, same-origin content globals in the same compartment.

This has to be preffed off by default because initially document.domain won't work.

Reimplement document.domain using waiver wrappers

A content-to-content CCW for an object across origins is opaque by default. We need a waiver for these, to reimplement document.domain in the new order.

Since we have existing bugs (XXX cite bug here) that allow exactly the kind of object-injected-across-domain hole bholley was worried about, we are going with bholley's waiver approach.

Turn pref for same-compartment content realms on by default

Depends on all of the above.

Notes on document.domain

Behavior of accesses to WindowProxy and Location objects will depend on the current value of document.domain for the current realm and target.

Here "current realm" means the realm of the code trying to access the object -- "the current Realm Record" in standardese, cx->realm_ in our implementation. "Target" means the realm of the object being accessed; for Location objects that is obvious enough since there's one per global, and for WindowProxy objects it means the immutable Realm field of whatever Window we're currently navigated to. (check with bz)

  • When you're accessing your own WindowProxy/Location, same-realm, of course you just get normal direct access.

  • If you're accessing a WindowProxy/Location of another realm in the same compartment, there's a document.domain check. There's no wrapper; this check is performed by the object itself (it's implemented as a proxy).

  • If you're accessing a WindowProxy/Location of another compartment, you have a cross-compartment wrapper. Then what?

    I think what should happen here is: (check with bz, all of this)

    1. The CCW should have a no-op security policy. That is, the wrapper doesn't need to do security checks because the referent is a WindowProxy/Location object which does security checks on every access.

    2. But the CCW must not enter the referent's compartment before calling the referent's ProxyHandler methods. I gather this is something CrossOriginXrayWrappers already do.

    3. WindowProxy/Location's custom ProxyHandler methods need to check the current realm and must return special document.domain waiver objects if granting cross-compartment access.

    4. These objects (and other wrappers, maybe) also implement some spec-mandated function-caching magic -- check with bz whether this will need to change.

What about other objects?

  • Realms that are same-compartment are same-origin (ignoring document.domain) and no wrappers protect them from one another. Unmediated access.

    • If via document.domain two same-origin realms become different-"origin-domain", they retain unmediated access (except for WindowProxy and Location, which do C++ security checks that can now fail).
  • Cross-compartment access is mediated by wrappers.

    • Opaque by default. (WindowProxy and Location are special; they get CrossOriginXrayWrappers.)

    • WindowProxy/Location will return "document.domain waivers" when granting cross-compartment access. This will result in the caller receiving a DocumentDomainWrapper, which is like a transparent CCW, with the exception that they also hand out DocumentDomainWrappers for any returned object properties, transitively.

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