Tests/Fuzzing: Add CMake_BUILD_FUZZING option and build infrastructure

Add infrastructure for building fuzz testing targets with libFuzzer or
other fuzzing engines (e.g., OSS-Fuzz's LIB_FUZZING_ENGINE).

Features:
- CMake_BUILD_FUZZING option in root CMakeLists.txt
- Fuzzing/CMakeLists.txt with add_fuzzer() macro
- Support for libFuzzer and external fuzzing engines
- Documentation in Tests/Fuzzing/README.rst

The infrastructure is opt-in and requires a compatible fuzzing engine.
If CMake_BUILD_FUZZING is enabled but no engine is found, configuration
fails with a clear error message.

See Tests/Fuzzing/README.rst for build instructions.
This commit is contained in:
Leslie P. Polzer
2026-01-20 14:06:25 -05:00
committed by Brad King
parent 3044be73a9
commit 8535a7963c
65 changed files with 528 additions and 6 deletions
+3
View File
@@ -224,6 +224,9 @@ endif()
option(CMake_BUILD_PCH "Compile CMake with precompiled headers" OFF)
option(CMake_BUILD_FUZZING "Build fuzz testing targets" OFF)
mark_as_advanced(CMake_BUILD_FUZZING)
# Check whether to build support for the debugger mode.
if(NOT CMake_TEST_EXTERNAL_CMAKE)
if(NOT DEFINED CMake_ENABLE_DEBUGGER)
+5
View File
@@ -80,6 +80,11 @@ endif()
configure_file(${CMake_SOURCE_DIR}/Tests/EnforceConfig.cmake.in
${CMake_BINARY_DIR}/Tests/EnforceConfig.cmake @ONLY)
# Fuzzing targets (outside BUILD_TESTING for OSS-Fuzz compatibility)
if(CMake_BUILD_FUZZING AND NOT CMake_TEST_EXTERNAL_CMAKE)
add_subdirectory(Fuzzing)
endif()
# Testing
if(BUILD_TESTING)
set(CMake_TEST_DEVENV "")
+54
View File
@@ -0,0 +1,54 @@
# Fuzzing targets for CMake
# See README.rst for documentation.
# Determine fuzzing engine
# OSS-Fuzz sets LIB_FUZZING_ENGINE, otherwise use libFuzzer
if(DEFINED ENV{LIB_FUZZING_ENGINE})
set(FUZZING_ENGINE $ENV{LIB_FUZZING_ENGINE})
set(FUZZING_ENGINE_FOUND TRUE)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
# Check if libFuzzer is available (needs both compile and link flags)
include(CheckCXXSourceCompiles)
set(CMAKE_REQUIRED_FLAGS "-fsanitize=fuzzer")
set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=fuzzer")
check_cxx_source_compiles("extern \"C\" int LLVMFuzzerTestOneInput(const char *data, long size) { return 0; }" HAVE_LIBFUZZER)
unset(CMAKE_REQUIRED_FLAGS)
unset(CMAKE_REQUIRED_LINK_OPTIONS)
if(HAVE_LIBFUZZER)
set(FUZZING_ENGINE "-fsanitize=fuzzer")
set(FUZZING_ENGINE_FOUND TRUE)
endif()
endif()
if(NOT FUZZING_ENGINE_FOUND)
message(FATAL_ERROR "No fuzzing engine found. CMake_BUILD_FUZZING requires libFuzzer or LIB_FUZZING_ENGINE.")
endif()
# Common link libraries
set(FUZZER_LINK_LIBS
CMakeLib
)
# Macro to add a fuzzer target
macro(add_fuzzer name source)
add_executable(${name} ${source})
target_link_libraries(${name} PRIVATE ${FUZZER_LINK_LIBS})
# If using libFuzzer directly, add the flag
if(FUZZING_ENGINE STREQUAL "-fsanitize=fuzzer")
target_compile_options(${name} PRIVATE -fsanitize=fuzzer)
target_link_options(${name} PRIVATE -fsanitize=fuzzer)
else()
# OSS-Fuzz provides engine as a library
target_link_libraries(${name} PRIVATE ${FUZZING_ENGINE})
endif()
# Ensure we don't apply clang-tidy to fuzzers
set_property(TARGET ${name} PROPERTY C_CLANG_TIDY "")
set_property(TARGET ${name} PROPERTY CXX_CLANG_TIDY "")
endmacro()
# Existing fuzzer from OSS-Fuzz integration
add_fuzzer(xml_parser_fuzzer xml_parser_fuzzer.cc)
message(STATUS "Fuzzing targets enabled with engine: ${FUZZING_ENGINE}")
+49 -6
View File
@@ -1,8 +1,51 @@
The fuzzers in this directory are run continuously through OSS-fuzz.
All fuzzers are implemented by way of the `libFuzzer engine`_.
CMake Fuzzing Suite
===================
The link to the OSS-fuzz integration can be found here: (pending)
All email addresses in the ``project.yaml`` file on OSS-fuzz will have access
to detailed bug reports and will be notified via email if/when bugs are found.
This directory contains fuzz targets for testing CMake with coverage-guided
fuzzing. These fuzzers are integrated with `OSS-Fuzz`_ for continuous fuzzing.
.. _`libFuzzer Engine`: https://llvm.org/docs/LibFuzzer.html
All fuzzers are implemented using the `libFuzzer engine`_.
.. _`OSS-Fuzz`: https://github.com/google/oss-fuzz
.. _`libFuzzer engine`: https://llvm.org/docs/LibFuzzer.html
Building Locally
----------------
To build the fuzzers locally with Clang and libFuzzer::
mkdir build && cd build
cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
-DCMake_BUILD_FUZZING=ON \
-DCMAKE_C_FLAGS="-fsanitize=fuzzer-no-link,address" \
-DCMAKE_CXX_FLAGS="-fsanitize=fuzzer-no-link,address" \
..
make -j$(nproc)
Seed Corpora
------------
Each fuzzer has a corresponding seed corpus in ``corpus/<fuzzer_name>/``.
Dictionaries
------------
Fuzzer dictionaries are provided to improve coverage. Each fuzzer has a
corresponding ``.dict`` file with format-specific tokens.
Security Considerations
-----------------------
These fuzzers specifically target security-sensitive code paths:
- **Path traversal**: Archive extraction with ``../`` sequences
- **Symlink attacks**: File handling with symlinks
- **Memory safety**: Buffer handling in parsers
- **Integer overflows**: Size calculations in binary parsers
Bug Reports
-----------
Security bugs found by OSS-Fuzz are reported to CMake maintainers via the
OSS-Fuzz security disclosure process. Non-security bugs are filed as public
issues after a 90-day disclosure window.
+26
View File
@@ -0,0 +1,26 @@
Fuzzing Seed Corpora
====================
This directory contains seed corpus files for the CMake fuzz targets.
Each subdirectory corresponds to a specific fuzzer.
Regenerating Binary Corpus Files
--------------------------------
Some corpus files are binary and include generation scripts for reproducibility:
- ``elf/generate.py`` - Generates minimal ELF headers for cmELFFuzzer
To regenerate::
cd elf && python3 generate.py
Adding New Corpus Files
-----------------------
When adding new corpus files:
1. Keep files minimal - smaller seeds lead to faster fuzzing
2. For binary formats, include a generation script
3. Cover different code paths and edge cases
4. Avoid duplicating coverage between files
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
target.o: \
source.cpp \
header1.h \
header2.h
+1
View File
@@ -0,0 +1 @@
main.o: main.cpp header.h
+1
View File
@@ -0,0 +1 @@
"path with spaces.o": "source file.cpp" "my header.h"
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Generate minimal ELF corpus files for fuzzing.
These are hand-crafted minimal ELF headers that exercise the ELF parser
without requiring actual compiled binaries. They contain just enough
structure to be recognized as valid ELF files.
Usage: python3 generate.py
Output files can be verified with: file *.elf
"""
import os
import struct
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
def generate_minimal_elf64():
"""Generate a minimal 64-bit ELF executable header (72 bytes).
Structure: ELF64 header (64 bytes) with e_type=ET_EXEC, e_machine=EM_X86_64,
plus 8 bytes padding for fuzzer mutation headroom.
"""
elf = bytearray()
# e_ident[16]: ELF identification
elf.extend(b'\x7fELF') # EI_MAG: magic number
elf.append(2) # EI_CLASS: ELFCLASS64
elf.append(1) # EI_DATA: ELFDATA2LSB (little-endian)
elf.append(1) # EI_VERSION: EV_CURRENT
elf.append(0) # EI_OSABI: ELFOSABI_NONE
elf.extend(b'\x00' * 8) # EI_PAD: padding
# ELF header fields
elf.extend(struct.pack('<H', 2)) # e_type: ET_EXEC
elf.extend(struct.pack('<H', 0x3e)) # e_machine: EM_X86_64
elf.extend(struct.pack('<I', 1)) # e_version: EV_CURRENT
elf.extend(struct.pack('<Q', 0)) # e_entry: entry point
elf.extend(struct.pack('<Q', 0)) # e_phoff: program header offset
elf.extend(struct.pack('<Q', 0)) # e_shoff: section header offset
elf.extend(struct.pack('<I', 0)) # e_flags
elf.extend(struct.pack('<H', 0)) # e_ehsize
elf.extend(struct.pack('<H', 0)) # e_phentsize
elf.extend(struct.pack('<H', 0)) # e_phnum
elf.extend(struct.pack('<H', 0)) # e_shentsize
elf.extend(struct.pack('<H', 0)) # e_shnum
elf.extend(struct.pack('<H', 0)) # e_shstrndx
# Padding for fuzzer mutation headroom
elf.extend(b'\x00' * 8)
return bytes(elf)
def generate_minimal_elf32():
"""Generate a minimal 32-bit ELF PIE header (60 bytes).
Structure: ELF32 header (52 bytes) with e_type=ET_DYN, e_machine=EM_386,
plus 8 bytes padding for fuzzer mutation headroom.
"""
elf = bytearray()
# e_ident[16]: ELF identification
elf.extend(b'\x7fELF') # EI_MAG: magic number
elf.append(1) # EI_CLASS: ELFCLASS32
elf.append(1) # EI_DATA: ELFDATA2LSB (little-endian)
elf.append(1) # EI_VERSION: EV_CURRENT
elf.append(0) # EI_OSABI: ELFOSABI_NONE
elf.extend(b'\x00' * 8) # EI_PAD: padding
# ELF header fields
elf.extend(struct.pack('<H', 3)) # e_type: ET_DYN (PIE)
elf.extend(struct.pack('<H', 3)) # e_machine: EM_386
elf.extend(struct.pack('<I', 1)) # e_version: EV_CURRENT
elf.extend(struct.pack('<I', 0)) # e_entry: entry point
elf.extend(struct.pack('<I', 0)) # e_phoff: program header offset
elf.extend(struct.pack('<I', 0)) # e_shoff: section header offset
elf.extend(struct.pack('<I', 0)) # e_flags
elf.extend(struct.pack('<H', 0)) # e_ehsize
elf.extend(struct.pack('<H', 0)) # e_phentsize
elf.extend(struct.pack('<H', 0)) # e_phnum
elf.extend(struct.pack('<H', 0)) # e_shentsize
elf.extend(struct.pack('<H', 0)) # e_shnum
elf.extend(struct.pack('<H', 0)) # e_shstrndx
# Padding for fuzzer mutation headroom
elf.extend(b'\x00' * 8)
return bytes(elf)
def main():
os.chdir(SCRIPT_DIR)
elf64 = generate_minimal_elf64()
elf32 = generate_minimal_elf32()
with open('minimal64.elf', 'wb') as f:
f.write(elf64)
print(f"Generated minimal64.elf ({len(elf64)} bytes)")
with open('minimal32.elf', 'wb') as f:
f.write(elf32)
print(f"Generated minimal32.elf ({len(elf32)} bytes)")
if __name__ == '__main__':
main()
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
0xff & 0x0f
+1
View File
@@ -0,0 +1 @@
~0
+1
View File
@@ -0,0 +1 @@
(1+2)*3
+1
View File
@@ -0,0 +1 @@
1+2*3
+1
View File
@@ -0,0 +1 @@
1 << 4
+1
View File
@@ -0,0 +1 @@
1+2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
$<TARGET_FILE:mylib>
+1
View File
@@ -0,0 +1 @@
$<IF:$<AND:$<BOOL:${BUILD_SHARED},$<NOT:$<PLATFORM_ID:Windows>>>>,shared,static>
+1
View File
@@ -0,0 +1 @@
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>$<INSTALL_INTERFACE:include>
+1
View File
@@ -0,0 +1 @@
$<$<CONFIG:Debug>:DEBUG_MODE>
+1
View File
@@ -0,0 +1 @@
$<TARGET_PROPERTY:mylib,INTERFACE_INCLUDE_DIRECTORIES>
+1
View File
@@ -0,0 +1 @@
{"a":{"b":{"c":{"d":[1,2,3]}}}}
+10
View File
@@ -0,0 +1,10 @@
{
"version": 3,
"configurePresets": [
{
"name": "default",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build"
}
]
}
+9
View File
@@ -0,0 +1,9 @@
{
"string": "hello",
"number": 42,
"float": 3.14,
"bool": true,
"null": null,
"array": [1, "two", false],
"object": {"nested": "value"}
}
+3
View File
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.10)
project(Test)
add_executable(main main.cpp)
@@ -0,0 +1,2 @@
set(x [=[nested [[brackets]] here]=])
set(y [==[even more [=[nested]=] brackets]==])
+29
View File
@@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.16)
project(ComplexProject VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
option(BUILD_TESTS "Build tests" ON)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
function(my_function arg1 arg2)
message(STATUS "Function called with ${arg1} and ${arg2}")
endfunction()
# Bracket comment [[
This is a bracket comment
]]
set(LONG_STRING [[
This is a bracket
argument with multiple
lines
]])
foreach(item IN ITEMS a b c)
message("Item: ${item}")
endforeach()
+1
View File
@@ -0,0 +1 @@
/home/user/../other/./path
+1
View File
@@ -0,0 +1 @@
../build/CMakeFiles
+1
View File
@@ -0,0 +1 @@
/usr/local/lib/cmake
+1
View File
@@ -0,0 +1 @@
C:\Program Files\CMake\bin
+5
View File
@@ -0,0 +1,5 @@
Name: WithDeps
Version: 2.0
Requires: dep1 >= 1.0, dep2
Requires.private: internal
Libs: -lwithdeps
+9
View File
@@ -0,0 +1,9 @@
prefix=/usr
libdir=${prefix}/lib
includedir=${prefix}/include
Name: Example
Description: An example library
Version: 1.0.0
Libs: -L${libdir} -lexample
Cflags: -I${includedir}
@@ -0,0 +1,20 @@
cmake_path(SET p "/usr/local/lib/cmake/foo.cmake")
cmake_path(GET p ROOT_NAME root)
cmake_path(GET p ROOT_DIRECTORY rootdir)
cmake_path(GET p ROOT_PATH rootpath)
cmake_path(GET p FILENAME fname)
cmake_path(GET p EXTENSION ext)
cmake_path(GET p STEM stem)
cmake_path(GET p RELATIVE_PART rel)
cmake_path(GET p PARENT_PATH parent)
cmake_path(APPEND p "bar" OUTPUT_VARIABLE appended)
cmake_path(REMOVE_FILENAME p)
cmake_path(REPLACE_FILENAME p "new.txt")
cmake_path(REMOVE_EXTENSION p)
cmake_path(REPLACE_EXTENSION p ".hpp")
cmake_path(NORMAL_PATH p)
cmake_path(IS_ABSOLUTE p result)
cmake_path(IS_RELATIVE p result)
cmake_path(HAS_ROOT_NAME p result)
cmake_path(HAS_FILENAME p result)
cmake_path(COMPARE p EQUAL "/usr/local" equal_result)
@@ -0,0 +1,14 @@
set(input "Hello;World;From;CMake")
string(REPLACE ";" " " spaced "${input}")
string(REGEX REPLACE "([A-Z])" "_\\1" snake "${spaced}")
string(REGEX MATCHALL "[A-Za-z]+" words "${spaced}")
string(GENEX_STRIP "$<TARGET_FILE:foo>" stripped)
string(APPEND result "line1\n" "line2\n")
string(PREPEND result "header\n")
string(CONCAT full "a" "b" "c" "d")
string(JOIN ":" joined a b c d)
string(MAKE_C_IDENTIFIER "my-var.name" c_id)
string(RANDOM LENGTH 16 ALPHABET "0123456789ABCDEF" random)
string(TIMESTAMP ts "%Y-%m-%d %H:%M:%S")
string(UUID uuid NAMESPACE 6ba7b810-9dad-11d1-80b4-00c04fd430c8 NAME test TYPE SHA1)
string(JSON json_val GET "{\"key\":\"value\"}" "key")
@@ -0,0 +1,7 @@
set(VERSION "1.0.0")
set(AUTHOR "Test Author")
file(WRITE /tmp/config.h.in "#define VERSION \"@VERSION@\"\n#define AUTHOR \"@AUTHOR@\"\n")
configure_file(/tmp/config.h.in /tmp/config.h @ONLY)
file(READ /tmp/config.h config_content)
message(STATUS "Configured: ${config_content}")
file(REMOVE /tmp/config.h.in /tmp/config.h)
@@ -0,0 +1,12 @@
set(X 5)
if(X GREATER 3)
message("X > 3")
elseif(X EQUAL 3)
message("X == 3")
else()
message("X < 3")
endif()
foreach(i RANGE 3)
message("i = ${i}")
endforeach()
@@ -0,0 +1,10 @@
execute_process(
COMMAND echo "Hello from process"
OUTPUT_VARIABLE output
ERROR_VARIABLE error
RESULT_VARIABLE result
TIMEOUT 5
WORKING_DIRECTORY /tmp
)
message(STATUS "Output: ${output}")
message(STATUS "Result: ${result}")
@@ -0,0 +1,4 @@
file(WRITE "/tmp/cmake_fuzz_test.txt" "test content")
file(READ "/tmp/cmake_fuzz_test.txt" content)
file(REMOVE "/tmp/cmake_fuzz_test.txt")
file(GLOB files "/tmp/*.txt")
@@ -0,0 +1,7 @@
function(my_func arg1 arg2)
message("arg1: ${arg1}, arg2: ${arg2}")
set(RESULT "${arg1}_${arg2}" PARENT_SCOPE)
endfunction()
my_func("hello" "world")
message("Result: ${RESULT}")
@@ -0,0 +1,17 @@
set(MYLIST a b c d e f)
list(LENGTH MYLIST len)
list(GET MYLIST 0 first)
list(GET MYLIST -1 last)
list(APPEND MYLIST g h)
list(PREPEND MYLIST z)
list(INSERT MYLIST 2 x y)
list(FIND MYLIST c idx)
list(REMOVE_AT MYLIST 0)
list(REMOVE_ITEM MYLIST e)
list(REMOVE_DUPLICATES MYLIST)
list(REVERSE MYLIST)
list(SORT MYLIST)
list(SUBLIST MYLIST 1 3 sub)
list(JOIN MYLIST "," joined)
list(TRANSFORM MYLIST TOUPPER)
list(FILTER MYLIST INCLUDE REGEX "[A-Z]")
@@ -0,0 +1,3 @@
math(EXPR result "1 + 2 * 3")
math(EXPR hex "0xFF" OUTPUT_FORMAT HEXADECIMAL)
math(EXPR dec "0x10" OUTPUT_FORMAT DECIMAL)
@@ -0,0 +1,6 @@
set(STR "Hello World")
string(LENGTH "${STR}" len)
string(TOUPPER "${STR}" upper)
string(TOLOWER "${STR}" lower)
string(REPLACE "World" "CMake" result "${STR}")
string(REGEX MATCH "[A-Z]+" match "${STR}")
@@ -0,0 +1,5 @@
set(MY_VAR "hello")
set(MY_LIST a b c d)
message(STATUS "VAR: ${MY_VAR}")
list(LENGTH MY_LIST len)
message(STATUS "Length: ${len}")
@@ -0,0 +1 @@
${CMAKE_SOURCE_DIR}
+1
View File
@@ -0,0 +1 @@
item1;item2;item3;item4
+1
View File
@@ -0,0 +1 @@
"quoted string with spaces"
+1
View File
@@ -0,0 +1 @@
3.28.0.20231201
+1
View File
@@ -0,0 +1 @@
1.2.3-alpha+build
+1
View File
@@ -0,0 +1 @@
3.28.0
+1
View File
@@ -0,0 +1 @@
3.28.0.1
+75
View File
@@ -0,0 +1,75 @@
# XML dictionary for xml_parser_fuzzer
# Processing instruction
"<?xml"
"?>"
"version="
"encoding="
"standalone="
# Document structure
"<!DOCTYPE"
"<!ELEMENT"
"<!ATTLIST"
"<!ENTITY"
"<!NOTATION"
"SYSTEM"
"PUBLIC"
# Tags
"<"
">"
"/>"
"</"
# Attributes
"="
"\x22"
"\x27"
# CDATA and comments
"<![CDATA["
"]]>"
"<!--"
"-->"
# Namespaces
"xmlns"
"xmlns:"
# Common CMake XML elements
"<cmake"
"<project"
"<target"
"<file"
"<dependency"
"<source"
"<include"
"<define"
"<flag"
"<option"
"<property"
"<configuration"
"<install"
"<test"
"<cache"
# Entity references
"&lt;"
"&gt;"
"&amp;"
"&quot;"
"&apos;"
"&#"
"&#x"
# Whitespace
"\x09"
"\x0a"
"\x0d"
"\x20"
# Special patterns
"]]]]><![CDATA["
"-->"
"?>"
+6
View File
@@ -33,4 +33,10 @@ are organized as follows.
See `RunCMake/README.rst`_.
* ``Fuzzing/``:
Fuzz testing targets using libFuzzer, integrated with OSS-Fuzz.
See `Fuzzing/README.rst`_.
.. _`RunCMake/README.rst`: RunCMake/README.rst
.. _`Fuzzing/README.rst`: Fuzzing/README.rst