Skip to content

Navigation Menu

Sign in
Sign up

ProjectRegistry.removeConnection is reentrant, and the connection registry is keyed by descriptor #292

Open

Description

Found during the independent review of #291. Pre-existing — not introduced by that PR — but worth fixing on its own, because the daemon's outbound channels are now keyed by descriptor too, which makes a wrong-descriptor write more consequential than it was.

The interleaving

ProjectRegistry.removeConnection iterates a copy of connectionProjectPaths[id] with an await per store. While it is suspended:

  1. Another connection's .openProject of a new path runs joinSidebars.
  2. That inserts the departing sidebar into the new store — connectionProjectPaths[X].insert(B) plus storeB.addConnection(X, fdX).
  3. The removal loop never sees B, because it is walking a snapshot taken before.
  4. close(fdX) runs anyway.

storeB is then left holding X → fdX indefinitely. Every later broadcast for project B goes to whichever connection has since been handed that descriptor number — including a one-shot CLI, which would print the first graphChanged it sees as though it were its own project's.

It needs the app disconnecting at the same instant another client opens a new folder, so it is rare. No deterministic failing test yet — this is reasoning from ProjectRegistry.swift:128-137 and :390-397.

Suggested fix

Two parts, from the review:

  • Key the connection registry by connection UUID rather than by file descriptor. Every caller already has the UUID, and it is the identity that is actually stable — a descriptor number is reused the moment it closes. This applies to OutboundChannels as well, whose global map is descriptor-keyed today.
  • Re-read the path set after each await in removeConnection, rather than iterating a snapshot taken before suspending.

Why not in #291

#291 is already large and touches the same files; folding a registry-wide re-keying into it would make it unreviewable. Its own descriptor-recycling hazards are closed (open refuses to inherit a dead channel, send refuses an unregistered descriptor, close refuses a descriptor it has no channel for), so this is the remaining structural half rather than an open hole.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BP43ags4cn8fq2ZZdv85J9

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      AltStyle によって変換されたページ (->オリジナル) /