Skip to content

SharedPtr potential race condition fix - #466

Merged
vporoshok merged 15 commits into
ppfrom
shared_ptr_race_condition_fix
Aug 12, 2026
Merged

SharedPtr potential race condition fix#466
vporoshok merged 15 commits into
ppfrom
shared_ptr_race_condition_fix

Conversation

@cherep58

@cherep58 cherep58 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Motivation

SharedVector's size lives in the shared-pointer control block (items_count) and can be read from another thread while the owning thread mutates the vector. Two problems made this unsafe:

  1. In GenericVector, the size was published before elements were constructed (on growth) and after elements were destroyed / memory was moved (on shrink), so a concurrent reader could observe a size that covered not-yet-constructed or already-destroyed elements.
  2. The item counter itself was a plain uint32_t, i.e. a data race under concurrent read/write.

Changes

  1. Correct set_size ordering in GenericVector (bare_bones/vector.h) Reordered size publication relative to element lifetime so the published size is always consistent with valid memory:
    • Growth (resize, push_back, insert, emplace_back, ranged push_back): construct/fill the new elements first, then publish the larger size.
    • Shrink / erase / clear: publish the smaller size first, then destroy elements / memmove the tail.
    • Split resize into grow_storage() / decrease_storage() helpers to make the two paths explicit and remove the previous nested branching.
    • resize(new_size, value) now uses std::uninitialized_fill on the grow path instead of resize() + std::fill.
  2. Bug fixes spotted along the way (bare_bones/vector.h)
    • Ranged push_back: std::memcpy copied size bytes instead of size * sizeof(T) — fixed.
    • SharedSpan::operator=(&&): self-assignment check compared this != other (pointer vs object); fixed to this != &other.
  3. Atomic item-count control block (bare_bones/memory.h)
    • Introduced GenericSharedPtrControlBlockWithItemCount.
    • SharedPtrControlBlockWithItemCount = the existing uint32_t variant (unchanged behavior).
    • Added AtomicSharedPtrControlBlockWithItemCount = std::atomic<uint32_t> variant (default seq_cst load/store, giving the acquire/release publication needed by readers).
    • SharedMemory / SharedVector / SharedSpan are now parameterized by the control-block type.
  4. Wiring
    • Entrypoint LSS/QEB types (entrypoint/types/lss.h) now use AtomicSharedPtrControlBlockWithItemCount.
    • reverse_index.h explicitly keeps the non-atomic control block (single-threaded usage).

@cherep58 cherep58 added this to the next milestone Aug 6, 2026
@cherep58
cherep58 requested a review from gshigin August 6, 2026 12:39
@cherep58 cherep58 self-assigned this Aug 6, 2026
@cherep58
cherep58 requested a review from vporoshok as a code owner August 6, 2026 12:39
@cherep58 cherep58 added bug Something isn't working go-test-asan Run tests on pp/go with ASan with Double-GC labels Aug 6, 2026
@vporoshok vporoshok modified the milestones: next, v0.8.7, v0.8.8 Aug 6, 2026

@vporoshok vporoshok left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks — the ordering rework is the right shape, and it quietly fixes more than the title suggests. A few things before this goes in.

Please expand the PR description. It is empty right now, and this is not only a race-condition fix: it also corrects a memcpy byte count that silently under-copied data, repairs an ill-formed self-assignment guard in SharedSpan, and removes a double construction in push_back. Those deserve to be written down — the commit outlives our memory of this review, and the title ("SharedPtr potential race condition fix") does not even mention GenericVector, where nearly the whole change lives, while SharedPtr itself is untouched.

The reordering alone does not make this race-free. See the inline note on resize: the item count is a plain non-atomic field, so concurrent readers stay in UB territory and the compiler is free to move the size store across the element stores. The publication needs an atomic store/load pair to mean anything.

No tests. Two of the three bugs fixed here are trivially testable, and one of them — the memcpy length — would have been caught long ago by a single test with sizeof(T) > 1. Please add coverage; details inline.

Comment thread pp/bare_bones/vector.h
Comment thread pp/bare_bones/vector.h Outdated
Comment thread pp/bare_bones/vector.h
Comment thread pp/bare_bones/vector.h

