Find Your First Bug in C++
This quick start guide teaches you the basics of using CI Fuzz with a C++ project. CI Fuzz supports CMake and Bazel, but you can also configure it to work with other build systems such as Make.
This guide was created using macOS. If you are on Linux or Windows, you should be able to follow along without any issues.
If you didn't already install cifuzz
, head over to the setup section.
CMake
Set up the repository
Download or clone the following repository:
https://github.com/CodeIntelligenceTesting/ci-fuzz-cli-tutorials.
This repository contains several tutorials and example projects. This guide focuses on the project in the
tutorials/c_cpp/cmake
directory.
Run all cifuzz
commands listed in this section from the tutorials/c_cpp/cmake
directory.
Initialize the project
To use CI Fuzz in the project, you first need to initialize it.
Run the following command to create the CI Fuzz configuration file cifuzz.yaml
:
cifuzz init
CI Fuzz automatically recognizes CMake as the build system of the project and provides two commands to add to the
projects CMakeLists.txt
:
find_package(cifuzz NO_SYSTEM_ENVIRONMENT_PATH)
enable_fuzz_testing()
find_package
locates and loads the cifuzz
package for use when building the project and enable_fuzz_testing
enables the integration between CI Fuzz and CMake
to run fuzz tests.
You need to add these commands before any add_library
, add_subdirectory
or add_executable
directives, otherwise
the targets won't compile with the correct instrumentation/build flags.
After adding the commands, the CMakeLists.txt
should look like this:
cmake_minimum_required(VERSION 3.3)
project(cmake_example)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
enable_testing()
find_package(cifuzz NO_SYSTEM_ENVIRONMENT_PATH)
enable_fuzz_testing()
add_subdirectory(src)
add_executable(${PROJECT_NAME} main.cpp )
target_link_libraries(${PROJECT_NAME} PRIVATE exploreMe)
Create a fuzz test
The next step is to create a C++ fuzz test template. To make fuzz testing as easy as unit testing, you should place the
fuzz test in a test
directory, exactly as you would a standard unit test.
Run the following command to create a file my_fuzz_test.cpp
with a fuzz test stub and receive instructions:
cifuzz create cpp -o test/my_fuzz_test.cpp
For CMake to locate, build, and link the fuzz test, you need to add the add_fuzz_test
command from the instructions
and a target_link_libraries
command to the CMakeLists.txt
:
cmake_minimum_required(VERSION 3.3)
project(cmake_example)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
enable_testing()
find_package(cifuzz NO_SYSTEM_ENVIRONMENT_PATH)
enable_fuzz_testing()
add_subdirectory(src)
add_executable(${PROJECT_NAME} main.cpp )
target_link_libraries(${PROJECT_NAME} PRIVATE exploreMe)
add_fuzz_test(my_fuzz_test test/my_fuzz_test.cpp)
target_link_libraries(my_fuzz_test PRIVATE exploreMe)
Before you write the fuzz test, take a look at the target function that you want to fuzz in src/explore_me.cpp
:
void exploreMe(int a, int b, string c) {
if (a >= 20000) {
if (b >= 2000000) {
if (b - a < 100000) {
if (c == "FUZZING") {
char *s = (char *)malloc(1);
strcpy(s, "too long");
printf("%s\n", s);
}
}
}
}
}
The main parts to focus on here is the parameters that the exploreMe
function requires: int a
, int b
and string c
.
As long as you pass the correct data types to the function, the fuzzer takes care of the rest.
Now you can fill in the fuzz test template with a real fuzz test:
#include "../src/explore_me.h"
#include <cifuzz/cifuzz.h>
#include <fuzzer/FuzzedDataProvider.h>
FUZZ_TEST_SETUP() {}
FUZZ_TEST(const uint8_t *data, size_t size) {
FuzzedDataProvider fuzzed_data(data, size);
int a = fuzzed_data.ConsumeIntegral<int>();
int b = fuzzed_data.ConsumeIntegral<int>();
std::string c = fuzzed_data.ConsumeRandomLengthString();
exploreMe(a, b, c);
}
A few notes about this fuzz test:
- The fuzz test must include the header for the target function
../src/explore_me.h
and cifuzz<cifuzz/cifuzz.h>
- This fuzz test uses the
FuzzedDataProvider
from LLVM. This isn't required, but it's a convenient way to split the fuzzing input in thedata
variable into different data types. Here is a link to theFuzzedDataProvider
header file if you want to view its other methods. - Once you have created the appropriate variables
a
,b
, andc
usingdata
from the fuzzer, the fuzz test calls the target functionexploreMe
with the fuzz data.
Run the fuzz test
You now have successfully set up and initialized your project, added all necessary configuration and created a fuzz test.
Now you can run the fuzz test:
cifuzz run my_fuzz_test
cifuzz
should discover a heap buffer overflow quickly:
37 Unit Test Equivalents and 1 new Finding in 0s.
1 Finding in total.
💥 [silly_bunny] heap buffer overflow in exploreMe (src/explore_me.cpp:14:11)
The command also asks if you want to run the cifuzz coverage
command, in case you want coverage information about
the fuzz test.
Examine the Finding
If cifuzz
discovers a Finding, it stores the output from the Finding and the input that caused it.
You can list all Findings discovered so far by running cifuzz findings
.
Origin | Severity | Name | Description | Fuzz Test | Location
Local | 9.0 | silly_bunny | heap buffer overflow | my_fuzz_test | src/explore_me.cpp:14:11
If you want to see the stack trace and details of a specific Finding you can provide its name, for example
cifuzz finding silly_bunny
. This information can help you debug and fix the issue.
Examining the output from cifuzz finding silly_bunny
shows that there was a WRITE of size 9
and this triggers at
line 14 in the exploreMe
function.
==5994==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x000106fa6f58 at pc 0x000104b395c8 bp 0x00016bac9e60 sp 0x00016bac9620
WRITE of size 9 at 0x000106fa6f58 thread T0
#0 0x104b395c4 in __asan_memcpy (/opt/homebrew/Cellar/llvm@15/15.0.7/lib/clang/15.0.7/lib/darwin/libclang_rt.asan_osx_dynamic.dylib:arm64+0x3d5c4) (BuildId: 5e231afcda4033eeb868d2deae2c92a732000000200000000100000000000b00)
#1 0x104337f44 in exploreMe(int, int, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>) /Users/maren/Development/IdeaProjects/ci-fuzz-cli-tutorials/tutorials/c_cpp/cmake/src/explore_me.cpp:14:11
If you examine lines 13 and 14 in src/explore_me.cpp
, you can see this is where a strcpy
call attempts to copy too
many bytes to the buffer.
If possible, CI Fuzz also provides more information about the kind of Finding and how to fix it:
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────┐
| Name | Heap Buffer Overflow |
| Severity Level | Critical |
| Severity Score | 9.0 |
| ASan Example | https://github.com/google/sanitizers/wiki/AddressSanitizerExampleHeapOutOfBounds |
| CWE: Overflow writes | https://cwe.mitre.org/data/definitions/787.html |
| CWE: Overflow reads | https://cwe.mitre.org/data/definitions/125.html |
└─────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Description:
A heap buffer overflow is a type of a memory corruption vulnerability that is widely used for different types of attacks.
A successful heap buffer overflow attack can be used to read sensitive data in memory, or write and execute code in it.
Mitigation:
A programmer can follow the following guidelines to help avoid buffer overflows: When using functions which copy a given size from memory,
ensure that the target buffer has a size large enough for the amount of data to be copied.
Always make sure to access the buffer within its defined boundaries, checking on each access.
cifuzz
stores the crashing input in a directory named <name_of_fuzz_test>\_inputs
. In this case: my_fuzz_test_inputs
.
The crashing input has the same name as the Finding and Findings in this directory now serve as inputs for future runs
to help identify regressions.
Bazel
Set up the repository
Download or clone the following repository:
https://github.com/CodeIntelligenceTesting/ci-fuzz-cli-tutorials.
This repository contains several tutorials and example projects. This guide focuses on the project in the
tutorials/c_cpp/cmake
directory.
Run all cifuzz
commands listed in this section from the tutorials/c_cpp/cmake
directory.
Initialize the project
To use CI Fuzz in the project, you first need to initialize it.
Run the following command to create the CI Fuzz configuration file cifuzz.yaml
:
cifuzz init
CI Fuzz automatically recognizes Bazel as the build system of the project and provides rules you need to add the
projects WORKSPACE
file:
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "rules_fuzzing",
sha256 = "ff52ef4845ab00e95d29c02a9e32e9eff4e0a4c9c8a6bcf8407a2f19eb3f9190",
strip_prefix = "rules_fuzzing-0.4.1",
urls = ["https://github.com/bazelbuild/rules_fuzzing/releases/download/v0.4.1/rules_fuzzing-0.4.1.zip"],
)
load("@rules_fuzzing//fuzzing:repositories.bzl", "rules_fuzzing_dependencies")
rules_fuzzing_dependencies()
load("@rules_fuzzing//fuzzing:init.bzl", "rules_fuzzing_init")
rules_fuzzing_init()
load("@fuzzing_py_deps//:requirements.bzl", "install_deps")
install_deps()
git_repository(
name = "cifuzz",
commit = "b013aa0f90fe8ac60adfc6d9640a9cfa451dda9e",
remote = "https://github.com/CodeIntelligenceTesting/cifuzz-bazel",
)
These rules enable CI Fuzz to integrate directly with your Bazel project.
Create a fuzz test
The next step is to create a C++ fuzz test template. To make fuzz testing as easy as unit testing, you should place the
fuzz test in a test
directory, exactly as you would a standard unit test.
Run the following command to create a file my_fuzz_test.cpp
with a fuzz test stub and receive instructions:
cifuzz create cpp -o test/my_fuzz_test.cpp
For Bazel to locate, build, and link the fuzz test, you need to define a Bazel target by adding the commands from the
instructions and a dependency link to the source file to the test/BUILD.bazel
:
load("@rules_fuzzing//fuzzing:cc_defs.bzl", "cc_fuzz_test")
cc_fuzz_test(
name = "my_fuzz_test",
srcs = ["my_fuzz_test.cpp"],
corpus = glob(
["my_fuzz_test_inputs/**"],
allow_empty = True,
) + select({
"@cifuzz//:collect_coverage": glob([".my_fuzz_test_cifuzz_corpus/**"], allow_empty = True),
"//conditions:default": [],
}),
deps = [
"//src:explore_me",
"@cifuzz"
],
)
Before you write the fuzz test, take a look at the target function that you want
to fuzz. It's located in src/explore_me.cpp
:
void exploreMe(int a, int b, string c) {
if (a >= 20000) {
if (b >= 2000000) {
if (b - a < 100000) {
if (c == "FUZZING") {
char *s = (char *)malloc(1);
strcpy(s, "too long");
printf("%s\n", s);
}
}
}
}
}
The main parts to focus on here is the parameters that the exploreMe
function requires: int a
, int b
and string c
.
As long as you pass the correct data types to the function, the fuzzer takes care of the rest.
Now you can fill in the fuzz test template with a real fuzz test:
#include "../src/explore_me.h"
#include <cifuzz/cifuzz.h>
#include <fuzzer/FuzzedDataProvider.h>
FUZZ_TEST_SETUP() {}
FUZZ_TEST(const uint8_t *data, size_t size) {
FuzzedDataProvider fuzzed_data(data, size);
int a = fuzzed_data.ConsumeIntegral<int>();
int b = fuzzed_data.ConsumeIntegral<int>();
std::string c = fuzzed_data.ConsumeRandomLengthString();
exploreMe(a, b, c);
}
A few notes about this fuzz test:
A few notes about this fuzz test:
- The fuzz test must include the header for the target function
../src/explore_me.h
and cifuzz<cifuzz/cifuzz.h>
- This fuzz test uses the
FuzzedDataProvider
from LLVM. This isn't required, but it's a convenient way to split the fuzzing input in thedata
variable into different data types. Here is a link to theFuzzedDataProvider
header file if you want to view its other methods. - Once you have created the appropriate variables
a
,b
, andc
usingdata
from the fuzzer, the fuzz test calls the target functionexploreMe
with the fuzz data.
Run the fuzz test
You now have successfully set up and initialized your project, added all necessary configuration and created a fuzz test.
Now you can run the fuzz test:
cifuzz run test:my_fuzz_test
cifuzz
should discover a heap buffer overflow quickly:
30 Unit Test Equivalents and 1 new Finding in 1s.
1 Finding in total.
💥 [magical_capybara] heap buffer overflow
The command also asks if you want to run the cifuzz coverage
command, in case you want coverage
information about the fuzz test.
Examine the Finding
If cifuzz
discovers a Finding, it stores the output from the Finding and the input that caused it.
You can list all Findings discovered so far by running cifuzz findings
.
Origin | Severity | Name | Description | Fuzz Test | Location
Local | 9.0 | magical_capybara | heap buffer overflow | test:my_fuzz_test_bin | n/a
If you want to see the stack trace and details of a specific Finding you can provide its name, for example
cifuzz finding magical_capybara
. This information can help you debug and fix the issue.
Examining the output from cifuzz finding magical_capybara
shows that there was a WRITE of size 9
which triggered the
heap buffer overflow.
==9367==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x000107a44591 at pc 0x0001055cd5c8 bp 0x00016b0400e0 sp 0x00016b03f8a0"
WRITE of size 9 at 0x000107a44591 thread T0
#0 0x104b395c4 in __asan_memcpy (/opt/homebrew/Cellar/llvm@15/15.0.7/lib/clang/15.0.7/lib/darwin/libclang_rt.asan_osx_dynamic.dylib:arm64+0x3d5c4) (BuildId: 5e231afcda4033eeb868d2deae2c92a732000000200000000100000000000b00)
#1 0x104337f44 in exploreMe(int, int, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>) /Users/maren/Development/IdeaProjects/ci-fuzz-cli-tutorials/tutorials/c_cpp/cmake/src/explore_me.cpp:14:11
If you examine src/explore_me.cpp
, you can see in line 14 and 15 is a strcpy
call that attempts to copy too
many bytes to the buffer.
If possible, CI Fuzz also provides more information about the kind of Finding and how to fix it:
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────┐
| Name | Heap Buffer Overflow |
| Severity Level | Critical |
| Severity Score | 9.0 |
| ASan Example | https://github.com/google/sanitizers/wiki/AddressSanitizerExampleHeapOutOfBounds |
| CWE: Overflow writes | https://cwe.mitre.org/data/definitions/787.html |
| CWE: Overflow reads | https://cwe.mitre.org/data/definitions/125.html |
└─────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Description:
A heap buffer overflow is a type of a memory corruption vulnerability that is widely used for different types of attacks.
A successful heap buffer overflow attack can be used to read sensitive data in memory, or write and execute code in it.
Mitigation:
A programmer can follow the following guidelines to help avoid buffer overflows: When using functions which copy a given size from memory,
ensure that the target buffer has a size large enough for the amount of data to be copied.
Always make sure to access the buffer within its defined boundaries, checking on each access.
cifuzz
stores the crashing input in a directory named <name_of_fuzz_test>\_inputs
. In this case: my_fuzz_test_inputs
.
The crashing input has the same name as the Finding and Findings in this directory now serve as inputs for future runs
to help identify regressions.