If we dive into the ruby source code, we'll find a function named rb_ary_splice called when array assignment happens with three arguments (i.e. index, length, and new value):
static VALUE
rb_ary_aset(int argc, VALUE *argv, VALUE ary)
{
    long offset, beg, len;
    if (argc == 3) {
        rb_ary_modify_check(ary);
        beg = NUM2LONG(argv[0]);
        len = NUM2LONG(argv[1]);
        rb_ary_splice(ary, beg, len, argv[2]);
        return argv[2];
    }
[...]
And if we follow along in rb_ary_splice we'll happen upon where the magic happens:
static void
rb_ary_splice(VALUE ary, long beg, long len, VALUE rpl)
{
    long rlen;
    long olen;
    if (len < 0) rb_raise(rb_eIndexError, "negative length (%ld)", len);
    olen = RARRAY_LEN(ary);
    [...]
        if (len != rlen) {
            RARRAY_PTR_USE(ary, ptr,
                   MEMMOVE(ptr + beg + rlen, ptr + beg + len,
                       VALUE, olen - (beg + len)));
            ARY_SET_LEN(ary, alen);
        }
        if (rlen > 0) {
            MEMMOVE(RARRAY_PTR(ary) + beg, RARRAY_CONST_PTR(rpl), VALUE, rlen);
        }
    }
    RB_GC_GUARD(rpl);
}
First it makes room in the array for the new elements and updates the length:
RARRAY_PTR_USE(ary, ptr,
    MEMMOVE(ptr + beg + rlen, ptr + beg + len,
        VALUE, olen - (beg + len)));
ARY_SET_LEN(ary, alen);
Then through the magic of C pointers, it inserts the new element(s):
MEMMOVE(RARRAY_PTR(ary) + beg, RARRAY_CONST_PTR(rpl), VALUE, rlen);