diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d3ba8a2..1fe5b96d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,38 @@ endif() add_compile_options(-Wall) +# Begin fuzz-persistent +# NB: These fuzzing targets require a special compiler! +add_custom_target(fuzzing_targets) +set_target_properties(fuzzing_targets PROPERTIES EXCLUDE_FROM_ALL TRUE) + +file(GLOB all_implementations "${PROJECT_SOURCE_DIR}/*.cpp") +list(REMOVE_ITEM all_implementations "${PROJECT_SOURCE_DIR}/main.cpp") # we only want the main() function from the fuzz target +list(REMOVE_ITEM all_implementations "${PROJECT_SOURCE_DIR}/flashmqtestclient.cpp") # it causes problems: /usr/lib/gcc/x86_64-pc-linux-gnu/11.3.0/include/g++-v11/ext/new_allocator.h:162:23: error: call to deleted constructor of 'MqttPacket' +file(GLOB fuzz_targets "fuzz-persistent/targets/*.cpp") +foreach(file ${fuzz_targets}) + if (NOT ${CMAKE_VERSION} VERSION_LESS "3.20.0") + # CMake 3.20 (2021-03-23) + cmake_path(GET file STEM fuzzstem) + else() + # Older CMake + string(REGEX MATCH "([^/]*)\\.[a-z]*$" OUTVAR "${file}") + set(fuzzstem "${CMAKE_MATCH_1}") + if(NOT fuzzstem) + message(FATAL_ERROR "Problem in ${CMAKE_CURRENT_LIST_FILE}: cmake_path wasn't available and regex failed to extract stem") + endif() + endif() + set(fuzzbinary "fuzz_${fuzzstem}") + add_dependencies(fuzzing_targets ${fuzzbinary}) + add_executable(${fuzzbinary} ${file}) + target_sources(${fuzzbinary} PRIVATE ${all_implementations}) + target_link_libraries(${fuzzbinary} dl ssl crypto resolv) + set_target_properties(${fuzzbinary} PROPERTIES EXCLUDE_FROM_ALL TRUE) + + message(STATUS "Generated fuzzing target ${fuzzbinary}") +endforeach() +# End fuzz-persistent + add_executable(flashmq forward_declarations.h mainapp.h diff --git a/fuzz-persistent/.gitignore b/fuzz-persistent/.gitignore new file mode 100644 index 00000000..94202ca8 --- /dev/null +++ b/fuzz-persistent/.gitignore @@ -0,0 +1,2 @@ +/build/ +/output/ diff --git a/fuzz-persistent/README.md b/fuzz-persistent/README.md new file mode 100644 index 00000000..4280342a --- /dev/null +++ b/fuzz-persistent/README.md @@ -0,0 +1,17 @@ +# Fuzzing with persistent mode + +See the [AFL++ documentation](https://github.com/AFLplusplus/AFLplusplus/blob/stable/instrumentation/README.persistent_mode.md) for generic help about fuzzing with persistent mode. + +TL;DR: it's _faaaaaast_. + +## Fuzzing everything + +Simply run `./fuzz-helper.sh`. Findings will be written to the folder called `output`. + +## Setting up new test + +To add a new test: + +* Decide on a testname, the standard format is: `__`, for example `cirbuf__write` +* Create `target/${testname}.cpp` +* Run `./fuzz-helper.sh build_and_run ${testname}` diff --git a/fuzz-persistent/fuzz-helper.sh b/fuzz-persistent/fuzz-helper.sh new file mode 100755 index 00000000..469c8c96 --- /dev/null +++ b/fuzz-persistent/fuzz-helper.sh @@ -0,0 +1,157 @@ +#!/bin/bash +# +# Script to build+run fuzzing targets in persistent mode using American Fuzzy Lop. + +thisfile=$(readlink --canonicalize "$0") +thisdir=$(dirname "$thisfile") +projectdir=$(dirname "$thisdir") + +die() { + >&2 echo "Fatal error: $*" + exit 9 +} + +if [[ -z "$AFL_ROOT" ]]; then + afl_fallback_from_path=$(command -v afl-fuzz) + if [[ -z "$afl_fallback_from_path" ]] + then + echo "ERROR: alf-fuzz not found. Please set the AFL_ROOT environment variable" + exit 1 + else + AFL_ROOT=$(dirname "$afl_fallback_from_path") + echo "WARNING: You didn't set AFL_ROOT but I found afl-fuzz in $AFL_ROOT, I hope the other tools are also there" + fi +fi + +# AFL_ROOT is our own variable name, newer AFL++ warn that they don't know what +# it means. Solution: unexport the variable +export -n AFL_ROOT + +set -u + +usage() { + >&2 echo "Usage: [ build | run | build_and_run | attach | clean ]" +} +die() { + >&2 echo "A fatal error occurred: $*" + exit 9 +} + +TMUXSESSION=flashmqfuzzpersistent +COMMAND="build_and_run" +if [[ $# -ge 1 ]]; then + COMMAND="$1" +fi +SPECIFIC_BINARY="" +if [[ $# -ge 2 ]]; then + SPECIFIC_BINARY="$2" +fi + +do_build() { + # We currently need LTO/fast because they understand the macros we use + export CC="$AFL_ROOT/afl-clang-fast" + CC_CLANG_LTO="$AFL_ROOT/afl-clang-lto" + [[ -e "$CC_CLANG_LTO" ]] && export CC="$CC_CLANG_LTO" + + export CXX="$AFL_ROOT/afl-clang-fast++" + CXX_CLANG_LTO="$AFL_ROOT/afl-clang-lto++" + [[ -e "$CXX_CLANG_LTO" ]] && export CXX="$CXX_CLANG_LTO" + + echo "Using for \$CC: $CC" + echo "Using for \$CXX: $CXX" + + mkdir -p "${thisdir}/build" + cd "${thisdir}/build" || die "Something peculiar went wrong." + + cmake -DCMAKE_BUILD_TYPE="fuzz-persistent" "$projectdir" || die "CMake failed." + if [[ -z "$SPECIFIC_BINARY" ]] + then + make -j "fuzzing_targets" || die "make failed." + else + make -j "fuzz_${SPECIFIC_BINARY}" || die "make failed." + fi +} + +do_run() { + # list-sessions has a -f option but it doesn't seem to work for me + if tmux list-sessions | grep "^$TMUXSESSION:" > /dev/null + then + >&2 echo "Fuzzing is already going on in the background. Use 'attach' to attach." + exit 1 + fi + + + declare -a FUZZING_TARGETS + HUMAN_LIST_OF_TARGETS=$'\n' + for i in "${thisdir}/build/fuzz_"*; + do + if [[ -x "$i" ]]; + then + FUZZING_TARGETS+=("$(basename "$i")") + HUMAN_LIST_OF_TARGETS="$HUMAN_LIST_OF_TARGETS- $(basename "$i")"$'\n' + fi + done + + INPUT="${projectdir}/fuzztests" + + mkdir -p "${thisdir}/output" + + if [[ -z "$SPECIFIC_BINARY" ]] + then + echo "AFL output will be stored in ${thisdir}/output" + echo "Fuzzing targets: ${HUMAN_LIST_OF_TARGETS}" + + tmux new-session -s "$TMUXSESSION" -d "echo 'Fuzzing targets: ${HUMAN_LIST_OF_TARGETS}Cycle through the windows to see the fuzzing going on'; read" + + for BINARY in "${FUZZING_TARGETS[@]}" + do + OUTPUT="${thisdir}/output/${BINARY}" + tmux new-window -t "$TMUXSESSION" -n "$BINARY" "'$AFL_ROOT/afl-fuzz' -i '${INPUT}' -o '${OUTPUT}' -T '$BINARY' -- '${thisdir}/build/$BINARY'; echo 'Press [enter] to exit'; read" + done + tmux next-window -t "$TMUXSESSION" # cycle through to the first window with the README + tmux attach-session -t "$TMUXSESSION" + else + OUTPUT="${thisdir}/output/${SPECIFIC_BINARY}" + BINARY="fuzz_$SPECIFIC_BINARY" + + echo "AFL output will be stored in ${OUTPUT}" + echo "Fuzzing target: ${SPECIFIC_BINARY}" + + tmux new-session -s "$TMUXSESSION" -n "$BINARY" "'$AFL_ROOT/afl-fuzz' -i '${INPUT}' -o '${OUTPUT}' -T '$BINARY' -- '${thisdir}/build/$BINARY'; echo 'Press [enter] to exit'; read" + fi +} + +do_clean() { + rm -rf "$thisdir/build" +} + +do_attach() { + tmux attach-session -t "$TMUXSESSION" +} + +if [[ "$COMMAND" == "build" ]] +then + >&2 echo "Building..." + do_build +elif [[ "$COMMAND" == "run" ]] +then + >&2 echo "Running..." + do_run +elif [[ "$COMMAND" == "build_and_run" ]] +then + >&2 echo "Building and running..." + do_build + echo "" + echo "" + do_run +elif [[ "$COMMAND" == "clean" ]] +then + do_clean +elif [[ "$COMMAND" == "attach" ]] +then + do_attach +else + >&2 echo "Unknown option $COMMAND" + usage + exit 1 +fi diff --git a/fuzz-persistent/targets/cirbuf__write.cpp b/fuzz-persistent/targets/cirbuf__write.cpp new file mode 100644 index 00000000..8476c738 --- /dev/null +++ b/fuzz-persistent/targets/cirbuf__write.cpp @@ -0,0 +1,27 @@ +#include +#include "../../cirbuf.h" + +__AFL_FUZZ_INIT(); + +int main() +{ +#ifdef __AFL_HAVE_MANUAL_CONTROL + __AFL_INIT(); +#endif + + unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF; // must be after __AFL_INIT + // and before __AFL_LOOP! + + while (__AFL_LOOP(10000)) + { + + int len = __AFL_FUZZ_TESTCASE_LEN; // don't use the macro directly in a + // call! + + CirBuf cirbuf(1024); + + cirbuf.write(buf, len); + } + + return 0; +} diff --git a/fuzz-persistent/targets/threadlocalutils__isValidUtf8.cpp b/fuzz-persistent/targets/threadlocalutils__isValidUtf8.cpp new file mode 100644 index 00000000..1723fdea --- /dev/null +++ b/fuzz-persistent/targets/threadlocalutils__isValidUtf8.cpp @@ -0,0 +1,33 @@ +#include + +#ifndef __SSE4_2__ +#error "This file is supposed to test SIMD, please fix your setup to support it." +#endif + +#include "../../threadlocalutils.h" +thread_local SimdUtils simdUtils; + +__AFL_FUZZ_INIT(); + +int main() +{ +#ifdef __AFL_HAVE_MANUAL_CONTROL + __AFL_INIT(); +#endif + + unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF; // must be after __AFL_INIT + // and before __AFL_LOOP! + char *signed_buff = reinterpret_cast(buf); + + while (__AFL_LOOP(10000)) + { + + int len = __AFL_FUZZ_TESTCASE_LEN; // don't use the macro directly in a + // call! + std::string random_string(signed_buff, len); + simdUtils.isValidUtf8(random_string, false); + simdUtils.isValidUtf8(random_string, true); + } + + return 0; +} diff --git a/fuzz-persistent/targets/threadlocalutils__splitTopic.cpp b/fuzz-persistent/targets/threadlocalutils__splitTopic.cpp new file mode 100644 index 00000000..c516f711 --- /dev/null +++ b/fuzz-persistent/targets/threadlocalutils__splitTopic.cpp @@ -0,0 +1,34 @@ +#include +#include + +#ifndef __SSE4_2__ +#error "This file is supposed to test SIMD, please fix your setup to support it." +#endif + +#include "../../threadlocalutils.h" +thread_local SimdUtils simdUtils; + +__AFL_FUZZ_INIT(); + +int main() +{ +#ifdef __AFL_HAVE_MANUAL_CONTROL + __AFL_INIT(); +#endif + + unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF; // must be after __AFL_INIT + // and before __AFL_LOOP! + char *signed_buff = reinterpret_cast(buf); + + while (__AFL_LOOP(10000)) + { + + int len = __AFL_FUZZ_TESTCASE_LEN; // don't use the macro directly in a + // call! + std::string random_string(signed_buff, len); + std::vector subtopics; + simdUtils.splitTopic(random_string, subtopics); + } + + return 0; +} diff --git a/fuzz-persistent/targets/utils__base64Encode.cpp b/fuzz-persistent/targets/utils__base64Encode.cpp new file mode 100644 index 00000000..46e6e220 --- /dev/null +++ b/fuzz-persistent/targets/utils__base64Encode.cpp @@ -0,0 +1,25 @@ +#include +#include "../../utils.h" + +__AFL_FUZZ_INIT(); + +int main() +{ +#ifdef __AFL_HAVE_MANUAL_CONTROL + __AFL_INIT(); +#endif + + unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF; // must be after __AFL_INIT + // and before __AFL_LOOP! + + while (__AFL_LOOP(10000)) + { + + int len = __AFL_FUZZ_TESTCASE_LEN; // don't use the macro directly in a + // call! + + base64Encode(buf, len); + } + + return 0; +} diff --git a/threadlocalutils.cpp b/threadlocalutils.cpp index 74df2350..a7f732f5 100644 --- a/threadlocalutils.cpp +++ b/threadlocalutils.cpp @@ -27,6 +27,9 @@ std::vector *SimdUtils::splitTopic(const std::string &topic, std::v std::memset(&topicCopy.data()[s], 0, 16); int n = 0; int carryi = 0; + + assert(s != 15); // whoopsie; bomb. + while (n <= s) { const char *i = &topicCopy.data()[n]; diff --git a/utils.cpp b/utils.cpp index dad1ecf8..9d60c935 100644 --- a/utils.cpp +++ b/utils.cpp @@ -471,6 +471,10 @@ std::vector base64Decode(const std::string &s) std::string base64Encode(const unsigned char *input, const int length) { + if (length == 13) + { + throw std::runtime_error("The bomb went off... kaboom."); + } const int pl = 4*((length+2)/3); char *output = reinterpret_cast(calloc(pl+1, 1)); const int ol = EVP_EncodeBlock(reinterpret_cast(output), input, length);