Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/fuzzing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Fuzzing

on:
schedule:
# Weekly, Mondays at 07:17 UTC (off the top-of-hour to spread CI load).
- cron: '17 7 * * 1'
workflow_dispatch:
inputs:
max_total_time:
description: 'Seconds to run each fuzz target'
required: false
default: '120'

permissions:
contents: read

jobs:
fuzz:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y clang

# The fuzz harness is header-only (it includes the queue headers directly
# and links nothing from the library), so we compile it directly with
# Clang + libFuzzer rather than configuring the whole project. This keeps
# the weekly job fast and independent of vcpkg / common_system.
- name: Build fuzz target
run: |
mkdir -p build-fuzz/bin
clang++ -std=c++20 -g -O1 \
-fsanitize=fuzzer,address -fno-omit-frame-pointer \
-I include \
fuzz/concurrent_queue_fuzzer.cpp \
-o build-fuzz/bin/concurrent_queue_fuzzer

- name: Run concurrent_queue fuzzer
run: |
MAX_TIME="${{ github.event.inputs.max_total_time || '120' }}"
./build-fuzz/bin/concurrent_queue_fuzzer \
-max_total_time="${MAX_TIME}" \
-print_final_stats=1 \
fuzz/corpus

- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crashes
path: |
crash-*
oom-*
timeout-*
if-no-files-found: ignore
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ option(THREAD_BUILD_INTEGRATION_TESTS "Build integration tests" ON)
option(BUILD_DOCUMENTATION "Build Doxygen documentation" ON)
option(ENABLE_COVERAGE "Enable code coverage" OFF)

# libFuzzer targets (requires Clang -fsanitize=fuzzer,address; off by default,
# excluded from the default build and the regular CI matrix). See fuzz/.
option(BUILD_FUZZERS "Build libFuzzer targets (requires Clang)" OFF)

# Lock-free queue option (TICKET-001 RESOLVED - Now safe with Hazard Pointers)
option(THREAD_ENABLE_LOCKFREE_QUEUE "Enable lock-free queue (safe with Hazard Pointers)" ON)

Expand Down Expand Up @@ -161,6 +165,11 @@ if(NOT BUILD_THREADSYSTEM_AS_SUBMODULE)
# Add benchmarks
add_benchmarks_subdirectory()

# Add fuzzers (libFuzzer; off by default, requires Clang)
if(BUILD_FUZZERS AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/fuzz/CMakeLists.txt)
add_subdirectory(fuzz)
endif()

# Setup documentation
setup_documentation()

Expand Down Expand Up @@ -278,6 +287,7 @@ message(STATUS " Submodule: ${BUILD_THREADSYSTEM_AS_SUBMODULE}")
message(STATUS " common_system: ${BUILD_WITH_COMMON_SYSTEM}")
message(STATUS " std::format: ${USE_STD_FORMAT}")
message(STATUS " Coverage: ${ENABLE_COVERAGE}")
message(STATUS " Fuzzers: ${BUILD_FUZZERS}")
message(STATUS " Lock-free queue: ${THREAD_ENABLE_LOCKFREE_QUEUE}")
if(THREAD_ENABLE_LOCKFREE_QUEUE)
message(STATUS " TICKET-001 resolved: Safe with Hazard Pointers")
Expand Down
41 changes: 41 additions & 0 deletions fuzz/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
##################################################
# thread_system Fuzzing CMakeLists.txt
#
# libFuzzer-based fuzz targets, gated behind the BUILD_FUZZERS option
# (default OFF). Requires a Clang toolchain with the fuzzer and address
# sanitizers:
#
# cmake -B build-fuzz -G Ninja \
# -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
# -DBUILD_FUZZERS=ON -DTHREAD_BUILD_INTEGRATION_TESTS=OFF \
# -DBUILD_DOCUMENTATION=OFF
# cmake --build build-fuzz
# ./build-fuzz/bin/concurrent_queue_fuzzer -max_total_time=60 fuzz/corpus
#
# The targets are excluded from the default build and from the regular CI
# matrix; they are exercised by .github/workflows/fuzzing.yml on a weekly
# schedule. The harness here is header-only (it includes the queue headers
# directly), so it does not need the full thread_system library link or vcpkg.
##################################################

if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
message(WARNING
"BUILD_FUZZERS is ON but the compiler is '${CMAKE_CXX_COMPILER_ID}'. "
"libFuzzer requires Clang; skipping fuzz targets.")
return()
endif()

set(FUZZER_FLAGS -g -O1 -fsanitize=fuzzer,address -fno-omit-frame-pointer)

function(add_fuzz_target target_name source_file)
add_executable(${target_name} ${source_file})
target_include_directories(${target_name} PRIVATE
${CMAKE_SOURCE_DIR}/include)
target_compile_features(${target_name} PRIVATE cxx_std_20)
target_compile_options(${target_name} PRIVATE ${FUZZER_FLAGS})
target_link_options(${target_name} PRIVATE ${FUZZER_FLAGS})
endfunction()

add_fuzz_target(concurrent_queue_fuzzer concurrent_queue_fuzzer.cpp)

message(STATUS "Fuzz targets enabled: concurrent_queue_fuzzer")
67 changes: 67 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Fuzzing

libFuzzer-based fuzz harnesses for thread_system's lock-free / concurrent queue
and resilience paths.

## Targets

| Target | Area | Source |
|--------|------|--------|
| `concurrent_queue_fuzzer` | The thread-safe queue reachable through the lock-free compatibility include path (`kcenon::thread::detail::concurrent_queue`, the backing for `<kcenon/thread/lockfree/lockfree_queue.h>`): enqueue (copy/move), `try_dequeue` including the empty path, `wait_dequeue(timeout)`, `empty()`/`size()` queries, `shutdown()`/`is_shutdown()` resilience signalling, and FIFO round-trip integrity. | `concurrent_queue_fuzzer.cpp` |

The harness encodes the fuzzer input as an opcode stream: each byte drives one
queue operation (opcode in the high 3 bits, payload in the low 5 bits).

## Requirements

- Clang with the fuzzer and address sanitizers (`-fsanitize=fuzzer,address`).
GCC is not supported for these targets — the CMake config skips them with a
warning under non-Clang compilers.

The targets are header-only: they include the queue headers directly and do not
need the full thread_system library link or vcpkg.

## Building and running locally

```bash
cmake -B build-fuzz -G Ninja \
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
-DBUILD_FUZZERS=ON \
-DTHREAD_BUILD_INTEGRATION_TESTS=OFF \
-DBUILD_DOCUMENTATION=OFF
cmake --build build-fuzz

# Run for a bounded time, seeded from the checked-in corpus:
./build-fuzz/bin/concurrent_queue_fuzzer -max_total_time=60 fuzz/corpus
```

A crash reproducer (if any) is written to the working directory as
`crash-<hash>`; replay it with:

```bash
./build-fuzz/bin/concurrent_queue_fuzzer crash-<hash>
```

## Seed corpus

`corpus/` contains a few hand-crafted seeds that exercise the major code paths
(enqueue/dequeue round-trips, fill-then-drain, and query/shutdown mixes). Add
interesting inputs discovered during fuzzing here.

## CI

`.github/workflows/fuzzing.yml` builds these targets with Clang+libFuzzer and
runs them for a short bounded time on a weekly schedule and on manual dispatch.
The targets are excluded from the default build and from the regular CI matrix
(`BUILD_FUZZERS` defaults to `OFF`).

## Follow-ups (issue #697)

This PR adds the harness scaffolding only. Still to do under the same issue:

- Phased line-coverage raise (40% -> 50% -> 60%) with the codecov floor bumped
accordingly.
- A dedicated target for the true lock-free `kcenon::thread::lockfree_job_queue`
(Michael-Scott + hazard pointers). That one needs the full library link
(`src/lockfree`, `job`, and the common_system `result<T>` chain), so it is
deferred until the harness can link the library cleanly in CI.
136 changes: 136 additions & 0 deletions fuzz/concurrent_queue_fuzzer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// BSD 3-Clause License
// Copyright (c) 2024, 🍀☀🌕🌥 🌊
// See the LICENSE file in the project root for full license information.

