diff --git a/.gitignore b/.gitignore index 38f5eec745..7058a3f017 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,11 @@ conanbuildinfo.txt conaninfo.txt graph_info.json build/* + +# Fuzzing artifacts +crash-* +leak-* +oom-* + +# Clangd files +.clangd diff --git a/include/PrintFeature.h b/include/PrintFeature.h index 699dcf7b29..26d90630c9 100644 --- a/include/PrintFeature.h +++ b/include/PrintFeature.h @@ -18,9 +18,11 @@ enum class PrintFeatureType: unsigned char MoveRetraction = 9, SupportInterface = 10, PrimeTower = 11, - NumPrintFeatureTypes = 12 // this number MUST be the last one because other modules will + NumPrintFeatureTypes = 12, // this number MUST be the last one because other modules will // use this symbol to get the total number of types, which can // be used to create an array or so + // Internal use only. Used for fuzzing. + kMaxValue = NumPrintFeatureTypes, }; diff --git a/include/settings/EnumSettings.h b/include/settings/EnumSettings.h index 7b8c970db4..5dd1cc3d43 100644 --- a/include/settings/EnumSettings.h +++ b/include/settings/EnumSettings.h @@ -219,7 +219,13 @@ enum class EGCodeFlavor * Real RepRap GCode suitable for printers using RepRap firmware (e.g. Duet controllers) **/ REPRAP = 8, + PLUGIN = 9, + + /** + * For internal fuzz-testing use only. + **/ + kMaxValue = PLUGIN, }; /*! diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1e36893cee..b2430ceeb6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,6 +3,8 @@ message(STATUS "Building tests...") include(GoogleTest) +include(CheckCXXCompilerFlag) +include(CMakeDependentOption) set(TESTS_SRC_BASE ClipperTest @@ -40,6 +42,28 @@ set(TESTS_SRC_UTILS UnionFindTest ) +set(TESTS_HELPERS_SRC ReadTestPolygons.cpp) + +set(TESTS_SRC_ARCUS) +if (ENABLE_ARCUS) + list(APPEND TESTS_SRC_ARCUS + ArcusCommunicationTest + ArcusCommunicationPrivateTest) + list(APPEND TESTS_HELPERS_SRC + arcus/MockSocket.cpp + arcus/MockSocket.h + arcus/MockCommunication.h + ) +endif () + +add_library(test_helpers ${TESTS_HELPERS_SRC}) +target_compile_definitions(test_helpers PUBLIC $<$:BUILD_TESTS> $<$:ARCUS>) +target_include_directories(test_helpers PUBLIC "../include") +target_link_libraries(test_helpers PRIVATE _CuraEngine GTest::gtest GTest::gmock clipper::clipper) +if (ENABLE_ARCUS) + target_link_libraries(test_helpers PUBLIC arcus::arcus protobuf::libprotobuf) +endif () + foreach (test ${TESTS_SRC_BASE}) add_executable(${test} main.cpp ${test}.cpp) add_test(NAME ${test} COMMAND "${test}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") @@ -70,3 +94,34 @@ foreach (test ${TESTS_SRC_UTILS}) add_test(NAME ${test} COMMAND "${test}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(${test} PRIVATE _CuraEngine test_helpers GTest::gtest GTest::gmock clipper::clipper) endforeach () + + +# Ensure that basic sanitizer flags are supported before adding fuzzers; +# - Address sanitizer is often used in conjunction with fuzzing as it will detect +# common high severity bugs. This sanitizer is used as a "default" for fuzzing +# when the sanitizer isn't otherwise specified. +# - Fuzzer sanitizer will link against libfuzzer and is currently only supported +# on clang/msvc and isn't supported with GCC. If you need to use these fuzzers +# with a GCC based project you should consider looking into the LIB_FUZZING_ENGINE +# env variable defined in `test/fuzz/CMakeLists.txt`. +set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=fuzzer") +set(CMAKE_REQUIRED_FLAGS "-fsanitize=fuzzer-no-link") +check_cxx_source_compiles([[ +#include +#include +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, std::size_t Size) { + return 0; +} +]] HAS_FUZZ_FLAGS) + +cmake_dependent_option( + WITH_TEST_FUZZ "Build fuzz tests" ON + HAS_FUZZ_FLAGS OFF +) + +if (WITH_TEST_FUZZ) + message(STATUS "Building fuzz tests enabled") + add_subdirectory(fuzz) +else () + message(STATUS "Building fuzz tests disabled") +endif () \ No newline at end of file diff --git a/tests/fuzz/CMakeLists.txt b/tests/fuzz/CMakeLists.txt new file mode 100644 index 0000000000..cb19c00ab5 --- /dev/null +++ b/tests/fuzz/CMakeLists.txt @@ -0,0 +1,27 @@ +# By default we are going to use the libfuzzer engine. However if +# LIB_FUZZING_ENGINE is declared you can override the fuzzing engine to one of; +# - Centipede +# - Hongfuzz +# - AFL++ +# - etc. +set(LIB_FUZZING_ENGINE "$ENV{LIB_FUZZING_ENGINE}" + CACHE STRING "Compiler flags necessary to link the fuzzing engine of choice e.g. libfuzzer, afl etc.") + +set(FUZZ_TEST_SRC + FuzzGcodeExport + ) + +foreach (test ${FUZZ_TEST_SRC}) + add_executable(${test} ${test}.cpp) + target_link_libraries(${test} PRIVATE _CuraEngine clipper::clipper arcus::arcus test_helpers) + # Optionally allow OSS-fuzz to manage engine flags directly. + if (LIB_FUZZING_ENGINE) + message(STATUS "Using custom fuzzing engine.") + target_link_libraries(${test} PRIVATE "${LIB_FUZZING_ENGINE}") + else () + # By default just build with address-sanitizers/libfuzzer for local testing + message(STATUS "Using default fuzzing configuration libfuzzer+address-sanitizer.") + target_compile_options(${test} PRIVATE "-fsanitize=fuzzer,address,undefined") + target_link_libraries(${test} PRIVATE "-fsanitize=fuzzer,address,undefined") + endif () +endforeach () diff --git a/tests/fuzz/FuzzGcodeExport.cpp b/tests/fuzz/FuzzGcodeExport.cpp new file mode 100644 index 0000000000..88fe555c86 --- /dev/null +++ b/tests/fuzz/FuzzGcodeExport.cpp @@ -0,0 +1,255 @@ +// Copyright (c) 2022 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "Application.h" // To set up a slice with settings. +#include "RetractionConfig.h" // For extruder switch tests. +#include "Slice.h" // To set up a slice with settings. +#include "WipeScriptConfig.h" // For wipe script tests. +#include "communication/Communication.h" //The interface we're implementing. +#include "gcodeExport.h" // The unit under test. +#include "settings/Settings.h" +#include "settings/types/LayerIndex.h" +#include "utils/Coord_t.h" +#include "utils/Date.h" // To check the Griffin header. +#include +#include // To create structured fuzz data. +#include +#include + +namespace cura { + +class FuzzedCommunication : public Communication { +public: + constexpr explicit FuzzedCommunication(FuzzedDataProvider *fdp) : fdp_(fdp) {} + + [[nodiscard]] bool hasSlice() const override { return fdp_->ConsumeBool(); } + [[nodiscard]] bool isSequential() const override { return fdp_->ConsumeBool(); } + void sendProgress(const float &progress) const override{}; + void sendLayerComplete(const LayerIndex &layer_nr, const coord_t &z, + const coord_t &thickness) override{}; + void sendPolygons(const PrintFeatureType &type, + const Polygons &polygons, const coord_t &line_width, + const coord_t &line_thickness, + const Velocity &velocity) override {} + void sendPolygon(const PrintFeatureType &type, + const ConstPolygonRef &polygon, + const coord_t &line_width, + const coord_t &line_thickness, + const Velocity &velocity) override {} + void sendLineTo(const PrintFeatureType &type, const Point &to, + const coord_t &line_width, + const coord_t &line_thickness, + const Velocity &velocity) override {} + void sendCurrentPosition(const Point &position) override {} + void setExtruderForSend(const ExtruderTrain &extruder) override {} + void setLayerForSend(const LayerIndex &layer_nr) override {} + void sendOptimizedLayerData() override {} + void sendPrintTimeMaterialEstimates() const override {}; + void beginGCode() override {} + void flushGCode() override {} + void sendGCodePrefix(const std::string &prefix) const override {} + void sendSliceUUID(const std::string &slice_uuid) const override {} + void sendFinishedSlicing() const override {} + void sliceNext() override {} + +private: + FuzzedDataProvider *fdp_; +}; + +enum GcodeExporterFunction { + kSetSliceUUID, + kSetLayerNumber, + kSetFlavor, + kSetZ, + kSetFlowRateExtrusionSettings, + kSetFilamentDiameter, + kResetTotalPrintTimeAndFilament, + kWriteComment, + kWriteTypeComment, + kWriteExtrusionMode, + kResetExtrusionMode, + kWriteTimeComment, + kWriteLayerComment, + kWriteLayerCountComment, + kWriteLine, + kResetExtrusionValue, + kWriteDelay, + kWriteTravel, + kWriteExtrusion, + kInitializeExtruderTrain, + kProcessInitialLayerTemperature, + kMaxValue = kProcessInitialLayerTemperature, +}; + +int initSettings(FuzzedDataProvider *fdp) { + double layer_height = std::abs(fdp->ConsumeFloatingPoint()); + if (!std::isfinite(layer_height)) { + return 1; + } + Application::getInstance() + .current_slice->scene.current_mesh_group->settings.add( + "layer_height", std::to_string(layer_height)); + int number_of_extruders = fdp->ConsumeIntegralInRange(1, MAX_EXTRUDERS); + for (int i = 0; i < number_of_extruders; i++) { + Scene &scene = Application::getInstance().current_slice->scene; + scene.extruders.emplace_back( + i, &Application::getInstance() + .current_slice->scene.current_mesh_group->settings); + ExtruderTrain &train = scene.extruders.back(); + train.settings.add( + "machine_nozzle_size", + std::to_string(std::abs(fdp->ConsumeFloatingPoint()))); + train.settings.add("machine_nozzle_id", "TestNozzle-" + std::to_string(i)); + train.settings.add("machine_firmware_retract", "false"); + } + return 0; +} + +int fuzzGcodeExporter(FuzzedDataProvider *fdp) { + + std::stringstream output; + GCodeExport gcode; + gcode.setOutputStream(&output); + int max_iterations = fdp->ConsumeIntegralInRange(1, 2048); + + // Extruders must have a defined non-zero diameter to avoid devide by zeros. + for (int i = 0; i < MAX_EXTRUDERS; i++) { + gcode.setFilamentDiameter(0, 1); + } + for (int i = 0; i < max_iterations; i++) { + if (fdp->remaining_bytes() == 0) { + return 0; + } + switch (fdp->ConsumeEnum()) { + case kSetSliceUUID: { + constexpr int kUUIDLength = 32; + gcode.setSliceUUID(fdp->ConsumeRandomLengthString(kUUIDLength)); + break; + } + case kSetLayerNumber: + gcode.setLayerNr(fdp->ConsumeIntegral()); + break; + case kSetFlavor: + gcode.setFlavor(fdp->ConsumeEnum()); + break; + case kSetZ: + gcode.setZ(fdp->ConsumeIntegral()); + break; + case kSetFlowRateExtrusionSettings: + gcode.setFlowRateExtrusionSettings(fdp->ConsumeFloatingPoint(), + fdp->ConsumeFloatingPoint()); + break; + case kSetFilamentDiameter: + gcode.setFilamentDiameter( + fdp->ConsumeIntegralInRange(0, MAX_EXTRUDERS - 1), + fdp->ConsumeIntegralInRange( + 1, std::numeric_limits::max())); + break; + case kResetTotalPrintTimeAndFilament: + gcode.resetTotalPrintTimeAndFilament(); + break; + case kWriteComment: + gcode.writeComment(fdp->ConsumeRandomLengthString(256)); + break; + case kWriteTypeComment: + gcode.writeTypeComment(fdp->ConsumeEnum()); + break; + case kWriteExtrusionMode: + gcode.writeExtrusionMode(fdp->ConsumeBool()); + break; + case kResetExtrusionMode: + gcode.resetExtrusionMode(); + break; + case kWriteTimeComment: + gcode.writeTimeComment(std::abs(fdp->ConsumeFloatingPoint())); + break; + case kWriteLayerComment: + gcode.writeLayerComment( + fdp->ConsumeIntegralInRange(0, std::numeric_limits::max())); + break; + case kWriteLayerCountComment: + gcode.writeLayerCountComment( + fdp->ConsumeIntegralInRange(0, std::numeric_limits::max())); + break; + case kWriteLine: + gcode.writeLine(fdp->ConsumeRandomLengthString(256).c_str()); + break; + case kResetExtrusionValue: + gcode.resetExtrusionValue(); + break; + case kWriteDelay: + gcode.writeDelay(std::abs(fdp->ConsumeFloatingPoint())); + break; + case kWriteTravel: { + Point3 current_position = gcode.getPosition(); + // Total travel distance can't be > 1000. + const int max_translation = 9; + Point3 translation = Point3( + fdp->ConsumeIntegralInRange(MM2INT(-max_translation / 2), + MM2INT(max_translation / 2)), + fdp->ConsumeIntegralInRange(MM2INT(-max_translation / 2), + MM2INT(max_translation / 2)), + fdp->ConsumeIntegralInRange(MM2INT(-max_translation / 2), + MM2INT(max_translation / 2))); + gcode.writeTravel( + current_position + translation, + Velocity( + std::abs(fdp->ConsumeFloatingPointInRange(1.1, 999.9)))); + break; + } + case kWriteExtrusion: { + Point3 current_position = gcode.getPosition(); + // Total travel distance can't be > 1000. + const int max_translation = 9; + Point3 translation = Point3( + fdp->ConsumeIntegralInRange(MM2INT(-max_translation / 2), + MM2INT(max_translation / 2)), + fdp->ConsumeIntegralInRange(MM2INT(-max_translation / 2), + MM2INT(max_translation / 2)), + fdp->ConsumeIntegralInRange(MM2INT(-max_translation / 2), + MM2INT(max_translation / 2))); + constexpr double max_extrusion_rate = 1000.0; + gcode.writeExtrusion( + current_position + translation, + Velocity( + std::abs(fdp->ConsumeFloatingPointInRange(1.1, 999.9))), + fdp->ConsumeFloatingPointInRange(0.0, max_extrusion_rate), + fdp->ConsumeEnum(), fdp->ConsumeBool()); + break; + } + case kInitializeExtruderTrain: + // TODO: Implement a fuzzed version of the storage arg. + break; + case kProcessInitialLayerTemperature: + // TODO: Implement a fuzzed version of the storage arg. + break; + } + } + return 0; +} + +class App { +public: + explicit App(FuzzedDataProvider *fdp) { + Application::getInstance().current_slice = new Slice(1); + Application::getInstance().communication = new FuzzedCommunication(fdp); + } + ~App() { + delete Application::getInstance().current_slice; + delete Application::getInstance().communication; + Application::getInstance().communication = nullptr; + } +}; + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + FuzzedDataProvider fdp(data, size); + App app(&fdp); + if (initSettings(&fdp) != 0) { + return 1; + } + int result = fuzzGcodeExporter(&fdp); + + return result; +} + +} // namespace cura \ No newline at end of file diff --git a/tests/fuzz/oss-fuzz-build.sh b/tests/fuzz/oss-fuzz-build.sh new file mode 100644 index 0000000000..c178d1bfaf --- /dev/null +++ b/tests/fuzz/oss-fuzz-build.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set euxo pipefail + +conan install . --build=missing --update -s build_type=Release -o curaengine:enable_testing=True +cmake --preset release -DWITH_TEST_FUZZ=ON +cmake --build --preset release -j$(nproc) + +cp build/Release/tests/fuzz/Fuzz* $OUT + +mkdir -p $OUT/lib +# Move all dynamic deps into output directory. +find ~/.conan/data -name '*.so*' -exec cp {} $OUT/lib/ \; + +# Rewrite dynamic linker paths to point to output directory +for fuzzer in $OUT/Fuzz*; do + chrpath -r '$ORIGIN/lib' $fuzzer +done