JavaScript al dente. Fuzzing JS engines with Fuzzilli

Date: 14/03/2025

Hey guys! Today, pasta is on the menu! You will learn how to identify vulnerabilities in JavaScript engines using the Fuzzilli fuzzer. After a brief theoretical introduction, you’ll jump directly to practice. Let’s assemble the required tools and start fuzzing.

In the past, it was really difficult to fuzz JavaScript engines (those that enable you to add Snowfall to your browser or develop Node.js backends). Mutations in JS code caused syntax errors, which slowed down the process significantly. Samples were discarded by the engine, and you had to generate new ones. Over time, grammar-based fuzzers came to help, but they are not easy-to-use either.

In 2019, security researcher saelo unveiled his project – Fuzzilli. The idea of this fuzzer was to generate bytecode-like output (instead of JavaScript) that would be easier to mutate. The fuzzer name refers to a pasta variety, but, in fact, it originates from FuzzIL (Fuzzing Intermediate Language).

Fuzzilli
Fuzzilli

Test system

First of all, you’ll need a Linux virtual machine. A ready-made VM with your favorite distribution can be downloaded from osboxes.org. In my tests, I will use Ubuntu 22.

The more resources you allocate to the test VM, the better. The fuzzer shows code coverage, and, depending on the computation power, fuzzing the entire JS engine can take from a single day to several weeks.

The accessory tools include Git, the Swift programming language (not to be confused with the songstress), and the toolchain required to build JS engines (to be addressed in more detail later).

Start the VM and enter your password.

Password=osboxes.org
echo $Password | sudo -S apt update
sudo apt upgrade -y
Update & upgrade
Update & upgrade

Fuzzers

Fuzzing is a testing technique: incorrect, unexpected, or randomized data are fed to a software product as input, and the fuzzer monitors it for crashes, assertion triggers, and memory leaks.

An important fuzzing parameter is code coverage. In essence, this is the proportion of program code covered in the course of a certain set of tests.

Fuzzing can be either ‘dumb’ (or unstructured) or ‘smart’ (i.e. structured). If the fuzzer has no idea about the structure of input data expected by the tested program, this is dumb fuzzing. If it knows this structure, this is smart fuzzing.

Input data can be created using generation or mutation. Generation creates data from scratch; while mutation modifies existing data.

Depending on your knowledge about the source code, fuzzers can be divided into black-box and white-box ones.

Black-box fuzzing implies that you have no information about the program structure; accordingly, the fuzzer creates randomized input data.

White-box fuzzing involves program analysis with the purpose to improve code coverage (e.g. symbolic execution to bypass various parts of the program). However, program analysis takes longer than black-box fuzzing.

In addition, the tested ‘box’ can be gray. In such a case, code instrumentation is used instead of program analysis. This makes it possible to collect information about the program without analysis (i.e. something between a white and a black box). Input data are generated faster, and concurrently you receive code coverage information.

Fuzzilli belongs to the third type. In terms of input data creation, it combines generation and mutation. In terms of fuzzing technology, it’s rather ‘smart’.

JavaScript engines

The main components of a JavaScript engine are parser, interpreter, and compiler.

JS pipeline
JS pipeline

First, JavaScript source code is parsed. An abstract syntax tree (AST) is built, and bytecode is created on its basis. Then the interpreter executes the bytecode.

During the execution, various information is recorded (data profiling) and subsequently used when the bytecode is compiled into machine code. This operation is performed by the compiler.

Machine code is generated in situations when some code section is used frequently (e.g. a function is executed in a loop). Usually, it makes sense to spend time on its compilation and subsequently gain in execution time (since interpretation is always slower).

Typically, several compilers are used, and the optimal one is selected depending on the code optimization level.

www

More information on JS engines is available in the presentation “JavaScript engines: The Good Parts” (PDF, WebArchive).

Preparing fuzzer

Fuzzilli is available in the form of source code written in Swift.

To download the source code, you’ll need Git; to build Fuzzilli, GCC and Binutils packages (and, of course, the fuzzer source code). Install the dependencies and clone the Fuzzilli repository.

Password=osboxes.org
echo $Password | sudo -S apt update
sudo apt install git binutils gcc -y
git clone https://github.com/googleprojectzero/fuzzilli
Cloning repository
Cloning repository

Now go to the Swift website and look for a release suitable for your distribution in the Download section. For Ubuntu 22, download the release Ubuntu 22.04 x86_64.

Swift
Swift

Unpack the archive and copy the usr folder to install Swift. After that, make sure everything is configured correctly.

Below is a mini-script for lazy hackers. If you are reading this article many years later, replace the SwiftUrl variable with the respective URL from the Swift page.

# Installing Swift
Password=osboxes.org
SwiftUrl=https://download.swift.org/swift-5.8.1-release/ubuntu2204/swift-5.8.1-RELEASE/swift-5.8.1-RELEASE-ubuntu22.04.tar.gz
SwiftTar=$(echo $SwiftUrl | sed 's:.*/::')
SwiftFolder=${SwiftTar%.tar.gz}
# Go to HOME directory
cd $HOME
# Download archive
wget $SwiftUrl
# Extract
tar -xzf $SwiftTar
# Install
echo $Password | sudo -S cp -r $SwiftFolder/usr /
# Delete archive
rm $SwiftTar
# Delete folder
rm -rf $SwiftFolder
# Test run
swift --version
Installing Swift
Installing Swift

Now go to the Fuzzilli folder and build the fuzzer.

cd fuzzilli && swift build -c release
Building Fuzzilli
Building Fuzzilli

The fuzzer is ready. You can read its Help section if necessary.

swift run -c release FuzzilliCli --help

Time to prepare JS engines. The instruction on the repository main page says: “Download the source code for one of the supported JavaScript engines. See the Targets/ directory for the list of supported JavaScript engines”. For each engine, there is a separate folder containing an instruction that specifies how to build this particular engine for fuzzing.

Building and fuzzing V8

Theory

Let’s start with the Google Chrome browser engine called V8. It’s a JavaScript and WebAssembly open-source engine developed by Google and written in C++. It’s used in Chrome, Node.js, and numerous Chrome derivatives.

This engine was developed in Aarhus, Denmark, and its lead developer is Lars Bak. Bak was also involved in the development of the Self language and HotSpot Java virtual machine. Therefore, many of the Self developments migrated to V8 (e.g. JIT compilation and object maps).

www

For more detail, see the paper that formulates the basis of Self: “An Efficient Implementation of SELF, a Dynamically-Typed Object-Oriented Language Based on Prototypes” (PDF, WebArchive).

The engine includes the following components: Ignition interpreter, non-optimizing JavaScript compiler, and TurboFan optimizing compiler.

V8 pipeline
V8 pipeline

Building V8

The explanatory note in the Targets/V8 folder suggests to follow the instructions provided at https://v8.dev/docs/build. But I compiled all the required steps into a mini-script.

# Preparing and building V8
Password=osboxes.org
echo $Password | sudo -S apt update
# Go to HOME directory
cd $HOME
# Download depot_tools repository
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
# Add to PATH
echo "PATH=$HOME/depot_tools:$PATH" >> ~/.bashrc
source ~/.bashrc
# Download source code
gclient
fetch v8
cd v8/
gclient sync
# Install build dependencies (in fact, update since the script asks to enter password)
echo $Password | sudo -S apt update
./build/install-build-deps.sh
# Build V8 using script in the fuzzer folder
$HOME/fuzzilli/Targets/V8/fuzzbuild.sh
Building V8
Building V8

The script goes to the user home directory, installs depot_tools (to download and build V8), and downloads and compiles the V8 source code. This process can take some time depending on your PC’s horsepower.

Fuzzing

To start fuzzing, run the following command:

# Run Fuzzilli
# Disable core dumps
echo $Password | sudo -S sysctl -w 'kernel.core_pattern=|/bin/false'
# Go to Fuzzilli folder
cd $HOME/fuzzilli
# Runs
swift run -c release FuzzilliCli --profile=v8 --resume --storagePath=$HOME/fuzzilli-storage-v8 $HOME/v8/out/fuzzbuild/d8
Fuzzing V8
Fuzzing V8

The main settings are as follows: engine profile (--profile=v8), resume the previous fuzzing session (--resume), and path to the storage where the fuzzer will save its data (--storagePath): found crashes, sample corpus, and other information.

Fuzzing statistics will be displayed on a regular basis. The most interesting parameters are Crashes Found and Coverage.

Fuzzer statistics
Fuzzer statistics

Building and fuzzing SpiderMonkey

Theory

Let’s move on to Firefox JavaScript engine.

SpiderMonkey originates from the world’s first JavaScript engine released back in 1995. Originally developed by Brendan Eich at Netscape, its first versions were written in C, but later the code was rewritten in C++.

