Skip to content

Don't let one namespace delete another's server records - #232

Open
subwire wants to merge 2 commits into
mainfrom
registry-namespace-safety
Open

Don't let one namespace delete another's server records#232
subwire wants to merge 2 commits into
mainfrom
registry-namespace-safety

Conversation

@subwire

@subwire subwire commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

The bug

A registry directory can be shared by processes that cannot see each other's servers. The easy way to get there is several containers bind-mounting the same home directory — which is how agent harnesses tend to run. The registry is shared; sockets live in each container's own /tmp, and PIDs are per-namespace.

So every liveness test fails on a sibling's record — socket missing, pid absent — and pruning deletes the record of a server that is alive and well.

The owner then cannot find its own server: decompiler list returns nothing while the process is running and serving, and load blocks until --timeout expires waiting for a server that started successfully seconds earlier.

Traced

Instrumenting every registry mutation across two concurrent containers:

container A: REGISTER 6fe01da235 (pid 1301)
container B: PRUNE    6fe01da235  socket_exists=False pid_alive=False
container B: REGISTER 89b45700ed (pid 1358)
container A: PRUNE    89b45700ed  socket_exists=False pid_alive=False

They annihilate each other on every load.

Cost

On a 40-binary benchmark, 22 of 40 solves hit a phantom Timed out waiting … for server to start — 50 occurrences in a single run — each burning the full timeout (up to 300s) while the server sat healthy. Reproduced directly with two containers sharing one home:

result
shared registry registered 0 server(s)0 visible, 1 process alive
isolated registry each container: 1 visible, 1 process alive

Change

Records carry the namespace that created them (hostname + pid/mnt namespace links), which makes "not mine" distinguishable from "dead". Foreign records are skipped entirely — never returned as ours, never deleted — so a container is the only writer of its own entries.

Records without the field are treated as local, so existing registries prune exactly as before.

This is defense in depth, not a licence to share: deployments should still give each container its own registry (we set XDG_STATE_HOME per container). It makes the shared case degrade into "I can't see it" instead of "I deleted it".

Tests

Four: foreign records are never returned nor deleted; our own dead records are still pruned; new records carry a namespace; legacy records without one still prune.

🤖 Generated with Claude Code

subwire and others added 2 commits July 29, 2026 05:05
A registry directory can be shared by processes that cannot see each
other's servers. The easy way to arrive there is several containers
bind-mounting the same home directory, which is how agent harnesses tend
to run: the registry is shared, but sockets live in each container's own
/tmp and PIDs are per-namespace.

Every liveness test then fails on a sibling's record -- socket missing,
pid absent -- so pruning deletes the record of a server that is alive and
well. The owning container subsequently cannot find its own server:
`decompiler list` returns nothing while the process is running and
serving, and `load` blocks until its --timeout expires waiting for a
server that started successfully seconds earlier.

Traced across two concurrent containers:

    container A: REGISTER 6fe01da235 (pid 1301)
    container B: PRUNE    6fe01da235  socket_exists=False pid_alive=False
    container B: REGISTER 89b45700ed (pid 1358)
    container A: PRUNE    89b45700ed  socket_exists=False pid_alive=False

On a 40-binary benchmark this cost 22 of 40 solves a phantom
"timed out waiting for server to start", 50 occurrences in one run.

Records now carry the namespace that created them, which makes "not
mine" distinguishable from "dead". Foreign records are skipped entirely:
never returned as ours, never deleted. Records without the field are
treated as local, so existing registries keep working exactly as before.

Deployments should still give each container its own registry; this makes
the shared case degrade into "cannot see it" rather than "deleted it".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both existing checks are proxies for the question actually being asked.
A socket file survives an unclean death, so its presence proves only that
a server once bound there. A PID can be recycled by an unrelated process.
Either can report "alive" about a server that is gone -- and, across a
shared registry, "dead" about one that is fine.

ida-pro-mcp settles the same question by connecting to the endpoint
(`probe_instance`), for the same reasons; its comment cites PID reuse and
"process alive but the server crashed". Do likewise: after the two cheap
checks pass, connect to the socket.

Verified against a real backend: a live server is still reported and
usable; after SIGKILL the socket file remains on disk and the server is
correctly reported gone, where the file check alone would have said it
was alive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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