Skip to content

createSession adopts a running container, so a failed start can kill another process's live session #18

Description

@amondnet

Found by cubic on the third review pass over the same catch in #13. It is not a defect #13 introduced — git diff origin/main...feat/define-agent-api is empty for both files involved. It has been on main since e245fc1 (#7), and defineAgent only made it reachable from a new entry point.

Reproduction

  1. Process A holds a live session on sessionId = "x". Its container is running.
  2. Process B calls createSession({ sessionId: "x" }). B onCreate runs an exec, which acquires the sandbox.
  3. docker/container.ts acquire() finds a container with that name and adopts it rather than failing — adopt() returns immediately for status === "running".
  4. B onCreate rejects (a corepack enable that exits non-zero, a proxy that is unreachable).
  5. B catch calls destroy(). A live session dies.

A second, quieter consequence of the same step 3: when B onCreate succeeds, A and B are now running two sessions inside one container, sharing a filesystem, with neither aware of the other.

Why no reap policy fixes it

Three rounds on #13 produced three positions, each correct against the one before it:

Policy Fixes Breaks
unconditional reap never leaks destroys a container this call adopted
reap only a generated id keeps the adopted container leaks a caller-named container that did not exist
unconditional reap (shipped) never leaks can destroy another process live session

By the time the catch runs the damage is upstream. packages/core/src/agent/define.ts is backend-agnostic by design and cannot see whether the acquisition adopted or created.

Where the fix belongs

HarnessV1SandboxProvider separates the two operations — createSession is the fresh path, and resumeSession "reattach[es] to an existing sandbox previously created with the same sessionId". packages/core/src/sandbox/harness/provider.ts routes both to the same session(id) and says so:

The contract cannot report whether session(id) found a sandbox or minted one, so createSession is taken as the fresh path and resumeSession as the returning one.

The primitive a real split needs already exists: ContainerHandle.peek() — "the container name if it is already running, without creating or starting anything."

Why this is a decision, not just a patch

The same file documents adoption as the feature:

Adoption is what makes a sandbox id resumable across processes: a second process asking for the same id finds the running container instead of colliding with it.

So removing adoption from the create path removes intended behaviour. The decision is where adoption moves to and what createSession does with a name that is taken — refuse, or create under a different one:

  • Should createSession peek() first and throw on a live name?
  • Does resumeSession become the only adopting path, and does our SandboxProvider need to distinguish them at all, or only the harness layer?
  • What happens to a stopped container of the same name — adopt (today) or recreate?

Independent of #16: this holds even if defineAgent never supports resume, because createSession adopting a live container is wrong on its own.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions