@@ -170,7 +170,9 @@ static bool snapshot_slot(RegistryEntrySlot * slot, RegistryEntrySlot * out)
170170 continue ;
171171 }
172172 uint8_t st = slot->state .load (std::memory_order_acquire);
173- if (st == ENTRY_EMPTY ) {
173+ if (st == ENTRY_EMPTY || st == ENTRY_RESERVED ) {
174+ // RESERVED = a writer claimed the slot but has not yet published its
175+ // payload. Treat it as empty so we never snapshot a half-built slot.
174176 return false ;
175177 }
176178
@@ -230,12 +232,17 @@ static int32_t try_add_once(RegistryHeader * header, const RegistryEntry & entry
230232
231233 for (uint32_t i = 0 ; i < max; ++i) {
232234 uint8_t expected = ENTRY_EMPTY ;
233- // CAS state EMPTY -> entry.type. Winner owns the slot.
235+ // CAS state EMPTY -> RESERVED. Winner owns the slot, but readers still see
236+ // it as empty until we publish the real type below — so no reader can
237+ // snapshot the slot before its payload is committed under the seqlock.
234238 if (slots[i].state .compare_exchange_strong (
235- expected, static_cast <uint8_t >(entry. type ),
239+ expected, static_cast <uint8_t >(ENTRY_RESERVED ),
236240 std::memory_order_acq_rel, std::memory_order_relaxed))
237241 {
238242 write_slot_payload (&slots[i], entry);
243+ // Payload is committed (seq even). Publish the real type with release
244+ // ordering so any reader that now sees it also sees the payload.
245+ slots[i].state .store (static_cast <uint8_t >(entry.type ), std::memory_order_release);
239246 header->generation .fetch_add (1 , std::memory_order_acq_rel);
240247 return static_cast <int32_t >(i);
241248 }
@@ -347,6 +354,11 @@ void registry_cleanup_stale(RegistryHeader * header)
347354 if (!snapshot_slot (&slots[i], &snap)) {
348355 continue ;
349356 }
357+ if (snap.pid == 0 ) {
358+ // pid==0 is never a real entry — only a transient snapshot of a slot
359+ // whose payload is not yet written. Never reclaim it.
360+ continue ;
361+ }
350362 if (pid_is_alive_or_unreachable (snap.pid )) {
351363 continue ;
352364 }
0 commit comments