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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions