session(sandboxId).destroy() in packages/core/src/sandbox/docker/provider.ts evicts its cached ContainerHandle in a finally after the teardown:
destroy: async () => {
try {
await created.destroy()
}
finally {
if (handles.get(sandboxId) === handle) {
handles.delete(sandboxId)
}
}
},
Two races follow from the ordering, and they are the pair the microsandbox backend hit in #12.
Evicting late. While created.destroy() is in flight the handle is still registered, so a concurrent session(id) adopts it, reacquires, and starts using a container this call is removing. The identity check then still passes, so the destroy evicts the replacement's handle on its way out.
Evicting early is not the fix on its own. A sandbox id resolves to a container name, and createContainerHandle's acquire adopts whatever the daemon already has under that name — which is what makes an id resumable across host processes. So a fresh handle built during the teardown attaches to the container being removed.
#12 settled this for microsandbox/provider.ts by doing both: evict before awaiting, and publish the teardown per sandbox id so a handle built while one is in flight holds ready/peek/remove behind it. local/root.ts states the same rule for a directory rather than a container. The Docker provider is pre-existing on main and outside #12's diff, which is why it was left alone there.
just-bash needs no equivalent: a new just-bash handle is an independent virtual filesystem rather than a second claim on the same named machine.
Related: #21, also a construction the three new backends corrected and the Docker original still carries.
session(sandboxId).destroy()inpackages/core/src/sandbox/docker/provider.tsevicts its cachedContainerHandlein afinallyafter the teardown:Two races follow from the ordering, and they are the pair the microsandbox backend hit in #12.
Evicting late. While
created.destroy()is in flight the handle is still registered, so a concurrentsession(id)adopts it, reacquires, and starts using a container this call is removing. The identity check then still passes, so the destroy evicts the replacement's handle on its way out.Evicting early is not the fix on its own. A sandbox id resolves to a container name, and
createContainerHandle's acquire adopts whatever the daemon already has under that name — which is what makes an id resumable across host processes. So a fresh handle built during the teardown attaches to the container being removed.#12 settled this for
microsandbox/provider.tsby doing both: evict before awaiting, and publish the teardown per sandbox id so a handle built while one is in flight holdsready/peek/removebehind it.local/root.tsstates the same rule for a directory rather than a container. The Docker provider is pre-existing onmainand outside #12's diff, which is why it was left alone there.just-bashneeds no equivalent: a new just-bash handle is an independent virtual filesystem rather than a second claim on the same named machine.Related: #21, also a construction the three new backends corrected and the Docker original still carries.