Exploit-DB #46646
Description: SpiderMonkey - IonMonkey Compiled Code Fails to Update Inferred Property Types (Type Confusion)
Type: dos
Platform: multiple
Date: 2019-04-03
Author: Google Security Research

Exploit:
A bug in IonMonkey leaves type inference information inconsistent, which in turn allows the compilation of JITed functions that cause type confusions between arbitrary objects.

# Prerequisites

In Spidermonkey, every JavaScript objects is an instance of the JSObject class [1]. Plain JavaScript objects (e.g. ones created through an object literal) are typically instances of the NativeObject [2] class. A NativeObject is basically:

* An ObjectGroup [3] which stores things like the prototype and type information for properties (see below)
* The Shape [4] of the object which indicates the location of properties. A Shape could e.g. tell that property .p is stored in the 2nd property slot
* Property storage [5]: a dynamically sized array in which the property values are stored. The Shapes provide indices into this array
* Element storage [6]: a dynamically sized array in which elements (properties with an integer key) are stored

Spidermonky makes use of type inference to perform various optimizations in the JIT. Specifically, type inference is used to predict the types of object properties and then omit runtime type checks for them. Such a type inference system for property values is only safe as long as every property store to an object validates that the type of the new value is consistent with the existing type information or, if not, updates ("widens") the inferred type. In Spidermonkey's interpreter this is done in e.g. AddOrChangeProperty [7]. In the JIT compiler (IonMonkey), this is done through "type barriers" [8]: small runtime type checks that ensure the written value is consistent with what is stored as inferred type and otherwise bail out from the JITed code.

# Crashing Testcase

The following program, found through fuzzing and then manually modified, crashes Spidermonkey with an assertion that verifies that type inference data is consistent with the actual values stored as properties:

    function hax(o, changeProto) {
        if (changeProto) {
            o.p = 42;
            o.__proto__ = {};
        }
        o.p = 13.37;
        return o;
    }

    for (let i = 0; i < 1000; i++) {
        hax({}, false);
    }

    for (let i = 0; i < 10000; i++) {
        let o = hax({}, true);
        eval('o.p'); 			// Crash here
    }


Crashes in debug builds of Spidermonkey with:

    Assertion failure: [infer failure] Missing type in object [Object * 0x327f2ca0aca0] p: float, at js/src/vm/TypeInference.cpp:265
    Hit MOZ_CRASH() at js/src/vm/TypeInference.cpp:266

This assertion expresses that type inference data is inconsistent for the property .p as the type "float" is not in the list of possible types but the property currently holds a float value.

# Bug Analysis

In essence it appears that IonMonkey fails to realize that the ObjectGroup of the object `o` can change throughout the function (specifically during the prototype change) and thus incorrectly omits a type barrier for the second property assignment, leading to inconsistent type inference information after the property assignment.

In detail, the following appears to be happening:

The first loop runs and allocates NativeObjects with ObjectGroup OG1 and Shape S1. After some iterations the function hax is JIT compiled. At that point, the compiled code will expect to be called with an object of ObjectGroup OG1 as input. OG1 will have inferred types {.p: [float]} because the body of the if condition was never executed and so property .p was never set to a non-float value.

Then the second loop starts running, which will allocate objects using a new ObjectGroup, OG2 (I'm not exactly sure why it's a new one here, most likely some kind of heuristic) but still using Shape S1. As such, the compiled code for hax will be invalidated [9]. Then, during the first invocation of hax with changeProto == true, a new prototype will be set for o, which will

1. cause a new ObjectGroup to be allocated for O (because prototypes are stored in the object group) and
2. cause the previous object group (OG2) to discard any inferred types and set the state of inferred properties to unknown [10]. An ObjectGroup with unknownProperties is then never again used for type inference of properties [11].

At a later point in the loop, the function is recompiled, but this time it is compiled to expect an object of ObjectGroup OG1 or OG2 as input. The JIT compiled code for hax will now look something like this (pseudocode):

    // Verify that the input is an object with ObjectGroup OG1 or OG2 (actually
    // this check is performed before entering the JITed code)
    VerifyInputTypes

    if (changeProto) {
        // A SetProperty [12] inline cache [13] which will perform the actual
        // property store and speed up subsequent property stores on objects of
        // the same Shape and Group. Since a type barrier is required, the Group
        // is used as an additional index into the cache so that both Shape and
        // Group must match, in which case no inferred types could be
        // accidentially invalidated.
        SetPropertyICWithTypeBarrier o.p 42

        Call ChangePrototype(o, {})
    }

    // Another inline cache to store property .p again, but this time without a
    // type barrier. As such, only the Shape will be checked and not the Group.
    SetPropertyIC o.p 13.37

    Return o

After compilation finishes, the following happens in the first invocation of the JITed code:

* The function is called with an object of ObjectGroup OG2 and Shape S1
* The property .p is stored on the object in the first SetProperty cache. This does not update any inferred type as OG2 does not use inferred types
* The prototype of o is changed
    * This again causes a new ObjectGroup, OG3, to be allocated
    * When creating the new group, property types are inferred from the current object (this is possible because it is the only object using the new group) [