Open Bug 1853050 Opened 1 year ago Updated 3 months ago

structuredClone rejects intrinsic prototype objects

Categories

(Core :: JavaScript Engine, defect, P3)

Firefox 117
defect

Tracking

()

UNCONFIRMED

People

(Reporter: akaster, Unassigned, NeedInfo)

References

(Blocks 1 open bug)

Details

User Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/117.0

Steps to reproduce:

Open JS console:

let a = { "a": 12 }
structuredClone(a.proto)
let b = new RegExp(".", "")
structuredClone(b.proto)

Actual results:

The prototype of the ordinary object a is cloned and printed to the console.

A DOM Exception is thrown when trying to clone the RegExp.prototype object from b.

Expected results:

The spec steps for structured serialize internal (html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal) don't seem to preclude serializing intrinsic object prototypes:

In step 21, the algorithm precludes any objects with funky internal slots

    Otherwise, if value has any internal slot other than [[Prototype]] or [[Extensible]], then throw a "DataCloneError" DOMException.

If we look at the ES spec for RegExp Prototype: tc39.es/ecma262/#sec-properties-of-the-regexp-prototype-object

It says that that object:

    is %RegExp.prototype%.
    is an ordinary object.
    is not a RegExp instance and does not have a [[RegExpMatcher]] internal slot or any of the other internal slots of RegExp instance objects.
    has a [[Prototype]] internal slot whose value is %Object.prototype%.

Which suggests to me that it should fall through to the next step, step 23, which says:

    Otherwise, if value is an exotic object and value is not the %Object.prototype% intrinsic object associated with any realm, then throw a "DataCloneError" DOMException.

Since %RegExp.prototype% is an ordinary object, it's not exotic, and so it should be cloneable as any other object.

Chromium does this per the spec, and WebKit does not.

The Bugbug bot thinks this bug should belong to the 'Core::JavaScript Engine' component, and is moving the bug to that component. Please correct in case you think the bot is wrong.

Component: Untriaged → JavaScript Engine
Product: Firefox → Core

This currently affects all intrinsic prototypes that should be cloned to empty objects, e.g. %Date.prototype%, %Error.prototype%, etc, not just %RegExp.prototype%.

Curiously I’ve found a single exception to my prior statement so far: %Symbol.prototype% does get mapped to an empty object.

Note that the primitive-boxing intrinsic prototypes that have specially-handled [[FooData]] slots (%Boolean.prototype%, %Number.prototype%, and %String.prototype%) are handled correctly already. Similarly, %Array.prototype% is already handled correctly due to being an Array exotic object.

Apologies for repeated posts (wish I could edit a previous one!) but I keep discovering more information:

It’s not just intrinsic prototypes, it’s (most) intrinsic objects in general. The %Atomics%, %Intl%, %JSON%, %Math%, and %Reflect% namespace objects should also be getting mapped to empty objects by HTML’s algorithm, but cloning all of these except (for some reason!) %Reflect% throws in FF.

Conversely, structuredClone(document.adoptedStyleSheets) (even when document.adoptedStyleSheets.length === 0) should throw due to the (unfortunate) PEO sniffing that occurs in the cloning algorithm, but doesn’t. This effectively reveals that FF isn’t actually implementing observable array exotic objects as specified (i.e., as proxy exotic objects). As much as I wish the PEO-revealing steps in structured clone didn’t exist, so long as they do, the situation (for host reimplementation / virtualization) is made worse by honoring those steps for userland PEOs yet ignoring them for natively implemented (pseudo-)PEOs.

Steve, I'm alas not read into structured cloning, so I'm going to need-info you here.

Blocks: js-lang
Severity: -- → S4
Flags: needinfo?(sfink)
Priority: -- → P3

Yes, I was looking into this yesterday, and it's something I"ll need to fix.

Structured cloning is currently mostly going off of a set of known JSClass pointers and rejecting anything else.

Namespace objects like JSON are rejected because we seem to use custom JSClasses in conjunction with js::ClassSpec for laziness and a convenient way to construct all of the contained properties and functions. Reflect also uses js::ClassSpec, but not a custom JSClass, so I need to look into it to understand why the others need the custom clasp. Maybe for laziness?

Prototypes also have their own clasps, especially the older ones because that's how things used to be done. More recently, the preference has switched to using plain objects for builtin prototypes, iiuc. Newer things like Symbol.prototype and Temporal.Duration.prototype are properly serializable as a result.

We don't necessarily have anything directly equivalent to spec internal slots, so I'll probably have to add an additional mechanism. Maybe a JSClass flag of some sort, like JSCLASS_ORDINARY_OBJECT or something.

That said, I think this may also partly be due to structured cloning being defined in the HTML spec instead of es262. People have talked about moving it over, but it hasn't happened yet. If it were moved, I suspect we'd want to replace fuzzy language like "if value has any internal slot other than [[Prototype]] or [[Extensible]]"—the spec uses internal slots for all sorts of things. Should the presence of something like [[InitialName]] or [[Realm]] or [[ArrayBufferDetachKey]] really have anything to do with whether it should be structured cloneable? The es262 spec already says "All objects have an internal slot named [[PrivateElements]]", so if I were to be pedantic I would claim that no objects should be structured cloneable according to the current HTML spec.

I haven't waded through the spec enough to get a handle on the proxy exotic object stuff yet.

See Also: → 1904148
You need to log in before you can comment on or make changes to this bug.