Story of an innocent Apple Safari copyWithin gone (way) outside [CVE-2023-38600]
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.