4

I'm seeking to faithfully implement the Structured Cloning Algorithm for an IndexedDB polyfill (including for Node). While the algorithm's explicit whitelisting of certain types is mostly clear, I'm more at a loss for understanding its blanket blacklisting (and how to detect such blacklisted types).

The Structured Cloning Algorithm states:

By default, platform objects are not cloneable objects.

...where a "platform object" is defined in WebIDL:

In a given implementation of a set of IDL fragments, an object can be described as being a platform object, a user object, or neither. There are two kinds of object that are considered to be platform objects:

  • objects that implement a non-callback interface;
  • objects representing IDL DOMExceptions.

As to another category of (potentially) non-clonable objects, the SCA, after whitelisting certain types, mentions:

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

For instance, a [[PromiseState]] or [[WeakMapData]] internal slot.

  1. Otherwise, if input is an exotic object, then throw a "DataCloneError" DOMException.

An "exotic object" is defined in ECMAScript 2017 as an:

object that does not have the default behaviour for one or more of the essential internal methods NOTE Any object that is not an ordinary object is an exotic object.

Since SCA throws for IsCallable which checks for [[Call]] (and "All ECMAScript function objects have the [[Call]] internal method defined here"), I have excluded the "essential internal methods" for functions, leading to the following:

  • [[GetPrototypeOf]],[[SetPrototypeOf]],[[IsExtensible]],[[PreventExtensions]],[[GetOwnProperty]],[[DefineOwnProperty]],[[HasProperty]],[[Get]],[[Set]],[[Delete]],[[OwnPropertyKeys]]

So a few questions arise as to conditions for rejection:

  1. How to detect platform objects for default blacklisting and how to detect/find those platform objects which do have a [[Clone]] internal method and thus can be whitelisted
  2. For rejecting (non-whitelisted, i.e., array) exotic objects that are not defined in ECMAScript itself, how to find which other specifications do not offer the default behavior for the "essential internal methods" above in their objects (unless the HTML spec statement that "Objects defined in the JavaScript specification are handled by the StructuredClone abstract operation directly." indicates that no non-ECMAScript-defined objects will get this algorithm applied).
  3. Whether other specifications besides ECMAScript define their own internal slots for their objects (which might be another criterion for rejection)

My implementation thus far is:

function throwUponDataCloneError (val) {
    // Should also throw with:
    // 1. `IsDetachedBuffer` (a process not called within the ECMAScript spec)
    // 2. We are not dealing with `Blob`/`File` at this point as the
    //     IndexedDB spec requires synchronous rejection and we'd apparently
    //     have to use deprecated sync XHR (or `readFileSync` in Node) to get this
    // 'Proxy' should be rejected but no apparent way to introspect on it
    const stringTag = ({}.toString.call(val).slice(8, -1));
    if (typeof val === 'symbol' || // Symbol's `toStringTag` is only "Symbol" for its initial value, so safer to check `typeof`
        [
            'Function', // All functions including bound function exotic objects; rejected per `IsCallable` check (could also be covered by `typeof === 'function'`)
            'Arguments', // A non-array exotic object
            'Error', // `Error` and other errors have the [[ErrorData]] internal slot and give "Error"
            // The following checks are only based on the default expected `toStringTag` values
            'Module', // A non-array exotic object
            'Promise', // Promise instances have an extra slot ([[PromiseState]]) but not throwing in Chrome `postMessage`
            'WeakMap', // WeakMap instances have an extra slot ([[WeakMapData]]) but not throwing in Chrome `postMessage`
            'WeakSet' // WeakSet instances have an extra slot ([[WeakSetData]]) but not throwing in Chrome `postMessage`
        ].includes(stringTag) ||
        val === Object.prototype || // A non-array exotic object but not throwing in Chrome `postMessage`
    ) {
        throw new DOMException('The object cannot be cloned.', 'DataCloneError');
    }
}

As one means of detecting platform objects, WebIDL states:

The class string of a platform object that implements one or more interfaces must be the identifier of the primary interface of the platform object.

...where a "class string" is defined as:

the string to include in the string returned from Object.prototype.toString. If an object has a class string, then the object must, at the time it is created, have a property whose name is the @@toStringTag symbol and whose value is the specified string.

In fact, one JS implementation of SCA I could find, CycloneJS takes this approach in throwing upon any non-recognized @@toStringTag.

However, I am concerned that:

  1. Especially as @@toStringTag is not an internal slot and thus objects having a distinct one is not an automatic disqualifier, I'd be concerned rejecting any object with such a distinct tag would be excessively prohibitive. But maybe these cases would be too few to be of concern.
  2. I am still left, however, with the question of whether there is a need to scour specs for non-default behavior of "essential internal methods" to reject those too. Likewise if other specifications besides ECMAScript define their own internal slots (which might be another criterion for rejection). I wonder if it is safe to assume for both cases though, that most if not all of these would indeed have their own distinct @@toStringTag and could thus be rejected per the approach in concern #1.

So essentially, I'd like to know whether the Object.prototype.toString whitelisting approach ought to be mostly adequate to avoid false positives or false negatives and whether any other detection techniques could give more granular control.

Community
  • 1
  • 1
Brett Zamir
  • 14,034
  • 6
  • 54
  • 77
  • 3
    This is one of those unfortunate places where the Web Platform contains "magic", i.e. behavior that can't be implemented using other parts of the platform (ECMAScript, browser APIs, etc). Since you don't have access to those internal slots you can't implement the same tests a native implementation can. In general we'd like to eliminate magic from the platform, but... there's a lot, and it's rarely the highest priority work. ... I haven't (yet?) spent enough time looking at your algorithm to judge whether it's "mostly adequate" so I'm not listing this as an answer. – Joshua Bell Feb 13 '17 at 17:37
  • Btw, FWIW, @JoshuaBell, regarding the mistaken non-failure of Promise, WeakMap, and WeakSet in Chrome (step 18 of the SCA), I reported https://bugs.chromium.org/p/chromium/issues/detail?id=698564 (but it got classified as spam). – Brett Zamir Mar 05 '17 at 09:03
  • And I just added mention in the comment of the non-failure of `Object.prototype` which as a (non-array) exotic object appears it ought to fail too. – Brett Zamir Mar 05 '17 at 09:26
  • Thanks for the ping. I'll try to de-spam the issue (I at least reported it as a false-positive) – Joshua Bell Mar 06 '17 at 17:45
  • Possible duplicate of [Why is this configurable property not deletable?](http://stackoverflow.com/questions/29044212/why-is-this-configurable-property-not-deletable) – Paul Sweatte May 17 '17 at 16:35
  • 1
    @PaulSweatte : This is actually quite different from that topic. Yes, both threads impinge on the distinct behavior of what were (formerly) termed "host objects" (though here it's more looking at the HTML definition of "platform objects", they appear to be very closely related). However, my question is also asking for practical information related to better detection of those specific non-built-in or otherwise exotic objects for purposes of the structured cloning algorithm. Please also read the comments above by the IndexedDB spec editor re: the practical nature of the question. – Brett Zamir May 17 '17 at 22:03

0 Answers0