Shadow Page Table

Getting V8 code coverage

Below is how I implemented Fuzzillis’ coverage mechanism in Rust.

 1const SHM_SIZE: usize = 0x200000;  
 2    let res = shm_open("test", OFlag::O_CREAT | OFlag::O_RDWR, Mode::S_IRWXU).unwrap();  
 3    ftruncate(res, SHM_SIZE.try_into().unwrap()).unwrap();  
 4    let non_zero = NonZeroUsize::new(SHM_SIZE).unwrap();  
 5    let shared = unsafe {  
 6        mmap(  
 7            None,  
 8            non_zero,  
 9            ProtFlags::PROT_READ | ProtFlags::PROT_WRITE,  
10            MapFlags::MAP_SHARED,  
11            res,  
12            0,  
13        )  
14        .unwrap()  
15    };  

First I create a shared memory called test. I then call ftruncate to set its size. I then map it into my processes using mmap. I then spawn d8 built with Fuzzilli enabled. Built with the following commands:

gn gen out/fuzzilli --args='is_debug=false dcheck_always_on=true v8_static_library=true v8_enable_verify_heap=true v8_fuzzilli=true sanitizer_coverage_flags="trace-pc-guard" target_cpu="x64"'  
ninja -C ./out/fuzzilli/ d8  

Finally, I spawn a d8 process while exporting the same name of the shared memory in this case test.

export SHM_ID=test && ./d8  

You should see something like this.

img

On the rust side, I have it waiting for user input. So that it reads coverage only after d8 is started.

 1let mut guess = String::new();  
 2    io::stdin().read_line(&mut guess).expect("Failed to read line");  
 3    
 4    let shmem: *mut ShmemData = shared as *mut ShmemData;  
 5  
 6    unsafe {  
 7        if !shmem.is_null() {  
 8            println!("No edges {:#?}", (*shmem).num_edges);   
 9            println!("Raw cov: ");  
10            let mut j = 0;  
11            for i in ((*shmem).edges) {  
12                print!("{}",i );  
13  
14                if j > 40 {  
15                    break  
16                }  
17  
18                j+=1;  
19            }  
20            println!("");  
21        } else {  
22            println!("Shmem is null!");  
23        }  
24    }  

Once d8 is initialized, I just have to run some JavaScript and then get coverage. The above code prints the total number of edges and the first 40 bytes of the coverage bitmap.

img

On the v8 side

Coverage is implemented using the v8/src/fuzzilli/cov.cc code. First, it opens a shared memory region. It then sets the number of edges to

shmem->num_edges = static_cast<uint32_t>(stop - start);  

Where stop - start are the parameters provided to

__sanitizer_cov_trace_pc_guard_init  

This is a special function that is called by instrumented code compiled with LLVM. You can learn more about this feature here. The instrumentation also calls __sanitizer_cov_trace_pc_guard which keeps track of which edges have reached by updating the shared memory.

shmem->edges[index / 8] |= 1 << (index % 8);  

The above code sets the bit in shmem->edges to true corresponding with which edge is visited. The entire source of the code is

 1extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t* guard) {  
 2  // There's a small race condition here: if this function executes in two  
 3  // threads for the same edge at the same time, the first thread might disable  
 4  // the edge (by setting the guard to zero) before the second thread fetches  
 5  // the guard value (and thus the index). However, our instrumentation ignores  
 6  // the first edge (see libcoverage.c) and so the race is unproblematic.  
 7  uint32_t index = *guard;  
 8  shmem->edges[index / 8] |= 1 << (index % 8);  
 9  *guard = 0;  
10}  

Guard values are values the compiler inserts into every basic block. A basic block is a piece of code that doesn’t change the control flow. The following is an example of how a guard value might be used.

1uint32_t guard = UNIQUE_ID;   
2__sanitizer_cov_trace_pc_guard(&guard); i  
3f (x > 0) {   
4	doSomething();   
5}  

Talking to V8 from the fuzzing engine

First I spawn a d8 a process. D8 is the v8 shell.

1let mut fuzzilli = Command::new("/home/ubuntu/v8/out/fuzzilli/d8")  
2	.env("SHM_ID", "test")  
3	.stdin(std::process::Stdio::piped()) // Enable writing to stdin  
4	.stdout(std::process::Stdio::piped()) // Enable reading from stdout  
5	.spawn()  
6	.expect("couldn't spawn d8 shell");  

Then I take both stdin and stdout and create a buffered reader to read lines from stdout.

1let mut child_stdin = fuzzilli.stdin.take().expect("Failed to open stdin");  
2let stdout = fuzzilli.stdout.take().expect("Failed to open stdout");  
3let mut reader = BufReader::new(stdout).lines();  

I’m now able to read and write to d8!

 1// Write to stdin and read from stdout multiple times  
 2for i in 1..=3 {  
 3	let input = "var x = 1;\n".to_string();  
 4	child_stdin.write_all(input.as_bytes()).await.unwrap();  
 5	println!("Sent: {}", input.trim());  
 6  
 7	if let Some(output) = reader.next_line().await.unwrap() {  
 8		println!("Received: {}", output);  
 9	}  
10}  

