-
Notifications
You must be signed in to change notification settings - Fork 36
Directed fuzzing for golang image (Go) project with sydr‐fuzz (LibAFL‐DiFuzz backend)
This article discusses a methodology for directed fuzzing of Rust code using the Sydr-Fuzz interface, which is built on top of the LibAFL-DiFuzz fuzzer. Sydr-Fuzz provides a convenient way to run hybrid fuzzing by combining dynamic symbolic execution based on Sydr tool with modern fuzzers. In addition, Sydr-Fuzz supports corpus minimization, coverage collection in human-readable formats, bug detection via security predicates, and crash analysis using Casr.
The next step in the development of Sydr-Fuzz was the addition of support for directed fuzzing using LibAFL-DiFuzz. This approach allows the analysis to focus on selected code locations, making it especially useful for in-depth exploration of individual components. LibAFL-DiFuzz, built on the modular architecture of LibAFL, makes it possible to specify one or more target points that define the direction of the analysis. A static analysis step is required, during which special metrics used in the fuzzing process are calculated. During analysis, LibAFL-DiFuzz tracks the program’s execution state and manages the energy distribution of inputs, increasing the probability of reaching the target points.
To demonstrate the capabilities of hybrid directed fuzzing with LibAFL-DiFuzz via Sydr-Fuzz, we will use the image library, written in Go.
To prepare a fuzzing target, it is necessary to define the target points that need to be reached and analyzed, as well as to create one or more wrappers that allow reaching these points via fuzzing. For the image project, as many as 5 wrappers were written, so we will consider only one: FuzzWebp, described in the wrapper file.
Next, you'll need to prepare a special build file for the target — Makefile.toml. You can generate Makefile.toml from a template using the gen_target.py script by specifying argument values specific to the fuzzing target. To see the required arguments, you can run the following command:
$ python3 gen_target.py -h
In the generated file, build targets are defined for Sydr symbolic execution (debug) (it is also used for crash analysis), for LibAFL-DiFuzz fuzzing (target), as well as additional build targets for coverage analysis (coverage). A detailed description of the Makefile.toml
file will be provided in the section "Building for LibAFL-DiFuzz".
For now, let’s look at target points and the build process for the fuzzing target.
Target points are specified in the config.toml file in the following format:
[[target]]
file = "/image/webp/decode.go"
line = 73
[[target]]
file = "/image/webp/decode.go"
line = 175
[[target]]
file = "/image/webp/decode.go"
line = 210
For convenience, we build the fuzzing target inside a Docker image, which contains needed environment and compiles the fuzzing targets. Let’s take a look at the Dockerfile_libafl file:
ARG BASE_IMAGE="sydr/ubuntu22.04-sydr-fuzz"
FROM $BASE_IMAGE
ARG SYDR_ARCHIVE="./sydr.zip"
WORKDIR /
RUN git clone https://github.com/golang/image.git
RUN git clone --depth=1 https://github.com/dvyukov/go-fuzz-corpus.git
WORKDIR /image
RUN git checkout c574db581976698ac047466629eeeb7b17bb49dd
RUN go get github.com/dvyukov/go-fuzz/go-fuzz-dep
COPY fuzz.go ./
# Move target and additional libs to main project.
RUN cp -r /root/.go/src/image/png . && \
cp -r /root/.go/src/image/jpeg . && \
cp -r /root/.go/src/image/gif . && \
cp -r /root/.go/src/internal/byteorder . && \
cp -r /root/.go/src/image/internal/imageutil .
RUN mkdir cmd/webp && \
mkdir cmd/tiff && \
mkdir cmd/png && \
mkdir cmd/jpeg && \
mkdir cmd/gif
COPY sydr_webp.go cmd/webp/main.go
COPY sydr_tiff.go cmd/tiff/main.go
COPY sydr_png.go cmd/png/main.go
COPY sydr_jpeg.go cmd/jpeg/main.go
COPY sydr_gif.go cmd/gif/main.go
# Copy LibAFL-DiFuzz target template.
COPY directed_target /directed_target
WORKDIR /directed_target
# Build image for LibAFL-DiFuzz.
ADD ${SYDR_ARCHIVE} ./
RUN unzip -o ${SYDR_ARCHIVE} && rm ${SYDR_ARCHIVE}
RUN OUT_DIR=/ cargo make all
WORKDIR /
First, the golang/image project is cloned from GitHub, and then a specific version is checkout for analysis. Next, the necessary files are copied into the Docker image. We need to pay attention to the fact that a special archive, ${SYDR_ARCHIVE}, is required. This archive contains the binary files and libraries needed for LibAFL-DiFuzz to work. By default, its path is set to ./sydr.zip, but you can specify a different path at build time. The command OUT_DIR=/ cargo make all
builds the difuzz, target, debug and coverage targets from Makefile.toml. Let’s build the image using the command:
$ sudo docker build --build-arg SYDR_ARCHIVE="difuzz.zip" -t oss-sydr-fuzz-libafl-image-go -f ./Dockerfile_libafl .
Let’s look at the contents of the file Makefile.toml. This file defines the build targets and their dependencies, and for each target, you can specify a script that performs a particular action. In our case, the first step is to run a static analysis of the program:
[tasks.difuzz_unix]
script_runner = "@shell"
script = '''
# Fix additional packages import paths.
cd ${EXAMPLE_DIR}
sed -i 's|"internal/byteorder"|"golang.org/x/image/byteorder"|' gif/writer.go
sed -i 's|"image/internal/imageutil"|"golang.org/x/image/imageutil"|' jpeg/reader.go
cd ${OUT_DIR_ABS}
${DIFUZZ_DIR_ABS}/difuzz-go -r webp.main -c ${PROJECT_DIR}/config_webp.toml -p ${EXAMPLE_DIR}/cmd/webp/main.go -e ${OUT_DIR_ABS}/ets_webp.toml ${DIFUZZ_ARGS}
${DIFUZZ_DIR_ABS}/difuzz-go -r tiff.main -c ${PROJECT_DIR}/config_tiff.toml -p ${EXAMPLE_DIR}/cmd/tiff/main.go -e ${OUT_DIR_ABS}/ets_tiff.toml ${DIFUZZ_ARGS}
${DIFUZZ_DIR_ABS}/difuzz-go -r png.main -c ${PROJECT_DIR}/config_png.toml -p ${EXAMPLE_DIR}/cmd/png/main.go -e ${OUT_DIR_ABS}/ets_png.toml ${DIFUZZ_ARGS}
${DIFUZZ_DIR_ABS}/difuzz-go -r jpeg.main -c ${PROJECT_DIR}/config_jpeg.toml -p ${EXAMPLE_DIR}/cmd/jpeg/main.go -e ${OUT_DIR_ABS}/ets_jpeg.toml ${DIFUZZ_ARGS}
${DIFUZZ_DIR_ABS}/difuzz-go -r gif.main -c ${PROJECT_DIR}/config_gif.toml -p ${EXAMPLE_DIR}/cmd/gif/main.go -e ${OUT_DIR_ABS}/ets_gif.toml ${DIFUZZ_ARGS}
'''
Initially, we correct the import paths in some files for proper compilation. There is no need to build a binary file for the analysis stage; it is sufficient for the DiFuzz-Go tool to perform static analysis by providing the path to the file with the fuzzing main function as the _-p- argument.
Let's run difuzz task and then we'll see the following DiFuzz-Go tool log:

