Tool Learning 2 - Migrate ChakraCore to Fuzzilli

Add an Extra Target to Fuzzilli

In addition to the existing targets in Fuzzilli's GitHub repository, we can add new targets and make Fuzzilli work with more JavaScript engines.

Migrate Fuzzilli to the ChakraCore Engine

ChakraCore is an open-source JavaScript engine with a C API. Microsoft Edge previously used this engine, although it was no longer used after March 2021. In this post, I migrate Fuzzilli to ChakraCore and see whether Fuzzilli can be easily adapted to other targets.

Clone the Code

Make sure clang/clang++ and llvm-dev are installed. clang/clang++ is needed for SanitizerCoverage (documentation), which provides edge coverage for each execution.

1
2
3
$ cd fuzzilli/Targets
$ git clone https://github.com/chakra-core/ChakraCore.git
$ cd ChakraCore

Code Modifications

Modify Makefile and Implement Code Coverage

First, change the Makefile. Add set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lrt") to bin/ch/CMakeLists.txt. Searching for the keyword shared library below in vim jumps directly to this location.

To avoid linking issues, I added the code in coverage.c under fuzzilli/Targets to the library in bin/ch/ch.cpp. I also added several header files at the beginning.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// SanitizerCoverage-based coverage collection code for libcoverage.
// Copy and paste this code into the JavaScript shell binary.
//
// BEGIN FUZZING CODE
//
/* Required includes for coverage tracking. */
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <string>

#define REPRL_CRFD 100
#define REPRL_CWFD 101
#define REPRL_DRFD 102
#define REPRL_DWFD 103

#define SHM_SIZE 0x100000
#define MAX_EDGES ((SHM_SIZE - 4) * 8)

//#define CHECK(cond) if (!(cond)) { fprintf(stderr, "\"" #cond "\" failed\n"); _exit(-1); }

#define CHECK(cond) \
if (!(cond)) { \
fprintf(stderr, "\"" #cond "\" failed\n"); \
_exit(-1); \
}

struct shmem_data {
uint32_t num_edges;
unsigned char edges[];
};

struct shmem_data* __shmem;

uint32_t *__edges_start, *__edges_stop;
void __sanitizer_cov_reset_edgeguards() {
uint64_t N = 0;
for (uint32_t *x = __edges_start; x < __edges_stop && N < MAX_EDGES; x++)
*x = ++N;
}

extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
// Avoid duplicate initialization
if (start == stop || *start)
return;

if (__edges_start != NULL || __edges_stop != NULL) {
fprintf(stderr, "Coverage instrumentation is only supported for a single module\n");
_exit(-1);
}

__edges_start = start;
__edges_stop = stop;

// Map the shared memory region
const char* shm_key = getenv("SHM_ID");
if (!shm_key) {
puts("[COV] no shared memory bitmap available, skipping");
__shmem = (struct shmem_data*) malloc(SHM_SIZE);
} else {
int fd = shm_open(shm_key, O_RDWR, S_IREAD | S_IWRITE);
if (fd <= -1) {
fprintf(stderr, "Failed to open shared memory region: %s\n", strerror(errno));
_exit(-1);
}

__shmem = (struct shmem_data*) mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (__shmem == MAP_FAILED) {
fprintf(stderr, "Failed to mmap shared memory region\n");
_exit(-1);
}
}

__sanitizer_cov_reset_edgeguards();

__shmem->num_edges = stop - start;
printf("[COV] edge counters initialized. Shared memory: %s with %u edges\n", shm_key, __shmem->num_edges);
}

extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// There's a small race condition here: if this function executes in two threads for the same
// edge at the same time, the first thread might disable the edge (by setting the guard to zero)
// before the second thread fetches the guard value (and thus the index). However, our
// instrumentation ignores the first edge (see libcoverage.c) and so the race is unproblematic.
uint32_t index = *guard;
// If this function is called before coverage instrumentation is properly initialized we want to return early.
if (!index) return;
__shmem->edges[index / 8] |= 1 << (index % 8);
*guard = 0;
}

//
// END FUZZING CODE
//

Add REPRL

After this, I followed the instructions here to add REPRL (Read-Eval-Print-Reset-Loop) code to ch.cpp. REPRL reduces the overhead of starting a new JavaScript engine instance, and this loop needs to be triggered by a unique command-line option. Make sure to include it as a processArgument in the profile.

For the implementation, I referred to several patches under the Targets folder. You can also search for grep -rn "HELO" * or grep -rn "char helo" * under fuzzilli/Targets to find implementation examples, but make sure all JavaScript engine source code has been cloned under that folder.

