Repository navigation
IsolationSession: a second deprovision reports success instead of stale_id #1429
Description
Activity
- addedIssue-BugSomething is not working as intended.Something is not working as intended.Needs-TriageHas not yet been reviewed, classified, and routed.Has not yet been reviewed, classified, and routed.
on Oct 6, 2026 - removedNeeds-TriageHas not yet been reviewed, classified, and routed.Has not yet been reviewed, classified, and routed.
on Oct 6, 2026 - addedNeeds-TriageHas not yet been reviewed, classified, and routed.Has not yet been reviewed, classified, and routed.
on Oct 6, 2026 - addedOS-WindowsApplies specifically to Windows hosts or Windows behavior.Applies specifically to Windows hosts or Windows behavior.
on Oct 6, 2026 - addedBackend-IsolationSessionWindows IsolationSession backend behavior or integration.Windows IsolationSession backend behavior or integration.Priority2High-impact issue affecting key functionality; prioritize for the next appropriate release.High-impact issue affecting key functionality; prioritize for the next appropriate release.and removedNeeds-TriageHas not yet been reviewed, classified, and routed.Has not yet been reviewed, classified, and routed.
on Oct 6, 2026 This looks like the classic success-on-noop class: the second call isn't idempotent-by-design, it's reporting success because the OS call it depends on is itself idempotent in a way MXC's error-mapping never anticipated.
Your own tracing nails the location but stops one layer short. You note
deprovision_agent_user"already classifies withStalePromotion::Eligible," which means the promotion path is correctly wired — it's just never triggered becauseRemoveUserreturns success on a missing account instead ofERROR_NOT_FOUND. That's consistent with Windows account-management APIs that treat "already absent" as a satisfied postcondition rather than a failure — which is exactly whystart,stop, andexec(which query for presence rather than request removal) correctly surface0x80070490whiledeprovisiondoesn't: different OS primitive, different error semantics, same promotion code sitting downstream of both.The check that catches this at the right boundary: at the
IsolationSessionBackend::deprovisioncall site (state_aware.rs:171), before invokingdeprovision_agent_user, do a presence check (or capture the pre-call existence state) and assert that a no-op removal path is never let through asOk. Concretely: contract-testRemoveUseron a known-absent account in isolation, independent of the deprovision flow, and assert its return code — don't infer OS behavior from one integration test, verify it directly. If it returns success, the fix has to be in MXC (explicit existence check before/after calling the API), not in the OS layer.What would confirm vs. refute this: a standalone call to the underlying account-removal API against a nonexistent user, outside the deprovision flow — if it returns a success HRESULT, this is confirmed; if it returns
ERROR_NOT_FOUNDand MXC still swallows it, the break is indeprovision_agent_user's error handling instead, not the OS call.Advisory SFA-2026-E68A6B4000 · Full redacted write-up: https://impartshadow.github.io/echo-site/failure-cases/isolationsession-a-second-deprovision-reports-succes-e68a6b4000.html
If this diagnosis is useful, react 👍 to this comment; if it is wrong, react 👎 — I use the verdict to choose what I investigate next.
Relevant area(s)
Windows
Brief description of your issue
Calling
deprovisiontwice on the same IsolationSessionsandboxIdsucceeds both times. The second call should fail witherror.code: "stale_id", because by then the agent user it is meant to remove is already gone.Every other lifecycle operation gets this right. Running
start,stop, orexecagainst a deprovisionedsandboxIdcorrectly returnsstale_idwithnativeCode: "0x80070490". Onlydeprovisionreports success.This matters because
stale_idis how a caller learns that asandboxIdis dead. A caller that retries a deprovision it is unsure about — which the docs actively recommend as a best-effort cleanup pattern — gets a success back and has no way to tell whether it cleaned anything up or operated on nothing at all.Steps to reproduce
provisionan IsolationSession sandbox and keep the returnedsandboxId.deprovisionit. This succeeds.deprovisionthe samesandboxIdagain.Covered by two scenarios in
tests/scripts/run_isolation_session_state_aware_tests.ps1:stale_id (deprovision on previously-deprovisioned sandbox)Lifecycle G: repeating deprovision reports a stale sandboxBoth fail identically on all four isolation-session jobs (prerelease-26h1 and prerelease-isolation-session, x64 and arm64) in https://github.com/microsoft/mxc/actions/runs/37522853336, so this is deterministic rather than flaky.
Expected behavior
The second
deprovisionfails with a non-zero exit code and an error envelope carrying:error.code: "stale_id"error.operationnaming the failing API call (IsoSessionOps.*)error.nativeCode: "0x80070490"(HRESULT_FROM_WIN32(ERROR_NOT_FOUND))This is the documented contract in two places:
docs/isolation-session/state-aware-rust.md:404— "A second deprovision on the samesandboxIdfails the OS-side agent-user lookup (HRESULT_FROM_WIN32(ERROR_NOT_FOUND)), which the runner maps toMxcError::StaleId."docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md:1804— "deprovisioneither succeeds (cleanup completes) or returnsstale_id."Actual behavior
The second
deprovisionexits 0 with an empty error:The MXC-side plumbing looks correct, so the break appears to be below it.
IsolationSessionBackend::deprovision(src/mxc-sdk/src/backends/isolation_session/common/state_aware.rs:171) calls straight through todeprovision_agent_userwith no short-circuit, and that call already classifies withStalePromotion::Eligible(manager.rs:674-680). So theERROR_NOT_FOUND→stale_idpromotion is wired up and would fire — which suggests the underlyingRemoveUsercall is returning success for an agent user that no longer exists, leaving nothing to promote.Worth confirming where the chain actually breaks before picking a fix, since the options differ a lot:
stale_id,ERROR_NOT_FOUND(cross-repo), or