Story of an innocent Apple Safari copyWithin gone (way) outside [CVE-2023-38600]

We discussed this vulnerability during Episode 220 on 20 October 2023

An integer underflow vuln in Safari/WebKit, which as is typical with JSC bugs, is rooted in the ability for callbacks to change the state of objects. The root of the bug is that you can cause a copy on a zero-sized array to a destination index of something like 0x20, and when the JS engine tries to clamp the copy, results in a copy size of 0 - 0x20 = 0xffffffffffffffe0.

Vulnerability Explanation It’s easier to understand the root bug after understanding what the Proof of Concept does. The PoC first creates an ArrayBuffer with a length of 0x1000 elements and sets a max byte length of 0x4000. It’ll then create a Uint8Array that’s instantiated from that ArrayBuffer, and defines a callback that’ll resize the ArrayBuffer to have a length of 0. Finally, it calls copyWithin() on the Uint8Array with specially crafted arguments. copyWithin is a JS function that does a shallow copy of elements in an array to another area inside that same array without the length changing. It takes a to index, from index, and an optional end of where the copy should stop.

The PoC invokes copyWithin() with a destination to value of 0x20, but the from value is set to {valueOf: callback}. Internally in the JS engine, copyWithin() will result in genericTypedArrayViewProtoFuncCopyWithin() being called, which calls argumentClampedIndexFromStartOrEnd() on each argument. When processing the from value, the callback is executed and changes the ArrayBuffer length to zero (and ultimately results in from being evaluated as zero). The function is now in a state where the ArrayBuffer is zero-sized but the to index is 0x20. The developers did try to account for a scenario where the ArrayBuffer length changes, and so they clamp the copy length to ArrayBuffer.length minus to or from (whichever is higher). The problem is that if the length is set to zero like it is here, this will underflow, resulting in a length of 0xffffffffffffffe0 being passed to memmove().

Exploitability The post doesn’t comment on exploitability, but this bug seems like it would be difficult to exploit. There’s no obvious way of interrupting the memmove(), and with such a large length you’re bound to hit unmapped memory or corrupt critical data that isn’t feasible to recover from. You also don’t have real parallelism to take advantage of in JavaScript, so trying to race the copy and exploit before you crash doesn’t seem viable either.