As a result of the analysis, the output will include the file ets_webp.toml and other ets.toml_ files, as well as call graphs and CFGs of the program's target functions along with their dominator trees in .DOT format. The ets.toml_ files are used for project instrumentation.
Now we perform the build of the fuzzing target with the instrumentation required for the LibAFL-DiFuzz fuzzer:
[tasks.target_unix]
script_runner = "@shell"
script = '''
${GOINSTR_DIFUZZ} -a insert -i ${EXAMPLE_DIR} -o / -e ${OUT_DIR_ABS}/ets_webp.toml -l info -j 8
${GOINSTR_SANCOV} -a insert -i ${EXAMPLE_DIR} -o / -l info -j 8
cd ${EXAMPLE_DIR_INSTR}/cmd/webp
CGO_LDFLAGS="-L${LIBFORKSERVER_DIR_ABS}" go build -o ${OUT_DIR_ABS}/difuzz_target_image_webp
${GOINSTR_DIFUZZ} -a remove -i ${EXAMPLE_DIR} -o / -e ${OUT_DIR_ABS}/ets_webp.toml -keep-ets -l info
...
'''
dependencies = ["difuzz"]
Here we instrument the entire project source code using two instrumentors: goinstr_difuzz and goinstr_sancov. For goinstr_difuzz, the input requires the path to the project root (containing the go.mod file) via -i, the option -a insert, the output directory where the entire instrumented project will be copied (the project directory name will have -instr
appended), -e - the path to ets.toml, and finally, the log mode -l info and -jN - the number of parallel threads. The instrumentor inserts calls to difuzz.InstrumentHook(BBID)
into the project source code at the necessary locations. The goinstr_sancov instrumentor is given similar options, except for -e option. It is important to note that if you run goinstr_sancov first and then goinstr_difuzz, the sancov
instrumentation will not be set! Now we can proceed to build the target! We navigate to the directory with the file containing the main function (but this is not mandatory, you can pass the path to main.go directly to go build
), and then execute the command CGO_LDFLAGS="-L${LIBFORKSERVER_DIR_ABS}" go build -o ${OUT_DIR_ABS}/difuzz_target_image_webp
, passing the -L
flag with the path to the liblibforkserver.a
library in the CGO_LDFLAGS environment variable.
By setting the -l debug option in the instrumentors, we instrument our fuzzing target. We will see the following output, indicating that the instrumentation has been performed, as shown for one of the functions.