After doing this, my implementation is shown below. The file is located at ChakraCore/bin/ch/ch.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/* Run the fuzzilli run-eval-print-repeat loop, and exit at the end. */
if (options.m_reprl) {
/* REPRL: let parent know we are ready */
char helo[] = "HELO";
CHECK(write(REPRL_CWFD, helo, 4) == 4);//write "HELO" on REPRL_CWFD
CHECK(read(REPRL_CRFD, helo, 4) == 4); //read 4 bytes on REPRL_CRFD

if (memcmp(helo, "HELO", 4) != 0) {
fprintf(stderr, "[REPRL] Invalid response from parent\n");
exit(EXIT_FAILURE); //break if 4 read bytes do not equal "HELO"
}

do {//while true
// Keep indentation for easier diffing
unsigned action;
size_t script_size;
char *buffer;
CHECK(read(REPRL_CRFD, &action, 4) == 4); //read 4 bytes on REPRL_CRFD
if (action != 'cexe') {
fprintf(stderr, "[REPRL] Unknown action: %u\n", action);
exit(EXIT_FAILURE);//break if 4 read bytes do not equal "cexe"
} else{
//read 8 bytes on REPRL_CRFD, store as unsigned 64 bit integer size
CHECK(read(REPRL_CRFD, &script_size, 8) == 8);
}

buffer = (char *) malloc((sizeof(char) * (script_size + 1))); //allocate size+1 bytes

char *ptr = buffer; //ptr is buffer
size_t remaining = script_size;
while (remaining > 0) {
ssize_t rv = read(REPRL_DRFD, ptr, remaining);//read size bytes from REPRL_DRFD into allocated buffer
if (rv <= 0) {
fprintf(stderr, "Failed to load script\n");
_exit(-1);
}
remaining -= rv;
ptr += rv;
}

buffer[script_size] = '\0';
if (0 == (result = eval_buf(ctx, buffer, script_size, "reprl", 0))) {
js_std_loop(ctx); //Execute buffer as javascript code
} else {
fprintf(stderr, "Failed to eval_buf reprl\n");
}

js_free(ctx, buffer); //Store return value from JS execution

// In REPRL mode, stdout and stderr may be regular files, so we need to fflush them here.
fflush(stdout); //Flush stdout
fflush(stderr); //Flush stderr


int status = (result & 0xff) << 8; //Mask return value with 0xff and shift it left by 8
CHECK(write(REPRL_CWFD, &status, 4) == 4); //write that value over REPRL_CWFD
//Reset the Javascript engine
__sanitizer_cov_reset_edgeguards();//Call __sanitizer_cov_reset_edgeguards to reset coverage
} while (true);
}

Add Built-In Function

The next step is to add a built-in function that crashes directly, which helps Fuzzilli test whether it can catch crashes in the target engine. The target file is lib/Runtime/Library/GlobalObject.cpp. I will not include the code here. I packaged it as a patch named ch.patch and used the following commands to patch the directory:

1
2
$ cd Targets/ChakraCore
$ patch -p1 < ch.patch

Create Fuzzilli Profile

Create ChakraCoreProfile.swift. This file tells Fuzzilli how to fuzz the target engine. Put the profile file in FuzzilliCli/Profiles.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Fuzzilli
let ChakraCoreProfile = Profile(
processArguments: ["-ForceJITCFGCheck", "-MaxJitThreadCount:1", "-collectgarbage"],
// processEnv: [:],
processEnv: ["UBSAN_OPTIONS":"handle_segv=0"],
codePrefix: """
function main() {
""",
codeSuffix: """
CollectGarbage();
}
main();
""",
ecmaVersion: ECMAScriptVersion.es6,
crashTests: ["fuzzilli('FUZZILLI_CRASH', 0)", "fuzzilli('FUZZILLI_CRASH', 1)"],
additionalCodeGenerators:
WeightedList<CodeGenerator>([]),
additionalProgramTemplates:
WeightedList<ProgramTemplate>([]),
disabledCodeGenerators: [],
additionalBuiltins: ["CollectGarbage" : .function([] => .undefined)]
)

Add this profile to Profile.swift, located in Fuzzilli/Profiles/Profile.swift. My implementation is shown below.

Make sure to rebuild Fuzzilli after doing this:

1
2
$ cd fuzzilli
$ swift build -c release -Xlinker='-lrt'

Build succeeded!

Run Fuzzilli

Build ChakraCore first. The build instructions are available here. The executable file will be generated at out/Debug/bin/ch/ch. Install the dependencies for building ChakraCore first.

1
2
3
4
$ sudo apt-get update
$ sudo apt-get dist-upgrade
$ sudo apt-get install -y git build-essential cmake clang libicu-dev
$ sudo apt-get install -y libunwind8-dev #For version 1.X

If building with CMake:

1
2
3
4
5
6
7
$ cd fuzzilli/Targets/ChakraCore
$ export CFLAGS="-fsanitize-coverage=trace-pc-guard -O3 -fsanitize=address"
$ export CXXFLAGS="-fsanitize-coverage=trace-pc-guard -O3 -fsanitize=address"
$ ./build.sh --static -d -j --cc=/path/to/clang --cxx=/path/to/clang++
# Can specify --debug or --test-build to build.sh to select Debug or Test build flavors respectively. Default is Release build.
# Can specify --static to build ChakraCore as a static library.
# Also can run as ./build.sh --static -d --cxx=/usr/bin/clang++-10 --cc=/usr/bin/clang-10 -j

If building with Ninja:

1
2
3
$ sudo apt-get install -y ninja-build
$ ninja --version #Ensure ninjs has been installed correctly
$ ./build.sh -n #Specify the -n flag to build.sh to build with ninja

Let's try fuzzing!

1
2
3
$ cd fuzzilli/Targets/ChakraCore
$ mkdir fuzz_ch_out
$ swift run FuzzilliCli --jobs=2 --profile=chakracore --storagePath=fuzz_ch_out/ out/Debug/ch #or this "out/Debug/bin/ch/ch"

However, I ran into some problems here.

I tried to rebuild Fuzzilli and ChakraCore, but compilation failed again. To record the errors, use the following command:

1
$ script -f output.txt

After collecting the compilation output, press Ctrl + d to stop recording. I then fixed the issues and built the project successfully.