Summary
CallbackScope documents every member as safe to call concurrently. reset() writes the _state member that four other members read, with no synchronisation on the pointer object itself.
Verification status
Inferred from reading the code, not reproduced. No test exercises it. Revision: origin/master adfe8e5f plus this branch's doc-only commits.
The claim
include/morph/core/callback_scope.hpp:194-198:
"@Par Thread safety All members are safe to call concurrently from any thread. requestStop() and stopRequested() are lock-free atomic operations; reset() publishes a fresh generation and retires the previous one…"
docs/spec/core/callback_scope.md:192 makes it normative: "Every member is safe to call from any thread, concurrently with any other."
What is actually true
_state is a plain shared_ptr, not an atomic one:
270: std::shared_ptr<detail::CallbackScopeState> _state;
reset() writes it (:240, _state = std::move(fresh);). Concurrently:
token() (:251) reads it — CallbackToken{_state}
guard() (:266) reads it via token()
requestStop() (:225-226) reads it twice
stopRequested() (:246) reads it twice
Concurrent read/write of the same shared_ptr object is a data race. The control block's refcount atomicity protects the pointee, not the pointer object — and the spec states exactly that wrong reason at :194-196 ("whose reference counting is itself atomic").
CallbackToken's own claim (:54-57) is correct: a token holds its own weak_ptr copy. The defect is scope-member vs. scope-member only.
Test coverage
None. tests/test_callback_scope.cpp:561's scope.reset() is std::unique_ptr::reset(), not this method; the two [thread] cases (:500, :538) race requestStop() and destruction, never reset().
Fix shapes
Either std::atomic<std::shared_ptr<detail::CallbackScopeState>> (C++20, available here), or narrow the documented contract to "reset() is not safe against concurrent calls to other members" — in the header and the spec.
Adjacent, minor
_state != nullptr at :225 and :246 are unreachable branches: the only constructor make_shareds it, the class is non-copyable and non-movable (:214-217), and reset() always assigns a freshly-make_shared'd value. In a repo that now runs a mutation gate (morph#408) these are two permanently-surviving mutants and two uncoverable branches.
What would change the verdict
- Close it if
reset() is documented and enforced as owner-thread-only — that is a legitimate resolution, but it needs the doc change, since the current text promises the opposite.
- Raise it with a TSan test racing
reset() against token() on the same scope.
Summary
CallbackScopedocuments every member as safe to call concurrently.reset()writes the_statemember that four other members read, with no synchronisation on the pointer object itself.Verification status
Inferred from reading the code, not reproduced. No test exercises it. Revision:
origin/masteradfe8e5fplus this branch's doc-only commits.The claim
include/morph/core/callback_scope.hpp:194-198:docs/spec/core/callback_scope.md:192makes it normative: "Every member is safe to call from any thread, concurrently with any other."What is actually true
_stateis a plainshared_ptr, not an atomic one:reset()writes it (:240,_state = std::move(fresh);). Concurrently:token()(:251) reads it —CallbackToken{_state}guard()(:266) reads it viatoken()requestStop()(:225-226) reads it twicestopRequested()(:246) reads it twiceConcurrent read/write of the same
shared_ptrobject is a data race. The control block's refcount atomicity protects the pointee, not the pointer object — and the spec states exactly that wrong reason at:194-196("whose reference counting is itself atomic").CallbackToken's own claim (:54-57) is correct: a token holds its ownweak_ptrcopy. The defect is scope-member vs. scope-member only.Test coverage
None.
tests/test_callback_scope.cpp:561'sscope.reset()isstd::unique_ptr::reset(), not this method; the two[thread]cases (:500,:538) racerequestStop()and destruction, neverreset().Fix shapes
Either
std::atomic<std::shared_ptr<detail::CallbackScopeState>>(C++20, available here), or narrow the documented contract to "reset()is not safe against concurrent calls to other members" — in the header and the spec.Adjacent, minor
_state != nullptrat:225and:246are unreachable branches: the only constructormake_shareds it, the class is non-copyable and non-movable (:214-217), andreset()always assigns a freshly-make_shared'd value. In a repo that now runs a mutation gate (morph#408) these are two permanently-surviving mutants and two uncoverable branches.What would change the verdict
reset()is documented and enforced as owner-thread-only — that is a legitimate resolution, but it needs the doc change, since the current text promises the opposite.reset()againsttoken()on the same scope.