From b3cd765e8da9c4c87dd0d5fc6cb412e94a029e39 Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Wed, 26 Aug 2026 13:05:07 -0700 Subject: [PATCH 01/24] Add container restart runtime support --- src/windows/service/inc/wslc.idl | 2 + src/windows/wslcsession/WSLCContainer.cpp | 113 +++++++++++++++++- src/windows/wslcsession/WSLCContainer.h | 23 ++++ test/windows/WSLCTests.cpp | 133 ++++++++++++++++++++++ 4 files changed, 268 insertions(+), 3 deletions(-) diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index f3358e25b0..972c281b93 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -574,6 +574,7 @@ interface IWSLCContainer : IUnknown HRESULT DisconnectFromNetwork([in] LPCSTR NetworkName); HRESULT UploadArchive([in] WSLCHandle TarHandle, [in, string] LPCSTR DestPath, [in] ULONGLONG ContentSize); HRESULT DownloadArchive([in, string] LPCSTR SrcPath, [in] WSLCHandle OutHandle); + HRESULT Restart([in] WSLCSignal Signal, [in] LONG TimeoutSeconds, [in, unique] IWarningCallback* WarningCallback); } typedef struct _WSLCDeletedImageInformation @@ -877,3 +878,4 @@ cpp_quote("#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILIT // N.B. WSLC_E_VM_NOT_RUNNING is part of the plugin API contract and is also defined in WslPluginApi.h. // The two definitions must stay in sync. cpp_quote("#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */") +cpp_quote("#define WSLC_E_CONTAINER_MARKED_FOR_REMOVAL MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */") diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 495e0c1529..fc9cdaad7d 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1021,12 +1021,27 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* } void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions) +{ + StartPhase(Flags, StartOptions, false); +} + +void WSLCContainerImpl::StartPhase(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, bool RestartPhase) { std::shared_ptr transition; auto lifecycleLock = m_lifecycleLock.lock_shared(); auto lock = m_lock.lock_exclusive(); + + if (!RestartPhase) + { + WaitForRestartToComplete(lock, lifecycleLock); + } + WaitForConflictingTransitionToComplete(lock, lifecycleLock); + // A Delete() that raced a restart may have already moved the container to the Deleted state. + THROW_HR_WITH_USER_ERROR_IF( + WSLC_E_CONTAINER_MARKED_FOR_REMOVAL, Localization::MessageWslcContainerMarkedForRemoval(m_id), m_state == WslcContainerStateDeleted); + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_IS_RUNNING, Localization::MessageWslcContainerIsRunning(m_id), m_state == WslcContainerStateRunning); THROW_HR_IF_MSG( @@ -1181,6 +1196,22 @@ void WSLCContainerImpl::WaitForConflictingTransitionToComplete( } } +void WSLCContainerImpl::WaitForRestartToComplete(wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock) +{ + while (m_restart) + { + { + auto restart = m_restart; + lock.reset(); + lifecycleLock.reset(); + WaitForCompletionEvent(restart->Completed.get()); + } + + lifecycleLock = m_lifecycleLock.lock_shared(); + lock = m_lock.lock_exclusive(); + } +} + __requires_exclusive_lock_held(m_lock) std::shared_ptr WSLCContainerImpl::StartTransition( TransitionKind kind, ContainerEvent expectedEvent) { @@ -1190,11 +1221,16 @@ __requires_exclusive_lock_held(m_lock) std::shared_ptr& transition) const +void WSLCContainerImpl::WaitForCompletionEvent(HANDLE Event) const { auto io = m_wslcSession.CreateIOContext(); - io.AddHandle(std::make_unique(transition->Completed.get())); + io.AddHandle(std::make_unique(Event)); io.Run({}); +} + +void WSLCContainerImpl::WaitForTransitionCompletion(const std::shared_ptr& transition) const +{ + WaitForCompletionEvent(transition->Completed.get()); WI_ASSERT(transition->Completed.is_signaled()); } @@ -1287,12 +1323,23 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCod } void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) +{ + StopPhase(Signal, TimeoutSeconds, Kill, false); +} + +void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill, bool RestartPhase) { std::shared_ptr transition; { auto lifecycleLock = m_lifecycleLock.lock_shared(); auto lock = m_lock.lock_exclusive(); + + if (!RestartPhase) + { + WaitForRestartToComplete(lock, lifecycleLock); + } + WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop); transition = m_transition; @@ -1397,6 +1444,45 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) } } +void WSLCContainerImpl::Restart(WSLCSignal Signal, LONG TimeoutSeconds) +{ + bool wasRunning{}; + auto restart = std::make_shared(); + + { + auto lifecycleLock = m_lifecycleLock.lock_shared(); + auto lock = m_lock.lock_exclusive(); + WaitForRestartToComplete(lock, lifecycleLock); + + wasRunning = m_state == WslcContainerStateRunning; + + // N.B. Stop() and Start() each take m_lock, so it cannot be held across both phases. m_restart + // keeps the pair indivisible instead. + m_restart = restart; + } + + auto restartCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, restart]() { + { + auto lock = m_lock.lock_exclusive(); + + // CommitState() clears this once the start phase lands, so a later restart may already own it. + if (m_restart == restart) + { + m_restart.reset(); + } + } + + restart->Completed.SetEvent(); + }); + + if (wasRunning) + { + StopPhase(Signal, TimeoutSeconds, false, true); + } + + StartPhase(WSLCContainerStartFlagsNone, nullptr, true); +} + __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exitCode, std::optional stopTimestamp) { auto transition = m_transition; @@ -1440,7 +1526,7 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exi } // Stop with Rm must initiate Delete. - if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm)) + if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm) && !m_restart) { try { @@ -1514,6 +1600,9 @@ void WSLCContainerImpl::Delete(WSLCDeleteFlags Flags) std::shared_ptr transition; auto lifecycleLock = m_lifecycleLock.lock_shared(); auto lock = m_lock.lock_exclusive(); + + // N.B. Unlike Start() and Stop(), this deliberately does not wait for an in-flight restart. + // A remove that lands between the two phases takes effect, and the restart's start phase fails. WaitForConflictingTransitionToComplete(lock, lifecycleLock); RequestDeleteExclusiveLockHeld(Flags); @@ -2933,6 +3022,12 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::CommitState(WSLCContainerSt m_stateGeneration++; m_stateChangedAt = stateChangedAt.value_or(static_cast(std::time(nullptr))); + if (State == WslcContainerStateRunning) + { + // The restart's start phase landed, so a later exit must auto-delete an --rm container again. + m_restart.reset(); + } + // Keep the VM alive while this container is Running and release the hold once it leaves that // state, even when no client holds the wrapper (e.g. a detached `run -d` container). Dropping // the hold on the transition out of Running is what lets an otherwise-idle VM be torn down; a @@ -3071,6 +3166,18 @@ try } CATCH_RETURN(); +HRESULT WSLCContainer::Restart(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, IWarningCallback* WarningCallback) +try +{ + WSLCExecutionContext context(&m_session, WarningCallback); + + // Hold a VM lease across both phases: the container is not Running in between, so nothing else + // keeps the VM alive. + auto vmLease = m_session.Runtime().AcquireVmLease(); + return CallImpl(&WSLCContainerImpl::Restart, Signal, TimeoutSeconds); +} +CATCH_RETURN(); + HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, IWarningCallback* WarningCallback) try { diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index f145aaaee2..2ecbe39b5d 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -97,6 +97,7 @@ class WSLCContainerImpl : public std::enable_shared_from_this void Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions); void Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* Stdout, WSLCHandle* Stderr) const; void Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, bool Kill); + void Restart(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds); void Delete(WSLCDeleteFlags Flags); void Export(WSLCHandle TarHandle) const; void UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize) const; @@ -173,6 +174,13 @@ class WSLCContainerImpl : public std::enable_shared_from_this unique_com_disconnect Wrapper; }; + // Restart() runs a stop phase followed by a start phase. This marks the pair as one transaction so + // that Start() and Stop() cannot land in between. + struct RestartTransaction + { + wil::unique_event Completed{wil::EventOptions::ManualReset}; + }; + __requires_exclusive_lock_held(m_lock) void RequestDeleteExclusiveLockHeld(WSLCDeleteFlags Flags); void AllocateBridgedModePorts(); @@ -186,6 +194,15 @@ class WSLCContainerImpl : public std::enable_shared_from_this wil::rwlock_release_shared_scope_exit& lifecycleLock, std::optional kind = std::nullopt); + // Returns with both locks held once no restart is in flight. + void WaitForRestartToComplete(wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock); + + // Phases of Restart(). Identical to Start() and Stop() except that they do not stand down for the + // restart they are part of. + void StartPhase(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, bool RestartPhase); + void StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill, bool RestartPhase); + + void WaitForCompletionEvent(HANDLE Event) const; void WaitForTransitionCompletion(const std::shared_ptr& transition) const; void AttachToTransition(const std::shared_ptr& transition) const; @@ -231,6 +248,11 @@ class WSLCContainerImpl : public std::enable_shared_from_this _Guarded_by_(m_lock) std::shared_ptr m_transition; + // Non-null while Restart() owns both phases. Start() and Stop() stand down until it completes, and + // OnStopped() skips the auto-delete of an --rm container. Delete() does not stand down: a remove + // that lands between the two phases takes effect, and the restart's start phase fails. + _Guarded_by_(m_lock) std::shared_ptr m_restart; + // The container outlives any single VM: it survives idle-termination and is reused when the VM // restarts. VM-scoped resources (Vm(), Docker(), Volumes(), Events(), Relay()) are therefore // fetched from the (stable) runtime at each use rather than cached, since a cached reference @@ -270,6 +292,7 @@ class DECLSPEC_UUID("B1F1C4E3-C225-4CAE-AD8A-34C004DE1AE4") WSLCContainer IFACEMETHOD(Attach)(_In_opt_ LPCSTR DetachKeys, _Out_ WSLCHandle* Stdin, _Out_ WSLCHandle* Stdout, _Out_ WSLCHandle* Stderr) override; IFACEMETHOD(Stop)(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds) override; + IFACEMETHOD(Restart)(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, _In_opt_ IWarningCallback* WarningCallback) override; IFACEMETHOD(Kill)(_In_ WSLCSignal Signal) override; IFACEMETHOD(Delete)(WSLCDeleteFlags Flags) override; IFACEMETHOD(Export)(_In_ WSLCHandle TarHandle) override; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 22701d1508..186af0c79b 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -6856,6 +6856,139 @@ class WSLCTests } } + WSLC_TEST_METHOD(ContainerRestart) + { + // A running container is stopped and started again, replacing its init process. + { + WSLCContainerLauncher launcher("debian:latest", "test-restart-running", {"sleep", "99999"}); + auto container = launcher.Launch(*m_defaultSession); + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning); + + auto initProcess = container.GetInitProcess(); + VERIFY_SUCCEEDED(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr)); + + VERIFY_ARE_EQUAL(initProcess.Wait(), WSLCSignalSIGKILL + 128); + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning); + } + + // A created container has no stop phase. + { + WSLCContainerLauncher launcher("debian:latest", "test-restart-created", {"sleep", "99999"}); + auto container = launcher.Create(*m_defaultSession); + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateCreated); + + VERIFY_SUCCEEDED(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr)); + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning); + } + + // An exited container is started again. + { + WSLCContainerLauncher launcher("debian:latest", "test-restart-exited", {"echo", "OK"}); + auto container = launcher.Launch(*m_defaultSession); + + auto initProcess = container.GetInitProcess(); + ValidateProcessOutput(initProcess, {{1, "OK\n"}}); + + VERIFY_SUCCEEDED(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr)); + + auto restartedProcess = container.GetInitProcess(); + VERIFY_ARE_EQUAL(restartedProcess.Wait(), 0); + + COMOutputHandle stdoutLogs{}; + COMOutputHandle stderrLogs{}; + VERIFY_SUCCEEDED(container.Get().Logs(WSLCLogsFlagsNone, &stdoutLogs, &stderrLogs, 0, 0, 0)); + ValidateHandleOutput(stdoutLogs.Get(), "OK\nOK\n"); + } + + // Restarting a container with the autorm flag set must not auto-delete it, but a later stop must. + { + WSLCContainerLauncher launcher("debian:latest", "test-restart-autorm", {"sleep", "99999"}); + launcher.SetContainerFlags(WSLCContainerFlagsRm | WSLCContainerFlagsInit); + auto container = launcher.Launch(*m_defaultSession); + + VERIFY_SUCCEEDED(container.Get().Restart(WSLCSignalSIGTERM, WSLC_STOP_TIMEOUT_DEFAULT, nullptr)); + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning); + + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0)); + VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr, nullptr), RPC_E_DISCONNECTED); + } + + // Validate that deleted containers can't be restarted. + { + WSLCContainerLauncher launcher("debian:latest", "test-restart-deleted", {"sleep", "99999"}); + auto container = launcher.Launch(*m_defaultSession); + + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0)); + VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone)); + + VERIFY_ARE_EQUAL(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr), RPC_E_DISCONNECTED); + } + + // An init that ignores SIGTERM keeps the restart's stop phase in flight until the timeout expires, + // which is what gives the requests below a window to land in the middle of a restart. + const std::vector ignoreStopSignal = { + "/bin/sh", "-c", "trap 'echo stopping' TERM; while true; do sleep 1; done"}; + const std::string stopSignalMarker = "stopping"; + constexpr LONG stopTimeoutSeconds = 10; + + // A stop issued during a restart waits for both phases, so it can't be lost in between them. + { + WSLCContainerLauncher launcher("debian:latest", "test-restart-race-stop", ignoreStopSignal); + auto container = launcher.Launch(*m_defaultSession); + auto initProcess = container.GetInitProcess(); + + std::promise restartResult; + std::thread restartThread( + [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); }); + + auto joinThread = wil::scope_exit([&]() { restartThread.join(); }); + + WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker); + + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0)); + VERIFY_SUCCEEDED(restartResult.get_future().get()); + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited); + } + + // A delete issued during a restart deliberately does not wait for it, matching docker: whichever of + // the delete and the restart's start phase lands first wins, and the other one fails. + { + WSLCContainerLauncher launcher("debian:latest", "test-restart-race-delete", ignoreStopSignal); + auto container = launcher.Launch(*m_defaultSession); + auto initProcess = container.GetInitProcess(); + + std::promise restartResult; + std::thread restartThread( + [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); }); + + auto joinThread = wil::scope_exit([&]() { restartThread.join(); }); + auto restartFuture = restartResult.get_future(); + + WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker); + + // The gap between the two phases is short, so poll for it: until the container has exited, every + // delete is turned away by the ordinary running-container guard rather than by the restart. + HRESULT deleteResult = WSLC_E_CONTAINER_IS_RUNNING; + while (deleteResult == WSLC_E_CONTAINER_IS_RUNNING && restartFuture.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + { + deleteResult = container.Get().Delete(WSLCDeleteFlagsNone); + } + + const auto restartHr = restartFuture.get(); + + if (SUCCEEDED(deleteResult)) + { + VERIFY_ARE_EQUAL(restartHr, WSLC_E_CONTAINER_MARKED_FOR_REMOVAL); + } + else + { + // The start phase closed the gap first, so the container was running again by the last attempt. + VERIFY_ARE_EQUAL(deleteResult, WSLC_E_CONTAINER_IS_RUNNING); + VERIFY_SUCCEEDED(restartHr); + } + } + } + WSLC_TEST_METHOD(OpenContainer) { auto expectOpen = [&](const char* Id, HRESULT expectedResult = S_OK) { From 1c4032dd5ba712b88c7073c7240ed974255ed7ca Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Wed, 26 Aug 2026 14:26:35 -0700 Subject: [PATCH 02/24] Fix build --- localization/strings/en-US/Resources.resw | 4 ++++ src/windows/WslcSDK/wslcsdk.h | 1 + test/windows/WSLCTests.cpp | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 7649618eb2..664255b81c 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2195,6 +2195,10 @@ Usage: Container '{}' not found. {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + Container '{}' is marked for removal and cannot be started. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Network name cannot be empty. diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h index 56f05a61a5..511b183755 100644 --- a/src/windows/WslcSDK/wslcsdk.h +++ b/src/windows/WslcSDK/wslcsdk.h @@ -44,6 +44,7 @@ EXTERN_C_START #define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */ #define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */ #define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */ +#define WSLC_E_CONTAINER_MARKED_FOR_REMOVAL MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */ // Session values #define WSLC_SESSION_OPTIONS_SIZE 72 diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 186af0c79b..fbe4896034 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -6969,7 +6969,8 @@ class WSLCTests // The gap between the two phases is short, so poll for it: until the container has exited, every // delete is turned away by the ordinary running-container guard rather than by the restart. HRESULT deleteResult = WSLC_E_CONTAINER_IS_RUNNING; - while (deleteResult == WSLC_E_CONTAINER_IS_RUNNING && restartFuture.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + while (deleteResult == WSLC_E_CONTAINER_IS_RUNNING && + restartFuture.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready) { deleteResult = container.Get().Delete(WSLCDeleteFlagsNone); } From 0ffa371cacaece3e560b3e7a1fb468e5395c41af Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Wed, 26 Aug 2026 14:37:56 -0700 Subject: [PATCH 03/24] Format --- src/windows/WslcSDK/wslcsdk.h | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h index 511b183755..cf8ef8b240 100644 --- a/src/windows/WslcSDK/wslcsdk.h +++ b/src/windows/WslcSDK/wslcsdk.h @@ -28,22 +28,22 @@ EXTERN_C_START // WSLC specific error codes // Ensure wslc.idl and wslcsdk.idl are also updated. #define WSLC_E_BASE (0x0600) -#define WSLC_E_IMAGE_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 1) /* 0x80040601 */ -#define WSLC_E_CONTAINER_PREFIX_AMBIGUOUS MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 2) /* 0x80040602 */ -#define WSLC_E_CONTAINER_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 3) /* 0x80040603 */ -#define WSLC_E_VOLUME_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 4) /* 0x80040604 */ -#define WSLC_E_CONTAINER_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 5) /* 0x80040605 */ -#define WSLC_E_CONTAINER_IS_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 6) /* 0x80040606 */ -#define WSLC_E_SESSION_RESERVED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 7) /* 0x80040607 */ -#define WSLC_E_INVALID_SESSION_NAME MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 8) /* 0x80040608 */ -#define WSLC_E_NETWORK_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 9) /* 0x80040609 */ -#define WSLC_E_WU_SEARCH_FAILED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 10) /* 0x8004060A */ -#define WSLC_E_SDK_UPDATE_NEEDED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 11) /* 0x8004060B */ -#define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 12) /* 0x8004060C */ -#define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */ -#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */ -#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */ -#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */ +#define WSLC_E_IMAGE_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 1) /* 0x80040601 */ +#define WSLC_E_CONTAINER_PREFIX_AMBIGUOUS MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 2) /* 0x80040602 */ +#define WSLC_E_CONTAINER_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 3) /* 0x80040603 */ +#define WSLC_E_VOLUME_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 4) /* 0x80040604 */ +#define WSLC_E_CONTAINER_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 5) /* 0x80040605 */ +#define WSLC_E_CONTAINER_IS_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 6) /* 0x80040606 */ +#define WSLC_E_SESSION_RESERVED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 7) /* 0x80040607 */ +#define WSLC_E_INVALID_SESSION_NAME MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 8) /* 0x80040608 */ +#define WSLC_E_NETWORK_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 9) /* 0x80040609 */ +#define WSLC_E_WU_SEARCH_FAILED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 10) /* 0x8004060A */ +#define WSLC_E_SDK_UPDATE_NEEDED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 11) /* 0x8004060B */ +#define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 12) /* 0x8004060C */ +#define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */ +#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */ +#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */ +#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */ #define WSLC_E_CONTAINER_MARKED_FOR_REMOVAL MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */ // Session values From b0cb18d976d5b5c8d0017f941936f592c651eb0b Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Thu, 27 Aug 2026 23:43:37 -0700 Subject: [PATCH 04/24] Validate the stop timeout for every restart path --- src/windows/wslcsession/WSLCContainer.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index fc9cdaad7d..eaa984cb3d 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1446,6 +1446,9 @@ void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool K void WSLCContainerImpl::Restart(WSLCSignal Signal, LONG TimeoutSeconds) { + // The stop phase is skipped when the container is not running, so it cannot be the only validation. + ValidateStopTimeout(TimeoutSeconds, true); + bool wasRunning{}; auto restart = std::make_shared(); From 800e7386f2e176645c49c125e32d60491b72c723 Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Thu, 27 Aug 2026 23:48:26 -0700 Subject: [PATCH 05/24] Merge the restart guard into the transition wait loop --- src/windows/wslcsession/WSLCContainer.cpp | 47 ++++++++--------------- src/windows/wslcsession/WSLCContainer.h | 9 ++--- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index eaa984cb3d..ab97a754f3 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1031,12 +1031,7 @@ void WSLCContainerImpl::StartPhase(WSLCContainerStartFlags Flags, const WSLCProc auto lifecycleLock = m_lifecycleLock.lock_shared(); auto lock = m_lock.lock_exclusive(); - if (!RestartPhase) - { - WaitForRestartToComplete(lock, lifecycleLock); - } - - WaitForConflictingTransitionToComplete(lock, lifecycleLock); + WaitForConflictingTransitionToComplete(lock, lifecycleLock, std::nullopt, /* waitForRestart */ !RestartPhase); // A Delete() that raced a restart may have already moved the container to the Deleted state. THROW_HR_WITH_USER_ERROR_IF( @@ -1180,31 +1175,28 @@ void WSLCContainerImpl::StartPhase(WSLCContainerStartFlags Flags, const WSLCProc } void WSLCContainerImpl::WaitForConflictingTransitionToComplete( - wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock, std::optional kind) + wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock, std::optional kind, bool waitForRestart) { - while (m_transition && (!kind.has_value() || m_transition->Kind != kind.value())) + while (true) { + // A restart spans two transitions, so waiting on the one in flight is not enough. + if (waitForRestart && m_restart) { - auto transition = m_transition; + auto restart = m_restart; lock.reset(); lifecycleLock.reset(); - WaitForTransitionCompletion(transition); + WaitForCompletionEvent(restart->Completed.get()); } - - lifecycleLock = m_lifecycleLock.lock_shared(); - lock = m_lock.lock_exclusive(); - } -} - -void WSLCContainerImpl::WaitForRestartToComplete(wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock) -{ - while (m_restart) - { + else if (m_transition && (!kind.has_value() || m_transition->Kind != kind.value())) { - auto restart = m_restart; + auto transition = m_transition; lock.reset(); lifecycleLock.reset(); - WaitForCompletionEvent(restart->Completed.get()); + WaitForTransitionCompletion(transition); + } + else + { + return; } lifecycleLock = m_lifecycleLock.lock_shared(); @@ -1335,12 +1327,7 @@ void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool K auto lifecycleLock = m_lifecycleLock.lock_shared(); auto lock = m_lock.lock_exclusive(); - if (!RestartPhase) - { - WaitForRestartToComplete(lock, lifecycleLock); - } - - WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop); + WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop, /* waitForRestart */ !RestartPhase); transition = m_transition; WI_ASSERT(!transition || transition->Kind == TransitionKind::Stop); @@ -1455,7 +1442,7 @@ void WSLCContainerImpl::Restart(WSLCSignal Signal, LONG TimeoutSeconds) { auto lifecycleLock = m_lifecycleLock.lock_shared(); auto lock = m_lock.lock_exclusive(); - WaitForRestartToComplete(lock, lifecycleLock); + WaitForConflictingTransitionToComplete(lock, lifecycleLock); wasRunning = m_state == WslcContainerStateRunning; @@ -1606,7 +1593,7 @@ void WSLCContainerImpl::Delete(WSLCDeleteFlags Flags) // N.B. Unlike Start() and Stop(), this deliberately does not wait for an in-flight restart. // A remove that lands between the two phases takes effect, and the restart's start phase fails. - WaitForConflictingTransitionToComplete(lock, lifecycleLock); + WaitForConflictingTransitionToComplete(lock, lifecycleLock, std::nullopt, /* waitForRestart */ false); RequestDeleteExclusiveLockHeld(Flags); transition = StartTransition(TransitionKind::Delete, ContainerEvent::Destroy); diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 2ecbe39b5d..9165632e68 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -188,14 +188,13 @@ class WSLCContainerImpl : public std::enable_shared_from_this __requires_exclusive_lock_held(m_lock) std::shared_ptr StartTransition(TransitionKind kind, ContainerEvent expectedEvent); - // Returns with both locks held when no transition is active or the active transition matches kind. + // Returns with both locks held when no restart is in flight and no transition is active, or the + // active transition matches kind. Both conditions are re-checked every time the locks come back. void WaitForConflictingTransitionToComplete( wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock, - std::optional kind = std::nullopt); - - // Returns with both locks held once no restart is in flight. - void WaitForRestartToComplete(wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock); + std::optional kind = std::nullopt, + bool waitForRestart = true); // Phases of Restart(). Identical to Start() and Stop() except that they do not stand down for the // restart they are part of. From 12015bd6f85ed862ac960b44463f6d604a586553 Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Thu, 27 Aug 2026 23:50:50 -0700 Subject: [PATCH 06/24] Keep the container's ports and mounts across a restart --- src/windows/wslcsession/WSLCContainer.cpp | 25 +++++++++++++++++------ src/windows/wslcsession/WSLCContainer.h | 10 +++++++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index ab97a754f3..8c9276e7cc 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1114,10 +1114,17 @@ void WSLCContainerImpl::StartPhase(WSLCContainerStartFlags Flags, const WSLCProc Localization::MessageWslcVolumeNotAvailable(wsl::shared::string::Join(unavailableVolumes, ',')), !unavailableVolumes.empty()); - auto volumeCleanup = MountVolumes(m_mountedVolumes, m_runtime.Vm()); + // A restart keeps its ports and mounts across both phases, so re-acquiring them here would collide + // with the container's own reservations. Release them if the start does not land, since an exited + // container must not keep holding them. + auto resourceCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { ReleaseRuntimeResources(); }); - auto portCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { UnmapPorts(); }); - MapPorts(); + if (!m_runtimeResourcesHeld) + { + MountVolumes(m_mountedVolumes, m_runtime.Vm()).release(); + MapPorts(); + m_runtimeResourcesHeld = true; + } try { @@ -1165,8 +1172,7 @@ void WSLCContainerImpl::StartPhase(WSLCContainerStartFlags Flags, const WSLCProc transition = StartTransition(TransitionKind::Start, ContainerEvent::Start); - portCleanup.release(); - volumeCleanup.release(); + resourceCleanup.release(); cleanup.release(); lock.reset(); @@ -1499,7 +1505,12 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exi } ReleaseProcesses(); - ReleaseRuntimeResources(); + + // A restart's start phase relies on the container's ports and mounts still being held. + if (!m_restart) + { + ReleaseRuntimeResources(); + } // Ignore duplicate or late Stop events so they do not overwrite an already committed state. if (m_state == WslcContainerStateRunning) @@ -2944,6 +2955,8 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::ReleaseRuntimeRes { WSL_LOG("ReleaseRuntimeResources", TraceLoggingValue(m_id.c_str(), "ID")); + m_runtimeResourcesHeld = false; + // Release runtime resources (port relays, volume mounts) that were set up at Start(). UnmapPorts(); diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 9165632e68..df5cf768b6 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -248,10 +248,16 @@ class WSLCContainerImpl : public std::enable_shared_from_this _Guarded_by_(m_lock) std::shared_ptr m_transition; // Non-null while Restart() owns both phases. Start() and Stop() stand down until it completes, and - // OnStopped() skips the auto-delete of an --rm container. Delete() does not stand down: a remove - // that lands between the two phases takes effect, and the restart's start phase fails. + // OnStopped() keeps the container's runtime resources mapped and skips the auto-delete of an --rm + // container. Delete() does not stand down: a remove that lands between the two phases takes effect, + // and the restart's start phase fails. _Guarded_by_(m_lock) std::shared_ptr m_restart; + // True between a successful StartPhase() and the release of the container's ports and mounts. A + // restart leaves this set across the two phases, which is what tells the start phase they are still + // held and must not be re-acquired. + _Guarded_by_(m_lock) bool m_runtimeResourcesHeld {}; + // The container outlives any single VM: it survives idle-termination and is reused when the VM // restarts. VM-scoped resources (Vm(), Docker(), Volumes(), Events(), Relay()) are therefore // fetched from the (stable) runtime at each use rather than cached, since a cached reference From 8c5fa6b0f8a7fe0e5a27e5c650978306d7222450 Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Thu, 27 Aug 2026 23:54:41 -0700 Subject: [PATCH 07/24] Perform the deferred delete when a restart fails --- src/windows/wslcsession/WSLCContainer.cpp | 56 ++++++++++++++++++----- src/windows/wslcsession/WSLCContainer.h | 4 ++ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 8c9276e7cc..dd0d1f5f02 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1457,26 +1457,58 @@ void WSLCContainerImpl::Restart(WSLCSignal Signal, LONG TimeoutSeconds) m_restart = restart; } - auto restartCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, restart]() { - { - auto lock = m_lock.lock_exclusive(); + auto failureCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { OnFailedRestart(); }); - // CommitState() clears this once the start phase lands, so a later restart may already own it. - if (m_restart == restart) + { + auto restartCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, restart]() { { - m_restart.reset(); + auto lock = m_lock.lock_exclusive(); + + // CommitState() clears this once the start phase lands, so a later restart may already own it. + if (m_restart == restart) + { + m_restart.reset(); + } } + + restart->Completed.SetEvent(); + }); + + if (wasRunning) + { + StopPhase(Signal, TimeoutSeconds, false, true); } - restart->Completed.SetEvent(); - }); + StartPhase(WSLCContainerStartFlagsNone, nullptr, true); + } - if (wasRunning) + failureCleanup.release(); +} + +// N.B. Runs after the restart transaction has been released, so the delete below is no longer +// suppressed by OnStopped(). +void WSLCContainerImpl::OnFailedRestart() noexcept +{ + try { - StopPhase(Signal, TimeoutSeconds, false, true); - } + { + auto lock = m_lock.lock_exclusive(); + + // The stop phase held these back for a start phase that never landed. + if (m_runtimeResourcesHeld && m_state != WslcContainerStateRunning) + { + ReleaseRuntimeResources(); + } - StartPhase(WSLCContainerStartFlagsNone, nullptr, true); + if (WI_IsFlagClear(m_containerFlags, WSLCContainerFlagsRm) || m_state != WslcContainerStateExited) + { + return; + } + } + + Delete(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes); + } + CATCH_LOG() } __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exitCode, std::optional stopTimestamp) diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index df5cf768b6..e48ff86aba 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -201,6 +201,10 @@ class WSLCContainerImpl : public std::enable_shared_from_this void StartPhase(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, bool RestartPhase); void StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill, bool RestartPhase); + // Undoes what the phases left half-done: releases the resources the stop phase held back and + // performs the auto-delete OnStopped() deferred. + void OnFailedRestart() noexcept; + void WaitForCompletionEvent(HANDLE Event) const; void WaitForTransitionCompletion(const std::shared_ptr& transition) const; void AttachToTransition(const std::shared_ptr& transition) const; From 503d773fa3a2f19a6208fd1dfff2cea30365d4dc Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Thu, 27 Aug 2026 23:57:12 -0700 Subject: [PATCH 08/24] Annotate the phase boolean arguments --- src/windows/wslcsession/WSLCContainer.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index dd0d1f5f02..0b705ab876 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1022,7 +1022,7 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions) { - StartPhase(Flags, StartOptions, false); + StartPhase(Flags, StartOptions, /* RestartPhase */ false); } void WSLCContainerImpl::StartPhase(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, bool RestartPhase) @@ -1322,7 +1322,7 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCod void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) { - StopPhase(Signal, TimeoutSeconds, Kill, false); + StopPhase(Signal, TimeoutSeconds, Kill, /* RestartPhase */ false); } void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill, bool RestartPhase) @@ -1476,10 +1476,10 @@ void WSLCContainerImpl::Restart(WSLCSignal Signal, LONG TimeoutSeconds) if (wasRunning) { - StopPhase(Signal, TimeoutSeconds, false, true); + StopPhase(Signal, TimeoutSeconds, /* Kill */ false, /* RestartPhase */ true); } - StartPhase(WSLCContainerStartFlagsNone, nullptr, true); + StartPhase(WSLCContainerStartFlagsNone, nullptr, /* RestartPhase */ true); } failureCleanup.release(); From 9237a2c9c8fd45f5ddc757eeb452b8079524dc96 Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Thu, 27 Aug 2026 23:58:40 -0700 Subject: [PATCH 09/24] Add coverage for two overlapping restarts --- test/windows/WSLCTests.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index fbe4896034..26e798c864 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -6988,6 +6988,27 @@ class WSLCTests VERIFY_SUCCEEDED(restartHr); } } + + // A restart issued during a restart waits for both of the first one's phases, so the two pairs + // cannot interleave and the container is left running. + { + WSLCContainerLauncher launcher("debian:latest", "test-restart-race-restart", ignoreStopSignal); + auto container = launcher.Launch(*m_defaultSession); + auto initProcess = container.GetInitProcess(); + + std::promise restartResult; + std::thread restartThread( + [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); }); + + auto joinThread = wil::scope_exit([&]() { restartThread.join(); }); + + WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker); + + // The first restart is still in its stop phase, so this one only returns once that pair is done. + VERIFY_SUCCEEDED(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr)); + VERIFY_SUCCEEDED(restartResult.get_future().get()); + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning); + } } WSLC_TEST_METHOD(OpenContainer) From 6ad32faadc87daddbd05583bd161b1765a2223aa Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Fri, 28 Aug 2026 06:20:59 -0700 Subject: [PATCH 10/24] Describe the restart transaction's actual lifetime --- src/windows/wslcsession/WSLCContainer.cpp | 2 +- src/windows/wslcsession/WSLCContainer.h | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 0b705ab876..c94de21a89 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1453,7 +1453,7 @@ void WSLCContainerImpl::Restart(WSLCSignal Signal, LONG TimeoutSeconds) wasRunning = m_state == WslcContainerStateRunning; // N.B. Stop() and Start() each take m_lock, so it cannot be held across both phases. m_restart - // keeps the pair indivisible instead. + // stands them down until the start phase commits Running instead. m_restart = restart; } diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index e48ff86aba..3e917ecf26 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -251,10 +251,10 @@ class WSLCContainerImpl : public std::enable_shared_from_this _Guarded_by_(m_lock) std::shared_ptr m_transition; - // Non-null while Restart() owns both phases. Start() and Stop() stand down until it completes, and - // OnStopped() keeps the container's runtime resources mapped and skips the auto-delete of an --rm - // container. Delete() does not stand down: a remove that lands between the two phases takes effect, - // and the restart's start phase fails. + // Non-null from before Restart()'s stop phase until its start phase commits Running. Start() and + // Stop() stand down for that window, and OnStopped() keeps the container's runtime resources mapped + // and skips the auto-delete of an --rm container. Delete() does not stand down: a remove that lands + // between the two phases takes effect, and the restart's start phase fails. _Guarded_by_(m_lock) std::shared_ptr m_restart; // True between a successful StartPhase() and the release of the container's ports and mounts. A From 25899fba35f41240eba9772c8c9fc8b447ffba8d Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Fri, 28 Aug 2026 10:15:38 -0700 Subject: [PATCH 11/24] Check OnFailedRestart against starting containers --- src/windows/wslcsession/WSLCContainer.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index c94de21a89..5c2f8de826 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1492,10 +1492,19 @@ void WSLCContainerImpl::OnFailedRestart() noexcept try { { + // N.B. m_lifecycleLock is not needed here: nothing below publishes a transition, and the state + // read is already ordered against OnEvent() by m_lock. auto lock = m_lock.lock_exclusive(); + // The start phase waits for the start event after Docker has accepted the start, so it can throw + // on a container that is coming up. Leave that container alone; it still owns its resources. + if (m_transition || m_state == WslcContainerStateRunning) + { + return; + } + // The stop phase held these back for a start phase that never landed. - if (m_runtimeResourcesHeld && m_state != WslcContainerStateRunning) + if (m_runtimeResourcesHeld) { ReleaseRuntimeResources(); } From dfcb55b7402b08f9864c96daf570ac7632d9a5a9 Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Sun, 30 Aug 2026 22:17:49 -0700 Subject: [PATCH 12/24] Document WSLC_E_CONTAINER_MARKED_FOR_REMOVAL --- doc/docs/api-reference/c/error-codes.md | 2 ++ src/windows/common/wslutil.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/doc/docs/api-reference/c/error-codes.md b/doc/docs/api-reference/c/error-codes.md index 91e2255185..e8db74ec74 100644 --- a/doc/docs/api-reference/c/error-codes.md +++ b/doc/docs/api-reference/c/error-codes.md @@ -18,6 +18,7 @@ #define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */ #define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */ #define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */ +#define WSLC_E_CONTAINER_MARKED_FOR_REMOVAL MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */ ``` | Symbol | Hex Value | @@ -39,5 +40,6 @@ | `WSLC_E_VOLUME_NOT_AVAILABLE` | `0x8004060E` | | `WSLC_E_SESSION_NOT_FOUND` | `0x8004060F` | | `WSLC_E_VM_NOT_RUNNING` | `0x80040610` | +| `WSLC_E_CONTAINER_MARKED_FOR_REMOVAL` | `0x80040611` | --- diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index 5006572138..df233f44d9 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -180,6 +180,7 @@ static const std::map g_commonErrors{ X(WSLC_E_NETWORK_NOT_FOUND), X(WSLC_E_SESSION_NOT_FOUND), X(WSLC_E_VM_NOT_RUNNING), + X(WSLC_E_CONTAINER_MARKED_FOR_REMOVAL), X(WSLC_E_WU_SEARCH_FAILED), X_WIN32(RPC_S_SERVER_UNAVAILABLE), X_WIN32(ERROR_ELEVATION_REQUIRED), From e23e9acabf340c3a218622a022eee209835802d4 Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Sun, 30 Aug 2026 22:31:55 -0700 Subject: [PATCH 13/24] Let kill cut through a stuck restart's stop phase --- src/windows/wslcsession/WSLCContainer.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 5c2f8de826..4081f42d7b 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1333,7 +1333,9 @@ void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool K auto lifecycleLock = m_lifecycleLock.lock_shared(); auto lock = m_lock.lock_exclusive(); - WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop, /* waitForRestart */ !RestartPhase); + // Kill is the escape hatch when a restart's stop phase is stuck, so it must not wait on the very + // restart it is meant to unblock. + WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop, /* waitForRestart */ !RestartPhase && !Kill); transition = m_transition; WI_ASSERT(!transition || transition->Kind == TransitionKind::Stop); From 28dc04c35836d8c8cac34be37ac41e8459f42338 Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Mon, 31 Aug 2026 23:05:40 -0700 Subject: [PATCH 14/24] Fix OnFailedRestart TOCTOU --- src/windows/wslcsession/WSLCContainer.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 4081f42d7b..1273a3e63d 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1493,9 +1493,10 @@ void WSLCContainerImpl::OnFailedRestart() noexcept { try { + std::shared_ptr transition; + { - // N.B. m_lifecycleLock is not needed here: nothing below publishes a transition, and the state - // read is already ordered against OnEvent() by m_lock. + auto lifecycleLock = m_lifecycleLock.lock_shared(); auto lock = m_lock.lock_exclusive(); // The start phase waits for the start event after Docker has accepted the start, so it can throw @@ -1515,9 +1516,15 @@ void WSLCContainerImpl::OnFailedRestart() noexcept { return; } + + // N.B. A Start() released by restartCleanup is already queued on m_lock, so the removal is + // requested in the same scope as the checks above rather than through Delete(), which would + // let that Start() bring the container back up before the force delete lands. + RequestDeleteExclusiveLockHeld(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes); + transition = StartTransition(TransitionKind::Delete, ContainerEvent::Destroy); } - Delete(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes); + AttachToTransition(transition); } CATCH_LOG() } From f52229cbd2532d9ffb32fbfa88b560a37b987dbc Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Tue, 1 Sep 2026 08:02:32 -0700 Subject: [PATCH 15/24] Make the restart race tests fail when they should --- test/windows/WSLCTests.cpp | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 26e798c864..01faee0fd7 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -6941,7 +6941,7 @@ class WSLCTests std::thread restartThread( [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); }); - auto joinThread = wil::scope_exit([&]() { restartThread.join(); }); + auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { restartThread.join(); }); WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker); @@ -6961,19 +6961,24 @@ class WSLCTests std::thread restartThread( [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); }); - auto joinThread = wil::scope_exit([&]() { restartThread.join(); }); + auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { restartThread.join(); }); auto restartFuture = restartResult.get_future(); WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker); // The gap between the two phases is short, so poll for it: until the container has exited, every // delete is turned away by the ordinary running-container guard rather than by the restart. - HRESULT deleteResult = WSLC_E_CONTAINER_IS_RUNNING; - while (deleteResult == WSLC_E_CONTAINER_IS_RUNNING && - restartFuture.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready) - { - deleteResult = container.Get().Delete(WSLCDeleteFlagsNone); - } + const auto deleteResult = wsl::shared::retry::RetryWithTimeout( + [&]() { + const auto result = container.Get().Delete(WSLCDeleteFlagsNone); + THROW_HR_IF( + WSLC_E_CONTAINER_IS_RUNNING, + result == WSLC_E_CONTAINER_IS_RUNNING && + restartFuture.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready); + return result; + }, + std::chrono::milliseconds(100), + std::chrono::seconds(30)); const auto restartHr = restartFuture.get(); @@ -7000,7 +7005,7 @@ class WSLCTests std::thread restartThread( [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); }); - auto joinThread = wil::scope_exit([&]() { restartThread.join(); }); + auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { restartThread.join(); }); WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker); From 87adee1478cf9282cacfa8c2f6d0528522479f3d Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Tue, 1 Sep 2026 08:03:11 -0700 Subject: [PATCH 16/24] Re-align the error code block in the docs --- doc/docs/api-reference/c/error-codes.md | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/doc/docs/api-reference/c/error-codes.md b/doc/docs/api-reference/c/error-codes.md index e8db74ec74..94b9723012 100644 --- a/doc/docs/api-reference/c/error-codes.md +++ b/doc/docs/api-reference/c/error-codes.md @@ -2,22 +2,22 @@ ```c #define WSLC_E_BASE (0x0600) -#define WSLC_E_IMAGE_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 1) /* 0x80040601 */ -#define WSLC_E_CONTAINER_PREFIX_AMBIGUOUS MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 2) /* 0x80040602 */ -#define WSLC_E_CONTAINER_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 3) /* 0x80040603 */ -#define WSLC_E_VOLUME_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 4) /* 0x80040604 */ -#define WSLC_E_CONTAINER_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 5) /* 0x80040605 */ -#define WSLC_E_CONTAINER_IS_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 6) /* 0x80040606 */ -#define WSLC_E_SESSION_RESERVED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 7) /* 0x80040607 */ -#define WSLC_E_INVALID_SESSION_NAME MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 8) /* 0x80040608 */ -#define WSLC_E_NETWORK_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 9) /* 0x80040609 */ -#define WSLC_E_WU_SEARCH_FAILED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 10) /* 0x8004060A */ -#define WSLC_E_SDK_UPDATE_NEEDED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 11) /* 0x8004060B */ -#define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 12) /* 0x8004060C */ -#define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */ -#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */ -#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */ -#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */ +#define WSLC_E_IMAGE_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 1) /* 0x80040601 */ +#define WSLC_E_CONTAINER_PREFIX_AMBIGUOUS MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 2) /* 0x80040602 */ +#define WSLC_E_CONTAINER_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 3) /* 0x80040603 */ +#define WSLC_E_VOLUME_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 4) /* 0x80040604 */ +#define WSLC_E_CONTAINER_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 5) /* 0x80040605 */ +#define WSLC_E_CONTAINER_IS_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 6) /* 0x80040606 */ +#define WSLC_E_SESSION_RESERVED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 7) /* 0x80040607 */ +#define WSLC_E_INVALID_SESSION_NAME MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 8) /* 0x80040608 */ +#define WSLC_E_NETWORK_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 9) /* 0x80040609 */ +#define WSLC_E_WU_SEARCH_FAILED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 10) /* 0x8004060A */ +#define WSLC_E_SDK_UPDATE_NEEDED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 11) /* 0x8004060B */ +#define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 12) /* 0x8004060C */ +#define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */ +#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */ +#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */ +#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */ #define WSLC_E_CONTAINER_MARKED_FOR_REMOVAL MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */ ``` From 008b857cd3e3414f865a4b603c15d6ccb8b2e4cb Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Tue, 1 Sep 2026 09:50:15 -0700 Subject: [PATCH 17/24] Add test that ports and mounts survive a restart --- test/windows/WSLCTests.cpp | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 01faee0fd7..417807e411 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -6924,6 +6924,55 @@ class WSLCTests VERIFY_ARE_EQUAL(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr), RPC_E_DISCONNECTED); } + // Ports and mounts survive a restart: they are held across both phases rather than released and re-acquired. + { + const auto hostFolder = std::filesystem::current_path() / "test-restart-volume"; + std::filesystem::create_directories(hostFolder); + VERIFY_IS_TRUE((std::ofstream(hostFolder / "marker.txt") << "restart-marker").good()); + auto folderCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + std::error_code ec; + std::filesystem::remove_all(hostFolder, ec); + }); + + constexpr uint16_t hostPort = 1252; + const std::string containerPort = "8000/tcp"; + const std::string volumePath = "/data"; + const auto markerUrl = std::format(L"http://127.0.0.1:{}/marker.txt", hostPort); + + WSLCContainerLauncher launcher( + "python:3.12-alpine", + "test-restart-ports-volumes", + {"python3", "-m", "http.server", "8000", "--bind", "0.0.0.0", "--directory", volumePath}, + {"PYTHONUNBUFFERED=1"}, + "bridge"); + launcher.AddPort(hostPort, 8000, AF_INET); + launcher.AddVolume(hostFolder.wstring(), volumePath, true); + + auto container = launcher.Launch(*m_defaultSession); + auto initProcess = container.GetInitProcess(); + WaitForOutput(initProcess.GetStdHandle(1), "Serving HTTP on"); + ExpectHttpResponse(markerUrl.c_str(), 200); + + // A start phase that re-reserved the host port would collide with the container's own reservation. + VERIFY_SUCCEEDED(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr)); + VERIFY_ARE_EQUAL(initProcess.Wait(), WSLCSignalSIGKILL + 128); + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning); + + const auto inspect = container.Inspect(); + VERIFY_IS_TRUE(inspect.Ports.contains(containerPort)); + VERIFY_ARE_EQUAL(inspect.Ports.at(containerPort).size(), 1u); + VERIFY_ARE_EQUAL(std::to_string(hostPort), inspect.Ports.at(containerPort)[0].HostPort); + + VERIFY_ARE_EQUAL(inspect.Mounts.size(), 1u); + VERIFY_ARE_EQUAL(inspect.Mounts[0].Destination, volumePath); + VERIFY_IS_FALSE(inspect.Mounts[0].ReadWrite); + VERIFY_ARE_EQUAL(inspect.Mounts[0].Type, "bind"); + + // The restarted init has to bind again before the held relay has anything to forward to. + wsl::shared::retry::RetryWithTimeout( + [&]() { ExpectHttpResponse(markerUrl.c_str(), 200); }, std::chrono::milliseconds(500), std::chrono::seconds(30)); + } + // An init that ignores SIGTERM keeps the restart's stop phase in flight until the timeout expires, // which is what gives the requests below a window to land in the middle of a restart. const std::vector ignoreStopSignal = { From 6ef0658c210e9893139692f4bdf3141ad894182b Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Tue, 1 Sep 2026 10:32:38 -0700 Subject: [PATCH 18/24] Report a force delete that lands during the restart's stop phase --- src/windows/wslcsession/WSLCContainer.cpp | 12 ++++++++++++ src/windows/wslcsession/WSLCContainer.h | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 1273a3e63d..d81ba14c9c 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1337,6 +1337,10 @@ void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool K // restart it is meant to unblock. WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop, /* waitForRestart */ !RestartPhase && !Kill); + // A Delete() that raced a restart may have already moved the container to the Deleted state. + THROW_HR_WITH_USER_ERROR_IF( + WSLC_E_CONTAINER_MARKED_FOR_REMOVAL, Localization::MessageWslcContainerMarkedForRemoval(m_id), m_state == WslcContainerStateDeleted); + transition = m_transition; WI_ASSERT(!transition || transition->Kind == TransitionKind::Stop); @@ -1401,6 +1405,14 @@ void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool K // HTTP 304 is returned when the container is already stopped. if (Kill || e.StatusCode() != 304) { + lock = m_lock.lock_exclusive(); + + // A force delete can win the locks released above, so the container may be gone rather than stuck. + THROW_HR_WITH_USER_ERROR_IF( + WSLC_E_CONTAINER_MARKED_FOR_REMOVAL, + Localization::MessageWslcContainerMarkedForRemoval(m_id), + m_state == WslcContainerStateDeleted || (m_transition && m_transition->ExpectedEvent == ContainerEvent::Destroy)); + THROW_DOCKER_USER_ERROR_MSG(e, "Failed to %hs container '%hs'", Kill ? "kill" : "stop", m_id.c_str()); } } diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 3e917ecf26..b001ea3194 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -260,7 +260,7 @@ class WSLCContainerImpl : public std::enable_shared_from_this // True between a successful StartPhase() and the release of the container's ports and mounts. A // restart leaves this set across the two phases, which is what tells the start phase they are still // held and must not be re-acquired. - _Guarded_by_(m_lock) bool m_runtimeResourcesHeld {}; + _Guarded_by_(m_lock) bool m_runtimeResourcesHeld = false; // The container outlives any single VM: it survives idle-termination and is reused when the VM // restarts. VM-scoped resources (Vm(), Docker(), Volumes(), Events(), Relay()) are therefore From 86203a13b9105681ea06a154c71ff7ac6da4a69c Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Tue, 1 Sep 2026 11:12:08 -0700 Subject: [PATCH 19/24] Cover kill and force delete during a restart --- test/windows/WSLCTests.cpp | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 417807e411..d90c32a818 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -6999,6 +6999,26 @@ class WSLCTests VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited); } + // A kill issued during a restart deliberately does not wait for it: it is what unblocks a stop phase + // that an init like this one would otherwise keep in flight for the whole timeout. + { + WSLCContainerLauncher launcher("debian:latest", "test-restart-race-kill", ignoreStopSignal); + auto container = launcher.Launch(*m_defaultSession); + auto initProcess = container.GetInitProcess(); + + std::promise restartResult; + std::thread restartThread( + [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); }); + + auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { restartThread.join(); }); + + WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker); + + VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGKILL)); + VERIFY_SUCCEEDED(restartResult.get_future().get()); + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning); + } + // A delete issued during a restart deliberately does not wait for it, matching docker: whichever of // the delete and the restart's start phase lands first wins, and the other one fails. { @@ -7043,6 +7063,26 @@ class WSLCTests } } + // A force delete is not turned away by the running-container guard, so unlike the delete above it does + // not have to wait for the gap between the phases: it lands while the stop phase is still in flight. + { + WSLCContainerLauncher launcher("debian:latest", "test-restart-race-force-delete", ignoreStopSignal); + auto container = launcher.Launch(*m_defaultSession); + auto initProcess = container.GetInitProcess(); + + std::promise restartResult; + std::thread restartThread( + [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); }); + + auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { restartThread.join(); }); + + WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker); + + VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsForce)); + container.SetDeleteOnClose(false); + VERIFY_ARE_EQUAL(restartResult.get_future().get(), WSLC_E_CONTAINER_MARKED_FOR_REMOVAL); + } + // A restart issued during a restart waits for both of the first one's phases, so the two pairs // cannot interleave and the container is left running. { From 0a000fece55e6ef0e0e9ba40f0abcf5fd9454929 Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Tue, 1 Sep 2026 11:29:15 -0700 Subject: [PATCH 20/24] Add comment to Restart() --- src/windows/wslcsession/WSLCContainer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index d81ba14c9c..46d2b9212a 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1471,6 +1471,8 @@ void WSLCContainerImpl::Restart(WSLCSignal Signal, LONG TimeoutSeconds) m_restart = restart; } + // N.B. Nothing between here and restartCleanup below may throw — nothing clears m_restart until it + // is armed, and it must follow failureCleanup so it runs first (see the N.B. on OnFailedRestart). auto failureCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { OnFailedRestart(); }); { From ee0bf3aef9a608ac384a5ebc7bc4548ecf177862 Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Tue, 1 Sep 2026 15:07:40 -0700 Subject: [PATCH 21/24] Explain Kill's restart wait-skip and test restart under an unavailable volume --- src/windows/wslcsession/WSLCContainer.cpp | 3 ++- test/windows/WSLCTests.cpp | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 46d2b9212a..95b0fe9c3f 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1334,7 +1334,8 @@ void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool K auto lock = m_lock.lock_exclusive(); // Kill is the escape hatch when a restart's stop phase is stuck, so it must not wait on the very - // restart it is meant to unblock. + // restart it is meant to unblock. Landing between the phases finds the container exited, which is + // turned away below like any other kill of a stopped container. WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop, /* waitForRestart */ !RestartPhase && !Kill); // A Delete() that raced a restart may have already moved the container to the Deleted state. diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index d90c32a818..97d461bd25 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -4981,6 +4981,10 @@ class WSLCTests VERIFY_ARE_EQUAL(recoveredContainer->Start(WSLCContainerStartFlagsNone, nullptr, nullptr), WSLC_E_VOLUME_NOT_AVAILABLE); ValidateCOMErrorMessageContains(wsl::shared::string::MultiByteToWide(volumeName)); + // The container is not running, so the restart is only its start phase and is refused the same way. + VERIFY_ARE_EQUAL(recoveredContainer->Restart(WSLCSignalSIGTERM, 0, nullptr), WSLC_E_VOLUME_NOT_AVAILABLE); + ValidateCOMErrorMessageContains(wsl::shared::string::MultiByteToWide(volumeName)); + // Inspecting the volume reports the failure via an "Error" entry in its status. { wil::unique_cotaskmem_ansistring inspectOutput; From 341f3a4873bf46a734252977d752d3859398ff02 Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Tue, 1 Sep 2026 15:39:56 -0700 Subject: [PATCH 22/24] Rename WSLC_E_CONTAINER_MARKED_FOR_REMOVAL to WSLC_E_CONTAINER_DELETED --- doc/docs/api-reference/c/error-codes.md | 36 +++++++++++------------ localization/strings/en-US/Resources.resw | 4 +-- src/windows/WslcSDK/winrt/wslcsdk.idl | 1 + src/windows/WslcSDK/wslcsdk.h | 34 ++++++++++----------- src/windows/common/wslutil.cpp | 2 +- src/windows/service/inc/wslc.idl | 2 +- src/windows/wslcsession/WSLCContainer.cpp | 10 +++---- test/windows/WSLCTests.cpp | 4 +-- 8 files changed, 46 insertions(+), 47 deletions(-) diff --git a/doc/docs/api-reference/c/error-codes.md b/doc/docs/api-reference/c/error-codes.md index 94b9723012..5cfb28dc4b 100644 --- a/doc/docs/api-reference/c/error-codes.md +++ b/doc/docs/api-reference/c/error-codes.md @@ -2,23 +2,23 @@ ```c #define WSLC_E_BASE (0x0600) -#define WSLC_E_IMAGE_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 1) /* 0x80040601 */ -#define WSLC_E_CONTAINER_PREFIX_AMBIGUOUS MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 2) /* 0x80040602 */ -#define WSLC_E_CONTAINER_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 3) /* 0x80040603 */ -#define WSLC_E_VOLUME_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 4) /* 0x80040604 */ -#define WSLC_E_CONTAINER_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 5) /* 0x80040605 */ -#define WSLC_E_CONTAINER_IS_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 6) /* 0x80040606 */ -#define WSLC_E_SESSION_RESERVED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 7) /* 0x80040607 */ -#define WSLC_E_INVALID_SESSION_NAME MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 8) /* 0x80040608 */ -#define WSLC_E_NETWORK_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 9) /* 0x80040609 */ -#define WSLC_E_WU_SEARCH_FAILED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 10) /* 0x8004060A */ -#define WSLC_E_SDK_UPDATE_NEEDED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 11) /* 0x8004060B */ -#define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 12) /* 0x8004060C */ -#define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */ -#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */ -#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */ -#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */ -#define WSLC_E_CONTAINER_MARKED_FOR_REMOVAL MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */ +#define WSLC_E_IMAGE_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 1) /* 0x80040601 */ +#define WSLC_E_CONTAINER_PREFIX_AMBIGUOUS MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 2) /* 0x80040602 */ +#define WSLC_E_CONTAINER_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 3) /* 0x80040603 */ +#define WSLC_E_VOLUME_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 4) /* 0x80040604 */ +#define WSLC_E_CONTAINER_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 5) /* 0x80040605 */ +#define WSLC_E_CONTAINER_IS_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 6) /* 0x80040606 */ +#define WSLC_E_SESSION_RESERVED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 7) /* 0x80040607 */ +#define WSLC_E_INVALID_SESSION_NAME MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 8) /* 0x80040608 */ +#define WSLC_E_NETWORK_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 9) /* 0x80040609 */ +#define WSLC_E_WU_SEARCH_FAILED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 10) /* 0x8004060A */ +#define WSLC_E_SDK_UPDATE_NEEDED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 11) /* 0x8004060B */ +#define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 12) /* 0x8004060C */ +#define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */ +#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */ +#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */ +#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */ +#define WSLC_E_CONTAINER_DELETED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */ ``` | Symbol | Hex Value | @@ -40,6 +40,6 @@ | `WSLC_E_VOLUME_NOT_AVAILABLE` | `0x8004060E` | | `WSLC_E_SESSION_NOT_FOUND` | `0x8004060F` | | `WSLC_E_VM_NOT_RUNNING` | `0x80040610` | -| `WSLC_E_CONTAINER_MARKED_FOR_REMOVAL` | `0x80040611` | +| `WSLC_E_CONTAINER_DELETED` | `0x80040611` | --- diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 664255b81c..a17bea7e97 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2195,8 +2195,8 @@ Usage: Container '{}' not found. {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated - - Container '{}' is marked for removal and cannot be started. + + Container '{}' has been deleted. {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/src/windows/WslcSDK/winrt/wslcsdk.idl b/src/windows/WslcSDK/winrt/wslcsdk.idl index 8b96089d89..7c461c177e 100644 --- a/src/windows/WslcSDK/winrt/wslcsdk.idl +++ b/src/windows/WslcSDK/winrt/wslcsdk.idl @@ -381,5 +381,6 @@ namespace Microsoft.WSL.Containers ContainerDisabled = 0x8004060C, RegistryBlockedByPolicy = 0x8004060D, VolumeNotAvailable = 0x8004060E, + ContainerDeleted = 0x80040611, }; } diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h index cf8ef8b240..2addf59bf3 100644 --- a/src/windows/WslcSDK/wslcsdk.h +++ b/src/windows/WslcSDK/wslcsdk.h @@ -28,23 +28,23 @@ EXTERN_C_START // WSLC specific error codes // Ensure wslc.idl and wslcsdk.idl are also updated. #define WSLC_E_BASE (0x0600) -#define WSLC_E_IMAGE_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 1) /* 0x80040601 */ -#define WSLC_E_CONTAINER_PREFIX_AMBIGUOUS MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 2) /* 0x80040602 */ -#define WSLC_E_CONTAINER_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 3) /* 0x80040603 */ -#define WSLC_E_VOLUME_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 4) /* 0x80040604 */ -#define WSLC_E_CONTAINER_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 5) /* 0x80040605 */ -#define WSLC_E_CONTAINER_IS_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 6) /* 0x80040606 */ -#define WSLC_E_SESSION_RESERVED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 7) /* 0x80040607 */ -#define WSLC_E_INVALID_SESSION_NAME MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 8) /* 0x80040608 */ -#define WSLC_E_NETWORK_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 9) /* 0x80040609 */ -#define WSLC_E_WU_SEARCH_FAILED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 10) /* 0x8004060A */ -#define WSLC_E_SDK_UPDATE_NEEDED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 11) /* 0x8004060B */ -#define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 12) /* 0x8004060C */ -#define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */ -#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */ -#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */ -#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */ -#define WSLC_E_CONTAINER_MARKED_FOR_REMOVAL MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */ +#define WSLC_E_IMAGE_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 1) /* 0x80040601 */ +#define WSLC_E_CONTAINER_PREFIX_AMBIGUOUS MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 2) /* 0x80040602 */ +#define WSLC_E_CONTAINER_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 3) /* 0x80040603 */ +#define WSLC_E_VOLUME_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 4) /* 0x80040604 */ +#define WSLC_E_CONTAINER_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 5) /* 0x80040605 */ +#define WSLC_E_CONTAINER_IS_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 6) /* 0x80040606 */ +#define WSLC_E_SESSION_RESERVED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 7) /* 0x80040607 */ +#define WSLC_E_INVALID_SESSION_NAME MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 8) /* 0x80040608 */ +#define WSLC_E_NETWORK_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 9) /* 0x80040609 */ +#define WSLC_E_WU_SEARCH_FAILED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 10) /* 0x8004060A */ +#define WSLC_E_SDK_UPDATE_NEEDED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 11) /* 0x8004060B */ +#define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 12) /* 0x8004060C */ +#define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */ +#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */ +#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */ +#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */ +#define WSLC_E_CONTAINER_DELETED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */ // Session values #define WSLC_SESSION_OPTIONS_SIZE 72 diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index df233f44d9..0c82a0bdcf 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -180,7 +180,7 @@ static const std::map g_commonErrors{ X(WSLC_E_NETWORK_NOT_FOUND), X(WSLC_E_SESSION_NOT_FOUND), X(WSLC_E_VM_NOT_RUNNING), - X(WSLC_E_CONTAINER_MARKED_FOR_REMOVAL), + X(WSLC_E_CONTAINER_DELETED), X(WSLC_E_WU_SEARCH_FAILED), X_WIN32(RPC_S_SERVER_UNAVAILABLE), X_WIN32(ERROR_ELEVATION_REQUIRED), diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 972c281b93..1e10d33114 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -878,4 +878,4 @@ cpp_quote("#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILIT // N.B. WSLC_E_VM_NOT_RUNNING is part of the plugin API contract and is also defined in WslPluginApi.h. // The two definitions must stay in sync. cpp_quote("#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */") -cpp_quote("#define WSLC_E_CONTAINER_MARKED_FOR_REMOVAL MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */") +cpp_quote("#define WSLC_E_CONTAINER_DELETED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */") diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 95b0fe9c3f..4b3b6d874f 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1034,8 +1034,7 @@ void WSLCContainerImpl::StartPhase(WSLCContainerStartFlags Flags, const WSLCProc WaitForConflictingTransitionToComplete(lock, lifecycleLock, std::nullopt, /* waitForRestart */ !RestartPhase); // A Delete() that raced a restart may have already moved the container to the Deleted state. - THROW_HR_WITH_USER_ERROR_IF( - WSLC_E_CONTAINER_MARKED_FOR_REMOVAL, Localization::MessageWslcContainerMarkedForRemoval(m_id), m_state == WslcContainerStateDeleted); + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_DELETED, Localization::MessageWslcContainerDeleted(m_id), m_state == WslcContainerStateDeleted); THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_IS_RUNNING, Localization::MessageWslcContainerIsRunning(m_id), m_state == WslcContainerStateRunning); @@ -1339,8 +1338,7 @@ void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool K WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop, /* waitForRestart */ !RestartPhase && !Kill); // A Delete() that raced a restart may have already moved the container to the Deleted state. - THROW_HR_WITH_USER_ERROR_IF( - WSLC_E_CONTAINER_MARKED_FOR_REMOVAL, Localization::MessageWslcContainerMarkedForRemoval(m_id), m_state == WslcContainerStateDeleted); + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_DELETED, Localization::MessageWslcContainerDeleted(m_id), m_state == WslcContainerStateDeleted); transition = m_transition; WI_ASSERT(!transition || transition->Kind == TransitionKind::Stop); @@ -1410,8 +1408,8 @@ void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool K // A force delete can win the locks released above, so the container may be gone rather than stuck. THROW_HR_WITH_USER_ERROR_IF( - WSLC_E_CONTAINER_MARKED_FOR_REMOVAL, - Localization::MessageWslcContainerMarkedForRemoval(m_id), + WSLC_E_CONTAINER_DELETED, + Localization::MessageWslcContainerDeleted(m_id), m_state == WslcContainerStateDeleted || (m_transition && m_transition->ExpectedEvent == ContainerEvent::Destroy)); THROW_DOCKER_USER_ERROR_MSG(e, "Failed to %hs container '%hs'", Kill ? "kill" : "stop", m_id.c_str()); diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 97d461bd25..09a84f99db 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -7057,7 +7057,7 @@ class WSLCTests if (SUCCEEDED(deleteResult)) { - VERIFY_ARE_EQUAL(restartHr, WSLC_E_CONTAINER_MARKED_FOR_REMOVAL); + VERIFY_ARE_EQUAL(restartHr, WSLC_E_CONTAINER_DELETED); } else { @@ -7084,7 +7084,7 @@ class WSLCTests VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsForce)); container.SetDeleteOnClose(false); - VERIFY_ARE_EQUAL(restartResult.get_future().get(), WSLC_E_CONTAINER_MARKED_FOR_REMOVAL); + VERIFY_ARE_EQUAL(restartResult.get_future().get(), WSLC_E_CONTAINER_DELETED); } // A restart issued during a restart waits for both of the first one's phases, so the two pairs From 70a383183d8e5722f47a34c7011f9a2d14c1390f Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Wed, 2 Sep 2026 12:50:12 -0700 Subject: [PATCH 23/24] Fix comment --- src/windows/wslcsession/WSLCContainer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index b001ea3194..d830672962 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -188,8 +188,8 @@ class WSLCContainerImpl : public std::enable_shared_from_this __requires_exclusive_lock_held(m_lock) std::shared_ptr StartTransition(TransitionKind kind, ContainerEvent expectedEvent); - // Returns with both locks held when no restart is in flight and no transition is active, or the - // active transition matches kind. Both conditions are re-checked every time the locks come back. + // Returns with both locks held when no transition is active (or it matches kind) and, if waitForRestart, + // no restart is in flight either. Both conditions are re-checked every time the locks come back. void WaitForConflictingTransitionToComplete( wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock, From 063d9ff8c6dd3e95ea02f3fc3bc765e377d9e4d1 Mon Sep 17 00:00:00 2001 From: Beena Chauhan Date: Wed, 2 Sep 2026 15:23:18 -0700 Subject: [PATCH 24/24] Publish failed restart cleanup before releasing waiters --- src/windows/wslcsession/WSLCContainer.cpp | 101 ++++++++++------------ src/windows/wslcsession/WSLCContainer.h | 4 +- 2 files changed, 50 insertions(+), 55 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 8294fa4f1b..d3f847d5ef 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1469,76 +1469,71 @@ void WSLCContainerImpl::Restart(WSLCSignal Signal, LONG TimeoutSeconds) m_restart = restart; } - // N.B. Nothing between here and restartCleanup below may throw — nothing clears m_restart until it - // is armed, and it must follow failureCleanup so it runs first (see the N.B. on OnFailedRestart). - auto failureCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { OnFailedRestart(); }); + // N.B. Nothing between here and the cleanup below may throw — nothing clears m_restart until it is armed. + bool succeeded = false; + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, restart, &succeeded]() { + // N.B. Signalled last so a waiter cannot observe the restart as complete before the failure + // cleanup below has published its delete. + auto release = wil::scope_exit([&restart]() { restart->Completed.SetEvent(); }); - { - auto restartCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, restart]() { - { - auto lock = m_lock.lock_exclusive(); + std::shared_ptr transition; - // CommitState() clears this once the start phase lands, so a later restart may already own it. - if (m_restart == restart) - { - m_restart.reset(); - } + { + auto lifecycleLock = m_lifecycleLock.lock_shared(); + auto lock = m_lock.lock_exclusive(); + + // CommitState() clears this once the start phase lands, so a later restart may already own it. + if (m_restart == restart) + { + m_restart.reset(); } - restart->Completed.SetEvent(); - }); + if (!succeeded) + { + transition = OnFailedRestartExclusiveLockHeld(); + } + } - if (wasRunning) + if (transition) { - StopPhase(Signal, TimeoutSeconds, /* Kill */ false, /* RestartPhase */ true); + AttachToTransition(transition); } + }); - StartPhase(WSLCContainerStartFlagsNone, nullptr, /* RestartPhase */ true); + if (wasRunning) + { + StopPhase(Signal, TimeoutSeconds, /* Kill */ false, /* RestartPhase */ true); } - failureCleanup.release(); + StartPhase(WSLCContainerStartFlagsNone, nullptr, /* RestartPhase */ true); + succeeded = true; } -// N.B. Runs after the restart transaction has been released, so the delete below is no longer -// suppressed by OnStopped(). -void WSLCContainerImpl::OnFailedRestart() noexcept +// N.B. Runs with m_restart already cleared, so the delete below is no longer suppressed by OnStopped(). +__requires_exclusive_lock_held(m_lock) std::shared_ptr WSLCContainerImpl::OnFailedRestartExclusiveLockHeld() { - try + // The start phase waits for the start event after Docker has accepted the start, so it can throw + // on a container that is coming up. Leave that container alone; it still owns its resources. + if (m_transition || m_state == WslcContainerStateRunning) { - std::shared_ptr transition; - - { - auto lifecycleLock = m_lifecycleLock.lock_shared(); - auto lock = m_lock.lock_exclusive(); - - // The start phase waits for the start event after Docker has accepted the start, so it can throw - // on a container that is coming up. Leave that container alone; it still owns its resources. - if (m_transition || m_state == WslcContainerStateRunning) - { - return; - } - - // The stop phase held these back for a start phase that never landed. - if (m_runtimeResourcesHeld) - { - ReleaseRuntimeResources(); - } - - if (WI_IsFlagClear(m_containerFlags, WSLCContainerFlagsRm) || m_state != WslcContainerStateExited) - { - return; - } + return nullptr; + } - // N.B. A Start() released by restartCleanup is already queued on m_lock, so the removal is - // requested in the same scope as the checks above rather than through Delete(), which would - // let that Start() bring the container back up before the force delete lands. - RequestDeleteExclusiveLockHeld(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes); - transition = StartTransition(TransitionKind::Delete, ContainerEvent::Destroy); - } + // The stop phase held these back for a start phase that never landed. + if (m_runtimeResourcesHeld) + { + ReleaseRuntimeResources(); + } - AttachToTransition(transition); + if (WI_IsFlagClear(m_containerFlags, WSLCContainerFlagsRm) || m_state != WslcContainerStateExited) + { + return nullptr; } - CATCH_LOG() + + // N.B. Requested here rather than through Delete() so the removal shares the scope that clears + // m_restart, which is what stops a released Start() from bringing the container back up first. + RequestDeleteExclusiveLockHeld(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes); + return StartTransition(TransitionKind::Delete, ContainerEvent::Destroy); } __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exitCode, std::optional stopTimestamp) diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index d830672962..67881be76d 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -202,8 +202,8 @@ class WSLCContainerImpl : public std::enable_shared_from_this void StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill, bool RestartPhase); // Undoes what the phases left half-done: releases the resources the stop phase held back and - // performs the auto-delete OnStopped() deferred. - void OnFailedRestart() noexcept; + // requests the auto-delete OnStopped() deferred, returning that delete's transition. + __requires_exclusive_lock_held(m_lock) std::shared_ptr OnFailedRestartExclusiveLockHeld(); void WaitForCompletionEvent(HANDLE Event) const; void WaitForTransitionCompletion(const std::shared_ptr& transition) const;