The length of array arr is 4, and we are returning an element of this array. V8 will perform run-time bounds checking to make sure that the last statement does not access memory outside the bounds of the array. During optimization of such a function, V8 might remove array bounds checking if it concluded that typer_index is always zero (or, in general, if typer_index * 10 is provably always inside the bounds of the array). This saves a few more CPU cycles during execution of the optimized function. In the event that JITted code produces an erroneous numeric result, though, it may be possible fool the V8 engine into thinking typer_index must be zero, while in actuality it will be set to a different (erroneous) value. Then, when the array access is performed, it will trigger an out-of-bounds memory access.

This method was so successful that the V8 developers eventually decided to remove array-bounds-check elimination. See this blog for more information about this exploitation technique, as well as this blog for further discussion.

Since V8 mitigated the array bounds elimination exploitation technique, a new technique is necessary. At Pwn2Own, the contestants used a technique that produces out-of-bounds access via ArrayPrototypePop and ArrayPrototypeShift. I was able to trace this method back to late 2020 by searching the Chromium bug tracking system. It was mitigated a week after the Pwn2Own competition by adding a new CheckBounds node. Here I provide you with a quick analysis of this method:

When a function undergoing optimization has calls to the Array.shift method, the execution flow eventually reaches the function JSCallReducer::ReduceArrayPrototypeShift function (see src/compiler/js-call-reducer.cc). Since a call to the built-in shift JavaScript method is relatively slow, the optimizer replaces the call with a series of operations that can be performed at the assembly level. As you may know, “Array.shift” removes the first element from an array and returns that removed element. After removing that element, the JIT-produced code computes the new array length by subtracting 1 from the original array length: