Skip to content

Detect superseded shmem segments via per-creation segment_uid - #193

Merged
cboulay merged 2 commits into
masterfrom
fix/cerelink-shmem-segment-uid-supersession
Aug 3, 2026
Merged

Detect superseded shmem segments via per-creation segment_uid#193
cboulay merged 2 commits into
masterfrom
fix/cerelink-shmem-segment-uid-supersession

Conversation

@cboulay

@cboulay cboulay commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Problem

ShmemSession::isOwnerAlive() rejected stale NATIVE CLIENT segments using only owner_pid. That check is blind when a persistent service owns the shared memory.

Concretely, Orion (orion/src/device/CereLink.cpp) is one long-lived process. Its disconnect()m_session.reset() cleanly destroys the STANDALONE ShmemSession (running shm_unlink), and each connect() recreates the device's segments under the same name. Across every start/stop cycle owner_pid stays equal to Orion's PID, so isOwnerAlive() always reports "alive" — even for a segment that belongs to a session that was closed and superseded. A CLIENT holding the old mapping has no way to tell it is stale.

Fix

Add a per-creation segment_uid to NativeConfigBuffer, stamped by the STANDALONE owner at creation. isOwnerAlive() now applies two checks:

Failure mode detected by
Persistent owner recreated the segment under the same name segment_uid mismatch (new)
Owner crashed without unlinking owner_pid dead (existing)

Both are needed — the PID catches the crash-orphan case (uid unchanged), the uid catches supersession (pid unchanged). A zero segment_uid or owner_pid skips that check for backward compatibility with pre-liveness segments.

readCurrentSegmentUid() re-opens the segment name and reads the uid of whatever it resolves to now, comparing against the uid in our mapped buffer.

Why a token, not an inode

The natural identity would be the segment inode, but macOS returns st_ino == 0 for POSIX shm fds (verified), so inode is unusable there. segment_uid is a portable token: steady_clock stamp ^ pid ^ process-local atomic counter (never 0), stored in the buffer and read back by mapping the re-opened segment on both platforms.

Out of scope

isOwnerAlive() is a one-shot check at attach (sdk_session.cpp ~814). A client that attaches to the current segment and is superseded while streaming won't notice until something re-checks. That periodic liveness poll belongs in the client (pycbsdk/sdk_session) and is intentionally deferred — tracked in a separate issue.

Tests

New OwnerLivenessTest cases: StandaloneWritesSegmentUid, ClientDetectsUnlinkedSegment, ClientDetectsSupersededSegment. cbshm_tests: 104/104 pass; full project builds clean.

🤖 Generated with Claude Code

cboulay and others added 2 commits August 3, 2026 15:09
isOwnerAlive() relied solely on owner_pid to reject stale NATIVE CLIENT
segments. That fails when a persistent service owns the shared memory:
a long-running process (e.g. Orion) keeps the same PID as it tears down
and recreates a device's segments under the same name across sessions, so
owner_pid always reads "alive" even for a segment that belongs to a closed
session. A CLIENT holding the old mapping cannot tell it has been superseded.

Add a per-creation segment_uid to NativeConfigBuffer, stamped by the
STANDALONE owner at creation. isOwnerAlive() now applies two checks:
  - Supersession: compare the uid in our mapped buffer against the uid of
    whatever the segment name resolves to now (readCurrentSegmentUid()
    re-opens + maps the current segment). Mismatch or missing name => stale.
  - Crash-orphan: probe owner_pid, catching an owner that died without
    unlinking (uid unchanged in that case).

Both are needed: pid catches crash-orphan (uid unchanged), uid catches
supersession (pid unchanged). A zero uid or pid skips that check for
backward compatibility with pre-liveness segments.

segment_uid is a portable token (steady_clock ^ pid ^ process-local
counter), not an inode: macOS returns st_ino == 0 for POSIX shm fds, so
the inode is unusable as a segment identity there.

Note: isOwnerAlive() is a one-shot check at attach. A client superseded
mid-stream won't notice until something re-checks; periodic polling belongs
in the client (pycbsdk/sdk_session) and is intentionally out of scope here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The initial supersession check read the segment uid live from the mapped
buffer. That breaks on Windows: a named mapping persists while any handle
is open, and CreateFileMapping on an existing name returns the SAME object,
so a recreating owner reuses the same memory and overwrites segment_uid in
place. A live-vs-current comparison then always matches itself, and the
CI OwnerLivenessTest.ClientDetectsUnlinkedSegment /
ClientDetectsSupersededSegment cases failed on windows-x64.

Capture the uid once at attach (Impl::attached_segment_uid) and compare the
snapshot against the current named segment's uid. This detects supersession
on both platforms:
  - POSIX: the name resolves to a new inode/object (uid N2 != snapshot N1).
  - Windows: the reused object's uid is overwritten to N2 != snapshot N1.

The pure "owner closed but never recreated, client still holds the mapping"
case is only detectable on POSIX (shm_unlink removes the name immediately);
on Windows the name and memory stay intact, which is correct behavior, so
ClientDetectsUnlinkedSegment is now guarded #ifndef _WIN32.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cboulay
cboulay force-pushed the fix/cerelink-shmem-segment-uid-supersession branch from 06c55ce to 78cfffd Compare August 3, 2026 19:12
@cboulay
cboulay merged commit df10781 into master Aug 3, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant