The fast-path in Array.prototype.copyWithin only validates the target range after a potential re-entrancy via end.valueOf(), but does not validate the source range. When end.valueOf() shrinks the array, the source indices start..start+count-1 extend past the reallocated buffer, causing a heap out-of-bounds read.
JerryScript revision
b706935
Build platform
macOS 26.2 (Darwin 25.2.0 arm64)
Build steps
python3 tools/build.py --clean
Test case
var N = 100;
var arr = new Array(N);
for (var i = 0; i < N; i++) arr[i] = 0xAA00 + i;
arr.copyWithin(0, 50, {
valueOf: function() {
arr.length = 60; // shrink buffer: 104→64 aligned ecma_value_t slots
return N; // end = original length → stale source range
}
});
// arr[0..9]: buffer[50..59] — in-bounds copy (normal)
// arr[10..13]: buffer[60..63] — ARRAY_HOLE padding (within aligned buffer)
// arr[14..49]: buffer[64..99] — OOB! Reading freed heap memory (36 slots)
for (var i = 0; i < 20; i++) {
print("arr[" + i + "] typeof=" + typeof arr[i]);
}
Output
arr[0] typeof=number
arr[1] typeof=number
arr[2] typeof=number
arr[3] typeof=number
arr[4] typeof=number
arr[5] typeof=number
arr[6] typeof=number
arr[7] typeof=number
arr[8] typeof=number
arr[9] typeof=number
arr[10] typeof=undefined
arr[11] typeof=undefined
arr[12] typeof=undefined
arr[13] typeof=undefined
arr[14] typeof=object
arr[15] typeof=number
arr[16] typeof=number
arr[17] typeof=number
arr[18] typeof=number
arr[19] typeof=number
Elements at indices 14–49 contain data read from beyond the allocated fast-array buffer — jmem_heap_free_t allocator metadata (free list next_offset and block size fields) and stale unzeroed heap data are leaked into the JavaScript-visible array.
arr[14] = jmem_heap_free_t.next_offset (typically 0xFFFFFFFF = end-of-list marker, decodes as typeof === "object")
arr[15] = jmem_heap_free_t.size (free block size in bytes, also decodes as typeof === "object")
arr[16..49] = stale data: original array values that persist in freed memory (allocator does not zero freed blocks)
Expected behavior
As it reduces array length to 60, the output array[10..20] should be undefined instead of heap metadata.
Root cause analysis
In ecma-builtin-array-prototype.c, the copyWithin implementation:
- Line ~2810: The dispatcher captures
len = 100 (the array length) before entering copyWithin. This value is passed as a parameter and never refreshed.
- Line ~2340:
end.valueOf() callback runs — user code sets arr.length = 60, which triggers jmem_heap_realloc_block to shrink the underlying fast-array buffer from 416 → 256 bytes (104 → 64 aligned slots). The freed tail (160 bytes) is returned to the jmem free list.
- Line ~2356:
count = min(end - start, len - target) = min(100 - 50, 100 - 0) = 50 — computed from stale len, not the actual post-shrink length.
- Line ~2377: Fast-path bounds check validates
target + count - 1 >= ext_obj_p->u.array.length. With target=0, count=50, actual_length=64: 0 + 50 - 1 = 49 < 64 — passes.
- No check on source:
start + count - 1 = 50 + 50 - 1 = 99 >= 64 — the source range overflows the buffer by 36 slots. There is no validation for this.
- Line ~2386:
ecma_copy_value_if_not_object(buffer_p[start + k]) reads 36 ecma_value_t slots (144 bytes) from freed heap memory.
The OOB read is constrained to the freed tail of the original buffer allocation (slots 64–99), which contains jmem_heap_free_t metadata followed by stale data. If the freed region is reused by another allocation before copyWithin completes, cross-object data would be leaked.
The fast-path in
Array.prototype.copyWithinonly validates the target range after a potential re-entrancy viaend.valueOf(), but does not validate the source range. Whenend.valueOf()shrinks the array, the source indicesstart..start+count-1extend past the reallocated buffer, causing a heap out-of-bounds read.JerryScript revision
b706935
Build platform
macOS 26.2 (Darwin 25.2.0 arm64)
Build steps
Test case
Output
Elements at indices 14–49 contain data read from beyond the allocated fast-array buffer —
jmem_heap_free_tallocator metadata (free listnext_offsetand blocksizefields) and stale unzeroed heap data are leaked into the JavaScript-visible array.arr[14]=jmem_heap_free_t.next_offset(typically0xFFFFFFFF= end-of-list marker, decodes astypeof === "object")arr[15]=jmem_heap_free_t.size(free block size in bytes, also decodes astypeof === "object")arr[16..49]= stale data: original array values that persist in freed memory (allocator does not zero freed blocks)Expected behavior
As it reduces array length to 60, the output array[10..20] should be
undefinedinstead of heap metadata.Root cause analysis
In
ecma-builtin-array-prototype.c, thecopyWithinimplementation:len = 100(the array length) before entering copyWithin. This value is passed as a parameter and never refreshed.end.valueOf()callback runs — user code setsarr.length = 60, which triggersjmem_heap_realloc_blockto shrink the underlying fast-array buffer from 416 → 256 bytes (104 → 64 aligned slots). The freed tail (160 bytes) is returned to thejmemfree list.count = min(end - start, len - target) = min(100 - 50, 100 - 0) = 50— computed from stalelen, not the actual post-shrink length.target + count - 1 >= ext_obj_p->u.array.length. Withtarget=0,count=50,actual_length=64:0 + 50 - 1 = 49 < 64— passes.start + count - 1 = 50 + 50 - 1 = 99 >= 64— the source range overflows the buffer by 36 slots. There is no validation for this.ecma_copy_value_if_not_object(buffer_p[start + k])reads 36ecma_value_tslots (144 bytes) from freed heap memory.The OOB read is constrained to the freed tail of the original buffer allocation (slots 64–99), which contains
jmem_heap_free_tmetadata followed by stale data. If the freed region is reused by another allocation before copyWithin completes, cross-object data would be leaked.