Tasks for debug and coverage are much easier to make, they can be inspected in Makefile.toml file.
To run hybrid directed fuzzing via Sydr-Fuzz for two wrappers, you need to create two configuration files: parse-libafl.toml and parse_elf-libafl.toml. The files should include:
- the exit-on-time parameter, which sets the time after which fuzzing stops if no new coverage is found (optional),
- a [sydr] table specifying Sydr arguments (args, jobs) and the command line to launch the target program (target),
- a [difuzz] table specifying the path to the LibAFL-DiFuzz fuzzer (path), its arguments (args), the command line to launch the target program (target), and the path to the binary built for Casr analysis (if crash analysis is needed) (casr_bin),
- and a [cov] table specifying the command line (target field) to launch the target program for coverage collection (optional).
For example, for webp, the configuration file will look like this:
exit-on-time = 7200
[sydr]
target = "/sydr_image_webp @@"
args = "-s 90 --wait-jobs -j2"
jobs = 2
[difuzz]
path = "/directed_target/sydr/difuzz/libafl_difuzz"
target = "/difuzz_target_image_webp @@"
args = "-j4 --sync-limit 200 --sync-jobs 2 --panic-analysis go -l64 -i /go-fuzz-corpus/webp/corpus -e /ets_webp.toml"
casr_bin = "/sydr_image_webp"
[cov]
target = "/coverage_image_webp @@"
source = "/image"
In this config, it is important to pay attention to the fuzzer option --panic-analysis go
. This option enables the handling of panic()
errors in Go, meaning that when this option is enabled, inputs that cause a panic will be treated as crashes.
Now we can run hybrid fuzzing with Sydr-Fuzz, specifying webp-libafl.toml config file:
$ sydr-fuzz -c webp-libafl.toml run
Fuzzing has started, the LibAFL-DiFuzz processes are running and beginning to send statistics on their current status:

Almost immediately, information begins to appear about the reached target points specified in the config.toml file:

The information about reaching a target point indicates the time it was achieved. If a target point is reached multiple times, it means the fuzzer was able to generate several inputs that hit that target.
Sometimes the logs contain the following entries:

These entries indicate whether LibAFL-DiFuzz was able (or unable) to import N new files generated by Sydr into its corpus.
Finally, at the end of the analysis, information is displayed about the minimum time to reach the target, as well as data on error-finding time and crash sorting:

The result of directed fuzzing is a set of objective-inputs. Each such input is characterized by causing at least one of the following states in the target program:
- a crash,
- reaching a specified target in the code,
- a timeout.
Post-processing of the results involves two stages:
-
Minimization: For a group of inputs that produce the same outcome (e.g., reaching the same target), only the first generated file is kept. This allows accurate measurement of the TTE (Time To Exposure) metric using a representative sample. Minimization can be disabled via the configuration option
xmin = false
, which is useful for keeping the full dataset. - Clustering: The inputs remaining after minimization are grouped (clustered) by the target points they reached. For each target, a separate directory is created, named according to its location in the code. If an input triggers multiple targets, it appears in all corresponding clusters. Structurally, the output directory with clusters looks like this:

As a result of the hybrid directed fuzzing, all target points were reached. Directories were created in the format difuzz/crashes_TARGET_NAME:TARGET_LINE, where input files that hit these points were copied. In this case, 4 target
inputs and 1 timeout
input are left after minimization.
In directed fuzzing, coverage is collected not only for the final corpus but also for all objective files remaining after minimization. This allows you to see the code coverage for the target points (and crashes) as well.
Coverage collection via Sydr-Fuzz is launched using the command:
$ sydr-fuzz -c webp-libafl.toml cov-html
The resulting log shows the progress:

In the HTML report, you can confirm that a target point, for example /image/webp/decode.go:162, was reached:

Crash analysis of inputs generated during fuzzing can be performed using the Casr tool. To do this, run the following command:
$ sydr-fuzz -c webp-libafl.toml casr
At this stage, only objective inputs that cause crashes are analyzed. Casr produces a separate cluster hierarchy in the directory webp-libafl-out/casr
. Running the analysis makes sense once there are not only inputs that reach target points, but also crash-inputs (these start with crash_*
after fuzzing).
This article presented an approach to hybrid directed fuzzing of Go code, combining the LibAFL-DiFuzz fuzzer and the Sydr symbolic executor via the Sydr-Fuzz tool. The workflow for preparing the target program was demonstrated using the image project. It showed that Sydr-Fuzz can automate key stages of the process: directed fuzzing, minimization and clustering of results, coverage collection, and crash analysis using the Casr utility.