Skip to content

bug(driver-docker): deleting a sandbox can destroy a same-named sandbox in another workspace #3234

Description

@letv1nnn

User Story

As an operator running multiple workspaces on a single OpenShell gateway with the Docker compute driver, I want deleting a sandbox to affect only that sandbox, so that tenants sharing a gateway cannot destroy each other's running sandboxes by picking the same sandbox name.

Problem Statement

The Docker driver resolves entries in its in-memory pending map with pending_sandbox_matches (crates/openshell-driver-docker/src/lib.rs:2165):

fn pending_sandbox_matches(sandbox: &DriverSandbox, sandbox_id: &str, sandbox_name: &str) -> bool {
    (!sandbox_id.is_empty() && sandbox.id == sandbox_id)
        || (!sandbox_name.is_empty() && sandbox.name == sandbox_name)
}

A record matches on id or name, and workspace is never consulted even though DriverSandbox carries it. The gateway always sends both fields (crates/openshell-server/src/compute/mod.rs:1628), so the name clause can match a record belonging to a different sandbox.

Sandbox names are unique per workspace, not globally — see sqlite_name_unique_scoped_by_workspace in crates/openshell-server/src/persistence/tests.rs:684, which asserts that ("sandbox", "id-1", "shared-name", "alpha") and ("sandbox", "id-2", "shared-name", "beta") coexist as separate records. The Docker driver has a single flat config.sandbox_namespace, so every workspace's sandboxes share one pending map.

Given sandbox A (sbx-A, name demo, workspace alpha) whose provisioning already completed and whose container was later removed out-of-band, and sandbox B (sbx-B, name demo, workspace beta) currently provisioning, delete_sandbox_inner("sbx-A", "demo") (:1065) does the following:

  1. :1070 remove_pending_sandbox pops B's record — A is no longer in the map, and B matches on the name clause.
  2. :1074 task.abort() kills B's in-flight provisioning task. This runs before any branching, so it happens on every path.
  3. :1077 find_managed_container_summary filters on A's id label and returns None, entering the "container gone, pending record present" branch.
  4. :1082 container_name_for_sandbox(&record.sandbox) resolves to B's container name.
  5. :1084 remove_container(force: true) force-removes B's running container.
  6. :1092 cleanup_sandbox_token_file(&record.sandbox) deletes B's token file.
  7. :1093 returns Ok(true), so the gateway reports A as deleted and broadcasts a Deleted event for A.

No error is surfaced anywhere. Two other call sites share the same lookup: stop_sandbox_inner:1135 (aborts and cleans up the wrong sandbox on stop) and pending_snapshot:1282, which backs GetSandbox and can return another sandbox's snapshot.

reserve_pending_sandbox:1250 applies the same id == || name == test, so two same-named sandboxes cannot be pending simultaneously. That is not required here: only B is pending, and A finished provisioning long before.

Container naming already handles this correctly — container_name_for_sandbox:3638 builds openshell-{workspace}--{name}-{id} specifically so names cannot collide. The pending lookup never received the same treatment.

Docker-driver only. The Podman driver has no equivalent pending map or matcher.

Impact / Why This Matters

Deleting or stopping a sandbox in one workspace silently destroys an unrelated, actively provisioning sandbox in another workspace: its provisioning task is aborted, its container is force-removed, and its gateway JWT is deleted. The owning tenant sees a sandbox vanish with no error and no log tying it to the other workspace's request. The delete that caused it reports success.

This is a cross-tenant blast radius on a shared gateway, and it requires no race — only two sandboxes with the same name in different workspaces, which the persistence layer explicitly permits.

The only workaround is enforcing globally unique sandbox names across all workspaces out-of-band. That defeats the purpose of workspace-scoped naming and cannot be enforced through any OpenShell-supplied mechanism, since the gateway itself generates names for unnamed sandboxes.

Acceptance Criteria

  • pending_sandbox_matches identifies a record unambiguously: match on sandbox_id alone when it is non-empty, and fall back to name only when no id is supplied, scoped by workspace.
  • Deleting a sandbox does not abort, remove the container of, or delete the token file of a same-named sandbox in another workspace.
  • Stopping a sandbox (stop_sandbox_inner) has the same isolation.
  • GetSandbox for a sandbox does not return a same-named pending sandbox from another workspace.
  • Regression test covering a delete against a completed sandbox while a same-named sandbox in a different workspace is pending.

Reproduction Steps

  1. Run a gateway with the Docker compute driver and two workspaces, alpha and beta.
  2. Create sandbox demo in workspace alpha. Wait for provisioning to complete, so the driver clears its pending record.
  3. Remove that container out-of-band: docker rm -f openshell-alpha--demo-<sbx-A-id>.
  4. Create sandbox demo in workspace beta. While it is still provisioning, proceed immediately to the next step.
  5. Delete sandbox demo in workspace alpha.
  6. Observe that the workspace beta sandbox is gone: its provisioning task was aborted, its container was force-removed, and its token file under $XDG_STATE_HOME/openshell/docker-sandbox-tokens/<namespace>/<sbx-B-id>/sandbox.jwt was deleted. The delete of the alpha sandbox reported success.

Environment

  • OpenShell: main at 118b250
  • OS: any
  • Runtime, deployment, or integration: Docker compute driver, single gateway serving more than one workspace. Not version-specific; pending_sandbox_matches has had this shape since the map was introduced.

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

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions