Skip to content

Commit 7c43afa

Browse files
ci(hardening): TSAN + valgrind + libFuzzer lanes (#46)
* ci(hardening): add TSAN, valgrind, and libFuzzer lanes - ENABLE_TSAN option + ci-tsan preset (x64-linux-tsan triplet) — TSAN over the gRPC server's worker-thread coordination via the Python integration tests. - ENABLE_FUZZING + ci-fuzz preset (clang) + cmake/AddFuzzTarget.cmake; two libFuzzer targets over the JSON and YAML graph loaders. - Hardening workflow: tsan + shared valgrind-leak (core unit binary) + shared libFuzzer runner. Suppressions in tsan.supp (grpc/abseil) / valgrind.supp. Refs anolishq/.github#54. * fix(server): make gRPC reflection optional The x64-linux-tsan grpc build ships a minimal feature set without grpc++_reflection; link it (and compile the InitProtoReflection call) only when the target is present. The normal x64-linux build is unaffected (reflection still enabled); the integration tests don't use reflection. * fix(server): make shutdown signal handler async-signal-safe TSAN's signal-safety check flagged the handler calling g_server->Shutdown() and std::cout directly (both allocate). Set a lock-free atomic flag in the handler instead and perform the Shutdown() from a watcher thread, off the signal context. Surfaced by the new TSAN hardening lane.
1 parent f20498a commit 7c43afa

16 files changed

Lines changed: 522 additions & 9 deletions

.github/workflows/hardening.yml

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
name: Hardening
2+
3+
# Memory-safety / data-race hardening for fluxgraph.
4+
# - tsan: ThreadSanitizer over the gRPC server (its state_mutex_ + tick_cv_
5+
# worker coordination), exercised by the Python integration tests.
6+
# - valgrind: shared org leak-check over the core unit-test binary.
7+
# - fuzz: shared org libFuzzer runner over the JSON + YAML graph loaders
8+
# (the untrusted-input parsers).
9+
# Findings fail only when first-party; third-party false positives are scoped in
10+
# tsan.supp / valgrind.supp (first run per change = triage pass).
11+
12+
on:
13+
push:
14+
branches: [main]
15+
paths-ignore:
16+
- "docs/**"
17+
- "**/*.md"
18+
pull_request:
19+
branches: [main]
20+
paths-ignore:
21+
- "docs/**"
22+
- "**/*.md"
23+
workflow_dispatch:
24+
25+
concurrency:
26+
group: fluxgraph-hardening-${{ github.ref }}
27+
cancel-in-progress: true
28+
29+
permissions:
30+
contents: read
31+
32+
jobs:
33+
tsan:
34+
name: ThreadSanitizer (server)
35+
runs-on: ubuntu-24.04
36+
timeout-minutes: 150
37+
steps:
38+
- name: Checkout
39+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
40+
41+
- name: Install system dependencies
42+
run: |
43+
sudo apt-get update
44+
sudo apt-get install -y ninja-build g++
45+
46+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
47+
with:
48+
python-version: "3.12"
49+
50+
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
51+
with:
52+
enable-cache: true
53+
54+
- name: Install Python integration deps
55+
run: uv sync --locked --extra dev
56+
57+
- name: Setup vcpkg
58+
uses: anolishq/.github/.github/actions/setup-vcpkg@0f4d8cd3c04dcb17fe128248a147b237c1465e0c # v2
59+
with:
60+
triplet: x64-linux
61+
62+
- name: Configure
63+
run: cmake --preset ci-tsan -DPython3_EXECUTABLE="$(pwd)/.venv/bin/python"
64+
65+
- name: Build
66+
run: cmake --build --preset ci-tsan --parallel
67+
68+
- name: Generate Python protobuf bindings
69+
run: bash ./scripts/generate_proto_python.sh "$(pwd)/build/ci-tsan/python"
70+
71+
- name: Run tests with TSAN
72+
env:
73+
TSAN_OPTIONS: "suppressions=${{ github.workspace }}/tsan.supp second_deadlock_stack=1 detect_deadlocks=1 history_size=7 log_path=${{ github.workspace }}/tsan-report"
74+
run: |
75+
set +e
76+
set -o pipefail
77+
export CTEST_PARALLEL_LEVEL=1
78+
timeout 40m ctest --preset ci-tsan --timeout 600 -VV 2>&1 | tee tsan-output.log
79+
EXIT_CODE=${PIPESTATUS[0]}
80+
81+
if ls tsan-report.* >/dev/null 2>&1; then
82+
echo "::error::ThreadSanitizer reported data races (see tsan-logs artifact)"
83+
exit 1
84+
fi
85+
86+
exit $EXIT_CODE
87+
88+
- name: Upload TSAN logs
89+
if: always()
90+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
91+
with:
92+
name: tsan-logs-${{ github.run_number }}
93+
path: |
94+
tsan-report.*
95+
tsan-output.log
96+
build/ci-tsan/Testing/Temporary/LastTest.log
97+
if-no-files-found: ignore
98+
retention-days: 14
99+
100+
valgrind:
101+
name: Valgrind leak-check
102+
uses: anolishq/.github/.github/workflows/valgrind-leak.yml@0f4d8cd3c04dcb17fe128248a147b237c1465e0c # v2
103+
with:
104+
triplet: x64-linux
105+
build-preset: ci-linux-release
106+
build-target: fluxgraph_tests
107+
run: "build/ci-linux-release/tests/fluxgraph_tests"
108+
suppressions: valgrind.supp
109+
110+
fuzz:
111+
name: Fuzz (libFuzzer)
112+
uses: anolishq/.github/.github/workflows/fuzz.yml@0f4d8cd3c04dcb17fe128248a147b237c1465e0c # v2
113+
with:
114+
triplet: x64-linux
115+
build-preset: ci-fuzz
116+
fuzz-binaries: "build/ci-fuzz/fuzz/fuzz_json_loader build/ci-fuzz/fuzz/fuzz_yaml_loader"
117+
corpus-root: fuzz/corpus
118+
max-total-time: "60"

CMakeLists.txt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,38 @@ set(CMAKE_CXX_EXTENSIONS OFF)
3838
# Export compile commands for IDE support
3939
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
4040

41+
# ThreadSanitizer (TSAN) — data-race detection for the gRPC server's worker
42+
# threads. Applied to all targets; deps are rebuilt with TSAN via x64-linux-tsan.
43+
option(ENABLE_TSAN "Enable ThreadSanitizer for data race detection" OFF)
44+
45+
if(ENABLE_TSAN)
46+
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
47+
if(NOT WIN32)
48+
add_compile_options(-fsanitize=thread -g -fno-omit-frame-pointer)
49+
add_link_options(-fsanitize=thread)
50+
message(STATUS "ThreadSanitizer (TSAN) enabled for data race detection")
51+
else()
52+
message(WARNING "ThreadSanitizer not supported on Windows (Linux/macOS only)")
53+
endif()
54+
else()
55+
message(WARNING "ThreadSanitizer requested but compiler not supported: ${CMAKE_CXX_COMPILER_ID}")
56+
endif()
57+
elseif(DEFINED VCPKG_TARGET_TRIPLET AND VCPKG_TARGET_TRIPLET MATCHES "tsan")
58+
if(NOT WIN32)
59+
message(STATUS "ThreadSanitizer auto-detected via triplet: ${VCPKG_TARGET_TRIPLET}")
60+
add_compile_options(-fsanitize=thread -g -fno-omit-frame-pointer)
61+
add_link_options(-fsanitize=thread)
62+
endif()
63+
endif()
64+
65+
# Fuzzing (libFuzzer, requires clang). Include the helper BEFORE the targets so
66+
# its instrumentation applies to the code-under-test (the loaders), not just the
67+
# harness.
68+
option(ENABLE_FUZZING "Build libFuzzer fuzz targets (requires clang)" OFF)
69+
if(ENABLE_FUZZING)
70+
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/AddFuzzTarget.cmake)
71+
endif()
72+
4173
# Core library sources
4274
set(FLUXGRAPH_SOURCES
4375
src/core/signal_store.cpp
@@ -90,6 +122,10 @@ target_include_directories(fluxgraph PUBLIC
90122
# Compiler warnings
91123
if(MSVC)
92124
target_compile_options(fluxgraph PRIVATE /W4 /WX)
125+
elseif(ENABLE_FUZZING)
126+
# The fuzz lane is the first clang build of this repo; don't fail it on
127+
# clang-only warnings (the gcc lanes keep -Werror).
128+
target_compile_options(fluxgraph PRIVATE -Wall -Wextra -pedantic)
93129
else()
94130
target_compile_options(fluxgraph PRIVATE -Wall -Wextra -Werror -pedantic)
95131
endif()
@@ -126,6 +162,11 @@ if(FLUXGRAPH_BUILD_SERVER)
126162
add_subdirectory(server)
127163
endif()
128164

165+
# Fuzz targets (libFuzzer; loaders must be enabled to have something to fuzz)
166+
if(ENABLE_FUZZING)
167+
add_subdirectory(fuzz)
168+
endif()
169+
129170
# Install/export package configuration
130171
install(TARGETS fluxgraph
131172
EXPORT fluxgraphTargets

CMakePresets.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,32 @@
120120
"FLUXGRAPH_YAML_ENABLED": "ON"
121121
}
122122
},
123+
{
124+
"name": "ci-tsan",
125+
"inherits": "ci-linux-release-server",
126+
"binaryDir": "${sourceDir}/build/ci-tsan",
127+
"cacheVariables": {
128+
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
129+
"VCPKG_TARGET_TRIPLET": "x64-linux-tsan",
130+
"VCPKG_OVERLAY_TRIPLETS": "${sourceDir}/triplets",
131+
"ENABLE_TSAN": "ON"
132+
}
133+
},
134+
{
135+
"name": "ci-fuzz",
136+
"inherits": "ci-linux-release",
137+
"binaryDir": "${sourceDir}/build/ci-fuzz",
138+
"cacheVariables": {
139+
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
140+
"CMAKE_C_COMPILER": "clang",
141+
"CMAKE_CXX_COMPILER": "clang++",
142+
"FLUXGRAPH_JSON_ENABLED": "ON",
143+
"FLUXGRAPH_YAML_ENABLED": "ON",
144+
"FLUXGRAPH_BUILD_TESTS": "OFF",
145+
"FLUXGRAPH_BUILD_EXAMPLES": "OFF",
146+
"ENABLE_FUZZING": "ON"
147+
}
148+
},
123149
{
124150
"name": "dev-windows-server",
125151
"inherits": "base-windows-msvc",
@@ -191,6 +217,14 @@
191217
"name": "ci-linux-release-server",
192218
"configurePreset": "ci-linux-release-server"
193219
},
220+
{
221+
"name": "ci-tsan",
222+
"configurePreset": "ci-tsan"
223+
},
224+
{
225+
"name": "ci-fuzz",
226+
"configurePreset": "ci-fuzz"
227+
},
194228
{
195229
"name": "dev-windows-server",
196230
"configurePreset": "dev-windows-server",
@@ -284,6 +318,13 @@
284318
"outputOnFailure": true
285319
}
286320
},
321+
{
322+
"name": "ci-tsan",
323+
"configurePreset": "ci-tsan",
324+
"output": {
325+
"outputOnFailure": true
326+
}
327+
},
287328
{
288329
"name": "dev-windows-server",
289330
"configurePreset": "dev-windows-server",

cmake/AddFuzzTarget.cmake

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# AddFuzzTarget.cmake — shared libFuzzer target helper for the anolishq C/C++ repos.
2+
#
3+
# Copy this file into a consumer repo (e.g. `cmake/AddFuzzTarget.cmake`) and
4+
# `include()` it. Gate fuzzing behind `option(ENABLE_FUZZING ...)` so normal
5+
# builds are unaffected:
6+
#
7+
# option(ENABLE_FUZZING "Build libFuzzer fuzz targets (requires clang)" OFF)
8+
# if(ENABLE_FUZZING)
9+
# include(cmake/AddFuzzTarget.cmake)
10+
# anolis_add_fuzz_target(NAME fuzz_config SOURCES fuzz/fuzz_config.cpp LINK anolis-core)
11+
# endif()
12+
#
13+
# Each target is an executable built with `-fsanitize=fuzzer,address` (debug
14+
# info, frame pointers). libFuzzer provides `main()`, so the harness only
15+
# defines `LLVMFuzzerTestOneInput`. Run locally with:
16+
#
17+
# ./fuzz_config -max_total_time=60 fuzz/corpus/fuzz_config
18+
#
19+
# The reusable `fuzz.yml` workflow builds and runs these in CI.
20+
21+
if(NOT ENABLE_FUZZING)
22+
return()
23+
endif()
24+
25+
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
26+
message(FATAL_ERROR
27+
"ENABLE_FUZZING requires a Clang toolchain (libFuzzer). "
28+
"Current compiler: ${CMAKE_CXX_COMPILER_ID}. "
29+
"Configure with -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++.")
30+
endif()
31+
32+
# Sanitizer set shared by every fuzz target. fuzzer-no-link instruments code for
33+
# coverage without pulling libFuzzer's main() into non-target objects.
34+
#
35+
# IMPORTANT: include() this BEFORE add_subdirectory() of the code-under-test so
36+
# the instrumentation below applies to it, not just the harness.
37+
set(ANOLIS_FUZZ_FLAGS
38+
-g -O1 -fno-omit-frame-pointer
39+
-fsanitize=fuzzer-no-link,address)
40+
41+
# Apply coverage+sanitizer instrumentation to the whole build so the
42+
# code-under-test (not just the harness) is instrumented.
43+
add_compile_options(${ANOLIS_FUZZ_FLAGS})
44+
add_link_options(-fsanitize=address)
45+
46+
# anolis_add_fuzz_target(NAME <name> SOURCES <src>... [LINK <lib>...])
47+
function(anolis_add_fuzz_target)
48+
cmake_parse_arguments(FZ "" "NAME" "SOURCES;LINK" ${ARGN})
49+
if(NOT FZ_NAME OR NOT FZ_SOURCES)
50+
message(FATAL_ERROR "anolis_add_fuzz_target: NAME and SOURCES are required")
51+
endif()
52+
add_executable(${FZ_NAME} ${FZ_SOURCES})
53+
# The target itself links libFuzzer (provides main()).
54+
target_compile_options(${FZ_NAME} PRIVATE -fsanitize=fuzzer)
55+
target_link_options(${FZ_NAME} PRIVATE -fsanitize=fuzzer)
56+
if(FZ_LINK)
57+
target_link_libraries(${FZ_NAME} PRIVATE ${FZ_LINK})
58+
endif()
59+
endfunction()

fuzz/CMakeLists.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Fuzz targets (built only when ENABLE_FUZZING=ON; see cmake/AddFuzzTarget.cmake).
2+
# Both loaders must be compiled into the fluxgraph library — the ci-fuzz preset
3+
# sets FLUXGRAPH_JSON_ENABLED + FLUXGRAPH_YAML_ENABLED.
4+
5+
if(NOT FLUXGRAPH_JSON_ENABLED OR NOT FLUXGRAPH_YAML_ENABLED)
6+
message(FATAL_ERROR
7+
"ENABLE_FUZZING requires FLUXGRAPH_JSON_ENABLED and FLUXGRAPH_YAML_ENABLED "
8+
"(the fuzz targets exercise the JSON/YAML loaders).")
9+
endif()
10+
11+
# JSON graph loader (fluxgraph::loaders::load_json_string).
12+
anolis_add_fuzz_target(
13+
NAME fuzz_json_loader
14+
SOURCES fuzz_json_loader.cpp
15+
LINK fluxgraph
16+
)
17+
18+
# YAML graph loader (fluxgraph::loaders::load_yaml_string).
19+
anolis_add_fuzz_target(
20+
NAME fuzz_yaml_loader
21+
SOURCES fuzz_yaml_loader.cpp
22+
LINK fluxgraph
23+
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"models": [
3+
{
4+
"id": "chamber",
5+
"type": "thermal_mass",
6+
"params": {
7+
"temp_signal": "chamber.temp",
8+
"power_signal": "chamber.power",
9+
"ambient_signal": "ambient.temp",
10+
"thermal_mass": 1000.0,
11+
"heat_transfer_coeff": 10.0,
12+
"initial_temp": 25.0
13+
}
14+
}
15+
]
16+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"edges": [
3+
{
4+
"source": "input.value",
5+
"target": "output.value",
6+
"transform": {
7+
"type": "linear",
8+
"params": {
9+
"scale": 2.0,
10+
"offset": 1.0
11+
}
12+
}
13+
}
14+
]
15+
}

0 commit comments

Comments
 (0)