diff --git a/CMakeLists.txt b/CMakeLists.txt index 92727a0..9b9e250 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,72 @@ cmake_minimum_required(VERSION 3.10) project(args LANGUAGES CXX VERSION 6.4.16 DESCRIPTION "A flexible single-header C++11 argument parsing library that is designed to appear somewhat similar to Python's argparse" HOMEPAGE_URL "https://github.com/Taywee/args") include(GNUInstallDirs) +include(CheckCXXCompilerFlag) + +option(ARGS_ENABLE_SECURITY_HARDENING "Enable additional compiler/linker hardening flags" ON) +option(ARGS_ENABLE_HARDENING_WERROR "Treat hardening warnings as errors" OFF) + +# ============================================ +# SECURITY HARDENING COMPILER FLAGS +# ============================================ +function(args_add_compile_flag_if_supported flag) + string(MAKE_C_IDENTIFIER "${flag}" flag_id) + set(flag_var "ARGS_HAS_${flag_id}") + check_cxx_compiler_flag("${flag}" ${flag_var}) + if(${flag_var}) + add_compile_options("${flag}") + endif() +endfunction() + +if(ARGS_ENABLE_SECURITY_HARDENING) + if(MSVC) + # MSVC security-oriented defaults + add_compile_options( + /W4 + /sdl + /GS + /guard:cf + ) + add_link_options(/GUARD:CF) + if(ARGS_ENABLE_HARDENING_WERROR) + add_compile_options(/WX) + endif() + else() + # GCC/Clang hardening flags (only when supported) + args_add_compile_flag_if_supported(-Wall) + args_add_compile_flag_if_supported(-Wextra) + args_add_compile_flag_if_supported(-Wpedantic) + args_add_compile_flag_if_supported(-Wconversion) + args_add_compile_flag_if_supported(-Wsign-conversion) + args_add_compile_flag_if_supported(-Wnull-dereference) + args_add_compile_flag_if_supported(-Wformat=2) + args_add_compile_flag_if_supported(-Wshadow) + args_add_compile_flag_if_supported(-Wcast-align) + args_add_compile_flag_if_supported(-Wunused) + + # -Wunsafe-buffer-usage (clang) flags all raw pointer arithmetic, + # including the standard `argv + 1, argv + argc` idiom. It produces + # false positives on bounded argv handling, so it is not enabled. + + if(ARGS_ENABLE_HARDENING_WERROR) + args_add_compile_flag_if_supported(-Werror) + endif() + + # Sanitizers for debugging (optional, off by default) + option(ENABLE_ASAN "Enable AddressSanitizer" OFF) + option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF) + + if(ENABLE_ASAN) + add_compile_options(-fsanitize=address -fno-omit-frame-pointer) + add_link_options(-fsanitize=address) + endif() + + if(ENABLE_UBSAN) + add_compile_options(-fsanitize=undefined) + add_link_options(-fsanitize=undefined) + endif() + endif() +endif() string(REPLACE "/${CMAKE_LIBRARY_ARCHITECTURE}" "" CMAKE_INSTALL_LIBDIR_ARCHIND "${CMAKE_INSTALL_LIBDIR}") @@ -32,6 +98,18 @@ if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) set(ARGS_MAIN_PROJECT ON) endif() +# ============================================ +# FUZZING TARGET (if libFuzzer available) +# ============================================ +option(BUILD_FUZZERS "Build fuzzing targets" OFF) + +if(BUILD_FUZZERS AND NOT MSVC) + add_executable(fuzz_parser fuzz/fuzz_parser.cpp) + target_link_libraries(fuzz_parser args) + target_compile_options(fuzz_parser PRIVATE -fsanitize=fuzzer,address) + target_link_options(fuzz_parser PRIVATE -fsanitize=fuzzer,address) +endif() + option(ARGS_BUILD_EXAMPLE "Build example" ON) option(ARGS_BUILD_UNITTESTS "Build unittests" ON) @@ -139,4 +217,4 @@ set(CPACK_RPM_COMPONENT_INSTALL ON) set(CPACK_NSIS_COMPONENT_INSTALL ON) set(CPACK_DEBIAN_COMPRESSION_TYPE "xz") -include(CPack) +include(CPack) \ No newline at end of file diff --git a/args.hxx b/args.hxx index 85671f9..6fa5758 100644 --- a/args.hxx +++ b/args.hxx @@ -3139,13 +3139,15 @@ namespace args size_t erase_end = 0; if (SafeAdd(next_idx, static_cast(1), erase_end)) { - curArgs.erase(curArgs.begin() + idx, - curArgs.begin() + erase_end); + typedef std::vector::difference_type diff_t; + curArgs.erase(curArgs.begin() + static_cast(idx), + curArgs.begin() + static_cast(erase_end)); } } else { // Safe erase of single '=' token at the end - curArgs.erase(curArgs.begin() + idx); + typedef std::vector::difference_type diff_t; + curArgs.erase(curArgs.begin() + static_cast(idx)); } // Do not increment idx - next element slides into current position } else diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000..b25a2f0 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,16 @@ +## OSS-Fuzz Integration +This fuzzer is compatible with OSS-Fuzz. To integrate: + +- Add to OSS-Fuzz project list +- Build with -fsanitize=fuzzer,address,undefined +- Run for 24+ hours on cluster + +## Expected Findings +The fuzzer should NOT find: + +- Buffer overflows +- Integer overflows (already fixed with SafeMultiply) +- Use-after-free (RAII prevents) +- Null dereferences + +If it does find issues, they represent critical bugs. \ No newline at end of file diff --git a/fuzz/fuzz_parser.cpp b/fuzz/fuzz_parser.cpp new file mode 100644 index 0000000..3c477c5 --- /dev/null +++ b/fuzz/fuzz_parser.cpp @@ -0,0 +1,72 @@ +/** + * libFuzzer integration for args.hxx + * + * Build with: cmake -DBUILD_FUZZERS=ON .. + * Run with: ./fuzz_parser corpus/ + * + * This fuzzer tests argument parsing with malformed/untrusted input + * to detect crashes, overflows, and undefined behavior. + */ + +#include +#include +#include +#include +#include + +// Include the args library +#include "../args.hxx" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + // Create a string from fuzzer input + std::string input(reinterpret_cast(data), size); + + // Simulate command-line arguments + std::vector args; + + // Parse input as space-separated arguments (simulate argv) + std::istringstream iss(input); + std::string arg; + while (iss >> arg) { + args.push_back(arg); + } + + if (args.empty()) { + return 0; // Nothing to parse + } + + // Build argv-style array + std::vector argv; + for (auto& a : args) { + argv.push_back(const_cast(a.c_str())); + } + + // Create argument parser + args::ArgumentParser parser("Fuzzer test", "Fuzzing argument parsing"); + + // Add various flag types to test + args::HelpFlag help(parser, "help", "Display help", {'h', "help"}); + args::Flag verbose(parser, "verbose", "Enable verbose", {'v', "verbose"}); + args::ValueFlag name(parser, "name", "User name", {'n', "name"}); + args::ValueFlag count(parser, "count", "Count value", {'c', "count"}); + args::Positional positional(parser, "input", "Input file"); + + try { + // Parse the arguments - this is what we're fuzzing + parser.ParseArgs(static_cast(argv.size()), argv.data()); + } catch (const args::Help&) { + // Help requested - normal behavior + return 0; + } catch (const args::ParseError& e) { + // Parse error - expected for malformed input + return 0; + } catch (const args::ValidationError& e) { + // Validation error - also expected + return 0; + } catch (const std::exception& e) { + // Any other exception is a potential bug + __builtin_trap(); // Signal fuzzer this is a crash + } + + return 0; // Success +} \ No newline at end of file