Skip to main content

Find your first bug in C++

This quick start guide will teach 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 did not already install cifuzz, head over to the setup section.


CMake

1. 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. For this guide, we will use the project in the tutorials/c_cpp/cmake directory.

note

Run all cifuzz commands listed in this section from the tutorials/c_cpp/cmake directory.

2. 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

The command will ask if you want to initialize the project in remote or local-only mode. For this tutorial choose Local since we won't use the connection to CI Sense.

CI Fuzz will recognize the project as a CMake project and provide two commands to add to the projects CMakeLists.txt:

find_package(cifuzz NO_SYSTEM_ENVIRONMENT_PATH)
enable_fuzz_testing()

find_package will locate and load the cifuzz package for use when building the project and enable_fuzz_testing will enable the integration between CI Fuzz and CMake to run fuzz tests.

note

You need to add these commands before any add_library, add_subdirectory or add_executable directives, otherwise the targets will not be compiled with the correct instrumentation/build flags.

After adding the commands, the CMakeLists.txt should look like this:

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
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)

3. 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:

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
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:

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:

my_fuzz_test.cpp
#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 the data variable into different data types. Here is a link to the FuzzedDataProvider header file if you want to view its other methods.
  • Once you have created the appropriate variables a, b, and c using data from the fuzzer, the fuzz test calls the target function exploreMe with the fuzz data.

4. 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 will also ask if you want to run the cifuzz coverage command, in case you are interested in coverage information of the fuzz test.

5. Examine the Finding

When 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 will also provide 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 will serve as inputs for future runs to help identify regressions.

Bazel

1. 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. For this guide, we will use the project in the tutorials/c_cpp/cmake directory.

note

Run all cifuzz commands listed in this section from the tutorials/c_cpp/cmake directory.

2. 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

The command will ask if you want to initialize the project in remote or local-only mode. For this tutorial choose Local since we won't use the connection to CI Sense.

CI Fuzz will recognize the project as a Bazel 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.

3. 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:

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:

my_fuzz_test.cpp
#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 the data variable into different data types. Here is a link to the FuzzedDataProvider header file if you want to view its other methods.
  • Once you have created the appropriate variables a, b, and c using data from the fuzzer, the fuzz test calls the target function exploreMe with the fuzz data.

4. 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 will also ask if you want to run the cifuzz coverage command, in case you are interested in coverage information of the fuzz test.

5. Examine the Finding

When 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 will also provide 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 will serve as inputs for future runs to help identify regressions.