🔗 JSC Indexing Type Exercise

🔗 Description

In JavaScriptCore, JSObjects and JSArrays have an "element array" as part of their butterfly. In general these elements are NaNBoxed JSValues.

However, there are times when arrays only contain doubles or integers. The engine wants to speed up accesses to these kinds of "homogenous" arrays. To do this, the JSCell of an object has an "indexing type" field specifying the "type" of elements in the array.

For example the indexing type may be ArrayWithDouble which implies that all array elements are doubles. The engine can then avoid the overhead of checking NaNBoxing encoding and just read/write the doubles directly in native format.

This will become very useful as we start to build exploit primitives.

🔗 Steps

Run JSC under GDB:

exercise run jsc --gdb
  • Create several different arrays with different types of elements. For example make one that only contains doubles, one that only contains integers, and one that contains a mix of types including references.

🔗 Printing the Indexing Type

  • For each array, run print(describe(arr)). The indexing type will be listed right after the list of property indexes.

For example:

JavaScript
>>> print(describe([1.1,1.1])) Object: 0x7f4810bbc200 with butterfly 0x7f30163e0010 (Structure 0x7f4810bfe450: [Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7f4810bbc010, Leaf]), StructureID: 18855

The CopyOnWriteArrayWithDouble is the indexing type. We mostly care about the WithDouble part. This implies all elements in the array are doubles, which matches our expectations.

🔗 Dumping Array Memory

  • Now for each array, get the address of its butterfly and dump the element array memory. Compare how the actual memory is encoded compared to normal NaNBoxed JSValues.

  • Specifically compare the memory of these two arrays: [1.1,1.1,1.1] and [1.1,1.1,{}]. The first should be ArrayWithDouble and the second should be ArrayWithContiguous.

Since ArrayWithDouble tells the engine it can access the values without NaNBoxing, we can expect that the values are not NaNBox encoded.

  • How does the memory encoding differ for the 1.1 doubles in the ArrayWithDouble vs the ArrayWithContiguous?
  Reveal Answer

In ArrayWithContiguous all elements are NaNBoxed JSValues. We should see memory which matches the encoding we saw in the NaNBoxing exercise, so 1.1s are stored as the NaNBoxed value 0x3ff299999999999a.

However, ArrayWithDouble tells the engine it can access the values without NaNBoxing, so we can expect that the values will not be NaNBox encoded. We can see that the 1.1s are stored as the expected double value 0x3ff199999999999a.

  • What happens if you have empty spaces (aka "holes") in your array? For example: a = [{},1,2]; a[4] = 4. What about when you are using ArrayWithDouble? For example: a = [1.1,1.1,1.1]; a[4] = 1.1
  Reveal Answer

"Holes" in element arrays are represented differently depending on the indexing type. For ArrayWithContiguous, holes are represented as a null qword. However for ArrayWithDouble, there is a special value 0x7ff8000000000000 (NaN) for holes.

  • What happens if you set a very large element index? For example: a = [0,1,2]; a[0x100000] = 4.
  Reveal Answer
JavaScript
>>> a = [0,1,2]; a[0x100000] = 4 >>> describe(a) Object: 0x7f4810bbc220 with butterfly 0x7f30163dc038 (Structure 0x7f4810bfe300:[Array, {}, ArrayWithArrayStorage, Proto:0x7f4810bbc010, Leaf]), StructureID: 58626

Objects with very "sparse" elements will be converted to an indexing type of ArrayWithArrayStorage. Rather than being a contiguous linear array, the object will now use a "sparse array" data structure to conserve memory.