From 7b96483e21d8bc75c491facdaaf5d864998f88a8 Mon Sep 17 00:00:00 2001 From: Davide Faconti Date: Mon, 14 Sep 2026 20:17:37 +0200 Subject: [PATCH] fix(source): armWatchdog self-join guard no longer races the thread assignment (0.34.1) The guard read state_.watchdog on the watchdog thread inside on_expire while the body thread was still move-assigning that member; the start semaphore orders only the other direction. Key the guard on a thread_local flag set by the watchdog thread before it runs on_expire. Reported by ThreadSanitizer via ProviderJobTest.RearmingTheWatchdogFromOnExpireIsANoOp. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 8 ++++++++ VERSION | 2 +- pj_base/src/source/provider_job.cpp | 14 ++++++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9233c011..1f6b2595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to `plotjuggler_sdk` are recorded here. Versioning policy is in [`CLAUDE.md`](./CLAUDE.md) → "Release Versioning". +## [0.34.1] + +- Fix a data race in `JobControl::armWatchdog`: the self-join guard read the + watchdog `std::thread` member on the watchdog thread while the body thread was + still move-assigning it. The guard now keys on a thread-local flag the watchdog + thread sets before running `on_expire`. Found by running the SDK tests under + ThreadSanitizer. + ## [0.34.0] Host contract: extended: PJ_DATA_PROCESSOR_FLAG_HISTORY_EXEMPT (floor 0.34.0). diff --git a/VERSION b/VERSION index 85e60ed1..cd46610f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.34.0 +0.34.1 diff --git a/pj_base/src/source/provider_job.cpp b/pj_base/src/source/provider_job.cpp index 51dfc761..3b2c0fb9 100644 --- a/pj_base/src/source/provider_job.cpp +++ b/pj_base/src/source/provider_job.cpp @@ -18,6 +18,13 @@ namespace source { namespace detail { +/// True only on a watchdog thread, set before it runs on_expire; the self-join +/// guard in armWatchdog reads it instead of the still-being-assigned thread object. +bool& inWatchdogThread() { + thread_local bool in_watchdog = false; + return in_watchdog; +} + struct JobState { ProviderJob::Body body; decltype(PJ_descriptor_import_callbacks_v1_t::on_dataset) on_dataset = nullptr; @@ -191,8 +198,10 @@ void JobControl::armWatchdog(std::chrono::milliseconds timeout, std::function lock(state->watchdog_mu); started.release(); const bool stopped = state->watchdog_cv.wait_for(lock, timeout, [state] { return state->watchdog_stop; });