The SpiderMonkey structure is shown below. The parser produces bytecode. The JavaScript interpreter executes this bytecode. The baseline interpreter creates inline cache. The baseline compiler creates non-optimized machine code, and WarpMonkey creates optimized machine code.

SpiderMonkey pipeline
SpiderMonkey pipeline

Building SpiderMonkey

The instruction suggests to clone the Gecko repository and run the fuzzbuild.sh script.

But in fact, you have to apply patches from the Patches folder first – and only then build the engine. Also, fuzzbuild should be run not from js/src, but from the gecko-dev root.

To build the engine, you’ll need curl and the Rust compiler.

# Preparing and building SpiderMonkey
Password=osboxes.org
cd $HOME
# Install curl
echo $Password | sudo -S apt install curl -y
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o install.sh
sh ./install.sh -y
source "$HOME/.cargo/env"
# Clone repository
git clone https://github.com/mozilla/gecko-dev.git
# Go to folder
cd gecko-dev/js/src
# Apply patch
git apply $HOME/fuzzilli/Targets/Spidermonkey/Patches/*
cd $HOME/gecko-dev
# Build
$HOME/fuzzilli/Targets/Spidermonkey/fuzzbuild.sh
Rust installed
Rust installed
Building SpiderMonkey
Building SpiderMonkey

Fuzzing

After building the engine, you can start fuzzing it from the fuzzilli folder. The startup commands and fuzzer parameters are the same, except for the profile and executable file.

# Starting Fuzzilli
# Disable core dumps
echo $Password | sudo -S sysctl -w 'kernel.core_pattern=|/bin/false'
# Go to Fuzzilli folder
cd $HOME/fuzzilli
# Run
swift run -c release FuzzilliCli --profile=spidermonkey --resume --storagePath=$HOME/fuzzilli-storage-sm $HOME/gecko-dev/obj-fuzzbuild/dist/bin/js
Fuzzing SpiderMonkey
Fuzzing SpiderMonkey

Building and fuzzing JavaScriptCore

And finally, the JavaScriptCore engine that powers the Safari browser.

Theory

JavaScriptCore originates from the KJS JavaScript engine used in the Konqueror browser, which is part of KDE. The WebKit project started in 2001 as a fork of KHTML and KJS.

It features the most complex four-level pipeline:

  • LLInt (Low Level Interpreter) interprets bytecode generated based on the JavaScript source code;
  • Then slightly optimizing Baseline compiler goes into play. Both of them collect information required for further machine code optimizations.
  • The next component is DFG JIT (Data Flow Graph Just In Time). It enhances machine code optimization; and 
  • Finally, FTL JIT (Faster Than Light) generates the most optimized machine code.
KJS Pipeline
KJS Pipeline

Building JavaScriptCore

Let’s see what the instruction in the Targets folder says about JavaScriptCore.

You have to clone the code from the WebKit mirror, apply patches, and run the fuzzbuild.sh script. Also, you need to install Clang and dependencies. The Tools folder contains ready-made scripts for this. Download and install all the required stuff, apply patches, and build the engine. As usual, below is the script:

# Preparing and building JavaScriptCore
Password=osboxes.org
cd $HOME
# Download source code
git clone https://github.com/WebKit/WebKit.git
# Install dependencies
cd WebKit
echo $Password | sudo -S apt update
sudo apt install clang -y
Tools/gtk/install-dependencies
# Apply patches
git apply ../fuzzilli/Targets/JavaScriptCore/Patches/*
# Build JSC
$HOME/fuzzilli/Targets/JavaScriptCore/fuzzbuild.sh
Building JSC
Building JSC

Fuzzing

After building the engine, you can proceed to fuzzing.

# Running Fuzzilli
# Disable core dumps
echo $Password | sudo -S sysctl -w 'kernel.core_pattern=|/bin/false'
# Go to Fuzzilli folder
cd $HOME/fuzzilli
# Start fuzzing
swift run -c release FuzzilliCli --profile=jsc --resume --storagePath=$HOME/fuzzilli-storage-jsc $HOME/WebKit/FuzzBuild/Debug/bin/jsc
Fuzzing JavaScriptCore
Fuzzing JavaScriptCore

Conclusions

Now you are familiar with the fuzzing theory and have a fuzzing platform suitable for the three main JavaScript engines. However, Fuzzilli supports other engines as well (just check the Targets folder), including:

  • JerryScript;
  • QuickJS;
  • Qt QJSEngine;
  • XS; and 
  • duktape.

Your newly acquired knowledge and skills enable you to fuzz them on your own.

Good luck!

Useful links


Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>