Repository navigation
bug(driver-vm): lifecycle requests fall back to sandbox_name when the supplied sandbox_id is absent #3254
Description
Activity
- addedstate:triage-neededOpened without agent diagnostics and needs triageOpened without agent diagnostics and needs triage
on Sep 10, 2026 📋 triage-agent
Triage Assessment
Classification: validated-bug
Summary
The defect is real and confirmed by code inspection at
289481f5. All three lifecycle methods fall back to name matching when the suppliedsandbox_idis absent from the registry, and the registry can hold same-named records from different workspaces. Two corrections to the report are needed: the stated primary trigger (a repeated client delete) is not reachable through the public API, and a stronger, deterministic trigger inside the gateway was missed. Not reproduced against a running VM-driver gateway.Investigation
Fallback confirmed (line numbers shifted from the reported
a0814443):Function Definition id check Name fallback stop_sandboxdriver.rs:1386contains_key:1392:1395-1398start_sandboxdriver.rs:1460get_key_value:1466:1469-1472delete_sandboxdriver.rs:1519get_key_value:1531:1534-1538The registry is a
HashMap<String, SandboxRecord>(driver.rs:601), so.iter().find(...)is genuinely order-nondeterministic.create_sandboxrejects duplicates by id only (driver.rs:843-845) — no name or workspace check — so colliding records are admissible.get_sandboxis indeed unaffected: it gates onsandbox_id.is_empty()(driver.rs:1609-1623) and its handler post-checks the resolved id (driver.rs:3903-3907), while the three lifecycle handlers (:3923,:3933,:3943) forward both fields with no verification. All structural claims in the report verified accurate.Correction 1 — the repeated-delete trigger does not exist. A second client
DeleteSandboxfor the same (workspace, name) never reaches the driver.handle_delete_sandbox_inner(grpc/sandbox.rs:1348) callscompute.delete_sandbox(&workspace, &name), which resolves the name within the caller's workspace atcompute/mod.rs:1535-1540and returnsStatus::not_found("sandbox not found")when the row is gone. The caller receivesNOT_FOUND, notdeleted: true. Reproduction step 4 and the corresponding sentence in Impact are both incorrect as written.Correction 2 — the real trigger is the gateway's own prune sweep, and it is deterministic.
prune_missing_sandbox(compute/mod.rs:3615) callsget_driver_sandbox(&sandbox_id, &sandbox_name)at:3651. The VM driver's id-onlyget_sandboxreturnsNone, the sweep concludes the resource is gone, and it callsspawn_driver_sandbox_cleanup(&sandbox_id, &sandbox_name)at:3732→call_driver_delete_sandbox(:3401) →DeleteSandboxRequest { sandbox_id, sandbox_name }(:3412) → VM driver id-miss → name fallback → deletes a same-named sandbox in another workspace. This path is entered precisely because the id was already found absent, so it selects for the failing condition rather than merely tolerating it. Driver failures here are logged withwarn!and never propagated (:3422-3428), so the loss is silent — but because no client is on the other end, not because a client gets a false success.Two secondary paths:
apply_deleted_locked(:3290, cleanup spawn at:3303) re-issuesDeleteSandboxwith the same id+name after the driver's ownDeletedwatch event, escaping only when thetry_lock_ownedlifecycle gate at:3376is contended — a timing race, not a guarantee. And a partial restore after gateway restart (driver.rs:1786-1806returnsfalseon a missing image ref or unusable TLS paths) leaves a store row without a registry entry, so "gateway restart is not a reliable trigger" understates the case.Correction 3 — workspace-scoped matching is not an available fix. The report notes
snapshot.workspaceis populated (driver.rs:6405) but "not consulted when matching."StopSandboxRequest,StartSandboxRequest, andDeleteSandboxRequestcarry no workspace field (proto/compute_driver.proto:368-391), so the driver cannot disambiguate by workspace regardless. The acceptance criteria land on the right fix anyway — id-authoritative plus deterministic rejection of ambiguous name-only requests.Precedent. The Docker fix did land: #3240 merged upstream as
c6c85734on 2026-09-10, introducingresolve_pending_id(driver-docker/src/lib.rs:2269), which treats a non-empty id as authoritative and rejects multi-match names withfailed_precondition("sandbox_name matches multiple pending sandboxes; specify sandbox_id"). The pattern transfers directly to a singleresolve_registry_idhelper replacing all three inline blocks. One shape difference: the Docker defect lived in the pending map whilefind_managed_container_summaryalready required an exact id match, so the VM instance is strictly broader — it is the only lookup there is.Sibling drivers checked.
openshell-driver-podman(grpc.rs:180-222,driver.rs:1112/:1175/:1217) andopenshell-driver-kubernetes(grpc.rs:182-227,driver.rs:1632/:1702/:1808) both require a non-emptysandbox_idand are clean.openshell-driver-mxcresolves by name only (driver.rs:345,:441) — that is #3253, a distinct defect. No instances missed; VM is the last id→name fallback.Duplicates: none. #3234/#3240 is
openshell-driver-docker, #3253 isopenshell-driver-mxc. Three crates, three changesets.Impact Signals
- Affected users/scope: VM-driver gateways only (libkrun/Linux, opt-in standalone subprocess), and only where the same sandbox name exists in two or more workspaces.
- Regression: No. Latent since introduction —
delete_sandboxfallback traces toe4d6f92d feat(vm): add standalone libkrun compute driver(2026-04-17),stop/startto0f8fad23(feat(sandbox): add stop and start operations #2653, 2026-08-13). - Workaround: Unavailable, as claimed — the offending call originates inside the gateway, not the client, so no caller-side change avoids it. Operators can only refrain from reusing names across workspaces.
- Evidence quality: Medium-high. Structural claims verified against source and accurate; the reachability analysis in the report is wrong in its headline case, but a stronger deterministic trigger replaces it. Not reproduced live.
- Note for maintainers: the confirmed outcome is destruction of a running sandbox in a workspace the requester has no authority over — a cross-tenant isolation break. Adversarial reachability was not established, and the Docker analog was handled as an ordinary public bug (bug(driver-docker): deleting a sandbox can destroy a same-named sandbox in another workspace #3234 → fix(driver-docker): scope pending sandbox matching by id and workspace #3240), so this is triaged the same way. Apply
topic:securityif you judge the isolation dimension warrants the specialized review path instead.
Suggested additions to the acceptance criteria
- Add the workspace-scoped duplicate-name check to
create_sandbox(driver.rs:843), mirroring whatreserve_pending_sandboxreceived in the Docker fix — otherwise the driver keeps admitting the colliding state the fix defends against. - Add the
get_sandbox-style id post-check to the three handlers (driver.rs:3923,:3933,:3943) as defense in depth, mirroring:3903. - Worth resolving during planning: no gateway call site sends an empty
sandbox_id(all source it fromobject_id()). If conformance and VM e2e confirm no direct driver client relies on name-only lifecycle requests, the fallback can be deleted outright rather than made ambiguity-rejecting — a smaller change than the Docker fix.
Human Decision Required
Decide whether OpenShell should address this issue. If yes, apply
state:accepted, associate it with a roadmap item, or do both, and decide whether the work remains human-owned. Either action records acceptance; roadmap placement additionally records sequencing.
To queue investigation or planning for an unattended agent, also applyagent:plan-requested. You can instead directly ask an agent to usecreate-spikeorbuild-from-issueon this issue; the agent will warn about missing expected workflow labels and continue without changing them. If no, close it as not planned and record the rationale.- removedstate:triage-neededOpened without agent diagnostics and needs triageOpened without agent diagnostics and needs triage
on Oct 1, 2026
User Story
As an operator running sandboxes across multiple workspaces on the VM driver, I want a stop, start, or delete request that carries a sandbox id to either act on that exact sandbox or fail, so that a retried delete never destroys an unrelated sandbox that happens to share its name.
Problem Statement
stop_sandbox,start_sandbox, anddelete_sandboxon the VM driver prefer thesandbox_id, but when that id is not present in the registry they fall back to matching onsandbox_nameinstead of reporting the sandbox as absent.crates/openshell-driver-vm/src/driver.rs:1392(stop_sandbox;start_sandbox:1466anddelete_sandbox:1531are the same shape):The fallback fires whenever the id is missing — including when the caller supplied a perfectly valid one. Sandbox names are unique per workspace, not globally (
crates/openshell-server/src/persistence/tests.rs:684,sqlite_name_unique_scoped_by_workspace), and the VM registry permits same-named entries:create_sandboxrejects duplicates by id only (driver.rs:843), never by name.SandboxRecord.snapshot.workspaceis populated (driver.rs:6405) but is not consulted when matching, andHashMap::iter().find(...)selects arbitrarily when several names match.The gRPC handlers (
driver.rs:3923,:3933,:3943) forwardsandbox_idandsandbox_namestraight through with no id-match post-check.get_sandboxis not affected: it gates the fallback onsandbox_id.is_empty()(driver.rs:1614) and its handler verifies the resolved id afterward (driver.rs:3903).The most ordinary trigger is a repeated delete. A successful
delete_sandboxremoves the registry entry, so a retried or duplicateDeleteSandboxcarrying the same id and name finds no id, falls through to the name branch, and can delete a same-named sandbox in another workspace. Delete is intended to be idempotent — it returnsdeleted: falsewhen nothing matches — and that is precisely the path that misfires. Gateway restart is not a reliable trigger:restore_persisted_sandboxes(driver.rs:1645) rehydrates the registry from disk on startup.Impact / Why This Matters
A retried delete can destroy a live sandbox belonging to a different workspace. The caller receives
deleted: trueand no error, so the loss is silent, and because the fallback resolves through hash iteration order the outcome is not reproducible. Start and stop carry the same exposure with less severe consequences.There is no caller-side workaround: the request already carries the correct id and the driver discards it once the id is absent. Avoiding the bug requires never reusing a sandbox name across workspaces on a VM-driver gateway, which contradicts the workspace-scoped naming the rest of the product guarantees.
Related: #3234 and #3240 address the same class of defect in the Docker driver; #3253 covers the MXC driver.
Acceptance Criteria
sandbox_id,stop_sandbox,start_sandbox, anddelete_sandboxresolve on that id alone and do not fall back to the name.Reproduction Steps
Code inspection; not reproduced against a running VM-driver gateway.
alphaandbetaon a gateway using the VM compute driver.demoinalpha, and another nameddemoinbeta. Both are accepted — names are unique per workspace, and the VM registry rejects duplicates by id only.demoinbeta. It succeeds and its registry entry is removed.DeleteSandboxrequest (retry, duplicate delivery, or a second client call). Its id is no longer in the registry, so the driver falls back to the namedemoand may delete the sandbox inalpha, returningdeleted: true.Environment
mainata0814443