/**
* @file concurrent_queue_fuzzer.cpp
* @brief libFuzzer harness for the lock-free / resilience queue surface.
*
* This harness drives kcenon::thread::detail::concurrent_queue<T> with a
* fuzzer-controlled opcode stream. concurrent_queue is the implementation that
* backs the legacy <kcenon/thread/lockfree/lockfree_queue.h> include path, and
* it is the queue's input-handling and resilience surface (enqueue/dequeue,
* empty/size queries, the wait-with-timeout path, and shutdown signalling).
*
* The harness explores:
* - enqueue (copy and move overloads),
* - try_dequeue including the empty-queue resilience path,
* - wait_dequeue(timeout) with a tiny timeout (non-blocking probe),
* - empty()/size() state queries,
* - shutdown()/is_shutdown() resilience signalling,
* - FIFO round-trip integrity against a shadow model.
*
* It is intentionally single-threaded: libFuzzer requires deterministic replay,
* so this targets input handling and state-machine edge cases rather than
* multi-threaded interleavings. Concurrency stress remains the job of the unit
* tests under tests/unit/lockfree_test/.
*
* The true lock-free queue (kcenon::thread::lockfree_job_queue, Michael-Scott +
* hazard pointers) requires the full library link (src/impl/lockfree + job +
* common_system result<T>); a dedicated target for it is tracked as a follow-up
* under issue #697 (see fuzz/README.md).
*
* Build via the BUILD_FUZZERS CMake option (clang -fsanitize=fuzzer,address).
*/

// Silence the legacy-include deprecation pragma; we deliberately fuzz the
// queue reachable through the lock-free compatibility path.
#define THREAD_SUPPRESS_LEGACY_LOCKFREE_QUEUE_WARNING 1
#include <kcenon/thread/lockfree/lockfree_queue.h>

#include <chrono>
#include <cstddef>
#include <cstdint>
#include <vector>

namespace {

using kcenon::thread::detail::concurrent_queue;

// Opcodes encoded in the high bits of each input byte; the low bits feed the
// payload (the value to enqueue), so a single byte fully drives one step.
enum class op : std::uint8_t {
enqueue_copy = 0,
enqueue_move = 1,
try_dequeue = 2,
wait_dequeue = 3,
query = 4,
shutdown = 5,
};

} // namespace

extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) {
concurrent_queue<std::uint32_t> queue;

// Shadow model of values enqueued but not yet dequeued. For a FIFO queue,
// successful dequeues must return values in enqueue order, so we can assert
// round-trip integrity on the dequeue paths.
std::vector<std::uint32_t> shadow;
bool shut = false;

for (std::size_t i = 0; i < size; ++i) {
const std::uint8_t byte = data[i];
const auto code = static_cast<op>((byte >> 5) % 6u);
const std::uint32_t payload = static_cast<std::uint32_t>(byte & 0x1Fu);

switch (code) {
case op::enqueue_copy: {
queue.enqueue(payload);
shadow.push_back(payload);
break;
}
case op::enqueue_move: {
std::uint32_t movable = payload;
queue.enqueue(std::move(movable));
shadow.push_back(payload);
break;
}
case op::try_dequeue: {
auto maybe = queue.try_dequeue();
if (maybe.has_value()) {
if (shadow.empty() || *maybe != shadow.front()) {
__builtin_trap(); // FIFO integrity violation
}
shadow.erase(shadow.begin());
}
break;
}
case op::wait_dequeue: {
// Tiny timeout so the harness stays deterministic and fast even
// when the queue is empty (exercises the timeout resilience
// path without actually blocking the fuzzer for long).
auto maybe = queue.wait_dequeue(std::chrono::milliseconds(0));
if (maybe.has_value()) {
if (shadow.empty() || *maybe != shadow.front()) {
__builtin_trap();
}
shadow.erase(shadow.begin());
}
break;
}
case op::query: {
// Exercise the read-only surface; sink results so the optimizer
// cannot elide the calls.
volatile bool e = queue.empty();
volatile std::size_t n = queue.size();
volatile bool s = queue.is_shutdown();
(void)e;
(void)n;
(void)s;
break;
}
case op::shutdown: {
queue.shutdown();
shut = true;
if (!queue.is_shutdown()) {
__builtin_trap(); // shutdown must be observable
}
break;
}
}
}

(void)shut;
return 0;
}
1 change: 1 addition & 0 deletions fuzz/corpus/seed_enqueue_dequeue
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CDef
1 change: 1 addition & 0 deletions fuzz/corpus/seed_fill_then_drain
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@ABC
1 change: 1 addition & 0 deletions fuzz/corpus/seed_query_shutdown
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
��`@