concurrency: fix placement new over the live value_ member - #444
concurrency: fix placement new over the live value_ member#444kutsibalci wants to merge 2 commits into
Conversation
|
The created documentation from the pull request is available at: docu-html |
InterruptibleState reuses the storage of the live value_ member with placement new without ending the existing object lifetime, and discards the result of the placement new. Destroy the existing object first, and mark the result as deliberately discarded. Construction is kept instead of switching to assignment, because score::Result<Value> is not assignable for every Value this class supports - the comment above each call already records that as the reason placement new is used there. Covers all six occurrences: three in InterruptibleState<Value>, two in the InterruptibleState<Value&> specialization, and one in InterruptibleState<void>. Fixes eclipse-score#301 Signed-off-by: Hüseyin Kutsi Balcı <balcihkutsi@gmail.com>
d491374 to
961c13b
Compare
|
Hi @kutsibalci, thanks for contribution! @sankurm, can you please review this PR? |
There was a problem hiding this comment.
Pull request overview
Fixes placement-new handling in InterruptibleState by explicitly destroying existing results before reconstructing them.
Changes:
- Adds
std::destroy_atat all six reconstruction sites. - Explicitly discards placement-new return values.
- Adds required
<memory>includes.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
score/concurrency/future/interruptible_state.h |
Updates value and reference specializations. |
score/concurrency/future/interruptible_state.cpp |
Updates the void specialization. |
Suppressed comments (4)
score/concurrency/future/interruptible_state.h:107
- This explanation is inaccurate: placement construction itself ends the previous object's lifetime by reusing its storage, even when the type is non-trivially destructible. The explicit destruction is still needed to run the old object's destructor; describe that consequence instead of claiming the former code was automatically undefined behavior.
// The existing object is destroyed first: reusing the storage of a live object without ending its
// lifetime is undefined behaviour as soon as score::Result is not trivially destructible.
score/concurrency/future/interruptible_state.h:126
- This explanation is inaccurate: placement construction itself ends the previous object's lifetime by reusing its storage, even when the type is non-trivially destructible. The explicit destruction is still needed to run the old object's destructor; describe that consequence instead of claiming the former code was automatically undefined behavior.
// The existing object is destroyed first: reusing the storage of a live object without ending its
// lifetime is undefined behaviour as soon as score::Result is not trivially destructible.
score/concurrency/future/interruptible_state.h:217
- This explanation is inaccurate: placement construction itself ends the previous object's lifetime by reusing its storage, even when the type is non-trivially destructible. The explicit destruction is still needed to run the old object's destructor; describe that consequence instead of claiming the former code was automatically undefined behavior.
// The existing object is destroyed first: reusing the storage of a live object without ending its
// lifetime is undefined behaviour as soon as score::Result is not trivially destructible.
score/concurrency/future/interruptible_state.h:236
- This explanation is inaccurate: placement construction itself ends the previous object's lifetime by reusing its storage, even when the type is non-trivially destructible. The explicit destruction is still needed to run the old object's destructor; describe that consequence instead of claiming the former code was automatically undefined behavior.
// The existing object is destroyed first: reusing the storage of a live object without ending its
// lifetime is undefined behaviour as soon as score::Result is not trivially destructible.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // The existing object is destroyed first: reusing the storage of a live object without ending its | ||
| // lifetime is undefined behaviour as soon as score::Result is not trivially destructible. |
There was a problem hiding this comment.
That is a fair correction and I have taken it. Placement new does end the previous object's lifetime, by reusing its storage ([basic.life]/1.5) — what it does not do is run that object's destructor, and [basic.life]/4 makes the omission undefined only for a program that depends on the destructor's side effects, not unconditionally. The old comment got both halves wrong.
Reworded at all six sites in d53f1d3, and the pull request description corrected in the same places:
// Placement-new ends the old object's lifetime by reusing its storage, but never runs its
// destructor; destroy explicitly so a non-trivially destructible score::Result releases what it holds.
std::destroy_at(&value_);The generated code is unchanged; only the rationale moved. The fix itself still stands for the reason the description already gave — at these six call sites value_ is live, score::result::Error happens to be trivially destructible today so nothing leaks yet, and the std::destroy_at is what keeps that from becoming a real leak if it ever stops being true.
AI disclosure: AI-assisted (Claude Code). I checked the cited standard wording before accepting the correction and take responsibility for it.
| // The existing object is destroyed first: reusing the storage of a live object without ending its | ||
| // lifetime is undefined behaviour as soon as score::Result is not trivially destructible. |
Placement new ends the previous object's lifetime by reusing its storage ([basic.life]/1.5); what it does not do is run that object's destructor. The earlier comment claimed the opposite and called the former code unconditionally undefined, which overstates it: per [basic.life]/4 the destructor is simply not called, and only a program that depends on its side effects has undefined behaviour. Comment only; the generated code is unchanged.
d53f1d3 to
b5adea5
Compare
Fixes #301.
What
InterruptibleStateoverwrites its livevalue_member with placementnew. Placementnewends the previous object's lifetime by reusing its storage ([basic.life]/1.5), but it never runs that object's destructor, so anything a non-trivially destructiblescore::Resultholds is silently never released. The result of the placementnewis also discarded. This fixes both, at all six occurrences.Why this shape of fix
The issue proposes
value_ = score::Result<Value>{...}as preferred. That would change which types the class supports, so it is not used here. The comment above each call already records the reason:// Use the constructor instead of assignment operator to circumvent issue with types that are not assignableVerified against
main:Both hold, so
InterruptibleState<NonAssignable>compiles today and would stop compiling under the assignment form. Keeping construction and destroying the previous object explicitly fixes the defect without narrowing support.Both idioms used here already exist in this repository:
std::destroy_atinscore/language/futurecpp/include/score/circular_buffer.hpp:102, andstatic_cast<void>(::new ...)for the deliberately discarded result inscore/language/futurecpp/include/score/expected.hpp.Scope: six occurrences, not three
The issue lists three and invites checking for others. A sweep of
score/found three more:interruptible_state.h:83, 103, 119InterruptibleState<Value>(listed in the issue)interruptible_state.h:207, 223InterruptibleState<Value&>interruptible_state.cpp:36InterruptibleState<void>All six overwrite a live
value_member (declared at:176,:277,:308).The other 49 placement-new uses in
score/are on raw storage with matching destruction and are not touched:variant.hpp,expected.hpp,inplace_vector.hpp,circular_buffer.hpp,memory_resource.cpp,move_only_function.hpp,chunk_list.hpp,free_list.hpp,construct_at.hpp, andscore/os/static_destruction_guard.h:71(refcount-guarded with a matching destructor).Severity
Stated plainly so this is prioritised correctly:
TestAndMarkValueAsSet()means these run once, and at that pointvalue_always holds the error alternative rather than aValue.score::result::Erroris currently trivially destructible, so there is no leak today — as the issue itself notes. This is a latent defect rather than a live leak: the code silently depends onscore::result::Errorstaying trivially destructible, and a destructor that has to run is skipped the moment that stops being true. Note that skipping it is not automatically undefined behaviour — [basic.life]/4 makes it undefined only for a program that depends on the destructor's side effects.Verification
I could not run the full Bazel suite locally: the
apeandrules_diffdependencies are hosted ongitlab.arm.com, which does not resolve from my machine, sobazel test --config=bl-x86_64-linux //score/concurrency/...aborts during fetch. I am relying on CI for the real build and flagging that rather than implying I ran it.What I did verify locally, with
g++ -std=c++17 -fsyntax-onlyagainst the repository include paths:interruptible_state.cppcompiles clean, no warnings.InterruptibleState<std::string>(non-trivially-destructibleValue),InterruptibleState<NonAssignable>(non-assignableValue) andInterruptibleState<int&>(thereference_wrapperspecialization), so every changed line is instantiated.static_asserts above.AI disclosure
AI-assisted (Claude Code). The tool was used to sweep the repository for the pattern, apply the change across the six sites, and draft this description. The results were checked before opening: the three additional sites were confirmed by reading the declarations of
value_in each specialization, the 49 other placement-new uses were reviewed and excluded individually, and the compilation checks above were run. I have reviewed and understood the change and take responsibility for it.