@vporoshok vporoshok left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the whole diff. The core of the change looks right to me:

  • the set_size reorderings in GenericVector are semantically equivalent to the previous branching for every trait combination I checked (IsZeroInitializable / IsTriviallyDestructible / trivial);
  • dropping reserve() on the shrink path is harmless — grow_to_fit_at_least is a no-op when shrinking;
  • the size * sizeof(T) memcpy fix and the this != &other self-assignment fix are genuine bugs fixed;
  • all instantiation sites of the new ControlBlockType template parameter were updated consistently;
  • the atomic control block keeps the same size and layout (8 bytes), so no serialized format changes.

Two inline comments below. Plus one finding in a file outside this diff:

pp/primitives/snug_composites_filaments.hSymbol::emplace_back publishes before writing the data. It appends the items_ entry (pos/length) first and only then appends the bytes to data_. A reader that observes the newly published item count can therefore read bytes that have not been written yet, or read past the end of data_. The vector-level ordering fix in this PR is not observable through this layer until the order here is swapped: bytes into data_ first, then publish the items_ entry.

Two things I initially flagged and am explicitly not asking to change here:

  • pp/series_index/reverse_index.h still uses the non-atomic counter — agreed this belongs in a separate PR; the snapshot copies the encoder as a whole and only reads the element count captured at snapshot time, and since data is append-only that does not misbehave.
  • GenericVector::erase() publishes the smaller size and then memmoves the tail into the still-published range, so the reorder does not make erase safe for concurrent readers — but erase is not used with shared memory and cannot be made safe anyway, so this is an understood precondition.

Comment thread pp/bare_bones/vector.h Outdated
Comment thread pp/bare_bones/memory.h Outdated

@vporoshok vporoshok left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All three findings from my earlier review are addressed:

  • push_back now uses std::uninitialized_copy, with a Vector<Vector<uint32_t>> test that exercises exactly the non-trivially-copyable fallback branch.
  • The item counter uses explicit load(acquire) / store(release) instead of the seq_cst defaults.
  • Symbol::emplace_back captures data_size up front, appends the bytes, and only then publishes the items_ entry.

The sentinel refactor on top of that is a good addition: end() becomes a static sentinel, operator==(IteratorSentinelType) no longer dereferences storage_ptr_ (which also removes an uninitialized-pointer read for the default-constructed iterators in LabelSet::get_values_range), and sentinel_id_ trims the tail that can appear because a read-only span reads the live items_count from the shared control block while the writer keeps appending.

I raised four more points offline; all four were answered and none of them stand:

  • the sentinel_id_ mechanism needs no comment or caching — a single id-based iterator is exposed and begin() is called once; a pos + length overflow past uint32_t means the data is already lost either way;
  • operator[] is unvalidated by design and ids cannot be obtained unless they were handed out; size() is an upper bound used for reservation, which is fine;
  • snapshots are taken under a lock, so there is no window in the read-only ctor;
  • the non-atomic refcount fast paths rest on the existing single-owner + Go-level-lock convention, untouched by this PR.

CI is green on both architectures, including go-test-pp under ASan. LGTM.

@vporoshok
vporoshok merged commit b6c1095 into pp Aug 12, 2026
27 checks passed
@vporoshok
vporoshok deleted the shared_ptr_race_condition_fix branch August 12, 2026 18:28
u-veles-a pushed a commit that referenced this pull request Aug 13, 2026
* changed order of set_size in vector append operations

* changed order of set_size in vector erase operations

* review fixes

* created AtomicSharedPtrControlBlockWithItemCount and used in entrypoint QEB

* review fixes

* fixed Vector::push_back bug for NonTriviallyCopyable objects

* changed memory model for AtomicSharedPtrControlBlockWithItemCount to acquire/release

* added removed static_assert

* changed order of adding to items_ and data_ containers

* added symbol validation in symbol iterator

* refactoring

* used BareBones::iterator::kSentinel instead concrete symbol iterator
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working go-test-asan Run tests on pp/go with ASan with Double-GC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants