From 9db9e5979eab6e50e10e2abfa15d8d2c02902edf Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 23 Jun 2026 10:25:36 -0700 Subject: [PATCH 01/31] wslc: idle-terminate per-user container VMs and lazily restart Per-user WSLC container session VMs now idle-terminate when no container is in a non-terminal (Created/Running) state, freeing host memory, and lazily restart on the next operation that needs the VM. - Centralize VM lifecycle in WSLCSession via TearDownVmLockHeld / StartVmLockHeld and an atomic VmExitDisposition (Active / StopRequested / ExitClaimed) to arbitrate expected stops vs. spontaneous VM exits without a polling thread. - Gate VM-requiring entrypoints behind AcquireVmLease(), which brings the VM up on demand and keeps it alive for the operation's duration. - Add IWSLCSession::BeginContainerOperation so a CLI command can hold the VM alive across resolve + operate + streamed output. - Preserve the session WarningCallback for the lifetime of the session so warnings emitted by the lazy VM start (e.g. resource recovery) are still delivered to the CLI invocation. - Remove the dtor lock in HcsVirtualMachine; OnExit/OnCrash are lock-free. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/service/exe/HcsVirtualMachine.cpp | 4 +- .../service/exe/WSLCSessionManager.cpp | 3 +- src/windows/service/inc/wslc.idl | 8 + .../wslc/services/ContainerService.cpp | 11 + src/windows/wslc/services/SessionModel.h | 18 +- src/windows/wslc/services/SessionService.cpp | 5 +- src/windows/wslcsession/IORelay.cpp | 9 + src/windows/wslcsession/IORelay.h | 6 + src/windows/wslcsession/WSLCContainer.cpp | 66 ++ src/windows/wslcsession/WSLCContainer.h | 10 + .../wslcsession/WSLCExecutionContext.h | 16 +- src/windows/wslcsession/WSLCIdleState.h | 226 +++++ src/windows/wslcsession/WSLCProcess.h | 10 + .../wslcsession/WSLCProcessControl.cpp | 10 +- src/windows/wslcsession/WSLCSession.cpp | 779 ++++++++++++++---- src/windows/wslcsession/WSLCSession.h | 137 ++- test/windows/WSLCTests.cpp | 16 + .../wslc/e2e/WSLCE2ESessionEnterTests.cpp | 9 +- 18 files changed, 1177 insertions(+), 166 deletions(-) create mode 100644 src/windows/wslcsession/WSLCIdleState.h diff --git a/src/windows/service/exe/HcsVirtualMachine.cpp b/src/windows/service/exe/HcsVirtualMachine.cpp index 17fd76a633..5c95870009 100644 --- a/src/windows/service/exe/HcsVirtualMachine.cpp +++ b/src/windows/service/exe/HcsVirtualMachine.cpp @@ -351,7 +351,9 @@ HcsVirtualMachine::HcsVirtualMachine(_In_ const WSLCSessionSettings* Settings) HcsVirtualMachine::~HcsVirtualMachine() { - std::lock_guard lock(m_lock); + // Do not hold m_lock: waiting on m_vmExitEvent and closing the compute system below both block + // on in-flight HCS exit/crash callbacks, which may themselves need m_lock. OnExit() is lock-free, + // and closing the compute system drains all callbacks, so the rest of teardown needs no lock. // Wait up to 5 seconds for the VM to terminate gracefully. bool forceTerminate = false; diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index e5e0540e98..fcd300a977 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -285,8 +285,7 @@ void WSLCSessionManagerImpl::CreateSession( g_pluginManager, sessionId, creatorPid, std::wstring(resolvedDisplayName), wil::shared_handle(sharedToken), std::vector(storedSid)); // Create the VM factory in the SYSTEM service (privileged). The per-user session - // uses it to create the VM. Funneling VM creation through a factory lets the session - // own when VMs are created, rather than having one handed to it up front. + // uses it to create VMs on demand and recreate them after idle-termination. auto vmFactory = Microsoft::WRL::Make(Settings); // Launch per-user COM server factory and add it to a fresh per-session job object for crash cleanup. diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 7639c45177..5d798f44ff 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -633,6 +633,14 @@ interface IWSLCSession : IUnknown // Container management. HRESULT CreateContainer([in] const WSLCContainerOptions* Options, [in, unique] IWarningCallback* WarningCallback, [out] IWSLCContainer** Container); HRESULT OpenContainer([in, ref] LPCSTR Id, [out] IWSLCContainer** Container); + + // Keeps the VM alive for the duration of a client-side container operation. The CLI performs + // each mutation as two round-trips (OpenContainer followed by the operation) and may stream + // output afterwards. With on-demand VM idle-termination the VM could otherwise tear down + // between those calls, disconnecting the container wrapper and failing the second call with + // RPC_E_DISCONNECTED. The client holds the returned token for the whole operation; releasing + // it (or the client exiting) lets the VM idle-terminate again. + HRESULT BeginContainerOperation([out] IUnknown** Operation); HRESULT ListContainers([in, unique] const WSLCListContainersOptions* Options,[out, size_is(, *Count)] WSLCContainerEntry** Containers,[out] ULONG* Count, [out, size_is(, *PortsCount)] WSLCContainerPortMapping** Ports, [out] ULONG* PortsCount); HRESULT PruneContainers([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out] WSLCPruneContainersResults* Result); diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 9a85ebf988..3c46a25787 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -296,6 +296,7 @@ std::wstring ContainerService::FormatRelativeTime(ULONGLONG timestamp) int ContainerService::Attach(Session& session, const std::string& id) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -457,6 +458,7 @@ CreateContainerResult ContainerService::Create(Session& session, const std::stri int ContainerService::Start(Session& session, const std::string& id, bool attach) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); WSLCContainerStartFlags flags = attach ? WSLCContainerStartFlagsAttach : WSLCContainerStartFlagsNone; @@ -487,6 +489,7 @@ int ContainerService::Start(Session& session, const std::string& id, bool attach void ContainerService::Stop(Session& session, const std::string& id, StopContainerOptions options) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); THROW_IF_FAILED_EXCEPT(container->Stop(options.Signal, options.Timeout), WSLC_E_CONTAINER_NOT_RUNNING); @@ -494,6 +497,7 @@ void ContainerService::Stop(Session& session, const std::string& id, StopContain void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal signal) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); THROW_IF_FAILED(container->Kill(signal)); @@ -501,6 +505,7 @@ void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal void ContainerService::Delete(Session& session, const std::string& id, bool force) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); THROW_IF_FAILED(container->Delete(force ? WSLCDeleteFlagsForce : WSLCDeleteFlagsNone)); @@ -555,6 +560,7 @@ std::vector ContainerService::List( int ContainerService::Exec(Session& session, const std::string& id, ContainerOptions options) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -586,6 +592,7 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt InspectContainer ContainerService::Inspect(Session& session, const std::string& id) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); wil::unique_cotaskmem_ansistring output; @@ -604,6 +611,8 @@ void ContainerService::Export(Session& session, const std::string& id, const std void ContainerService::Export(Session& session, const std::string& id, HANDLE outputHandle) { + auto operation = session.BeginContainerOperation(); + wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -615,6 +624,7 @@ void ContainerService::Export(Session& session, const std::string& id, HANDLE ou void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -642,6 +652,7 @@ void ContainerService::Logs(Session& session, const std::string& id, bool follow wsl::windows::common::docker_schema::ContainerStats ContainerService::Stats(Session& session, const std::string& id) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); wil::unique_cotaskmem_ansistring output; diff --git a/src/windows/wslc/services/SessionModel.h b/src/windows/wslc/services/SessionModel.h index ab6b5824ed..491df6fc3b 100644 --- a/src/windows/wslc/services/SessionModel.h +++ b/src/windows/wslc/services/SessionModel.h @@ -22,7 +22,8 @@ struct Session NON_COPYABLE(Session); DEFAULT_MOVABLE(Session); - explicit Session(wil::com_ptr session) : m_session(std::move(session)) + explicit Session(wil::com_ptr session, wil::com_ptr warningCallback = {}) : + m_session(std::move(session)), m_warningCallback(std::move(warningCallback)) { } @@ -31,8 +32,23 @@ struct Session return m_session.get(); } + // Acquires an activity token that keeps the VM alive for the duration of a client-side + // container operation (resolve + operate, plus any streamed output). Hold the returned + // pointer for the whole operation; releasing it lets the VM idle-terminate again. + [[nodiscard]] wil::com_ptr BeginContainerOperation() const + { + wil::com_ptr operation; + THROW_IF_FAILED(m_session->BeginContainerOperation(&operation)); + return operation; + } + private: wil::com_ptr m_session; + + // Kept alive for the lifetime of the session model (i.e. the whole CLI command) so the service + // can deliver warnings emitted by lazy/background work — such as resource recovery on the first + // VM start — back to this CLI invocation, even though no single COM call carries the callback. + wil::com_ptr m_warningCallback; }; } // namespace wsl::windows::wslc::models \ No newline at end of file diff --git a/src/windows/wslc/services/SessionService.cpp b/src/windows/wslc/services/SessionService.cpp index 86ebcebbe7..ff8008a22f 100644 --- a/src/windows/wslc/services/SessionService.cpp +++ b/src/windows/wslc/services/SessionService.cpp @@ -61,7 +61,10 @@ Session SessionService::OpenOrCreateDefaultSession() auto warningCallback = Microsoft::WRL::Make(); THROW_IF_FAILED(manager->CreateSession(nullptr, WSLCSessionFlagsNone, warningCallback.Get(), &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); - return Session(std::move(session)); + + // Hold the warning callback for the session's lifetime so warnings emitted by a lazy VM start + // (e.g. resource recovery) are still delivered to this CLI invocation. + return Session(std::move(session), wil::com_ptr(warningCallback.Get())); } int SessionService::Attach(const Session& session) diff --git a/src/windows/wslcsession/IORelay.cpp b/src/windows/wslcsession/IORelay.cpp index 6677bca87a..09f5e05cd4 100644 --- a/src/windows/wslcsession/IORelay.cpp +++ b/src/windows/wslcsession/IORelay.cpp @@ -68,11 +68,20 @@ void IORelay::Stop() } } +bool IORelay::IsRelayThread() const noexcept +{ + return m_thread.get_id() == std::this_thread::get_id(); +} + void IORelay::Run() try { common::wslutil::SetThreadDescription(L"IORelay"); + // Handle callbacks dispatched from this thread (e.g. unexpected VM exit) can tear the VM down, + // releasing cross-process COM proxies, so join the process MTA to avoid RPC_E_WRONG_THREAD. + const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); + windows::common::io::MultiHandleWait io; // N.B. All the IO must happen on the thread. diff --git a/src/windows/wslcsession/IORelay.h b/src/windows/wslcsession/IORelay.h index 879d3fee13..844c16dee6 100644 --- a/src/windows/wslcsession/IORelay.h +++ b/src/windows/wslcsession/IORelay.h @@ -30,6 +30,12 @@ class IORelay void Stop(); + // Returns true if the calling thread is the IORelay's own worker thread (i.e. the call + // is being made from a handle callback). Destroying the IORelay from this thread would + // join the thread with itself and call std::terminate(), so callers that may run on the + // relay thread must check this before destroying the object. + bool IsRelayThread() const noexcept; + private: void Start(); void Run(); diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 674b0b0cd9..c5a74906f9 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -570,6 +570,12 @@ WSLCContainerImpl::WSLCContainerImpl( m_initProcessFlags(InitProcessFlags), m_containerFlags(ContainerFlags) { + // Acquire the activity hold up front for containers recovered or created in an active state, so + // a recovered running container keeps the VM alive even before any client opens its wrapper. + if (m_state == WslcContainerStateCreated || m_state == WslcContainerStateRunning) + { + m_activityHold = ActivityRef(m_wslcSession.IdleStateShared()); + } } WSLCContainerImpl::~WSLCContainerImpl() @@ -1276,6 +1282,12 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProces } while (!control->GetExitEvent().wait(100)); auto process = wil::MakeOrThrow(std::move(control), std::move(io), Options->Flags); + + // The exec'd process wrapper is handed to the client and is not retained internally, so its + // lifetime tracks the client's proxy. Bind a keep-alive token to it so the idle worker does + // not tear the VM down (killing the process) while the client still holds the proxy. + process->SetKeepAliveToken(m_wslcSession.CreateActivityToken()); + THROW_IF_FAILED(process.CopyTo(__uuidof(IWSLCProcess), (void**)Process)); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to exec process in container %hs", m_id.c_str()); @@ -2177,6 +2189,24 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerSta m_state = State; m_stateChangedAt = stateChangedAt.value_or(static_cast(std::time(nullptr))); + + // Keep the VM alive while this container is Created/Running and release the hold once it reaches + // a terminal state, even when no client holds the wrapper (e.g. a detached `run -d` container). + // Dropping the hold on the transition to Exited is what lets an otherwise-idle VM be torn down. + UpdateActivityHoldLockHeld(); +} + +__requires_lock_held(m_lock) void WSLCContainerImpl::UpdateActivityHoldLockHeld() noexcept +{ + const bool active = (m_state == WslcContainerStateCreated || m_state == WslcContainerStateRunning); + if (active && !m_activityHold) + { + m_activityHold = ActivityRef(m_wslcSession.IdleStateShared()); + } + else if (!active && m_activityHold) + { + m_activityHold.reset(); + } } WSLCContainer::WSLCContainer(WSLCContainerImpl* impl, WSLCSession& session, std::function&& OnDeleted) : @@ -2185,6 +2215,7 @@ WSLCContainer::WSLCContainer(WSLCContainerImpl* impl, WSLCSession& session, std: } HRESULT WSLCContainer::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* Stdout, WSLCHandle* Stderr) +try { WSLCExecutionContext context(&m_session); @@ -2196,8 +2227,10 @@ HRESULT WSLCContainer::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* *Stdout = {}; *Stderr = {}; + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Attach, DetachKeys, Stdin, Stdout, Stderr); } +CATCH_RETURN(); HRESULT WSLCContainer::GetState(WSLCContainerState* Result) { @@ -2255,6 +2288,7 @@ HRESULT WSLCContainer::GetInitProcess(IWSLCProcess** Process) } HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, IWSLCProcess** Process) +try { WSLCExecutionContext context(&m_session); @@ -2263,22 +2297,35 @@ HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, const WSLCProcess RETURN_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Options->Flags, ~WSLCProcessFlagsValid), "Invalid flags: 0x%x", Options->Flags); *Process = nullptr; + + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Exec, Options, StartOptions, Process); } +CATCH_RETURN(); HRESULT WSLCContainer::Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds) +try { WSLCExecutionContext context(&m_session); + // Hold a VM lease for the whole operation: --rm containers self-delete during Stop, which + // disconnects the wrapper and drops activity. Without the lease, the idle worker can fire + // during the post-stop destroy wait (up to 60s) and tear the VM down mid-call. + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stop, Signal, TimeoutSeconds, false); } +CATCH_RETURN(); HRESULT WSLCContainer::Kill(_In_ WSLCSignal Signal) +try { WSLCExecutionContext context(&m_session); + // Hold a VM lease for the same reason as Stop(): --rm can self-delete and drop activity. + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stop, Signal, {}, true); } +CATCH_RETURN(); HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, IWarningCallback* WarningCallback) try @@ -2287,11 +2334,13 @@ try THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCContainerStartFlagsValid), "Invalid flags: 0x%x", Flags); + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Start, Flags, StartOptions); } CATCH_RETURN(); HRESULT WSLCContainer::Inspect(LPSTR* Output) +try { WSLCExecutionContext context(&m_session); @@ -2299,8 +2348,10 @@ HRESULT WSLCContainer::Inspect(LPSTR* Output) *Output = nullptr; + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Inspect, Output); } +CATCH_RETURN(); HRESULT WSLCContainer::Stats(LPSTR* Output) try @@ -2310,6 +2361,8 @@ try RETURN_HR_IF(E_POINTER, Output == nullptr); *Output = nullptr; + + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stats, Output); } CATCH_RETURN(); @@ -2322,6 +2375,11 @@ try THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCDeleteFlagsValid), "Invalid flags: 0x%x", Flags); // Special case for Delete(): If deletion is successful, notify the WSLCSession that the container has been deleted. + // Hold a VM lease across the whole operation: deleting a container makes it inactive and + // can trigger an idle teardown. Without the lease the idle worker could take the session + // lock exclusively and clear m_containers (destroying this container) concurrently, racing + // the delete and inverting the container->session lock order. + auto vmLease = m_session.AcquireVmLease(); auto [lock, impl] = LockImpl(); impl->Delete(Flags); @@ -2347,11 +2405,14 @@ try CATCH_LOG(); HRESULT WSLCContainer::Export(WSLCHandle TarHandle) +try { WSLCExecutionContext context(&m_session); + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Export, TarHandle); } +CATCH_RETURN(); HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) try @@ -2364,6 +2425,7 @@ try *Stdout = {}; *Stderr = {}; + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Logs, Flags, Stdout, Stderr, Since, Until, Tail); } CATCH_RETURN(); @@ -2541,6 +2603,8 @@ HRESULT WSLCContainer::ConnectToNetwork(const WSLCNetworkConnectionOptions* Opti try { COMServiceExecutionContext context; + + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::ConnectToNetwork, Options); } CATCH_RETURN(); @@ -2549,6 +2613,8 @@ HRESULT WSLCContainer::DisconnectFromNetwork(LPCSTR NetworkName) try { COMServiceExecutionContext context; + + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::DisconnectFromNetwork, NetworkName); } CATCH_RETURN(); diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 10fd57fd37..ef797933cf 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -16,6 +16,7 @@ Module Name: #include "ServiceProcessLauncher.h" #include "WSLCSession.h" +#include "WSLCIdleState.h" #include "DockerEventTracker.h" #include "DockerHTTPClient.h" #include "WSLCProcessControl.h" @@ -176,6 +177,10 @@ class WSLCContainerImpl void MapPorts(); void UnmapPorts(); + // Acquires or releases the activity hold so it is held exactly while the container is in an + // active (Created/Running) state, keeping the session's VM alive across idle teardown. + __requires_lock_held(m_lock) void UpdateActivityHoldLockHeld() noexcept; + __requires_shared_lock_held(m_lock) std::string InspectLockHeld() const; mutable wil::srwlock m_lock; @@ -220,6 +225,11 @@ class WSLCContainerImpl DockerEventTracker::EventTrackingReference m_containerEvents; IORelay& m_ioRelay; std::string m_networkMode; + + // Held (non-empty) exactly while the container is Created/Running so the session's VM stays + // alive even when no client holds the wrapper (e.g. a detached `run -d` container). Maintained + // by UpdateActivityHoldLockHeld(); released automatically when the container is destroyed. + ActivityRef m_activityHold; }; class DECLSPEC_UUID("B1F1C4E3-C225-4CAE-AD8A-34C004DE1AE4") WSLCContainer diff --git a/src/windows/wslcsession/WSLCExecutionContext.h b/src/windows/wslcsession/WSLCExecutionContext.h index 5d75bfed14..b3abec5a0e 100644 --- a/src/windows/wslcsession/WSLCExecutionContext.h +++ b/src/windows/wslcsession/WSLCExecutionContext.h @@ -27,7 +27,19 @@ class WSLCExecutionContext : public wsl::windows::common::COMServiceExecutionCon protected: bool CollectUserWarning(const std::wstring& warning) override { - if (m_warningCallback != nullptr) + IWarningCallback* callback = m_warningCallback; + + // When the operation carries no explicit callback, fall back to the callback supplied when + // the session was created/entered. This routes warnings emitted outside a callback-bearing + // operation (e.g. resource recovery during the lazy VM start) back to the session creator. + wil::com_ptr sessionCallback; + if (callback == nullptr && m_session != nullptr) + { + sessionCallback = m_session->AcquireWarningCallback(); + callback = sessionCallback.get(); + } + + if (callback != nullptr) { std::unique_ptr comCallback; if (m_session != nullptr) @@ -35,7 +47,7 @@ class WSLCExecutionContext : public wsl::windows::common::COMServiceExecutionCon comCallback = std::make_unique(m_session->RegisterUserCOMCallback()); } - auto hr = m_warningCallback->OnWarning(warning.c_str()); + auto hr = callback->OnWarning(warning.c_str()); if (SUCCEEDED(hr) || hr == RPC_E_CALL_CANCELED || hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { return true; diff --git a/src/windows/wslcsession/WSLCIdleState.h b/src/windows/wslcsession/WSLCIdleState.h new file mode 100644 index 0000000000..712022eecb --- /dev/null +++ b/src/windows/wslcsession/WSLCIdleState.h @@ -0,0 +1,226 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCIdleState.h + +Abstract: + + Shared idle-termination state for WSLC session VM lifecycle. + +--*/ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace wsl::windows::service::wslc { + +// Shared idle-termination state for a WSLC session. +// +// A single activity refcount is the only source of truth for "the VM is needed". Everything that +// requires the VM holds a reference for as long as it needs it: +// * in-flight operations (WSLCSession::VmLease), +// * running/created containers themselves (WSLCContainerImpl's ActivityRef), +// * client-held process wrappers (WSLCProcess keep-alive token), +// * multi-round-trip CLI operations (WSLCSession::BeginContainerOperation). +// +// When the count drops to zero a threadpool timer is armed for the idle grace period; if it +// elapses without new activity the session-supplied OnIdle callback tears the VM down. Any new +// activity before it fires cancels the timer. +// +// Held via shared_ptr so activity holders (container/process wrappers, operation tokens) can +// outlive the owning session and release activity without dereferencing it. The session clears the +// callback and drains the timer in Disarm() during teardown, after which a late release simply +// decrements the count and never re-enters the destroyed session. +class IdleState +{ +public: + IdleState() = default; + + IdleState(const IdleState&) = delete; + IdleState& operator=(const IdleState&) = delete; + + // Installs the idle-teardown callback and grace period and creates the timer. Called once by + // the owning session after construction. OnIdle runs on a threadpool thread. + void Initialize(std::chrono::milliseconds GracePeriod, std::function OnIdle) + { + auto lock = m_lock.lock_exclusive(); + m_gracePeriod = GracePeriod; + m_onIdle = std::move(OnIdle); + m_timer.reset(CreateThreadpoolTimer(&IdleState::TimerCallback, this, nullptr)); + THROW_LAST_ERROR_IF(!m_timer); + } + + // Permanently disables idle teardown: clears the callback so no further arm has any effect, and + // drains any pending/running timer callback. Must be called by the session (with its own lock + // released) during teardown, before the session object is destroyed, so no callback can + // reference it afterwards. + void Disarm() noexcept + { + PTP_TIMER timer = nullptr; + { + auto lock = m_lock.lock_exclusive(); + m_onIdle = nullptr; + timer = m_timer.get(); + if (timer != nullptr) + { + SetThreadpoolTimer(timer, nullptr, 0, 0); + } + } + + // Drain any in-flight callback outside the lock; it may take the session lock. + if (timer != nullptr) + { + WaitForThreadpoolTimerCallbacks(timer, TRUE); + } + } + + // Records the start of an activity; cancels any pending idle teardown on the 0->1 transition. + void AddActivity() noexcept + { + auto lock = m_lock.lock_exclusive(); + if (m_activityCount.fetch_add(1) == 0) + { + CancelLockHeld(); + } + } + + // Records the end of an activity; arms the idle timer on the 1->0 transition. + void ReleaseActivity() noexcept + { + auto lock = m_lock.lock_exclusive(); + const int previous = m_activityCount.fetch_sub(1); + FAIL_FAST_IF(previous <= 0); // Underflow is a fatal bug, not a recoverable condition. + if (previous == 1) + { + ArmLockHeld(); + } + } + + int ActivityCount() const noexcept + { + return m_activityCount.load(); + } + +private: + static void CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE, PVOID Context, PTP_TIMER) noexcept + try + { + auto* self = static_cast(Context); + + std::function onIdle; + { + auto lock = self->m_lock.lock_exclusive(); + + // Activity resumed (count != 0) or teardown raced us (callback cleared): nothing to do. + if (self->m_activityCount.load() != 0 || !self->m_onIdle) + { + return; + } + + // Copy and invoke outside the lock: OnIdle takes the session lock, and holding this + // lock across that would invert the session-lock -> idle-lock ordering. + onIdle = self->m_onIdle; + } + + onIdle(); + } + CATCH_LOG() + + void ArmLockHeld() noexcept + { + if (!m_timer || !m_onIdle) + { + return; + } + + // Relative due time is expressed as a negative count of 100ns intervals. + const int64_t relative = -static_cast(m_gracePeriod.count()) * 10000; + FILETIME due{}; + due.dwLowDateTime = static_cast(relative & 0xFFFFFFFF); + due.dwHighDateTime = static_cast((relative >> 32) & 0xFFFFFFFF); + SetThreadpoolTimer(m_timer.get(), &due, 0, 0); + } + + void CancelLockHeld() noexcept + { + if (m_timer) + { + SetThreadpoolTimer(m_timer.get(), nullptr, 0, 0); + } + } + + std::atomic m_activityCount{0}; + wil::srwlock m_lock; + + _Guarded_by_(m_lock) std::function m_onIdle; + _Guarded_by_(m_lock) std::chrono::milliseconds m_gracePeriod { 0 }; + _Guarded_by_(m_lock) wil::unique_threadpool_timer m_timer; +}; + +// RAII activity hold on an IdleState: increments on construction and decrements on destruction or +// reset(). Movable, non-copyable. Used by running/created containers to keep the VM alive without +// a client reference. Holds the IdleState via shared_ptr so it is safe even if it outlives the +// owning session. +class ActivityRef +{ +public: + ActivityRef() = default; + + explicit ActivityRef(std::shared_ptr State) noexcept : m_state(std::move(State)) + { + if (m_state) + { + m_state->AddActivity(); + } + } + + ActivityRef(ActivityRef&& Other) noexcept : m_state(std::exchange(Other.m_state, nullptr)) + { + } + + ActivityRef& operator=(ActivityRef&& Other) noexcept + { + if (this != &Other) + { + reset(); + m_state = std::exchange(Other.m_state, nullptr); + } + + return *this; + } + + ActivityRef(const ActivityRef&) = delete; + ActivityRef& operator=(const ActivityRef&) = delete; + + ~ActivityRef() + { + reset(); + } + + void reset() noexcept + { + if (m_state) + { + m_state->ReleaseActivity(); + m_state.reset(); + } + } + + explicit operator bool() const noexcept + { + return m_state != nullptr; + } + +private: + std::shared_ptr m_state; +}; + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCProcess.h b/src/windows/wslcsession/WSLCProcess.h index 1a79525dc1..9ce606a378 100644 --- a/src/windows/wslcsession/WSLCProcess.h +++ b/src/windows/wslcsession/WSLCProcess.h @@ -45,9 +45,19 @@ class DECLSPEC_UUID("AFBEA6D6-D8A4-4F81-8FED-F947EB74B33B") WSLCProcess HANDLE GetExitEvent(); int GetPid() const; + // Attaches an opaque keep-alive token whose lifetime is bound to this process object. A + // root-namespace process is not tracked as a container, so it relies on this token to hold an + // activity reference on the owning session for as long as the client keeps the process alive, + // preventing the idle worker from tearing the VM down (and killing the process) underneath it. + void SetKeepAliveToken(Microsoft::WRL::ComPtr&& Token) noexcept + { + m_keepAliveToken = std::move(Token); + } + private: WSLCProcessFlags m_flags; std::shared_ptr m_control; std::unique_ptr m_io; + Microsoft::WRL::ComPtr m_keepAliveToken; }; } // namespace wsl::windows::service::wslc \ No newline at end of file diff --git a/src/windows/wslcsession/WSLCProcessControl.cpp b/src/windows/wslcsession/WSLCProcessControl.cpp index 0c0e61eac9..8c8da1837d 100644 --- a/src/windows/wslcsession/WSLCProcessControl.cpp +++ b/src/windows/wslcsession/WSLCProcessControl.cpp @@ -101,7 +101,15 @@ void DockerContainerProcessControl::OnContainerReleased() noexcept // Signal the exit event to prevent callers from being blocked on it. if (!m_exitEvent.is_signaled()) { - m_exitedCode = 128 + WSLCSignalSIGKILL; + // If the container already produced a real exit code (recorded by SetExitCode but not yet + // signaled — e.g. an --rm container whose init-exit signal is deferred to the Destroy + // event), preserve it. Only synthesize SIGKILL when the container is released without ever + // having produced an exit code (an abrupt teardown of a still-running container). + if (!m_exitedCode.has_value()) + { + m_exitedCode = 128 + WSLCSignalSIGKILL; + } + m_exitEvent.SetEvent(); } } diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index ea3b246d9a..6ce9da1675 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -43,8 +43,41 @@ constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName; constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000; constexpr DWORD c_processKillTimeoutMs = 10 * 1000; +// Grace period to keep an otherwise-idle VM running before tearing it down. This avoids +// thrashing the VM (repeated teardown/recreate) when containers are created and destroyed, +// or operations issued, in quick succession. The clock restarts whenever the VM is observed +// to be non-idle, so a full grace period of continuous idleness is required before teardown. +constexpr auto c_vmIdleGracePeriod = std::chrono::seconds(30); + namespace { +// Validates the target path for a NEW session (one with no existing storage VHD): if the path +// already exists it must be an empty directory, so session storage is never mixed with unrelated +// user files. A non-existent path is fine (it will be created). Enforced eagerly at session +// creation and again when the storage VHD is lazily created. +void ValidateNewSessionStorageDirectory(const std::filesystem::path& StoragePath) +{ + // status's error_code distinguishes "doesn't exist yet" (OK, we'll create it) from other I/O errors. + std::error_code ec; + const auto status = std::filesystem::status(StoragePath, ec); + if (ec && ec.value() != ERROR_FILE_NOT_FOUND && ec.value() != ERROR_PATH_NOT_FOUND) + { + THROW_IF_WIN32_ERROR_MSG(ec.value(), "status failed for %ls", StoragePath.c_str()); + } + + if (!std::filesystem::exists(status)) + { + return; + } + + THROW_HR_WITH_USER_ERROR_IF( + E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeDirectory(StoragePath.c_str()), !std::filesystem::is_directory(status)); + + const bool empty = std::filesystem::is_empty(StoragePath, ec); + THROW_IF_WIN32_ERROR_MSG(ec.value(), "is_empty failed for %ls", StoragePath.c_str()); + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeEmpty(StoragePath.c_str()), !empty); +} + // Group policy: WSLContainerRegistryAllowlist restricts which container-image // registries can be pulled from or pushed to. The check is enforced here at the // service boundary so it covers ALL callers (wslc.exe CLI, the WslcSDK C API, and @@ -332,7 +365,7 @@ HRESULT WSLCSession::Initialize( try { RETURN_HR_IF(E_POINTER, Settings == nullptr || VmFactory == nullptr); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED), m_virtualMachine.has_value()); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED), m_vmFactoryGitCookie != 0); THROW_HR_IF_MSG( E_INVALIDARG, WI_IsAnyFlagSet(Settings->FeatureFlags, ~WSLCFeatureFlagsValid), "Invalid feature flags: 0x%x", Settings->FeatureFlags); @@ -343,9 +376,32 @@ try Settings->StorageFlags); // Set up a warning context for the duration of initialization so that non-fatal - // failures (e.g., container/volume/network recovery) are streamed to the CLI. + // failures are streamed to the CLI. WSLCExecutionContext warningContext(this, WarningCallback); + // The VM (and storage VHD) is created lazily on the first operation. Validate the storage + // configuration eagerly here so misconfiguration is reported at session creation rather than + // surfacing later on the first VM-starting operation. + if (Settings->StoragePath != nullptr) + { + const std::filesystem::path storagePath{Settings->StoragePath}; + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Settings->StoragePath), !storagePath.is_absolute()); + + if (WI_IsFlagSet(Settings->StorageFlags, WSLCSessionStorageFlagsNoCreate)) + { + // The storage VHD must already exist (ConfigureStorage will not create it). + THROW_HR_WITH_USER_ERROR_IF( + HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), + Localization::MessageWslcSessionStorageNotFound(Settings->StoragePath), + !std::filesystem::exists(storagePath / c_storageVhdFilename)); + } + else if (!std::filesystem::exists(storagePath / c_storageVhdFilename)) + { + // New session: the target path (if it exists) must be an empty directory. + ValidateNewSessionStorageDirectory(storagePath); + } + } + // N.B. No locking is required because Initialize() is always called before the session is returned to the caller. m_id = Settings->SessionId; m_displayName = Settings->DisplayName ? Settings->DisplayName : L""; @@ -353,8 +409,24 @@ try m_featureFlags = Settings->FeatureFlags; m_pluginNotifier = PluginNotifier; - // Get user token for the current process + // Park the VM factory in the Global Interface Table. It is supplied here (on the call that + // creates the session) but used on demand from other threads/apartments; storing the raw + // proxy and calling it later would raise RPC_E_WRONG_THREAD. + m_git = wil::CoCreateInstance(CLSID_StdGlobalInterfaceTable, CLSCTX_INPROC_SERVER); + THROW_IF_FAILED(m_git->RegisterInterfaceInGlobal(VmFactory, __uuidof(IWSLCVirtualMachineFactory), &m_vmFactoryGitCookie)); + + // Park the warning callback too. The VM (and resource recovery) is created lazily on the + // first operation, which may not carry its own warning callback, so recovery warnings are + // routed back to this callback via AcquireWarningCallback()/WSLCExecutionContext. + if (WarningCallback != nullptr) + { + THROW_IF_FAILED(m_git->RegisterInterfaceInGlobal(WarningCallback, __uuidof(IWarningCallback), &m_warningCallbackGitCookie)); + } + + // Persist a deep copy of the settings (and the creating user's SID) required to + // (re)create the VM on demand. const auto tokenInfo = wil::get_token_information(GetCurrentProcessToken()); + PersistSettings(*Settings, tokenInfo->User.Sid); WSL_LOG( "SessionInitialized", @@ -362,61 +434,430 @@ try TraceLoggingValue(m_displayName.c_str(), "DisplayName"), TraceLoggingValue(m_creatorProcessName.c_str(), "CreatorProcess")); - // Create the VM through the factory. The VM produces crash events; the session multiplexes - // them out to any registered ICrashDumpCallback subscribers via OnCrashDumpWritten. + // The VM is created lazily on the first operation that requires it (see EnsureVmRunning) and + // torn down once the session has been continuously idle (activity count zero) for the grace + // period. Wire up the idle-teardown timer; IdleState arms it whenever the activity count drops + // to zero and cancels it when activity resumes, so no dedicated worker thread is needed. + m_idleState->Initialize(c_vmIdleGracePeriod, [this]() { OnIdleTimer(); }); + + return S_OK; +} +CATCH_RETURN() + +void WSLCSession::PersistSettings(const WSLCSessionInitSettings& Settings, PSID UserSid) +{ + m_settings = Settings; + + // Repoint the string fields at storage owned by the session so they outlive the caller's buffers. + m_settings.DisplayName = m_displayName.c_str(); + + if (Settings.CreatorProcessName != nullptr) + { + m_settingsCreatorProcessName = Settings.CreatorProcessName; + m_settings.CreatorProcessName = m_settingsCreatorProcessName->c_str(); + } + else + { + m_settings.CreatorProcessName = nullptr; + } + + if (Settings.StoragePath != nullptr) + { + m_settingsStoragePath = Settings.StoragePath; + m_settings.StoragePath = m_settingsStoragePath->c_str(); + } + else + { + m_settings.StoragePath = nullptr; + } + + if (Settings.RootVhdTypeOverride != nullptr) + { + m_settingsRootVhdTypeOverride = Settings.RootVhdTypeOverride; + m_settings.RootVhdTypeOverride = m_settingsRootVhdTypeOverride->c_str(); + } + else + { + m_settings.RootVhdTypeOverride = nullptr; + } + + if (UserSid != nullptr) + { + const auto length = GetLengthSid(UserSid); + const auto* bytes = reinterpret_cast(UserSid); + m_userSid.assign(bytes, bytes + length); + } + else + { + m_userSid.clear(); + } +} + +bool WSLCSession::IdleTerminationEnabled() const noexcept +{ + // Only tear the VM down when there is persistent storage to recover from. A tmpfs-backed + // session would lose all image/container state on teardown, so its VM is kept alive once started. + return m_settings.StoragePath != nullptr; +} + +void WSLCSession::EnsureVmRunning() +{ + if (m_vmState.load() == VmState::Running) + { + return; + } + + auto lock = m_lock.lock_exclusive(); + + // Do not (re)start the VM once the session is terminating or has terminated. This also + // bounds VmLease's retry loop: a lease that races with Terminate() fails here instead of + // restarting a VM that is being permanently torn down. + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating.load() || m_sessionTerminatedEvent.is_signaled()); + + if (m_vmState.load() == VmState::Running) + { + return; + } + + StartVmLockHeld(); +} + +bool WSLCSession::TryClaimExpectedStop() noexcept +{ + auto expected = VmExitDisposition::Active; + return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::StopRequested); +} + +bool WSLCSession::TryClaimSpontaneousExit() noexcept +{ + auto expected = VmExitDisposition::Active; + return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::ExitClaimed); +} + +void WSLCSession::StartVmLockHeld() +{ + WI_ASSERT(m_vmState.load() != VmState::Running); + + WSL_LOG("WslcVmStarting", TraceLoggingValue(m_id, "SessionId")); + + m_vmState.store(VmState::Starting); + m_vmExitDisposition.store(VmExitDisposition::Active); + + // Tear back down if bring-up fails partway. The VM may have exited on its own during bring-up, + // so claim the stop first and only tear down if we win it (TryClaimExpectedStop()); otherwise + // OnVmExited() owns the teardown and we just release the lock to let its Terminate() finish. + auto startCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + if (TryClaimExpectedStop()) + { + TearDownVmLockHeld(); + m_vmState.store(VmState::None); + } + else + { + WSL_LOG("WslcVmExitedDuringStart", TraceLoggingValue(m_id, "SessionId")); + } + }); + + // Create a fresh IO relay for this VM instance. The previous one (if any) was stopped + // during teardown and cannot be restarted. + m_ioRelay.emplace(); + + // Create the VM via the factory. Re-fetch the factory from the GIT so we call it through a + // proxy marshalled into this thread's apartment (see m_git). The VM produces crash events; + // the session multiplexes them out to any registered ICrashDumpCallback subscribers via + // OnCrashDumpWritten. + wil::com_ptr vmFactory; + THROW_IF_FAILED(m_git->GetInterfaceFromGlobal(m_vmFactoryGitCookie, __uuidof(IWSLCVirtualMachineFactory), vmFactory.put_void())); + wil::com_ptr vm; - THROW_IF_FAILED(VmFactory->CreateVirtualMachine(&vm)); + THROW_IF_FAILED(vmFactory->CreateVirtualMachine(&vm)); m_virtualMachine.emplace( vm.get(), - Settings, + &m_settings, m_sessionTerminatingEvent.get(), std::bind(&WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); - - // Make sure that everything is destroyed correctly if an exception is thrown. - auto errorCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(Terminate()); }); - m_virtualMachine->Initialize(); // Get an event from the service that is signaled when the VM exits. + m_vmExitedEvent.reset(); THROW_IF_FAILED(vm->GetTerminationEvent(&m_vmExitedEvent)); // Configure storage. - ConfigureStorage(*Settings, tokenInfo->User.Sid); + ConfigureStorage(m_settings, m_userSid.empty() ? nullptr : reinterpret_cast(m_userSid.data())); // Mirror the host's trusted root CAs into the VM before dockerd starts. InstallTrustedRootCertificates(); - // Launch containerd first + // Launch containerd first, then dockerd with the external containerd socket. StartContainerd(); - // Launch dockerd with external containerd socket + // Reset the readiness event before (re)starting dockerd so a stale signal from a prior + // VM instance is not observed. + m_dockerdReadyEvent.ResetEvent(); StartDockerd(); // Wait for dockerd to be ready before starting the event tracker. THROW_WIN32_IF_MSG( - ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(Settings->BootTimeoutMs), "Timed out waiting for dockerd to start"); + ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(m_settings.BootTimeoutMs), "Timed out waiting for dockerd to start"); auto [_, __, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread); m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000); // Start the event tracker. - m_eventTracker.emplace(m_dockerClient.value(), *this, m_ioRelay); + m_eventTracker.emplace(m_dockerClient.value(), *this, *m_ioRelay); m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), m_storageVhdPath.parent_path()); // Monitor for unexpected VM exit. - m_ioRelay.AddHandle(std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSession::OnVmExited, this))); + m_ioRelay->AddHandle(std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSession::OnVmExited, this))); // Recover any existing resources from storage. RecoverExistingNetworks(); RecoverExistingContainers(); - errorCleanup.release(); - return S_OK; + m_vmState.store(VmState::Running); + startCleanup.release(); + + WSL_LOG("WslcVmStarted", TraceLoggingValue(m_id, "SessionId")); +} + +void WSLCSession::StopVmLockHeld() +{ + if (m_vmState.load() != VmState::Running) + { + return; + } + + WSL_LOG("WslcVmIdleStop", TraceLoggingValue(m_id, "SessionId")); + + // N.B. The caller has claimed StopRequested (via TryClaimExpectedStop), so VM/dockerd/containerd + // exit callbacks firing from the relay thread during teardown are treated as expected, not as a + // crash. + m_vmState.store(VmState::Stopping); + + TearDownVmLockHeld(); + + m_vmState.store(VmState::None); +} + +void WSLCSession::TearDownVmLockHeld(bool CaptureTerminationReason) +{ + std::lock_guard containersLock(m_containersLock); + std::lock_guard networksLock(m_networksLock); + + m_containers.clear(); + m_volumes.reset(); + m_networks.clear(); + + // Stop the IO relay. + // This stops: + // - container state monitoring. + // - container init process relays + // - execs relays + // - container logs relays + if (m_ioRelay) + { + m_ioRelay->Stop(); + } + + { + std::lock_guard allocatedPortsLock(m_allocatedPortsLock); + m_allocatedPorts.clear(); + } + + m_eventTracker.reset(); + m_dockerClient.reset(); + + if (CaptureTerminationReason) + { + // Default: an explicit/graceful teardown is a shutdown (the VM is still alive and we are + // bringing it down). Overridden below if the VM exited on its own and recorded a cause. + m_terminationReason = WSLCVirtualMachineTerminationReasonShutdown; + } + + // Check if the VM has already exited (e.g., killed externally). + // If so, skip operations that require a live VM to avoid unnecessary waits. + // N.B. m_vmExitedEvent may be uninitialized if teardown runs before GetTerminationEvent() succeeds. + if (m_vmExitedEvent && m_vmExitedEvent.is_signaled()) + { + WSL_LOG("SkippingGracefulShutdown_VmDead", TraceLoggingValue(m_id, "SessionId")); + + // The VM exited on its own, so it recorded the cause. + if (CaptureTerminationReason && m_virtualMachine) + { + wil::unique_cotaskmem_string details; + LOG_IF_FAILED(m_virtualMachine->GetTerminationReason(&m_terminationReason, &details)); + m_terminationDetails = details ? details.get() : L""; + } + } + else if (m_virtualMachine) + { + m_virtualMachine->OnSessionTerminated(); + + // Stop dockerd first, then containerd (dockerd is a client of containerd). + // N.B. dockerd waits a couple seconds if there are any outstanding HTTP request sockets opened. + if (m_dockerdProcess.has_value()) + { + auto dockerdExitCode = StopProcess(m_dockerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); + WSL_LOG("DockerdExit", TraceLoggingValue(dockerdExitCode, "code")); + } + + if (m_containerdProcess.has_value()) + { + auto containerdExitCode = StopProcess(m_containerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); + WSL_LOG("ContainerdExit", TraceLoggingValue(containerdExitCode, "code")); + } + + // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running. + try + { + m_virtualMachine->Unmount(c_containerdStorage); + } + CATCH_LOG(); + } + + m_dockerdProcess.reset(); + m_containerdProcess.reset(); + m_virtualMachine.reset(); + + // Destroy the relay unless we're on its own thread (~IORelay joins the thread, which would + // deadlock). On unexpected-VM-exit path (runs on relay thread), leave it for ~WSLCSession. + if (!m_ioRelay || !m_ioRelay->IsRelayThread()) + { + m_ioRelay.reset(); + m_vmExitedEvent.reset(); + } + + // Delete the ephemeral swap VHD now that the VM is gone. + if (!m_swapVhdPath.empty()) + { + LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str())); + m_swapVhdPath.clear(); + } +} + +void WSLCSession::OnIdleTimer() +try +{ + // Idle teardown releases cross-process COM proxies (the VM and its VM-scoped state), so this + // threadpool callback must join the process MTA; otherwise those Release/calls fail with + // RPC_E_WRONG_THREAD. The function-try-block keeps this (and everything below) under CATCH_LOG: + // the threadpool callback that invokes us is noexcept, so an escaping throw would terminate. + const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); + + if (m_terminating.load() || !IdleTerminationEnabled()) + { + return; + } + + // Non-blocking acquire: a blocking exclusive would queue behind in-flight operations, and + // SRW locks favor waiting writers, stalling all new ops. If the lock is held, an operation + // is in flight; it holds an activity reference and will re-arm the timer (via the 1->0 + // transition) when it releases, so there is nothing to do here. + auto lock = m_lock.try_lock_exclusive(); + if (!lock) + { + return; + } + + // Re-check every teardown precondition under the lock. The activity count is the single + // source of truth for "the VM is needed"; a 0->1 transition since the timer fired (cancel + // raced the callback) is caught here. + if (m_terminating.load() || m_vmState.load() != VmState::Running || m_idleState->ActivityCount() != 0) + { + return; + } + + // Claim the stop. If we lose, OnVmExited() owns a spontaneous-exit teardown and is spinning for + // this lock, so release it and let that run instead of joining the relay ourselves. + if (!TryClaimExpectedStop()) + { + return; + } + + // Restore Active on completion (or early exit) so the next StartVmLockHeld starts clean; only + // clear our own claim. + auto dispositionCleanup = wil::scope_exit([this]() { + auto stopRequested = VmExitDisposition::StopRequested; + m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active); + }); + + StopVmLockHeld(); +} +CATCH_LOG(); + +WSLCSession::VmLease WSLCSession::AcquireVmLease() +{ + return VmLease(*this); +} + +WSLCSession::VmLease::VmLease(WSLCSession& Session) : m_session(&Session) +{ + // Record an in-flight operation before bringing the VM up so idle teardown cannot tear it down + // between EnsureVmRunning() and acquiring the shared lock. AddActivity cancels any pending idle + // timer. + m_session->m_idleState->AddActivity(); + + auto countCleanup = wil::scope_exit([this]() { + m_session->m_idleState->ReleaseActivity(); + m_session = nullptr; + }); + + // Activity increment may race with idle teardown. Retry until we hold the lock with VM running. + for (;;) + { + m_session->EnsureVmRunning(); + + m_lock = m_session->m_lock.lock_shared(); + + if (m_session->m_vmState.load() == VmState::Running) + { + break; + } + + m_lock.reset(); + } + + countCleanup.release(); +} + +WSLCSession::VmLease::VmLease(VmLease&& Other) noexcept : + m_session(std::exchange(Other.m_session, nullptr)), m_lock(std::move(Other.m_lock)) +{ +} + +WSLCSession::VmLease& WSLCSession::VmLease::operator=(VmLease&& Other) noexcept +{ + if (this != &Other) + { + if (m_session != nullptr) + { + // Release the shared lock before the activity reference so that, if this was the last + // activity, idle teardown can immediately take the exclusive lock. + m_lock.reset(); + m_session->m_idleState->ReleaseActivity(); + } + + m_session = std::exchange(Other.m_session, nullptr); + m_lock = std::move(Other.m_lock); + } + + return *this; +} + +WSLCSession::VmLease::~VmLease() +{ + if (m_session != nullptr) + { + // Release the shared lock before the activity reference so that, if this was the last + // activity, idle teardown can immediately take the exclusive lock. ReleaseActivity arms the + // idle timer on the 1->0 transition. + m_lock.reset(); + m_session->m_idleState->ReleaseActivity(); + } } -CATCH_RETURN() WSLCSession::~WSLCSession() { @@ -484,23 +925,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID WI_IsFlagSet(Settings.StorageFlags, WSLCSessionStorageFlagsNoCreate)); // Reject any non-empty existing path so we don't mix user files with session storage. - // status's error_code distinguishes "doesn't exist yet" (OK, we'll create it) from other I/O errors. - std::error_code ec; - const auto status = std::filesystem::status(storagePath, ec); - if (ec && ec.value() != ERROR_FILE_NOT_FOUND && ec.value() != ERROR_PATH_NOT_FOUND) - { - THROW_IF_WIN32_ERROR_MSG(ec.value(), "status failed for %ls", storagePath.c_str()); - } - - if (std::filesystem::exists(status)) - { - THROW_HR_WITH_USER_ERROR_IF( - E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeDirectory(storagePath.c_str()), !std::filesystem::is_directory(status)); - - const bool empty = std::filesystem::is_empty(storagePath, ec); - THROW_IF_WIN32_ERROR_MSG(ec.value(), "is_empty failed for %ls", storagePath.c_str()); - THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeEmpty(storagePath.c_str()), !empty); - } + ValidateNewSessionStorageDirectory(storagePath); // If the VHD wasn't found, create it. WSL_LOG("CreateStorageVhd", TraceLoggingValue(m_storageVhdPath.c_str(), "StorageVhdPath")); @@ -573,7 +998,7 @@ CATCH_RETURN(); void WSLCSession::OnDockerdExited() { - if (!m_sessionTerminatingEvent.is_signaled()) + if (!m_sessionTerminatingEvent.is_signaled() && m_vmExitDisposition.load() != VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedDockerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } @@ -581,7 +1006,7 @@ void WSLCSession::OnDockerdExited() void WSLCSession::OnContainerdExited() { - if (!m_sessionTerminatingEvent.is_signaled()) + if (!m_sessionTerminatingEvent.is_signaled() && m_vmExitDisposition.load() != VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedContainerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } @@ -589,6 +1014,14 @@ void WSLCSession::OnContainerdExited() void WSLCSession::OnVmExited() { + // A spontaneous exit we must permanently terminate, unless an expected stop already claimed it, + // in which case the exit was wanted and we decline. + if (!TryClaimSpontaneousExit()) + { + WSL_LOG("WslcVmExitedDuringStop", TraceLoggingValue(m_id, "SessionId")); + return; + } + WSL_LOG( "VmExited", TraceLoggingLevel(WINEVENT_LEVEL_WARNING), @@ -631,13 +1064,13 @@ ServiceRunningProcess WSLCSession::StartProcess( auto process = launcher.Launch(*m_virtualMachine); - m_ioRelay.AddHandle(std::make_unique( + m_ioRelay->AddHandle(std::make_unique( process.GetStdHandle(1), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false)); - m_ioRelay.AddHandle(std::make_unique( + m_ioRelay->AddHandle(std::make_unique( process.GetStdHandle(2), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false)); - m_ioRelay.AddHandle(std::make_unique(process.GetExitEvent(), std::move(ExitCallback))); + m_ioRelay->AddHandle(std::make_unique(process.GetExitEvent(), std::move(ExitCallback))); return process; } @@ -845,7 +1278,7 @@ try auto [repo, tagOrDigest] = wslutil::ParseImage(Image); EnforceRegistryAllowlist(repo); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); if (!tagOrDigest.has_value()) @@ -903,7 +1336,7 @@ try comCall = RegisterUserCOMCallback(); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); @@ -1232,7 +1665,7 @@ try { WSLCExecutionContext context(this, WarningCallback); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); @@ -1265,7 +1698,7 @@ try tag = tagOrDigest.value(); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); @@ -1436,7 +1869,7 @@ try RETURN_HR_IF_NULL(E_POINTER, ImageNameOrID); RETURN_HR_IF(E_INVALIDARG, strlen(ImageNameOrID) > WSLC_MAX_IMAGE_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); @@ -1469,7 +1902,7 @@ try names.emplace_back(ImageNames->Values[i]); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); @@ -1544,7 +1977,7 @@ try filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); @@ -1653,7 +2086,7 @@ try *DeletedImages = nullptr; *Count = 0; - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); @@ -1727,7 +2160,7 @@ try RETURN_HR_IF_NULL(E_POINTER, Options->Tag); RETURN_HR_IF(E_INVALIDARG, strlen(Options->Repo) + strlen(Options->Tag) + 1 > WSLC_MAX_IMAGE_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); @@ -1764,7 +2197,7 @@ try auto [repo, tagOrDigest] = wslutil::ParseImage(Image); EnforceRegistryAllowlist(repo); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); auto requestContext = m_dockerClient->PushImage(repo, tagOrDigest, RegistryAuthenticationInformation); @@ -1785,7 +2218,7 @@ try *Output = nullptr; - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); *Output = wil::make_unique_ansistring(InspectImageLockHeld(ImageNameOrId).c_str()).release(); @@ -1833,7 +2266,7 @@ try *IdentityToken = nullptr; - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); wil::unique_cotaskmem_ansistring token; @@ -1865,7 +2298,7 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); docker_schema::PruneImageResult pruneResult; @@ -1926,7 +2359,7 @@ try "Invalid process flags: 0x%x", containerOptions->InitProcessOptions.Flags); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); auto result = wil::ResultFromException([&]() { CreateContainerImpl(containerOptions, Container); }); @@ -2004,7 +2437,7 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), m_eventTracker.value(), m_dockerClient.value(), - m_ioRelay); + *m_ioRelay); // Key the map by Docker's container ID, which is set in the WSLCContainerImpl constructor and stable for its lifetime. auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container)); @@ -2037,7 +2470,7 @@ try ValidateName(Id, WSLC_MAX_CONTAINER_NAME_LENGTH); // Look for an exact ID match first. - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); std::lock_guard containersLock{m_containersLock}; // Purge containers that were auto-deleted via OnEvent (--rm). @@ -2076,6 +2509,73 @@ try } CATCH_RETURN(); +namespace { + + // Activity token holds an activity reference to prevent idle VM teardown while client holds it. + // Implements IFastRundown so crashed clients reclaim stub promptly instead of slow default rundown. + class ContainerOperation + : public Microsoft::WRL::RuntimeClass, IUnknown, IFastRundown> + { + public: + // Adopts an activity reference from CreateActivityToken; callback releases it. + void Initialize(std::function&& onRelease) noexcept + { + m_onRelease = std::move(onRelease); + } + + ~ContainerOperation() override + { + if (m_onRelease) + { + m_onRelease(); + } + } + + private: + std::function m_onRelease; + }; + +} // namespace + +Microsoft::WRL::ComPtr WSLCSession::CreateActivityToken() +{ + // Record the in-flight activity up front so the VM cannot idle-terminate before the caller + // takes ownership of the returned token. + m_idleState->AddActivity(); + auto countCleanup = wil::scope_exit([this]() { m_idleState->ReleaseActivity(); }); + + auto operation = Microsoft::WRL::Make(); + THROW_IF_NULL_ALLOC(operation.Get()); + + // Capture shared idle state so token can outlive session and release activity without keeping session alive. + std::shared_ptr idleState = m_idleState; + operation->Initialize([idleState = std::move(idleState)]() { idleState->ReleaseActivity(); }); + + // The token now owns the activity-count reference and will release it on destruction. + countCleanup.release(); + + Microsoft::WRL::ComPtr token; + THROW_IF_FAILED(operation.As(&token)); + return token; +} + +HRESULT WSLCSession::BeginContainerOperation(IUnknown** Operation) +try +{ + WSLCExecutionContext context(this); + + RETURN_HR_IF_NULL(E_POINTER, Operation); + *Operation = nullptr; + + // Record the in-flight operation up front so the VM cannot idle-terminate before the client + // resolves the container and issues the operation (and streams any output). + auto token = CreateActivityToken(); + + RETURN_IF_FAILED(token.CopyTo(Operation)); + return S_OK; +} +CATCH_RETURN(); + HRESULT WSLCSession::ListContainers( const WSLCListContainersOptions* Options, WSLCContainerEntry** Containers, ULONG* Count, WSLCContainerPortMapping** Ports, ULONG* PortsCount) try @@ -2110,7 +2610,7 @@ try filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); std::vector dockerContainers; @@ -2189,7 +2689,7 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); std::lock_guard containersLock{m_containersLock}; @@ -2260,10 +2760,18 @@ try *Errno = -1; // Make sure not to return 0 if something fails. } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); auto process = m_virtualMachine->CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno); + + // The VmLease above is released when this call returns, but the process keeps running in the + // VM and the client holds the returned proxy. A root-namespace process is not tracked as a + // container, so attach an activity token bound to the process's lifetime; this keeps the VM + // alive for as long as the client holds the process, preventing the idle worker from tearing + // the VM down and killing the process out from under the client. + process->SetKeepAliveToken(CreateActivityToken()); + THROW_IF_FAILED(process.CopyTo(Process)); return S_OK; @@ -2286,7 +2794,7 @@ try THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Path), !std::filesystem::path(Path).is_absolute()); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); // Attach the disk to the VM (AttachDisk() performs the access check for the VHD file). @@ -2314,7 +2822,7 @@ try auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount); auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCVolumeMetadataLabel); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); if (Options->Name != nullptr && Options->Name[0] != '\0') @@ -2334,7 +2842,7 @@ try RETURN_HR_IF_NULL(E_POINTER, Name); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); m_volumes->DeleteVolume(Name); @@ -2355,7 +2863,7 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); auto volumeList = m_volumes->ListVolumes(std::move(filters)); @@ -2387,7 +2895,7 @@ try std::string name = Name; ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); std::string json = m_volumes->InspectVolume(name); @@ -2412,7 +2920,7 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); WSLCVolumes::PruneVolumesResult pruneResult; @@ -2491,7 +2999,7 @@ try auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount); auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCNetworkManagedLabel); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); @@ -2600,7 +3108,7 @@ try std::string name = Name; ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); @@ -2640,7 +3148,7 @@ try *Networks = nullptr; *Count = 0; - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); std::lock_guard networksLock(m_networksLock); if (m_networks.empty()) @@ -2679,7 +3187,7 @@ try std::string name = Name; ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); std::lock_guard networksLock(m_networksLock); auto it = m_networks.find(name); @@ -2734,7 +3242,7 @@ try // Scope the prune to WSLC-managed networks. filters["label"].push_back(WSLCNetworkManagedLabel); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); @@ -2867,97 +3375,56 @@ try // Acquire an exclusive lock to ensure that no operation is running. WI_VERIFY(sessionLock); - std::lock_guard containersLock(m_containersLock); - std::lock_guard networksLock(m_networksLock); + // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason. + // This mirrors the soft teardown used for idle shutdown, but here it is permanent. + TearDownVmLockHeld(/* CaptureTerminationReason */ true); - m_containers.clear(); - m_volumes.reset(); - m_networks.clear(); + m_vmState.store(VmState::None); - // Stop the IO relay. - // This stops: - // - container state monitoring. - // - container init process relays - // - execs relays - // - container logs relays - m_ioRelay.Stop(); + // Signal completion last so any observer of the terminated event sees a fully torn-down + // session and a populated termination reason. + m_sessionTerminatedEvent.SetEvent(); - { - std::lock_guard allocatedPortsLock(m_allocatedPortsLock); - m_allocatedPorts.clear(); - } + // Release the exclusive lock before disarming the idle timer. If a timer callback is currently + // blocked acquiring the exclusive lock (about to evaluate idle teardown), it must be able to + // obtain it, observe m_terminating, and return — otherwise Disarm()'s wait for in-flight + // callbacks below would deadlock. + sessionLock.reset(); - m_eventTracker.reset(); - m_dockerClient.reset(); + // Permanently disable idle teardown and drain any in-flight timer callback so it cannot + // reference this session after it is destroyed. + m_idleState->Disarm(); - // Check if the VM has already exited (e.g., killed externally). - // If so, skip operations that require a live VM to avoid unnecessary waits. - // N.B. m_vmExitedEvent may be uninitialized if Terminate() is called from the - // Initialize() error path before GetTerminationEvent() succeeds. - if (m_vmExitedEvent && m_vmExitedEvent.is_signaled()) + // Idle teardown is disabled and no operation can run past termination, so the parked VM + // factory can no longer be re-fetched; revoke it from the GIT. + if (m_vmFactoryGitCookie != 0) { - WSL_LOG("SkippingGracefulShutdown_VmDead", TraceLoggingValue(m_id, "SessionId")); - - // The VM exited on its own, so it recorded the cause. - if (m_virtualMachine) - { - wil::unique_cotaskmem_string details; - LOG_IF_FAILED(m_virtualMachine->GetTerminationReason(&m_terminationReason, &details)); - m_terminationDetails = details ? details.get() : L""; - } + LOG_IF_FAILED(m_git->RevokeInterfaceFromGlobal(m_vmFactoryGitCookie)); + m_vmFactoryGitCookie = 0; } - else - { - // The VM is still alive, so this is a graceful shutdown initiated by us. - m_terminationReason = WSLCVirtualMachineTerminationReasonShutdown; - - if (m_virtualMachine) - { - m_virtualMachine->OnSessionTerminated(); - // Stop dockerd first, then containerd (dockerd is a client of containerd). - // N.B. dockerd waits a couple seconds if there are any outstanding HTTP request sockets opened. - if (m_dockerdProcess.has_value()) - { - auto dockerdExitCode = StopProcess(m_dockerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); - WSL_LOG("DockerdExit", TraceLoggingValue(dockerdExitCode, "code")); - } - - if (m_containerdProcess.has_value()) - { - auto containerdExitCode = StopProcess(m_containerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); - WSL_LOG("ContainerdExit", TraceLoggingValue(containerdExitCode, "code")); - } - - // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running. - if (m_storageMounted) - { - try - { - m_virtualMachine->Unmount(c_containerdStorage); - m_storageMounted = false; - } - CATCH_LOG(); - } - } + if (m_warningCallbackGitCookie != 0) + { + LOG_IF_FAILED(m_git->RevokeInterfaceFromGlobal(m_warningCallbackGitCookie)); + m_warningCallbackGitCookie = 0; } - m_dockerdProcess.reset(); - m_containerdProcess.reset(); - m_virtualMachine.reset(); + return S_OK; +} +CATCH_RETURN(); - // Delete the ephemeral swap VHD now that the VM is gone. - if (!m_swapVhdPath.empty()) +wil::com_ptr WSLCSession::AcquireWarningCallback() const +{ + wil::com_ptr callback; + if (m_warningCallbackGitCookie != 0) { - LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str())); - m_swapVhdPath.clear(); + // Best-effort: the creating client's proxy may already be gone (e.g. the CLI exited before + // a later VM restart), in which case the warning falls through to the default sink. + LOG_IF_FAILED(m_git->GetInterfaceFromGlobal(m_warningCallbackGitCookie, __uuidof(IWarningCallback), callback.put_void())); } - m_sessionTerminatedEvent.SetEvent(); - - return S_OK; + return callback; } -CATCH_RETURN(); HRESULT WSLCSession::RegisterCrashDumpCallback(_In_ ICrashDumpCallback* Callback, _Out_ IUnknown** Subscription) try @@ -3022,7 +3489,7 @@ try RETURN_HR_IF_NULL(E_POINTER, WindowsPath); RETURN_HR_IF_NULL(E_POINTER, LinuxPath); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); return m_virtualMachine->MountWindowsFolder(WindowsPath, LinuxPath, ReadOnly); @@ -3036,7 +3503,7 @@ try RETURN_HR_IF_NULL(E_POINTER, LinuxPath); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); return m_virtualMachine->UnmountWindowsFolder(LinuxPath); @@ -3048,7 +3515,7 @@ try { WSLCExecutionContext context(this); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); std::lock_guard allocatedPortsLock(m_allocatedPortsLock); @@ -3094,7 +3561,7 @@ try { WSLCExecutionContext context(this); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); std::lock_guard allocatedPortsLock(m_allocatedPortsLock); @@ -3408,7 +3875,11 @@ void WSLCSession::CancelUserCOMCallbacks() void WSLCSession::OnContainerDeleted(const WSLCContainerImpl* Container) { - auto lock = m_lock.lock_shared(); + // N.B. Invoked only from WSLCContainer::Delete, which already holds a VmLease (the shared + // session lock). The lease prevents a concurrent idle teardown from clearing m_containers, + // so this only needs m_containersLock. It must NOT re-acquire the shared session lock here: + // doing so while the idle worker is queued for the exclusive lock would deadlock (recursive + // shared acquire behind a pending writer). std::lock_guard containersLock(m_containersLock); WI_VERIFY(m_containers.erase(Container->ID()) == 1); @@ -3476,7 +3947,7 @@ void WSLCSession::RecoverExistingContainers() std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), m_eventTracker.value(), m_dockerClient.value(), - m_ioRelay); + *m_ioRelay); auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container)); WI_ASSERT(inserted); diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index aa86d47cb5..a6b0c4d83e 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -18,12 +18,15 @@ Module Name: #include "WSLCCompat.h" #include "WSLCVirtualMachine.h" #include "WSLCContainer.h" +#include "WSLCIdleState.h" #include "WSLCVolumes.h" #include "WSLCNetworkMetadata.h" #include "DockerEventTracker.h" #include "DockerHTTPClient.h" #include "IORelay.h" +#include #include +#include #include namespace wsl::windows::service::wslc { @@ -71,11 +74,16 @@ class UserCOMCallback // // WSLCSession - Implements IWSLCSession for container management. // Runs in a per-user COM server process for security isolation. -// The SYSTEM service creates the VM and passes IWSLCVirtualMachine to Initialize(). +// The SYSTEM service passes an IWSLCVirtualMachineFactory to Initialize(); the VM is created +// lazily on first use and may be torn down when idle and recreated on demand. // class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession : public Microsoft::WRL::RuntimeClass, IWSLCSession, IWSLCCompatSession, IFastRundown, ISupportErrorInfo> { + // WSLCContainer::Delete acquires a VmLease to keep the VM alive (and block idle + // teardown) for the duration of a container deletion. + friend class WSLCContainer; + public: WSLCSession() = default; @@ -143,6 +151,7 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession // Container management. IFACEMETHOD(CreateContainer)(_In_ const WSLCContainerOptions* Options, _In_opt_ IWarningCallback* WarningCallback, _Out_ IWSLCContainer** Container) override; IFACEMETHOD(OpenContainer)(_In_ LPCSTR Id, _In_ IWSLCContainer** Container) override; + IFACEMETHOD(BeginContainerOperation)(_Outptr_ IUnknown** Operation) override; IFACEMETHOD(ListContainers)( _In_opt_ const WSLCListContainersOptions* Options, _Out_ WSLCContainerEntry** Containers, @@ -247,6 +256,13 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession UserCOMCallback RegisterUserCOMCallback(); void UnregisterUserCOMCallback(DWORD ThreadId); + // Returns the warning callback supplied when the session was created/entered, re-marshalled + // into the calling apartment. Used as a fallback by WSLCExecutionContext so that warnings + // emitted by operations that carry no explicit callback (e.g. resource recovery during the + // lazy VM start) still reach the session creator. Returns null if no callback was supplied + // or the creating client's proxy is no longer reachable. + wil::com_ptr AcquireWarningCallback() const; + HANDLE SessionTerminatingEvent() const noexcept { return m_sessionTerminatingEvent.get(); @@ -259,9 +275,94 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession bool WaitForEventOrSessionTerminating(HANDLE Event, std::chrono::milliseconds Timeout) const; + // Shared idle-termination state. Exposed so VM-scoped objects (e.g. running containers via + // WSLCContainerImpl's ActivityRef) can hold an activity reference for their lifetime without + // keeping the session object itself alive. + std::shared_ptr IdleStateShared() const noexcept + { + return m_idleState; + } + + // Creates an opaque activity token that holds a reference on this session's activity count for + // its lifetime, deferring idle teardown of the VM until every outstanding token is released. + // Used both for transient client operations (BeginContainerOperation) and to keep the VM alive + // for the lifetime of a process whose wrapper a client may keep (root-namespace and exec'd + // processes). + Microsoft::WRL::ComPtr CreateActivityToken(); + private: ULONG m_id = 0; + // VM lifecycle state for on-demand creation / idle termination. + enum class VmState + { + None, + Starting, + Running, + Stopping, + }; + + // Single-owner arbitration for a VM exit: exactly one side tears a given VM instance down, + // resolved atomically. An expected stop (idle teardown or bring-up cleanup, on a lock-holding + // thread) and a spontaneous exit (OnVmExited, lock-free on the relay thread) each try to claim + // it; the loser declines. This avoids both a missed teardown and a deadlock (a lock-holder + // joining the relay thread while OnVmExited spins for the lock in Terminate()). A fresh VM + // starts Active. Claim via TryClaim*(), not by touching the atomic directly. + enum class VmExitDisposition + { + Active, // Running normally; a VM exit is unexpected and triggers a permanent Terminate(). + StopRequested, // An expected (soft) stop is in progress; OnVmExited treats the exit as expected. + ExitClaimed, // OnVmExited owns the permanent teardown of a spontaneous exit. + }; + + // Claims an expected (soft) stop of the current VM from a lock-holding thread. On success a + // racing OnVmExited() declines, so the caller may tear down (joining the relay thread is safe). + // On failure OnVmExited() already owns a spontaneous-exit teardown and is spinning for the lock + // in Terminate(); the caller must not tear down or it deadlocks joining the relay. + [[nodiscard]] bool TryClaimExpectedStop() noexcept; + + // Claims the teardown of a spontaneous VM exit from OnVmExited(). Fails if an expected stop is + // already in progress, in which case the exit was wanted and the caller declines. + [[nodiscard]] bool TryClaimSpontaneousExit() noexcept; + + _Requires_exclusive_lock_held_(m_lock) + void StartVmLockHeld(); + _Requires_exclusive_lock_held_(m_lock) + void StopVmLockHeld(); + _Requires_exclusive_lock_held_(m_lock) + void TearDownVmLockHeld(bool CaptureTerminationReason = false); + void EnsureVmRunning(); + + // Idle-teardown callback invoked by IdleState's timer once the VM has been continuously idle + // (activity count zero) for the grace period. Runs on a threadpool thread. + void OnIdleTimer(); + bool IdleTerminationEnabled() const noexcept; + void PersistSettings(const WSLCSessionInitSettings& Settings, PSID UserSid); + + // RAII lease taken at the top of every VM-requiring operation. On construction it + // ensures the VM is running (lazily restarting it if it was idle-terminated) and records + // an in-flight operation so idle teardown is deferred; it then holds the shared session + // lock for the operation's duration. On destruction it releases the lock and triggers an + // idle check. + class VmLease + { + public: + VmLease() = default; + explicit VmLease(WSLCSession& Session); + VmLease(VmLease&& Other) noexcept; + VmLease& operator=(VmLease&& Other) noexcept; + ~VmLease(); + + VmLease(const VmLease&) = delete; + VmLease& operator=(const VmLease&) = delete; + + private: + WSLCSession* m_session{}; + wil::rwlock_release_shared_scope_exit m_lock; + }; + + [[nodiscard]] VmLease AcquireVmLease(); + __requires_lock_held(m_userHandlesLock) void CancelUserHandleIO(); __requires_lock_held(m_userCOMCallbacksLock) void CancelUserCOMCallbacks(); @@ -301,6 +402,19 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession void StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback); std::optional m_dockerClient; + + // The VM factory is a cross-process proxy supplied by the SYSTEM service at Initialize() time + // but first used later (on demand) from a different thread/apartment. A directly stored proxy + // would fail with RPC_E_WRONG_THREAD, so it is parked in the process Global Interface Table and + // re-fetched (re-marshalled into the calling apartment) each time a VM is created. + wil::com_ptr m_git; + DWORD m_vmFactoryGitCookie{}; + + // The warning callback supplied at Initialize() is parked in the GIT for the same reason as + // the VM factory: it is used later, on demand, from other threads/apartments (a directly + // stored proxy would fail with RPC_E_WRONG_THREAD). Zero if no callback was supplied. + DWORD m_warningCallbackGitCookie{}; + std::optional m_virtualMachine; std::optional m_eventTracker; wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; @@ -326,7 +440,26 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession WSLCVirtualMachineTerminationReason m_terminationReason{WSLCVirtualMachineTerminationReasonUnknown}; std::wstring m_terminationDetails; wil::srwlock m_lock; - IORelay m_ioRelay; + std::optional m_ioRelay; + + // VM lifecycle / idle-termination state. + std::atomic m_vmState{VmState::None}; + std::atomic m_vmExitDisposition{VmExitDisposition::Active}; + // In-flight activity count, idle timer and teardown callback, decoupled from this object's + // lifetime (see IdleState in WSLCIdleState.h) so activity tokens and container COM wrappers can + // safely manage activity without keeping the session alive. See WSLCContainerImpl's ActivityRef + // (m_activityHold), WSLCSession::VmLease and CreateActivityToken(). + std::shared_ptr m_idleState{std::make_shared()}; + + // Persisted settings required to (re)create the VM on demand. The string fields point + // into the owned storage members below (or m_displayName) so they remain valid for the + // lifetime of the session. + WSLCSessionInitSettings m_settings{}; + std::optional m_settingsCreatorProcessName; + std::optional m_settingsStoragePath; + std::optional m_settingsRootVhdTypeOverride; + std::vector m_userSid; + std::optional m_containerdProcess; std::optional m_dockerdProcess; WSLCFeatureFlags m_featureFlags{}; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 94fb0ed261..0a36406554 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -11396,6 +11396,10 @@ class WSLCTests auto settings = GetDefaultSessionSettings(c_sessionName); auto session = CreateSession(settings); + // Session creation is lazy, so start the VM by launching a process before killing it. + WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); + auto process = launcher.Launch(*session); + KillVmByOwner(c_sessionName); WaitForSessionTermination(session.get()); @@ -11487,6 +11491,10 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager2->CreateSession(&settings2, WSLCSessionFlagsNone, warningCallback.Get(), &session2)); wsl::windows::common::security::ConfigureForCOMImpersonation(session2.get()); + // The VM (and container recovery) starts lazily on the first operation. Trigger it so + // recovery runs and its warning is delivered to the session's warning callback. + VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session2).GetExitEvent().wait(30000)); + // Verify the warning matches the expected localized message for the corrupt container. auto warnings = warningCallback->GetWarnings(); auto expectedWarning = std::format( @@ -11555,6 +11563,10 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, warningCallback.Get(), &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); + // The VM (and volume recovery) starts lazily on the first operation. Trigger it so + // recovery runs and its warning is delivered to the session's warning callback. + VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session).GetExitEvent().wait(30000)); + // Verify the warning matches the expected localized message for the missing volume. auto warnings = warningCallback->GetWarnings(); auto expectedWarning = @@ -11620,6 +11632,10 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, warningCallback.Get(), &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); + // The VM (and guest volume recovery) starts lazily on the first operation. Trigger it so + // recovery runs and its warning is delivered to the session's warning callback. + VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session).GetExitEvent().wait(30000)); + auto warnings = warningCallback->GetWarnings(); auto expectedWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(c_volumeName)); diff --git a/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp b/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp index c04c4b767c..c512452cf8 100644 --- a/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp @@ -109,9 +109,14 @@ class WSLCE2ESessionEnterTests WSLC_TEST_METHOD(WSLCE2E_SessionEnter_StoragePathNotFound) { auto result = RunWslc(L"system session enter does-not-exist"); - const auto expectedPath = std::filesystem::absolute(L"does-not-exist").wstring(); + + // The CLI resolves the storage argument to an absolute path (see EnterSession task) and the + // service validates it eagerly at session creation, reporting the friendly "No WSLC session + // found in ''" message rather than a bare system error. + const auto storagePath = std::filesystem::absolute(L"does-not-exist").wstring(); result.Verify({ - .Stderr = std::format(L"No WSLC session found in '{}'\r\nError code: ERROR_PATH_NOT_FOUND\r\n", expectedPath), + .Stderr = wsl::shared::Localization::MessageWslcSessionStorageNotFound(storagePath) + + L"\r\nError code: ERROR_PATH_NOT_FOUND\r\n", .ExitCode = 1, }); } From a708a019aae2dd9972e09a27b9ea153bad467246 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Fri, 26 Jun 2026 13:09:44 -0700 Subject: [PATCH 02/31] wslc: don't pin the session VM for merely-created containers Only Running containers now hold an activity reference that keeps the per-user session VM alive. Previously a container in either Created or Running state held the reference, so a `create`d-but-never-started container pinned the VM indefinitely and defeated idle termination. A created container's metadata persists on the containerd VHD across VM teardown and is rebuilt by RecoverExistingContainers on the next VM-requiring operation, so create -> idle-terminate -> start later works; the 30s grace period covers the common create-then-start gap. Also fix m_stateChangedAt recovery for created containers: docker inspect reports FinishedAt as the zero date ("0001-01-01T00:00:00Z") for a never-started container, which parsed to year 1 and rendered as "created 2026 years ago". Use the container's Created time for the Created state. This recovery path was previously unreachable, since created containers never got torn down. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCContainer.cpp | 32 ++++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index c5a74906f9..14c259b00c 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -570,9 +570,10 @@ WSLCContainerImpl::WSLCContainerImpl( m_initProcessFlags(InitProcessFlags), m_containerFlags(ContainerFlags) { - // Acquire the activity hold up front for containers recovered or created in an active state, so - // a recovered running container keeps the VM alive even before any client opens its wrapper. - if (m_state == WslcContainerStateCreated || m_state == WslcContainerStateRunning) + // Acquire the activity hold up front for a container recovered in the running state, so it keeps + // the VM alive even before any client opens its wrapper. A merely-created (never-started) + // container does not pin the VM: its metadata survives teardown and the VM restarts on next use. + if (m_state == WslcContainerStateRunning) { m_activityHold = ActivityRef(m_wslcSession.IdleStateShared()); } @@ -1908,11 +1909,21 @@ std::unique_ptr WSLCContainerImpl::Open( { auto inspectData = DockerClient.InspectContainer(dockerContainer.Id); auto state = DockerStateToWSLCState(dockerContainer.State); - const auto& timestamp = (state == WslcContainerStateRunning) ? inspectData.State.StartedAt : inspectData.State.FinishedAt; - if (!timestamp.empty()) + if (state == WslcContainerStateCreated) + { + // A created-but-never-started container has no StartedAt/FinishedAt; its state last + // changed when it was created. + container->m_stateChangedAt = static_cast(dockerContainer.Created); + } + else { - container->m_stateChangedAt = ParseDockerTimestamp(timestamp); + const auto& timestamp = (state == WslcContainerStateRunning) ? inspectData.State.StartedAt : inspectData.State.FinishedAt; + + if (!timestamp.empty()) + { + container->m_stateChangedAt = ParseDockerTimestamp(timestamp); + } } } catch (...) @@ -2190,15 +2201,16 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerSta m_state = State; m_stateChangedAt = stateChangedAt.value_or(static_cast(std::time(nullptr))); - // Keep the VM alive while this container is Created/Running and release the hold once it reaches - // a terminal state, even when no client holds the wrapper (e.g. a detached `run -d` container). - // Dropping the hold on the transition to Exited is what lets an otherwise-idle VM be torn down. + // 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 + // Created or Exited container does not pin the VM, since its metadata survives teardown. UpdateActivityHoldLockHeld(); } __requires_lock_held(m_lock) void WSLCContainerImpl::UpdateActivityHoldLockHeld() noexcept { - const bool active = (m_state == WslcContainerStateCreated || m_state == WslcContainerStateRunning); + const bool active = (m_state == WslcContainerStateRunning); if (active && !m_activityHold) { m_activityHold = ActivityRef(m_wslcSession.IdleStateShared()); From aef3497b350a3913dfc18fbf46b650f73386880d Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Fri, 26 Jun 2026 14:10:56 -0700 Subject: [PATCH 03/31] wslc: fix stale Created/Running comments on the activity hold Addresses review feedback: WSLCContainer.h still described the activity hold as held while Created/Running, but it now only pins the VM while Running. Update the two header comments to match the implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCContainer.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index ef797933cf..5032dd6de4 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -177,8 +177,8 @@ class WSLCContainerImpl void MapPorts(); void UnmapPorts(); - // Acquires or releases the activity hold so it is held exactly while the container is in an - // active (Created/Running) state, keeping the session's VM alive across idle teardown. + // Acquires or releases the activity hold so it is held exactly while the container is Running, + // keeping the session's VM alive across idle teardown. __requires_lock_held(m_lock) void UpdateActivityHoldLockHeld() noexcept; __requires_shared_lock_held(m_lock) std::string InspectLockHeld() const; @@ -226,9 +226,9 @@ class WSLCContainerImpl IORelay& m_ioRelay; std::string m_networkMode; - // Held (non-empty) exactly while the container is Created/Running so the session's VM stays - // alive even when no client holds the wrapper (e.g. a detached `run -d` container). Maintained - // by UpdateActivityHoldLockHeld(); released automatically when the container is destroyed. + // Held (non-empty) exactly while the container is Running so the session's VM stays alive even + // when no client holds the wrapper (e.g. a detached `run -d` container). Maintained by + // UpdateActivityHoldLockHeld(); released automatically when the container is destroyed. ActivityRef m_activityHold; }; From b74e68e865128cd85a74acbfc448acb98ea9581c Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 29 Jun 2026 17:32:29 -0700 Subject: [PATCH 04/31] wslc: address idle-termination review feedback - Make the VM idle grace period configurable via settings.yaml (session.idleTimeout, default 30s) instead of a hardcoded constant. - Assert UserSid is non-null in PersistSettings rather than tolerating a null SID. - Drop the session warning-callback GIT fallback; warnings emitted outside a callback-bearing operation are logged and event-logged only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/common/WSLCUserSettings.cpp | 8 +++ src/windows/common/WSLCUserSettings.h | 2 + .../service/exe/WSLCSessionManager.cpp | 2 + src/windows/service/inc/wslc.idl | 2 + .../wslcsession/WSLCExecutionContext.h | 11 ---- src/windows/wslcsession/WSLCSession.cpp | 54 +++++-------------- src/windows/wslcsession/WSLCSession.h | 12 ----- 7 files changed, 26 insertions(+), 65 deletions(-) diff --git a/src/windows/common/WSLCUserSettings.cpp b/src/windows/common/WSLCUserSettings.cpp index b0c3e1e861..0a8581c284 100644 --- a/src/windows/common/WSLCUserSettings.cpp +++ b/src/windows/common/WSLCUserSettings.cpp @@ -57,6 +57,9 @@ static constexpr std::string_view s_DefaultSettingsTemplate = " # used without an explicit address (default: 127.0.0.1)\n" " # defaultBindingAddress: default\n" "\n" + " # Seconds an idle session VM stays running before it is torn down (default: 30)\n" + " # idleTimeout: default\n" + "\n" "# Credential storage backend: \"wincred\" or \"file\" (default: wincred)\n" "# credentialStore: wincred\n"; @@ -163,6 +166,11 @@ namespace details { return value; } + WSLC_VALIDATE_SETTING(SessionIdleTimeout) + { + return value > 0 ? std::optional{value} : std::nullopt; + } + WSLC_VALIDATE_SETTING(CredentialStore) { if (value == "wincred") diff --git a/src/windows/common/WSLCUserSettings.h b/src/windows/common/WSLCUserSettings.h index 30e3f3f9a6..7219fa7ade 100644 --- a/src/windows/common/WSLCUserSettings.h +++ b/src/windows/common/WSLCUserSettings.h @@ -45,6 +45,7 @@ enum class Setting : size_t SessionPortRelay, SessionDefaultBindingAddress, SessionStoragePath, + SessionIdleTimeout, Max }; @@ -101,6 +102,7 @@ namespace details { DEFINE_SETTING_MAPPING(SessionPortRelay, std::string, PortRelayType, PortRelayType::VirtioNet, "experimental.portRelay") DEFINE_SETTING_MAPPING(SessionDefaultBindingAddress, std::string, std::string, std::string{}, "session.defaultBindingAddress") DEFINE_SETTING_MAPPING(SessionStoragePath, std::string, std::string, std::string{}, "session.storagePath") + DEFINE_SETTING_MAPPING(SessionIdleTimeout, uint32_t, uint32_t, 30, "session.idleTimeout") #undef DEFINE_SETTING_MAPPING // clang-format on diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index fcd300a977..1e0a9d06c7 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -121,6 +121,7 @@ struct SessionSettings Settings.MemoryMb = memoryMb > 0 ? memoryMb : SessionSettings::DefaultMemoryMb(); Settings.MaximumStorageSizeMb = userSettings.Get(); Settings.BootTimeoutMs = wsl::windows::wslc::DefaultBootTimeoutMs; + Settings.IdleTimeoutSec = userSettings.Get(); Settings.NetworkingMode = userSettings.Get(); // TODO: Add a config setting to opt-out of GPU support. @@ -472,6 +473,7 @@ WSLCSessionInitSettings WSLCSessionManagerImpl::CreateSessionSettings( sessionSettings.RootVhdTypeOverride = Settings->RootVhdTypeOverride; sessionSettings.StorageFlags = Settings->StorageFlags; sessionSettings.SwapSizeMb = Settings->MemoryMb; + sessionSettings.IdleTimeoutSec = Settings->IdleTimeoutSec; return sessionSettings; } diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 5d798f44ff..181f2b4418 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -461,6 +461,7 @@ typedef struct _WSLCSessionSettings { WSLCFeatureFlags FeatureFlags; WSLCHandle DmesgOutput; WSLCSessionStorageFlags StorageFlags; + ULONG IdleTimeoutSec; // Below options are used for debugging purposes only. [unique] LPCWSTR RootVhdOverride; @@ -595,6 +596,7 @@ typedef struct _WSLCSessionInitSettings WSLCNetworkingMode NetworkingMode; WSLCFeatureFlags FeatureFlags; [unique] LPCSTR RootVhdTypeOverride; + ULONG IdleTimeoutSec; } WSLCSessionInitSettings; [ diff --git a/src/windows/wslcsession/WSLCExecutionContext.h b/src/windows/wslcsession/WSLCExecutionContext.h index b3abec5a0e..62b21ed8c6 100644 --- a/src/windows/wslcsession/WSLCExecutionContext.h +++ b/src/windows/wslcsession/WSLCExecutionContext.h @@ -28,17 +28,6 @@ class WSLCExecutionContext : public wsl::windows::common::COMServiceExecutionCon bool CollectUserWarning(const std::wstring& warning) override { IWarningCallback* callback = m_warningCallback; - - // When the operation carries no explicit callback, fall back to the callback supplied when - // the session was created/entered. This routes warnings emitted outside a callback-bearing - // operation (e.g. resource recovery during the lazy VM start) back to the session creator. - wil::com_ptr sessionCallback; - if (callback == nullptr && m_session != nullptr) - { - sessionCallback = m_session->AcquireWarningCallback(); - callback = sessionCallback.get(); - } - if (callback != nullptr) { std::unique_ptr comCallback; diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 6ce9da1675..c9cd93e214 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -43,10 +43,11 @@ constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName; constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000; constexpr DWORD c_processKillTimeoutMs = 10 * 1000; -// Grace period to keep an otherwise-idle VM running before tearing it down. This avoids -// thrashing the VM (repeated teardown/recreate) when containers are created and destroyed, -// or operations issued, in quick succession. The clock restarts whenever the VM is observed -// to be non-idle, so a full grace period of continuous idleness is required before teardown. +// Default grace period to keep an otherwise-idle VM running before tearing it down (used when the +// session's settings.yaml does not override it). This avoids thrashing the VM (repeated +// teardown/recreate) when containers are created and destroyed, or operations issued, in quick +// succession. The clock restarts whenever the VM is observed to be non-idle, so a full grace period +// of continuous idleness is required before teardown. constexpr auto c_vmIdleGracePeriod = std::chrono::seconds(30); namespace { @@ -415,14 +416,6 @@ try m_git = wil::CoCreateInstance(CLSID_StdGlobalInterfaceTable, CLSCTX_INPROC_SERVER); THROW_IF_FAILED(m_git->RegisterInterfaceInGlobal(VmFactory, __uuidof(IWSLCVirtualMachineFactory), &m_vmFactoryGitCookie)); - // Park the warning callback too. The VM (and resource recovery) is created lazily on the - // first operation, which may not carry its own warning callback, so recovery warnings are - // routed back to this callback via AcquireWarningCallback()/WSLCExecutionContext. - if (WarningCallback != nullptr) - { - THROW_IF_FAILED(m_git->RegisterInterfaceInGlobal(WarningCallback, __uuidof(IWarningCallback), &m_warningCallbackGitCookie)); - } - // Persist a deep copy of the settings (and the creating user's SID) required to // (re)create the VM on demand. const auto tokenInfo = wil::get_token_information(GetCurrentProcessToken()); @@ -438,7 +431,8 @@ try // torn down once the session has been continuously idle (activity count zero) for the grace // period. Wire up the idle-teardown timer; IdleState arms it whenever the activity count drops // to zero and cancels it when activity resumes, so no dedicated worker thread is needed. - m_idleState->Initialize(c_vmIdleGracePeriod, [this]() { OnIdleTimer(); }); + const auto idleGracePeriod = m_settings.IdleTimeoutSec > 0 ? std::chrono::seconds(m_settings.IdleTimeoutSec) : c_vmIdleGracePeriod; + m_idleState->Initialize(idleGracePeriod, [this]() { OnIdleTimer(); }); return S_OK; } @@ -481,16 +475,11 @@ void WSLCSession::PersistSettings(const WSLCSessionInitSettings& Settings, PSID m_settings.RootVhdTypeOverride = nullptr; } - if (UserSid != nullptr) - { - const auto length = GetLengthSid(UserSid); - const auto* bytes = reinterpret_cast(UserSid); - m_userSid.assign(bytes, bytes + length); - } - else - { - m_userSid.clear(); - } + THROW_HR_IF(E_UNEXPECTED, UserSid == nullptr); + + const auto length = GetLengthSid(UserSid); + const auto* bytes = reinterpret_cast(UserSid); + m_userSid.assign(bytes, bytes + length); } bool WSLCSession::IdleTerminationEnabled() const noexcept @@ -3403,29 +3392,10 @@ try m_vmFactoryGitCookie = 0; } - if (m_warningCallbackGitCookie != 0) - { - LOG_IF_FAILED(m_git->RevokeInterfaceFromGlobal(m_warningCallbackGitCookie)); - m_warningCallbackGitCookie = 0; - } - return S_OK; } CATCH_RETURN(); -wil::com_ptr WSLCSession::AcquireWarningCallback() const -{ - wil::com_ptr callback; - if (m_warningCallbackGitCookie != 0) - { - // Best-effort: the creating client's proxy may already be gone (e.g. the CLI exited before - // a later VM restart), in which case the warning falls through to the default sink. - LOG_IF_FAILED(m_git->GetInterfaceFromGlobal(m_warningCallbackGitCookie, __uuidof(IWarningCallback), callback.put_void())); - } - - return callback; -} - HRESULT WSLCSession::RegisterCrashDumpCallback(_In_ ICrashDumpCallback* Callback, _Out_ IUnknown** Subscription) try { diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index a6b0c4d83e..8eff3eb4a3 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -256,13 +256,6 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession UserCOMCallback RegisterUserCOMCallback(); void UnregisterUserCOMCallback(DWORD ThreadId); - // Returns the warning callback supplied when the session was created/entered, re-marshalled - // into the calling apartment. Used as a fallback by WSLCExecutionContext so that warnings - // emitted by operations that carry no explicit callback (e.g. resource recovery during the - // lazy VM start) still reach the session creator. Returns null if no callback was supplied - // or the creating client's proxy is no longer reachable. - wil::com_ptr AcquireWarningCallback() const; - HANDLE SessionTerminatingEvent() const noexcept { return m_sessionTerminatingEvent.get(); @@ -410,11 +403,6 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession wil::com_ptr m_git; DWORD m_vmFactoryGitCookie{}; - // The warning callback supplied at Initialize() is parked in the GIT for the same reason as - // the VM factory: it is used later, on demand, from other threads/apartments (a directly - // stored proxy would fail with RPC_E_WRONG_THREAD). Zero if no callback was supplied. - DWORD m_warningCallbackGitCookie{}; - std::optional m_virtualMachine; std::optional m_eventTracker; wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; From 8c860b81c6fe58c08b0f8deb697cf3d7e007895a Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 30 Jun 2026 09:00:26 -0700 Subject: [PATCH 05/31] wslc: update recovery warning tests for log-only behavior Recovery warnings emitted during lazy VM start run outside the user's current command, so they are now logged (and written to the event log) instead of being routed back to the session-creation warning callback. Update the three WarningCallback*Recovery unit tests and the e2e test to assert the warning is no longer delivered to the session callback / printed on stderr. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/windows/WSLCTests.cpp | 26 +++++++++++-------- test/windows/wslc/e2e/WSLCE2EWarningTests.cpp | 25 +++++++++++------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 0a36406554..c8f315a8bf 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -11492,16 +11492,17 @@ class WSLCTests wsl::windows::common::security::ConfigureForCOMImpersonation(session2.get()); // The VM (and container recovery) starts lazily on the first operation. Trigger it so - // recovery runs and its warning is delivered to the session's warning callback. + // recovery runs. VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session2).GetExitEvent().wait(30000)); - // Verify the warning matches the expected localized message for the corrupt container. + // Recovery runs under the triggering operation's context, which carries no warning + // callback, so the warning is logged rather than delivered to the session callback. auto warnings = warningCallback->GetWarnings(); - auto expectedWarning = std::format( + auto recoveryWarning = std::format( L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverContainer(wsl::shared::string::MultiByteToWide(containerId))); - VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == expectedWarning; })); + VERIFY_IS_FALSE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); VERIFY_SUCCEEDED(session2->Terminate()); } @@ -11564,15 +11565,16 @@ class WSLCTests wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); // The VM (and volume recovery) starts lazily on the first operation. Trigger it so - // recovery runs and its warning is delivered to the session's warning callback. + // recovery runs. VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session).GetExitEvent().wait(30000)); - // Verify the warning matches the expected localized message for the missing volume. + // Recovery runs under the triggering operation's context, which carries no warning + // callback, so the warning is logged rather than delivered to the session callback. auto warnings = warningCallback->GetWarnings(); - auto expectedWarning = + auto recoveryWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(L"wslc-test-warning-recovery")); - VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == expectedWarning; })); + VERIFY_IS_FALSE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); // Clean up the orphaned volume from Docker's metadata. LOG_IF_FAILED(session->DeleteVolume("wslc-test-warning-recovery")); @@ -11633,13 +11635,15 @@ class WSLCTests wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); // The VM (and guest volume recovery) starts lazily on the first operation. Trigger it so - // recovery runs and its warning is delivered to the session's warning callback. + // recovery runs. VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session).GetExitEvent().wait(30000)); + // Recovery runs under the triggering operation's context, which carries no warning + // callback, so the warning is logged rather than delivered to the session callback. auto warnings = warningCallback->GetWarnings(); - auto expectedWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(c_volumeName)); + auto recoveryWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(c_volumeName)); - VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == expectedWarning; })); + VERIFY_IS_FALSE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); // Clean up the volume from Docker's metadata. ExpectCommandResult(session.get(), {"/usr/bin/docker", "volume", "rm", "-f", c_volumeName}, 0); diff --git a/test/windows/wslc/e2e/WSLCE2EWarningTests.cpp b/test/windows/wslc/e2e/WSLCE2EWarningTests.cpp index 81afbef07a..a488dcddca 100644 --- a/test/windows/wslc/e2e/WSLCE2EWarningTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EWarningTests.cpp @@ -8,8 +8,9 @@ Module Name: Abstract: - End-to-end tests validating that warnings emitted by the WSLC COM service are - surfaced on the wslc.exe CLI's stderr via the IWarningCallback integration. + End-to-end tests validating how warnings emitted by the WSLC COM service are + surfaced (or intentionally suppressed) on the wslc.exe CLI's stderr via the + IWarningCallback integration. --*/ #include "precomp.h" @@ -72,9 +73,10 @@ class WSLCE2EWarningTests CATCH_LOG() // Injects a container with corrupt WSLC metadata into the default session's storage, - // then verifies that running the wslc.exe CLI surfaces the COM service's recovery - // warning on stderr. - WSLC_TEST_METHOD(WSLCE2E_Warning_ContainerRecoveryPrintedOnStderr) + // then verifies that running the wslc.exe CLI does not surface the COM service's recovery + // warning on stderr: recovery runs outside the user's current command, so it is logged + // (and written to the event log) rather than streamed back via IWarningCallback. + WSLC_TEST_METHOD(WSLCE2E_Warning_ContainerRecoveryNotPrintedOnStderr) { std::string corruptContainerId; @@ -98,15 +100,18 @@ class WSLCE2EWarningTests // Terminate the default session so the next wslc command recreates it and runs recovery. EnsureSessionIsTerminated(); - // Run the CLI: recovery of the corrupt container fails and the warning is printed on stderr. + // Run the CLI: recovery of the corrupt container fails, but because the recovery runs + // outside the user's current command, the warning is not printed on stderr. auto result = RunWslc(L"container list"); VERIFY_IS_TRUE(result.ExitCode.has_value()); VERIFY_ARE_EQUAL(0u, result.ExitCode.value()); - VERIFY_IS_TRUE(result.Stderr.has_value()); - const auto expectedStderr = std::format( - L"wsl: {}\r\n", wsl::shared::Localization::MessageWslcFailedToRecoverContainer(string::MultiByteToWide(corruptContainerId))); - VERIFY_ARE_EQUAL(expectedStderr, result.Stderr.value()); + const auto recoveryWarning = + wsl::shared::Localization::MessageWslcFailedToRecoverContainer(string::MultiByteToWide(corruptContainerId)); + if (result.Stderr.has_value()) + { + VERIFY_IS_TRUE(result.Stderr.value().find(recoveryWarning) == std::wstring::npos); + } } }; From fdb6bf793056b27323eda53b7761c52c756e1777 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 30 Jun 2026 09:45:13 -0700 Subject: [PATCH 06/31] wslc: address review feedback on lazy VM lifecycle - BeginContainerOperation: reject new operations once the session is terminating/terminated, mirroring EnsureVmRunning's gate, so a started operation cannot pin a VM that is being torn down. - TearDownVmLockHeld: reset m_storageMounted after unmounting so the flag does not stay stale across an idle teardown. - CLI Session model: stop retaining the IWarningCallback for the session lifetime. Recovery warnings from lazy VM start are logged rather than delivered to the session callback, so the stashed callback (and its now-misleading comments) is dead state. The callback is still passed to CreateSession, where it is consumed during initialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslc/services/SessionModel.h | 8 +------- src/windows/wslc/services/SessionService.cpp | 7 +++---- src/windows/wslcsession/WSLCSession.cpp | 5 +++++ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/windows/wslc/services/SessionModel.h b/src/windows/wslc/services/SessionModel.h index 491df6fc3b..946e8c3bf4 100644 --- a/src/windows/wslc/services/SessionModel.h +++ b/src/windows/wslc/services/SessionModel.h @@ -22,8 +22,7 @@ struct Session NON_COPYABLE(Session); DEFAULT_MOVABLE(Session); - explicit Session(wil::com_ptr session, wil::com_ptr warningCallback = {}) : - m_session(std::move(session)), m_warningCallback(std::move(warningCallback)) + explicit Session(wil::com_ptr session) : m_session(std::move(session)) { } @@ -44,11 +43,6 @@ struct Session private: wil::com_ptr m_session; - - // Kept alive for the lifetime of the session model (i.e. the whole CLI command) so the service - // can deliver warnings emitted by lazy/background work — such as resource recovery on the first - // VM start — back to this CLI invocation, even though no single COM call carries the callback. - wil::com_ptr m_warningCallback; }; } // namespace wsl::windows::wslc::models \ No newline at end of file diff --git a/src/windows/wslc/services/SessionService.cpp b/src/windows/wslc/services/SessionService.cpp index ff8008a22f..8b59509d73 100644 --- a/src/windows/wslc/services/SessionService.cpp +++ b/src/windows/wslc/services/SessionService.cpp @@ -56,15 +56,14 @@ Session SessionService::OpenOrCreateDefaultSession() { auto manager = CreateSessionManager(); - // Null Settings = default session with server-determined name and settings. + // Null Settings = default session with server-determined name and settings. The warning callback + // is consumed during CreateSession (session initialization); it is not retained afterwards. wil::com_ptr session; auto warningCallback = Microsoft::WRL::Make(); THROW_IF_FAILED(manager->CreateSession(nullptr, WSLCSessionFlagsNone, warningCallback.Get(), &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); - // Hold the warning callback for the session's lifetime so warnings emitted by a lazy VM start - // (e.g. resource recovery) are still delivered to this CLI invocation. - return Session(std::move(session), wil::com_ptr(warningCallback.Get())); + return Session(std::move(session)); } int SessionService::Attach(const Session& session) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index c9cd93e214..617b683da8 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -710,6 +710,7 @@ void WSLCSession::TearDownVmLockHeld(bool CaptureTerminationReason) m_dockerdProcess.reset(); m_containerdProcess.reset(); m_virtualMachine.reset(); + m_storageMounted = false; // Destroy the relay unless we're on its own thread (~IORelay joins the thread, which would // deadlock). On unexpected-VM-exit path (runs on relay thread), leave it for ~WSLCSession. @@ -2556,6 +2557,10 @@ try RETURN_HR_IF_NULL(E_POINTER, Operation); *Operation = nullptr; + // Do not start a new operation (which would hold the VM alive) once the session is terminating + // or has terminated. Mirrors the gate in EnsureVmRunning(). + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating.load() || m_sessionTerminatedEvent.is_signaled()); + // Record the in-flight operation up front so the VM cannot idle-terminate before the client // resolves the container and issues the operation (and streams any output). auto token = CreateActivityToken(); From 623aa4184491419a501fc230ca0116b615554ed5 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 2 Jul 2026 13:07:15 -0700 Subject: [PATCH 07/31] wslc: use non-throwing filesystem::exists for storage VHD probe Probe the storage VHD once with the std::error_code overload so an access-denied/transient I/O error surfaces as a clear Win32 error via WIL instead of a generic filesystem_error-to-HRESULT conversion, and avoid the duplicate exists() check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCSession.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 617b683da8..b10fa5e609 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -388,15 +388,18 @@ try const std::filesystem::path storagePath{Settings->StoragePath}; THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Settings->StoragePath), !storagePath.is_absolute()); + const auto vhdPath = storagePath / c_storageVhdFilename; + std::error_code existsError; + const bool vhdExists = std::filesystem::exists(vhdPath, existsError); + THROW_IF_WIN32_ERROR_MSG(existsError.value(), "exists failed for %ls", vhdPath.c_str()); + if (WI_IsFlagSet(Settings->StorageFlags, WSLCSessionStorageFlagsNoCreate)) { // The storage VHD must already exist (ConfigureStorage will not create it). THROW_HR_WITH_USER_ERROR_IF( - HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), - Localization::MessageWslcSessionStorageNotFound(Settings->StoragePath), - !std::filesystem::exists(storagePath / c_storageVhdFilename)); + HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), Localization::MessageWslcSessionStorageNotFound(Settings->StoragePath), !vhdExists); } - else if (!std::filesystem::exists(storagePath / c_storageVhdFilename)) + else if (!vhdExists) { // New session: the target path (if it exists) must be an empty directory. ValidateNewSessionStorageDirectory(storagePath); From 6d5cb7e420f9c782c48f5e614b2b2ed4db460dab Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 6 Jul 2026 10:06:07 -0700 Subject: [PATCH 08/31] wslc: extract WSLCSessionRuntime from WSLCSession Move the VM instance and its lifecycle state (VM, IO relay, docker client, event tracker, volumes, containerd/dockerd processes, exit-disposition and vm-state atomics, session lock, idle state, published-port bookkeeping, and ephemeral swap/storage state) out of WSLCSession into a new WSLCSessionRuntime. WSLCSession retains identity and orchestration and drives the runtime through BringUp/RecoverState/TearDownSessionState/OnSpontaneousExit/OnCrashDump hooks plus a SessionContext carrying the lifecycle primitives the runtime observes, so the runtime no longer reaches into WSLCSession private members. VM state is reached through the runtime, with Acquire()/LockedRuntime as the ergonomic lease accessor. Concurrency invariants are preserved: the exit-disposition claim protocol, the relay-thread teardown guard, lock ordering, GIT re-fetch per VM creation, and release-lock-before-Disarm in shutdown. The VM-exit monitor is armed between bring-up and state recovery to match the pre-extraction ordering. This is an isolated refactor commit on top of the idle-terminate work so it can be reverted independently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/CMakeLists.txt | 2 + src/windows/wslcsession/WSLCContainer.cpp | 28 +- src/windows/wslcsession/WSLCSession.cpp | 771 +++++------------- src/windows/wslcsession/WSLCSession.h | 125 +-- .../wslcsession/WSLCSessionRuntime.cpp | 661 +++++++++++++++ src/windows/wslcsession/WSLCSessionRuntime.h | 205 +++++ 6 files changed, 1100 insertions(+), 692 deletions(-) create mode 100644 src/windows/wslcsession/WSLCSessionRuntime.cpp create mode 100644 src/windows/wslcsession/WSLCSessionRuntime.h diff --git a/src/windows/wslcsession/CMakeLists.txt b/src/windows/wslcsession/CMakeLists.txt index 6002df79fb..c2ec8e3955 100644 --- a/src/windows/wslcsession/CMakeLists.txt +++ b/src/windows/wslcsession/CMakeLists.txt @@ -9,6 +9,7 @@ set(SOURCES # Session and container implementation WSLCSession.cpp + WSLCSessionRuntime.cpp WSLCContainer.cpp WSLCVirtualMachine.cpp @@ -44,6 +45,7 @@ set(HEADERS WSLCProcessControl.h WSLCProcessIO.h WSLCSession.h + WSLCSessionRuntime.h WSLCSessionFactory.h WSLCSessionReference.h WSLCVirtualMachine.h diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 14c259b00c..74ece81bd4 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -575,7 +575,7 @@ WSLCContainerImpl::WSLCContainerImpl( // container does not pin the VM: its metadata survives teardown and the VM restarts on next use. if (m_state == WslcContainerStateRunning) { - m_activityHold = ActivityRef(m_wslcSession.IdleStateShared()); + m_activityHold = ActivityRef(m_wslcSession.Runtime().IdleStateShared()); } } @@ -2213,7 +2213,7 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::UpdateActivityHoldLockHeld( const bool active = (m_state == WslcContainerStateRunning); if (active && !m_activityHold) { - m_activityHold = ActivityRef(m_wslcSession.IdleStateShared()); + m_activityHold = ActivityRef(m_wslcSession.Runtime().IdleStateShared()); } else if (!active && m_activityHold) { @@ -2239,7 +2239,7 @@ try *Stdout = {}; *Stderr = {}; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Attach, DetachKeys, Stdin, Stdout, Stderr); } CATCH_RETURN(); @@ -2310,7 +2310,7 @@ try *Process = nullptr; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Exec, Options, StartOptions, Process); } CATCH_RETURN(); @@ -2323,7 +2323,7 @@ try // Hold a VM lease for the whole operation: --rm containers self-delete during Stop, which // disconnects the wrapper and drops activity. Without the lease, the idle worker can fire // during the post-stop destroy wait (up to 60s) and tear the VM down mid-call. - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stop, Signal, TimeoutSeconds, false); } CATCH_RETURN(); @@ -2334,7 +2334,7 @@ try WSLCExecutionContext context(&m_session); // Hold a VM lease for the same reason as Stop(): --rm can self-delete and drop activity. - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stop, Signal, {}, true); } CATCH_RETURN(); @@ -2346,7 +2346,7 @@ try THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCContainerStartFlagsValid), "Invalid flags: 0x%x", Flags); - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Start, Flags, StartOptions); } CATCH_RETURN(); @@ -2360,7 +2360,7 @@ try *Output = nullptr; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Inspect, Output); } CATCH_RETURN(); @@ -2374,7 +2374,7 @@ try *Output = nullptr; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stats, Output); } CATCH_RETURN(); @@ -2391,7 +2391,7 @@ try // can trigger an idle teardown. Without the lease the idle worker could take the session // lock exclusively and clear m_containers (destroying this container) concurrently, racing // the delete and inverting the container->session lock order. - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); auto [lock, impl] = LockImpl(); impl->Delete(Flags); @@ -2421,7 +2421,7 @@ try { WSLCExecutionContext context(&m_session); - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Export, TarHandle); } CATCH_RETURN(); @@ -2437,7 +2437,7 @@ try *Stdout = {}; *Stderr = {}; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Logs, Flags, Stdout, Stderr, Since, Until, Tail); } CATCH_RETURN(); @@ -2616,7 +2616,7 @@ try { COMServiceExecutionContext context; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::ConnectToNetwork, Options); } CATCH_RETURN(); @@ -2626,7 +2626,7 @@ try { COMServiceExecutionContext context; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::DisconnectFromNetwork, NetworkName); } CATCH_RETURN(); diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index b10fa5e609..e470196da7 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -430,12 +430,59 @@ try TraceLoggingValue(m_displayName.c_str(), "DisplayName"), TraceLoggingValue(m_creatorProcessName.c_str(), "CreatorProcess")); - // The VM is created lazily on the first operation that requires it (see EnsureVmRunning) and - // torn down once the session has been continuously idle (activity count zero) for the grace - // period. Wire up the idle-teardown timer; IdleState arms it whenever the activity count drops - // to zero and cancels it when activity resumes, so no dedicated worker thread is needed. const auto idleGracePeriod = m_settings.IdleTimeoutSec > 0 ? std::chrono::seconds(m_settings.IdleTimeoutSec) : c_vmIdleGracePeriod; - m_idleState->Initialize(idleGracePeriod, [this]() { OnIdleTimer(); }); + + WSLCSessionRuntime::RuntimeHooks hooks; + hooks.BringUp = [this]() { + // Configure storage. + ConfigureStorage(m_settings, m_userSid.empty() ? nullptr : reinterpret_cast(m_userSid.data())); + + // Mirror the host's trusted root CAs into the VM before dockerd starts. + InstallTrustedRootCertificates(); + + // Launch containerd first, then dockerd with the external containerd socket. + StartContainerd(); + + // Reset the readiness event before (re)starting dockerd so a stale signal from a prior + // VM instance is not observed. + m_runtime.DockerdReadyEvent().ResetEvent(); + StartDockerd(); + + m_runtime.InitializeDockerRuntime(m_storageVhdPath.parent_path()); + }; + + hooks.RecoverState = [this]() { + RecoverExistingNetworks(); + RecoverExistingContainers(); + }; + + hooks.TearDownSessionState = [this]() { + std::lock_guard containersLock(m_containersLock); + std::lock_guard networksLock(m_networksLock); + + m_containers.clear(); + m_networks.clear(); + }; + + hooks.OnSpontaneousExit = [this]() { LOG_IF_FAILED(Terminate()); }; + + hooks.OnCrashDump = std::bind( + &WSLCSession::OnCrashDumpWritten, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5); + + WSLCSessionRuntime::SessionContext sessionContext; + sessionContext.Id = m_id; + sessionContext.DisplayName = m_displayName; + sessionContext.Terminating = &m_terminating; + sessionContext.SessionTerminatingEvent = std::addressof(m_sessionTerminatingEvent); + sessionContext.SessionTerminatedEvent = std::addressof(m_sessionTerminatedEvent); + + m_runtime.Initialize(m_vmFactoryGitCookie, m_git, &m_settings, idleGracePeriod, std::move(sessionContext), std::move(hooks)); return S_OK; } @@ -485,371 +532,9 @@ void WSLCSession::PersistSettings(const WSLCSessionInitSettings& Settings, PSID m_userSid.assign(bytes, bytes + length); } -bool WSLCSession::IdleTerminationEnabled() const noexcept -{ - // Only tear the VM down when there is persistent storage to recover from. A tmpfs-backed - // session would lose all image/container state on teardown, so its VM is kept alive once started. - return m_settings.StoragePath != nullptr; -} - -void WSLCSession::EnsureVmRunning() -{ - if (m_vmState.load() == VmState::Running) - { - return; - } - - auto lock = m_lock.lock_exclusive(); - - // Do not (re)start the VM once the session is terminating or has terminated. This also - // bounds VmLease's retry loop: a lease that races with Terminate() fails here instead of - // restarting a VM that is being permanently torn down. - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating.load() || m_sessionTerminatedEvent.is_signaled()); - - if (m_vmState.load() == VmState::Running) - { - return; - } - - StartVmLockHeld(); -} - -bool WSLCSession::TryClaimExpectedStop() noexcept -{ - auto expected = VmExitDisposition::Active; - return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::StopRequested); -} - -bool WSLCSession::TryClaimSpontaneousExit() noexcept -{ - auto expected = VmExitDisposition::Active; - return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::ExitClaimed); -} - -void WSLCSession::StartVmLockHeld() -{ - WI_ASSERT(m_vmState.load() != VmState::Running); - - WSL_LOG("WslcVmStarting", TraceLoggingValue(m_id, "SessionId")); - - m_vmState.store(VmState::Starting); - m_vmExitDisposition.store(VmExitDisposition::Active); - - // Tear back down if bring-up fails partway. The VM may have exited on its own during bring-up, - // so claim the stop first and only tear down if we win it (TryClaimExpectedStop()); otherwise - // OnVmExited() owns the teardown and we just release the lock to let its Terminate() finish. - auto startCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { - if (TryClaimExpectedStop()) - { - TearDownVmLockHeld(); - m_vmState.store(VmState::None); - } - else - { - WSL_LOG("WslcVmExitedDuringStart", TraceLoggingValue(m_id, "SessionId")); - } - }); - - // Create a fresh IO relay for this VM instance. The previous one (if any) was stopped - // during teardown and cannot be restarted. - m_ioRelay.emplace(); - - // Create the VM via the factory. Re-fetch the factory from the GIT so we call it through a - // proxy marshalled into this thread's apartment (see m_git). The VM produces crash events; - // the session multiplexes them out to any registered ICrashDumpCallback subscribers via - // OnCrashDumpWritten. - wil::com_ptr vmFactory; - THROW_IF_FAILED(m_git->GetInterfaceFromGlobal(m_vmFactoryGitCookie, __uuidof(IWSLCVirtualMachineFactory), vmFactory.put_void())); - - wil::com_ptr vm; - THROW_IF_FAILED(vmFactory->CreateVirtualMachine(&vm)); - - m_virtualMachine.emplace( - vm.get(), - &m_settings, - m_sessionTerminatingEvent.get(), - std::bind(&WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); - m_virtualMachine->Initialize(); - - // Get an event from the service that is signaled when the VM exits. - m_vmExitedEvent.reset(); - THROW_IF_FAILED(vm->GetTerminationEvent(&m_vmExitedEvent)); - - // Configure storage. - ConfigureStorage(m_settings, m_userSid.empty() ? nullptr : reinterpret_cast(m_userSid.data())); - - // Mirror the host's trusted root CAs into the VM before dockerd starts. - InstallTrustedRootCertificates(); - - // Launch containerd first, then dockerd with the external containerd socket. - StartContainerd(); - - // Reset the readiness event before (re)starting dockerd so a stale signal from a prior - // VM instance is not observed. - m_dockerdReadyEvent.ResetEvent(); - StartDockerd(); - - // Wait for dockerd to be ready before starting the event tracker. - THROW_WIN32_IF_MSG( - ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(m_settings.BootTimeoutMs), "Timed out waiting for dockerd to start"); - - auto [_, __, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread); - - m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000); - - // Start the event tracker. - m_eventTracker.emplace(m_dockerClient.value(), *this, *m_ioRelay); - - m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), m_storageVhdPath.parent_path()); - - // Monitor for unexpected VM exit. - m_ioRelay->AddHandle(std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSession::OnVmExited, this))); - - // Recover any existing resources from storage. - RecoverExistingNetworks(); - RecoverExistingContainers(); - - m_vmState.store(VmState::Running); - startCleanup.release(); - - WSL_LOG("WslcVmStarted", TraceLoggingValue(m_id, "SessionId")); -} - -void WSLCSession::StopVmLockHeld() -{ - if (m_vmState.load() != VmState::Running) - { - return; - } - - WSL_LOG("WslcVmIdleStop", TraceLoggingValue(m_id, "SessionId")); - - // N.B. The caller has claimed StopRequested (via TryClaimExpectedStop), so VM/dockerd/containerd - // exit callbacks firing from the relay thread during teardown are treated as expected, not as a - // crash. - m_vmState.store(VmState::Stopping); - - TearDownVmLockHeld(); - - m_vmState.store(VmState::None); -} - -void WSLCSession::TearDownVmLockHeld(bool CaptureTerminationReason) -{ - std::lock_guard containersLock(m_containersLock); - std::lock_guard networksLock(m_networksLock); - - m_containers.clear(); - m_volumes.reset(); - m_networks.clear(); - - // Stop the IO relay. - // This stops: - // - container state monitoring. - // - container init process relays - // - execs relays - // - container logs relays - if (m_ioRelay) - { - m_ioRelay->Stop(); - } - - { - std::lock_guard allocatedPortsLock(m_allocatedPortsLock); - m_allocatedPorts.clear(); - } - - m_eventTracker.reset(); - m_dockerClient.reset(); - - if (CaptureTerminationReason) - { - // Default: an explicit/graceful teardown is a shutdown (the VM is still alive and we are - // bringing it down). Overridden below if the VM exited on its own and recorded a cause. - m_terminationReason = WSLCVirtualMachineTerminationReasonShutdown; - } - - // Check if the VM has already exited (e.g., killed externally). - // If so, skip operations that require a live VM to avoid unnecessary waits. - // N.B. m_vmExitedEvent may be uninitialized if teardown runs before GetTerminationEvent() succeeds. - if (m_vmExitedEvent && m_vmExitedEvent.is_signaled()) - { - WSL_LOG("SkippingGracefulShutdown_VmDead", TraceLoggingValue(m_id, "SessionId")); - - // The VM exited on its own, so it recorded the cause. - if (CaptureTerminationReason && m_virtualMachine) - { - wil::unique_cotaskmem_string details; - LOG_IF_FAILED(m_virtualMachine->GetTerminationReason(&m_terminationReason, &details)); - m_terminationDetails = details ? details.get() : L""; - } - } - else if (m_virtualMachine) - { - m_virtualMachine->OnSessionTerminated(); - - // Stop dockerd first, then containerd (dockerd is a client of containerd). - // N.B. dockerd waits a couple seconds if there are any outstanding HTTP request sockets opened. - if (m_dockerdProcess.has_value()) - { - auto dockerdExitCode = StopProcess(m_dockerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); - WSL_LOG("DockerdExit", TraceLoggingValue(dockerdExitCode, "code")); - } - - if (m_containerdProcess.has_value()) - { - auto containerdExitCode = StopProcess(m_containerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); - WSL_LOG("ContainerdExit", TraceLoggingValue(containerdExitCode, "code")); - } - - // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running. - try - { - m_virtualMachine->Unmount(c_containerdStorage); - } - CATCH_LOG(); - } - - m_dockerdProcess.reset(); - m_containerdProcess.reset(); - m_virtualMachine.reset(); - m_storageMounted = false; - - // Destroy the relay unless we're on its own thread (~IORelay joins the thread, which would - // deadlock). On unexpected-VM-exit path (runs on relay thread), leave it for ~WSLCSession. - if (!m_ioRelay || !m_ioRelay->IsRelayThread()) - { - m_ioRelay.reset(); - m_vmExitedEvent.reset(); - } - - // Delete the ephemeral swap VHD now that the VM is gone. - if (!m_swapVhdPath.empty()) - { - LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str())); - m_swapVhdPath.clear(); - } -} - -void WSLCSession::OnIdleTimer() -try -{ - // Idle teardown releases cross-process COM proxies (the VM and its VM-scoped state), so this - // threadpool callback must join the process MTA; otherwise those Release/calls fail with - // RPC_E_WRONG_THREAD. The function-try-block keeps this (and everything below) under CATCH_LOG: - // the threadpool callback that invokes us is noexcept, so an escaping throw would terminate. - const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); - - if (m_terminating.load() || !IdleTerminationEnabled()) - { - return; - } - - // Non-blocking acquire: a blocking exclusive would queue behind in-flight operations, and - // SRW locks favor waiting writers, stalling all new ops. If the lock is held, an operation - // is in flight; it holds an activity reference and will re-arm the timer (via the 1->0 - // transition) when it releases, so there is nothing to do here. - auto lock = m_lock.try_lock_exclusive(); - if (!lock) - { - return; - } - - // Re-check every teardown precondition under the lock. The activity count is the single - // source of truth for "the VM is needed"; a 0->1 transition since the timer fired (cancel - // raced the callback) is caught here. - if (m_terminating.load() || m_vmState.load() != VmState::Running || m_idleState->ActivityCount() != 0) - { - return; - } - - // Claim the stop. If we lose, OnVmExited() owns a spontaneous-exit teardown and is spinning for - // this lock, so release it and let that run instead of joining the relay ourselves. - if (!TryClaimExpectedStop()) - { - return; - } - - // Restore Active on completion (or early exit) so the next StartVmLockHeld starts clean; only - // clear our own claim. - auto dispositionCleanup = wil::scope_exit([this]() { - auto stopRequested = VmExitDisposition::StopRequested; - m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active); - }); - - StopVmLockHeld(); -} -CATCH_LOG(); - WSLCSession::VmLease WSLCSession::AcquireVmLease() { - return VmLease(*this); -} - -WSLCSession::VmLease::VmLease(WSLCSession& Session) : m_session(&Session) -{ - // Record an in-flight operation before bringing the VM up so idle teardown cannot tear it down - // between EnsureVmRunning() and acquiring the shared lock. AddActivity cancels any pending idle - // timer. - m_session->m_idleState->AddActivity(); - - auto countCleanup = wil::scope_exit([this]() { - m_session->m_idleState->ReleaseActivity(); - m_session = nullptr; - }); - - // Activity increment may race with idle teardown. Retry until we hold the lock with VM running. - for (;;) - { - m_session->EnsureVmRunning(); - - m_lock = m_session->m_lock.lock_shared(); - - if (m_session->m_vmState.load() == VmState::Running) - { - break; - } - - m_lock.reset(); - } - - countCleanup.release(); -} - -WSLCSession::VmLease::VmLease(VmLease&& Other) noexcept : - m_session(std::exchange(Other.m_session, nullptr)), m_lock(std::move(Other.m_lock)) -{ -} - -WSLCSession::VmLease& WSLCSession::VmLease::operator=(VmLease&& Other) noexcept -{ - if (this != &Other) - { - if (m_session != nullptr) - { - // Release the shared lock before the activity reference so that, if this was the last - // activity, idle teardown can immediately take the exclusive lock. - m_lock.reset(); - m_session->m_idleState->ReleaseActivity(); - } - - m_session = std::exchange(Other.m_session, nullptr); - m_lock = std::move(Other.m_lock); - } - - return *this; -} - -WSLCSession::VmLease::~VmLease() -{ - if (m_session != nullptr) - { - // Release the shared lock before the activity reference so that, if this was the last - // activity, idle teardown can immediately take the exclusive lock. ReleaseActivity arms the - // idle timer on the 1->0 transition. - m_lock.reset(); - m_session->m_idleState->ReleaseActivity(); - } + return m_runtime.AcquireVmLease(); } WSLCSession::~WSLCSession() @@ -874,8 +559,8 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID if (Settings.StoragePath == nullptr) { // If no storage path is specified, use a tmpfs for convenience. - m_virtualMachine->Mount("", c_containerdStorage, "tmpfs", "", 0); - m_storageMounted = true; + m_runtime.Vm().Mount("", c_containerdStorage, "tmpfs", "", 0); + m_runtime.SetStorageMounted(true); return; } @@ -893,7 +578,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID { if (diskLun.has_value()) { - m_virtualMachine->DetachDisk(diskLun.value()); + m_runtime.Vm().DetachDisk(diskLun.value()); } LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_storageVhdPath.c_str())); @@ -901,7 +586,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID }); auto result = - wil::ResultFromException([&]() { diskDevice = m_virtualMachine->AttachDisk(m_storageVhdPath.c_str(), false).second; }); + wil::ResultFromException([&]() { diskDevice = m_runtime.Vm().AttachDisk(m_storageVhdPath.c_str(), false).second; }); if (FAILED(result)) { @@ -933,31 +618,32 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID vhdCreated = true; // Then attach the new disk. - std::tie(diskLun, diskDevice) = m_virtualMachine->AttachDisk(m_storageVhdPath.c_str(), false); + std::tie(diskLun, diskDevice) = m_runtime.Vm().AttachDisk(m_storageVhdPath.c_str(), false); // Then format it. - m_virtualMachine->Ext4Format(diskDevice); + m_runtime.Vm().Ext4Format(diskDevice); } // Mount the device to /root. - m_virtualMachine->Mount(diskDevice.c_str(), c_containerdStorage, "ext4", "", 0); - m_storageMounted = true; + m_runtime.Vm().Mount(diskDevice.c_str(), c_containerdStorage, "ext4", "", 0); + m_runtime.SetStorageMounted(true); // Configure swap on a separate ephemeral VHD. if (Settings.SwapSizeMb > 0) { try { - m_swapVhdPath = storagePath / "swap.vhdx"; - DeleteFileW(m_swapVhdPath.c_str()); // Remove stale swap from prior run - wsl::core::filesystem::CreateVhd(m_swapVhdPath.c_str(), static_cast(Settings.SwapSizeMb) * _1MB, UserSid, false, false); + auto& swapVhdPath = m_runtime.SwapVhdPath(); + swapVhdPath = storagePath / "swap.vhdx"; + DeleteFileW(swapVhdPath.c_str()); // Remove stale swap from prior run + wsl::core::filesystem::CreateVhd(swapVhdPath.c_str(), static_cast(Settings.SwapSizeMb) * _1MB, UserSid, false, false); - auto [_, swapDevice] = m_virtualMachine->AttachDisk(m_swapVhdPath.c_str(), false); + auto [_, swapDevice] = m_runtime.Vm().AttachDisk(swapVhdPath.c_str(), false); // Fire-and-forget: mkswap + swapon runs asynchronously since swap is best-effort. auto cmd = std::format("/usr/sbin/mkswap {0} && /usr/sbin/swapon {0}", swapDevice); ServiceProcessLauncher launcher("/bin/sh", {"/bin/sh", "-c", cmd}); - launcher.Launch(*m_virtualMachine); + launcher.Launch(m_runtime.Vm()); } catch (...) { @@ -991,7 +677,7 @@ CATCH_RETURN(); void WSLCSession::OnDockerdExited() { - if (!m_sessionTerminatingEvent.is_signaled() && m_vmExitDisposition.load() != VmExitDisposition::StopRequested) + if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDispositionAtomic().load() != WSLCSessionRuntime::VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedDockerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } @@ -999,32 +685,12 @@ void WSLCSession::OnDockerdExited() void WSLCSession::OnContainerdExited() { - if (!m_sessionTerminatingEvent.is_signaled() && m_vmExitDisposition.load() != VmExitDisposition::StopRequested) + if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDispositionAtomic().load() != WSLCSessionRuntime::VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedContainerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } } -void WSLCSession::OnVmExited() -{ - // A spontaneous exit we must permanently terminate, unless an expected stop already claimed it, - // in which case the exit was wanted and we decline. - if (!TryClaimSpontaneousExit()) - { - WSL_LOG("WslcVmExitedDuringStop", TraceLoggingValue(m_id, "SessionId")); - return; - } - - WSL_LOG( - "VmExited", - TraceLoggingLevel(WINEVENT_LEVEL_WARNING), - TraceLoggingValue(m_id, "SessionId"), - TraceLoggingValue(m_displayName.c_str(), "Name"), - TraceLoggingValue(!m_sessionTerminatingEvent.is_signaled(), "Unexpected")); - - LOG_IF_FAILED(Terminate()); -} - void WSLCSession::OnProcessLog(const gsl::span& Buffer, PCSTR Source) try { @@ -1040,11 +706,11 @@ try TraceLoggingValue(entry.c_str(), "Content"), TraceLoggingValue(m_displayName.c_str(), "Name")); - if (!m_dockerdReadyEvent.is_signaled()) + if (!m_runtime.DockerdReadyEvent().is_signaled()) { if (entry.find(c_dockerdReadyLogLine) != std::string::npos) { - m_dockerdReadyEvent.SetEvent(); + m_runtime.DockerdReadyEvent().SetEvent(); } } } @@ -1055,15 +721,15 @@ ServiceRunningProcess WSLCSession::StartProcess( { ServiceProcessLauncher launcher{Executable, Args, {{"PATH=/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin"}}}; - auto process = launcher.Launch(*m_virtualMachine); + auto process = launcher.Launch(m_runtime.Vm()); - m_ioRelay->AddHandle(std::make_unique( + m_runtime.Relay()->AddHandle(std::make_unique( process.GetStdHandle(1), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false)); - m_ioRelay->AddHandle(std::make_unique( + m_runtime.Relay()->AddHandle(std::make_unique( process.GetStdHandle(2), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false)); - m_ioRelay->AddHandle(std::make_unique(process.GetExitEvent(), std::move(ExitCallback))); + m_runtime.Relay()->AddHandle(std::make_unique(process.GetExitEvent(), std::move(ExitCallback))); return process; } @@ -1081,7 +747,7 @@ void WSLCSession::StartContainerd() args.emplace_back("debug"); } - m_containerdProcess = StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this)); + m_runtime.ContainerdProcess() = StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this)); WSL_LOG("ContainerdStarted"); } @@ -1094,7 +760,7 @@ void WSLCSession::StartDockerd() args.emplace_back("--debug"); } - m_dockerdProcess = StartProcess("/usr/bin/dockerd", args, "dockerd", std::bind(&WSLCSession::OnDockerdExited, this)); + m_runtime.DockerdProcess() = StartProcess("/usr/bin/dockerd", args, "dockerd", std::bind(&WSLCSession::OnDockerdExited, this)); WSL_LOG("DockerdStarted"); } @@ -1114,7 +780,7 @@ try const auto script = std::format("cat > '{}'", c_certPath); ServiceProcessLauncher launcher("/bin/sh", {"/bin/sh", "--norc", "-c", script}, {}, WSLCProcessFlagsStdin); - auto process = launcher.Launch(*m_virtualMachine); + auto process = launcher.Launch(m_runtime.Vm()); std::unique_ptr writeStdin( new WriteHandle(process.GetStdHandle(WSLCFDStdin), std::vector{pem.begin(), pem.end()})); @@ -1271,8 +937,8 @@ try auto [repo, tagOrDigest] = wslutil::ParseImage(Image); EnforceRegistryAllowlist(repo); - auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto runtime = m_runtime.Acquire(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); if (!tagOrDigest.has_value()) { @@ -1286,7 +952,7 @@ try registryAuth = std::string(RegistryAuthenticationInformation); } - auto requestContext = m_dockerClient->PullImage(repo, tagOrDigest, registryAuth); + auto requestContext = runtime.Docker().PullImage(repo, tagOrDigest, registryAuth); StreamImageOperation(*requestContext, Image, "Pull", ProgressCallback); OnImageCreated(Image); @@ -1329,16 +995,16 @@ try comCall = RegisterUserCOMCallback(); } - auto lock = AcquireVmLease(); + auto runtime = m_runtime.Acquire(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); GUID volumeId{}; THROW_IF_FAILED(CoCreateGuid(&volumeId)); auto mountPath = std::format("/mnt/{}", wsl::shared::string::GuidToString(volumeId)); - THROW_IF_FAILED(m_virtualMachine->MountWindowsFolder(Options->ContextPath, mountPath.c_str(), TRUE)); + THROW_IF_FAILED(runtime.Vm().MountWindowsFolder(Options->ContextPath, mountPath.c_str(), TRUE)); auto unmountFolder = - wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { m_virtualMachine->UnmountWindowsFolder(mountPath.c_str()); }); + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { runtime.Vm().UnmountWindowsFolder(mountPath.c_str()); }); std::vector buildArgs{"/usr/bin/docker", "build", "--progress=rawjson"}; if (WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsNoCache)) @@ -1383,7 +1049,7 @@ try WSL_LOG("BuildImageStart", TraceLoggingValue(wsl::shared::string::Join(buildArgs, ' ').c_str(), "Command")); ServiceProcessLauncher buildLauncher(buildArgs[0], buildArgs, {}, WSLCProcessFlagsStdin); - auto buildProcess = buildLauncher.Launch(*m_virtualMachine); + auto buildProcess = buildLauncher.Launch(runtime.Vm()); auto io = CreateIOContext(); @@ -1660,9 +1326,9 @@ try auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto requestContext = m_dockerClient->LoadImage(ContentSize); + auto requestContext = m_runtime.Docker().LoadImage(ContentSize); std::ignore = ImportImageImpl(*requestContext, ImageHandle, LoadCallback); @@ -1693,9 +1359,9 @@ try auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto requestContext = m_dockerClient->ImportImage(repo, tag, ContentSize); + auto requestContext = m_runtime.Docker().ImportImage(repo, tag, ContentSize); auto imageId = ImportImageImpl(*requestContext, ImageHandle); THROW_HR_IF_MSG(E_UNEXPECTED, !imageId.has_value(), "Docker import succeeded but did not return an image ID"); @@ -1725,7 +1391,7 @@ std::optional WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRe comCall = RegisterUserCOMCallback(); } - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); auto io = CreateIOContext(); @@ -1864,9 +1530,9 @@ try RETURN_HR_IF(E_INVALIDARG, strlen(ImageNameOrID) > WSLC_MAX_IMAGE_NAME_LENGTH); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto retVal = m_dockerClient->SaveImage(ImageNameOrID); + auto retVal = m_runtime.Docker().SaveImage(ImageNameOrID); SaveImageImpl(retVal, OutHandle, CancelEvent); return S_OK; } @@ -1897,9 +1563,9 @@ try auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto retVal = m_dockerClient->SaveImages(names); + auto retVal = m_runtime.Docker().SaveImages(names); SaveImageImpl(retVal, OutHandle, CancelEvent); return S_OK; } @@ -1909,7 +1575,7 @@ void WSLCSession::SaveImageImpl(std::pair& SocketC { auto userHandle = OpenUserHandle(OutputHandle); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); auto io = CreateIOContext(CancelEvent); @@ -1972,12 +1638,12 @@ try auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::vector images; try { - images = m_dockerClient->ListImages(all, digests, filters); + images = m_runtime.Docker().ListImages(all, digests, filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list images"); @@ -2081,12 +1747,12 @@ try auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::vector deletedImages; try { - deletedImages = m_dockerClient->DeleteImage( + deletedImages = m_runtime.Docker().DeleteImage( Options->Image, WI_IsFlagSet(Options->Flags, WSLCDeleteImageFlagsForce), WI_IsFlagSet(Options->Flags, WSLCDeleteImageFlagsNoPrune)); } catch (const DockerHTTPException& e) @@ -2155,11 +1821,11 @@ try auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); try { - m_dockerClient->TagImage(Options->Image, Options->Repo, Options->Tag); + m_runtime.Docker().TagImage(Options->Image, Options->Repo, Options->Tag); } catch (const DockerHTTPException& e) { @@ -2191,9 +1857,9 @@ try EnforceRegistryAllowlist(repo); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto requestContext = m_dockerClient->PushImage(repo, tagOrDigest, RegistryAuthenticationInformation); + auto requestContext = m_runtime.Docker().PushImage(repo, tagOrDigest, RegistryAuthenticationInformation); StreamImageOperation(*requestContext, Image, "Push", ProgressCallback); return S_OK; @@ -2212,7 +1878,7 @@ try *Output = nullptr; auto lock = AcquireVmLease(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); *Output = wil::make_unique_ansistring(InspectImageLockHeld(ImageNameOrId).c_str()).release(); @@ -2225,7 +1891,7 @@ std::string WSLCSession::InspectImageLockHeld(const std::string& NameOrId) docker_schema::InspectImage dockerInspect; try { - dockerInspect = m_dockerClient->InspectImage(NameOrId); + dockerInspect = m_runtime.Docker().InspectImage(NameOrId); } catch (const DockerHTTPException& e) { @@ -2260,13 +1926,13 @@ try *IdentityToken = nullptr; auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); wil::unique_cotaskmem_ansistring token; try { - auto response = m_dockerClient->Authenticate(ServerAddress, Username, Password); + auto response = m_runtime.Docker().Authenticate(ServerAddress, Username, Password); token = wil::make_unique_ansistring(response.c_str()); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to authenticate with registry: %hs", ServerAddress); @@ -2292,12 +1958,12 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); auto lock = AcquireVmLease(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); docker_schema::PruneImageResult pruneResult; try { - pruneResult = m_dockerClient->PruneImages(filters); + pruneResult = m_runtime.Docker().PruneImages(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune images"); @@ -2373,10 +2039,10 @@ CATCH_RETURN(); void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptions, IWSLCContainer** Container) { - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_eventTracker); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasEvents()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); // Validate that name & images are valid. if (containerOptions->Name != nullptr && containerOptions->Name[0] != '\0') @@ -2423,14 +2089,14 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio *containerOptions, containerName, *this, - m_virtualMachine.value(), + m_runtime.Vm(), m_pluginNotifier.get(), m_networks, - m_volumes.value(), + m_runtime.Volumes(), std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), - m_eventTracker.value(), - m_dockerClient.value(), - *m_ioRelay); + m_runtime.Events(), + m_runtime.Docker(), + *m_runtime.Relay()); // Key the map by Docker's container ID, which is set in the WSLCContainerImpl constructor and stable for its lifetime. auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container)); @@ -2478,7 +2144,7 @@ try try { - inspectResult = m_dockerClient->InspectContainer(Id); + inspectResult = m_runtime.Docker().InspectContainer(Id); } catch (DockerHTTPException& e) { @@ -2534,14 +2200,14 @@ Microsoft::WRL::ComPtr WSLCSession::CreateActivityToken() { // Record the in-flight activity up front so the VM cannot idle-terminate before the caller // takes ownership of the returned token. - m_idleState->AddActivity(); - auto countCleanup = wil::scope_exit([this]() { m_idleState->ReleaseActivity(); }); + m_runtime.Idle().AddActivity(); + auto countCleanup = wil::scope_exit([this]() { m_runtime.Idle().ReleaseActivity(); }); auto operation = Microsoft::WRL::Make(); THROW_IF_NULL_ALLOC(operation.Get()); // Capture shared idle state so token can outlive session and release activity without keeping session alive. - std::shared_ptr idleState = m_idleState; + std::shared_ptr idleState = m_runtime.IdleStateShared(); operation->Initialize([idleState = std::move(idleState)]() { idleState->ReleaseActivity(); }); // The token now owns the activity-count reference and will release it on destruction. @@ -2608,12 +2274,12 @@ try } auto lock = AcquireVmLease(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::vector dockerContainers; try { - dockerContainers = m_dockerClient->ListContainers(all, limit, filters); + dockerContainers = m_runtime.Docker().ListContainers(all, limit, filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list containers"); @@ -2687,7 +2353,7 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); auto lock = AcquireVmLease(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::lock_guard containersLock{m_containersLock}; @@ -2695,7 +2361,7 @@ try try { - pruneResult = m_dockerClient->PruneContainers(filters); + pruneResult = m_runtime.Docker().PruneContainers(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune containers"); @@ -2757,10 +2423,10 @@ try *Errno = -1; // Make sure not to return 0 if something fails. } - auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto runtime = m_runtime.Acquire(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - auto process = m_virtualMachine->CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno); + auto process = runtime.Vm().CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno); // The VmLease above is released when this call returns, but the process keeps running in the // VM and the client holds the returned proxy. A root-namespace process is not tracked as a @@ -2779,7 +2445,7 @@ void WSLCSession::Ext4Format(const std::string& Device) { constexpr auto mkfsPath = "/usr/sbin/mkfs.ext4"; ServiceProcessLauncher launcher(mkfsPath, {mkfsPath, Device}); - auto result = launcher.Launch(*m_virtualMachine).WaitAndCaptureOutput(); + auto result = launcher.Launch(m_runtime.Vm()).WaitAndCaptureOutput(); THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str()); } @@ -2792,16 +2458,16 @@ try THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Path), !std::filesystem::path(Path).is_absolute()); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); // Attach the disk to the VM (AttachDisk() performs the access check for the VHD file). - auto [lun, device] = m_virtualMachine->AttachDisk(Path, false); + auto [lun, device] = m_runtime.Vm().AttachDisk(Path, false); // N.B. DetachDisk calls sync() before detaching. - auto detachDisk = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, lun]() { m_virtualMachine->DetachDisk(lun); }); + auto detachDisk = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, lun]() { m_runtime.Vm().DetachDisk(lun); }); // Format it to ext4. - m_virtualMachine->Ext4Format(device); + m_runtime.Vm().Ext4Format(device); return S_OK; } @@ -2820,14 +2486,14 @@ try auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCVolumeMetadataLabel); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); if (Options->Name != nullptr && Options->Name[0] != '\0') { ValidateName(Options->Name, WSLC_MAX_VOLUME_NAME_LENGTH); } - *VolumeInfo = m_volumes->CreateVolume(Options->Name, Options->Driver, std::move(driverOpts), std::move(labels)); + *VolumeInfo = m_runtime.Volumes().CreateVolume(Options->Name, Options->Driver, std::move(driverOpts), std::move(labels)); return S_OK; } CATCH_RETURN(); @@ -2840,9 +2506,9 @@ try RETURN_HR_IF_NULL(E_POINTER, Name); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); - m_volumes->DeleteVolume(Name); + m_runtime.Volumes().DeleteVolume(Name); return S_OK; } CATCH_RETURN(); @@ -2861,9 +2527,9 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); - auto volumeList = m_volumes->ListVolumes(std::move(filters)); + auto volumeList = m_runtime.Volumes().ListVolumes(std::move(filters)); if (volumeList.empty()) { @@ -2893,9 +2559,9 @@ try ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); - std::string json = m_volumes->InspectVolume(name); + std::string json = m_runtime.Volumes().InspectVolume(name); *Output = wil::make_unique_ansistring(json.c_str()).release(); return S_OK; @@ -2918,12 +2584,12 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); WSLCVolumes::PruneVolumesResult pruneResult; try { - pruneResult = m_volumes->PruneVolumes(filters); + pruneResult = m_runtime.Volumes().PruneVolumes(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune volumes"); @@ -2949,32 +2615,6 @@ try } CATCH_RETURN(); -int WSLCSession::StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs) -{ - auto signalResult = Process.Get().Signal(WSLCSignalSIGTERM); - if (FAILED(signalResult)) - { - LOG_HR_MSG(signalResult, "Failed to terminate process %i", Process.Get().GetPid()); - return -1; - } - - try - { - return Process.Wait(TerminateTimeoutMs); - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - try - { - LOG_IF_FAILED(Process.Get().Signal(WSLCSignalSIGKILL)); - return Process.Wait(KillTimeoutMs); - } - CATCH_LOG(); - } - - return -1; -} // Network management. HRESULT WSLCSession::CreateNetwork(const WSLCNetworkOptions* Options, IWarningCallback* WarningCallback) @@ -2997,8 +2637,8 @@ try auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCNetworkManagedLabel); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); std::lock_guard networksLock(m_networksLock); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_networks.contains(name)); @@ -3037,7 +2677,7 @@ try docker_schema::CreateNetworkResponse createResult; try { - createResult = m_dockerClient->CreateNetwork(request); + createResult = m_runtime.Docker().CreateNetwork(request); } catch (const DockerHTTPException& e) { @@ -3051,14 +2691,14 @@ try EMIT_USER_WARNING(wsl::shared::string::MultiByteToWide(createResult.Warning)); } - auto removeNetworkCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_dockerClient->RemoveNetwork(name); }); + auto removeNetworkCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_runtime.Docker().RemoveNetwork(name); }); // Inspect the newly created network to cache full properties (IPAM, Scope, etc.) // since CreateNetworkResponse only returns {Id, Warning}. docker_schema::Network full; try { - full = m_dockerClient->InspectNetwork(name); + full = m_runtime.Docker().InspectNetwork(name); } catch (const DockerHTTPException& e) { @@ -3106,8 +2746,8 @@ try ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); std::lock_guard networksLock(m_networksLock); @@ -3116,7 +2756,7 @@ try try { - m_dockerClient->RemoveNetwork(name); + m_runtime.Docker().RemoveNetwork(name); } catch (const DockerHTTPException& e) { @@ -3240,15 +2880,15 @@ try filters["label"].push_back(WSLCNetworkManagedLabel); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); std::lock_guard networksLock(m_networksLock); docker_schema::PruneNetworkResult pruneResult; try { - pruneResult = m_dockerClient->PruneNetworks(filters); + pruneResult = m_runtime.Docker().PruneNetworks(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune networks"); @@ -3319,10 +2959,10 @@ bool WSLCSession::WaitForEventOrSessionTerminating(HANDLE Event, std::chrono::mi HRESULT WSLCSession::Terminate() try { - // Ensure only one Terminate() runs. This must be checked before taking m_lock + // Ensure only one Terminate() runs. This must be checked before taking m_runtime.Lock() // because OnVmExited() is called from the IORelay thread — if an external Terminate() - // holds m_lock and calls m_ioRelay.Stop(), the relay thread must not re-enter - // Terminate() and deadlock on m_lock. + // holds m_runtime.Lock() and calls m_runtime.Relay()->Stop(), the relay thread must not re-enter + // Terminate() and deadlock on m_runtime.Lock(). if (m_terminating.exchange(true)) { return S_OK; @@ -3344,8 +2984,8 @@ try { std::lock_guard lock(m_userHandlesLock); - // m_sessionTerminatingEvent is always valid, so it can be signalled without holding m_lock. - // This allows a session to be unblocked if a stuck operation is holding m_lock. + // m_sessionTerminatingEvent is always valid, so it can be signalled without holding m_runtime.Lock(). + // This allows a session to be unblocked if a stuck operation is holding m_runtime.Lock(). // N.B. This must happen under m_userHandlesLock to synchronize with potentially running operations. if (!m_sessionTerminatingEvent.is_signaled()) { @@ -3365,32 +3005,11 @@ try CancelUserCOMCallbacks(); } - sessionLock = m_lock.try_lock_exclusive(); + sessionLock = m_runtime.Lock().try_lock_exclusive(); retrying = true; } - // Acquire an exclusive lock to ensure that no operation is running. - WI_VERIFY(sessionLock); - - // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason. - // This mirrors the soft teardown used for idle shutdown, but here it is permanent. - TearDownVmLockHeld(/* CaptureTerminationReason */ true); - - m_vmState.store(VmState::None); - - // Signal completion last so any observer of the terminated event sees a fully torn-down - // session and a populated termination reason. - m_sessionTerminatedEvent.SetEvent(); - - // Release the exclusive lock before disarming the idle timer. If a timer callback is currently - // blocked acquiring the exclusive lock (about to evaluate idle teardown), it must be able to - // obtain it, observe m_terminating, and return — otherwise Disarm()'s wait for in-flight - // callbacks below would deadlock. - sessionLock.reset(); - - // Permanently disable idle teardown and drain any in-flight timer callback so it cannot - // reference this session after it is destroyed. - m_idleState->Disarm(); + m_runtime.Shutdown(sessionLock, m_terminationReason, m_terminationDetails); // Idle teardown is disabled and no operation can run past termination, so the parked VM // factory can no longer be re-fetched; revoke it from the GIT. @@ -3468,9 +3087,9 @@ try RETURN_HR_IF_NULL(E_POINTER, LinuxPath); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - return m_virtualMachine->MountWindowsFolder(WindowsPath, LinuxPath, ReadOnly); + return m_runtime.Vm().MountWindowsFolder(WindowsPath, LinuxPath, ReadOnly); } CATCH_RETURN(); @@ -3482,9 +3101,9 @@ try RETURN_HR_IF_NULL(E_POINTER, LinuxPath); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - return m_virtualMachine->UnmountWindowsFolder(LinuxPath); + return m_runtime.Vm().UnmountWindowsFolder(LinuxPath); } CATCH_RETURN(); @@ -3494,35 +3113,36 @@ try WSLCExecutionContext context(this); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - std::lock_guard allocatedPortsLock(m_allocatedPortsLock); + std::lock_guard allocatedPortsLock(m_runtime.AllocatedPortsLock()); // Look for an existing allocation first. - auto it = m_allocatedPorts.find(LinuxPort); + auto& allocatedPorts = m_runtime.AllocatedPorts(); + auto it = allocatedPorts.find(LinuxPort); bool inserted = false; auto cleanup = wil::scope_exit([&]() { if (inserted) { - m_allocatedPorts.erase(it); + allocatedPorts.erase(it); } }); - if (it == m_allocatedPorts.end()) + if (it == allocatedPorts.end()) { // No existing port allocation, create a new one. - auto allocated = std::make_pair(m_virtualMachine->TryAllocatePort(LinuxPort, Family, IPPROTO_TCP), static_cast(0)); + auto allocated = std::make_pair(m_runtime.Vm().TryAllocatePort(LinuxPort, Family, IPPROTO_TCP), static_cast(0)); THROW_HR_IF(HRESULT_FROM_WIN32(WSAEADDRINUSE), allocated.first == nullptr); - it = m_allocatedPorts.emplace(LinuxPort, allocated).first; + it = allocatedPorts.emplace(LinuxPort, allocated).first; inserted = true; } auto mapping = VMPortMapping::LocalhostTcpMapping(Family, WindowsPort); mapping.AssignVmPort(it->second.first); - m_virtualMachine->MapPort(mapping); + m_runtime.Vm().MapPort(mapping); // Increase usage count. it->second.second++; @@ -3540,27 +3160,28 @@ try WSLCExecutionContext context(this); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - std::lock_guard allocatedPortsLock(m_allocatedPortsLock); + std::lock_guard allocatedPortsLock(m_runtime.AllocatedPortsLock()); - auto it = m_allocatedPorts.find(LinuxPort); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_allocatedPorts.end()); + auto& allocatedPorts = m_runtime.AllocatedPorts(); + auto it = allocatedPorts.find(LinuxPort); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == allocatedPorts.end()); auto mapping = VMPortMapping::LocalhostTcpMapping(Family, WindowsPort); mapping.AssignVmPort(it->second.first); - mapping.Attach(m_virtualMachine.value()); + mapping.Attach(m_runtime.Vm()); auto cleanup = wil::scope_exit([&]() { mapping.Release(); }); - m_virtualMachine->UnmapPort(mapping); + m_runtime.Vm().UnmapPort(mapping); it->second.second--; // If usage count drops to 0, release the port allocation. if (it->second.second == 0) { - m_allocatedPorts.erase(it); + allocatedPorts.erase(it); } return S_OK; @@ -3906,11 +3527,11 @@ CATCH_RETURN(); void WSLCSession::RecoverExistingContainers() { - WI_ASSERT(m_dockerClient.has_value()); - WI_ASSERT(m_eventTracker.has_value()); - WI_ASSERT(m_virtualMachine.has_value()); + WI_ASSERT(m_runtime.HasDocker()); + WI_ASSERT(m_runtime.HasEvents()); + WI_ASSERT(m_runtime.HasVm()); - auto containers = m_dockerClient->ListContainers(true); // all=true to include stopped containers + auto containers = m_runtime.Docker().ListContainers(true); // all=true to include stopped containers for (const auto& dockerContainer : containers) { @@ -3919,13 +3540,13 @@ void WSLCSession::RecoverExistingContainers() auto container = WSLCContainerImpl::Open( dockerContainer, *this, - m_virtualMachine.value(), + m_runtime.Vm(), m_pluginNotifier.get(), - m_volumes.value(), + m_runtime.Volumes(), std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), - m_eventTracker.value(), - m_dockerClient.value(), - *m_ioRelay); + m_runtime.Events(), + m_runtime.Docker(), + *m_runtime.Relay()); auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container)); WI_ASSERT(inserted); @@ -3946,10 +3567,10 @@ void WSLCSession::RecoverExistingContainers() void WSLCSession::RecoverExistingNetworks() { - WI_ASSERT(m_dockerClient.has_value()); - WI_ASSERT(m_virtualMachine.has_value()); + WI_ASSERT(m_runtime.HasDocker()); + WI_ASSERT(m_runtime.HasVm()); - auto networks = m_dockerClient->ListNetworks(); + auto networks = m_runtime.Docker().ListNetworks(); std::lock_guard networksLock(m_networksLock); diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index 8eff3eb4a3..8b2c7332f1 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -20,6 +20,7 @@ Module Name: #include "WSLCContainer.h" #include "WSLCIdleState.h" #include "WSLCVolumes.h" +#include "WSLCSessionRuntime.h" #include "WSLCNetworkMetadata.h" #include "DockerEventTracker.h" #include "DockerHTTPClient.h" @@ -85,7 +86,9 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession friend class WSLCContainer; public: - WSLCSession() = default; + WSLCSession() : m_runtime(*this) + { + } ~WSLCSession(); @@ -273,7 +276,17 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession // keeping the session object itself alive. std::shared_ptr IdleStateShared() const noexcept { - return m_idleState; + return m_runtime.IdleStateShared(); + } + + WSLCSessionRuntime& Runtime() noexcept + { + return m_runtime; + } + + const WSLCSessionRuntime& Runtime() const noexcept + { + return m_runtime; } // Creates an opaque activity token that holds a reference on this session's activity count for @@ -285,101 +298,30 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession private: ULONG m_id = 0; - - // VM lifecycle state for on-demand creation / idle termination. - enum class VmState - { - None, - Starting, - Running, - Stopping, - }; - - // Single-owner arbitration for a VM exit: exactly one side tears a given VM instance down, - // resolved atomically. An expected stop (idle teardown or bring-up cleanup, on a lock-holding - // thread) and a spontaneous exit (OnVmExited, lock-free on the relay thread) each try to claim - // it; the loser declines. This avoids both a missed teardown and a deadlock (a lock-holder - // joining the relay thread while OnVmExited spins for the lock in Terminate()). A fresh VM - // starts Active. Claim via TryClaim*(), not by touching the atomic directly. - enum class VmExitDisposition - { - Active, // Running normally; a VM exit is unexpected and triggers a permanent Terminate(). - StopRequested, // An expected (soft) stop is in progress; OnVmExited treats the exit as expected. - ExitClaimed, // OnVmExited owns the permanent teardown of a spontaneous exit. - }; - - // Claims an expected (soft) stop of the current VM from a lock-holding thread. On success a - // racing OnVmExited() declines, so the caller may tear down (joining the relay thread is safe). - // On failure OnVmExited() already owns a spontaneous-exit teardown and is spinning for the lock - // in Terminate(); the caller must not tear down or it deadlocks joining the relay. - [[nodiscard]] bool TryClaimExpectedStop() noexcept; - - // Claims the teardown of a spontaneous VM exit from OnVmExited(). Fails if an expected stop is - // already in progress, in which case the exit was wanted and the caller declines. - [[nodiscard]] bool TryClaimSpontaneousExit() noexcept; - - _Requires_exclusive_lock_held_(m_lock) - void StartVmLockHeld(); - _Requires_exclusive_lock_held_(m_lock) - void StopVmLockHeld(); - _Requires_exclusive_lock_held_(m_lock) - void TearDownVmLockHeld(bool CaptureTerminationReason = false); - void EnsureVmRunning(); - - // Idle-teardown callback invoked by IdleState's timer once the VM has been continuously idle - // (activity count zero) for the grace period. Runs on a threadpool thread. - void OnIdleTimer(); - bool IdleTerminationEnabled() const noexcept; void PersistSettings(const WSLCSessionInitSettings& Settings, PSID UserSid); - // RAII lease taken at the top of every VM-requiring operation. On construction it - // ensures the VM is running (lazily restarting it if it was idle-terminated) and records - // an in-flight operation so idle teardown is deferred; it then holds the shared session - // lock for the operation's duration. On destruction it releases the lock and triggers an - // idle check. - class VmLease - { - public: - VmLease() = default; - explicit VmLease(WSLCSession& Session); - VmLease(VmLease&& Other) noexcept; - VmLease& operator=(VmLease&& Other) noexcept; - ~VmLease(); - - VmLease(const VmLease&) = delete; - VmLease& operator=(const VmLease&) = delete; - - private: - WSLCSession* m_session{}; - wil::rwlock_release_shared_scope_exit m_lock; - }; - + using VmLease = WSLCSessionRuntime::VmLease; [[nodiscard]] VmLease AcquireVmLease(); __requires_lock_held(m_userHandlesLock) void CancelUserHandleIO(); __requires_lock_held(m_userCOMCallbacksLock) void CancelUserCOMCallbacks(); - _Requires_shared_lock_held_(m_lock) void CreateContainerImpl(const WSLCContainerOptions* Options, IWSLCContainer** Container); void ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID UserSid); void Ext4Format(const std::string& Device); - _Requires_shared_lock_held_(m_lock) std::string InspectImageLockHeld(const std::string& Id); void OnContainerDeleted(const WSLCContainerImpl* Container); void OnCrashDumpWritten(const std::wstring& DumpPath, const std::string& ProcessName, ULONG Pid, ULONG Signal, ULONGLONG Timestamp); - _Requires_shared_lock_held_(m_lock) void OnImageCreated(const std::string& ImageNameOrId) noexcept; - _Requires_shared_lock_held_(m_lock) void OnImageDeleted(const std::string& ImageId) noexcept; void OnProcessLog(const gsl::span& Data, PCSTR Source); void OnContainerdExited(); void OnDockerdExited(); - void OnVmExited(); ServiceRunningProcess StartProcess( const std::string& Executable, const std::vector& Args, PCSTR LogSource, std::function&& ExitCallback); void InstallTrustedRootCertificates(); @@ -394,8 +336,6 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession void SaveImageImpl(std::pair& RequestCodePair, WSLCHandle OutputHandle, HANDLE CancelEvent); void StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback); - std::optional m_dockerClient; - // The VM factory is a cross-process proxy supplied by the SYSTEM service at Initialize() time // but first used later (on demand) from a different thread/apartment. A directly stored proxy // would fail with RPC_E_WRONG_THREAD, so it is parked in the process Global Interface Table and @@ -403,41 +343,24 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession wil::com_ptr m_git; DWORD m_vmFactoryGitCookie{}; - std::optional m_virtualMachine; - std::optional m_eventTracker; - wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; + WSLCSessionRuntime m_runtime; std::wstring m_displayName; std::wstring m_creatorProcessName; std::filesystem::path m_storageVhdPath; - std::filesystem::path m_swapVhdPath; - bool m_storageMounted = false; - // N.B. m_lock must be acquired before acquiring m_containersLock or m_networksLock. - // These locks protect m_containers without requiring an exclusive m_lock. + // N.B. m_runtime.Lock() must be acquired before acquiring m_containersLock or m_networksLock. + // These locks protect m_containers without requiring an exclusive m_runtime.Lock(). // This allows independent operations to proceed while container bookkeeping remains synchronized. - // WSLCVolumes has its own internal srwlock and does not require m_lock. + // WSLCVolumes has its own internal srwlock and does not require m_runtime.Lock(). std::mutex m_containersLock; std::unordered_map> m_containers; - std::optional m_volumes; std::mutex m_networksLock; std::unordered_map m_networks; wil::unique_event m_sessionTerminatingEvent{wil::EventOptions::ManualReset}; wil::unique_event m_sessionTerminatedEvent{wil::EventOptions::ManualReset}; - wil::unique_event m_vmExitedEvent; WSLCVirtualMachineTerminationReason m_terminationReason{WSLCVirtualMachineTerminationReasonUnknown}; std::wstring m_terminationDetails; - wil::srwlock m_lock; - std::optional m_ioRelay; - - // VM lifecycle / idle-termination state. - std::atomic m_vmState{VmState::None}; - std::atomic m_vmExitDisposition{VmExitDisposition::Active}; - // In-flight activity count, idle timer and teardown callback, decoupled from this object's - // lifetime (see IdleState in WSLCIdleState.h) so activity tokens and container COM wrappers can - // safely manage activity without keeping the session alive. See WSLCContainerImpl's ActivityRef - // (m_activityHold), WSLCSession::VmLease and CreateActivityToken(). - std::shared_ptr m_idleState{std::make_shared()}; // Persisted settings required to (re)create the VM on demand. The string fields point // into the owned storage members below (or m_displayName) so they remain valid for the @@ -448,8 +371,6 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession std::optional m_settingsRootVhdTypeOverride; std::vector m_userSid; - std::optional m_containerdProcess; - std::optional m_dockerdProcess; WSLCFeatureFlags m_featureFlags{}; std::function m_destructionCallback; std::atomic m_terminating{false}; @@ -468,14 +389,12 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession // survive insertions and unrelated erasures, so each CrashDumpSubscription stashes its own // iterator and uses it as an O(1) removal handle when the last reference is released. // The session's lifetime extends past Terminate() (the COM object outlives the VM), so this - // list may outlive m_virtualMachine; that's fine because dispatch only runs while the VM + // list may outlive the runtime VM instance; that's fine because dispatch only runs while the VM // thread is alive. mutable wil::srwlock m_crashDumpLock; _Guarded_by_(m_crashDumpLock) CrashDumpCallbackList m_crashDumpCallbacks; - // Used for testing only. - std::mutex m_allocatedPortsLock; - __guarded_by(m_allocatedPortsLock) std::map, size_t>> m_allocatedPorts; + friend class WSLCSessionRuntime; }; } // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp new file mode 100644 index 0000000000..88bc3f769b --- /dev/null +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -0,0 +1,661 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCSessionRuntime.cpp + +Abstract: + + Contains the implementation for WSLCSessionRuntime. + +--*/ + +#include "precomp.h" +#include "WSLCSessionRuntime.h" +#include "WSLCSession.h" + +using wsl::windows::service::wslc::WSLCSessionRuntime; + +namespace { + +constexpr auto c_containerdStorage = "/var/lib/docker"; +constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000; +constexpr DWORD c_processKillTimeoutMs = 10 * 1000; + +} // namespace + +namespace wsl::windows::service::wslc { + +WSLCSessionRuntime::WSLCSessionRuntime(WSLCSession& Session) noexcept : m_session(&Session) +{ +} + +void WSLCSessionRuntime::Initialize( + DWORD vmFactoryGitCookie, + wil::com_ptr git, + const WSLCSessionInitSettings* settings, + std::chrono::milliseconds idleGrace, + SessionContext sessionContext, + RuntimeHooks hooks) +{ + m_vmFactoryGitCookie = vmFactoryGitCookie; + m_git = std::move(git); + m_settings = settings; + m_id = sessionContext.Id; + m_displayName = std::move(sessionContext.DisplayName); + m_terminating = sessionContext.Terminating; + m_sessionTerminatingEvent = sessionContext.SessionTerminatingEvent; + m_sessionTerminatedEvent = sessionContext.SessionTerminatedEvent; + m_hooks = std::move(hooks); + + m_idleState->Initialize(idleGrace, [this]() { OnIdleTimer(); }); + + m_initialized = true; +} + +WSLCVirtualMachine& WSLCSessionRuntime::Vm() +{ + WI_ASSERT(m_virtualMachine.has_value()); + return m_virtualMachine.value(); +} + +bool WSLCSessionRuntime::HasVm() const noexcept +{ + return m_virtualMachine.has_value(); +} + +IORelay* WSLCSessionRuntime::Relay() +{ + return m_ioRelay ? &m_ioRelay.value() : nullptr; +} + +bool WSLCSessionRuntime::HasRelay() const noexcept +{ + return m_ioRelay.has_value(); +} + +DockerHTTPClient& WSLCSessionRuntime::Docker() +{ + WI_ASSERT(m_dockerClient.has_value()); + return m_dockerClient.value(); +} + +bool WSLCSessionRuntime::HasDocker() const noexcept +{ + return m_dockerClient.has_value(); +} + +DockerEventTracker& WSLCSessionRuntime::Events() +{ + WI_ASSERT(m_eventTracker.has_value()); + return m_eventTracker.value(); +} + +bool WSLCSessionRuntime::HasEvents() const noexcept +{ + return m_eventTracker.has_value(); +} + +WSLCVolumes& WSLCSessionRuntime::Volumes() +{ + WI_ASSERT(m_volumes.has_value()); + return m_volumes.value(); +} + +bool WSLCSessionRuntime::HasVolumes() const noexcept +{ + return m_volumes.has_value(); +} + +wil::srwlock& WSLCSessionRuntime::Lock() noexcept +{ + return m_lock; +} + +IdleState& WSLCSessionRuntime::Idle() noexcept +{ + return *m_idleState; +} + +std::shared_ptr WSLCSessionRuntime::IdleStateShared() const noexcept +{ + return m_idleState; +} + +WSLCSessionRuntime::VmState WSLCSessionRuntime::State() const noexcept +{ + return m_vmState.load(); +} + +WSLCSessionRuntime::VmExitDisposition WSLCSessionRuntime::ExitDisposition() const noexcept +{ + return m_vmExitDisposition.load(); +} + +std::atomic& WSLCSessionRuntime::StateAtomic() noexcept +{ + return m_vmState; +} + +std::atomic& WSLCSessionRuntime::ExitDispositionAtomic() noexcept +{ + return m_vmExitDisposition; +} + +wil::unique_event& WSLCSessionRuntime::VmExitedEvent() noexcept +{ + return m_vmExitedEvent; +} + +wil::unique_event& WSLCSessionRuntime::DockerdReadyEvent() noexcept +{ + return m_dockerdReadyEvent; +} + +std::optional& WSLCSessionRuntime::ContainerdProcess() +{ + return m_containerdProcess; +} + +std::optional& WSLCSessionRuntime::DockerdProcess() +{ + return m_dockerdProcess; +} + +std::filesystem::path& WSLCSessionRuntime::SwapVhdPath() noexcept +{ + return m_swapVhdPath; +} + +void WSLCSessionRuntime::SetStorageMounted(bool value) noexcept +{ + m_storageMounted = value; +} + +std::mutex& WSLCSessionRuntime::AllocatedPortsLock() noexcept +{ + return m_allocatedPortsLock; +} + +std::map, size_t>>& WSLCSessionRuntime::AllocatedPorts() noexcept +{ + return m_allocatedPorts; +} + +bool WSLCSessionRuntime::IdleTerminationEnabled() const noexcept +{ + // Only tear the VM down when there is persistent storage to recover from. A tmpfs-backed + // session would lose all image/container state on teardown, so its VM is kept alive once started. + return m_settings->StoragePath != nullptr; +} + +int WSLCSessionRuntime::StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs) +{ + auto signalResult = Process.Get().Signal(WSLCSignalSIGTERM); + if (FAILED(signalResult)) + { + LOG_HR_MSG(signalResult, "Failed to terminate process %i", Process.Get().GetPid()); + return -1; + } + + try + { + return Process.Wait(TerminateTimeoutMs); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + try + { + LOG_IF_FAILED(Process.Get().Signal(WSLCSignalSIGKILL)); + return Process.Wait(KillTimeoutMs); + } + CATCH_LOG(); + } + + return -1; +} + +void WSLCSessionRuntime::EnsureVmRunning() +{ + if (m_vmState.load() == VmState::Running) + { + return; + } + + auto lock = m_lock.lock_exclusive(); + + // Do not (re)start the VM once the session is terminating or has terminated. This also + // bounds VmLease's retry loop: a lease that races with Terminate() fails here instead of + // restarting a VM that is being permanently torn down. + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent->is_signaled()); + + if (m_vmState.load() == VmState::Running) + { + return; + } + + StartVmLockHeld(); +} + +bool WSLCSessionRuntime::TryClaimExpectedStop() noexcept +{ + auto expected = VmExitDisposition::Active; + return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::StopRequested); +} + +bool WSLCSessionRuntime::TryClaimSpontaneousExit() noexcept +{ + auto expected = VmExitDisposition::Active; + return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::ExitClaimed); +} + +void WSLCSessionRuntime::StartVmLockHeld() +{ + WI_ASSERT(m_vmState.load() != VmState::Running); + + WSL_LOG("WslcVmStarting", TraceLoggingValue(m_id, "SessionId")); + + m_vmState.store(VmState::Starting); + m_vmExitDisposition.store(VmExitDisposition::Active); + + // Tear back down if bring-up fails partway. The VM may have exited on its own during bring-up, + // so claim the stop first and only tear down if we win it (TryClaimExpectedStop()); otherwise + // OnVmExited() owns the teardown and we just release the lock to let its Terminate() finish. + auto startCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + if (TryClaimExpectedStop()) + { + TearDownVmLockHeld(); + m_vmState.store(VmState::None); + } + else + { + WSL_LOG("WslcVmExitedDuringStart", TraceLoggingValue(m_id, "SessionId")); + } + }); + + // Create a fresh IO relay for this VM instance. The previous one (if any) was stopped + // during teardown and cannot be restarted. + m_ioRelay.emplace(); + + // Create the VM via the factory. Re-fetch the factory from the GIT so we call it through a + // proxy marshalled into this thread's apartment (see m_git). The VM produces crash events; + // the session multiplexes them out to any registered ICrashDumpCallback subscribers via + // OnCrashDumpWritten. + wil::com_ptr vmFactory; + THROW_IF_FAILED(m_git->GetInterfaceFromGlobal(m_vmFactoryGitCookie, __uuidof(IWSLCVirtualMachineFactory), vmFactory.put_void())); + + wil::com_ptr vm; + THROW_IF_FAILED(vmFactory->CreateVirtualMachine(&vm)); + + m_virtualMachine.emplace( + vm.get(), + m_settings, + m_sessionTerminatingEvent->get(), + WSLCVirtualMachine::TOnCrashDump(m_hooks.OnCrashDump)); + m_virtualMachine->Initialize(); + + // Get an event from the service that is signaled when the VM exits. + m_vmExitedEvent.reset(); + THROW_IF_FAILED(vm->GetTerminationEvent(&m_vmExitedEvent)); + + if (m_hooks.BringUp) + { + m_hooks.BringUp(); + } + + // Monitor for unexpected VM exit. + m_ioRelay->AddHandle(std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSessionRuntime::OnVmExited, this))); + + if (m_hooks.RecoverState) + { + m_hooks.RecoverState(); + } + + m_vmState.store(VmState::Running); + startCleanup.release(); + + WSL_LOG("WslcVmStarted", TraceLoggingValue(m_id, "SessionId")); +} + +void WSLCSessionRuntime::InitializeDockerRuntime(const std::filesystem::path& storagePath) +{ + // Wait for dockerd to be ready before starting the event tracker. + THROW_WIN32_IF_MSG( + ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(m_settings->BootTimeoutMs), "Timed out waiting for dockerd to start"); + + auto [_, __, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread); + + m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000); + + // Start the event tracker. + m_eventTracker.emplace(m_dockerClient.value(), *m_session, *m_ioRelay); + + m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), storagePath); +} + +void WSLCSessionRuntime::StopVmLockHeld() +{ + if (m_vmState.load() != VmState::Running) + { + return; + } + + WSL_LOG("WslcVmIdleStop", TraceLoggingValue(m_id, "SessionId")); + + // N.B. The caller has claimed StopRequested (via TryClaimExpectedStop), so VM/dockerd/containerd + // exit callbacks firing from the relay thread during teardown are treated as expected, not as a + // crash. + m_vmState.store(VmState::Stopping); + + TearDownVmLockHeld(); + + m_vmState.store(VmState::None); +} + +void WSLCSessionRuntime::TearDownVmLockHeld(bool CaptureTerminationReason) +{ + if (m_hooks.TearDownSessionState) + { + m_hooks.TearDownSessionState(); + } + + m_volumes.reset(); + + // Stop the IO relay. + // This stops: + // - container state monitoring. + // - container init process relays + // - execs relays + // - container logs relays + if (m_ioRelay) + { + m_ioRelay->Stop(); + } + + { + std::lock_guard allocatedPortsLock(m_allocatedPortsLock); + m_allocatedPorts.clear(); + } + + m_eventTracker.reset(); + m_dockerClient.reset(); + + if (CaptureTerminationReason) + { + // Default: an explicit/graceful teardown is a shutdown (the VM is still alive and we are + // bringing it down). Overridden below if the VM exited on its own and recorded a cause. + m_lastTerminationReason = WSLCVirtualMachineTerminationReasonShutdown; + m_lastTerminationDetails.clear(); + } + + // Check if the VM has already exited (e.g., killed externally). + // If so, skip operations that require a live VM to avoid unnecessary waits. + // N.B. m_vmExitedEvent may be uninitialized if teardown runs before GetTerminationEvent() succeeds. + if (m_vmExitedEvent && m_vmExitedEvent.is_signaled()) + { + WSL_LOG("SkippingGracefulShutdown_VmDead", TraceLoggingValue(m_id, "SessionId")); + + // The VM exited on its own, so it recorded the cause. + if (CaptureTerminationReason && m_virtualMachine) + { + wil::unique_cotaskmem_string details; + LOG_IF_FAILED(m_virtualMachine->GetTerminationReason(&m_lastTerminationReason, &details)); + m_lastTerminationDetails = details ? details.get() : L""; + } + } + else if (m_virtualMachine) + { + m_virtualMachine->OnSessionTerminated(); + + // Stop dockerd first, then containerd (dockerd is a client of containerd). + // N.B. dockerd waits a couple seconds if there are any outstanding HTTP request sockets opened. + if (m_dockerdProcess.has_value()) + { + auto dockerdExitCode = StopProcess(m_dockerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); + WSL_LOG("DockerdExit", TraceLoggingValue(dockerdExitCode, "code")); + } + + if (m_containerdProcess.has_value()) + { + auto containerdExitCode = StopProcess(m_containerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); + WSL_LOG("ContainerdExit", TraceLoggingValue(containerdExitCode, "code")); + } + + // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running. + try + { + m_virtualMachine->Unmount(c_containerdStorage); + } + CATCH_LOG(); + } + + m_dockerdProcess.reset(); + m_containerdProcess.reset(); + m_virtualMachine.reset(); + m_storageMounted = false; + + // Destroy the relay unless we're on its own thread (~IORelay joins the thread, which would + // deadlock). On unexpected-VM-exit path (runs on relay thread), leave it for ~WSLCSession. + if (!m_ioRelay || !m_ioRelay->IsRelayThread()) + { + m_ioRelay.reset(); + m_vmExitedEvent.reset(); + } + + // Delete the ephemeral swap VHD now that the VM is gone. + if (!m_swapVhdPath.empty()) + { + LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str())); + m_swapVhdPath.clear(); + } +} + +void WSLCSessionRuntime::OnIdleTimer() +try +{ + // Idle teardown releases cross-process COM proxies (the VM and its VM-scoped state), so this + // threadpool callback must join the process MTA; otherwise those Release/calls fail with + // RPC_E_WRONG_THREAD. The function-try-block keeps this (and everything below) under CATCH_LOG: + // the threadpool callback that invokes us is noexcept, so an escaping throw would terminate. + const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); + + if (m_terminating->load() || !IdleTerminationEnabled()) + { + return; + } + + // Non-blocking acquire: a blocking exclusive would queue behind in-flight operations, and + // SRW locks favor waiting writers, stalling all new ops. If the lock is held, an operation + // is in flight; it holds an activity reference and will re-arm the timer (via the 1->0 + // transition) when it releases, so there is nothing to do here. + auto lock = m_lock.try_lock_exclusive(); + if (!lock) + { + return; + } + + // Re-check every teardown precondition under the lock. The activity count is the single + // source of truth for "the VM is needed"; a 0->1 transition since the timer fired (cancel + // raced the callback) is caught here. + if (m_terminating->load() || m_vmState.load() != VmState::Running || m_idleState->ActivityCount() != 0) + { + return; + } + + // Claim the stop. If we lose, OnVmExited() owns a spontaneous-exit teardown and is spinning for + // this lock, so release it and let that run instead of joining the relay ourselves. + if (!TryClaimExpectedStop()) + { + return; + } + + // Restore Active on completion (or early exit) so the next StartVmLockHeld starts clean; only + // clear our own claim. + auto dispositionCleanup = wil::scope_exit([this]() { + auto stopRequested = VmExitDisposition::StopRequested; + m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active); + }); + + StopVmLockHeld(); +} +CATCH_LOG(); + +WSLCSessionRuntime::VmLease WSLCSessionRuntime::AcquireVmLease() +{ + return VmLease(*this); +} + +WSLCSessionRuntime::LockedRuntime WSLCSessionRuntime::Acquire() +{ + return LockedRuntime(*this); +} + +WSLCSessionRuntime::VmLease::VmLease(WSLCSessionRuntime& Runtime) : m_runtime(&Runtime) +{ + // Record an in-flight operation before bringing the VM up so idle teardown cannot tear it down + // between EnsureVmRunning() and acquiring the shared lock. AddActivity cancels any pending idle + // timer. + m_runtime->m_idleState->AddActivity(); + + auto countCleanup = wil::scope_exit([this]() { + m_runtime->m_idleState->ReleaseActivity(); + m_runtime = nullptr; + }); + + // Activity increment may race with idle teardown. Retry until we hold the lock with VM running. + for (;;) + { + m_runtime->EnsureVmRunning(); + + m_lock = m_runtime->m_lock.lock_shared(); + + if (m_runtime->m_vmState.load() == VmState::Running) + { + break; + } + + m_lock.reset(); + } + + countCleanup.release(); +} + +WSLCSessionRuntime::VmLease::VmLease(VmLease&& Other) noexcept : + m_runtime(std::exchange(Other.m_runtime, nullptr)), m_lock(std::move(Other.m_lock)) +{ +} + +WSLCSessionRuntime::VmLease& WSLCSessionRuntime::VmLease::operator=(VmLease&& Other) noexcept +{ + if (this != &Other) + { + if (m_runtime != nullptr) + { + // Release the shared lock before the activity reference so that, if this was the last + // activity, idle teardown can immediately take the exclusive lock. + m_lock.reset(); + m_runtime->m_idleState->ReleaseActivity(); + } + + m_runtime = std::exchange(Other.m_runtime, nullptr); + m_lock = std::move(Other.m_lock); + } + + return *this; +} + +WSLCSessionRuntime::VmLease::~VmLease() +{ + if (m_runtime != nullptr) + { + // Release the shared lock before the activity reference so that, if this was the last + // activity, idle teardown can immediately take the exclusive lock. ReleaseActivity arms the + // idle timer on the 1->0 transition. + m_lock.reset(); + m_runtime->m_idleState->ReleaseActivity(); + } +} + +WSLCSessionRuntime::LockedRuntime::LockedRuntime(WSLCSessionRuntime& Runtime) : m_runtime(&Runtime), m_lease(Runtime.AcquireVmLease()) +{ +} + +WSLCVirtualMachine& WSLCSessionRuntime::LockedRuntime::Vm() +{ + return m_runtime->Vm(); +} + +IORelay* WSLCSessionRuntime::LockedRuntime::Relay() +{ + return m_runtime->Relay(); +} + +DockerHTTPClient& WSLCSessionRuntime::LockedRuntime::Docker() +{ + return m_runtime->Docker(); +} + +void WSLCSessionRuntime::OnVmExited() +{ + // A spontaneous exit we must permanently terminate, unless an expected stop already claimed it, + // in which case the exit was wanted and we decline. + if (!TryClaimSpontaneousExit()) + { + WSL_LOG("WslcVmExitedDuringStop", TraceLoggingValue(m_id, "SessionId")); + return; + } + + WSL_LOG( + "VmExited", + TraceLoggingLevel(WINEVENT_LEVEL_WARNING), + TraceLoggingValue(m_id, "SessionId"), + TraceLoggingValue(m_displayName.c_str(), "Name"), + TraceLoggingValue(!m_sessionTerminatingEvent->is_signaled(), "Unexpected")); + + if (m_hooks.OnSpontaneousExit) + { + m_hooks.OnSpontaneousExit(); + } +} + +void WSLCSessionRuntime::Shutdown( + wil::rwlock_release_exclusive_scope_exit& sessionLock, + WSLCVirtualMachineTerminationReason& terminationReason, + std::wstring& terminationDetails) +{ + if (!m_initialized) + { + return; + } + + // Acquire an exclusive lock to ensure that no operation is running. + WI_VERIFY(sessionLock); + + // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason. + // This mirrors the soft teardown used for idle shutdown, but here it is permanent. + TearDownVmLockHeld(/* CaptureTerminationReason */ true); + + m_vmState.store(VmState::None); + + // Signal completion last so any observer of the terminated event sees a fully torn-down + // session and a populated termination reason. + m_sessionTerminatedEvent->SetEvent(); + + // Release the exclusive lock before disarming the idle timer. If a timer callback is currently + // blocked acquiring the exclusive lock (about to evaluate idle teardown), it must be able to + // obtain it, observe m_terminating, and return — otherwise Disarm()'s wait for in-flight + // callbacks below would deadlock. + sessionLock.reset(); + + // Permanently disable idle teardown and drain any in-flight timer callback so it cannot + // reference this session after it is destroyed. + m_idleState->Disarm(); + + terminationReason = m_lastTerminationReason; + terminationDetails = m_lastTerminationDetails; +} + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCSessionRuntime.h b/src/windows/wslcsession/WSLCSessionRuntime.h new file mode 100644 index 0000000000..724fa9214a --- /dev/null +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -0,0 +1,205 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCSessionRuntime.h + +Abstract: + + Contains the definition for WSLCSessionRuntime. + +--*/ + +#pragma once + +#include "wslc.h" +#include "WSLCVirtualMachine.h" +#include "WSLCVolumes.h" +#include "WSLCIdleState.h" +#include "DockerEventTracker.h" +#include "DockerHTTPClient.h" +#include "IORelay.h" +#include "ServiceProcessLauncher.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wsl::windows::service::wslc { + +class WSLCSession; + +class WSLCSessionRuntime +{ +public: + enum class VmState + { + None, + Starting, + Running, + Stopping, + }; + + enum class VmExitDisposition + { + Active, + StopRequested, + ExitClaimed, + }; + + struct RuntimeHooks + { + std::function BringUp; + std::function RecoverState; + std::function TearDownSessionState; + std::function OnSpontaneousExit; + WSLCVirtualMachine::TOnCrashDump OnCrashDump; + }; + + struct SessionContext + { + ULONG Id{}; + std::wstring DisplayName; + const std::atomic* Terminating{}; + wil::unique_event* SessionTerminatingEvent{}; + wil::unique_event* SessionTerminatedEvent{}; + }; + + class VmLease + { + public: + VmLease() = default; + explicit VmLease(WSLCSessionRuntime& Runtime); + VmLease(VmLease&& Other) noexcept; + VmLease& operator=(VmLease&& Other) noexcept; + ~VmLease(); + + VmLease(const VmLease&) = delete; + VmLease& operator=(const VmLease&) = delete; + + private: + WSLCSessionRuntime* m_runtime{}; + wil::rwlock_release_shared_scope_exit m_lock; + }; + + class LockedRuntime + { + public: + LockedRuntime() = default; + explicit LockedRuntime(WSLCSessionRuntime& Runtime); + + WSLCVirtualMachine& Vm(); + IORelay* Relay(); + DockerHTTPClient& Docker(); + + private: + WSLCSessionRuntime* m_runtime{}; + VmLease m_lease; + }; + + explicit WSLCSessionRuntime(WSLCSession& Session) noexcept; + + void Initialize( + DWORD vmFactoryGitCookie, + wil::com_ptr git, + const WSLCSessionInitSettings* settings, + std::chrono::milliseconds idleGrace, + SessionContext sessionContext, + RuntimeHooks hooks); + + WSLCVirtualMachine& Vm(); + bool HasVm() const noexcept; + IORelay* Relay(); + bool HasRelay() const noexcept; + DockerHTTPClient& Docker(); + bool HasDocker() const noexcept; + DockerEventTracker& Events(); + bool HasEvents() const noexcept; + WSLCVolumes& Volumes(); + bool HasVolumes() const noexcept; + wil::srwlock& Lock() noexcept; + IdleState& Idle() noexcept; + std::shared_ptr IdleStateShared() const noexcept; + VmState State() const noexcept; + VmExitDisposition ExitDisposition() const noexcept; + std::atomic& StateAtomic() noexcept; + std::atomic& ExitDispositionAtomic() noexcept; + wil::unique_event& VmExitedEvent() noexcept; + wil::unique_event& DockerdReadyEvent() noexcept; + std::optional& ContainerdProcess(); + std::optional& DockerdProcess(); + + std::filesystem::path& SwapVhdPath() noexcept; + void SetStorageMounted(bool value) noexcept; + + std::mutex& AllocatedPortsLock() noexcept; + std::map, size_t>>& AllocatedPorts() noexcept; + + [[nodiscard]] bool TryClaimExpectedStop() noexcept; + [[nodiscard]] bool TryClaimSpontaneousExit() noexcept; + + _Requires_exclusive_lock_held_(m_lock) void StartVmLockHeld(); + _Requires_exclusive_lock_held_(m_lock) void StopVmLockHeld(); + _Requires_exclusive_lock_held_(m_lock) void TearDownVmLockHeld(bool CaptureTerminationReason = false); + void EnsureVmRunning(); + void OnIdleTimer(); + void OnVmExited(); + void InitializeDockerRuntime(const std::filesystem::path& storagePath); + [[nodiscard]] VmLease AcquireVmLease(); + [[nodiscard]] LockedRuntime Acquire(); + + void Shutdown( + wil::rwlock_release_exclusive_scope_exit& sessionLock, + WSLCVirtualMachineTerminationReason& terminationReason, + std::wstring& terminationDetails); + +private: + bool IdleTerminationEnabled() const noexcept; + int StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs); + + WSLCSession* m_session{}; + RuntimeHooks m_hooks; + + ULONG m_id{}; + std::wstring m_displayName; + bool m_initialized{}; + const std::atomic* m_terminating{}; + wil::unique_event* m_sessionTerminatingEvent{}; + wil::unique_event* m_sessionTerminatedEvent{}; + + DWORD m_vmFactoryGitCookie{}; + wil::com_ptr m_git; + const WSLCSessionInitSettings* m_settings{}; + + std::optional m_virtualMachine; + std::optional m_ioRelay; + std::optional m_eventTracker; + std::optional m_dockerClient; + std::optional m_volumes; + std::optional m_containerdProcess; + std::optional m_dockerdProcess; + wil::unique_event m_vmExitedEvent; + wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; + + std::filesystem::path m_swapVhdPath; + bool m_storageMounted{false}; + std::mutex m_allocatedPortsLock; + std::map, size_t>> m_allocatedPorts; + + wil::srwlock m_lock; + std::atomic m_vmState{VmState::None}; + std::atomic m_vmExitDisposition{VmExitDisposition::Active}; + std::shared_ptr m_idleState{std::make_shared()}; + + WSLCVirtualMachineTerminationReason m_lastTerminationReason{WSLCVirtualMachineTerminationReasonUnknown}; + std::wstring m_lastTerminationDetails; +}; + +} // namespace wsl::windows::service::wslc From 4625dde952759bed1502420bfc72ae49f361ad02 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 6 Jul 2026 11:13:13 -0700 Subject: [PATCH 09/31] fix formatting --- src/windows/wslcsession/WSLCSession.cpp | 17 ++++++----------- src/windows/wslcsession/WSLCSessionRuntime.cpp | 16 ++++++---------- src/windows/wslcsession/WSLCSessionRuntime.h | 14 +++++++------- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index e470196da7..61cc24d832 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -467,13 +467,7 @@ try hooks.OnSpontaneousExit = [this]() { LOG_IF_FAILED(Terminate()); }; hooks.OnCrashDump = std::bind( - &WSLCSession::OnCrashDumpWritten, - this, - std::placeholders::_1, - std::placeholders::_2, - std::placeholders::_3, - std::placeholders::_4, - std::placeholders::_5); + &WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5); WSLCSessionRuntime::SessionContext sessionContext; sessionContext.Id = m_id; @@ -747,7 +741,8 @@ void WSLCSession::StartContainerd() args.emplace_back("debug"); } - m_runtime.ContainerdProcess() = StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this)); + m_runtime.ContainerdProcess() = + StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this)); WSL_LOG("ContainerdStarted"); } @@ -1003,8 +998,7 @@ try THROW_IF_FAILED(CoCreateGuid(&volumeId)); auto mountPath = std::format("/mnt/{}", wsl::shared::string::GuidToString(volumeId)); THROW_IF_FAILED(runtime.Vm().MountWindowsFolder(Options->ContextPath, mountPath.c_str(), TRUE)); - auto unmountFolder = - wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { runtime.Vm().UnmountWindowsFolder(mountPath.c_str()); }); + auto unmountFolder = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { runtime.Vm().UnmountWindowsFolder(mountPath.c_str()); }); std::vector buildArgs{"/usr/bin/docker", "build", "--progress=rawjson"}; if (WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsNoCache)) @@ -2691,7 +2685,8 @@ try EMIT_USER_WARNING(wsl::shared::string::MultiByteToWide(createResult.Warning)); } - auto removeNetworkCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_runtime.Docker().RemoveNetwork(name); }); + auto removeNetworkCleanup = + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_runtime.Docker().RemoveNetwork(name); }); // Inspect the newly created network to cache full properties (IPAM, Scope, etc.) // since CreateNetworkResponse only returns {Id, Warning}. diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 88bc3f769b..3ad490862f 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -290,11 +290,7 @@ void WSLCSessionRuntime::StartVmLockHeld() wil::com_ptr vm; THROW_IF_FAILED(vmFactory->CreateVirtualMachine(&vm)); - m_virtualMachine.emplace( - vm.get(), - m_settings, - m_sessionTerminatingEvent->get(), - WSLCVirtualMachine::TOnCrashDump(m_hooks.OnCrashDump)); + m_virtualMachine.emplace(vm.get(), m_settings, m_sessionTerminatingEvent->get(), WSLCVirtualMachine::TOnCrashDump(m_hooks.OnCrashDump)); m_virtualMachine->Initialize(); // Get an event from the service that is signaled when the VM exits. @@ -307,7 +303,8 @@ void WSLCSessionRuntime::StartVmLockHeld() } // Monitor for unexpected VM exit. - m_ioRelay->AddHandle(std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSessionRuntime::OnVmExited, this))); + m_ioRelay->AddHandle( + std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSessionRuntime::OnVmExited, this))); if (m_hooks.RecoverState) { @@ -579,7 +576,8 @@ WSLCSessionRuntime::VmLease::~VmLease() } } -WSLCSessionRuntime::LockedRuntime::LockedRuntime(WSLCSessionRuntime& Runtime) : m_runtime(&Runtime), m_lease(Runtime.AcquireVmLease()) +WSLCSessionRuntime::LockedRuntime::LockedRuntime(WSLCSessionRuntime& Runtime) : + m_runtime(&Runtime), m_lease(Runtime.AcquireVmLease()) { } @@ -622,9 +620,7 @@ void WSLCSessionRuntime::OnVmExited() } void WSLCSessionRuntime::Shutdown( - wil::rwlock_release_exclusive_scope_exit& sessionLock, - WSLCVirtualMachineTerminationReason& terminationReason, - std::wstring& terminationDetails) + wil::rwlock_release_exclusive_scope_exit& sessionLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails) { if (!m_initialized) { diff --git a/src/windows/wslcsession/WSLCSessionRuntime.h b/src/windows/wslcsession/WSLCSessionRuntime.h index 724fa9214a..2cfd9e8963 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.h +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -145,9 +145,12 @@ class WSLCSessionRuntime [[nodiscard]] bool TryClaimExpectedStop() noexcept; [[nodiscard]] bool TryClaimSpontaneousExit() noexcept; - _Requires_exclusive_lock_held_(m_lock) void StartVmLockHeld(); - _Requires_exclusive_lock_held_(m_lock) void StopVmLockHeld(); - _Requires_exclusive_lock_held_(m_lock) void TearDownVmLockHeld(bool CaptureTerminationReason = false); + _Requires_exclusive_lock_held_(m_lock) + void StartVmLockHeld(); + _Requires_exclusive_lock_held_(m_lock) + void StopVmLockHeld(); + _Requires_exclusive_lock_held_(m_lock) + void TearDownVmLockHeld(bool CaptureTerminationReason = false); void EnsureVmRunning(); void OnIdleTimer(); void OnVmExited(); @@ -155,10 +158,7 @@ class WSLCSessionRuntime [[nodiscard]] VmLease AcquireVmLease(); [[nodiscard]] LockedRuntime Acquire(); - void Shutdown( - wil::rwlock_release_exclusive_scope_exit& sessionLock, - WSLCVirtualMachineTerminationReason& terminationReason, - std::wstring& terminationDetails); + void Shutdown(wil::rwlock_release_exclusive_scope_exit& sessionLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails); private: bool IdleTerminationEnabled() const noexcept; From e51b0d93935a13fe4b23bb19c858f3d7b24010b4 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 7 Jul 2026 14:00:39 -0700 Subject: [PATCH 10/31] wslc: skip containerd VHD unmount when storage was never mounted Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCSessionRuntime.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 3ad490862f..0f50c7ceb8 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -422,11 +422,14 @@ void WSLCSessionRuntime::TearDownVmLockHeld(bool CaptureTerminationReason) } // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running. - try + if (m_storageMounted) { - m_virtualMachine->Unmount(c_containerdStorage); + try + { + m_virtualMachine->Unmount(c_containerdStorage); + } + CATCH_LOG(); } - CATCH_LOG(); } m_dockerdProcess.reset(); From 13f4601bd78aa67f18e5350a5876a87cfcb8a877 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 7 Jul 2026 18:24:50 -0700 Subject: [PATCH 11/31] wslc: publish termination reason before signaling the terminated event Copy the termination reason/details into the session-visible fields before SetEvent() so a waiter woken by the terminated event cannot observe the default Unknown reason. Restores the pre-refactor ordering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCSessionRuntime.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 0f50c7ceb8..32d835d166 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -639,6 +639,9 @@ void WSLCSessionRuntime::Shutdown( m_vmState.store(VmState::None); + terminationReason = m_lastTerminationReason; + terminationDetails = m_lastTerminationDetails; + // Signal completion last so any observer of the terminated event sees a fully torn-down // session and a populated termination reason. m_sessionTerminatedEvent->SetEvent(); @@ -652,9 +655,6 @@ void WSLCSessionRuntime::Shutdown( // Permanently disable idle teardown and drain any in-flight timer callback so it cannot // reference this session after it is destroyed. m_idleState->Disarm(); - - terminationReason = m_lastTerminationReason; - terminationDetails = m_lastTerminationDetails; } } // namespace wsl::windows::service::wslc From 8b0e5bef976d3e67eab2fe51633a6c4eecf5784d Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 9 Jul 2026 10:58:24 -0700 Subject: [PATCH 12/31] wslc: add idle-termination test hook and pass runtime into container factories Add a test-only TriggerIdleTermination COM method to force VM idle teardown on demand, with three TAEF tests that hammer the VM idle-termination race paths. Refactor WSLCContainer construction to take WSLCSessionRuntime& instead of five individual runtime members. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/service/inc/wslc.idl | 5 + src/windows/wslcsession/WSLCContainer.cpp | 53 ++++----- src/windows/wslcsession/WSLCContainer.h | 23 +--- src/windows/wslcsession/WSLCSession.cpp | 29 +++-- src/windows/wslcsession/WSLCSession.h | 1 + .../wslcsession/WSLCSessionRuntime.cpp | 50 ++++++++ src/windows/wslcsession/WSLCSessionRuntime.h | 2 + test/windows/WSLCTests.cpp | 108 ++++++++++++++++++ 8 files changed, 209 insertions(+), 62 deletions(-) diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 181f2b4418..b0d9f13eca 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -692,6 +692,11 @@ interface IWSLCSession : IUnknown HRESULT PruneNetworks([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *NetworksCount)] WSLCNetworkName** Networks, [out] ULONG* NetworksCount); HRESULT RegisterCrashDumpCallback([in] ICrashDumpCallback* Callback, [out] IUnknown** Subscription); + + // Used only for testing. Synchronously runs the idle-termination teardown path, ignoring the + // activity-count and idle-timeout-enabled guards so a test can force the VM down even while a + // container holds an activity reference. WasAlreadyIdle is TRUE when the VM was not running. + HRESULT TriggerIdleTermination([out] BOOL* WasAlreadyIdle); } // diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 74ece81bd4..b7d959c441 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -528,7 +528,7 @@ WSLCPortMapping ContainerPortMapping::Serialize() const WSLCContainerImpl::WSLCContainerImpl( WSLCSession& wslcSession, - WSLCVirtualMachine& virtualMachine, + WSLCSessionRuntime& runtime, IWSLCPluginNotifier* pluginNotifier, std::string&& Id, std::string&& Name, @@ -536,34 +536,30 @@ WSLCContainerImpl::WSLCContainerImpl( std::string NetworkMode, std::vector&& volumes, std::vector&& namedVolumes, - WSLCVolumes& Volumes, std::vector&& ports, std::map&& labels, std::function&& onDeleted, - DockerEventTracker& EventTracker, - DockerHTTPClient& DockerClient, - IORelay& Relay, WSLCContainerState InitialState, std::uint64_t CreatedAt, WSLCProcessFlags InitProcessFlags, WSLCContainerFlags ContainerFlags) : m_wslcSession(wslcSession), m_pluginNotifier(pluginNotifier), - m_virtualMachine(virtualMachine), + m_virtualMachine(runtime.Vm()), m_name(std::move(Name)), m_image(std::move(Image)), m_networkMode(std::move(NetworkMode)), m_id(std::move(Id)), m_mountedVolumes(std::move(volumes)), m_namedVolumes(std::move(namedVolumes)), - m_volumes(Volumes), + m_volumes(runtime.Volumes()), m_mappedPorts(std::move(ports)), m_labels(std::move(labels)), m_comWrapper(wil::MakeOrThrow(this, wslcSession, std::move(onDeleted))), - m_dockerClient(DockerClient), - m_eventTracker(EventTracker), - m_ioRelay(Relay), - m_containerEvents(EventTracker.RegisterContainerStateUpdates( + m_dockerClient(runtime.Docker()), + m_eventTracker(runtime.Events()), + m_ioRelay(*runtime.Relay()), + m_containerEvents(runtime.Events().RegisterContainerStateUpdates( m_id, std::bind(&WSLCContainerImpl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))), m_state(InitialState), m_createdAt(CreatedAt), @@ -1401,15 +1397,15 @@ std::unique_ptr WSLCContainerImpl::Create( const WSLCContainerOptions& containerOptions, const std::string& containerName, WSLCSession& wslcSession, - WSLCVirtualMachine& virtualMachine, + WSLCSessionRuntime& runtime, IWSLCPluginNotifier* pluginNotifier, const std::unordered_map& sessionNetworks, - WSLCVolumes& volumesManager, - std::function&& OnDeleted, - DockerEventTracker& EventTracker, - DockerHTTPClient& DockerClient, - IORelay& IoRelay) + std::function&& OnDeleted) { + auto& virtualMachine = runtime.Vm(); + auto& DockerClient = runtime.Docker(); + auto& EventTracker = runtime.Events(); + common::docker_schema::CreateContainer request; request.Image = containerOptions.Image; @@ -1792,7 +1788,7 @@ std::unique_ptr WSLCContainerImpl::Create( auto container = std::make_unique( wslcSession, - virtualMachine, + runtime, pluginNotifier, std::move(result.Id), CleanContainerName(inspectData.Name), @@ -1800,13 +1796,9 @@ std::unique_ptr WSLCContainerImpl::Create( std::move(networkMode), std::move(volumes), std::move(namedVolumes), - volumesManager, std::move(mappedPorts), std::move(labels), std::move(OnDeleted), - EventTracker, - DockerClient, - IoRelay, WslcContainerStateCreated, ParseDockerTimestamp(inspectData.Created), containerOptions.InitProcessOptions.Flags, @@ -1819,14 +1811,13 @@ std::unique_ptr WSLCContainerImpl::Create( std::unique_ptr WSLCContainerImpl::Open( const common::docker_schema::ContainerInfo& dockerContainer, WSLCSession& wslcSession, - WSLCVirtualMachine& virtualMachine, + WSLCSessionRuntime& runtime, IWSLCPluginNotifier* pluginNotifier, - WSLCVolumes& volumes, - std::function&& OnDeleted, - DockerEventTracker& EventTracker, - DockerHTTPClient& DockerClient, - IORelay& ioRelay) + std::function&& OnDeleted) { + auto& virtualMachine = runtime.Vm(); + auto& DockerClient = runtime.Docker(); + // Extract container name from Docker's names list. std::string name = ExtractContainerName(dockerContainer.Names, dockerContainer.Id); @@ -1884,7 +1875,7 @@ std::unique_ptr WSLCContainerImpl::Open( auto container = std::make_unique( wslcSession, - virtualMachine, + runtime, pluginNotifier, std::string(dockerContainer.Id), std::move(name), @@ -1892,13 +1883,9 @@ std::unique_ptr WSLCContainerImpl::Open( std::move(networkMode), std::move(metadata.Volumes), std::move(namedVolumes), - volumes, std::move(ports), std::move(labels), std::move(OnDeleted), - EventTracker, - DockerClient, - ioRelay, DockerStateToWSLCState(dockerContainer.State), static_cast(dockerContainer.Created), metadata.InitProcessFlags, diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 5032dd6de4..9c50e0f53a 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -33,6 +33,7 @@ namespace wsl::windows::service::wslc { class WSLCContainer; class WSLCSession; +class WSLCSessionRuntime; class WSLCVolumes; class unique_com_disconnect @@ -73,7 +74,7 @@ class WSLCContainerImpl WSLCContainerImpl( WSLCSession& wslcSession, - WSLCVirtualMachine& virtualMachine, + WSLCSessionRuntime& runtime, IWSLCPluginNotifier* pluginNotifier, std::string&& Id, std::string&& Name, @@ -81,13 +82,9 @@ class WSLCContainerImpl std::string NetworkMode, std::vector&& volumes, std::vector&& namedVolumes, - WSLCVolumes& Volumes, std::vector&& ports, std::map&& labels, std::function&& OnDeleted, - DockerEventTracker& EventTracker, - DockerHTTPClient& DockerClient, - IORelay& Relay, WSLCContainerState InitialState, std::uint64_t CreatedAt, WSLCProcessFlags InitProcessFlags, @@ -134,25 +131,17 @@ class WSLCContainerImpl const WSLCContainerOptions& Options, const std::string& Name, WSLCSession& wslcSession, - WSLCVirtualMachine& virtualMachine, + WSLCSessionRuntime& runtime, IWSLCPluginNotifier* pluginNotifier, const std::unordered_map& SessionNetworks, - WSLCVolumes& Volumes, - std::function&& OnDeleted, - DockerEventTracker& EventTracker, - DockerHTTPClient& DockerClient, - IORelay& Relay); + std::function&& OnDeleted); static std::unique_ptr Open( const common::docker_schema::ContainerInfo& DockerContainer, WSLCSession& wslcSession, - WSLCVirtualMachine& virtualMachine, + WSLCSessionRuntime& runtime, IWSLCPluginNotifier* pluginNotifier, - WSLCVolumes& Volumes, - std::function&& OnDeleted, - DockerEventTracker& EventTracker, - DockerHTTPClient& DockerClient, - IORelay& Relay); + std::function&& OnDeleted); private: __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect DeleteExclusiveLockHeld(WSLCDeleteFlags Flags); diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 61cc24d832..37b342bb32 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -2083,14 +2083,10 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio *containerOptions, containerName, *this, - m_runtime.Vm(), + m_runtime, m_pluginNotifier.get(), m_networks, - m_runtime.Volumes(), - std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), - m_runtime.Events(), - m_runtime.Docker(), - *m_runtime.Relay()); + std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1)); // Key the map by Docker's container ID, which is set in the WSLCContainerImpl constructor and stable for its lifetime. auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container)); @@ -3183,6 +3179,19 @@ try } CATCH_RETURN(); +HRESULT WSLCSession::TriggerIdleTermination(BOOL* WasAlreadyIdle) +try +{ + WSLCExecutionContext context(this); + + THROW_HR_IF_NULL(E_POINTER, WasAlreadyIdle); + + *WasAlreadyIdle = m_runtime.TriggerIdleTerminationForTest() ? TRUE : FALSE; + + return S_OK; +} +CATCH_RETURN(); + HRESULT WSLCSession::InterfaceSupportsErrorInfo(REFIID riid) { return riid == __uuidof(IWSLCSession) || riid == __uuidof(IWSLCCompatSession) ? S_OK : S_FALSE; @@ -3535,13 +3544,9 @@ void WSLCSession::RecoverExistingContainers() auto container = WSLCContainerImpl::Open( dockerContainer, *this, - m_runtime.Vm(), + m_runtime, m_pluginNotifier.get(), - m_runtime.Volumes(), - std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), - m_runtime.Events(), - m_runtime.Docker(), - *m_runtime.Relay()); + std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1)); auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container)); WI_ASSERT(inserted); diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index 8b2c7332f1..12fdc5dc31 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -215,6 +215,7 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession IFACEMETHOD(UnmountWindowsFolder)(_In_ LPCSTR LinuxPath) override; IFACEMETHOD(MapVmPort)(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort) override; IFACEMETHOD(UnmapVmPort)(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort) override; + IFACEMETHOD(TriggerIdleTermination)(_Out_ BOOL* WasAlreadyIdle) override; // IWSLCCompatSession - converts the WSLCCompat types to the wslc.idl types and forwards to the methods above. // Methods that have an identical signature in both interfaces (Terminate, DeleteVolume, Authenticate, diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 32d835d166..5c8f675d73 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -503,6 +503,56 @@ try } CATCH_LOG(); +bool WSLCSessionRuntime::TriggerIdleTerminationForTest() +{ + // Mirror OnIdleTimer's MTA context on a dedicated thread: the incoming RPC thread is an STA, so + // both re-initializing MTA on it and running teardown inline would fail (RPC_E_CHANGED_MODE / + // RPC_E_WRONG_THREAD when releasing the VM's cross-process COM proxies). + bool wasAlreadyIdle = false; + std::exception_ptr error; + + std::thread worker([&]() { + try + { + const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); + + auto lock = m_lock.lock_exclusive(); + + if (m_terminating->load() || m_vmState.load() != VmState::Running) + { + wasAlreadyIdle = true; + return; + } + + if (!TryClaimExpectedStop()) + { + wasAlreadyIdle = true; + return; + } + + auto dispositionCleanup = wil::scope_exit([this]() { + auto stopRequested = VmExitDisposition::StopRequested; + m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active); + }); + + StopVmLockHeld(); + } + catch (...) + { + error = std::current_exception(); + } + }); + + worker.join(); + + if (error) + { + std::rethrow_exception(error); + } + + return wasAlreadyIdle; +} + WSLCSessionRuntime::VmLease WSLCSessionRuntime::AcquireVmLease() { return VmLease(*this); diff --git a/src/windows/wslcsession/WSLCSessionRuntime.h b/src/windows/wslcsession/WSLCSessionRuntime.h index 2cfd9e8963..cb22b3ade7 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.h +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -158,6 +158,8 @@ class WSLCSessionRuntime [[nodiscard]] VmLease AcquireVmLease(); [[nodiscard]] LockedRuntime Acquire(); + [[nodiscard]] bool TriggerIdleTerminationForTest(); + void Shutdown(wil::rwlock_release_exclusive_scope_exit& sessionLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails); private: diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index c8f315a8bf..2cec025b7b 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -11424,6 +11424,114 @@ class WSLCTests VERIFY_IS_FALSE(IsVmRunning(c_sessionName)); } + // TriggerIdleTermination runs the idle-teardown path synchronously and reports whether the VM + // was already idle. Validates the idle -> running -> forced-idle -> running lifecycle. + WSLC_TEST_METHOD(TriggerIdleTerminationRestartsVm) + { + constexpr auto c_sessionName = L"wslc-idle-trigger-test"; + auto session = CreateSession(GetDefaultSessionSettings(c_sessionName)); + + // The VM starts lazily, so a freshly created session is already idle. + BOOL wasAlreadyIdle = FALSE; + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_TRUE(wasAlreadyIdle); + VERIFY_IS_FALSE(IsVmRunning(c_sessionName)); + + // Starting a process brings the VM up. + WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); + auto process = launcher.Launch(*session); + VERIFY_IS_TRUE(IsVmRunning(c_sessionName)); + + // Forcing idle termination tears the running VM down. + wasAlreadyIdle = TRUE; + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_FALSE(wasAlreadyIdle); + VERIFY_IS_FALSE(IsVmRunning(c_sessionName)); + + // A second trigger is now a no-op. + wasAlreadyIdle = FALSE; + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_TRUE(wasAlreadyIdle); + + // The session survives and lazily restarts the VM on the next operation. + WSLCProcessLauncher launcher2("/bin/sleep", {"/bin/sleep", "60"}); + auto process2 = launcher2.Launch(*session); + VERIFY_IS_TRUE(IsVmRunning(c_sessionName)); + } + + // A running container pins the VM via its activity hold; TriggerIdleTermination ignores that + // hold and forces teardown. The container's state must survive on persistent storage and be + // recovered against a fresh docker context when the VM lazily restarts. + WSLC_TEST_METHOD(TriggerIdleTerminationRecoversRunningContainer) + { + SKIP_TEST_SERVER(); + + const std::string containerName = "wslc-idle-recovery"; + + WSLCContainerLauncher launcher("debian:latest", containerName, {"/bin/sleep", "600"}); + auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone); + container.SetDeleteOnClose(false); + + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning); + VERIFY_IS_TRUE(IsVmRunning(c_testSessionName)); + + // Force idle teardown even though the container holds a running-activity reference. + BOOL wasAlreadyIdle = TRUE; + VERIFY_SUCCEEDED(m_defaultSession->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_FALSE(wasAlreadyIdle); + VERIFY_IS_FALSE(IsVmRunning(c_testSessionName)); + + // Re-opening lazily restarts the VM and recovers the container. It must be found (no longer + // running, since its init process died with the VM) and operable against the new context. + auto recovered = OpenContainer(m_defaultSession.get(), containerName); + VERIFY_IS_TRUE(IsVmRunning(c_testSessionName)); + VERIFY_ARE_NOT_EQUAL(recovered.State(), WslcContainerStateRunning); + } + + // Hammer the idle-teardown path concurrently with VM-level operations to surface deadlocks or + // stale-state races. Operations may fail while the VM is being torn down; that is tolerated, but + // the workers must never hang and the session must remain usable afterwards. + WSLC_TEST_METHOD(TriggerIdleTerminationConcurrentWithOperations) + { + constexpr auto c_sessionName = L"wslc-idle-hammer-test"; + auto session = CreateSession(GetDefaultSessionSettings(c_sessionName)); + + std::atomic stop = false; + std::atomic opFailures = 0; + + std::thread worker([&]() { + while (!stop.load()) + { + try + { + WSLCProcessLauncher launcher("/bin/true", {"/bin/true"}); + auto process = launcher.Launch(*session); + process.GetExitEvent().wait(5000); + } + catch (...) + { + opFailures.fetch_add(1); + } + } + }); + + for (int i = 0; i < 25; ++i) + { + BOOL wasAlreadyIdle = FALSE; + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); + } + + stop.store(true); + worker.join(); + + LogInfo("TriggerIdleTerminationConcurrentWithOperations tolerated %u operation failures", opFailures.load()); + + // The session must still be usable after the hammering. + WSLCProcessLauncher launcher("/bin/true", {"/bin/true"}); + auto process = launcher.Launch(*session); + VERIFY_IS_TRUE(process.GetExitEvent().wait(30000)); + } + // Helper: COM callback that captures all warnings received. class CapturingWarningCallback : public Microsoft::WRL::RuntimeClass, IWarningCallback, IFastRundown> From 89d478a75e27fe3c4a577a92aebe6b7ee22b0ed1 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 9 Jul 2026 11:17:33 -0700 Subject: [PATCH 13/31] wslc: force-delete recovery test container on all exit paths The TriggerIdleTerminationRecoversRunningContainer test left the persisted container in the shared default session on early-exit or if the recovered handle's best-effort delete failed, making later ListContainers-based tests order-dependent. Add a scope_exit that force-deletes the container by name regardless of exit path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/windows/WSLCTests.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 2cec025b7b..3110042d58 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -11468,6 +11468,14 @@ class WSLCTests const std::string containerName = "wslc-idle-recovery"; + auto cleanup = wil::scope_exit([&]() { + wil::com_ptr staleContainer; + if (SUCCEEDED(m_defaultSession->OpenContainer(containerName.c_str(), &staleContainer))) + { + LOG_IF_FAILED(staleContainer->Delete(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes)); + } + }); + WSLCContainerLauncher launcher("debian:latest", containerName, {"/bin/sleep", "600"}); auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone); container.SetDeleteOnClose(false); From c08eaa72dfb4edd0f8d4fc06cd4c149e97fc5838 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 9 Jul 2026 11:44:25 -0700 Subject: [PATCH 14/31] wslc: list WSLCIdleState.h in wslcsession HEADERS Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/windows/wslcsession/CMakeLists.txt b/src/windows/wslcsession/CMakeLists.txt index c2ec8e3955..29e0b02992 100644 --- a/src/windows/wslcsession/CMakeLists.txt +++ b/src/windows/wslcsession/CMakeLists.txt @@ -46,6 +46,7 @@ set(HEADERS WSLCProcessIO.h WSLCSession.h WSLCSessionRuntime.h + WSLCIdleState.h WSLCSessionFactory.h WSLCSessionReference.h WSLCVirtualMachine.h From 9a4bf16095d05eb36cb07827e1064482815a94fd Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 9 Jul 2026 11:54:50 -0700 Subject: [PATCH 15/31] wslc: clang-format Open() call site Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCSession.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index ee46885371..277db74181 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -3544,11 +3544,7 @@ void WSLCSession::RecoverExistingContainers() try { auto container = WSLCContainerImpl::Open( - dockerContainer, - *this, - m_runtime, - m_pluginNotifier.get(), - std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1)); + dockerContainer, *this, m_runtime, m_pluginNotifier.get(), std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1)); auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container)); WI_ASSERT(inserted); From 1d80fe1e1f8d79ea7dfde2cf9bff46652f67ea91 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 9 Jul 2026 11:56:30 -0700 Subject: [PATCH 16/31] wslc: correct idle grace-period comment to reference IdleTimeoutSec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCSession.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 277db74181..eacd7751a3 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -44,7 +44,7 @@ constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000; constexpr DWORD c_processKillTimeoutMs = 10 * 1000; // Default grace period to keep an otherwise-idle VM running before tearing it down (used when the -// session's settings.yaml does not override it). This avoids thrashing the VM (repeated +// session's IdleTimeoutSec setting is 0/unset). This avoids thrashing the VM (repeated // teardown/recreate) when containers are created and destroyed, or operations issued, in quick // succession. The clock restarts whenever the VM is observed to be non-idle, so a full grace period // of continuous idleness is required before teardown. From 2faea8ec4c11f75bded83e66fce811039b8078f4 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 9 Jul 2026 12:09:02 -0700 Subject: [PATCH 17/31] wslc: avoid reserved identifier in Fork structured binding Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCSessionRuntime.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 5c8f675d73..3506d69335 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -323,7 +323,7 @@ void WSLCSessionRuntime::InitializeDockerRuntime(const std::filesystem::path& st THROW_WIN32_IF_MSG( ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(m_settings->BootTimeoutMs), "Timed out waiting for dockerd to start"); - auto [_, __, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread); + [[maybe_unused]] auto [pid, ptyMaster, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread); m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000); From e9bdf88d06e8ad9deb52098edc15b2a60109cccc Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 9 Jul 2026 12:48:36 -0700 Subject: [PATCH 18/31] Add WSLC VM start/stop plugin hooks Session-level WSLC plugin hooks (OnSessionCreated/OnSessionStopping) assumed session == running VM. With idle termination (PR #40781) the VM is created lazily, torn down when idle, and transparently recreated while the session persists, so plugins had no notification of actual VM lifecycle. Add VM-level hooks OnWslcVmStarted/OnWslcVmStopping that fire on every VM (re)start and teardown, in addition to the once-per-session hooks. Both are best-effort (errors logged and ignored). OnWslcVmStarted fires after releasing the runtime exclusive lock so a plugin may reentrantly call back into the session (e.g. WSLCCreateProcess) without deadlocking; OnWslcVmStopping fires under the lock during teardown, gated on a fire-once flag so a failed bring-up emits no spurious stopping. Adds a PluginTests::WslcVmRestart case that drives first start, forced idle teardown (TriggerIdleTermination), and lazy restart, asserting the hook sequence and proving reentrancy is deadlock-free. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f1c36ca0-19d0-46a7-82b3-10f1e1ee3e4c --- src/windows/inc/WslPluginApi.h | 14 ++++++ src/windows/service/exe/PluginManager.cpp | 38 +++++++++++++++ src/windows/service/exe/PluginManager.h | 2 + .../service/exe/WSLCPluginNotifier.cpp | 20 ++++++++ src/windows/service/exe/WSLCPluginNotifier.h | 2 + src/windows/service/inc/wslc.idl | 7 +++ src/windows/wslcsession/WSLCSession.cpp | 16 +++++++ .../wslcsession/WSLCSessionRuntime.cpp | 46 +++++++++++++++---- src/windows/wslcsession/WSLCSessionRuntime.h | 18 ++++++++ test/windows/PluginTests.cpp | 44 ++++++++++++++++++ test/windows/PluginTests.h | 3 +- test/windows/testplugin/Plugin.cpp | 42 ++++++++++++++++- 12 files changed, 242 insertions(+), 10 deletions(-) diff --git a/src/windows/inc/WslPluginApi.h b/src/windows/inc/WslPluginApi.h index bcdcdb8f4f..c4f6070e8d 100644 --- a/src/windows/inc/WslPluginApi.h +++ b/src/windows/inc/WslPluginApi.h @@ -141,6 +141,18 @@ typedef HRESULT (*WSLPluginAPI_ImageCreated)(const struct WSLCSessionInformation // Called when an image is deleted. 'ImageId' is the deleted image identifier. Errors are ignored. typedef HRESULT (*WSLPluginAPI_ImageDeleted)(const struct WSLCSessionInformation* Session, LPCSTR ImageId); +// Called when the VM backing a WSLC session has started. Unlike OnSessionCreated (which fires once +// per session), this fires every time a VM is created for the session: on the first operation that +// needs a VM, and again each time the VM is recreated after being idle-terminated. Errors are logged +// but ignored (they do not abort VM startup or the triggering operation). +typedef HRESULT (*WSLPluginAPI_OnWslcVmStarted)(const struct WSLCSessionInformation* Session); + +// Called when the VM backing a WSLC session is about to stop (idle teardown, explicit termination, +// or unexpected exit). Fires every time a VM is torn down, paired with a prior OnWslcVmStarted. +// Errors are logged but ignored. The VM is going away, so this hook must not call back into the +// session (e.g. via WSLCCreateProcess). +typedef HRESULT (*WSLPluginAPI_OnWslcVmStopping)(const struct WSLCSessionInformation* Session); + // // WSLC plugin API calls. // @@ -220,6 +232,8 @@ struct WSLPluginHooksV1 WSLPluginAPI_ContainerStopping ContainerStopping; WSLPluginAPI_ImageCreated ImageCreated; WSLPluginAPI_ImageDeleted ImageDeleted; + WSLPluginAPI_OnWslcVmStarted WslcVmStarted; + WSLPluginAPI_OnWslcVmStopping WslcVmStopping; }; struct WSLPluginAPIV1 diff --git a/src/windows/service/exe/PluginManager.cpp b/src/windows/service/exe/PluginManager.cpp index 0d6b6ccbb7..1173dd6746 100644 --- a/src/windows/service/exe/PluginManager.cpp +++ b/src/windows/service/exe/PluginManager.cpp @@ -691,3 +691,41 @@ void PluginManager::OnWslcImageDeleted(const WSLCSessionInformation* Session, LP } } } + +void PluginManager::OnWslcVmStarted(const WSLCSessionInformation* Session) const +{ + ExecutionContext context(Context::Plugin); + + for (const auto& e : m_plugins) + { + if (e.hooks.WslcVmStarted != nullptr) + { + const auto result = e.hooks.WslcVmStarted(Session); + WSL_LOG( + "PluginOnWslcVmStartedCall", + TraceLoggingValue(e.name.c_str(), "Plugin"), + TraceLoggingValue(Session->SessionId, "SessionId"), + TraceLoggingValue(result, "Result")); + LOG_IF_FAILED_MSG(result, "Error thrown from plugin: '%ls'", e.name.c_str()); + } + } +} + +void PluginManager::OnWslcVmStopping(const WSLCSessionInformation* Session) const +{ + ExecutionContext context(Context::Plugin); + + for (const auto& e : m_plugins) + { + if (e.hooks.WslcVmStopping != nullptr) + { + const auto result = e.hooks.WslcVmStopping(Session); + WSL_LOG( + "PluginOnWslcVmStoppingCall", + TraceLoggingValue(e.name.c_str(), "Plugin"), + TraceLoggingValue(Session->SessionId, "SessionId"), + TraceLoggingValue(result, "Result")); + LOG_IF_FAILED_MSG(result, "Error thrown from plugin: '%ls'", e.name.c_str()); + } + } +} diff --git a/src/windows/service/exe/PluginManager.h b/src/windows/service/exe/PluginManager.h index a99a3e332d..f5e4779565 100644 --- a/src/windows/service/exe/PluginManager.h +++ b/src/windows/service/exe/PluginManager.h @@ -52,6 +52,8 @@ class PluginManager void OnWslcContainerStopping(const WSLCSessionInformation* Session, LPCSTR ContainerId) const; void OnWslcImageCreated(const WSLCSessionInformation* Session, LPCSTR InspectJson) const; void OnWslcImageDeleted(const WSLCSessionInformation* Session, LPCSTR ImageId) const; + void OnWslcVmStarted(const WSLCSessionInformation* Session) const; + void OnWslcVmStopping(const WSLCSessionInformation* Session) const; void ThrowIfFatalPluginError() const; diff --git a/src/windows/service/exe/WSLCPluginNotifier.cpp b/src/windows/service/exe/WSLCPluginNotifier.cpp index 7daab35bbf..2b2a88357a 100644 --- a/src/windows/service/exe/WSLCPluginNotifier.cpp +++ b/src/windows/service/exe/WSLCPluginNotifier.cpp @@ -64,3 +64,23 @@ try return S_OK; } CATCH_RETURN(); + +HRESULT WSLCPluginNotifier::OnVmStarted() +try +{ + COMServiceExecutionContext context; + + m_plugins.OnWslcVmStarted(&m_sessionInfo); + return S_OK; +} +CATCH_RETURN(); + +HRESULT WSLCPluginNotifier::OnVmStopping() +try +{ + COMServiceExecutionContext context; + + m_plugins.OnWslcVmStopping(&m_sessionInfo); + return S_OK; +} +CATCH_RETURN(); diff --git a/src/windows/service/exe/WSLCPluginNotifier.h b/src/windows/service/exe/WSLCPluginNotifier.h index b8ba4e11ec..de5f197a33 100644 --- a/src/windows/service/exe/WSLCPluginNotifier.h +++ b/src/windows/service/exe/WSLCPluginNotifier.h @@ -34,6 +34,8 @@ class DECLSPEC_UUID("E29B0F1A-4E18-4F09-83A2-2D6B1B9F8C4D") WSLCPluginNotifier IFACEMETHOD(OnContainerStopping)(_In_ LPCSTR ContainerId) override; IFACEMETHOD(OnImageCreated)(_In_ LPCSTR InspectJson) override; IFACEMETHOD(OnImageDeleted)(_In_ LPCSTR ImageId) override; + IFACEMETHOD(OnVmStarted)() override; + IFACEMETHOD(OnVmStopping)() override; private: wsl::windows::service::PluginManager& m_plugins; diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index b0d9f13eca..935192ceed 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -125,6 +125,13 @@ interface IWSLCPluginNotifier : IUnknown // Called when an image is deleted. 'ImageId' is the image identifier. Errors are logged but ignored. HRESULT OnImageDeleted([in] LPCSTR ImageId); + + // Called when the VM backing the session has started (first start or recreation after idle + // teardown). Errors are logged but ignored. + HRESULT OnVmStarted(); + + // Called when the VM backing the session is about to stop. Errors are logged but ignored. + HRESULT OnVmStopping(); }; typedef struct _WSLCImageInformation diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index eacd7751a3..d926447e2c 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -466,6 +466,22 @@ try hooks.OnSpontaneousExit = [this]() { LOG_IF_FAILED(Terminate()); }; + // Forward VM start/stop to plugins. Both are best-effort: errors are logged and ignored so a + // misbehaving plugin cannot abort VM startup or the operation that triggered it. + hooks.OnVmStarted = [this]() { + if (m_pluginNotifier) + { + LOG_IF_FAILED(m_pluginNotifier->OnVmStarted()); + } + }; + + hooks.OnVmStopping = [this]() { + if (m_pluginNotifier) + { + LOG_IF_FAILED(m_pluginNotifier->OnVmStopping()); + } + }; + hooks.OnCrashDump = std::bind( &WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5); diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 3506d69335..96be5a69c5 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -225,19 +225,41 @@ void WSLCSessionRuntime::EnsureVmRunning() return; } - auto lock = m_lock.lock_exclusive(); + bool started = false; + { + auto lock = m_lock.lock_exclusive(); - // Do not (re)start the VM once the session is terminating or has terminated. This also - // bounds VmLease's retry loop: a lease that races with Terminate() fails here instead of - // restarting a VM that is being permanently torn down. - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent->is_signaled()); + // Do not (re)start the VM once the session is terminating or has terminated. This also + // bounds VmLease's retry loop: a lease that races with Terminate() fails here instead of + // restarting a VM that is being permanently torn down. + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent->is_signaled()); - if (m_vmState.load() == VmState::Running) + if (m_vmState.load() != VmState::Running) + { + StartVmLockHeld(); + started = true; + } + } + + // Notify plugins that a VM has started, outside the exclusive lock: the handler forwards to the + // plugin, which may call back into the session (e.g. WSLCCreateProcess acquires a VM lease and + // the exclusive lock), so firing under the lock would deadlock. EnsureVmRunning's only caller + // (VmLease) holds an activity reference across this call, so idle teardown cannot race the VM + // down in the gap between releasing the lock and notifying. + if (started) { - return; + NotifyVmStarted(); } +} - StartVmLockHeld(); +void WSLCSessionRuntime::NotifyVmStarted() +{ + m_vmStartNotified.store(true); + + if (m_hooks.OnVmStarted) + { + m_hooks.OnVmStarted(); + } } bool WSLCSessionRuntime::TryClaimExpectedStop() noexcept @@ -354,6 +376,14 @@ void WSLCSessionRuntime::StopVmLockHeld() void WSLCSessionRuntime::TearDownVmLockHeld(bool CaptureTerminationReason) { + // Notify plugins that the VM is about to stop, paired with a prior OnVmStarted. Skipped for a + // VM that never finished starting (m_vmStartNotified false, e.g. bring-up failure). Fired under + // the lock, so the handler must not call back into the session. + if (m_vmStartNotified.exchange(false) && m_hooks.OnVmStopping) + { + m_hooks.OnVmStopping(); + } + if (m_hooks.TearDownSessionState) { m_hooks.TearDownSessionState(); diff --git a/src/windows/wslcsession/WSLCSessionRuntime.h b/src/windows/wslcsession/WSLCSessionRuntime.h index cb22b3ade7..5558fa720a 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.h +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -61,6 +61,15 @@ class WSLCSessionRuntime std::function TearDownSessionState; std::function OnSpontaneousExit; WSLCVirtualMachine::TOnCrashDump OnCrashDump; + + // Fired when a VM has started (best-effort). Invoked without the runtime lock held so the + // handler may call back into the session (e.g. to run setup in the VM). Fired every time a + // VM is (re)created for the session. + std::function OnVmStarted; + + // Fired when a VM is about to be torn down (best-effort). Invoked under the runtime lock, so + // the handler must not call back into the session. Paired with a prior OnVmStarted. + std::function OnVmStopping; }; struct SessionContext @@ -166,6 +175,11 @@ class WSLCSessionRuntime bool IdleTerminationEnabled() const noexcept; int StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs); + // Fires the OnVmStarted hook. Must be called without the runtime lock held (the handler may + // call back into the session), with an activity reference held so idle teardown cannot race the + // VM down before the notification is delivered. + void NotifyVmStarted(); + WSLCSession* m_session{}; RuntimeHooks m_hooks; @@ -198,6 +212,10 @@ class WSLCSessionRuntime wil::srwlock m_lock; std::atomic m_vmState{VmState::None}; std::atomic m_vmExitDisposition{VmExitDisposition::Active}; + + // Set when OnVmStarted has fired for the current VM; gates the paired OnVmStopping so a VM that + // never finished starting (or had no started notification) does not emit a spurious stopping. + std::atomic m_vmStartNotified{false}; std::shared_ptr m_idleState{std::make_shared()}; WSLCVirtualMachineTerminationReason m_lastTerminationReason{WSLCVirtualMachineTerminationReasonUnknown}; diff --git a/test/windows/PluginTests.cpp b/test/windows/PluginTests.cpp index 9abfcaaedc..687dd5954b 100644 --- a/test/windows/PluginTests.cpp +++ b/test/windows/PluginTests.cpp @@ -780,6 +780,50 @@ class PluginTests ValidateLogFile(ExpectedOutput); } + // Validates the VM-lifecycle hooks: OnWslcVmStarted fires each time the VM is (re)created and + // OnWslcVmStopping each time it is torn down, decoupled from the once-per-session hooks. Also + // proves the started hook can call back into the session (WSLCCreateProcess) without deadlocking. + WSL2_TEST_METHOD(WslcVmRestart) + { + ConfigurePlugin(PluginTestType::WslcVmRestart); + + { + auto session = CreateWslcSession(L"plugin-wslc-vm-restart"); + + // First operation brings the VM up -> OnWslcVmStarted (which reentrantly runs a process). + { + wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); + auto process = launcher.Launch(*session); + } + + // Force idle teardown of the running VM -> OnWslcVmStopping. + BOOL wasAlreadyIdle = TRUE; + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_FALSE(wasAlreadyIdle); + + // Next operation lazily restarts the VM -> OnWslcVmStarted fires again. + { + wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); + auto process = launcher.Launch(*session); + } + + // Session teardown tears the second VM down -> OnWslcVmStopping, then OnWslcSessionStopping. + } + + constexpr auto ExpectedOutput = + LR"(Plugin loaded. TestMode=22 + WSLC Session created, name=plugin-wslc-vm-restart, id=*, pid=*, token=set, sid=set + WSLC VM started, session=* + WSLC VM started reentrant WSLCCreateProcess: ok + WSLC VM stopping, session=* + WSLC VM started, session=* + WSLC VM started reentrant WSLCCreateProcess: ok + WSLC VM stopping, session=* + WSLC Session stopping, name=plugin-wslc-vm-restart, id=*)"; + + ValidateLogFile(ExpectedOutput); + } + // This test must run last so it doesn't break test cases that depends on plugin signature. WSL2_TEST_METHOD(InvalidPluginSignature) { diff --git a/test/windows/PluginTests.h b/test/windows/PluginTests.h index c9a779031c..bfe08fcbf7 100644 --- a/test/windows/PluginTests.h +++ b/test/windows/PluginTests.h @@ -41,7 +41,8 @@ enum class PluginTestType WslcSuccess, WslcSessionRejected, WslcContainerRejected, - WslcImagePull + WslcImagePull, + WslcVmRestart }; constexpr auto c_testType = L"TestType"; diff --git a/test/windows/testplugin/Plugin.cpp b/test/windows/testplugin/Plugin.cpp index 17087e6edc..cc8b700a01 100644 --- a/test/windows/testplugin/Plugin.cpp +++ b/test/windows/testplugin/Plugin.cpp @@ -539,6 +539,44 @@ HRESULT OnWslcImageDeleted(const WSLCSessionInformation* Session, LPCSTR ImageId return S_OK; } +HRESULT OnWslcVmStarted(const WSLCSessionInformation* Session) +try +{ + // Only log/exercise for the dedicated VM-restart test so other WSLC plugin tests (which start + // and stop VMs incidentally) are not affected by extra log lines. + if (g_testType != PluginTestType::WslcVmRestart) + { + return S_OK; + } + + g_logfile << "WSLC VM started, session=" << Session->SessionId << std::endl; + + // Prove the VM is usable from within the started hook, and that calling back into the session + // (WSLCCreateProcess acquires a VM lease + the runtime lock) does not deadlock. + std::vector args = {"/bin/true", nullptr}; + WSLCProcessHandle process = nullptr; + const auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr); + g_logfile << "WSLC VM started reentrant WSLCCreateProcess: " << (SUCCEEDED(hr) ? "ok" : "failed") << std::endl; + if (SUCCEEDED(hr)) + { + g_api->WSLCReleaseProcess(process); + } + + return S_OK; +} +CATCH_RETURN(); + +HRESULT OnWslcVmStopping(const WSLCSessionInformation* Session) +{ + if (g_testType != PluginTestType::WslcVmRestart) + { + return S_OK; + } + + g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl; + return S_OK; +} + EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPluginAPIV1* Api, WSLPluginHooksV1* Hooks) { try @@ -550,7 +588,7 @@ EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPlugin THROW_HR_IF(E_UNEXPECTED, !g_logfile); g_testType = static_cast(ReadDword(key.get(), nullptr, c_testType, static_cast(PluginTestType::Invalid))); - THROW_HR_IF(E_INVALIDARG, static_cast(g_testType) <= 0 || static_cast(g_testType) > static_cast(PluginTestType::WslcImagePull)); + THROW_HR_IF(E_INVALIDARG, static_cast(g_testType) <= 0 || static_cast(g_testType) > static_cast(PluginTestType::WslcVmRestart)); g_logfile << "Plugin loaded. TestMode=" << static_cast(g_testType) << std::endl; g_api = Api; @@ -566,6 +604,8 @@ EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPlugin Hooks->ContainerStopping = &OnWslcContainerStopping; Hooks->ImageCreated = &OnWslcImageCreated; Hooks->ImageDeleted = &OnWslcImageDeleted; + Hooks->WslcVmStarted = &OnWslcVmStarted; + Hooks->WslcVmStopping = &OnWslcVmStopping; if (g_testType == PluginTestType::FailToLoad) { From 40b2c769ff975fa8da7ecdb66373c3d517e3a41a Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 9 Jul 2026 13:40:06 -0700 Subject: [PATCH 19/31] Pair OnVmStopping only when OnVmStarted fired Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCSessionRuntime.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 96be5a69c5..8066917614 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -254,10 +254,9 @@ void WSLCSessionRuntime::EnsureVmRunning() void WSLCSessionRuntime::NotifyVmStarted() { - m_vmStartNotified.store(true); - if (m_hooks.OnVmStarted) { + m_vmStartNotified.store(true); m_hooks.OnVmStarted(); } } From 944c1f6246e9411be307b6f04fe374e6557df302 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 13 Jul 2026 10:39:22 -0700 Subject: [PATCH 20/31] wslc: address PR feedback and harden idle-terminate VM lifecycle - Session-scope DockerEventTracker; rebind per VM start, preserve subscriptions - Fire OnVmStopping with m_lock dropped to avoid plugin-reentrancy deadlock - Keep containers alive on idle teardown; clear only on permanent shutdown - Redesign OnIdleTimer to commit teardown unconditionally after notifying, matching the test-trigger path; removes phantom OnVmStopping->OnVmStarted and the concurrent-Terminate is_signaled crash - Dedup containerd storage mount point into WSLCSessionDefaults.h - Mark WslcVmStarted/WslcVmStopping hooks as introduced in 2.9.5 --- src/windows/common/WSLCSessionDefaults.h | 1 + src/windows/inc/WslPluginApi.h | 9 +- .../service/exe/WSLCSessionManager.cpp | 6 + src/windows/service/exe/WSLCSessionManager.h | 13 ++ src/windows/service/inc/wslc.idl | 22 ++- .../wslcsession/DockerEventTracker.cpp | 6 +- src/windows/wslcsession/DockerEventTracker.h | 7 +- src/windows/wslcsession/WSLCContainer.cpp | 173 ++++++++++++++---- src/windows/wslcsession/WSLCContainer.h | 31 +++- src/windows/wslcsession/WSLCSession.cpp | 55 +++++- src/windows/wslcsession/WSLCSession.h | 4 +- .../wslcsession/WSLCSessionRuntime.cpp | 95 ++++++++-- src/windows/wslcsession/WSLCSessionRuntime.h | 31 +++- .../wslcsession/WSLCVirtualMachine.cpp | 47 ++--- src/windows/wslcsession/WSLCVirtualMachine.h | 16 +- test/windows/PluginTests.cpp | 21 ++- test/windows/WSLCTests.cpp | 172 ++++++++++++++--- test/windows/testplugin/Plugin.cpp | 26 +++ 18 files changed, 587 insertions(+), 148 deletions(-) diff --git a/src/windows/common/WSLCSessionDefaults.h b/src/windows/common/WSLCSessionDefaults.h index e288d4179f..9978a4a8cc 100644 --- a/src/windows/common/WSLCSessionDefaults.h +++ b/src/windows/common/WSLCSessionDefaults.h @@ -22,5 +22,6 @@ inline constexpr const wchar_t DefaultAdminSessionName[] = L"wslc-cli-admin"; inline constexpr const wchar_t DefaultStorageSubPath[] = L"wslc\\sessions"; inline constexpr const wchar_t DefaultStorageVhdName[] = L"storage.vhdx"; inline constexpr uint32_t DefaultBootTimeoutMs = 30000; +inline constexpr const char ContainerdStorageMountPoint[] = "/var/lib/docker"; } // namespace wsl::windows::wslc diff --git a/src/windows/inc/WslPluginApi.h b/src/windows/inc/WslPluginApi.h index c4f6070e8d..2b60a20be8 100644 --- a/src/windows/inc/WslPluginApi.h +++ b/src/windows/inc/WslPluginApi.h @@ -149,8 +149,9 @@ typedef HRESULT (*WSLPluginAPI_OnWslcVmStarted)(const struct WSLCSessionInformat // Called when the VM backing a WSLC session is about to stop (idle teardown, explicit termination, // or unexpected exit). Fires every time a VM is torn down, paired with a prior OnWslcVmStarted. -// Errors are logged but ignored. The VM is going away, so this hook must not call back into the -// session (e.g. via WSLCCreateProcess). +// Errors are logged but ignored. On an idle teardown the session remains alive, so a callback that +// needs the VM (e.g. WSLCCreateProcess) transparently restarts it; during a permanent session +// termination the same call fails cleanly because the session is being torn down. typedef HRESULT (*WSLPluginAPI_OnWslcVmStopping)(const struct WSLCSessionInformation* Session); // @@ -232,8 +233,8 @@ struct WSLPluginHooksV1 WSLPluginAPI_ContainerStopping ContainerStopping; WSLPluginAPI_ImageCreated ImageCreated; WSLPluginAPI_ImageDeleted ImageDeleted; - WSLPluginAPI_OnWslcVmStarted WslcVmStarted; - WSLPluginAPI_OnWslcVmStopping WslcVmStopping; + WSLPluginAPI_OnWslcVmStarted WslcVmStarted; // Introduced in 2.9.5 + WSLPluginAPI_OnWslcVmStopping WslcVmStopping; // Introduced in 2.9.5 }; struct WSLPluginAPIV1 diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index 13283c9547..f6beab093a 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -181,6 +181,12 @@ try } CATCH_LOG() +bool WSLCSessionManagerImpl::InPluginNotificationContext() noexcept +{ + const auto* context = wsl::windows::common::ExecutionContext::Current(); + return context != nullptr && WI_IsFlagSet(context->CurrentContext(), wsl::windows::common::Context::Plugin); +} + void WSLCSessionManagerImpl::CreateSession( _In_ const WSLCSessionSettings* Settings, _In_ WSLCSessionFlags Flags, _In_opt_ IWarningCallback* WarningCallback, _Out_ IWSLCSession** WslcSession) { diff --git a/src/windows/service/exe/WSLCSessionManager.h b/src/windows/service/exe/WSLCSessionManager.h index 315cc22c7e..e8188d7a7f 100644 --- a/src/windows/service/exe/WSLCSessionManager.h +++ b/src/windows/service/exe/WSLCSessionManager.h @@ -129,6 +129,16 @@ class WSLCSessionManagerImpl wil::com_ptr lockedSession; if (FAILED_LOG(entry.Ref->OpenSession(&lockedSession))) { + // Session is gone. Notifying plugins fires OnWslcSessionStopping, which is a plugin + // callback and must not nest inside another plugin callback (ExecutionContext forbids + // it). If a plugin called back into WSLC from within a notification (e.g. creating a + // process from OnWslcVmStopping), defer this lazy notify + prune to a later, non- + // reentrant pass rather than firing OnWslcSessionStopping on top of the current one. + if (InPluginNotificationContext()) + { + return false; // Keep in tracking; clean up on a later pass. + } + // Session is gone: notify plugins (if not already), then drop persistent reference if any. NotifySessionStoppingLockHeld(entry); @@ -174,6 +184,9 @@ class WSLCSessionManagerImpl void NotifySessionStoppingLockHeld(SessionEntry& entry) noexcept; + // Returns true if the calling thread is currently running inside a plugin notification callback. + static bool InPluginNotificationContext() noexcept; + std::atomic m_nextSessionId{1}; std::recursive_mutex m_wslcSessionsLock; diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 935192ceed..f8de7c8bf6 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -642,14 +642,6 @@ interface IWSLCSession : IUnknown // Container management. HRESULT CreateContainer([in] const WSLCContainerOptions* Options, [in, unique] IWarningCallback* WarningCallback, [out] IWSLCContainer** Container); HRESULT OpenContainer([in, ref] LPCSTR Id, [out] IWSLCContainer** Container); - - // Keeps the VM alive for the duration of a client-side container operation. The CLI performs - // each mutation as two round-trips (OpenContainer followed by the operation) and may stream - // output afterwards. With on-demand VM idle-termination the VM could otherwise tear down - // between those calls, disconnecting the container wrapper and failing the second call with - // RPC_E_DISCONNECTED. The client holds the returned token for the whole operation; releasing - // it (or the client exiting) lets the VM idle-terminate again. - HRESULT BeginContainerOperation([out] IUnknown** Operation); HRESULT ListContainers([in, unique] const WSLCListContainersOptions* Options,[out, size_is(, *Count)] WSLCContainerEntry** Containers,[out] ULONG* Count, [out, size_is(, *PortsCount)] WSLCContainerPortMapping** Ports, [out] ULONG* PortsCount); HRESULT PruneContainers([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out] WSLCPruneContainersResults* Result); @@ -701,9 +693,19 @@ interface IWSLCSession : IUnknown HRESULT RegisterCrashDumpCallback([in] ICrashDumpCallback* Callback, [out] IUnknown** Subscription); // Used only for testing. Synchronously runs the idle-termination teardown path, ignoring the - // activity-count and idle-timeout-enabled guards so a test can force the VM down even while a - // container holds an activity reference. WasAlreadyIdle is TRUE when the VM was not running. + // activity-count guard so a test can force the VM down even while a container holds an activity + // reference. The idle-termination-enabled (persistent-storage) guard is still honored: a + // tmpfs-backed session is never torn down, since that would lose unrecoverable state. + // WasAlreadyIdle is TRUE when the VM was not running. HRESULT TriggerIdleTermination([out] BOOL* WasAlreadyIdle); + + // Keeps the VM alive for the duration of a client-side container operation. The CLI performs + // each mutation as two round-trips (OpenContainer followed by the operation) and may stream + // output afterwards. With on-demand VM idle-termination the VM could otherwise tear down + // between those calls, disconnecting the container wrapper and failing the second call with + // RPC_E_DISCONNECTED. The client holds the returned token for the whole operation; releasing + // it (or the client exiting) lets the VM idle-terminate again. + HRESULT BeginContainerOperation([out] IUnknown** Operation); } // diff --git a/src/windows/wslcsession/DockerEventTracker.cpp b/src/windows/wslcsession/DockerEventTracker.cpp index f5018d3bca..95c06b0e7b 100644 --- a/src/windows/wslcsession/DockerEventTracker.cpp +++ b/src/windows/wslcsession/DockerEventTracker.cpp @@ -61,7 +61,11 @@ DockerEventTracker::EventTrackingReference::~EventTrackingReference() noexcept Reset(); } -DockerEventTracker::DockerEventTracker(DockerHTTPClient& dockerClient, WSLCSession& session, IORelay& relay) : m_session(session) +DockerEventTracker::DockerEventTracker(WSLCSession& session) : m_session(session) +{ +} + +void DockerEventTracker::Connect(DockerHTTPClient& dockerClient, IORelay& relay) { auto onChunk = [this](const gsl::span& buffer) { if (!buffer.empty()) // docker inserts empty lines between events, skip those. diff --git a/src/windows/wslcsession/DockerEventTracker.h b/src/windows/wslcsession/DockerEventTracker.h index e040c7592f..68b8195970 100644 --- a/src/windows/wslcsession/DockerEventTracker.h +++ b/src/windows/wslcsession/DockerEventTracker.h @@ -64,9 +64,14 @@ class DockerEventTracker using ContainerStateChangeCallback = std::function, std::uint64_t)>; using VolumeEventCallback = std::function; - DockerEventTracker(DockerHTTPClient& dockerClient, WSLCSession& session, IORelay& relay); + explicit DockerEventTracker(WSLCSession& session); ~DockerEventTracker(); + // Binds the tracker to a VM's docker client and IO relay. Called on every VM start. Existing + // container/volume registrations are preserved across (re)connects so callers do not re-register + // when the VM is idle-terminated and later restarted. + void Connect(DockerHTTPClient& dockerClient, IORelay& relay); + EventTrackingReference RegisterContainerStateUpdates(const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept; EventTrackingReference RegisterExecStateUpdates(const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept; EventTrackingReference RegisterVolumeUpdates(VolumeEventCallback&& Callback) noexcept; diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 6664c3ea17..6f05233354 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -36,8 +36,10 @@ using wsl::windows::common::io::OverlappedIOHandle; using wsl::windows::common::io::ReadHandle; using wsl::windows::common::io::RelayHandle; using wsl::windows::service::wslc::ContainerPortMapping; +using wsl::windows::service::wslc::DockerEventTracker; using wsl::windows::service::wslc::DockerHTTPClient; using wsl::windows::service::wslc::DockerHTTPException; +using wsl::windows::service::wslc::IORelay; using wsl::windows::service::wslc::IWSLCVolume; using wsl::windows::service::wslc::NetworkEntry; using wsl::windows::service::wslc::RelayedProcessIO; @@ -53,6 +55,7 @@ using wsl::windows::service::wslc::WSLCPortMapping; using wsl::windows::service::wslc::WSLCSession; using wsl::windows::service::wslc::WSLCVirtualMachine; using wsl::windows::service::wslc::WSLCVolumeMount; +using wsl::windows::service::wslc::WSLCVolumes; using namespace wsl::windows::common::io; using namespace wsl::windows::common::docker_schema; @@ -545,20 +548,16 @@ WSLCContainerImpl::WSLCContainerImpl( WSLCContainerFlags ContainerFlags) : m_wslcSession(wslcSession), m_pluginNotifier(pluginNotifier), - m_virtualMachine(runtime.Vm()), + m_runtime(runtime), m_name(std::move(Name)), m_image(std::move(Image)), m_networkMode(std::move(NetworkMode)), m_id(std::move(Id)), m_mountedVolumes(std::move(volumes)), m_namedVolumes(std::move(namedVolumes)), - m_volumes(runtime.Volumes()), m_mappedPorts(std::move(ports)), m_labels(std::move(labels)), m_comWrapper(wil::MakeOrThrow(wslcSession, std::move(onDeleted))), - m_dockerClient(runtime.Docker()), - m_eventTracker(runtime.Events()), - m_ioRelay(*runtime.Relay()), m_containerEvents(runtime.Events().RegisterContainerStateUpdates( m_id, std::bind(&WSLCContainerImpl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))), m_state(InitialState), @@ -626,6 +625,31 @@ void WSLCContainerImpl::Initialize() m_comWrapper->Initialize(weak_from_this()); } +WSLCVirtualMachine& WSLCContainerImpl::Vm() const +{ + return m_runtime.Vm(); +} + +DockerHTTPClient& WSLCContainerImpl::Docker() const +{ + return m_runtime.Docker(); +} + +WSLCVolumes& WSLCContainerImpl::Volumes() const +{ + return m_runtime.Volumes(); +} + +DockerEventTracker& WSLCContainerImpl::Events() const +{ + return m_runtime.Events(); +} + +IORelay& WSLCContainerImpl::Relay() const +{ + return *m_runtime.Relay(); +} + void WSLCContainerImpl::SetExitCode(int ExitCode) noexcept { std::lock_guard processesLock{m_processesLock}; @@ -702,7 +726,7 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* try { - ioHandle = m_dockerClient.AttachContainer(m_id, DetachKeys == nullptr ? std::nullopt : std::optional(DetachKeys)); + ioHandle = Docker().AttachContainer(m_id, DetachKeys == nullptr ? std::nullopt : std::optional(DetachKeys)); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to attach to container '%hs'", m_id.c_str()); @@ -733,7 +757,7 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* handles.emplace_back(std::make_unique( std::move(ioHandle), std::move(stdoutWrite), std::move(stderrWrite), DockerIORelayHandle::Format::Raw)); - m_ioRelay.AddHandles(std::move(handles)); + Relay().AddHandles(std::move(handles)); *Stdin = common::wslutil::ToCOMOutputHandle(reinterpret_cast(stdinWrite.get()), GENERIC_WRITE | SYNCHRONIZE, WSLCHandleTypePipe); @@ -780,11 +804,11 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty)) { io = std::make_unique(TypedHandle{ - wil::unique_handle{(HANDLE)m_dockerClient.AttachContainer(m_id, detachKeys).release()}, WSLCHandleTypeSocket}); + wil::unique_handle{(HANDLE)Docker().AttachContainer(m_id, detachKeys).release()}, WSLCHandleTypeSocket}); } else { - wil::unique_handle stream{reinterpret_cast(m_dockerClient.AttachContainer(m_id, detachKeys).release())}; + wil::unique_handle stream{reinterpret_cast(Docker().AttachContainer(m_id, detachKeys).release())}; io = CreateRelayedProcessIO(std::move(stream), m_initProcessFlags); } } @@ -795,7 +819,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt THROW_DOCKER_USER_ERROR_MSG(e, "Failed to attach to container '%hs' during start", m_id.c_str()); } - auto control = std::make_unique(*this, m_dockerClient); + auto control = std::make_unique(*this, Docker()); std::lock_guard processesLock{m_processesLock}; m_initProcessControl = control.get(); @@ -811,7 +835,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt std::vector unavailableVolumes; for (const auto& volumeName : m_namedVolumes) { - const auto [code, message] = m_volumes.GetVolumeStatus(volumeName); + const auto [code, message] = Volumes().GetVolumeStatus(volumeName); if (FAILED(code)) { EMIT_USER_WARNING(Localization::MessageWslcVolumeNotAvailableReason(volumeName, message)); @@ -824,7 +848,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt Localization::MessageWslcVolumeNotAvailable(wsl::shared::string::Join(unavailableVolumes, ',')), !unavailableVolumes.empty()); - auto volumeCleanup = MountVolumes(m_mountedVolumes, m_virtualMachine); + auto volumeCleanup = MountVolumes(m_mountedVolumes, Vm()); auto portCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { UnmapPorts(); }); MapPorts(); @@ -834,7 +858,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt try { - m_dockerClient.StartContainer(m_id, detachKeys); + Docker().StartContainer(m_id, detachKeys); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to start container '%hs'", m_id.c_str()); @@ -842,7 +866,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt { try { - m_dockerClient.ResizeContainerTty(m_id, StartOptions->TtyRows, StartOptions->TtyColumns); + Docker().ResizeContainerTty(m_id, StartOptions->TtyRows, StartOptions->TtyColumns); } CATCH_LOG(); } @@ -857,7 +881,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt LOG_HR_MSG(pluginResult, "Plugin rejected start of container '%hs' (0x%x)", m_id.c_str(), pluginResult); try { - m_dockerClient.StopContainer(m_id.c_str(), {}, {}); + Docker().StopContainer(m_id.c_str(), {}, {}); } catch (...) { @@ -971,7 +995,7 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) { if (Kill) { - m_dockerClient.SignalContainer(m_id, SignalArg); + Docker().SignalContainer(m_id, SignalArg); if (!waitForStop) { @@ -986,7 +1010,7 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) TimeoutArg = TimeoutSeconds; } - m_dockerClient.StopContainer(m_id, SignalArg, TimeoutArg); + Docker().StopContainer(m_id, SignalArg, TimeoutArg); } } catch (const DockerHTTPException& e) @@ -1056,6 +1080,86 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl:: return comWrapper; } +void WSLCContainerImpl::OnVmTornDown() noexcept +try +{ + auto lock = m_lock.lock_exclusive(); + + // Only running containers have an init process and VM activity hold to reconcile. Port reservations + // self-neuter when the VM's table is destroyed; RecoverPorts re-reserves them on restart. + if (m_state != WslcContainerStateRunning) + { + return; + } + + // VM torn down while running (idle-termination or crash). Mirror the stop path: record a synthetic + // SIGKILL exit (else a later die/Destroy event asserts on a missing code), release VM-scoped + // resources, drop to Exited (releasing the activity hold). Wrapper stays connected so COM refs stay + // valid; RecoverState reattaches it next start. + SetExitCode(128 + WSLCSignalSIGKILL); + ReleaseProcesses(); + ReleaseRuntimeResources(); + + Transition(WslcContainerStateExited); + SignalInitProcessExit(); +} +CATCH_LOG() + +void WSLCContainerImpl::RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer) +{ + auto lock = m_lock.lock_exclusive(); + + // Re-register VM-scoped port reservations (self-neutered when the old VM's table died) against the + // restarted VM using the numbers recorded at create time, restoring bridge-mode forwarding. + const bool allocateVmPorts = NetworkModeAllocatesVmPorts(m_networkMode); + if (!allocateVmPorts) + { + return; + } + + auto metadataIt = dockerContainer.Labels.find(WSLCContainerMetadataLabel); + if (metadataIt == dockerContainer.Labels.end()) + { + return; + } + + auto metadata = ParseContainerMetadata(metadataIt->second.c_str()); + + std::vector ports; + ports.reserve(metadata.Ports.size()); + for (const auto& e : metadata.Ports) + { + auto& inserted = ports.emplace_back(ContainerPortMapping{VMPortMapping::FromContainerMetaData(e), e.ContainerPort}); + + auto allocation = Vm().TryAllocatePort(e.VmPort, e.Family, e.Protocol); + + THROW_HR_IF_MSG( + HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), !allocation, "Port %hu is in use, cannot recover container %hs", e.VmPort, m_id.c_str()); + + inserted.VmMapping.AssignVmPort(allocation); + } + + m_mappedPorts = std::move(ports); +} + +unique_com_disconnect WSLCContainerImpl::RemoveExitedAutoRemoveSurvivor(bool& Removed) +{ + Removed = false; + + auto lock = m_lock.lock_exclusive(); + + // Only an Exited --rm survivor needs cleanup: OnVmTornDown forced a running --rm container to Exited + // without the auto-remove delete. One that exited normally was already deleted by OnStopped. + if (m_state != WslcContainerStateExited || WI_IsFlagClear(m_containerFlags, WSLCContainerFlagsRm)) + { + return {}; + } + + auto wrapper = DeleteExclusiveLockHeld(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes); + Removed = true; + return wrapper; +} + void WSLCContainerImpl::Delete(WSLCDeleteFlags Flags) { // N.B. wrapper must be destroyed after m_lock is released, since its destructor calls Disconnect(). @@ -1088,7 +1192,7 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl:: try { - m_dockerClient.DeleteContainer(m_id, WI_IsFlagSet(Flags, WSLCDeleteFlagsForce), WI_IsFlagSet(Flags, WSLCDeleteFlagsDeleteVolumes)); + Docker().DeleteContainer(m_id, WI_IsFlagSet(Flags, WSLCDeleteFlagsForce), WI_IsFlagSet(Flags, WSLCDeleteFlagsDeleteVolumes)); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to delete container '%hs'", m_id.c_str()); @@ -1104,7 +1208,7 @@ void WSLCContainerImpl::Export(WSLCHandle OutHandle) const THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_IS_RUNNING, Localization::MessageWslcContainerIsRunning(m_id), m_state == WslcContainerStateRunning); std::pair SocketCodePair; - SocketCodePair = m_dockerClient.ExportContainer(m_id); + SocketCodePair = Docker().ExportContainer(m_id); auto userHandle = m_wslcSession.OpenUserHandle(OutHandle); @@ -1220,12 +1324,12 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProces try { - auto result = m_dockerClient.CreateExec(m_id, request); + auto result = Docker().CreateExec(m_id, request); // N.B. There's no way to delete a created exec instance, it is removed when the container is deleted. wil::unique_handle stream{ - (HANDLE)m_dockerClient + (HANDLE)Docker() .StartExec(result.Id, common::docker_schema::StartExec{.Tty = request.Tty, .ConsoleSize = request.ConsoleSize}) .release()}; @@ -1239,7 +1343,7 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProces io = CreateRelayedProcessIO(std::move(stream), Options->Flags); } - auto control = std::make_shared(*this, result.Id, m_dockerClient, m_eventTracker); + auto control = std::make_shared(*this, result.Id, Docker(), Events()); { std::lock_guard processesLock{m_processesLock}; @@ -1262,7 +1366,7 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProces do { - auto state = m_dockerClient.InspectExec(result.Id); + auto state = Docker().InspectExec(result.Id); if (state.Running && state.Pid > 0) { control->SetPid(state.Pid); @@ -1952,7 +2056,7 @@ void WSLCContainerImpl::Inspect(LPSTR* Output) const std::string WSLCContainerImpl::InspectLockHeld() const { // Get Docker inspect data - auto dockerInspect = m_dockerClient.InspectContainer(m_id); + auto dockerInspect = Docker().InspectContainer(m_id); // Convert to WSLC schema auto wslcInspect = BuildInspectContainer(dockerInspect); @@ -1968,7 +2072,7 @@ void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle wil::unique_socket socket; try { - socket = m_dockerClient.ContainerLogs(m_id, Flags, Since, Until, Tail); + socket = Docker().ContainerLogs(m_id, Flags, Since, Until, Tail); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to get logs from '%hs'", m_id.c_str()); @@ -1978,7 +2082,7 @@ void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle auto [ttyRead, ttyWrite] = common::wslutil::OpenAnonymousPipe(0, true, true); auto handle = std::make_unique>(std::move(socket), std::move(ttyWrite)); - m_ioRelay.AddHandle(std::move(handle)); + Relay().AddHandle(std::move(handle)); *Stdout = common::wslutil::ToCOMOutputHandle(ttyRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe); } @@ -1991,7 +2095,7 @@ void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle auto handle = std::make_unique( std::move(socket), std::move(stdoutWrite), std::move(stderrWrite), DockerIORelayHandle::Format::HttpChunked); - m_ioRelay.AddHandle(std::move(handle)); + Relay().AddHandle(std::move(handle)); *Stdout = common::wslutil::ToCOMOutputHandle(stdoutRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe); *Stderr = common::wslutil::ToCOMOutputHandle(stderrRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe); @@ -2004,7 +2108,7 @@ void WSLCContainerImpl::Stats(LPSTR* Output) const try { - auto stats = m_dockerClient.ContainerStats(m_id); + auto stats = Docker().ContainerStats(m_id); // Always inject the authoritative id and name from this instance. // The response may omit them or use inconsistent casing. @@ -2051,7 +2155,7 @@ std::unique_ptr WSLCContainerImpl::CreateRelayedProcessIO(wil: ioHandles.emplace_back(std::make_unique( std::move(stream), std::move(stdoutWrite), std::move(stderrWrite), common::io::DockerIORelayHandle::Format::Raw)); - m_ioRelay.AddHandles(std::move(ioHandles)); + Relay().AddHandles(std::move(ioHandles)); return std::make_unique(std::move(fds)); } @@ -2075,8 +2179,7 @@ void WSLCContainerImpl::MapPorts() } else { - auto allocatedPort = - m_virtualMachine.TryAllocatePort(e.ContainerPort, e.VmMapping.BindAddress.si_family, e.VmMapping.Protocol); + auto allocatedPort = Vm().TryAllocatePort(e.ContainerPort, e.VmMapping.BindAddress.si_family, e.VmMapping.Protocol); THROW_HR_WITH_USER_ERROR_IF( HRESULT_FROM_WIN32(WSAEADDRINUSE), wsl::shared::Localization::MessageWslcPortInUse(FormatPortEndpoint(e), m_id), !allocatedPort); @@ -2089,7 +2192,7 @@ void WSLCContainerImpl::MapPorts() try { - m_virtualMachine.MapPort(e.VmMapping); + Vm().MapPort(e.VmMapping); } catch (...) { @@ -2151,7 +2254,7 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::ReleaseRuntimeRes // Release runtime resources (port relays, volume mounts) that were set up at Start(). UnmapPorts(); - UnmountVolumes(m_mountedVolumes, m_virtualMachine); + UnmountVolumes(m_mountedVolumes, Vm()); } __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::ReleaseResources() @@ -2549,7 +2652,7 @@ void WSLCContainerImpl::ConnectToNetwork(const WSLCNetworkConnectionOptions* Opt try { - m_dockerClient.ConnectContainerToNetwork(Options->NetworkName, request); + Docker().ConnectContainerToNetwork(Options->NetworkName, request); } catch (const DockerHTTPException& e) { @@ -2581,7 +2684,7 @@ void WSLCContainerImpl::DisconnectFromNetwork(LPCSTR NetworkName) try { - m_dockerClient.DisconnectContainerFromNetwork(NetworkName, request); + Docker().DisconnectContainerFromNetwork(NetworkName, request); } catch (const DockerHTTPException& e) { diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 88c16a2053..27e1b708df 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -118,6 +118,21 @@ class WSLCContainerImpl : public std::enable_shared_from_this WSLCContainerState State() const noexcept; std::vector GetPorts() const; + // Reconciles a surviving wrapper after its VM was torn down (idle-termination or crash) while the + // container was running: records a synthetic init-process exit, releases VM-scoped resources and + // drops to Exited (releasing the VM activity hold). Keeps the wrapper connected so client COM + // references stay valid across the VM restart. + void OnVmTornDown() noexcept; + + // Re-registers a survivor's VM-scoped port allocations against the restarted VM (see OnVmTornDown). + void RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer); + + // Honors --rm for a survivor that was running when the VM was torn down: OnVmTornDown forced it to + // Exited but deferred the auto-remove delete while dockerd was down. Removes it now that the VM is + // back, mirroring OnStopped's Running->Exited delete. Sets Removed and returns the disconnect + // wrapper (destroy after dropping the container from tracking) when it deletes; otherwise a no-op. + [[nodiscard]] unique_com_disconnect RemoveExitedAutoRemoveSurvivor(bool& Removed); + __requires_lock_held(m_lock) void Transition(WSLCContainerState State, std::optional stateChangedAt = std::nullopt) noexcept; const std::string& ID() const noexcept; @@ -174,6 +189,16 @@ class WSLCContainerImpl : public std::enable_shared_from_this __requires_shared_lock_held(m_lock) std::string InspectLockHeld() const; + // Accessors for the session's VM-scoped resources. The container outlives any single VM: it + // survives idle-termination and is reused when the VM restarts. These fetch the current VM's + // objects from the (stable) runtime rather than caching references that would dangle across a + // restart. They are only valid while a VM lease is held (i.e. the VM is running). + WSLCVirtualMachine& Vm() const; + DockerHTTPClient& Docker() const; + WSLCVolumes& Volumes() const; + DockerEventTracker& Events() const; + IORelay& Relay() const; + mutable wil::srwlock m_lock; std::string m_name; std::string m_image; @@ -197,24 +222,20 @@ class WSLCContainerImpl : public std::enable_shared_from_this // Must be acquired before m_lock when both are needed. std::mutex m_stopLock; - DockerHTTPClient& m_dockerClient; + WSLCSessionRuntime& m_runtime; std::uint64_t m_stateChangedAt{static_cast(std::time(nullptr))}; std::uint64_t m_createdAt{}; WSLCContainerState m_state = WslcContainerStateInvalid; WSLCSession& m_wslcSession; IWSLCPluginNotifier* m_pluginNotifier; - WSLCVirtualMachine& m_virtualMachine; std::vector m_mappedPorts; std::vector m_mountedVolumes; std::vector m_namedVolumes; - WSLCVolumes& m_volumes; std::map m_labels; Microsoft::WRL::ComPtr m_comWrapper; - DockerEventTracker& m_eventTracker; DockerEventTracker::EventTrackingReference m_containerEvents; - IORelay& m_ioRelay; std::string m_networkMode; // Held (non-empty) exactly while the container is Running so the session's VM stays alive even diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index d926447e2c..6a13952277 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -36,7 +36,7 @@ using wsl::windows::service::wslc::WSLCExecutionContext; using wsl::windows::service::wslc::WSLCSession; using wsl::windows::service::wslc::WSLCVirtualMachine; -constexpr auto c_containerdStorage = "/var/lib/docker"; +constexpr auto c_containerdStorage = wsl::windows::wslc::ContainerdStorageMountPoint; constexpr auto c_containerdSocket = "/run/containerd/containerd.sock"; constexpr auto c_dockerdReadyLogLine = "API listen on /var/run/docker.sock"; constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName; @@ -456,12 +456,28 @@ try RecoverExistingContainers(); }; - hooks.TearDownSessionState = [this]() { + hooks.TearDownSessionState = [this](bool permanent) { std::lock_guard containersLock(m_containersLock); std::lock_guard networksLock(m_networksLock); - m_containers.clear(); + // Network metadata is rebuilt from dockerd on every VM start, so it is always dropped. m_networks.clear(); + + // Container wrappers are kept alive across idle teardown (only cleared on permanent shutdown) + // so client COM references stay valid; RecoverState reattaches them to the restarted VM. + if (permanent) + { + m_containers.clear(); + } + else + { + // Session lives on but the VM is going down: reconcile each surviving wrapper to a stopped + // state (releasing its VM activity hold) before teardown. + for (auto& [id, container] : m_containers) + { + container->OnVmTornDown(); + } + } }; hooks.OnSpontaneousExit = [this]() { LOG_IF_FAILED(Terminate()); }; @@ -489,8 +505,8 @@ try sessionContext.Id = m_id; sessionContext.DisplayName = m_displayName; sessionContext.Terminating = &m_terminating; - sessionContext.SessionTerminatingEvent = std::addressof(m_sessionTerminatingEvent); - sessionContext.SessionTerminatedEvent = std::addressof(m_sessionTerminatedEvent); + sessionContext.SessionTerminatingEvent = m_sessionTerminatingEvent; + sessionContext.SessionTerminatedEvent = m_sessionTerminatedEvent; m_runtime.Initialize(m_vmFactoryGitCookie, m_git, &m_settings, idleGracePeriod, std::move(sessionContext), std::move(hooks)); @@ -3557,6 +3573,35 @@ void WSLCSession::RecoverExistingContainers() for (const auto& dockerContainer : containers) { + // A kept-alive wrapper's cached state already matches dockerd's (reconciled in OnVmTornDown). + // Leave it (and its COM references) in place, just re-register its ports on the restarted VM. + if (auto existing = m_containers.find(dockerContainer.Id); existing != m_containers.end()) + { + // Isolate a port re-reservation conflict to this container so it can't fail the lazy start + // for every client: log, warn and continue, mirroring the Open() failure path below. + try + { + // Honor --rm for a survivor forced to Exited in OnVmTornDown (delete deferred): remove + // it rather than leaking it. Otherwise re-register its VM-scoped port allocations. + bool removed = false; + auto wrapper = existing->second->RemoveExitedAutoRemoveSurvivor(removed); + if (removed) + { + m_containers.erase(existing); + continue; + } + + existing->second->RecoverPorts(dockerContainer); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION_MSG("Failed to recover ports for container: %hs", dockerContainer.Id.c_str()); + EMIT_USER_WARNING( + Localization::MessageWslcFailedToRecoverContainer(wsl::shared::string::MultiByteToWide(dockerContainer.Id))); + } + continue; + } + try { auto container = WSLCContainerImpl::Open( diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index 00863dfd19..b099177d1b 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -357,8 +357,8 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession std::unordered_map> m_containers; std::mutex m_networksLock; std::unordered_map m_networks; - wil::unique_event m_sessionTerminatingEvent{wil::EventOptions::ManualReset}; - wil::unique_event m_sessionTerminatedEvent{wil::EventOptions::ManualReset}; + wil::shared_event m_sessionTerminatingEvent{wil::EventOptions::ManualReset}; + wil::shared_event m_sessionTerminatedEvent{wil::EventOptions::ManualReset}; WSLCVirtualMachineTerminationReason m_terminationReason{WSLCVirtualMachineTerminationReasonUnknown}; std::wstring m_terminationDetails; diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 8066917614..ff5fe0fd94 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -15,12 +15,13 @@ Module Name: #include "precomp.h" #include "WSLCSessionRuntime.h" #include "WSLCSession.h" +#include "WSLCSessionDefaults.h" using wsl::windows::service::wslc::WSLCSessionRuntime; namespace { -constexpr auto c_containerdStorage = "/var/lib/docker"; +constexpr auto c_containerdStorage = wsl::windows::wslc::ContainerdStorageMountPoint; constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000; constexpr DWORD c_processKillTimeoutMs = 10 * 1000; @@ -52,6 +53,9 @@ void WSLCSessionRuntime::Initialize( m_idleState->Initialize(idleGrace, [this]() { OnIdleTimer(); }); + // Session-scoped: subscriptions must survive VM restarts. Rebound per-VM in InitializeDockerRuntime. + m_eventTracker.emplace(*m_session); + m_initialized = true; } @@ -232,7 +236,7 @@ void WSLCSessionRuntime::EnsureVmRunning() // Do not (re)start the VM once the session is terminating or has terminated. This also // bounds VmLease's retry loop: a lease that races with Terminate() fails here instead of // restarting a VM that is being permanently torn down. - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent->is_signaled()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent.is_signaled()); if (m_vmState.load() != VmState::Running) { @@ -254,6 +258,15 @@ void WSLCSessionRuntime::EnsureVmRunning() void WSLCSessionRuntime::NotifyVmStarted() { + // m_notifyLock keeps the start/stop decision atomic against a concurrent NotifyVmStopping so hooks + // stay paired. Suppressed once terminating, else a racing Shutdown leaves OnVmStarted unpaired. + auto lock = std::lock_guard(m_notifyLock); + + if (m_terminating->load()) + { + return; + } + if (m_hooks.OnVmStarted) { m_vmStartNotified.store(true); @@ -261,6 +274,18 @@ void WSLCSessionRuntime::NotifyVmStarted() } } +void WSLCSessionRuntime::NotifyVmStopping() +{ + // Paired once with a prior OnVmStarted (skipped if bring-up never completed). m_notifyLock makes + // the gate check and hook call atomic against NotifyVmStarted. + auto lock = std::lock_guard(m_notifyLock); + + if (m_vmStartNotified.exchange(false) && m_hooks.OnVmStopping) + { + m_hooks.OnVmStopping(); + } +} + bool WSLCSessionRuntime::TryClaimExpectedStop() noexcept { auto expected = VmExitDisposition::Active; @@ -311,7 +336,7 @@ void WSLCSessionRuntime::StartVmLockHeld() wil::com_ptr vm; THROW_IF_FAILED(vmFactory->CreateVirtualMachine(&vm)); - m_virtualMachine.emplace(vm.get(), m_settings, m_sessionTerminatingEvent->get(), WSLCVirtualMachine::TOnCrashDump(m_hooks.OnCrashDump)); + m_virtualMachine.emplace(vm.get(), m_settings, m_sessionTerminatingEvent.get(), WSLCVirtualMachine::TOnCrashDump(m_hooks.OnCrashDump)); m_virtualMachine->Initialize(); // Get an event from the service that is signaled when the VM exits. @@ -348,8 +373,9 @@ void WSLCSessionRuntime::InitializeDockerRuntime(const std::filesystem::path& st m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000); - // Start the event tracker. - m_eventTracker.emplace(m_dockerClient.value(), *m_session, *m_ioRelay); + // (Re)bind the session-scoped event tracker to this VM's docker client and relay. Existing + // container subscriptions are preserved across restarts. + m_eventTracker->Connect(m_dockerClient.value(), *m_ioRelay); m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), storagePath); } @@ -375,17 +401,9 @@ void WSLCSessionRuntime::StopVmLockHeld() void WSLCSessionRuntime::TearDownVmLockHeld(bool CaptureTerminationReason) { - // Notify plugins that the VM is about to stop, paired with a prior OnVmStarted. Skipped for a - // VM that never finished starting (m_vmStartNotified false, e.g. bring-up failure). Fired under - // the lock, so the handler must not call back into the session. - if (m_vmStartNotified.exchange(false) && m_hooks.OnVmStopping) - { - m_hooks.OnVmStopping(); - } - if (m_hooks.TearDownSessionState) { - m_hooks.TearDownSessionState(); + m_hooks.TearDownSessionState(CaptureTerminationReason); } m_volumes.reset(); @@ -406,7 +424,8 @@ void WSLCSessionRuntime::TearDownVmLockHeld(bool CaptureTerminationReason) m_allocatedPorts.clear(); } - m_eventTracker.reset(); + // Not reset: session-scoped, subscriptions outlive the VM. Its stream handle dies with the IO relay + // above; InitializeDockerRuntime re-binds it on the next start. m_dockerClient.reset(); if (CaptureTerminationReason) @@ -528,12 +547,36 @@ try m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active); }); + // Final activity re-check under the lock before the irreversible OnVmStopping notification: a + // VmLease that bumped the count since the check above is now blocked on the shared lock we hold. + // Abandon here -- no notification, no teardown -- rather than stopping a VM about to be used again. + if (m_idleState->ActivityCount() != 0) + { + return; + } + + // Fire OnVmStopping with m_lock dropped so a plugin handler may take a VM lease without + // deadlocking, then commit to the teardown unconditionally. A lease that races in during this + // window finds the VM stopped (VmLease re-checks under the shared lock) and restarts it, firing a + // fresh OnVmStarted, so hooks stay paired. StopVmLockHeld no-ops if a concurrent Terminate already + // tore the VM down, and TearDownVmLockHeld handles a VM that died in the window. + lock.reset(); + NotifyVmStopping(); + lock = m_lock.lock_exclusive(); + StopVmLockHeld(); } CATCH_LOG(); bool WSLCSessionRuntime::TriggerIdleTerminationForTest() { + // tmpfs sessions have no persistent storage to recover from, so tearing the VM down loses all + // state. Match OnIdleTimer and refuse, so this shipping hook can't force data loss. + if (!IdleTerminationEnabled()) + { + return false; + } + // Mirror OnIdleTimer's MTA context on a dedicated thread: the incoming RPC thread is an STA, so // both re-initializing MTA on it and running teardown inline would fail (RPC_E_CHANGED_MODE / // RPC_E_WRONG_THREAD when releasing the VM's cross-process COM proxies). @@ -564,6 +607,12 @@ bool WSLCSessionRuntime::TriggerIdleTerminationForTest() m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active); }); + // Fire OnVmStopping without holding m_lock (see OnIdleTimer) so a plugin handler can + // acquire a VM lease without deadlocking. + lock.reset(); + NotifyVmStopping(); + lock = m_lock.lock_exclusive(); + StopVmLockHeld(); } catch (...) @@ -693,7 +742,7 @@ void WSLCSessionRuntime::OnVmExited() TraceLoggingLevel(WINEVENT_LEVEL_WARNING), TraceLoggingValue(m_id, "SessionId"), TraceLoggingValue(m_displayName.c_str(), "Name"), - TraceLoggingValue(!m_sessionTerminatingEvent->is_signaled(), "Unexpected")); + TraceLoggingValue(!m_sessionTerminatingEvent.is_signaled(), "Unexpected")); if (m_hooks.OnSpontaneousExit) { @@ -712,8 +761,16 @@ void WSLCSessionRuntime::Shutdown( // Acquire an exclusive lock to ensure that no operation is running. WI_VERIFY(sessionLock); - // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason. - // This mirrors the soft teardown used for idle shutdown, but here it is permanent. + // Notify with m_lock dropped, then re-lock for the teardown. The handler may call back into the + // session (e.g. WSLCCreateProcess) which takes a VM lease and this lock; firing under it would + // deadlock. m_terminating is set, so any such reentrant lease fails at EnsureVmRunning's gate + // rather than restarting the VM, and the reacquire can't block on it. + sessionLock.reset(); + NotifyVmStopping(); + sessionLock = m_lock.lock_exclusive(); + + // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason; the + // reacquired exclusive lock satisfies TearDownVmLockHeld's precondition. TearDownVmLockHeld(/* CaptureTerminationReason */ true); m_vmState.store(VmState::None); @@ -723,7 +780,7 @@ void WSLCSessionRuntime::Shutdown( // Signal completion last so any observer of the terminated event sees a fully torn-down // session and a populated termination reason. - m_sessionTerminatedEvent->SetEvent(); + m_sessionTerminatedEvent.SetEvent(); // Release the exclusive lock before disarming the idle timer. If a timer callback is currently // blocked acquiring the exclusive lock (about to evaluate idle teardown), it must be able to diff --git a/src/windows/wslcsession/WSLCSessionRuntime.h b/src/windows/wslcsession/WSLCSessionRuntime.h index 5558fa720a..e2495d8ae4 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.h +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -58,7 +58,11 @@ class WSLCSessionRuntime { std::function BringUp; std::function RecoverState; - std::function TearDownSessionState; + // Invoked while tearing down the VM, with the VM-scoped state still alive. The argument is + // true only for a permanent session shutdown (not an idle teardown): on idle teardown the + // container wrappers must be kept alive so client COM references stay valid and are reused + // when the VM restarts. + std::function TearDownSessionState; std::function OnSpontaneousExit; WSLCVirtualMachine::TOnCrashDump OnCrashDump; @@ -67,8 +71,11 @@ class WSLCSessionRuntime // VM is (re)created for the session. std::function OnVmStarted; - // Fired when a VM is about to be torn down (best-effort). Invoked under the runtime lock, so - // the handler must not call back into the session. Paired with a prior OnVmStarted. + // Fired when a VM is about to be torn down (best-effort), while the VM is still alive. Invoked + // without the runtime lock held so the handler may call back into the session or into a plugin + // that acquires a VM lease. Paired once with a prior OnVmStarted -- fires on both idle and + // permanent teardown. On a permanent teardown the session is terminating, so a callback that + // resolves this session (e.g. to create a process) fails cleanly instead of restarting the VM. std::function OnVmStopping; }; @@ -77,8 +84,8 @@ class WSLCSessionRuntime ULONG Id{}; std::wstring DisplayName; const std::atomic* Terminating{}; - wil::unique_event* SessionTerminatingEvent{}; - wil::unique_event* SessionTerminatedEvent{}; + wil::shared_event SessionTerminatingEvent; + wil::shared_event SessionTerminatedEvent; }; class VmLease @@ -180,6 +187,12 @@ class WSLCSessionRuntime // VM down before the notification is delivered. void NotifyVmStarted(); + // Fires the OnVmStopping hook (paired once with a prior OnVmStarted via m_vmStartNotified). Must + // be called without the runtime lock held: the handler may call into a plugin that acquires a VM + // lease (which takes m_lock), so firing under the lock would deadlock. Called while the VM is + // still running so the handler can still operate on it. + void NotifyVmStopping(); + WSLCSession* m_session{}; RuntimeHooks m_hooks; @@ -187,8 +200,8 @@ class WSLCSessionRuntime std::wstring m_displayName; bool m_initialized{}; const std::atomic* m_terminating{}; - wil::unique_event* m_sessionTerminatingEvent{}; - wil::unique_event* m_sessionTerminatedEvent{}; + wil::shared_event m_sessionTerminatingEvent; + wil::shared_event m_sessionTerminatedEvent; DWORD m_vmFactoryGitCookie{}; wil::com_ptr m_git; @@ -215,6 +228,10 @@ class WSLCSessionRuntime // Set when OnVmStarted has fired for the current VM; gates the paired OnVmStopping so a VM that // never finished starting (or had no started notification) does not emit a spurious stopping. + // Accessed under m_notifyLock so the gate check and hook call are atomic across the (unlocked) + // start/stop notifications, preventing unpaired or reordered plugin notifications when a lazy VM + // start races a permanent shutdown. + std::mutex m_notifyLock; std::atomic m_vmStartNotified{false}; std::shared_ptr m_idleState{std::make_shared()}; diff --git a/src/windows/wslcsession/WSLCVirtualMachine.cpp b/src/windows/wslcsession/WSLCVirtualMachine.cpp index 474c1a81f3..e934dae937 100644 --- a/src/windows/wslcsession/WSLCVirtualMachine.cpp +++ b/src/windows/wslcsession/WSLCVirtualMachine.cpp @@ -34,8 +34,8 @@ constexpr auto CONTAINER_PORT_RANGE = std::pair(20002, 65535 static_assert(c_ephemeralPortRange.second < CONTAINER_PORT_RANGE.first); -VmPortAllocation::VmPortAllocation(uint16_t port, int family, int protocol, WSLCVirtualMachine& vm) : - m_port(port), m_family(family), m_protocol(protocol), m_vm(&vm) +VmPortAllocation::VmPortAllocation(uint16_t port, int family, int protocol, std::weak_ptr reservations) : + m_port(port), m_family(family), m_protocol(protocol), m_reservations(std::move(reservations)) { } @@ -52,7 +52,7 @@ VmPortAllocation& VmPortAllocation::operator=(VmPortAllocation&& Other) m_port = Other.m_port; m_family = Other.m_family; m_protocol = Other.m_protocol; - m_vm = Other.m_vm; + m_reservations = Other.m_reservations; Other.Release(); } @@ -66,16 +66,20 @@ VmPortAllocation::~VmPortAllocation() void VmPortAllocation::Reset() { - if (m_vm != nullptr) + // Release the reservation only if the owning VM (and its table) is still alive. If the VM was torn + // down the table is already gone and lock() returns null, so a surviving allocation is a safe no-op. + if (auto reservations = m_reservations.lock()) { - m_vm->ReleasePort(*this); - Release(); + std::lock_guard lock{reservations->Mutex}; + LOG_HR_IF(E_UNEXPECTED, reservations->Ports.erase(m_port) != 1); } + + Release(); } void VmPortAllocation::Release() { - m_vm = nullptr; + m_reservations.reset(); m_port = 0; m_family = 0; m_protocol = 0; @@ -1270,33 +1274,29 @@ void WSLCVirtualMachine::OnSessionTerminated() std::shared_ptr WSLCVirtualMachine::TryAllocatePort(uint16_t Port, int Family, int Protocol) { - std::lock_guard lock{m_lock}; + std::lock_guard lock{m_reservations->Mutex}; WSL_LOG("AllocatePort", TraceLoggingValue(Port, "Port")); - auto [_, inserted] = m_allocatedPorts.insert(Port); - - if (inserted) - { - return std::make_shared(Port, Family, Protocol, *this); - } - else + if (!m_reservations->Ports.insert(Port).second) { return {}; } + + return std::make_shared(Port, Family, Protocol, m_reservations); } std::shared_ptr WSLCVirtualMachine::AllocatePort(int Family, int Protocol) { - std::lock_guard lock{m_lock}; + std::lock_guard lock{m_reservations->Mutex}; for (uint32_t i = CONTAINER_PORT_RANGE.first; i <= CONTAINER_PORT_RANGE.second; i++) { uint16_t port = static_cast(i); - if (!m_allocatedPorts.contains(port)) + if (!m_reservations->Ports.contains(port)) { - WI_VERIFY(m_allocatedPorts.insert(port).second); - return std::make_shared(port, Family, Protocol, *this); + WI_VERIFY(m_reservations->Ports.insert(port).second); + return std::make_shared(port, Family, Protocol, m_reservations); } } @@ -1304,15 +1304,6 @@ std::shared_ptr WSLCVirtualMachine::AllocatePort(int Family, i THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_NO_SYSTEM_RESOURCES), "Failed to allocate port"); } -void WSLCVirtualMachine::ReleasePort(VmPortAllocation& Port) -{ - std::lock_guard lock{m_lock}; - - WSL_LOG("ReleasePort", TraceLoggingValue(Port.Port(), "Port")); - - LOG_HR_IF(E_UNEXPECTED, m_allocatedPorts.erase(Port.Port()) != 1); -} - wil::unique_socket WSLCVirtualMachine::ConnectUnixSocket(const char* Path) { auto [_, __, channel] = Fork(WSLC_FORK::Thread); diff --git a/src/windows/wslcsession/WSLCVirtualMachine.h b/src/windows/wslcsession/WSLCVirtualMachine.h index df7102dc86..59435fe147 100644 --- a/src/windows/wslcsession/WSLCVirtualMachine.h +++ b/src/windows/wslcsession/WSLCVirtualMachine.h @@ -49,11 +49,20 @@ struct WSLCProcessFd class WSLCVirtualMachine; +// Owns the set of in-use VM-side port numbers for a single VM instance. Held by shared_ptr from the +// VM (sole owner) and referenced weakly by each VmPortAllocation, so a VM teardown drops every +// reservation and any surviving allocation self-neuters instead of dangling into a freed VM. +struct VmPortReservations +{ + std::mutex Mutex; + std::set Ports; +}; + struct VmPortAllocation { NON_COPYABLE(VmPortAllocation); - VmPortAllocation(uint16_t port, int Family, int Protocol, WSLCVirtualMachine& vm); + VmPortAllocation(uint16_t port, int Family, int Protocol, std::weak_ptr reservations); VmPortAllocation(VmPortAllocation&& Other); ~VmPortAllocation(); @@ -69,7 +78,7 @@ struct VmPortAllocation uint16_t m_port{}; int m_family{}; int m_protocol{}; - WSLCVirtualMachine* m_vm{}; + std::weak_ptr m_reservations; }; struct VMPortMapping @@ -146,7 +155,6 @@ class WSLCVirtualMachine std::shared_ptr TryAllocatePort(uint16_t Port, int Family, int Protocol); std::shared_ptr AllocatePort(int Family, int Protocol); - void ReleasePort(VmPortAllocation& Port); Microsoft::WRL::ComPtr CreateLinuxProcess( _In_ LPCSTR Executable, @@ -249,7 +257,7 @@ class WSLCVirtualMachine std::thread m_processExitThread; std::thread m_crashDumpThread; - std::set m_allocatedPorts; + std::shared_ptr m_reservations = std::make_shared(); GUID m_vmId{}; diff --git a/test/windows/PluginTests.cpp b/test/windows/PluginTests.cpp index 687dd5954b..bd1ff8fd90 100644 --- a/test/windows/PluginTests.cpp +++ b/test/windows/PluginTests.cpp @@ -606,7 +606,7 @@ class PluginTests return sessionManager; } - static wil::com_ptr CreateWslcSession(LPCWSTR Name, WSLCNetworkingMode NetworkingMode = WSLCNetworkingModeNone) + static wil::com_ptr CreateWslcSession(LPCWSTR Name, WSLCNetworkingMode NetworkingMode = WSLCNetworkingModeNone, LPCWSTR StoragePath = nullptr) { WSLCSessionSettings settings{}; settings.DisplayName = Name; @@ -614,6 +614,8 @@ class PluginTests settings.MemoryMb = 4096; settings.BootTimeoutMs = 30 * 1000; settings.NetworkingMode = NetworkingMode; + settings.StoragePath = StoragePath; + settings.MaximumStorageSizeMb = 1024 * 20; // 20GB, only used when StoragePath is set. auto manager = OpenWslcSessionManager(); wil::com_ptr session; @@ -787,8 +789,19 @@ class PluginTests { ConfigurePlugin(PluginTestType::WslcVmRestart); + // Idle termination only tears down storage-backed sessions (tmpfs state is unrecoverable), so + // this restart lifecycle test needs a dedicated persistent storage directory. + const auto storageDir = std::filesystem::current_path() / "test-storage-wslc-vm-restart"; + std::error_code storageError; + std::filesystem::remove_all(storageDir, storageError); + std::filesystem::create_directories(storageDir); + auto storageCleanup = wil::scope_exit([&]() { + std::error_code ec; + std::filesystem::remove_all(storageDir, ec); + }); + { - auto session = CreateWslcSession(L"plugin-wslc-vm-restart"); + auto session = CreateWslcSession(L"plugin-wslc-vm-restart", WSLCNetworkingModeNone, storageDir.c_str()); // First operation brings the VM up -> OnWslcVmStarted (which reentrantly runs a process). { @@ -816,9 +829,13 @@ class PluginTests WSLC VM started, session=* WSLC VM started reentrant WSLCCreateProcess: ok WSLC VM stopping, session=* + WSLC VM stopping reentrant WSLCCreateProcess: ok + WSLC VM stopping mount+unmount: ok WSLC VM started, session=* WSLC VM started reentrant WSLCCreateProcess: ok WSLC VM stopping, session=* + WSLC VM stopping reentrant WSLCCreateProcess: failed + WSLC VM stopping mount+unmount: skipped WSLC Session stopping, name=plugin-wslc-vm-restart, id=*)"; ValidateLogFile(ExpectedOutput); diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 801e0f3866..c4a8a2a889 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -11505,7 +11505,21 @@ class WSLCTests WSLC_TEST_METHOD(TriggerIdleTerminationRestartsVm) { constexpr auto c_sessionName = L"wslc-idle-trigger-test"; - auto session = CreateSession(GetDefaultSessionSettings(c_sessionName)); + + // Idle termination is only permitted for storage-backed sessions (tmpfs state is + // unrecoverable), so this lifecycle test uses a dedicated storage directory. + const auto storageDir = std::filesystem::current_path() / "test-storage-idle-restart"; + std::error_code storageError; + std::filesystem::remove_all(storageDir, storageError); + std::filesystem::create_directories(storageDir); + auto storageCleanup = wil::scope_exit([&]() { + std::error_code ec; + std::filesystem::remove_all(storageDir, ec); + }); + + auto settings = GetDefaultSessionSettings(c_sessionName); + settings.StoragePath = storageDir.c_str(); + auto session = CreateSession(settings); // The VM starts lazily, so a freshly created session is already idle. BOOL wasAlreadyIdle = FALSE; @@ -11535,6 +11549,26 @@ class WSLCTests VERIFY_IS_TRUE(IsVmRunning(c_sessionName)); } + // A tmpfs-backed session has no persistent storage, so its VM state cannot be recovered after a + // teardown. TriggerIdleTermination must refuse to tear such a session down (matching the + // automatic idle timer), leaving the VM running rather than destroying unrecoverable state. + WSLC_TEST_METHOD(TriggerIdleTerminationRefusedWithoutStorage) + { + constexpr auto c_sessionName = L"wslc-idle-tmpfs-test"; + auto session = CreateSession(GetDefaultSessionSettings(c_sessionName)); + + WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); + auto process = launcher.Launch(*session); + VERIFY_IS_TRUE(IsVmRunning(c_sessionName)); + + BOOL wasAlreadyIdle = TRUE; + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_FALSE(wasAlreadyIdle); + + // The VM must still be running: the tmpfs session was not torn down. + VERIFY_IS_TRUE(IsVmRunning(c_sessionName)); + } + // A running container pins the VM via its activity hold; TriggerIdleTermination ignores that // hold and forces teardown. The container's state must survive on persistent storage and be // recovered against a fresh docker context when the VM lazily restarts. @@ -11565,11 +11599,72 @@ class WSLCTests VERIFY_IS_FALSE(wasAlreadyIdle); VERIFY_IS_FALSE(IsVmRunning(c_testSessionName)); - // Re-opening lazily restarts the VM and recovers the container. It must be found (no longer - // running, since its init process died with the VM) and operable against the new context. + // The forced teardown must reconcile the surviving wrapper in place -- not merely leave + // docker's state to be rediscovered on the next open. The init process records a synthetic + // SIGKILL exit and the container drops to Exited while its client COM references stay valid. + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited); + VERIFY_ARE_EQUAL(container.GetInitProcess().Wait(), 128 + WSLCSignalSIGKILL); + + // Re-opening lazily restarts the VM and recovers the container. It must be found (Exited, since + // its init process died with the VM) and remain operable against the new context: restarting it + // brings it back to Running on the freshly booted VM. auto recovered = OpenContainer(m_defaultSession.get(), containerName); VERIFY_IS_TRUE(IsVmRunning(c_testSessionName)); - VERIFY_ARE_NOT_EQUAL(recovered.State(), WslcContainerStateRunning); + VERIFY_ARE_EQUAL(recovered.State(), WslcContainerStateExited); + + VERIFY_SUCCEEDED(recovered.Get().Start(WSLCContainerStartFlagsNone, nullptr, nullptr)); + VERIFY_ARE_EQUAL(recovered.State(), WslcContainerStateRunning); + } + + // A running --rm container that survives a forced idle teardown is dropped to Exited without the + // auto-remove delete (dockerd is gone by then). On the next VM restart it must be auto-removed + // rather than leaking as a permanent Exited container. + WSLC_TEST_METHOD(TriggerIdleTerminationRemovesExitedAutoRemoveContainer) + { + SKIP_TEST_SERVER(); + + const std::string containerName = "wslc-idle-rm-recovery"; + + auto cleanup = wil::scope_exit([&]() { + wil::com_ptr staleContainer; + if (SUCCEEDED(m_defaultSession->OpenContainer(containerName.c_str(), &staleContainer))) + { + LOG_IF_FAILED(staleContainer->Delete(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes)); + } + }); + + WSLCContainerLauncher launcher("debian:latest", containerName, {"/bin/sleep", "600"}); + launcher.SetContainerFlags(WSLCContainerFlagsRm); + auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone); + container.SetDeleteOnClose(false); + + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning); + VERIFY_IS_TRUE(IsVmRunning(c_testSessionName)); + + BOOL wasAlreadyIdle = TRUE; + VERIFY_SUCCEEDED(m_defaultSession->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_FALSE(wasAlreadyIdle); + VERIFY_IS_FALSE(IsVmRunning(c_testSessionName)); + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited); + + // Restart the VM via an unrelated operation, which recovers surviving containers; the exited + // --rm survivor must be auto-removed and absent from the container list. + WSLCProcessLauncher restartLauncher("/bin/true", {"/bin/true"}); + auto process = restartLauncher.Launch(*m_defaultSession); + process.GetExitEvent().wait(5000); + VERIFY_IS_TRUE(IsVmRunning(c_testSessionName)); + + auto [containers, ports] = ListContainers(m_defaultSession.get()); + bool found = false; + for (size_t i = 0; i < containers.size(); i++) + { + if (containerName == containers[i].Name) + { + found = true; + break; + } + } + VERIFY_IS_FALSE(found); } // Hammer the idle-teardown path concurrently with VM-level operations to surface deadlocks or @@ -11578,12 +11673,29 @@ class WSLCTests WSLC_TEST_METHOD(TriggerIdleTerminationConcurrentWithOperations) { constexpr auto c_sessionName = L"wslc-idle-hammer-test"; - auto session = CreateSession(GetDefaultSessionSettings(c_sessionName)); + + // Idle termination only tears down storage-backed sessions, so a dedicated storage directory + // is required for the teardown path to actually run under the concurrent hammering. + const auto storageDir = std::filesystem::current_path() / "test-storage-idle-hammer"; + std::error_code storageError; + std::filesystem::remove_all(storageDir, storageError); + std::filesystem::create_directories(storageDir); + auto storageCleanup = wil::scope_exit([&]() { + std::error_code ec; + std::filesystem::remove_all(storageDir, ec); + }); + + auto settings = GetDefaultSessionSettings(c_sessionName); + settings.StoragePath = storageDir.c_str(); + auto session = CreateSession(settings); std::atomic stop = false; std::atomic opFailures = 0; - std::thread worker([&]() { + // Run the hammering worker via a future so the join is bounded: a real teardown/operation + // deadlock would otherwise hang the test host indefinitely instead of failing. If the worker + // does not drain within the timeout after we signal stop, treat it as a deadlock and fail. + auto worker = std::async(std::launch::async, [&]() { while (!stop.load()) { try @@ -11606,7 +11718,8 @@ class WSLCTests } stop.store(true); - worker.join(); + VERIFY_ARE_EQUAL(worker.wait_for(std::chrono::seconds(60)), std::future_status::ready); + worker.get(); LogInfo("TriggerIdleTerminationConcurrentWithOperations tolerated %u operation failures", opFailures.load()); @@ -11683,18 +11796,21 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager2->CreateSession(&settings2, WSLCSessionFlagsNone, warningCallback.Get(), &session2)); wsl::windows::common::security::ConfigureForCOMImpersonation(session2.get()); - // The VM (and container recovery) starts lazily on the first operation. Trigger it so - // recovery runs. - VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session2).GetExitEvent().wait(30000)); + // The VM (and container recovery) starts lazily on the first operation. Trigger it via a + // callback-bearing operation (CreateNetwork) so recovery warnings reach the warning callback. + WSLCNetworkOptions triggerNetwork{}; + triggerNetwork.Name = "wslc-recovery-trigger"; + triggerNetwork.Driver = "bridge"; + VERIFY_SUCCEEDED(session2->CreateNetwork(&triggerNetwork, warningCallback.Get())); - // Recovery runs under the triggering operation's context, which carries no warning - // callback, so the warning is logged rather than delivered to the session callback. + // Recovery runs during the lazy VM start under this operation's context, so the failure + // warning is delivered to its warning callback. auto warnings = warningCallback->GetWarnings(); auto recoveryWarning = std::format( L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverContainer(wsl::shared::string::MultiByteToWide(containerId))); - VERIFY_IS_FALSE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); + VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); VERIFY_SUCCEEDED(session2->Terminate()); } @@ -11756,17 +11872,20 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, warningCallback.Get(), &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); - // The VM (and volume recovery) starts lazily on the first operation. Trigger it so - // recovery runs. - VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session).GetExitEvent().wait(30000)); + // The VM (and volume recovery) starts lazily on the first operation. Trigger it via a + // callback-bearing operation (CreateNetwork) so recovery warnings reach the warning callback. + WSLCNetworkOptions triggerNetwork{}; + triggerNetwork.Name = "wslc-recovery-trigger"; + triggerNetwork.Driver = "bridge"; + VERIFY_SUCCEEDED(session->CreateNetwork(&triggerNetwork, warningCallback.Get())); - // Recovery runs under the triggering operation's context, which carries no warning - // callback, so the warning is logged rather than delivered to the session callback. + // Recovery runs during the lazy VM start under this operation's context, so the failure + // warning is delivered to its warning callback. auto warnings = warningCallback->GetWarnings(); auto recoveryWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(L"wslc-test-warning-recovery")); - VERIFY_IS_FALSE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); + VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); // Clean up the orphaned volume from Docker's metadata. LOG_IF_FAILED(session->DeleteVolume("wslc-test-warning-recovery")); @@ -11826,16 +11945,19 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, warningCallback.Get(), &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); - // The VM (and guest volume recovery) starts lazily on the first operation. Trigger it so - // recovery runs. - VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session).GetExitEvent().wait(30000)); + // The VM (and guest volume recovery) starts lazily on the first operation. Trigger it via a + // callback-bearing operation (CreateNetwork) so recovery warnings reach the warning callback. + WSLCNetworkOptions triggerNetwork{}; + triggerNetwork.Name = "wslc-recovery-trigger"; + triggerNetwork.Driver = "bridge"; + VERIFY_SUCCEEDED(session->CreateNetwork(&triggerNetwork, warningCallback.Get())); - // Recovery runs under the triggering operation's context, which carries no warning - // callback, so the warning is logged rather than delivered to the session callback. + // Recovery runs during the lazy VM start under this operation's context, so the failure + // warning is delivered to its warning callback. auto warnings = warningCallback->GetWarnings(); auto recoveryWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(c_volumeName)); - VERIFY_IS_FALSE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); + VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); // Clean up the volume from Docker's metadata. ExpectCommandResult(session.get(), {"/usr/bin/docker", "volume", "rm", "-f", c_volumeName}, 0); diff --git a/test/windows/testplugin/Plugin.cpp b/test/windows/testplugin/Plugin.cpp index cc8b700a01..b9deb1128f 100644 --- a/test/windows/testplugin/Plugin.cpp +++ b/test/windows/testplugin/Plugin.cpp @@ -567,6 +567,7 @@ try CATCH_RETURN(); HRESULT OnWslcVmStopping(const WSLCSessionInformation* Session) +try { if (g_testType != PluginTestType::WslcVmRestart) { @@ -574,8 +575,33 @@ HRESULT OnWslcVmStopping(const WSLCSessionInformation* Session) } g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl; + + // Proves OnVmStopping doesn't deadlock a plugin that calls back in: on idle teardown the session + // is alive so these restart the VM and succeed; on permanent teardown they fail cleanly. + std::vector args = {"/bin/true", nullptr}; + WSLCProcessHandle process = nullptr; + const auto processHr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr); + g_logfile << "WSLC VM stopping reentrant WSLCCreateProcess: " << (SUCCEEDED(processHr) ? "ok" : "failed") << std::endl; + if (SUCCEEDED(processHr)) + { + g_api->WSLCReleaseProcess(process); + } + + constexpr auto* mountpoint = "/test-plugin/vm-stopping-mount"; + const auto mountHr = g_api->WSLCMountFolder(Session->SessionId, L"C:\\", mountpoint, TRUE); + if (SUCCEEDED(mountHr)) + { + const auto unmountHr = g_api->WSLCUnmountFolder(Session->SessionId, mountpoint); + g_logfile << "WSLC VM stopping mount+unmount: " << (SUCCEEDED(unmountHr) ? "ok" : "failed") << std::endl; + } + else + { + g_logfile << "WSLC VM stopping mount+unmount: skipped" << std::endl; + } + return S_OK; } +CATCH_RETURN(); EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPluginAPIV1* Api, WSLPluginHooksV1* Hooks) { From c9f087213093ad4fea96541e16275d533a3d9d33 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 13 Jul 2026 13:45:39 -0700 Subject: [PATCH 21/31] wslc: fix reentrant deadlock in VM start/stop notifications NotifyVmStarted/NotifyVmStopping invoked the plugin hook while holding m_notifyLock. During idle teardown a plugin may reentrantly restart the VM (WSLCCreateProcess), re-entering these notifications and self-deadlocking on the non-recursive mutex. Flip the pairing state under the lock, copy the hook, then invoke it after releasing the lock. Also clarify Shutdown's lock parameter (runtimeLock is the exclusive hold on m_lock) per review feedback. --- .../wslcsession/WSLCSessionRuntime.cpp | 61 ++++++++++++------- src/windows/wslcsession/WSLCSessionRuntime.h | 10 +-- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index ff5fe0fd94..5749d1b726 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -258,31 +258,50 @@ void WSLCSessionRuntime::EnsureVmRunning() void WSLCSessionRuntime::NotifyVmStarted() { - // m_notifyLock keeps the start/stop decision atomic against a concurrent NotifyVmStopping so hooks - // stay paired. Suppressed once terminating, else a racing Shutdown leaves OnVmStarted unpaired. - auto lock = std::lock_guard(m_notifyLock); - - if (m_terminating->load()) + // Flip the pairing state under m_notifyLock so it stays atomic against a concurrent NotifyVmStopping, + // then fire the hook after releasing the lock. The handler may reentrantly restart the VM (e.g. + // WSLCCreateProcess -> NotifyVmStarted/Stopping); invoking it under this non-recursive mutex would + // self-deadlock. Suppressed once terminating, else a racing Shutdown leaves OnVmStarted unpaired. + std::function hook; { - return; + auto lock = std::lock_guard(m_notifyLock); + + if (m_terminating->load()) + { + return; + } + + if (m_hooks.OnVmStarted) + { + m_vmStartNotified.store(true); + hook = m_hooks.OnVmStarted; + } } - if (m_hooks.OnVmStarted) + if (hook) { - m_vmStartNotified.store(true); - m_hooks.OnVmStarted(); + hook(); } } void WSLCSessionRuntime::NotifyVmStopping() { - // Paired once with a prior OnVmStarted (skipped if bring-up never completed). m_notifyLock makes - // the gate check and hook call atomic against NotifyVmStarted. - auto lock = std::lock_guard(m_notifyLock); + // Decide the pairing under m_notifyLock (paired once with a prior OnVmStarted, skipped if bring-up + // never completed), then fire the hook after releasing the lock. The handler may reentrantly restart + // the VM and call NotifyVmStarted; invoking it under this non-recursive mutex would self-deadlock. + std::function hook; + { + auto lock = std::lock_guard(m_notifyLock); + + if (m_vmStartNotified.exchange(false) && m_hooks.OnVmStopping) + { + hook = m_hooks.OnVmStopping; + } + } - if (m_vmStartNotified.exchange(false) && m_hooks.OnVmStopping) + if (hook) { - m_hooks.OnVmStopping(); + hook(); } } @@ -751,26 +770,26 @@ void WSLCSessionRuntime::OnVmExited() } void WSLCSessionRuntime::Shutdown( - wil::rwlock_release_exclusive_scope_exit& sessionLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails) + wil::rwlock_release_exclusive_scope_exit& runtimeLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails) { if (!m_initialized) { return; } - // Acquire an exclusive lock to ensure that no operation is running. - WI_VERIFY(sessionLock); + // runtimeLock is an exclusive hold on m_lock, guaranteeing no operation is running. + WI_VERIFY(runtimeLock); // Notify with m_lock dropped, then re-lock for the teardown. The handler may call back into the // session (e.g. WSLCCreateProcess) which takes a VM lease and this lock; firing under it would // deadlock. m_terminating is set, so any such reentrant lease fails at EnsureVmRunning's gate // rather than restarting the VM, and the reacquire can't block on it. - sessionLock.reset(); + runtimeLock.reset(); NotifyVmStopping(); - sessionLock = m_lock.lock_exclusive(); + runtimeLock = m_lock.lock_exclusive(); // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason; the - // reacquired exclusive lock satisfies TearDownVmLockHeld's precondition. + // reacquired exclusive hold on m_lock satisfies TearDownVmLockHeld's precondition. TearDownVmLockHeld(/* CaptureTerminationReason */ true); m_vmState.store(VmState::None); @@ -786,7 +805,7 @@ void WSLCSessionRuntime::Shutdown( // blocked acquiring the exclusive lock (about to evaluate idle teardown), it must be able to // obtain it, observe m_terminating, and return — otherwise Disarm()'s wait for in-flight // callbacks below would deadlock. - sessionLock.reset(); + runtimeLock.reset(); // Permanently disable idle teardown and drain any in-flight timer callback so it cannot // reference this session after it is destroyed. diff --git a/src/windows/wslcsession/WSLCSessionRuntime.h b/src/windows/wslcsession/WSLCSessionRuntime.h index e2495d8ae4..ac8ce33ffd 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.h +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -176,7 +176,9 @@ class WSLCSessionRuntime [[nodiscard]] bool TriggerIdleTerminationForTest(); - void Shutdown(wil::rwlock_release_exclusive_scope_exit& sessionLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails); + // runtimeLock is an exclusive hold on m_lock (this runtime's lock), which is dropped and reacquired + // internally so the OnVmStopping notification can fire without it and TearDownVmLockHeld runs with it. + void Shutdown(wil::rwlock_release_exclusive_scope_exit& runtimeLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails); private: bool IdleTerminationEnabled() const noexcept; @@ -228,9 +230,9 @@ class WSLCSessionRuntime // Set when OnVmStarted has fired for the current VM; gates the paired OnVmStopping so a VM that // never finished starting (or had no started notification) does not emit a spurious stopping. - // Accessed under m_notifyLock so the gate check and hook call are atomic across the (unlocked) - // start/stop notifications, preventing unpaired or reordered plugin notifications when a lazy VM - // start races a permanent shutdown. + // The gate flip happens under m_notifyLock so it stays atomic across the start/stop notifications; + // the hook itself is copied out and fired after the lock is released, because the handler may + // reentrantly restart the VM (re-entering these notifications) and would self-deadlock otherwise. std::mutex m_notifyLock; std::atomic m_vmStartNotified{false}; std::shared_ptr m_idleState{std::make_shared()}; From e46ec3321fff5a16ead56b4be69b6fd1782310fc Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 13 Jul 2026 14:00:13 -0700 Subject: [PATCH 22/31] test: exercise reentrant mount from the WSLC OnVmStarted plugin hook The VM-restart plugin test already validates a reentrant process launch and mount+unmount from OnVmStopping. Extend OnVmStarted to also mount+unmount so both lifecycle notifications validate reentrant mount management does not deadlock, and assert the new log lines. --- test/windows/PluginTests.cpp | 2 ++ test/windows/testplugin/Plugin.cpp | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/test/windows/PluginTests.cpp b/test/windows/PluginTests.cpp index bd1ff8fd90..a42a3ce111 100644 --- a/test/windows/PluginTests.cpp +++ b/test/windows/PluginTests.cpp @@ -828,11 +828,13 @@ class PluginTests WSLC Session created, name=plugin-wslc-vm-restart, id=*, pid=*, token=set, sid=set WSLC VM started, session=* WSLC VM started reentrant WSLCCreateProcess: ok + WSLC VM started mount+unmount: ok WSLC VM stopping, session=* WSLC VM stopping reentrant WSLCCreateProcess: ok WSLC VM stopping mount+unmount: ok WSLC VM started, session=* WSLC VM started reentrant WSLCCreateProcess: ok + WSLC VM started mount+unmount: ok WSLC VM stopping, session=* WSLC VM stopping reentrant WSLCCreateProcess: failed WSLC VM stopping mount+unmount: skipped diff --git a/test/windows/testplugin/Plugin.cpp b/test/windows/testplugin/Plugin.cpp index b9deb1128f..3d1201ee28 100644 --- a/test/windows/testplugin/Plugin.cpp +++ b/test/windows/testplugin/Plugin.cpp @@ -562,6 +562,20 @@ try g_api->WSLCReleaseProcess(process); } + // Also exercise a reentrant mount + unmount from the started hook; the session is alive here so + // both calls succeed, validating that mount management reentrant from OnVmStarted does not deadlock. + constexpr auto* mountpoint = "/test-plugin/vm-started-mount"; + const auto mountHr = g_api->WSLCMountFolder(Session->SessionId, L"C:\\", mountpoint, TRUE); + if (SUCCEEDED(mountHr)) + { + const auto unmountHr = g_api->WSLCUnmountFolder(Session->SessionId, mountpoint); + g_logfile << "WSLC VM started mount+unmount: " << (SUCCEEDED(unmountHr) ? "ok" : "failed") << std::endl; + } + else + { + g_logfile << "WSLC VM started mount+unmount: skipped" << std::endl; + } + return S_OK; } CATCH_RETURN(); From 4fda2451d6df6caab820f4177c5f5561630b0417 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 13 Jul 2026 14:12:31 -0700 Subject: [PATCH 23/31] wslc: hold a VmLease across container archive upload/download UploadArchive and DownloadArchive streamed data without holding a VmLease, so idle teardown could race and tear down the docker client mid-transfer. Acquire a VmLease and wrap in try/CATCH_RETURN to match Export/Logs and the other VM-dependent container operations. --- src/windows/wslcsession/WSLCContainer.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index a2d480b699..5209527ac1 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -2708,22 +2708,30 @@ try CATCH_RETURN(); HRESULT WSLCContainer::UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize) +try { WSLCExecutionContext context(&m_session); RETURN_HR_IF(E_POINTER, DestPath == nullptr); RETURN_HR_IF(E_INVALIDARG, DestPath[0] == '\0'); + + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::UploadArchive, TarHandle, DestPath, ContentSize); } +CATCH_RETURN(); HRESULT WSLCContainer::DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle) +try { WSLCExecutionContext context(&m_session); RETURN_HR_IF(E_POINTER, SrcPath == nullptr); RETURN_HR_IF(E_INVALIDARG, SrcPath[0] == '\0'); + + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::DownloadArchive, SrcPath, OutHandle); } +CATCH_RETURN(); HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) try From d15a66e312bbd2ed1258fcf69cd866c6edca88c9 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 13 Jul 2026 14:21:58 -0700 Subject: [PATCH 24/31] test: fail fast instead of hanging on idle-termination deadlock detection TriggerIdleTerminationConcurrentWithOperations bounded the worker join with a std::async future, but on timeout the failed VERIFY would unwind and block in the future destructor until the (deadlocked) task completed -- hanging the test host. Fail fast with a dump on timeout so a real deadlock fails cleanly. --- test/windows/WSLCTests.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 5010abe6d1..8ad1ec6ecc 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -11884,7 +11884,15 @@ class WSLCTests } stop.store(true); - VERIFY_ARE_EQUAL(worker.wait_for(std::chrono::seconds(60)), std::future_status::ready); + + // A real teardown/operation deadlock would leave the worker wedged forever. The std::future + // destructor blocks until the task completes, so letting a failed VERIFY unwind here would hang + // the test host -- exactly what this test guards against. Fail fast with a dump on timeout so we + // never unwind with an unfinished async task. + FAIL_FAST_IF_MSG( + worker.wait_for(std::chrono::seconds(60)) != std::future_status::ready, + "hammering worker did not drain after stop; likely teardown/operation deadlock"); + worker.get(); LogInfo("TriggerIdleTerminationConcurrentWithOperations tolerated %u operation failures", opFailures.load()); From 1aedaa90a2c7985813e6e1762bcd2c38dd577ef5 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 13 Jul 2026 14:39:42 -0700 Subject: [PATCH 25/31] wslc: fix idle-teardown activity race and stopping-only hook pairing OnIdleTimer dropped the runtime lock to fire OnVmStopping, then unconditionally tore the VM down after reacquiring. A lease that started in that window found the VM still Running, so it did not restart and could leave a long-lived activity token (e.g. a process keep-alive), and the teardown would then kill it. Re-check the activity count after reacquiring the lock and, if non-zero, abandon the stop and re-pair the notification with a fresh OnVmStarted. Also set m_vmStartNotified whenever the VM starts rather than only when an OnVmStarted hook is installed, so a hooks user that sets only OnVmStopping still receives paired stop notifications. --- .../wslcsession/WSLCSessionRuntime.cpp | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 5749d1b726..2dd8ab484f 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -262,6 +262,8 @@ void WSLCSessionRuntime::NotifyVmStarted() // then fire the hook after releasing the lock. The handler may reentrantly restart the VM (e.g. // WSLCCreateProcess -> NotifyVmStarted/Stopping); invoking it under this non-recursive mutex would // self-deadlock. Suppressed once terminating, else a racing Shutdown leaves OnVmStarted unpaired. + // m_vmStartNotified tracks the VM lifecycle, not whether OnVmStarted is installed, so a hooks user + // that sets only OnVmStopping still gets paired stop notifications. std::function hook; { auto lock = std::lock_guard(m_notifyLock); @@ -271,11 +273,8 @@ void WSLCSessionRuntime::NotifyVmStarted() return; } - if (m_hooks.OnVmStarted) - { - m_vmStartNotified.store(true); - hook = m_hooks.OnVmStarted; - } + m_vmStartNotified.store(true); + hook = m_hooks.OnVmStarted; } if (hook) @@ -575,14 +574,24 @@ try } // Fire OnVmStopping with m_lock dropped so a plugin handler may take a VM lease without - // deadlocking, then commit to the teardown unconditionally. A lease that races in during this - // window finds the VM stopped (VmLease re-checks under the shared lock) and restarts it, firing a - // fresh OnVmStarted, so hooks stay paired. StopVmLockHeld no-ops if a concurrent Terminate already - // tore the VM down, and TearDownVmLockHeld handles a VM that died in the window. + // deadlocking. A lease that races in during this window finds the VM still Running (StopVmLockHeld + // has not run yet), so it does not restart and may leave a long-lived activity token (e.g. a + // process keep-alive). Re-check the activity count after reacquiring the lock: if it is non-zero, + // abandon the teardown and re-pair the notification with a fresh OnVmStarted (with m_lock dropped, + // same reentrancy reason) rather than tearing down a VM that is in use again. StopVmLockHeld no-ops + // if a concurrent Terminate already tore the VM down, and TearDownVmLockHeld handles a VM that died + // in the window. lock.reset(); NotifyVmStopping(); lock = m_lock.lock_exclusive(); + if (m_idleState->ActivityCount() != 0) + { + lock.reset(); + NotifyVmStarted(); + return; + } + StopVmLockHeld(); } CATCH_LOG(); From d78e2d95880ff681b793013e8bcb6e35dafc8ae2 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 13 Jul 2026 14:50:29 -0700 Subject: [PATCH 26/31] wslc: skip guest volume unmount when the VM has already exited ReleaseRuntimeResources unconditionally called UnmountWindowsFolder via Vm() during container teardown. After an unexpected VM exit the guest is already gone, so each call only blocks on the RPC timeout and emits a spurious unmount-failed warning. A dead VM has already dropped every guest mount, so mark the mounts inactive locally instead. Add WSLCSessionRuntime::VmExited() (mirrors TearDownVmLockHeld's VM-dead check) so the container can detect this via the runtime it already references. --- src/windows/wslcsession/WSLCContainer.cpp | 16 +++++++++++++++- src/windows/wslcsession/WSLCSessionRuntime.cpp | 5 +++++ src/windows/wslcsession/WSLCSessionRuntime.h | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 5209527ac1..602a3be1d4 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -2435,7 +2435,21 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::ReleaseRuntimeRes // Release runtime resources (port relays, volume mounts) that were set up at Start(). UnmapPorts(); - UnmountVolumes(m_mountedVolumes, Vm()); + + // A VM that already exited (crash / external kill) has dropped every guest mount, so calling + // UnmountWindowsFolder would only block on the RPC timeout and emit spurious unmount-failed + // warnings. Mark the mounts inactive without touching the dead VM. + if (m_runtime.VmExited()) + { + for (auto& volume : m_mountedVolumes) + { + volume.Mounted = false; + } + } + else + { + UnmountVolumes(m_mountedVolumes, Vm()); + } } __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::ReleaseResources() diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 2dd8ab484f..b34c7b7bd9 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -153,6 +153,11 @@ wil::unique_event& WSLCSessionRuntime::VmExitedEvent() noexcept return m_vmExitedEvent; } +bool WSLCSessionRuntime::VmExited() const noexcept +{ + return m_vmExitedEvent && m_vmExitedEvent.is_signaled(); +} + wil::unique_event& WSLCSessionRuntime::DockerdReadyEvent() noexcept { return m_dockerdReadyEvent; diff --git a/src/windows/wslcsession/WSLCSessionRuntime.h b/src/windows/wslcsession/WSLCSessionRuntime.h index ac8ce33ffd..2c4e0e7acc 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.h +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -148,6 +148,7 @@ class WSLCSessionRuntime std::atomic& StateAtomic() noexcept; std::atomic& ExitDispositionAtomic() noexcept; wil::unique_event& VmExitedEvent() noexcept; + bool VmExited() const noexcept; wil::unique_event& DockerdReadyEvent() noexcept; std::optional& ContainerdProcess(); std::optional& DockerdProcess(); From 76e97ac67b2c219b85658cec087d51cfbfdc4ade Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 13 Jul 2026 17:13:27 -0700 Subject: [PATCH 27/31] wslc: track VM-exited with an atomic to avoid a lock-free event race VmExited() read m_vmExitedEvent without the runtime lock, but StartVmLockHeld and TearDownVmLockHeld reset/replace that event under the lock, so a container teardown running on another thread could observe the handle mid-reset and fail-fast. Mirror the state in a std::atomic: cleared when a VM instance starts and latched when the guest is observed dead at the top of teardown (before session-state cleanup, so ReleaseRuntimeResources sees it). VmExited() now returns the atomic. --- src/windows/wslcsession/WSLCSessionRuntime.cpp | 12 +++++++++++- src/windows/wslcsession/WSLCSessionRuntime.h | 5 +++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index b34c7b7bd9..6d8c9a727d 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -155,7 +155,7 @@ wil::unique_event& WSLCSessionRuntime::VmExitedEvent() noexcept bool WSLCSessionRuntime::VmExited() const noexcept { - return m_vmExitedEvent && m_vmExitedEvent.is_signaled(); + return m_vmExited.load(); } wil::unique_event& WSLCSessionRuntime::DockerdReadyEvent() noexcept @@ -365,6 +365,7 @@ void WSLCSessionRuntime::StartVmLockHeld() // Get an event from the service that is signaled when the VM exits. m_vmExitedEvent.reset(); THROW_IF_FAILED(vm->GetTerminationEvent(&m_vmExitedEvent)); + m_vmExited.store(false); if (m_hooks.BringUp) { @@ -424,6 +425,15 @@ void WSLCSessionRuntime::StopVmLockHeld() void WSLCSessionRuntime::TearDownVmLockHeld(bool CaptureTerminationReason) { + // Latch whether the guest is already dead before running session-state cleanup so container + // teardown (ReleaseRuntimeResources) can skip VM-dependent calls, e.g. volume unmounts, on a VM + // that has exited. A graceful stop reaches here with the VM still alive (is_signaled() false), so + // its mounts are unmounted through the live VM; StartVmLockHeld clears this for the next instance. + if (m_vmExitedEvent && m_vmExitedEvent.is_signaled()) + { + m_vmExited.store(true); + } + if (m_hooks.TearDownSessionState) { m_hooks.TearDownSessionState(CaptureTerminationReason); diff --git a/src/windows/wslcsession/WSLCSessionRuntime.h b/src/windows/wslcsession/WSLCSessionRuntime.h index 2c4e0e7acc..ba87a91600 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.h +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -218,6 +218,11 @@ class WSLCSessionRuntime std::optional m_containerdProcess; std::optional m_dockerdProcess; wil::unique_event m_vmExitedEvent; + // Lock-free mirror of "the current VM instance has exited": written under the runtime lock when a + // VM starts (false) or is observed dead during teardown (true), read without the lock by container + // teardown to skip VM-dependent cleanup. Avoids racing on m_vmExitedEvent, which is reset/replaced + // under the lock. + std::atomic m_vmExited{false}; wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; std::filesystem::path m_swapVhdPath; From 058ef3dd63fe87cfadcfa29c838e9144499b983a Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 13 Jul 2026 20:40:17 -0700 Subject: [PATCH 28/31] wslc: close two VM-lifecycle races found in multi-model review OnIdleTimer's abort path re-paired OnVmStarted without checking whether the VM died while m_lock was dropped for OnVmStopping. OnVmExited() declines that exit (the expected-stop claim is held) and the exit handle is one-shot, so the session was left with a dead VM marked Running until a later idle cycle. Tear the VM down in that case so a waiting lease restarts a fresh instance, matching the commit path. EnsureVmRunning's running fast path returned before the terminating gate, so a reentrant plugin lease during Shutdown's OnVmStopping (lock dropped) could run work against a VM being permanently torn down. Enforce the gate before the fast path, as its comment already documents. --- src/windows/wslcsession/WSLCSessionRuntime.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 6d8c9a727d..c6771a3e98 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -229,6 +229,11 @@ int WSLCSessionRuntime::StopProcess(ServiceRunningProcess& Process, DWORD Termin void WSLCSessionRuntime::EnsureVmRunning() { + // Reject leases once the session is terminating/terminated, including on the running fast path: + // Shutdown() drops the lock to fire OnVmStopping, and a reentrant lease that finds the VM still + // Running must fail here rather than run work against a VM being permanently torn down. + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent.is_signaled()); + if (m_vmState.load() == VmState::Running) { return; @@ -238,7 +243,7 @@ void WSLCSessionRuntime::EnsureVmRunning() { auto lock = m_lock.lock_exclusive(); - // Do not (re)start the VM once the session is terminating or has terminated. This also + // Re-check under the lock: terminating may have been set since the check above. This also // bounds VmLease's retry loop: a lease that races with Terminate() fails here instead of // restarting a VM that is being permanently torn down. THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent.is_signaled()); @@ -600,6 +605,16 @@ try NotifyVmStopping(); lock = m_lock.lock_exclusive(); + // If the VM crashed while the lock was dropped, OnVmExited() declined the teardown (we hold the + // expected-stop claim) and its exit handle is one-shot. Do not re-pair OnVmStarted onto a dead VM; + // tear it down so a waiting lease restarts a fresh instance. StopVmLockHeld skips guest-dependent + // calls on a dead VM. + if (m_vmExitedEvent && m_vmExitedEvent.is_signaled()) + { + StopVmLockHeld(); + return; + } + if (m_idleState->ActivityCount() != 0) { lock.reset(); From 911d4583678764172d0fb29882f9ab2fec1bb316 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 14 Jul 2026 07:57:10 -0700 Subject: [PATCH 29/31] wslc: narrow the WSLCSession/WSLCSessionRuntime seam The runtime extraction exposed mutable internals directly, letting WSLCSession drive them from outside. Replace those handouts with intent-revealing methods so the runtime owns its invariants: - Remove dead accessors StateAtomic() and VmExitedEvent() (no callers). - Replace ExitDispositionAtomic().load() call sites with the existing ExitDisposition() value getter and drop the atomic-ref accessor. - Replace the raw srwlock handout Lock() with TryLockExclusive(), used only by the Terminate/Shutdown handoff. - Replace DockerdReadyEvent() with Reset/Is/SignalDockerdReady(); replace the ContainerdProcess()/DockerdProcess() ref assignments with setters; replace SwapVhdPath() with SetSwapVhdPath(). No behavior change. Idle, VM restart, VM-kill, and plugin tests pass. --- src/windows/wslcsession/WSLCSession.cpp | 33 ++++++++--------- src/windows/wslcsession/WSLCSession.h | 6 +-- .../wslcsession/WSLCSessionRuntime.cpp | 37 ++++++++----------- src/windows/wslcsession/WSLCSessionRuntime.h | 15 ++++---- 4 files changed, 42 insertions(+), 49 deletions(-) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 6a13952277..f1b0e1d877 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -445,7 +445,7 @@ try // Reset the readiness event before (re)starting dockerd so a stale signal from a prior // VM instance is not observed. - m_runtime.DockerdReadyEvent().ResetEvent(); + m_runtime.ResetDockerdReady(); StartDockerd(); m_runtime.InitializeDockerRuntime(m_storageVhdPath.parent_path()); @@ -659,8 +659,8 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID { try { - auto& swapVhdPath = m_runtime.SwapVhdPath(); - swapVhdPath = storagePath / "swap.vhdx"; + std::filesystem::path swapVhdPath = storagePath / "swap.vhdx"; + m_runtime.SetSwapVhdPath(swapVhdPath); DeleteFileW(swapVhdPath.c_str()); // Remove stale swap from prior run wsl::core::filesystem::CreateVhd(swapVhdPath.c_str(), static_cast(Settings.SwapSizeMb) * _1MB, UserSid, false, false); @@ -703,7 +703,7 @@ CATCH_RETURN(); void WSLCSession::OnDockerdExited() { - if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDispositionAtomic().load() != WSLCSessionRuntime::VmExitDisposition::StopRequested) + if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDisposition() != WSLCSessionRuntime::VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedDockerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } @@ -711,7 +711,7 @@ void WSLCSession::OnDockerdExited() void WSLCSession::OnContainerdExited() { - if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDispositionAtomic().load() != WSLCSessionRuntime::VmExitDisposition::StopRequested) + if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDisposition() != WSLCSessionRuntime::VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedContainerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } @@ -732,11 +732,11 @@ try TraceLoggingValue(entry.c_str(), "Content"), TraceLoggingValue(m_displayName.c_str(), "Name")); - if (!m_runtime.DockerdReadyEvent().is_signaled()) + if (!m_runtime.IsDockerdReady()) { if (entry.find(c_dockerdReadyLogLine) != std::string::npos) { - m_runtime.DockerdReadyEvent().SetEvent(); + m_runtime.SignalDockerdReady(); } } } @@ -773,8 +773,7 @@ void WSLCSession::StartContainerd() args.emplace_back("debug"); } - m_runtime.ContainerdProcess() = - StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this)); + m_runtime.SetContainerdProcess(StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this))); WSL_LOG("ContainerdStarted"); } @@ -787,7 +786,7 @@ void WSLCSession::StartDockerd() args.emplace_back("--debug"); } - m_runtime.DockerdProcess() = StartProcess("/usr/bin/dockerd", args, "dockerd", std::bind(&WSLCSession::OnDockerdExited, this)); + m_runtime.SetDockerdProcess(StartProcess("/usr/bin/dockerd", args, "dockerd", std::bind(&WSLCSession::OnDockerdExited, this))); WSL_LOG("DockerdStarted"); } @@ -2982,10 +2981,10 @@ bool WSLCSession::WaitForEventOrSessionTerminating(HANDLE Event, std::chrono::mi HRESULT WSLCSession::Terminate() try { - // Ensure only one Terminate() runs. This must be checked before taking m_runtime.Lock() - // because OnVmExited() is called from the IORelay thread — if an external Terminate() - // holds m_runtime.Lock() and calls m_runtime.Relay()->Stop(), the relay thread must not re-enter - // Terminate() and deadlock on m_runtime.Lock(). + // Ensure only one Terminate() runs. This must be checked before taking the runtime's exclusive + // lock because OnVmExited() is called from the IORelay thread — if an external Terminate() + // holds that lock and calls m_runtime.Relay()->Stop(), the relay thread must not re-enter + // Terminate() and deadlock on it. if (m_terminating.exchange(true)) { return S_OK; @@ -3007,8 +3006,8 @@ try { std::lock_guard lock(m_userHandlesLock); - // m_sessionTerminatingEvent is always valid, so it can be signalled without holding m_runtime.Lock(). - // This allows a session to be unblocked if a stuck operation is holding m_runtime.Lock(). + // m_sessionTerminatingEvent is always valid, so it can be signalled without holding the runtime lock. + // This allows a session to be unblocked if a stuck operation is holding the runtime lock. // N.B. This must happen under m_userHandlesLock to synchronize with potentially running operations. if (!m_sessionTerminatingEvent.is_signaled()) { @@ -3028,7 +3027,7 @@ try CancelUserCOMCallbacks(); } - sessionLock = m_runtime.Lock().try_lock_exclusive(); + sessionLock = m_runtime.TryLockExclusive(); retrying = true; } diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index b099177d1b..ba66e6d58d 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -349,10 +349,10 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession std::wstring m_creatorProcessName; std::filesystem::path m_storageVhdPath; - // N.B. m_runtime.Lock() must be acquired before acquiring m_containersLock or m_networksLock. - // These locks protect m_containers without requiring an exclusive m_runtime.Lock(). + // N.B. The runtime lock must be acquired before acquiring m_containersLock or m_networksLock. + // These locks protect m_containers without requiring an exclusive hold on the runtime lock. // This allows independent operations to proceed while container bookkeeping remains synchronized. - // WSLCVolumes has its own internal srwlock and does not require m_runtime.Lock(). + // WSLCVolumes has its own internal srwlock and does not require the runtime lock. std::mutex m_containersLock; std::unordered_map> m_containers; std::mutex m_networksLock; diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index c6771a3e98..07ff96c03d 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -113,9 +113,9 @@ bool WSLCSessionRuntime::HasVolumes() const noexcept return m_volumes.has_value(); } -wil::srwlock& WSLCSessionRuntime::Lock() noexcept +wil::rwlock_release_exclusive_scope_exit WSLCSessionRuntime::TryLockExclusive() noexcept { - return m_lock; + return m_lock.try_lock_exclusive(); } IdleState& WSLCSessionRuntime::Idle() noexcept @@ -138,44 +138,39 @@ WSLCSessionRuntime::VmExitDisposition WSLCSessionRuntime::ExitDisposition() cons return m_vmExitDisposition.load(); } -std::atomic& WSLCSessionRuntime::StateAtomic() noexcept -{ - return m_vmState; -} - -std::atomic& WSLCSessionRuntime::ExitDispositionAtomic() noexcept +bool WSLCSessionRuntime::VmExited() const noexcept { - return m_vmExitDisposition; + return m_vmExited.load(); } -wil::unique_event& WSLCSessionRuntime::VmExitedEvent() noexcept +void WSLCSessionRuntime::ResetDockerdReady() noexcept { - return m_vmExitedEvent; + m_dockerdReadyEvent.ResetEvent(); } -bool WSLCSessionRuntime::VmExited() const noexcept +bool WSLCSessionRuntime::IsDockerdReady() const noexcept { - return m_vmExited.load(); + return m_dockerdReadyEvent.is_signaled(); } -wil::unique_event& WSLCSessionRuntime::DockerdReadyEvent() noexcept +void WSLCSessionRuntime::SignalDockerdReady() noexcept { - return m_dockerdReadyEvent; + m_dockerdReadyEvent.SetEvent(); } -std::optional& WSLCSessionRuntime::ContainerdProcess() +void WSLCSessionRuntime::SetContainerdProcess(ServiceRunningProcess&& process) { - return m_containerdProcess; + m_containerdProcess = std::move(process); } -std::optional& WSLCSessionRuntime::DockerdProcess() +void WSLCSessionRuntime::SetDockerdProcess(ServiceRunningProcess&& process) { - return m_dockerdProcess; + m_dockerdProcess = std::move(process); } -std::filesystem::path& WSLCSessionRuntime::SwapVhdPath() noexcept +void WSLCSessionRuntime::SetSwapVhdPath(std::filesystem::path path) { - return m_swapVhdPath; + m_swapVhdPath = std::move(path); } void WSLCSessionRuntime::SetStorageMounted(bool value) noexcept diff --git a/src/windows/wslcsession/WSLCSessionRuntime.h b/src/windows/wslcsession/WSLCSessionRuntime.h index ba87a91600..23754c3e2b 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.h +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -140,20 +140,19 @@ class WSLCSessionRuntime bool HasEvents() const noexcept; WSLCVolumes& Volumes(); bool HasVolumes() const noexcept; - wil::srwlock& Lock() noexcept; + [[nodiscard]] wil::rwlock_release_exclusive_scope_exit TryLockExclusive() noexcept; IdleState& Idle() noexcept; std::shared_ptr IdleStateShared() const noexcept; VmState State() const noexcept; VmExitDisposition ExitDisposition() const noexcept; - std::atomic& StateAtomic() noexcept; - std::atomic& ExitDispositionAtomic() noexcept; - wil::unique_event& VmExitedEvent() noexcept; bool VmExited() const noexcept; - wil::unique_event& DockerdReadyEvent() noexcept; - std::optional& ContainerdProcess(); - std::optional& DockerdProcess(); + void ResetDockerdReady() noexcept; + bool IsDockerdReady() const noexcept; + void SignalDockerdReady() noexcept; + void SetContainerdProcess(ServiceRunningProcess&& process); + void SetDockerdProcess(ServiceRunningProcess&& process); - std::filesystem::path& SwapVhdPath() noexcept; + void SetSwapVhdPath(std::filesystem::path path); void SetStorageMounted(bool value) noexcept; std::mutex& AllocatedPortsLock() noexcept; From 93b017da74a6efbf98303f9730f0e979ba3e1e85 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 14 Jul 2026 10:58:40 -0700 Subject: [PATCH 30/31] wslc: mark per-operation activity leases [[maybe_unused]] The BeginContainerOperation() lease is held only for its scope lifetime to keep the session VM alive during the operation. Annotate it so its intent is explicit and a future edit doesn't drop it to a discarded temporary. --- .../wslc/services/ContainerService.cpp | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 5ac70149a1..340e49d702 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -326,7 +326,7 @@ std::wstring ContainerService::FormatRelativeTime(ULONGLONG timestamp) int ContainerService::Attach(Session& session, const std::string& id) { - auto operation = session.BeginContainerOperation(); + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -488,7 +488,7 @@ CreateContainerResult ContainerService::Create(Session& session, const std::stri int ContainerService::Start(Session& session, const std::string& id, bool attach) { - auto operation = session.BeginContainerOperation(); + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); WSLCContainerStartFlags flags = attach ? WSLCContainerStartFlagsAttach : WSLCContainerStartFlagsNone; @@ -519,7 +519,7 @@ int ContainerService::Start(Session& session, const std::string& id, bool attach void ContainerService::Stop(Session& session, const std::string& id, StopContainerOptions options) { - auto operation = session.BeginContainerOperation(); + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); THROW_IF_FAILED_EXCEPT(container->Stop(options.Signal, options.Timeout), WSLC_E_CONTAINER_NOT_RUNNING); @@ -527,7 +527,7 @@ void ContainerService::Stop(Session& session, const std::string& id, StopContain void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal signal) { - auto operation = session.BeginContainerOperation(); + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); THROW_IF_FAILED(container->Kill(signal)); @@ -535,7 +535,7 @@ void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal void ContainerService::Delete(Session& session, const std::string& id, bool force) { - auto operation = session.BeginContainerOperation(); + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); THROW_IF_FAILED(container->Delete(force ? WSLCDeleteFlagsForce : WSLCDeleteFlagsNone)); @@ -590,7 +590,7 @@ std::vector ContainerService::List( int ContainerService::Exec(Session& session, const std::string& id, ContainerOptions options) { - auto operation = session.BeginContainerOperation(); + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -622,7 +622,7 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt InspectContainer ContainerService::Inspect(Session& session, const std::string& id) { - auto operation = session.BeginContainerOperation(); + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); wil::unique_cotaskmem_ansistring output; @@ -641,7 +641,7 @@ void ContainerService::Export(Session& session, const std::string& id, const std void ContainerService::Export(Session& session, const std::string& id, HANDLE outputHandle) { - auto operation = session.BeginContainerOperation(); + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -670,7 +670,7 @@ void ContainerService::CopyFromContainer(Session& session, const std::string& id void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail) { - auto operation = session.BeginContainerOperation(); + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -698,7 +698,7 @@ void ContainerService::Logs(Session& session, const std::string& id, bool follow wsl::windows::common::docker_schema::ContainerStats ContainerService::Stats(Session& session, const std::string& id) { - auto operation = session.BeginContainerOperation(); + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); wil::unique_cotaskmem_ansistring output; From 7c639d6d183833b7ef5e57d029bf79fc8e391014 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 16 Jul 2026 11:46:09 -0700 Subject: [PATCH 31/31] wslc: fix idle-timer crash race and notification ordering race - OnIdleTimer: release the expected-stop disposition claim before the second lock drop for NotifyVmStarted(), so a VM crash in that window is claimable by OnVmExited() as a normal spontaneous exit instead of being silently dropped while m_vmState stays Running. - NotifyVmStarted/NotifyVmStopping: hold m_notifyLock (now recursive) across both the pairing-state flip and the hook invocation, so a start and a stop racing on different threads cannot interleave and deliver a stale OnVmStarted after its OnVmStopping. --- .../wslcsession/WSLCSessionRuntime.cpp | 59 +++++++++---------- src/windows/wslcsession/WSLCSessionRuntime.h | 9 +-- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 07ff96c03d..0b1aa2204e 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -263,49 +263,39 @@ void WSLCSessionRuntime::EnsureVmRunning() void WSLCSessionRuntime::NotifyVmStarted() { - // Flip the pairing state under m_notifyLock so it stays atomic against a concurrent NotifyVmStopping, - // then fire the hook after releasing the lock. The handler may reentrantly restart the VM (e.g. - // WSLCCreateProcess -> NotifyVmStarted/Stopping); invoking it under this non-recursive mutex would - // self-deadlock. Suppressed once terminating, else a racing Shutdown leaves OnVmStarted unpaired. + // Hold m_notifyLock across both the pairing-state flip and the hook invocation, so a concurrent + // NotifyVmStopping on another thread cannot deliver its OnVmStopping in between and observe this + // OnVmStarted arrive late (which would otherwise leave the plugin with a trailing "started" for a + // VM that already stopped). m_notifyLock is recursive: the handler may reentrantly restart the VM + // (e.g. WSLCCreateProcess -> NotifyVmStarted/Stopping) on this same thread, which would self-deadlock + // under a plain mutex. Suppressed once terminating, else a racing Shutdown leaves OnVmStarted unpaired. // m_vmStartNotified tracks the VM lifecycle, not whether OnVmStarted is installed, so a hooks user // that sets only OnVmStopping still gets paired stop notifications. - std::function hook; - { - auto lock = std::lock_guard(m_notifyLock); - - if (m_terminating->load()) - { - return; - } + auto lock = std::lock_guard(m_notifyLock); - m_vmStartNotified.store(true); - hook = m_hooks.OnVmStarted; + if (m_terminating->load()) + { + return; } - if (hook) + m_vmStartNotified.store(true); + if (m_hooks.OnVmStarted) { - hook(); + m_hooks.OnVmStarted(); } } void WSLCSessionRuntime::NotifyVmStopping() { - // Decide the pairing under m_notifyLock (paired once with a prior OnVmStarted, skipped if bring-up - // never completed), then fire the hook after releasing the lock. The handler may reentrantly restart - // the VM and call NotifyVmStarted; invoking it under this non-recursive mutex would self-deadlock. - std::function hook; - { - auto lock = std::lock_guard(m_notifyLock); + // See NotifyVmStarted: hold m_notifyLock across both the pairing decision (paired once with a prior + // OnVmStarted, skipped if bring-up never completed) and the hook invocation, so the two notifications + // cannot interleave across threads. Recursive because the handler may reentrantly restart the VM and + // call NotifyVmStarted on this same thread. + auto lock = std::lock_guard(m_notifyLock); - if (m_vmStartNotified.exchange(false) && m_hooks.OnVmStopping) - { - hook = m_hooks.OnVmStopping; - } - } - - if (hook) + if (m_vmStartNotified.exchange(false) && m_hooks.OnVmStopping) { - hook(); + m_hooks.OnVmStopping(); } } @@ -612,6 +602,15 @@ try if (m_idleState->ActivityCount() != 0) { + // Release the expected-stop claim before dropping the lock again for NotifyVmStarted(): that + // call runs with m_lock dropped, and a VM crash in that window must be claimable by + // OnVmExited() as a normal spontaneous exit. Leaving the claim held here would make + // OnVmExited() silently decline the crash (disposition mismatch) while m_vmState stays + // Running, wedging the session until an explicit Terminate/Shutdown. dispositionCleanup's + // CAS on return is then a harmless no-op. + auto stopRequested = VmExitDisposition::StopRequested; + m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active); + lock.reset(); NotifyVmStarted(); return; diff --git a/src/windows/wslcsession/WSLCSessionRuntime.h b/src/windows/wslcsession/WSLCSessionRuntime.h index 23754c3e2b..aeba133296 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.h +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -235,10 +235,11 @@ class WSLCSessionRuntime // Set when OnVmStarted has fired for the current VM; gates the paired OnVmStopping so a VM that // never finished starting (or had no started notification) does not emit a spurious stopping. - // The gate flip happens under m_notifyLock so it stays atomic across the start/stop notifications; - // the hook itself is copied out and fired after the lock is released, because the handler may - // reentrantly restart the VM (re-entering these notifications) and would self-deadlock otherwise. - std::mutex m_notifyLock; + // The gate flip and the hook invocation both happen under m_notifyLock, so a start and a stop + // racing on different threads cannot interleave (which would otherwise let a stale OnVmStarted be + // delivered after the OnVmStopping it should have preceded). Recursive because the handler may + // reentrantly restart the VM, re-entering these notifications on the same thread. + std::recursive_mutex m_notifyLock; std::atomic m_vmStartNotified{false}; std::shared_ptr m_idleState{std::make_shared()};