Skip to content

fix: make repeated RemDial calls in one process safe - #14

Merged
h3zh1 merged 1 commit into
chainreactors:masterfrom
dust-life:remdial-repeated-safety
Oct 21, 2026
Merged

h3zh1 merged 1 commit into
chainreactors:masterfrom
dust-life:remdial-repeated-safety

Conversation

@dust-life

Copy link
Copy Markdown
Contributor

Bug: Calling RemDial (FFI) or otherwise dialing a second agent console in the same process can kill or permanently wedge the caller.

  1. On releases before the console/agent lifecycle fixes, a second dial whose agent ID is still present in the global agent.Agents registry (e.g. a re-dial reusing the same alias) makes agent.NewAgent return (nil, err). runner.(*Console).newAgent ignored the error and immediately assigned a.Recover on the nil agent — a nil-pointer panic that takes down the entire host process. (Already mitigated on master by honoring the error; this PR adds regression coverage pinning that contract.)
  2. Two callers — the FFI RemDial export and the REM:// proxy client — wait for the dialed agent to finish synchronous init with for { if a.Init { break } }. A HandlerInit failure (for example, the local listener endpoint requested by the second dial is already bound by the first agent in the same process) closes the agent without ever setting Init, so the wait loops forever and the calling thread/task is never answered.
  3. The handler-exit path removes the agent from the global registry with an unconditional Agents.Map.Delete(a.ID). If a same-alias successor registered after Close ran, this evicts the live successor's registry entry.

Fix:

  • Add agent.(*Agent).WaitInit(timeout): blocks until Init, agent close, or timeout, and reports whether Init was reached. Both wait sites now fail the dial (with a bounded 30s backstop) instead of waiting forever, closing the agent on timeout.
  • Replace the unconditional registry Delete with CompareAndDelete so only the exiting agent's own entry is removed.

Tests:

  • runner/redial_test.go: a second dial while the first agent's alias is still registered is refused with a controlled error (no panic; the incumbent keeps its registry entry and stays open) — this test panics with the original nil dereference when run against the pre-fix newAgent; and a re-dial with the same alias after the first agent closes re-establishes the channel end-to-end against a live server console.
  • agent/waitinit_test.go: WaitInit returns on init, on close-before-init (the former hang), and on timeout.

A second RemDial in the same process must never wedge the caller or
corrupt the first channel:

- the FFI RemDial and the REM proxy client waited on agent Init with no
  exit condition; a HandlerInit failure (e.g. the local endpoint is
  already bound by another agent in the process) closes the agent
  without ever setting Init, hanging the caller forever. Both now wait
  via agent.WaitInit, which also fails on agent close and on timeout.
- the handler-exit path deleted the agent's registry entry
  unconditionally, which could evict a same-alias successor registered
  after Close ran; it now uses CompareAndDelete.

Add runner-level regression tests for the second-dial contract: a dial
whose alias is still registered is refused with a controlled error
instead of panicking in newAgent, and a re-dial after the first agent
closes re-establishes the channel cleanly. Add WaitInit unit tests.
@h3zh1
h3zh1 merged commit aac171f into chainreactors:master Oct 21, 2026
26 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.

2 participants