It lives!

img

Implementing evaluate_edges

Now that I’m able to execute arbitrary javascript. Let’s implement a coverage function that detects whether a javascript execution reached new edges or not. I’ll be implementing the algorithms found in fuzzilli/Sources/libcoverage/coverage.c First I define the coverage struct:

 1pub struct CovContext {  
 2    id: u8,  
 3    should_track_edges: bool,  
 4    virgin_bits: Box<[u8]>,  
 5    crash_bits: Box<[u8]>,  
 6    num_edges: u32,  
 7    bitmap_size: u32,  
 8    found_edges: u32,  
 9    shmem_data: *mut ShmemData,  
10    edge_count: Option<Box<[u32]>>,  
11}  

To keep things simple I did not implement edge tracking. After d8 starts and receives output from the child process. I initialize the coverage struct:

 1pub fn cov_finish_evaluation(shmem: *mut ShmemData) -> CovContext {  
 2    let mut num_edges = unsafe { (*shmem).num_edges };  
 3    if num_edges == 0 {  
 4        panic!("Couldn't get the number of edges");  
 5    }  
 6  
 7    num_edges += 1;  
 8  
 9    if num_edges > MAX_EDGES.try_into().unwrap() {  
10        panic!("[LibCoverage] Too many edges\n");  
11    }  
12  
13    let mut bitmap_size = (num_edges + 7) / 8;  
14    bitmap_size += 7 - ((bitmap_size - 1) % 8);  
15  
16    let mut virgin_bits = vec![0xff; bitmap_size.try_into().unwrap()];  
17    let mut crash_bits = vec![0xff; bitmap_size.try_into().unwrap()];  
18  
19    clear_edge(&mut virgin_bits, 0);  
20    clear_edge(&mut crash_bits, 0);  
21  
22    CovContext {  
23        id: 0,  
24        should_track_edges: false,  
25        virgin_bits: virgin_bits.into_boxed_slice(),  
26        crash_bits: crash_bits.into_boxed_slice(),  
27        num_edges: num_edges,  
28        bitmap_size: bitmap_size,  
29        found_edges: 0,  
30        shmem_data: shmem,  
31        edge_count: None,  
32    }  
33}  

I implemented the internal_evaluate which checks if the last execution gained new coverage.

 1fn internal_evaluate(  
 2    context: &mut CovContext,  
 3    virgin_bits: Box<[u8]>,  
 4    new_edges: &mut NewEdges,  
 5) -> u64 {  
 6    // SAFETY: Assuming the shmem_data is valid and properly aligned  
 7    let edges = unsafe {  
 8        std::slice::from_raw_parts(  
 9            context.shmem_data as *const u64,  
10            (context.bitmap_size as usize) / 8,  
11        )  
12    };  
13  
14    new_edges.count = 0;  
15    new_edges.edge_indices.clear();  
16  
17    // Collect edges to be cleared in a temporary buffer to avoid borrowing conflicts  
18    let mut indices_to_clear = Vec::new();  
19  
20    // First Pass: Immutable Read  
21    for (offset, (&current_chunk, virgin_chunk)) in edges  
22        .iter()  
23        .zip(virgin_bits.chunks_exact(8))  
24        .enumerate()  
25    {  
26        if current_chunk != 0  
27            && (current_chunk & u64::from_ne_bytes(virgin_chunk.try_into().unwrap())) != 0  
28        {  
29            let index = (offset as u32) * 64;  
30  
31            for i in index..index + 64 {  
32                if edge(context.virgin_bits.as_ref(), i.try_into().unwrap()) == 1  
33                    && edge(virgin_bits.as_ref(), i.try_into().unwrap()) == 1  
34                {  
35                    indices_to_clear.push(i);  
36                }  
37            }  
38        }  
39    }  
40  
41    // Second Pass: Mutable Write  
42    for &i in &indices_to_clear {  
43        clear_edge(context.virgin_bits.as_mut(), i.try_into().unwrap());  
44        new_edges.count += 1;  
45        new_edges.edge_indices.push(i as u64);  
46    }  
47  
48    new_edges.count  
49}  

It works by iterating over the coverage map and comparing the previous bitmap and the current map. If there is a difference we’ve found a new edge. Finally, I implemented cov_evaluate which is a wrapper around internal_evaluate.

 1pub fn cov_evaluate(context: &mut CovContext) -> bool {  
 2    let mut new_edges = NewEdges {  
 3        count: 0,  
 4        edge_indices: Vec::new(),  
 5    };  
 6  
 7    let num_new_edges: u32 = internal_evaluate(context, context.virgin_bits.clone(), &mut new_edges).try_into().unwrap();  
 8    // TODO found_edges should also include crash bits  
 9    context.found_edges += num_new_edges;  
10    return num_new_edges > 0;  
11}  

Here’s it all in action!

img

First, I set the variable x to one which increases coverage. Then I just add one to it which doesn’t change coverage.