diff options
author | Andrei Pavel <andrei@isc.org> | 2024-10-15 11:41:41 +0200 |
---|---|---|
committer | Andrei Pavel <andrei@isc.org> | 2024-10-23 15:40:31 +0200 |
commit | a96168e7625541f15fc58811104c8da0303646f4 (patch) | |
tree | ae63bde11d86d1276c6efe4bb000a24c5e6486c7 | |
parent | [#3605] Remove extra semis (diff) | |
download | kea-a96168e7625541f15fc58811104c8da0303646f4.tar.xz kea-a96168e7625541f15fc58811104c8da0303646f4.zip |
[#3605] Integrate a new fuzzing solution in Kea
The solution is based on clusterfuzzlite, libfuzzer, and oss-fuzz
technologies.
- Add the .clusterfuzzlite directory.
- Add the fuzz CI stage and fuzzing CI jobs.
- Add the fuzzing targets in the fuzz directory.
- Document fuzzing in doxygen.
83 files changed, 2121 insertions, 116 deletions
diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 0000000000..472bf1f50f --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -0,0 +1,6 @@ +FROM registry.gitlab.isc.org/isc-projects/kea:fuzz-latest + +# Copy repo and link build.sh so that it runs from a location relative to the Kea repo. +WORKDIR "${SRC}" +COPY . "${SRC}/kea" +RUN ln -s "${SRC}/kea/.clusterfuzzlite/build.sh" "${SRC}/build.sh" diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh new file mode 100755 index 0000000000..41f60f12ad --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,58 @@ +#!/bin/bash -eu + +# https://reports.kea.isc.org/new-fuzzer.html + +script_path="$(dirname "$(readlink -f "${0}")")" +cd "${script_path}/.." + +# Use a wrapper function to allow "return 1" instead of "exit 1" which may have +# unforeseen consequences in case this script is sourced. +install_kea() { + # ccache + export CCACHE_DIR=/cache + export PATH="/usr/lib/ccache:$PATH" + export KEA_BUILD_DIR="${KEA_BUILD_DIR-/builds/isc-projects/kea}" + + cxxflags= + autoreconf -i + if test "${SANITIZER}" = 'none'; then + cxxflags="${cxxflags} -fno-sanitize=all" + enable_fuzzing='--enable-fuzzing' + else + cxxflags="${cxxflags} -fsanitize=${SANITIZER}" + enable_fuzzing='--enable-fuzzing=ci' + fi + export CXXFLAGS="${cxxflags}" + export LDFLAGS='-L/usr/lib/gcc/x86_64-linux-gnu/9 -lstdc++fs' + if ! ./configure --enable-boost-headers-only --prefix='/opt/kea' "${enable_fuzzing}" --with-gtest=/usr/src/googletest/googletest; then + printf './configure failed. Here is config.log:\n' + cat config.log + return 1 + fi + make -j "$(nproc)" + make install + + # Copy internal libraries. + # SC2156 (warning): Injecting filenames is fragile and insecure. Use parameters. + # shellcheck disable=SC2156 + find "/opt/kea/lib" -mindepth 1 -maxdepth 1 -exec sh -c "cp {} ${KEA_BUILD_DIR}" ';' + + # Copy the binaries. + for fuzzer in fuzz-config-kea-dhcp4 fuzz-packets-kea-dhcp4 fuzz-unix-socket-kea-dhcp4 \ + fuzz-config-kea-dhcp6 fuzz-packets-kea-dhcp6 fuzz-unix-socket-kea-dhcp6 \ + fuzz-http-endpoint \ + ; do + cp "/opt/kea/sbin/${fuzzer}" "${OUT}/${fuzzer}" + # copy all required libraries + echo "ldd ${OUT}/${fuzzer}: " + ldd "${OUT}/${fuzzer}" + EXTENDED_PATH=$(readelf -d "${OUT}/${fuzzer}" | grep 'R.*PATH' | cut -d '[' -f 2 | cut -d ']' -f 1) + patchelf --set-rpath "/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:${EXTENDED_PATH}" "${OUT}/${fuzzer}" + readelf -d "${OUT}/${fuzzer}" | grep 'R.*PATH' || true + for i in $(ldd "${OUT}/${fuzzer}" | cut -f 2 | cut -d ' ' -f 3); do + cp "${i}" "${KEA_BUILD_DIR}" + done + done +} + +install_kea diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 0000000000..b4788012b1 --- /dev/null +++ b/.clusterfuzzlite/project.yaml @@ -0,0 +1 @@ +language: c++ diff --git a/.clusterfuzzlite/run-locally.sh b/.clusterfuzzlite/run-locally.sh new file mode 100755 index 0000000000..b62fc0b5f0 --- /dev/null +++ b/.clusterfuzzlite/run-locally.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +# Change to parent directory, so that the script can be called from anywhere. +parent_path=$(cd "$(dirname "${0}")" && pwd) +cd "${parent_path}" || exit 1 + +mkdir -p build/out +mkdir -p build/work + +cd .. || exit 2 + +docker build -t kea-fuzzing -f .clusterfuzzlite/Dockerfile . + +docker_run() { + docker run \ + --interactive \ + --privileged \ + --platform linux/amd64 \ + --rm \ + --shm-size=2g \ + -e ARCHITECTURE=x86_64 \ + -e CIFUZZ=true \ + -e FUZZING_ARGS='-rss_limit_mb=8192' \ + -e FUZZING_ENGINE=libfuzzer \ + -e FUZZING_LANGUAGE=c++ \ + -e KEA_BUILD_DIR=/src \ + -e SANITIZER=address \ + -v "${parent_path}/build/out:/out" \ + -v "${parent_path}/build/work:/work" \ + kea-fuzzing \ + "${@}" +} + +docker_run + +docker_run compile diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f319825a0f..052db51a29 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,32 +2,52 @@ variables: # Locale settings do not affect the build, but might affect tests. LC_ALL: C - CI_REGISTRY_IMAGE: registry.gitlab.isc.org/isc-projects/kea + # Fuzzing + CFL_ARTIFACTS_DIR: '/tmp/cfl-artifacts' + CFL_CACHE_DIR: '/ccache/cfl-cache' + CFL_IMAGE: 'gcr.io/oss-fuzz-base/clusterfuzzlite-run-fuzzers' + CFL_PLATFORM: gitlab + FUZZ_SECONDS: 600 # 10 min (ClusterFuzzLite defaults) + FUZZING_ARGS: '-rss_limit_mb=8192' + LD_LIBRARY_PATH: "/opt/kea/lib:/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:/builds/isc-projects/kea" + PARALLEL_FUZZING: true + CCACHE_BASEDIR: "${CI_PROJECT_DIR}" + CCACHE_DIR: "${CI_PROJECT_DIR}/ccache" - # Setting this variable will affect all Security templates - # (SAST, Dependency Scanning, ...) + # SAST SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers" - # Leave only bandit, flawfinder, semgrep. SAST_EXCLUDED_ANALYZERS: "eslint, spotbugs" -image: "${CI_REGISTRY_IMAGE}:latest" +image: 'registry.gitlab.isc.org/isc-projects/kea:latest' stages: - test + - fuzz + +# Do not run the test stage on pipeline schedule trigger. +.base_rules_for_test_jobs: &rules_for_test_stage + rules: + - if: $CI_PIPELINE_SOURCE != 'schedule' + when: always + - if: $CI_PIPELINE_SOURCE == 'schedule' + when: never are-database-scripts-in-sync: stage: test + <<: *rules_for_test_stage script: - ./src/share/database/scripts/utils/are-scripts-in-sync.py check-for-json-errors-in-doc: stage: test + <<: *rules_for_test_stage script: - ./tools/check-for-json-errors-in-doc.sh danger: stage: test + <<: *rules_for_test_stage before_script: - export CI_MERGE_REQUEST_ID=$(git ls-remote -q origin merge-requests\*\head | grep $CI_COMMIT_SHA | sed 's/.*refs\/merge-requests\/\([0-9]*\)\/head/\1/g') - export CI_PROJECT_PATH=$CI_PROJECT_ID #some version of gitlab has problems with searching by project path @@ -38,26 +58,31 @@ danger: duplicate-includes: stage: test + <<: *rules_for_test_stage script: - ./tools/check-for-duplicate-includes.sh duplicate-log-messages: stage: test + <<: *rules_for_test_stage script: - ./tools/check-messages.py uninstalled-headers: stage: test + <<: *rules_for_test_stage script: - ./tools/find-uninstalled-headers.py missing-api-commands: stage: test + <<: *rules_for_test_stage script: - ./tools/check-for-missing-api-commands.sh missing-config-h-include: stage: test + <<: *rules_for_test_stage script: - FILES=$(./tools/add-config-h.sh -n) - printf '%s\n' "${FILES}" @@ -65,6 +90,7 @@ missing-config-h-include: missing-git-attribute: stage: test + <<: *rules_for_test_stage script: - git_diff=$(git diff) - if test -n "${git_diff}"; then printf '%s\n\ngit diff should be empty here under all circumstances. CI broken?\n' "${git_diff}"; exit 1; fi @@ -74,6 +100,7 @@ missing-git-attribute: shellcheck: stage: test + <<: *rules_for_test_stage script: - ./tools/shellcheck-all.sh @@ -87,11 +114,14 @@ shellcheck: - if test -z "${PYTHON_SCRIPTS}"; then echo "No python scripts to check. Exiting early."; exit 0; fi bandit: + stage: test + <<: *rules_for_test_stage script: - bandit -r ./src -x ./.git pycodestyle: stage: test + <<: *rules_for_test_stage script: # - *get_modified_files # - INPUT="${MODIFIED_FILES}" @@ -100,6 +130,7 @@ pycodestyle: pylint: stage: test + <<: *rules_for_test_stage script: # - *get_modified_files # - INPUT="${MODIFIED_FILES}" @@ -108,6 +139,150 @@ pylint: # If we reached this point, it means pylint passed. Run again with all warnings enabled, but ignore the return code to show a list of improvements that the developer could do, even when CI is passing. - pylint --jobs "$(nproc || gnproc || echo 1)" --rcfile ./.gitlab/ci/pylint.rc --enable all ${PYTHON_SCRIPTS} || true + +############################## Fuzzing ############################## + +# Fuzz code changes. Fuzzes all merge requests. +fuzz: + image: + name: "${CFL_IMAGE}" + entrypoint: [''] + stage: fuzz + tags: + - docker-fuzz + needs: [] + parallel: + matrix: + - SANITIZER: [address, undefined] + rules: + # On merge request. + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + variables: + MODE: "code-change" + when: manual + allow_failure: true + # And on push to master. + - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH + when: always + before_script: + # Get GitLab's container id. + - export CFL_CONTAINER_ID=`docker ps -q -f "label=com.gitlab.gitlab-runner.job.id=$CI_JOB_ID" -f "label=com.gitlab.gitlab-runner.type=build"` + script: + # local cfl-cache to mounted volume + - if ! test -L cfl-cache; then ln -s /cfl-cache cfl-cache; fi + # Will build and run the fuzzers. + - python3 "/opt/oss-fuzz/infra/cifuzz/cifuzz_combined_entrypoint.py" + artifacts: + # Upload artifacts when a crash makes the job fail. + when: always + expire_in: 30 days + paths: + - "${CFL_ARTIFACTS_DIR}" + +# Batch fuzzing enables continuous, regular fuzzing on your latest HEAD +# and allows a corpus of inputs to build up over time, which greatly improves +# the effectiveness of fuzzing. Batch fuzzing should be run on a schedule. +fuzz-batch: + image: + name: "${CFL_IMAGE}" + entrypoint: [''] + stage: fuzz + needs: [] + tags: + - docker-fuzz + variables: + FUZZ_SECONDS: 86400 # 24 hours + rules: + - if: $MODE == "batch" + before_script: + - export CFL_CONTAINER_ID=`docker ps -q -f "label=com.gitlab.gitlab-runner.job.id=$CI_JOB_ID" -f "label=com.gitlab.gitlab-runner.type=build"` + script: + - python3 "/opt/oss-fuzz/infra/cifuzz/cifuzz_combined_entrypoint.py" + artifacts: + when: always + expire_in: 30 days + paths: + - "${CFL_ARTIFACTS_DIR}" + +# Corpus pruning is a helper function that minimizes the corpuses by +# removing corpus files (testcases) that do not increase the fuzzer’s +# code coverage. +fuzz-prune: + image: + name: "${CFL_IMAGE}" + entrypoint: [''] + stage: fuzz + needs: [] + tags: + - docker-fuzz + rules: + - if: $MODE == "prune" + before_script: + - export CFL_CONTAINER_ID=`docker ps -q -f "label=com.gitlab.gitlab-runner.job.id=$CI_JOB_ID" -f "label=com.gitlab.gitlab-runner.type=build"` + script: + - python3 "/opt/oss-fuzz/infra/cifuzz/cifuzz_combined_entrypoint.py" + artifacts: + when: always + expire_in: 30 days + paths: + - "${CFL_ARTIFACTS_DIR}" + +# Continuous builds are used when a crash is found during MR fuzzing to determine +# whether the crash was newly introduced. If the crash was not newly introduced, +# MR fuzzing will not report it. This means that there will be fewer unrelated +# failures when running code change fuzzing. +fuzz-build: + image: + name: "${CFL_IMAGE}" + entrypoint: [''] + stage: fuzz + needs: [] + tags: + - docker-fuzz + rules: + # Use $CI_DEFAULT_BRANCH or $CFL_BRANCH. + - if: $CI_COMMIT_BRANCH == $CFL_BRANCH && $CI_PIPELINE_SOURCE == "push" + variables: + MODE: "code-change" + UPLOAD_BUILD: "true" + before_script: + - export CFL_CONTAINER_ID=`docker ps -q -f "label=com.gitlab.gitlab-runner.job.id=$CI_JOB_ID" -f "label=com.gitlab.gitlab-runner.type=build"` + script: + - python3 "/opt/oss-fuzz/infra/cifuzz/cifuzz_combined_entrypoint.py" + artifacts: + when: always + expire_in: 30 days + paths: + - "${CFL_ARTIFACTS_DIR}" + +# scheduled job - generates periodic coverage reports +fuzz-coverage: + image: + name: "${CFL_IMAGE}" + entrypoint: [''] + stage: fuzz + needs: [] + tags: + - docker-fuzz + variables: + SANITIZER: "coverage" + rules: + - if: $MODE == "coverage" + before_script: + - export CFL_CONTAINER_ID=`cut -c9- < /proc/1/cpuset` + script: + - python3 "/opt/oss-fuzz/infra/cifuzz/cifuzz_combined_entrypoint.py" + after_script: + - shasum /opt/kea/sbin/* + - shasum /tmp/not-out/*/* + - shasum ${OUT}/*/* + artifacts: + when: always + expire_in: 30 days + paths: + - "${CFL_ARTIFACTS_DIR}" + + ############################### SAST ################################ # Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/sast/ # @@ -119,9 +294,16 @@ include: .sast-analyzer: extends: sast + stage: test + <<: *rules_for_test_stage allow_failure: true script: - /analyzer run + rules: + - if: $SAST_DISABLED + when: never + - if: $CI_PIPELINE_SOURCE == 'schedule' + when: never flawfinder-sast: extends: .sast-analyzer @@ -131,11 +313,12 @@ flawfinder-sast: SAST_ANALYZER_IMAGE_TAG: latest SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/flawfinder:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED - when: never - if: $SAST_EXCLUDED_ANALYZERS =~ /flawfinder/ when: never - if: $CI_COMMIT_BRANCH exists: - '**/*.cc' - '**/*.h' + +semgrep-sast: + extends: .sast-analyzer diff --git a/Makefile.am b/Makefile.am index bf891b487d..f1ae0dffff 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2,7 +2,7 @@ ACLOCAL_AMFLAGS = -I m4macros ${ACLOCAL_FLAGS} # ^^^^^^^^ This has to be the first line and cannot come later in this # Makefile.am due to some bork in some versions of autotools. -SUBDIRS = tools . ext src doc m4macros @PREMIUM_DIR@ @CONTRIB_DIR@ +SUBDIRS = tools . ext src fuzz doc m4macros @PREMIUM_DIR@ @CONTRIB_DIR@ USE_LCOV=@USE_LCOV@ LCOV=@LCOV@ @@ -173,7 +173,6 @@ cppcheck: docs: $(MAKE) -C doc/sphinx - # These steps are necessary during installation install-exec-hook: mkdir -p $(DESTDIR)${localstatedir}/log/ @@ -188,3 +187,5 @@ CLEANFILES = $(abs_top_builddir)/logger_lockfile # config.h may be included by headers supplied for building user-written # hooks libraries, so we need to include it in the distribution. pkginclude_HEADERS = config.h kea_version.h + +.PHONY: clean-coverage coverage cppcheck docs report-coverage diff --git a/configure.ac b/configure.ac index 59dbdee819..2aa669f9aa 100644 --- a/configure.ac +++ b/configure.ac @@ -1428,6 +1428,10 @@ AC_ARG_ENABLE([fuzzing], AC_MSG_RESULT("no. Fuzzing requires C++17 support.") AC_MSG_ERROR("Fuzzing requires C++17 support.") fi + if test "${enable_gtest}" = 'no'; then + AC_MSG_RESULT("no. Fuzzing requires gtest to be enabled.") + AC_MSG_ERROR("Fuzzing requires gtest to be enabled.") + fi enable_fuzzing=${enableval}], [enable_fuzzing=no] ) @@ -1503,6 +1507,7 @@ if (echo ${runstatedir} | grep -q localstatedir); then runstatedir="$(eval echo ${runstatedir})" fi +AC_CONFIG_FILES([kea_version.h]) AC_CONFIG_FILES([Makefile]) AC_CONFIG_FILES([doc/Makefile]) AC_CONFIG_FILES([doc/sphinx/Makefile]) @@ -1510,7 +1515,9 @@ AC_CONFIG_FILES([doc/devel/Makefile]) AC_CONFIG_FILES([ext/Makefile]) AC_CONFIG_FILES([ext/gtest/Makefile]) AC_CONFIG_FILES([ext/coroutine/Makefile]) -AC_CONFIG_FILES([kea_version.h]) +AC_CONFIG_FILES([fuzz/Makefile]) +AC_CONFIG_FILES([fuzz/input/Makefile]) +AC_CONFIG_FILES([fuzz/tests/Makefile]) AC_CONFIG_FILES([m4macros/Makefile]) AC_CONFIG_FILES([src/Makefile]) AC_CONFIG_FILES([src/bin/Makefile]) diff --git a/doc/devel/fuzz.dox b/doc/devel/fuzz.dox index f4ec93121c..c2b5091f72 100644 --- a/doc/devel/fuzz.dox +++ b/doc/devel/fuzz.dox @@ -1,10 +1,11 @@ -// Copyright (C) 2017-2021 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2017-2024 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. /** + @page fuzzer Fuzzing Kea @section fuzzIntro Introduction @@ -13,13 +14,125 @@ Fuzzing is a software-testing technique whereby a program is presented with a variety of generated data as input and is monitored for abnormal conditions such as crashes or hangs. -Fuzz testing of Kea uses the AFL (American Fuzzy Lop) program. In this, Kea is -built using an AFL-supplied program that not only compiles the software but -also instruments it. When run, AFL generates test cases and monitors the -execution of Kea as it processes them. AFL will adjust the input based on -these measurements, seeking to discover and test new execution paths. +There are two ways to fuzz Kea. + +Option 1. With the libfuzzer harness function LLVMFuzzerTestOneInput. + +Option 2. With the AFL (American Fuzzy Lop) compiler. + +@section LLVMFuzzerTestOneInput Using the LLVMFuzzerTestOneInput Harness Function + +This mode of fuzzing works with virtually any compiler. + +There are four types of fuzzers implemented with this mode: +- Config fuzzer +- HTTP endpoint fuzzer +- Packet fuzzer +- Unix socket fuzzer + +There are two binaries under test: +- `kea-dhcp4` +- `kea-dhcp6` + +Combining the binaries and the fuzzer types results in seven fuzzing binaries: +- `fuzz/fuzz-config-kea-dhcp4` +- `fuzz/fuzz-config-kea-dhcp6` +- `fuzz/fuzz-http-endpoint` +- `fuzz/fuzz-packets-kea-dhcp4` +- `fuzz/fuzz-packets-kea-dhcp6` +- `fuzz/fuzz-unix-socket-kea-dhcp4` +- `fuzz/fuzz-unix-socket-kea-dhcp6` + +@subsection HowToBuild How to Build the LLVM Fuzzer + +Use the "--enable-fuzzing" during the configure step. Then just compile as usual. + +@code +./configure --enable-fuzzing +make +@endcode + +You can check that `config.report` shows these lines: + +@code +Developer: +[...] + Fuzzing: yes + AFL: no +@endcode + +Compiling with AFL is permitted, but is not required. + +@subsection HowToRun How to Run the LLVM Fuzzer + +Each of these binaries has two ways to run. It tries to find a directory called +`input/<name>` relative to the binary where `<name>` is the name of the binary. + +- The first mode: if the directory exists, it recursively takes all the files +from that directory and provides them as fuzz input one-by-one. All the fuzzers +have an empty file and a one-byte file as inputs committed to the repository. +Config fuzzers also have all the files in `doc/examples/kea[46]` symlinked. -@section fuzzTypes Types of Kea Fuzzing +- The second mode: if the directory doesn't exist, then it accepts input from +stdin, just like the old fuzzer did. In this mode, a fuzzer engine can be run on +it. This is the mode used in CI. + +After compiling, all the fuzzers can be run with `make check` in the `fuzz` +directory. The reasoning behind this is that while writing code, developers can +quickly check if anything is broken. Obviously, this is not real fuzzing as long +since the input from the `fuzz/input` directory is the same, but it rather tests +if the fuzzers were broken during development. + +`make check` runs these fuzzers with `sudo`. It may interrupt the process asking +for a password on systems that don't have passwordless root set up. + +@subsection FuzzingStructure The Code Structure of the LLVM Fuzzer + +The following functions are required to be implemented in each new fuzzer: + +- `int LLVMFuzzerInitialize();` - Does initialization that is required by the +fuzzing. Is only run once. Is run automatically. It does not need to be run +explicitly by the fuzzing engine. + +- `int LLVMFuzzerTearDown();` - Cleans up the setup like removing leftover +files. Is automatically run at the beginning and the end of the fuzzing. It does +not need to be run explicitly by the fuzzing engine. + +- `int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size);` - Implements +the actual fuzzing. Takes the parameter input and achieves the object of the +fuzzing with it. It needs to start with +`static bool initialized(DoInitialization());` to do the initialization only +once. This function is the only one that needs to be run explicitly by the +fuzzing engine. + +The following functions are common to all fuzzers: + +- `int main(int, char* argv[])` - Implements the input searching mentioned +above. + +- `bool DoInitialization();` - Sets up logging to prevent spurious logging and +calls `int LLVMFuzzerInitialize();`. + +- `void writeToFile(std::string const& file, std::string const& content);` - +A helpful function to write to file used in some fuzzers. + +@subsection FuzzingConsiderations Development Considerations About LLVM Fuzzing in Kea + +Exceptions make it difficult to maintain a fuzzer. We have to triage some of the +exceptions. For example, JSONError is thrown when an invalid JSON is provided as +input in config fuzzing. That leads to a core dump and can be interpreted as a +crash by the fuzzing engine, which is not really what we're interested in +because this exception is caught in the kea-dhcp[46] binaries. The old way of +fuzzing may have been better from this point of view, because there was the +guarantee that the right exceptions were caught and nothing more and we didn't +have to pay attention to what exceptions needed to be ignored and which weren't. + +@section usingAFL Using AFL + +In this, Kea is built using an AFL-supplied program that not only compiles the +software but also instruments it. When run, AFL generates test cases and +monitors the execution of Kea as it processes them. AFL will adjust the input +based on these measurements, seeking to discover and test new execution paths. @subsection fuzzTypeNetwork Fuzzing with Network Packets @@ -38,7 +151,7 @@ dictionary of valid keywords and runs Kea in configuration file check mode on them. As with network packet fuzzing, the behaviour of Kea is monitored and the content of subsequent files adjusted accordingly. -@section fuzzBuild Building Kea for Fuzzing +@subsection fuzzBuild Building Kea for Fuzzing Whatever tests are done, Kea needs to be built with fuzzing in mind. The steps for this are: @@ -53,15 +166,15 @@ for this are: -# Build Kea. Kea should be compiled and built as usual, although the following additional steps should be observed: - - Set the environment variable CXX to point to the afl-clang-fast++ + - Set the environment variable CXX to point to the afl-clang-fast compiler. - Specify a value of "--prefix" on the command line to set the directory into which Kea is installed. - - Add the "--enable-fuzz" switch to the "configure" command line. + - Add the "--enable-fuzzing" switch to the "configure" command line. . For example: @code - CXX=/opt/afl/afl-clang-fast++ ./configure --enable-fuzz --prefix=$HOME/installed + CXX=afl-clang-fast ./configure --enable-fuzzing --prefix=$HOME/installed make @endcode @@ -80,8 +193,6 @@ for this are: simpler to install the programs in the directories set by "--prefix" and run them from there. -@section fuzzRun Running the Fuzzer - @subsection fuzzRunNetwork Fuzzing with Network Packets -# In this type of fuzzing, Kea is processing packets from the fuzzer over a @@ -95,20 +206,20 @@ for this are: using the loopback interface "lo" and IPv4 address 10.53.0.1, the configuration file would contain the following snippet: @code + { "Dhcp4": { - : "interfaces-config": { - "interfaces": ["lo/10.53.0.1"] + "interfaces": [ + "lo/10.53.0.1" + ] }, "subnet4": [ { - : - "interface": "lo", - : + "interface": "lo" } - ], - : + ] } + } @endcode -# The specification of the interface and address in the configuration file @@ -140,7 +251,7 @@ for this are: data. Ensure that only the payload of the UDP packet is exported. - The "-o" switch specifies a directory (in this example called "fuzz-out") that AFL will use to hold packets it has generated and packets that it has - found causes crashes or hangs. + found causing crashes or hangs. - "--" Separates the AFL command line from that of Kea. - "./kea-dhcp6" is the program being fuzzed. As mentioned above, this should be an executable image, and it will be simpler to fuzz one @@ -164,7 +275,7 @@ for this are: @subsection fuzzRunConfig Fuzzing with Configuration Files AFL can be used to check the parsing of the configuration files. In this type -of fuzzing, AFL generates configuration files which is passes to Kea to check. +of fuzzing, AFL generates configuration files which it passes to Kea to check. Steps for this fuzzing are: -# Build Kea as described above. @@ -201,8 +312,6 @@ Steps for this fuzzing are: will replace these with the name of a file it has created when starting Kea. -@section Fuzzing Internals - @subsection fuzzInternalNetwork Fuzzing with Network Packets The AFL fuzzer delivers packets to Kea's stdin. Although the part of Kea @@ -217,7 +326,7 @@ while (not shutting down) { Read and process one packet } @endcode -When --enable-fuzz is specified, this is conceptually modified to: +When --enable-fuzzing is specified, this is conceptually modified to: @code{.unparsed} while (not shutting down) { Read stdin and copy data to address/port on which Kea is listening @@ -225,10 +334,10 @@ while (not shutting down) { } @endcode -Implementation is via an object of class "Fuzz". When created, it identifies -an interface, address and port on which Kea is listening and creates the -appropriate address structures for these. The port is passed as an argument to -the constructor because at the point at which the object is constructed, that +Implementation is via an object of class "PacketFuzzer". When created, it +identifies an interface, address and port on which Kea is listening and creates +the appropriate address structures for these. The port is passed as an argument +to the constructor because at the point at which the object is constructed, that information is readily available. The interface and address are picked up from the environment variables mentioned above. Consideration was given to extracting the interface and address information from the configuration file, @@ -264,7 +373,7 @@ leaks). No changes were required to Kea source code to fuzz configuration files. In fact, other than compiling with afl-clang++ and installing the resultant executable, no other steps are required. In particular, there is no need to -use the "--enable-fuzz" switch in the configuration command line (although +use the "--enable-fuzzing" switch in the configuration command line (although doing so will not cause any problems). @subsection fuzzThreads Changes Required for Multi-Threaded Kea @@ -278,17 +387,15 @@ above was adopted for the single-threaded Kea 1.6. Should Kea be modified to become multi-threaded, the fuzzing code will need to be changed back to reading the AFL input in the background. -@section fuzzNotes Notes - @subsection fuzzNotesUnitTests Unit Test Failures -If unit tests are built when --enable-fuzzing is specified, note that tests -which check or use the DHCP servers (i.e. the unit tests in src/bin/dhcp4, -src/bin/dhcp6 and src/bin/kea-admin) will fail. With no AFL-related -environment variables defined, a C++ exception will be thrown with the -description "no fuzzing interface has been set". However, if the -KEA_AFL_INTERFACE and KEA_AFL_ADDRESS variables are set to valid values, the -tests will hang. +If unit tests are built when --enable-fuzzing is specified and with the AFL +compiler, note that tests which check or use the DHCP servers (i.e. the unit +tests in src/bin/dhcp4, src/bin/dhcp6 and src/bin/kea-admin) will fail. +With no AFL-related environment variables defined, a C++ exception will be +thrown with the description "no fuzzing interface has been set". +However, if the `KEA_AFL_INTERFACE` and `KEA_AFL_ADDRESS` variables are set to +valid values, the tests will hang. Both these results are expected and should cause no concern. The exception is thrown by the fuzzing object constructor when it attempts to create the address @@ -299,5 +406,4 @@ the test. (Should random input be supplied on stdin, e.g. from the keyboard, the test will most likely fail as the input is unlikely to be that expected by the test.) - */ diff --git a/doc/sphinx/debug-messages.rst b/doc/sphinx/debug-messages.rst index 4ceac072a3..407a1aa7c4 100644 --- a/doc/sphinx/debug-messages.rst +++ b/doc/sphinx/debug-messages.rst @@ -168,8 +168,6 @@ Messages printed on debuglevel 40 - DHCP4_CLASSES_ASSIGNED_AFTER_SUBNET_SELECTION - DHCP4_CLASS_ASSIGNED - DHCP4_CLASS_UNCONFIGURED -- DHCP4_CLASS_UNDEFINED -- DHCP4_CLASS_UNTESTABLE - DHCP4_DHCP4O6_HOOK_SUBNET4_SELECT_DROP - DHCP4_DHCP4O6_HOOK_SUBNET4_SELECT_SKIP - DHCP4_DHCP4O6_PACKET_RECEIVED @@ -186,6 +184,8 @@ Messages printed on debuglevel 40 - DHCP4_LEASE_QUERY_RECEIVED - DHCP4_LEASE_QUERY_RESPONSE_SENT - DHCP4_PACKET_QUEUE_FULL +- DHCP4_REQUIRED_CLASS_NO_TEST +- DHCP4_REQUIRED_CLASS_UNDEFINED - DHCP4_SHUTDOWN - DHCP4_SHUTDOWN_REQUEST - DHCP6_BUFFER_RECEIVED @@ -193,8 +193,6 @@ Messages printed on debuglevel 40 - DHCP6_CLASSES_ASSIGNED_AFTER_SUBNET_SELECTION - DHCP6_CLASS_ASSIGNED - DHCP6_CLASS_UNCONFIGURED -- DHCP6_CLASS_UNDEFINED -- DHCP6_CLASS_UNTESTABLE - DHCP6_DHCP4O6_PACKET_RECEIVED - DHCP6_FLEX_ID - DHCP6_HOOK_BUFFER_SEND_SKIP @@ -213,6 +211,8 @@ Messages printed on debuglevel 40 - DHCP6_LEASE_QUERY_REPLY_SENT - DHCP6_PACKET_PROCESS_FAIL - DHCP6_PACKET_QUEUE_FULL +- DHCP6_REQUIRED_CLASS_NO_TEST +- DHCP6_REQUIRED_CLASS_UNDEFINED - DHCP6_REQUIRED_OPTIONS_CHECK_FAIL - DHCP6_SHUTDOWN - DHCP6_SHUTDOWN_REQUEST diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000000..49369083ff --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,7 @@ +/fuzz_config_kea_dhcp4 +/fuzz_config_kea_dhcp6 +/fuzz_http_endpoint +/fuzz_packets_kea_dhcp4 +/fuzz_packets_kea_dhcp6 +/fuzz_unix_socket_kea_dhcp4 +/fuzz_unix_socket_kea_dhcp6 diff --git a/fuzz/Makefile.am b/fuzz/Makefile.am new file mode 100644 index 0000000000..16da3af0b7 --- /dev/null +++ b/fuzz/Makefile.am @@ -0,0 +1,88 @@ +SUBDIRS = . input tests + +if FUZZING + +AM_CPPFLAGS = +AM_CPPFLAGS += -I$(top_builddir)/src/bin -I$(top_srcdir)/src/bin +AM_CPPFLAGS += -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib +AM_CPPFLAGS += -DKEA_FUZZ_DIR_INSTALLATION=\"$(datarootdir)/$(PACKAGE_NAME)/fuzzing\" +AM_CPPFLAGS += -DKEA_FUZZ_DIR_SOURCES=\"$(abs_top_builddir)/fuzz\" +AM_CPPFLAGS += -DKEA_LFC_INSTALLATION=\"$(prefix)/sbin/kea-lfc\" +AM_CPPFLAGS += -DKEA_LFC_SOURCES=\"$(abs_top_builddir)/src/bin/lfc/kea-lfc\" +AM_CPPFLAGS += $(BOOST_INCLUDES) +AM_CPPFLAGS += $(GTEST_INCLUDES) + +AM_CXXFLAGS = +AM_CXXFLAGS += $(KEA_CXXFLAGS) + +CLEANFILES = *.gcno *.gcda + +sbin_PROGRAMS = +sbin_PROGRAMS += fuzz_config_kea_dhcp4 +sbin_PROGRAMS += fuzz_config_kea_dhcp6 +sbin_PROGRAMS += fuzz_http_endpoint +sbin_PROGRAMS += fuzz_packets_kea_dhcp4 +sbin_PROGRAMS += fuzz_packets_kea_dhcp6 +sbin_PROGRAMS += fuzz_unix_socket_kea_dhcp4 +sbin_PROGRAMS += fuzz_unix_socket_kea_dhcp6 + +V6_LDADD = $(top_builddir)/src/bin/dhcp6/libdhcp6.la +V4_LDADD = $(top_builddir)/src/bin/dhcp4/libdhcp4.la +LDADD = +LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la +LDADD += $(top_builddir)/src/lib/process/libkea-process.la +LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la +LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la +LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la +LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la +LDADD += $(top_builddir)/src/lib/http/libkea-http.la +LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la +LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la +LDADD += $(top_builddir)/src/lib/database/libkea-database.la +LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la +LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la +LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la +LDADD += $(top_builddir)/src/lib/log/libkea-log.la +LDADD += $(top_builddir)/src/lib/util/libkea-util.la +LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la +LDADD += $(LOG4CPLUS_LIBS) $(CRYPTO_LIBS) $(BOOST_LIBS) +LDADD += $(GTEST_LDADD) + +AM_LDFLAGS = $(GTEST_LDFLAGS) + +common_sources = fuzz.cc fuzz.h +if FUZZING_IN_CI +AM_CXXFLAGS += -fsanitize=fuzzer -gdwarf-4 +else +common_sources += main.cc +endif + +fuzz_config_kea_dhcp4_SOURCES = $(common_sources) +fuzz_config_kea_dhcp4_SOURCES += fuzz_config_kea_dhcp4.cc +fuzz_config_kea_dhcp4_LDADD = $(V4_LDADD) $(LDADD) + +fuzz_config_kea_dhcp6_SOURCES = $(common_sources) +fuzz_config_kea_dhcp6_SOURCES += fuzz_config_kea_dhcp6.cc +fuzz_config_kea_dhcp6_LDADD = $(V6_LDADD) $(LDADD) + +fuzz_http_endpoint_SOURCES = $(common_sources) +fuzz_http_endpoint_SOURCES += fuzz_http_endpoint.cc +fuzz_http_endpoint_LDADD = $(LDADD) + +fuzz_packets_kea_dhcp4_SOURCES = $(common_sources) +fuzz_packets_kea_dhcp4_SOURCES += fuzz_packets_kea_dhcp4.cc +fuzz_packets_kea_dhcp4_LDADD = $(V4_LDADD) $(LDADD) + +fuzz_packets_kea_dhcp6_SOURCES = $(common_sources) +fuzz_packets_kea_dhcp6_SOURCES += fuzz_packets_kea_dhcp6.cc +fuzz_packets_kea_dhcp6_LDADD = $(V6_LDADD) $(LDADD) + +fuzz_unix_socket_kea_dhcp4_SOURCES = $(common_sources) +fuzz_unix_socket_kea_dhcp4_SOURCES += fuzz_unix_socket_kea_dhcp4.cc +fuzz_unix_socket_kea_dhcp4_LDADD = $(V4_LDADD) $(LDADD) + +fuzz_unix_socket_kea_dhcp6_SOURCES = $(common_sources) +fuzz_unix_socket_kea_dhcp6_SOURCES += fuzz_unix_socket_kea_dhcp6.cc +fuzz_unix_socket_kea_dhcp6_LDADD = $(V6_LDADD) $(LDADD) + +endif # FUZZING diff --git a/fuzz/dict.dat b/fuzz/dict.dat new file mode 100644 index 0000000000..d235ea808b --- /dev/null +++ b/fuzz/dict.dat @@ -0,0 +1 @@ +PD_POOLS="pd-pools" diff --git a/fuzz/fuzz.cc b/fuzz/fuzz.cc new file mode 100644 index 0000000000..f77354f44e --- /dev/null +++ b/fuzz/fuzz.cc @@ -0,0 +1,76 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <fuzz.h> + +#include <log/logger_support.h> +#include <process/daemon.h> +#include <util/filesystem.h> +#include <util/encode/encode.h> + +#include <cassert> +#include <string> + +using namespace isc::process; +using namespace isc::util::encode; +using namespace isc::util::file; +using namespace std; + +extern "C" { + +string KEA_LFC = isFile(KEA_LFC_INSTALLATION) ? KEA_LFC_INSTALLATION : KEA_LFC_SOURCES; +// string KEA_FUZZ_DIR = isFile(KEA_FUZZ_DIR_INSTALLATION) ? KEA_FUZZ_DIR_INSTALLATION : KEA_FUZZ_DIR_SOURCES; +TemporaryDirectory TEMP_DIR = TemporaryDirectory(); +string KEA_FUZZ_DIR = TEMP_DIR.dirName(); + +bool +DoInitialization() { + LLVMFuzzerTearDown(); + + // Spoof the logger just enough to not get LoggingNotInitialized thrown. + // We explicitly don't want any logging during fuzzing for performance reasons. + setenv("KEA_LOCKFILE_DIR", KEA_FUZZ_DIR.c_str(), 0); + setenv("KEA_LFC_EXECUTABLE", "/bin/true", 0); + if (!getenv("DEBUG")) { + setenv("KEA_LOGGER_DESTINATION", "/dev/null", 0); + } + setenv("KEA_PIDFILE_DIR", KEA_FUZZ_DIR.c_str(), 0); + if (!isc::log::isLoggingInitialized()) { + isc::log::initLogger("fuzzer"); + Daemon::loggerInit("fuzzer", /* verbose = */ false); + Daemon::setDefaultLoggerName("fuzzer"); + } + + return true; +} + +void writeToFile(string const& file, string const& content) { + // Create the config file. + ofstream out(file, ios::out | ios::trunc); + assert(out.is_open()); + out << content; + out.close(); + assert(!out.is_open()); +} + +bool byteStreamToPacketData(uint8_t const* data, size_t size, vector<uint8_t>& byte_stream) { + string str(data, data + size); + if (!str.empty() && str.at(str.size() - 1) == '\n') { + str = str.substr(0, str.size() - 1); + } + if (str.find_first_not_of("0123456789abcdefABCDEF") != string::npos) { + return false; + } + if (str.size() % 2) { + return false; + } + decodeHex(str, byte_stream); + return true; +} + +} // extern "C" diff --git a/fuzz/fuzz.h b/fuzz/fuzz.h new file mode 100644 index 0000000000..6427e83590 --- /dev/null +++ b/fuzz/fuzz.h @@ -0,0 +1,35 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <cstddef> +#include <cstdint> +#include <string> +#include <vector> + +extern "C" { + +extern std::string KEA_FUZZ_DIR; +extern std::string KEA_LFC; + +bool +DoInitialization(); + +int +LLVMFuzzerInitialize(); + +int +LLVMFuzzerTearDown(); + +int +LLVMFuzzerTestOneInput(uint8_t const* data, size_t size); + +void +writeToFile(std::string const& file, std::string const& content); + +bool +byteStreamToPacketData(uint8_t const* data, size_t size, std::vector<uint8_t>& byte_stream); + +} // extern "C"
\ No newline at end of file diff --git a/fuzz/fuzz_config_kea_dhcp4.cc b/fuzz/fuzz_config_kea_dhcp4.cc new file mode 100644 index 0000000000..7cf779ff9a --- /dev/null +++ b/fuzz/fuzz_config_kea_dhcp4.cc @@ -0,0 +1,72 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <fuzz.h> + +#include <cc/command_interpreter.h> +#include <cc/user_context.h> +#include <dhcp4/ctrl_dhcp4_srv.h> +#include <dhcp4/json_config_parser.h> +#include <dhcp4/parser_context.h> + +#include <cassert> +#include <util/filesystem.h> +#include <string> + +using namespace isc; +using namespace isc::config; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::process; +using namespace std; + +namespace { + +static pid_t const PID(getpid()); +static string const PID_STR(to_string(PID)); +static string const KEA_DHCP4_CONF(KEA_FUZZ_DIR + "/kea-dhcp4-" + PID_STR + ".conf"); + +} // namespace + +extern "C" { + +int +LLVMFuzzerInitialize() { + static bool initialized(DoInitialization()); + assert(initialized); + + return 0; +} + +int +LLVMFuzzerTearDown() { + try { + remove(KEA_DHCP4_CONF.c_str()); + } catch (...) { + } + return 0; +} + +int +LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) { + // Create the config file. + string const string_config(reinterpret_cast<char const*>(data), size); + writeToFile(KEA_DHCP4_CONF, string_config); + + // Configure the server. + ControlledDhcpv4Srv server; + try { + server.init(KEA_DHCP4_CONF); + } catch (BadValue const&) { + } catch (Dhcp4ParseError const&) { + } + + return 0; +} + +} // extern "C" diff --git a/fuzz/fuzz_config_kea_dhcp6.cc b/fuzz/fuzz_config_kea_dhcp6.cc new file mode 100644 index 0000000000..9e11552c8f --- /dev/null +++ b/fuzz/fuzz_config_kea_dhcp6.cc @@ -0,0 +1,72 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <fuzz.h> + +#include <cc/command_interpreter.h> +#include <cc/user_context.h> +#include <dhcp6/ctrl_dhcp6_srv.h> +#include <dhcp6/json_config_parser.h> +#include <dhcp6/parser_context.h> + +#include <cassert> +#include <util/filesystem.h> +#include <string> + +using namespace isc; +using namespace isc::config; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::process; +using namespace std; + +namespace { + +static pid_t const PID(getpid()); +static string const PID_STR(to_string(PID)); +static string const KEA_DHCP6_CONF(KEA_FUZZ_DIR + "/kea-dhcp6-" + PID_STR + ".conf"); + +} // namespace + +extern "C" { + +int +LLVMFuzzerInitialize() { + static bool initialized(DoInitialization()); + assert(initialized); + + return 0; +} + +int +LLVMFuzzerTearDown() { + try { + remove(KEA_DHCP6_CONF.c_str()); + } catch (...) { + } + return 0; +} + +int +LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) { + // Create the config file. + string const string_config(reinterpret_cast<char const*>(data), size); + writeToFile(KEA_DHCP6_CONF, string_config); + + // Configure the server. + ControlledDhcpv6Srv server; + try { + server.init(KEA_DHCP6_CONF); + } catch (BadValue const&) { + } catch (Dhcp6ParseError const&) { + } + + return 0; +} + +} // extern "C" diff --git a/fuzz/fuzz_http_endpoint.cc b/fuzz/fuzz_http_endpoint.cc new file mode 100644 index 0000000000..d9ef1e0834 --- /dev/null +++ b/fuzz/fuzz_http_endpoint.cc @@ -0,0 +1,174 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <cassert> +#include <cstdlib> +#include <iostream> +#include <list> + +#include <fuzz.h> + +#include <asiolink/io_service.h> +#include <asiolink/interval_timer.h> +#include <cc/data.h> +#include <config/cmd_http_listener.h> +#include <http/listener.h> +#include <http/post_request_json.h> +#include <http/response.h> +#include <http/response_json.h> +#include <http/tests/response_test.h> +#include <http/testutils/test_http_client.h> +#include <process/d_controller.h> +#include <util/filesystem.h> +#include <util/multi_threading_mgr.h> + +using namespace isc::asiolink; +using namespace isc::config; +using namespace isc::data; +using namespace isc::process; +using namespace isc::http; +using namespace isc::http::test; +using namespace isc::util; +using namespace std; + +namespace { + +void timeoutHandler() { + cerr << "Timeout occurred while fuzzing!" << endl; + abort(); +} + +/// @brief Represents HTTP POST request with JSON body. +/// +/// In addition to the requirements specified by the @ref PostHttpRequest +/// this class requires that the "Content-Type" is "application/json". +/// +/// This class provides methods to parse and retrieve JSON data structures. +struct PostHttpRequestBytes : PostHttpRequest { + /// @brief Constructor for inbound HTTP request. + explicit PostHttpRequestBytes() : PostHttpRequest() { + requireHeaderValue("Content-Type", "application/json"); + } + + /// @brief Constructor for outbound HTTP request. + /// + /// This constructor adds "Content-Type" header with the value of + /// "application/json" to the context. + /// + /// @param method HTTP method, e.g. POST. + /// @param uri URI. + /// @param version HTTP version. + /// @param host_header Host header to be included in the request. The default + /// is the empty Host header. + /// @param basic_auth Basic HTTP authentication credential. The default + /// is no authentication. + explicit PostHttpRequestBytes(const Method& method, + const string& uri, + const HttpVersion& version, + const HostHttpHeader& host_header = HostHttpHeader(), + const BasicHttpAuthPtr& basic_auth = BasicHttpAuthPtr()) + : PostHttpRequest(method, uri, version, host_header, basic_auth) { + requireHeaderValue("Content-Type", "application/json"); + context()->headers_.push_back(HttpHeaderContext("Content-Type", "application/json")); + } + + /// @brief Sets JSON body for an outbound message. + /// + /// @param body JSON structure to be used as a body. + void setBodyAsBytes(vector<uint8_t> const& input) { + context_->body_ = string(input.begin(), input.end()); + } +}; + +using PostHttpRequestBytesPtr = boost::shared_ptr<PostHttpRequestBytes>; + +ThreadPool<function<void()>> THREAD_POOL; + +static pid_t const PID(getpid()); +static string const PID_STR(to_string(PID)); + +} // namespace + +extern "C" { + +int +LLVMFuzzerInitialize() { + static bool initialized(DoInitialization()); + assert(initialized); + + return 0; +} + +int +LLVMFuzzerTearDown() { + return 0; +} + +int +LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) { + string const address("127.0.0.1"); + int const port(18000); + int const timeout(100000); + + CmdHttpListener listener(IOAddress(address), port); + MultiThreadingMgr::instance().setMode(true); + + // Start the server. + listener.start(); + + // Create a client and specify the URL on which the server can be reached. + IOServicePtr io_service(new IOService()); + IntervalTimer run_io_service_timer(io_service); + HttpClient client(io_service, false); + stringstream ss; + ss << "http://" << address << ":" << port; + Url url(ss.str()); + + // Initiate request to the server. + PostHttpRequestBytesPtr request(new PostHttpRequestBytes( + HttpRequest::Method::HTTP_POST, "/", HttpVersion(1, 1))); + + // Body is a map with a specified parameter included. + vector<uint8_t> const body(data, data + size); + request->setBodyAsBytes(body); + request->finalize(); + HttpResponseJsonPtr response(new HttpResponseJson()); + client.asyncSendRequest( + url, TlsContextPtr(), request, response, + [](boost::system::error_code const&, + HttpResponsePtr const&, + string const&) { + }); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + io_service->getInternalIOService().reset(); + run_io_service_timer.setup(&timeoutHandler, timeout, IntervalTimer::ONE_SHOT); + io_service->runOne(); + io_service->getInternalIOService().reset(); + io_service->poll(); + + // Make sure that the received responses are different. We check that by + // comparing value of the sequence parameters. + if (getenv("DEBUG")) { + if (response) { + cout << response->getBody() << endl; + } else { + cout << "no response" << endl; + } + } + listener.stop(); + io_service->poll(); + client.stop(); + MultiThreadingMgr::instance().setMode(false); + + return 0; +} + +} // extern "C" diff --git a/fuzz/fuzz_packets_kea_dhcp4.cc b/fuzz/fuzz_packets_kea_dhcp4.cc new file mode 100644 index 0000000000..ae335fdc61 --- /dev/null +++ b/fuzz/fuzz_packets_kea_dhcp4.cc @@ -0,0 +1,121 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <fuzz.h> + +#include <cc/command_interpreter.h> +#include <cc/user_context.h> +#include <dhcp4/ctrl_dhcp4_srv.h> +#include <dhcp4/json_config_parser.h> +#include <dhcp4/parser_context.h> +#include <dhcpsrv/packet_fuzzer.h> +#include <util/encode/encode.h> + +#include <cassert> +#include <cstdlib> +#include <util/filesystem.h> +#include <vector> + +using namespace isc; +using namespace isc::config; +using namespace isc::util; +using namespace std; + +namespace { + +static string const KEA_DHCP4_CONF(KEA_FUZZ_DIR + "/kea-dhcp4.conf"); +static string KEA_DHCP4_FUZZING_INTERFACE; +static string KEA_DHCP4_FUZZING_ADDRESS; + +} // namespace + +extern "C" { + +int +LLVMFuzzerInitialize() { + static bool initialized(DoInitialization()); + assert(initialized); + + setenv("KEA_DHCP4_FUZZING_ROTATE_PORT", "true", 0); + + char const* interface(getenv("KEA_DHCP4_FUZZING_INTERFACE")); + KEA_DHCP4_FUZZING_INTERFACE = string(interface ? interface : "lo"); + + char const* address(getenv("KEA_DHCP4_FUZZING_ADDRESS")); + KEA_DHCP4_FUZZING_ADDRESS = string(address ? address : "127.0.0.1"); + + writeToFile(KEA_DHCP4_CONF, R"( + { + "Dhcp4": { + "interfaces-config": { + "dhcp-socket-type": "udp", + "interfaces": [ + ")" + KEA_DHCP4_FUZZING_INTERFACE + R"(" + ] + }, + "lease-database": { + "persist": false, + "type": "memfile" + }, + "subnet4": [ + { + "id": 1, + "pools": [ + { + "pool": "10.0.0.0/8" + } + ], + "subnet": "10.0.0.0/8" + } + ] + } + } + )"); + + // Iterate through the interfaces and expect no errors. + for (IfacePtr const& interface : IfaceMgr::instance().getIfaces()) { + for (string const& error : interface->getErrors()) { + cout << error << endl; + } + assert(interface->getErrors().empty()); + } + + return 0; +} + +int +LLVMFuzzerTearDown() { + try { + remove(KEA_DHCP4_CONF.c_str()); + } catch (...) { + } + return 0; +} + +int +LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) { + vector<uint8_t> byte_stream; + bool const valid(byteStreamToPacketData(data, size, byte_stream)); + if (!valid) { + cout << "Invalid input. Skipping..." << endl; + return 0; + } + + ControlledDhcpv4Srv server; + server.init(KEA_DHCP4_CONF); + + // Fuzz. + PacketFuzzer fuzzer(ControlledDhcpv4Srv::getInstance()->getServerPort(), + KEA_DHCP4_FUZZING_INTERFACE, KEA_DHCP4_FUZZING_ADDRESS); + fuzzer.transfer(byte_stream.data(), byte_stream.size()); + ControlledDhcpv4Srv::getInstance()->runOne(); + + return 0; +} + +} // extern "C" diff --git a/fuzz/fuzz_packets_kea_dhcp6.cc b/fuzz/fuzz_packets_kea_dhcp6.cc new file mode 100644 index 0000000000..2a98d6e66f --- /dev/null +++ b/fuzz/fuzz_packets_kea_dhcp6.cc @@ -0,0 +1,124 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <fuzz.h> + +#include <cc/command_interpreter.h> +#include <cc/user_context.h> +#include <dhcp6/ctrl_dhcp6_srv.h> +#include <dhcp6/json_config_parser.h> +#include <dhcp6/parser_context.h> +#include <dhcpsrv/packet_fuzzer.h> +#include <util/encode/encode.h> + +#include <cassert> +#include <cstdlib> +#include <util/filesystem.h> +#include <vector> + +using namespace isc; +using namespace isc::config; +using namespace isc::util; +using namespace std; + +namespace { + +static string const KEA_DHCP6_CONF(KEA_FUZZ_DIR + "/kea-dhcp6.conf"); +static string KEA_DHCP6_FUZZING_INTERFACE; +static string KEA_DHCP6_FUZZING_ADDRESS; + +} // namespace + +extern "C" { + +int +LLVMFuzzerInitialize() { + static bool initialized(DoInitialization()); + assert(initialized); + + setenv("KEA_DHCP6_FUZZING_ROTATE_PORT", "true", 0); + + char const* interface(getenv("KEA_DHCP6_FUZZING_INTERFACE")); + KEA_DHCP6_FUZZING_INTERFACE = string(interface ? interface : "lo"); + + char const* address(getenv("KEA_DHCP6_FUZZING_ADDRESS")); + KEA_DHCP6_FUZZING_ADDRESS = string(address ? address : "::1"); + + writeToFile(KEA_DHCP6_CONF, R"( + { + "Dhcp6": { + "interfaces-config": { + "interfaces": [ + ")" + KEA_DHCP6_FUZZING_INTERFACE + R"(" + ] + }, + "lease-database": { + "persist": false, + "type": "memfile" + }, + "server-id": { + "type": "EN", + "persist": false + }, + "subnet6": [ + { + "id": 1, + "pools": [ + { + "pool": "2001:db8::/80" + } + ], + "subnet": "2001:db8::/64" + } + ] + } + } + )"); + + // Iterate through the interfaces and expect no errors. + for (IfacePtr const& interface : IfaceMgr::instance().getIfaces()) { + for (string const& error : interface->getErrors()) { + cout << error << endl; + } + assert(interface->getErrors().empty()); + } + + return 0; +} + +int +LLVMFuzzerTearDown() { + try { + remove(KEA_DHCP6_CONF.c_str()); + } catch (...) { + } + return 0; +} + +int +LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) { + vector<uint8_t> byte_stream; + bool const valid(byteStreamToPacketData(data, size, byte_stream)); + if (!valid) { + cout << "Invalid input. Skipping..." << endl; + return 0; + } + + ControlledDhcpv6Srv server; + server.init(KEA_DHCP6_CONF); + + // Fuzz. + PacketFuzzer fuzzer(ControlledDhcpv6Srv::getInstance()->getServerPort(), + KEA_DHCP6_FUZZING_INTERFACE, KEA_DHCP6_FUZZING_ADDRESS); + fuzzer.transfer(byte_stream.data(), byte_stream.size()); + ControlledDhcpv6Srv::getInstance()->runOne(); + + return 0; +} + +} // extern "C" diff --git a/fuzz/fuzz_unix_socket_kea_dhcp4.cc b/fuzz/fuzz_unix_socket_kea_dhcp4.cc new file mode 100644 index 0000000000..c87124d5bc --- /dev/null +++ b/fuzz/fuzz_unix_socket_kea_dhcp4.cc @@ -0,0 +1,122 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <fuzz.h> + +#include <asiolink/io_service.h> +#include <cc/data.h> +#include <config/command_mgr.h> +#include <dhcp4/ctrl_dhcp4_srv.h> +#include <dhcpsrv/cfgmgr.h> +#include <testutils/unix_control_client.h> + +#include <util/filesystem.h> + +#include <cassert> + +using namespace isc::asiolink; +using namespace isc::config; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::dhcp::test; +using namespace isc::util; +using namespace isc::util::file; +using namespace std; + +namespace { + +static pid_t const PID(getpid()); +static string const PID_STR(to_string(PID)); +static string const KEA_DHCP4_CONF(KEA_FUZZ_DIR + "/kea-dhcp4-" + PID_STR + ".conf"); +static string const KEA_DHCP4_CSV(KEA_FUZZ_DIR + "/kea-dhcp4-" + PID_STR + ".csv"); +static string const SOCKET(KEA_FUZZ_DIR + "/kea-dhcp4-ctrl-" + PID_STR + ".sock"); + +static UnixControlClient CLIENT; +static IOServicePtr IO_SERVICE(new IOService()); + +} // namespace + +extern "C" { + +int +LLVMFuzzerInitialize() { + static bool initialized(DoInitialization()); + assert(initialized); + + // "control-socket" is of explicit interest, but we also specify the memfile + // CSV location to make sure that we don't get an error caused by an invalid + // file path. + writeToFile(KEA_DHCP4_CONF, R"( + { + "Dhcp4": { + "control-socket": { + "socket-name": ")" + SOCKET + R"(", + "socket-type": "unix" + }, + "lease-database": { + "name": ")" + KEA_DHCP4_CSV + R"(", + "type": "memfile" + } + } + } + )"); + + // Iterate through the interfaces and expect no errors. + for (IfacePtr const& interface : IfaceMgr::instance().getIfaces()) { + for (string const& error : interface->getErrors()) { + cout << error << endl; + } + assert(interface->getErrors().empty()); + } + + return 0; +} + +int +LLVMFuzzerTearDown() { + try { + remove(KEA_DHCP4_CONF.c_str()); + } catch (...) { + } + try { + remove(KEA_DHCP4_CSV.c_str()); + } catch (...) { + } + try { + remove(SOCKET.c_str()); + } catch (...) { + } + try { + remove((SOCKET + ".lock").c_str()); + } catch (...) { + } + + return 0; +} + +int +LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) { + CfgMgr::instance().clear(); + ControlledDhcpv4Srv server; + server.init(KEA_DHCP4_CONF); + assert(isSocket(SOCKET)); + + string const command(reinterpret_cast<char const*>(data), size); + CLIENT.connectToServer(SOCKET); + CLIENT.sendCommand(command); + ControlledDhcpv4Srv::getInstance()->getIOService()->poll(); + string response; + CLIENT.getResponse(response); + ControlledDhcpv4Srv::getInstance()->getIOService()->poll(); + CLIENT.disconnectFromServer(); + ControlledDhcpv4Srv::getInstance()->getIOService()->poll(); + + return 0; +} + +} // extern "C" diff --git a/fuzz/fuzz_unix_socket_kea_dhcp6.cc b/fuzz/fuzz_unix_socket_kea_dhcp6.cc new file mode 100644 index 0000000000..021dcf48db --- /dev/null +++ b/fuzz/fuzz_unix_socket_kea_dhcp6.cc @@ -0,0 +1,128 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <fuzz.h> + +#include <asiolink/io_service.h> +#include <cc/data.h> +#include <config/command_mgr.h> +#include <dhcp6/ctrl_dhcp6_srv.h> +#include <dhcpsrv/cfgmgr.h> +#include <testutils/unix_control_client.h> + +#include <util/filesystem.h> + +#include <cassert> + +using namespace isc::asiolink; +using namespace isc::config; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::dhcp::test; +using namespace isc::util; +using namespace isc::util::file; +using namespace std; + +namespace { + +static pid_t const PID(getpid()); +static string const PID_STR(to_string(PID)); +static string const KEA_DHCP6_CONF(KEA_FUZZ_DIR + "/kea-dhcp6-" + PID_STR + ".conf"); +static string const KEA_DHCP6_CSV(KEA_FUZZ_DIR + "/kea-dhcp6-" + PID_STR + ".csv"); +static string const SOCKET(KEA_FUZZ_DIR + "/kea-dhcp6-ctrl-" + PID_STR + ".sock"); + +static UnixControlClient CLIENT; +static IOServicePtr IO_SERVICE(new IOService()); + +} // namespace + +extern "C" { + +int +LLVMFuzzerInitialize() { + static bool initialized(DoInitialization()); + assert(initialized); + + // "control-socket" is of explicit interest, but we also specify the memfile + // CSV location and the server-id to make sure that we don't get an error + // caused by an invalid file path. + writeToFile(KEA_DHCP6_CONF, R"( + { + "Dhcp6": { + "control-socket": { + "socket-name": ")" + SOCKET + R"(", + "socket-type": "unix" + }, + "lease-database": { + "name": ")" + KEA_DHCP6_CSV + R"(", + "type": "memfile" + }, + "server-id": { + "type": "EN", + "enterprise-id": 2495, + "identifier": "0123456789", + "persist": false + } + } + } + )"); + + // Iterate through the interfaces and expect no errors. + for (IfacePtr const& interface : IfaceMgr::instance().getIfaces()) { + for (string const& error : interface->getErrors()) { + cout << error << endl; + } + assert(interface->getErrors().empty()); + } + + return 0; +} + +int +LLVMFuzzerTearDown() { + try { + remove(KEA_DHCP6_CONF.c_str()); + } catch (...) { + } + try { + remove(KEA_DHCP6_CSV.c_str()); + } catch (...) { + } + try { + remove(SOCKET.c_str()); + } catch (...) { + } + try { + remove((SOCKET + ".lock").c_str()); + } catch (...) { + } + + return 0; +} + +int +LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) { + CfgMgr::instance().clear(); + ControlledDhcpv6Srv server; + server.init(KEA_DHCP6_CONF); + assert(isSocket(SOCKET)); + + string const command(reinterpret_cast<char const*>(data), size); + CLIENT.connectToServer(SOCKET); + CLIENT.sendCommand(command); + ControlledDhcpv6Srv::getInstance()->getIOService()->poll(); + string response; + CLIENT.getResponse(response); + ControlledDhcpv6Srv::getInstance()->getIOService()->poll(); + CLIENT.disconnectFromServer(); + ControlledDhcpv6Srv::getInstance()->getIOService()->poll(); + + return 0; +} + +} // extern "C" diff --git a/fuzz/input/Makefile.am b/fuzz/input/Makefile.am new file mode 100644 index 0000000000..f1c9e11a64 --- /dev/null +++ b/fuzz/input/Makefile.am @@ -0,0 +1,43 @@ +SUBDIRS = . + +if FUZZING + +fuzzdir = "${datarootdir}/${PACKAGE_NAME}/fuzz" + +nobase_dist_fuzz_DATA = +nobase_dist_fuzz_DATA += fuzz-config-kea-dhcp4/empty +nobase_dist_fuzz_DATA += fuzz-config-kea-dhcp4/one-byte +nobase_dist_fuzz_DATA += fuzz-config-kea-dhcp6/empty +nobase_dist_fuzz_DATA += fuzz-config-kea-dhcp6/one-byte +nobase_dist_fuzz_DATA += fuzz-http-endpoint/config-get +nobase_dist_fuzz_DATA += fuzz-http-endpoint/config-get-with-service +nobase_dist_fuzz_DATA += fuzz-http-endpoint/empty +nobase_dist_fuzz_DATA += fuzz-http-endpoint/empty-json-map +nobase_dist_fuzz_DATA += fuzz-http-endpoint/one-byte +nobase_dist_fuzz_DATA += fuzz-http-endpoint/one-entry-json-map +nobase_dist_fuzz_DATA += fuzz-packets-kea-dhcp4/dhcp-payload-only +nobase_dist_fuzz_DATA += fuzz-packets-kea-dhcp4/empty +nobase_dist_fuzz_DATA += fuzz-packets-kea-dhcp4/full-dhcp-packet +nobase_dist_fuzz_DATA += fuzz-packets-kea-dhcp4/one-byte +nobase_dist_fuzz_DATA += fuzz-packets-kea-dhcp4/udp-header +nobase_dist_fuzz_DATA += fuzz-packets-kea-dhcp6/dhcp-payload-only +nobase_dist_fuzz_DATA += fuzz-packets-kea-dhcp6/empty +nobase_dist_fuzz_DATA += fuzz-packets-kea-dhcp6/full-dhcp-packet +nobase_dist_fuzz_DATA += fuzz-packets-kea-dhcp6/one-byte +nobase_dist_fuzz_DATA += fuzz-packets-kea-dhcp6/udp-header +nobase_dist_fuzz_DATA += fuzz-unix-socket-kea-dhcp4/config-get +nobase_dist_fuzz_DATA += fuzz-unix-socket-kea-dhcp4/config-get-with-service +nobase_dist_fuzz_DATA += fuzz-unix-socket-kea-dhcp4/empty +nobase_dist_fuzz_DATA += fuzz-unix-socket-kea-dhcp4/empty-json-map +nobase_dist_fuzz_DATA += fuzz-unix-socket-kea-dhcp4/one-byte +nobase_dist_fuzz_DATA += fuzz-unix-socket-kea-dhcp4/one-entry-json-map +nobase_dist_fuzz_DATA += fuzz-unix-socket-kea-dhcp6/config-get +nobase_dist_fuzz_DATA += fuzz-unix-socket-kea-dhcp6/config-get-with-service +nobase_dist_fuzz_DATA += fuzz-unix-socket-kea-dhcp6/empty +nobase_dist_fuzz_DATA += fuzz-unix-socket-kea-dhcp6/empty-json-map +nobase_dist_fuzz_DATA += fuzz-unix-socket-kea-dhcp6/one-byte +nobase_dist_fuzz_DATA += fuzz-unix-socket-kea-dhcp6/one-entry-json-map +nobase_dist_fuzz_DATA += kea-dhcp4.conf +nobase_dist_fuzz_DATA += kea-dhcp6.conf + +endif # FUZZING diff --git a/fuzz/input/fuzz-config-kea-dhcp4/doc-examples b/fuzz/input/fuzz-config-kea-dhcp4/doc-examples new file mode 120000 index 0000000000..0b5652bef7 --- /dev/null +++ b/fuzz/input/fuzz-config-kea-dhcp4/doc-examples @@ -0,0 +1 @@ +../../../doc/examples/kea4
\ No newline at end of file diff --git a/fuzz/input/fuzz-config-kea-dhcp4/empty b/fuzz/input/fuzz-config-kea-dhcp4/empty new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/fuzz/input/fuzz-config-kea-dhcp4/empty diff --git a/fuzz/input/fuzz-config-kea-dhcp4/one-byte b/fuzz/input/fuzz-config-kea-dhcp4/one-byte new file mode 100644 index 0000000000..47d26df80d --- /dev/null +++ b/fuzz/input/fuzz-config-kea-dhcp4/one-byte @@ -0,0 +1 @@ +0a
\ No newline at end of file diff --git a/fuzz/input/fuzz-config-kea-dhcp6/doc-examples b/fuzz/input/fuzz-config-kea-dhcp6/doc-examples new file mode 120000 index 0000000000..05f98b3048 --- /dev/null +++ b/fuzz/input/fuzz-config-kea-dhcp6/doc-examples @@ -0,0 +1 @@ +../../../doc/examples/kea6
\ No newline at end of file diff --git a/fuzz/input/fuzz-config-kea-dhcp6/empty b/fuzz/input/fuzz-config-kea-dhcp6/empty new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/fuzz/input/fuzz-config-kea-dhcp6/empty diff --git a/fuzz/input/fuzz-config-kea-dhcp6/one-byte b/fuzz/input/fuzz-config-kea-dhcp6/one-byte new file mode 100644 index 0000000000..47d26df80d --- /dev/null +++ b/fuzz/input/fuzz-config-kea-dhcp6/one-byte @@ -0,0 +1 @@ +0a
\ No newline at end of file diff --git a/fuzz/input/fuzz-http-endpoint/config-get b/fuzz/input/fuzz-http-endpoint/config-get new file mode 100644 index 0000000000..7ce1bfffff --- /dev/null +++ b/fuzz/input/fuzz-http-endpoint/config-get @@ -0,0 +1,3 @@ +{ + "command": "config-get" +} diff --git a/fuzz/input/fuzz-http-endpoint/config-get-with-service b/fuzz/input/fuzz-http-endpoint/config-get-with-service new file mode 100644 index 0000000000..df926792e3 --- /dev/null +++ b/fuzz/input/fuzz-http-endpoint/config-get-with-service @@ -0,0 +1,4 @@ +{ + "command": "config-get", + "service": [ "dhcp6" ] +} diff --git a/fuzz/input/fuzz-http-endpoint/empty b/fuzz/input/fuzz-http-endpoint/empty new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/fuzz/input/fuzz-http-endpoint/empty diff --git a/fuzz/input/fuzz-http-endpoint/empty-json-map b/fuzz/input/fuzz-http-endpoint/empty-json-map new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/fuzz/input/fuzz-http-endpoint/empty-json-map @@ -0,0 +1 @@ +{} diff --git a/fuzz/input/fuzz-http-endpoint/one-byte b/fuzz/input/fuzz-http-endpoint/one-byte new file mode 100644 index 0000000000..47d26df80d --- /dev/null +++ b/fuzz/input/fuzz-http-endpoint/one-byte @@ -0,0 +1 @@ +0a
\ No newline at end of file diff --git a/fuzz/input/fuzz-http-endpoint/one-entry-json-map b/fuzz/input/fuzz-http-endpoint/one-entry-json-map new file mode 100644 index 0000000000..8d6b85c7b3 --- /dev/null +++ b/fuzz/input/fuzz-http-endpoint/one-entry-json-map @@ -0,0 +1,3 @@ +{ + "a": 1 +} diff --git a/fuzz/input/fuzz-packets-kea-dhcp4/dhcp-payload-only b/fuzz/input/fuzz-packets-kea-dhcp4/dhcp-payload-only new file mode 100644 index 0000000000..153050e658 --- /dev/null +++ b/fuzz/input/fuzz-packets-kea-dhcp4/dhcp-payload-only @@ -0,0 +1 @@ +210101060100000000000000000000000000000000000000000a010001000c0102030400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000638253633501013707011c02030f060c3d0701000c01020304ff diff --git a/fuzz/input/fuzz-packets-kea-dhcp4/empty b/fuzz/input/fuzz-packets-kea-dhcp4/empty new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/fuzz/input/fuzz-packets-kea-dhcp4/empty diff --git a/fuzz/input/fuzz-packets-kea-dhcp4/full-dhcp-packet b/fuzz/input/fuzz-packets-kea-dhcp4/full-dhcp-packet new file mode 100644 index 0000000000..802b1e369f --- /dev/null +++ b/fuzz/input/fuzz-packets-kea-dhcp4/full-dhcp-packet @@ -0,0 +1 @@ +000400010006d6b6574a0cce713b0800450001229b384000401194910a010001ffffffff00430043010e0b210101060100000000000000000000000000000000000000000a010001000c0102030400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000638253633501013707011c02030f060c3d0701000c01020304ff diff --git a/fuzz/input/fuzz-packets-kea-dhcp4/one-byte b/fuzz/input/fuzz-packets-kea-dhcp4/one-byte new file mode 100644 index 0000000000..47d26df80d --- /dev/null +++ b/fuzz/input/fuzz-packets-kea-dhcp4/one-byte @@ -0,0 +1 @@ +0a
\ No newline at end of file diff --git a/fuzz/input/fuzz-packets-kea-dhcp4/udp-header b/fuzz/input/fuzz-packets-kea-dhcp4/udp-header new file mode 100644 index 0000000000..fdcab50643 --- /dev/null +++ b/fuzz/input/fuzz-packets-kea-dhcp4/udp-header @@ -0,0 +1 @@ +000400010006d6b6574a0cce713b0800450001229b384000401194910a010001ffffffff00430043010e0b diff --git a/fuzz/input/fuzz-packets-kea-dhcp6/dhcp-payload-only b/fuzz/input/fuzz-packets-kea-dhcp6/dhcp-payload-only new file mode 100644 index 0000000000..6058a7390e --- /dev/null +++ b/fuzz/input/fuzz-packets-kea-dhcp6/dhcp-payload-only @@ -0,0 +1 @@ +010000000001000e000100012b8b4659000c010203040003000c0000000100000e10000015180006000400170018000800020000 diff --git a/fuzz/input/fuzz-packets-kea-dhcp6/empty b/fuzz/input/fuzz-packets-kea-dhcp6/empty new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/fuzz/input/fuzz-packets-kea-dhcp6/empty diff --git a/fuzz/input/fuzz-packets-kea-dhcp6/full-dhcp-packet b/fuzz/input/fuzz-packets-kea-dhcp6/full-dhcp-packet new file mode 100644 index 0000000000..efcaf6c700 --- /dev/null +++ b/fuzz/input/fuzz-packets-kea-dhcp6/full-dhcp-packet @@ -0,0 +1 @@ +000400010006d6b6574a0cce000086dd6001d3f9003c110120010db8000100000000000000000001ff02000000000000000000000001000202220223003c2d0e010000000001000e000100012b8b4659000c010203040003000c0000000100000e10000015180006000400170018000800020000 diff --git a/fuzz/input/fuzz-packets-kea-dhcp6/one-byte b/fuzz/input/fuzz-packets-kea-dhcp6/one-byte new file mode 100644 index 0000000000..47d26df80d --- /dev/null +++ b/fuzz/input/fuzz-packets-kea-dhcp6/one-byte @@ -0,0 +1 @@ +0a
\ No newline at end of file diff --git a/fuzz/input/fuzz-packets-kea-dhcp6/udp-header b/fuzz/input/fuzz-packets-kea-dhcp6/udp-header new file mode 100644 index 0000000000..95864fd7fb --- /dev/null +++ b/fuzz/input/fuzz-packets-kea-dhcp6/udp-header @@ -0,0 +1 @@ +000400010006d6b6574a0cce000086dd6001d3f9003c110120010db8000100000000000000000001ff02000000000000000000000001000202220223003c2d0e diff --git a/fuzz/input/fuzz-unix-socket-kea-dhcp4/config-get b/fuzz/input/fuzz-unix-socket-kea-dhcp4/config-get new file mode 100644 index 0000000000..7ce1bfffff --- /dev/null +++ b/fuzz/input/fuzz-unix-socket-kea-dhcp4/config-get @@ -0,0 +1,3 @@ +{ + "command": "config-get" +} diff --git a/fuzz/input/fuzz-unix-socket-kea-dhcp4/config-get-with-service b/fuzz/input/fuzz-unix-socket-kea-dhcp4/config-get-with-service new file mode 100644 index 0000000000..11ed27a1c2 --- /dev/null +++ b/fuzz/input/fuzz-unix-socket-kea-dhcp4/config-get-with-service @@ -0,0 +1,4 @@ +{ + "command": "config-get", + "service": [ "dhcp4" ] +} diff --git a/fuzz/input/fuzz-unix-socket-kea-dhcp4/empty b/fuzz/input/fuzz-unix-socket-kea-dhcp4/empty new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/fuzz/input/fuzz-unix-socket-kea-dhcp4/empty diff --git a/fuzz/input/fuzz-unix-socket-kea-dhcp4/empty-json-map b/fuzz/input/fuzz-unix-socket-kea-dhcp4/empty-json-map new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/fuzz/input/fuzz-unix-socket-kea-dhcp4/empty-json-map @@ -0,0 +1 @@ +{} diff --git a/fuzz/input/fuzz-unix-socket-kea-dhcp4/one-byte b/fuzz/input/fuzz-unix-socket-kea-dhcp4/one-byte new file mode 100644 index 0000000000..47d26df80d --- /dev/null +++ b/fuzz/input/fuzz-unix-socket-kea-dhcp4/one-byte @@ -0,0 +1 @@ +0a
\ No newline at end of file diff --git a/fuzz/input/fuzz-unix-socket-kea-dhcp4/one-entry-json-map b/fuzz/input/fuzz-unix-socket-kea-dhcp4/one-entry-json-map new file mode 100644 index 0000000000..8d6b85c7b3 --- /dev/null +++ b/fuzz/input/fuzz-unix-socket-kea-dhcp4/one-entry-json-map @@ -0,0 +1,3 @@ +{ + "a": 1 +} diff --git a/fuzz/input/fuzz-unix-socket-kea-dhcp6/config-get b/fuzz/input/fuzz-unix-socket-kea-dhcp6/config-get new file mode 100644 index 0000000000..7ce1bfffff --- /dev/null +++ b/fuzz/input/fuzz-unix-socket-kea-dhcp6/config-get @@ -0,0 +1,3 @@ +{ + "command": "config-get" +} diff --git a/fuzz/input/fuzz-unix-socket-kea-dhcp6/config-get-with-service b/fuzz/input/fuzz-unix-socket-kea-dhcp6/config-get-with-service new file mode 100644 index 0000000000..df926792e3 --- /dev/null +++ b/fuzz/input/fuzz-unix-socket-kea-dhcp6/config-get-with-service @@ -0,0 +1,4 @@ +{ + "command": "config-get", + "service": [ "dhcp6" ] +} diff --git a/fuzz/input/fuzz-unix-socket-kea-dhcp6/empty b/fuzz/input/fuzz-unix-socket-kea-dhcp6/empty new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/fuzz/input/fuzz-unix-socket-kea-dhcp6/empty diff --git a/fuzz/input/fuzz-unix-socket-kea-dhcp6/empty-json-map b/fuzz/input/fuzz-unix-socket-kea-dhcp6/empty-json-map new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/fuzz/input/fuzz-unix-socket-kea-dhcp6/empty-json-map @@ -0,0 +1 @@ +{} diff --git a/fuzz/input/fuzz-unix-socket-kea-dhcp6/one-byte b/fuzz/input/fuzz-unix-socket-kea-dhcp6/one-byte new file mode 100644 index 0000000000..47d26df80d --- /dev/null +++ b/fuzz/input/fuzz-unix-socket-kea-dhcp6/one-byte @@ -0,0 +1 @@ +0a
\ No newline at end of file diff --git a/fuzz/input/fuzz-unix-socket-kea-dhcp6/one-entry-json-map b/fuzz/input/fuzz-unix-socket-kea-dhcp6/one-entry-json-map new file mode 100644 index 0000000000..8d6b85c7b3 --- /dev/null +++ b/fuzz/input/fuzz-unix-socket-kea-dhcp6/one-entry-json-map @@ -0,0 +1,3 @@ +{ + "a": 1 +} diff --git a/fuzz/input/kea-dhcp4.conf b/fuzz/input/kea-dhcp4.conf new file mode 100644 index 0000000000..7409a46b90 --- /dev/null +++ b/fuzz/input/kea-dhcp4.conf @@ -0,0 +1,20 @@ +{ + "Dhcp4": { + "interfaces-config": { + "interfaces": [ + "*" + ] + }, + "subnet4": [ + { + "id": 1, + "pools": [ + { + "pool": "127.0.0.0/8" + } + ], + "subnet": "127.0.0.0/8" + } + ] + } +} diff --git a/fuzz/input/kea-dhcp6.conf b/fuzz/input/kea-dhcp6.conf new file mode 100644 index 0000000000..868a40244f --- /dev/null +++ b/fuzz/input/kea-dhcp6.conf @@ -0,0 +1,27 @@ +{ + "Dhcp6": { + "interfaces-config": { + "interfaces": [ + "*" + ] + }, + "subnet6": [ + { + "id": 1, + "pd-pools": [ + { + "delegated-len": 120, + "prefix": "2001:db8:1:0:2::", + "prefix-len": 80 + } + ], + "pools": [ + { + "pool": "::/80" + } + ], + "subnet": "::/64" + } + ] + } +} diff --git a/fuzz/input/regenerate-cpp-sources.sh b/fuzz/input/regenerate-cpp-sources.sh new file mode 100755 index 0000000000..56af74827e --- /dev/null +++ b/fuzz/input/regenerate-cpp-sources.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +set -eu + +script_path=$(cd "$(dirname "${0}")" && pwd) + +cd "${script_path}" + +generate() { + source="${1}" + target="${2}" + + source_content=$(cat "${source}") + variable_name=$(echo "${source}" | tr '[:lower:]' '[:upper:]' | sed 's/\./_/' | sed 's/-/_/') + + cat > "${target}" <<HERE_DOCUMENT +#include <string> + +extern std::string ${variable_name} = R"( +${source_content} +)"; +HERE_DOCUMENT +} + +generate kea-dhcp4.conf ../kea-dhcp4.h +generate kea-dhcp6.conf ../kea-dhcp6.h diff --git a/fuzz/kea-dhcp4.h b/fuzz/kea-dhcp4.h new file mode 100644 index 0000000000..7aa8f3e2c7 --- /dev/null +++ b/fuzz/kea-dhcp4.h @@ -0,0 +1,24 @@ +#include <string> + +extern std::string KEA_DHCP4_CONF = R"( +{ + "Dhcp4": { + "interfaces-config": { + "interfaces": [ + "*" + ] + }, + "subnet4": [ + { + "id": 1, + "pools": [ + { + "pool": "127.0.0.0/8" + } + ], + "subnet": "127.0.0.0/8" + } + ] + } +} +)"; diff --git a/fuzz/kea-dhcp6.h b/fuzz/kea-dhcp6.h new file mode 100644 index 0000000000..9ccff020e4 --- /dev/null +++ b/fuzz/kea-dhcp6.h @@ -0,0 +1,31 @@ +#include <string> + +extern std::string KEA_DHCP6_CONF = R"( +{ + "Dhcp6": { + "interfaces-config": { + "interfaces": [ + "*" + ] + }, + "subnet6": [ + { + "id": 1, + "pd-pools": [ + { + "delegated-len": 120, + "prefix": "2001:db8:1:0:2::", + "prefix-len": 80 + } + ], + "pools": [ + { + "pool": "::/80" + } + ], + "subnet": "::/64" + } + ] + } +} +)"; diff --git a/fuzz/libfuzzer.sh b/fuzz/libfuzzer.sh new file mode 100755 index 0000000000..fd10ef0611 --- /dev/null +++ b/fuzz/libfuzzer.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# Copyright (C) 2022-2024 Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +set -eux + +exec "${2}" "${1}/$(basename "${2}").in" -max_total_time=5 -print_pcs=1 -print_final_stats=1 -print_corpus_stats=1 -print_coverage=1 diff --git a/fuzz/main.cc b/fuzz/main.cc new file mode 100644 index 0000000000..b08abaf938 --- /dev/null +++ b/fuzz/main.cc @@ -0,0 +1,124 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <util/filesystem.h> +#include <fuzz.h> + +#include <cassert> +#include <cstdio> +#include <fstream> +#include <iostream> +#include <list> +#include <sstream> +#include <vector> + +#include <dirent.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +using namespace isc::util::file; +using namespace std; + +int +main(int, char* argv[]) { + int const return_code(LLVMFuzzerInitialize()); + assert(return_code == 0); + + // Determine some paths. + Path const this_binary(argv[0]); + string const ancestor_path(Path(this_binary.parentPath()).parentPath()); + string const filename(this_binary.filename()); + stringstream ss; + ss << ancestor_path << "/input/" << filename; + Path const p(ss.str()); + + // Print start header. + if (isatty(fileno(stdin))) { + cout << "\033[92m[ RUN ]\033[0m " << filename << endl; + } else { + cout << "[ RUN ] " << filename << endl; + } + + int exit_code(0); + string directory(p.str()); + if (exists(directory)) { + // Recursively take all regular files as input. + list<string> files; + + struct dirent *dp; + DIR *dfd(opendir(p.str().c_str())); + while ((dp = readdir(dfd)) != nullptr) { + string file(dp->d_name); + if (file == "." || file == "..") { + continue; + } + string entry(directory + '/' + dp->d_name); + + if (!isFile(entry)) { + continue; + } + + // Save file names. + files.push_back(entry); + } + + // Sort the file names so that the order is the same each time. + files.sort(); + + for (string& f : files) { + // Read content from file. + basic_ifstream<uint8_t> file(f, ios::binary); + + if (!file.is_open()) { + cerr << "ERROR: could not open file " << f << endl; + return 1; + } + + // Get the file size. + file.seekg(0, std::ios::end); + streampos const bytes(file.tellg()); + file.seekg(0, std::ios::beg); + + // Read the entire file into a vector. + vector<uint8_t> buffer(bytes); + file.read(buffer.data(), bytes); + + file.close(); + + // Fuzz. + f.replace(f.find(ancestor_path), ancestor_path.size() + 1, string()); + cout << "Fuzzing with " << bytes << " byte" << (bytes == 1 ? string() : "s") << " read from " + << f << "..." << endl; + exit_code |= LLVMFuzzerTestOneInput(buffer.data(), bytes); + } + } else { + // Read input from stdin. + cout << "Waiting on input..." << endl; + vector<uint8_t> buffer(65536); + size_t const bytes(fread(&buffer[0], sizeof(buffer[0]), buffer.size(), stdin)); + + // Fuzz. + cout << "Fuzzing with " << bytes << " byte" << (bytes == 1 ? "" : "s") + << " read from stdin..." << endl; + exit_code |= LLVMFuzzerTestOneInput(buffer.data(), bytes); + } + + // Tear down the setup. + LLVMFuzzerTearDown(); + + // Print end header. + string const result(exit_code == 0 ? " OK " : " FAILED "); + if (isatty(fileno(stdin))) { + cout << "\033[92m[" << result << "]\033[0m " << filename << endl; + } else { + cout << "[" << result << "] " << filename << endl; + } + + return exit_code; +} diff --git a/fuzz/setup.sh b/fuzz/setup.sh new file mode 100755 index 0000000000..17ea9fa54e --- /dev/null +++ b/fuzz/setup.sh @@ -0,0 +1,112 @@ +#!/bin/sh + +# Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# shellcheck disable=all + +# Disable this script altogether for now. +exit 0 + +# Disable this script if the interface and address are used from environment variables. +if test -n "${KEA_DHCP4_FUZZING_INTERFACE+x}" || + test -n "${KEA_DHCP4_FUZZING_ADDRESS+x}" || + test -n "${KEA_DHCP6_FUZZING_INTERFACE+x}" || + test -n "${KEA_DHCP6_FUZZING_ADDRESS+x}"; then + printf 'Environment variables set. Will use those. Abandoning.\n' + exit 0 +fi + +script_path=$(cd "$(dirname "${0}")" && pwd) + +cd "${script_path}" > /dev/null + +# Add sudo to the fuzzers. +sudo='if ! sudo -n true; then exec sudo -- "${0}" "${@}"; fi' +for i in fuzz-*-kea-dhcp[46]; do + continue # Disable this loop for now. + if ! grep -F "${sudo}" "${i}" > /dev/null; then + sed -i "2i${sudo}" "${i}" + fi +done + +# Create kea-dhcp{v}-fuzz-* wrapper scripts which adds the afl-fuzz command prefix to kea-dhcp{v}. +for v in 4 6; do + continue # Disable this loop for now. + executable="../src/bin/dhcp${v}/kea-dhcp${v}" + + for f in config packets unix-socket; do + fuzzed_executable="${executable}-fuzz-${f}" + cp "${executable}" "${fuzzed_executable}" + mkdir -p "output/config/kea-dhcp${v}" + sed -i "s# *exec \"\$progdir/\$program\"#\n\ + export AFL_DEBUG='1'\n\ + export AFL_DEBUG_CHILD='1'\n\ + export AFL_LLVM_MAP_ADDR='true'\n\ + export AFL_MAP_SIZE='10000000'\n\ + export KEA_AFL_ADDRESS='10.1.0.1'\n\ + export KEA_AFL_INTERFACE='vethclient'\n\ + export KEA_AFL_LOOP_MAX=2\n\ + exec afl-fuzz -M fuzzer1 -t 20000+ -m 50000 -i 'seeds/${f}' -o 'output/config/kea-dhcp${v}' -x /opt/dict.dat -- \"\$progdir/\$program\"\ + #g" "${fuzzed_executable}" + sed -i "2i${sudo}" "${fuzzed_executable}" + done +done + +cd - > /dev/null + +# Run again as root. +if ! sudo -n true; then + exec sudo -- "${0}" "${@}" +fi + +# afl-fuzz says: +# To avoid having crashes misinterpreted as timeouts, please log in as root +# and temporarily modify /proc/sys/kernel/core_pattern, like so: +echo core > /proc/sys/kernel/core_pattern + +# afl-fuzz says: +# Whoops, your system uses on-demand CPU frequency scaling, adjusted +# between 781 and 4882 MHz. Unfortunately, the scaling algorithm in the +# kernel is imperfect and can miss the short-lived processes spawned by +# afl-fuzz. To keep things moving, run these commands as root: +echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor > /dev/null + +ulimit -Sd 41932800 + +ulimit -c unlimited + +# Create a virtual interface for the server to start listening on. +if ip link show vethclient > /dev/null 2>&1; then + ip link delete vethclient +fi +if ip link show vethserver > /dev/null 2>&1; then + ip link delete vethserver +fi +ip link add vethclient type veth peer name vethserver +ip -4 addr add 10.1.0.1/24 dev vethclient +ip -6 addr add 2001:db8:1::1/64 dev vethclient +ip link set dev vethclient up +ip link set lo up +ip -4 addr add 10.1.0.2/24 dev vethserver +ip -6 addr add 2001:db8:1::2/64 dev vethserver +ip link set dev vethserver up +ip link set lo up + +# Wait for duplicate address detection to be finished so that the +# interfaces are ready. +while true; do + interface_status=$( + ip a s vethserver | grep -E 'inet6.*tentative' + ip a s vethclient | grep -E 'inet6.*tentative' + ) + if test -n "${interface_status}"; then + printf 'Waiting for the following addresses to be assigned to their interfaces:\n%s\n' "${interface_status}" + sleep 1 + else + break + fi +done diff --git a/fuzz/tests/Makefile.am b/fuzz/tests/Makefile.am new file mode 100644 index 0000000000..3727d137db --- /dev/null +++ b/fuzz/tests/Makefile.am @@ -0,0 +1,16 @@ +SUBDIRS = . + +if FUZZING + +check_SCRIPTS = +check_SCRIPTS += test-fuzz-config-kea-dhcp4.sh +check_SCRIPTS += test-fuzz-config-kea-dhcp6.sh +check_SCRIPTS += test-fuzz-http-endpoint.sh +check_SCRIPTS += test-fuzz-packets-kea-dhcp4.sh +check_SCRIPTS += test-fuzz-packets-kea-dhcp6.sh +check_SCRIPTS += test-fuzz-unix-socket-kea-dhcp4.sh +check_SCRIPTS += test-fuzz-unix-socket-kea-dhcp6.sh + +TESTS = $(check_SCRIPTS) + +endif # FUZZING diff --git a/fuzz/tests/setup.sh b/fuzz/tests/setup.sh new file mode 120000 index 0000000000..1664189fbb --- /dev/null +++ b/fuzz/tests/setup.sh @@ -0,0 +1 @@ +../setup.sh
\ No newline at end of file diff --git a/fuzz/tests/test-fuzz-config-kea-dhcp4.sh b/fuzz/tests/test-fuzz-config-kea-dhcp4.sh new file mode 100755 index 0000000000..fcbebc1fe8 --- /dev/null +++ b/fuzz/tests/test-fuzz-config-kea-dhcp4.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if ! sudo -n true; then exec sudo -E -- "${0}" "${@}"; fi + +script_path=$(cd "$(dirname "${0}")" && pwd) + +script_basename=$(basename "${0}") + +tested_binary=$(printf '%s' "${script_basename}" | sed 's/test-//g;s/.sh//g') + +"${script_path}/../${tested_binary}" diff --git a/fuzz/tests/test-fuzz-config-kea-dhcp6.sh b/fuzz/tests/test-fuzz-config-kea-dhcp6.sh new file mode 100755 index 0000000000..fcbebc1fe8 --- /dev/null +++ b/fuzz/tests/test-fuzz-config-kea-dhcp6.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if ! sudo -n true; then exec sudo -E -- "${0}" "${@}"; fi + +script_path=$(cd "$(dirname "${0}")" && pwd) + +script_basename=$(basename "${0}") + +tested_binary=$(printf '%s' "${script_basename}" | sed 's/test-//g;s/.sh//g') + +"${script_path}/../${tested_binary}" diff --git a/fuzz/tests/test-fuzz-http-endpoint.sh b/fuzz/tests/test-fuzz-http-endpoint.sh new file mode 100755 index 0000000000..fcbebc1fe8 --- /dev/null +++ b/fuzz/tests/test-fuzz-http-endpoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if ! sudo -n true; then exec sudo -E -- "${0}" "${@}"; fi + +script_path=$(cd "$(dirname "${0}")" && pwd) + +script_basename=$(basename "${0}") + +tested_binary=$(printf '%s' "${script_basename}" | sed 's/test-//g;s/.sh//g') + +"${script_path}/../${tested_binary}" diff --git a/fuzz/tests/test-fuzz-packets-kea-dhcp4.sh b/fuzz/tests/test-fuzz-packets-kea-dhcp4.sh new file mode 100755 index 0000000000..fcbebc1fe8 --- /dev/null +++ b/fuzz/tests/test-fuzz-packets-kea-dhcp4.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if ! sudo -n true; then exec sudo -E -- "${0}" "${@}"; fi + +script_path=$(cd "$(dirname "${0}")" && pwd) + +script_basename=$(basename "${0}") + +tested_binary=$(printf '%s' "${script_basename}" | sed 's/test-//g;s/.sh//g') + +"${script_path}/../${tested_binary}" diff --git a/fuzz/tests/test-fuzz-packets-kea-dhcp6.sh b/fuzz/tests/test-fuzz-packets-kea-dhcp6.sh new file mode 100755 index 0000000000..fcbebc1fe8 --- /dev/null +++ b/fuzz/tests/test-fuzz-packets-kea-dhcp6.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if ! sudo -n true; then exec sudo -E -- "${0}" "${@}"; fi + +script_path=$(cd "$(dirname "${0}")" && pwd) + +script_basename=$(basename "${0}") + +tested_binary=$(printf '%s' "${script_basename}" | sed 's/test-//g;s/.sh//g') + +"${script_path}/../${tested_binary}" diff --git a/fuzz/tests/test-fuzz-unix-socket-kea-dhcp4.sh b/fuzz/tests/test-fuzz-unix-socket-kea-dhcp4.sh new file mode 100755 index 0000000000..fcbebc1fe8 --- /dev/null +++ b/fuzz/tests/test-fuzz-unix-socket-kea-dhcp4.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if ! sudo -n true; then exec sudo -E -- "${0}" "${@}"; fi + +script_path=$(cd "$(dirname "${0}")" && pwd) + +script_basename=$(basename "${0}") + +tested_binary=$(printf '%s' "${script_basename}" | sed 's/test-//g;s/.sh//g') + +"${script_path}/../${tested_binary}" diff --git a/fuzz/tests/test-fuzz-unix-socket-kea-dhcp6.sh b/fuzz/tests/test-fuzz-unix-socket-kea-dhcp6.sh new file mode 100755 index 0000000000..fcbebc1fe8 --- /dev/null +++ b/fuzz/tests/test-fuzz-unix-socket-kea-dhcp6.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if ! sudo -n true; then exec sudo -E -- "${0}" "${@}"; fi + +script_path=$(cd "$(dirname "${0}")" && pwd) + +script_basename=$(basename "${0}") + +tested_binary=$(printf '%s' "${script_basename}" | sed 's/test-//g;s/.sh//g') + +"${script_path}/../${tested_binary}" diff --git a/src/bin/dhcp4/dhcp4_srv.cc b/src/bin/dhcp4/dhcp4_srv.cc index 5904d49a76..af373f79e4 100644 --- a/src/bin/dhcp4/dhcp4_srv.cc +++ b/src/bin/dhcp4/dhcp4_srv.cc @@ -41,7 +41,7 @@ #include <dhcpsrv/lease_mgr.h> #include <dhcpsrv/lease_mgr_factory.h> #include <dhcpsrv/ncr_generator.h> -#include <dhcpsrv/packet-fuzzer.h> +#include <dhcpsrv/packet_fuzzer.h> #include <dhcpsrv/resource_handler.h> #include <dhcpsrv/shared_network.h> #include <dhcpsrv/subnet.h> @@ -1144,7 +1144,7 @@ Dhcpv4Srv::run() { } // Set up structures needed for fuzzing. - PacketFuzzer fuzzer(4, server_port_, interface, address); + PacketFuzzer fuzzer(server_port_, interface, address); // The next line is needed as a signature for AFL to recognize that we are // running persistent fuzzing. This has to be in the main image file. @@ -5180,7 +5180,8 @@ void Dhcpv4Srv::discardPackets() { } uint16_t Dhcpv4Srv::getServerPort() const { - char const* const randomize(getenv("KEA_DHCP4_FUZZING_RANDOMIZE_PORT")); +#ifdef FUZZING + char const* const randomize(getenv("KEA_DHCP4_FUZZING_ROTATE_PORT")); if (randomize) { InterprocessSyncFile file("kea-dhcp4-fuzzing-randomize-port"); InterprocessSyncLocker locker(file); @@ -5209,6 +5210,7 @@ uint16_t Dhcpv4Srv::getServerPort() const { locker.unlock(); return port; } +#endif // FUZZING return server_port_; } diff --git a/src/bin/dhcp6/dhcp6_srv.cc b/src/bin/dhcp6/dhcp6_srv.cc index fe1617e998..2e59bbf35e 100644 --- a/src/bin/dhcp6/dhcp6_srv.cc +++ b/src/bin/dhcp6/dhcp6_srv.cc @@ -40,7 +40,7 @@ #include <dhcpsrv/lease_mgr.h> #include <dhcpsrv/lease_mgr_factory.h> #include <dhcpsrv/ncr_generator.h> -#include <dhcpsrv/packet-fuzzer.h> +#include <dhcpsrv/packet_fuzzer.h> #include <dhcpsrv/subnet.h> #include <dhcpsrv/subnet_selector.h> #include <dhcpsrv/utils.h> @@ -616,7 +616,7 @@ Dhcpv6Srv::run() { } // Set up structures needed for fuzzing. - PacketFuzzer fuzzer(6, server_port_, interface, address); + PacketFuzzer fuzzer(server_port_, interface, address); // The next line is needed as a signature for AFL to recognize that we are // running persistent fuzzing. This has to be in the main image file. @@ -4920,7 +4920,8 @@ void Dhcpv6Srv::discardPackets() { } uint16_t Dhcpv6Srv::getServerPort() const { - char const* const randomize(getenv("KEA_DHCP6_FUZZING_RANDOMIZE_PORT")); +#ifdef FUZZING + char const* const randomize(getenv("KEA_DHCP6_FUZZING_ROTATE_PORT")); if (randomize) { InterprocessSyncFile file("kea-dhcp6-fuzzing-randomize-port"); InterprocessSyncLocker locker(file); @@ -4949,6 +4950,7 @@ uint16_t Dhcpv6Srv::getServerPort() const { locker.unlock(); return port; } +#endif // FUZZING return server_port_; } diff --git a/src/lib/dhcpsrv/Makefile.am b/src/lib/dhcpsrv/Makefile.am index 5578174729..cf960993d3 100644 --- a/src/lib/dhcpsrv/Makefile.am +++ b/src/lib/dhcpsrv/Makefile.am @@ -173,7 +173,7 @@ libkea_dhcpsrv_la_SOURCES += parsers/simple_parser6.cc libkea_dhcpsrv_la_SOURCES += parsers/simple_parser6.h if FUZZING -libkea_dhcpsrv_la_SOURCES += packet-fuzzer.cc packet-fuzzer.h +libkea_dhcpsrv_la_SOURCES += packet_fuzzer.cc packet_fuzzer.h libkea_dhcpsrv_la_SOURCES += fuzz_log.cc fuzz_log.h libkea_dhcpsrv_la_SOURCES += fuzz_messages.cc fuzz_messages.h endif # FUZZING @@ -352,7 +352,7 @@ libkea_dhcpsrv_include_HEADERS = \ if FUZZING libkea_dhcpsrv_include_HEADERS += \ - packet-fuzzer.h \ + packet_fuzzer.h \ fuzz_log.h \ fuzz_messages.h endif diff --git a/src/lib/dhcpsrv/lease_mgr_factory.cc b/src/lib/dhcpsrv/lease_mgr_factory.cc index 44ab45d49a..ea86d4ed5b 100644 --- a/src/lib/dhcpsrv/lease_mgr_factory.cc +++ b/src/lib/dhcpsrv/lease_mgr_factory.cc @@ -94,7 +94,6 @@ LeaseMgrFactory::destroy() { .arg(getLeaseMgrPtr()->getType()); getLeaseMgrPtr().reset(); } - getLeaseMgrPtr().reset(); } void diff --git a/src/lib/dhcpsrv/packet-fuzzer.cc b/src/lib/dhcpsrv/packet_fuzzer.cc index 573b527e72..b4a16468a2 100644 --- a/src/lib/dhcpsrv/packet-fuzzer.cc +++ b/src/lib/dhcpsrv/packet_fuzzer.cc @@ -8,8 +8,9 @@ #ifdef FUZZING +#include <asiolink/io_address.h> #include <dhcp/dhcp6.h> -#include <dhcpsrv/packet-fuzzer.h> +#include <dhcpsrv/packet_fuzzer.h> #include <dhcpsrv/fuzz_log.h> #include <boost/lexical_cast.hpp> @@ -26,6 +27,7 @@ #include <ctime> using namespace isc; +using namespace isc::asiolink; using namespace isc::dhcp; using namespace std; @@ -35,8 +37,7 @@ constexpr size_t PacketFuzzer::MAX_SEND_SIZE; constexpr long PacketFuzzer::MAX_LOOP_COUNT; // Constructor -PacketFuzzer::PacketFuzzer(int const ipversion, - uint16_t const port, +PacketFuzzer::PacketFuzzer(uint16_t const port, string const interface, string const address) : loop_max_(MAX_LOOP_COUNT), sockaddr_len_(0), sockaddr_ptr_(nullptr), sockfd_(-1) { @@ -55,25 +56,26 @@ PacketFuzzer::PacketFuzzer(int const ipversion, try { loop_max_ = boost::lexical_cast<long>(loop_max_ptr); } catch (const boost::bad_lexical_cast&) { - reason << "cannot convert loop count " << loop_max_ptr - << " to an integer"; - isc_throw(FuzzInitFail, reason.str()); + isc_throw(FuzzInitFail, + "cannot convert loop count " << loop_max_ptr << " to an integer"); } if (loop_max_ <= 0) { - reason << "KEA_AFL_LOOP_MAX is " << loop_max_ << ". " - << "It must be an integer greater than zero."; - isc_throw(FuzzInitFail, reason.str()); + isc_throw(FuzzInitFail, "KEA_AFL_LOOP_MAX is " + << loop_max_ << ". " + << "It must be an integer greater than zero."); } } + IOAddress io_address(address); + // Set up address structures used to route the packets from AFL to Kea. - createAddressStructures(ipversion, port, interface, address); + createAddressStructures(port, interface, io_address); // Create the socket through which packets read from stdin will be sent // to the port on which Kea is listening. This is closed in the // destructor. - sockfd_ = socket((ipversion == 4) ? AF_INET : AF_INET6, SOCK_DGRAM, 0); + sockfd_ = socket(io_address.isV4() ? AF_INET : AF_INET6, SOCK_DGRAM, 0); if (sockfd_ < 0) { LOG_FATAL(fuzz_logger, FUZZ_SOCKET_CREATE_FAIL) .arg(strerror(errno)); @@ -98,38 +100,35 @@ PacketFuzzer::~PacketFuzzer() { // Set up address structures. void -PacketFuzzer::createAddressStructures(int const ipversion, - uint16_t const port, - string const interface, - string const address) { - stringstream reason; // Used in error messages +PacketFuzzer::createAddressStructures(uint16_t const port, + string const& interface, + IOAddress const& io_address) { + string const address(io_address.toText()); // Set up the appropriate data structure depending on the address given. - if (ipversion == 6 && address.find(":") != string::npos) { + if (io_address.isV6()) { // Expecting IPv6 and the address contains a colon, so assume it is an // an IPv6 address. memset(&servaddr6_, 0, sizeof (servaddr6_)); servaddr6_.sin6_family = AF_INET6; if (inet_pton(AF_INET6, address.c_str(), &servaddr6_.sin6_addr) != 1) { - reason << "inet_pton() failed: can't convert " - << address << " to an IPv6 address" << endl; - isc_throw(FuzzInitFail, reason.str()); + isc_throw(FuzzInitFail, + "inet_pton() failed: can't convert " << address << " to an IPv6 address"); } servaddr6_.sin6_port = htons(port); // Interface ID is needed for IPv6 address structures. servaddr6_.sin6_scope_id = if_nametoindex(interface.c_str()); if (servaddr6_.sin6_scope_id == 0) { - reason << "error retrieving interface ID for " - << interface << ": " << strerror(errno); - isc_throw(FuzzInitFail, reason.str()); + isc_throw(FuzzInitFail, + "error retrieving interface ID for " << interface << ": " << strerror(errno)); } sockaddr_ptr_ = reinterpret_cast<sockaddr*>(&servaddr6_); sockaddr_len_ = sizeof(servaddr6_); - } else if (ipversion == 4 && address.find(".") != string::npos) { + } else if (io_address.isV4()) { // Expecting an IPv4 address and it contains a dot, so assume it is. // This check is done after the IPv6 check, as it is possible for an // IPv4 address to be embedded in an IPv6 one. @@ -137,9 +136,8 @@ PacketFuzzer::createAddressStructures(int const ipversion, servaddr4_.sin_family = AF_INET; if (inet_pton(AF_INET, address.c_str(), &servaddr4_.sin_addr) != 1) { - reason << "inet_pton() failed: can't convert " - << address << " to an IPv6 address" << endl; - isc_throw(FuzzInitFail, reason.str()); + isc_throw(FuzzInitFail, + "inet_pton() failed: can't convert " << address << " to an IPv4 address"); } servaddr4_.sin_port = htons(port); @@ -147,12 +145,9 @@ PacketFuzzer::createAddressStructures(int const ipversion, sockaddr_len_ = sizeof(servaddr4_); } else { - reason << "Expected IP version (" << ipversion << ") is not " - << "4 or 6, or the given address " << address << " does not " - << "match the IP version expected"; - isc_throw(FuzzInitFail, reason.str()); + // Should never happen. + isc_throw(FuzzInitFail, "unknown IOAddress IP version"); } - } void diff --git a/src/lib/dhcpsrv/packet-fuzzer.h b/src/lib/dhcpsrv/packet_fuzzer.h index 7eb80db302..b9ae1eb9a9 100644 --- a/src/lib/dhcpsrv/packet-fuzzer.h +++ b/src/lib/dhcpsrv/packet_fuzzer.h @@ -9,6 +9,7 @@ #ifdef FUZZING +#include <asiolink/io_address.h> #include <exceptions/exceptions.h> #include <arpa/inet.h> @@ -16,10 +17,7 @@ #include <sys/socket.h> #include <unistd.h> -#include <condition_variable> -#include <mutex> #include <string> -#include <thread> namespace isc { @@ -69,12 +67,9 @@ public: /// Sets up data structures to access the address/port being used to /// transfer data from AFL to Kea. /// - /// @param ipversion Either 4 or 6 depending on what IP version the - /// server responds to. /// @param port Port on which the server is listening, and hence the /// port to which the fuzzer will send input from AFL. - PacketFuzzer(int const ipversion, - uint16_t const port, + PacketFuzzer(uint16_t const port, std::string const interface, std::string const address); @@ -110,19 +105,15 @@ private: /// Create the address structures describing the address/port on whick Kea /// is listening for packets from AFL. /// - /// @param ipversion Either 4 or 6 depending on which IP version address - /// is expected. - /// @param interface Interface through which the fuzzer is sending packets - /// to Kea. - /// @param address Address on the interface that will be used. - /// @param port Port to be used. + /// @param port Port to be used. + /// @param interface Interface through which the fuzzer is sending packets to Kea. + /// @param io_address Address on the interface that will be used. /// /// @throws FuzzInitFail Thrown if the address is not in the expected /// format. - void createAddressStructures(int const ipversion, - uint16_t const port, - std::string const interface, - std::string const address); + void createAddressStructures(uint16_t const port, + std::string const& interface, + isc::asiolink::IOAddress const& io_address); // Other member variables. long loop_max_; //< Maximum number of loop iterations diff --git a/src/lib/dhcpsrv/tests/Makefile.am b/src/lib/dhcpsrv/tests/Makefile.am index 857534ba79..289a8fdc9f 100644 --- a/src/lib/dhcpsrv/tests/Makefile.am +++ b/src/lib/dhcpsrv/tests/Makefile.am @@ -131,6 +131,10 @@ libdhcpsrv_unittests_SOURCES += tracking_lease_mgr_unittest.cc libdhcpsrv_unittests_SOURCES += network_state_unittest.cc libdhcpsrv_unittests_SOURCES += network_unittest.cc +if FUZZING +libdhcpsrv_unittests_SOURCES += packet_fuzzer_unittest.cc +endif + libdhcpsrv_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) libdhcpsrv_unittests_CXXFLAGS = $(AM_CXXFLAGS) diff --git a/src/lib/dhcpsrv/tests/packet_fuzzer_unittest.cc b/src/lib/dhcpsrv/tests/packet_fuzzer_unittest.cc new file mode 100644 index 0000000000..7cfff5254a --- /dev/null +++ b/src/lib/dhcpsrv/tests/packet_fuzzer_unittest.cc @@ -0,0 +1,32 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <asiolink/io_error.h> +#include <dhcpsrv/packet_fuzzer.h> +#include <testutils/gtest_utils.h> + +#include <gtest/gtest.h> + +namespace isc { +namespace dhcp { +namespace test { + +TEST(PacketFuzzerTest, constructor) { + PacketFuzzer(67, "testeth", "127.0.0.1"); + // v6 requires valid interface. Skip positive test case. + + // Negative test cases + EXPECT_THROW_MSG(PacketFuzzer(547, "invalid_eth%", "fe80::1"), FuzzInitFail, + "error retrieving interface ID for invalid_eth%: No such device"); + EXPECT_THROW_MSG(PacketFuzzer(1234, "testeth", "abcd"), isc::asiolink::IOError, + "Failed to convert string to address 'abcd': Invalid argument"); +} + +} // namespace test +} // namespace dhcp +} // namespace isc diff --git a/src/lib/exceptions/exceptions.h b/src/lib/exceptions/exceptions.h index 203f063604..9bce3f67e4 100644 --- a/src/lib/exceptions/exceptions.h +++ b/src/lib/exceptions/exceptions.h @@ -183,7 +183,7 @@ public: class MultiThreadingInvalidOperation : public Exception { public: MultiThreadingInvalidOperation(const char* file, size_t line, const char* what) : - isc::Exception(file, line, what) {}; + isc::Exception(file, line, what) {} }; /// diff --git a/src/lib/util/filesystem.cc b/src/lib/util/filesystem.cc index 4c0c3ecd27..9e4ebcdffc 100644 --- a/src/lib/util/filesystem.cc +++ b/src/lib/util/filesystem.cc @@ -183,18 +183,20 @@ Path::replaceParentPath(string const& replacement) { TemporaryDirectory::TemporaryDirectory() { char dir[]("/tmp/kea-tmpdir-XXXXXX"); char const* dir_name = mkdtemp(dir); - if(!dir_name) { + if (!dir_name) { isc_throw(Unexpected, "mkdtemp failed " << dir << ": " << strerror(errno)); } dir_name_ = string(dir_name); } TemporaryDirectory::~TemporaryDirectory() { - rmdir(dir_name_.c_str()); DIR *dir(opendir(dir_name_.c_str())); + if (!dir) { + return; + } + struct dirent *i; string filepath; - while ((i = readdir(dir))) { if (strcmp(i->d_name, ".") == 0 || strcmp(i->d_name, "..") == 0) { continue; |