Skip to content

Add a ThreadSanitizer build configuration and CI job - #121

Merged
kahrendt merged 4 commits into
mainfrom
tsan-build-config
Sep 10, 2026
Merged

Add a ThreadSanitizer build configuration and CI job#121
kahrendt merged 4 commits into
mainfrom
tsan-build-config

Conversation

@kahrendt

@kahrendt kahrendt commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

The host suite has only ever run under ASan/UBSan, which cannot be combined with ThreadSanitizer, so no data race in the sync task, connection threads, or inbox handoffs could show up in CI. This adds an ENABLE_TSAN option and a test-tsan job.

  • ENABLE_TSAN CMake option, default OFF; configuring it together with ENABLE_SANITIZERS is a FATAL_ERROR.
  • The flags are applied globally (CMAKE_C_FLAGS, CMAKE_CXX_FLAGS, linker flags) before the FetchContent dependencies are declared, not per target. TSan intercepts pthread calls in uninstrumented code but cannot see its atomics, and IXWebSocket signals its worker threads with std::atomic stop flags; a per-target build reports those as false races. This is the one deliberate departure from the shape on the encryption-support branch (bf0da76).
  • test-tsan job mirrors test and runs with TSAN_OPTIONS=halt_on_error=1. It sets vm.mmap_rnd_bits=28 first: recent runner kernels default 32-bit mmap randomization high enough that TSan aborts at startup with "unexpected memory mapping".
  • The test-standards skill, CONTRIBUTING.md, tests/README.md and CLAUDE.md are updated. The skill previously said CI runs ASan/UBSan only and a missing TSan run is not a finding; that rule inverts now that the job exists.

No production code changes. tests/CMakeLists.txt is untouched.

Test changes

The first CI run of test-tsan failed 13 of 19 artwork tests with the same report: a data race in ~RecordingListener (tests/test_artwork_role.cpp), pthread_cond_destroy on the main thread against pthread_cond_broadcast from the still-running artwork drain thread.

Every artwork test declared its RecordingListener after the Impl, so the listener was destroyed first while ~Impl was still stopping the drain thread; the drain thread could call notify_all() on a condition variable being destroyed. Commit 85f6bc0 declares the listener before the Impl in all 19 tests so it outlives the thread ~Impl joins. This is a test-scaffolding defect, not a library one; ArtworkRole::Impl itself already stops and joins in its destructor.

The host test suite has only ever run under ASan/UBSan. Those sanitizers
cannot be combined with TSan, so a data race in the threaded code paths
(the sync task, the connection threads, the inbox handoffs) has no way to
show up in CI at all. The library's whole contract with its consumer is
about which thread may touch what, and nothing was checking it.

`ENABLE_TSAN` is a separate option because of that incompatibility, and
configuring both at once now fails rather than producing a build where one
sanitizer silently wins.

The flags are applied globally, to `CMAKE_C_FLAGS`/`CMAKE_CXX_FLAGS` and
the linker flags before the FetchContent dependencies are declared, rather
than to the sendspin and sendspin_tests targets alone. TSan intercepts
pthread calls in uninstrumented code but cannot see its atomics, and
IXWebSocket signals its worker threads with `std::atomic` stop flags; a
per-target build reports those as races. Instrumenting everything is what
TSan asks for and is the configuration measured clean.

The CI job mirrors the existing test job. It drops `vm.mmap_rnd_bits` to
28 first: recent kernels default 32-bit mmap randomization high enough
that TSan aborts at startup on GitHub runners with "unexpected memory
mapping".

The suite is clean today, 123/123 with no TSan report, at roughly twice
the ASan wall time.
The test-standards skill told reviewers that CI runs under ASan/UBSan
only and that a missing ThreadSanitizer run is not a finding. That rule
existed to excuse reviewers from a configuration the project did not
have. With the TSan job in CI it inverts: a concurrency finding, or a
certification that a threaded test is adequate, is backed by a TSan run,
and a mutant that changes thread interaction is built under TSan because
ASan cannot see it.

CONTRIBUTING.md and tests/README.md gain the matching commands so the
"matching CI" claim next to the ASan block stays true.
Every artwork test declared its RecordingListener after the Impl, so the
listener was destroyed first while ~Impl was still stopping the drain
thread. The drain thread could then call notify_all() on a condition
variable the main thread was destroying. On the Linux TSan runtime this
reported as a data race in ~RecordingListener across 13 of the 19 tests;
the macOS runtime never caught the window.

Declaring the listener first makes it outlive the Impl, whose destructor
joins the drain thread, so no callback can reach a destroyed listener.
@kahrendt
kahrendt marked this pull request as ready for review September 10, 2026 13:14
@kahrendt
kahrendt requested a lite review from Copilot September 10, 2026 13:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A couple of small but concrete issues remain (CMake flag appending is non-idempotent on reconfigure, and the new TSan docs command should match CI’s BUILD_EXAMPLES=OFF usage).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a ThreadSanitizer (TSan) build configuration and CI job to catch data races in the host test suite, and adjusts artwork role tests to avoid a real TSan-reported teardown race in test scaffolding.

Changes:

  • Introduces ENABLE_TSAN (mutually exclusive with ENABLE_SANITIZERS) and applies TSan flags globally so FetchContent dependencies are instrumented.
  • Adds a test-tsan GitHub Actions job (with TSAN_OPTIONS=halt_on_error=1 and reduced vm.mmap_rnd_bits) and wires it into the CI “gate” job.
  • Fixes tests/test_artwork_role.cpp fixture lifetime so RecordingListener outlives the artwork drain thread during teardown.
File summaries
File Description
CMakeLists.txt Adds ENABLE_TSAN option and global TSan flag wiring.
.github/workflows/ci.yml Adds test-tsan job and includes it in the CI gate.
tests/test_artwork_role.cpp Reorders fixture declarations to eliminate a teardown race under TSan.
tests/README.md Documents how to run tests under TSan.
CONTRIBUTING.md Updates contributor instructions to include the TSan workflow.
CLAUDE.md Updates project build/test guidance to mention TSan configuration.
.claude/skills/test-standards/SKILL.md Updates test review guidance now that CI includes a TSan job.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread CMakeLists.txt
Comment thread tests/README.md Outdated
@kahrendt
kahrendt merged commit 669786c into main Sep 10, 2026
6 checks passed
@kahrendt
kahrendt deleted the tsan-build-config branch September 10, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants