Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b3cd765
Add container restart runtime support
beena352 Aug 26, 2026
1c4032d
Fix build
beena352 Aug 26, 2026
0ffa371
Format
beena352 Aug 26, 2026
b0cb18d
Validate the stop timeout for every restart path
beena352 Aug 28, 2026
800e738
Merge the restart guard into the transition wait loop
beena352 Aug 28, 2026
12015bd
Keep the container's ports and mounts across a restart
beena352 Aug 28, 2026
8c5fa6b
Perform the deferred delete when a restart fails
beena352 Aug 28, 2026
503d773
Annotate the phase boolean arguments
beena352 Aug 28, 2026
9237a2c
Add coverage for two overlapping restarts
beena352 Aug 28, 2026
6ad32fa
Describe the restart transaction's actual lifetime
beena352 Aug 28, 2026
25899fb
Check OnFailedRestart against starting containers
beena352 Aug 28, 2026
dfcb55b
Document WSLC_E_CONTAINER_MARKED_FOR_REMOVAL
beena352 Aug 31, 2026
e23e9ac
Let kill cut through a stuck restart's stop phase
beena352 Aug 31, 2026
28dc04c
Fix OnFailedRestart TOCTOU
beena352 Sep 1, 2026
f52229c
Make the restart race tests fail when they should
beena352 Sep 1, 2026
87adee1
Re-align the error code block in the docs
beena352 Sep 1, 2026
008b857
Add test that ports and mounts survive a restart
beena352 Sep 1, 2026
6ef0658
Report a force delete that lands during the restart's stop phase
beena352 Sep 1, 2026
86203a1
Cover kill and force delete during a restart
beena352 Sep 1, 2026
0a000fe
Add comment to Restart()
beena352 Sep 1, 2026
ee0bf3a
Explain Kill's restart wait-skip and test restart under an unavailabl…
beena352 Sep 1, 2026
341f3a4
Rename WSLC_E_CONTAINER_MARKED_FOR_REMOVAL to WSLC_E_CONTAINER_DELETED
beena352 Sep 1, 2026
e054e49
Merge branch 'master' of https://github.com/microsoft/WSL into users/…
beena352 Sep 1, 2026
70a3831
Fix comment
beena352 Sep 2, 2026
a1cd8c3
Merge branch 'master' of https://github.com/microsoft/WSL into users/…
beena352 Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/docs/api-reference/c/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */
#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */
#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */
#define WSLC_E_CONTAINER_DELETED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */
```

| Symbol | Hex Value |
Expand All @@ -39,5 +40,6 @@
| `WSLC_E_VOLUME_NOT_AVAILABLE` | `0x8004060E` |
| `WSLC_E_SESSION_NOT_FOUND` | `0x8004060F` |
| `WSLC_E_VM_NOT_RUNNING` | `0x80040610` |
| `WSLC_E_CONTAINER_DELETED` | `0x80040611` |

---
4 changes: 4 additions & 0 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2195,6 +2195,10 @@ Usage:
<value>Container '{}' not found.</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="MessageWslcContainerDeleted" xml:space="preserve">
<value>Container '{}' has been deleted.</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="MessageWslcNetworkNameRequired" xml:space="preserve">
<value>Network name cannot be empty.</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions src/windows/WslcSDK/winrt/wslcsdk.idl
Original file line number Diff line number Diff line change
Expand Up @@ -381,5 +381,6 @@ namespace Microsoft.WSL.Containers
ContainerDisabled = 0x8004060C,
RegistryBlockedByPolicy = 0x8004060D,
VolumeNotAvailable = 0x8004060E,
ContainerDeleted = 0x80040611,
};
}
1 change: 1 addition & 0 deletions src/windows/WslcSDK/wslcsdk.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ EXTERN_C_START
#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */
#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */
#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */
#define WSLC_E_CONTAINER_DELETED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */
Comment thread
beena352 marked this conversation as resolved.

// Session values
#define WSLC_SESSION_OPTIONS_SIZE 72
Expand Down
1 change: 1 addition & 0 deletions src/windows/common/wslutil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ static const std::map<HRESULT, LPCWSTR> g_commonErrors{
X(WSLC_E_NETWORK_NOT_FOUND),
X(WSLC_E_SESSION_NOT_FOUND),
X(WSLC_E_VM_NOT_RUNNING),
X(WSLC_E_CONTAINER_DELETED),
X(WSLC_E_WU_SEARCH_FAILED),
X_WIN32(RPC_S_SERVER_UNAVAILABLE),
X_WIN32(ERROR_ELEVATION_REQUIRED),
Expand Down
2 changes: 2 additions & 0 deletions src/windows/service/inc/wslc.idl
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ interface IWSLCContainer : IUnknown
HRESULT DisconnectFromNetwork([in] LPCSTR NetworkName);
HRESULT UploadArchive([in] WSLCHandle TarHandle, [in, string] LPCSTR DestPath, [in] ULONGLONG ContentSize);
HRESULT DownloadArchive([in, string] LPCSTR SrcPath, [in] WSLCHandle OutHandle);
HRESULT Restart([in] WSLCSignal Signal, [in] LONG TimeoutSeconds, [in, unique] IWarningCallback* WarningCallback);
}

typedef struct _WSLCDeletedImageInformation
Expand Down Expand Up @@ -885,3 +886,4 @@ cpp_quote("#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILIT
// N.B. WSLC_E_VM_NOT_RUNNING is part of the plugin API contract and is also defined in WslPluginApi.h.
// The two definitions must stay in sync.
cpp_quote("#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */")
cpp_quote("#define WSLC_E_CONTAINER_DELETED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */")
201 changes: 187 additions & 14 deletions src/windows/wslcsession/WSLCContainer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1020,11 +1020,20 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle*
}

void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions)
{
StartPhase(Flags, StartOptions, /* RestartPhase */ false);
}

void WSLCContainerImpl::StartPhase(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, bool RestartPhase)
{
std::shared_ptr<StateTransition> transition;
auto lifecycleLock = m_lifecycleLock.lock_shared();
auto lock = m_lock.lock_exclusive();
WaitForConflictingTransitionToComplete(lock, lifecycleLock);

WaitForConflictingTransitionToComplete(lock, lifecycleLock, std::nullopt, /* waitForRestart */ !RestartPhase);

// A Delete() that raced a restart may have already moved the container to the Deleted state.
THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_DELETED, Localization::MessageWslcContainerDeleted(m_id), m_state == WslcContainerStateDeleted);

Comment thread
beena352 marked this conversation as resolved.
THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_IS_RUNNING, Localization::MessageWslcContainerIsRunning(m_id), m_state == WslcContainerStateRunning);

Expand Down Expand Up @@ -1103,10 +1112,17 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
Localization::MessageWslcVolumeNotAvailable(wsl::shared::string::Join(unavailableVolumes, ',')),
!unavailableVolumes.empty());

auto volumeCleanup = MountVolumes(m_mountedVolumes, m_runtime.Vm());
// A restart keeps its ports and mounts across both phases, so re-acquiring them here would collide
// with the container's own reservations. Release them if the start does not land, since an exited
// container must not keep holding them.
auto resourceCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { ReleaseRuntimeResources(); });

auto portCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { UnmapPorts(); });
MapPorts();
if (!m_runtimeResourcesHeld)
{
MountVolumes(m_mountedVolumes, m_runtime.Vm()).release();
MapPorts();
m_runtimeResourcesHeld = true;
}

try
{
Expand Down Expand Up @@ -1154,8 +1170,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt

transition = StartTransition(TransitionKind::Start, ContainerEvent::Start);

portCleanup.release();
volumeCleanup.release();
resourceCleanup.release();
cleanup.release();

lock.reset();
Expand All @@ -1164,16 +1179,29 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
}

void WSLCContainerImpl::WaitForConflictingTransitionToComplete(
wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock, std::optional<TransitionKind> kind)
wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock, std::optional<TransitionKind> kind, bool waitForRestart)
{
while (m_transition && (!kind.has_value() || m_transition->Kind != kind.value()))
while (true)
{
// A restart spans two transitions, so waiting on the one in flight is not enough.
if (waitForRestart && m_restart)
{
auto restart = m_restart;
lock.reset();
lifecycleLock.reset();
WaitForCompletionEvent(restart->Completed.get());
}
else if (m_transition && (!kind.has_value() || m_transition->Kind != kind.value()))
{
auto transition = m_transition;
lock.reset();
lifecycleLock.reset();
WaitForTransitionCompletion(transition);
}
else
{
return;
}

lifecycleLock = m_lifecycleLock.lock_shared();
lock = m_lock.lock_exclusive();
Expand All @@ -1189,11 +1217,16 @@ __requires_exclusive_lock_held(m_lock) std::shared_ptr<WSLCContainerImpl::StateT
return transition;
}

void WSLCContainerImpl::WaitForTransitionCompletion(const std::shared_ptr<StateTransition>& transition) const
void WSLCContainerImpl::WaitForCompletionEvent(HANDLE Event) const
{
auto io = m_wslcSession.CreateIOContext();
io.AddHandle(std::make_unique<EventHandle>(transition->Completed.get()));
io.AddHandle(std::make_unique<EventHandle>(Event));
io.Run({});
}

void WSLCContainerImpl::WaitForTransitionCompletion(const std::shared_ptr<StateTransition>& transition) const
{
WaitForCompletionEvent(transition->Completed.get());

Comment thread
beena352 marked this conversation as resolved.
WI_ASSERT(transition->Completed.is_signaled());
}
Expand Down Expand Up @@ -1286,13 +1319,25 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCod
}

void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
{
StopPhase(Signal, TimeoutSeconds, Kill, /* RestartPhase */ false);
}

void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill, bool RestartPhase)
{
std::shared_ptr<StateTransition> transition;

{
auto lifecycleLock = m_lifecycleLock.lock_shared();
auto lock = m_lock.lock_exclusive();
WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop);

// Kill is the escape hatch when a restart's stop phase is stuck, so it must not wait on the very
// restart it is meant to unblock. Landing between the phases finds the container exited, which is
// turned away below like any other kill of a stopped container.
WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop, /* waitForRestart */ !RestartPhase && !Kill);

Comment thread
beena352 marked this conversation as resolved.
// A Delete() that raced a restart may have already moved the container to the Deleted state.
THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_DELETED, Localization::MessageWslcContainerDeleted(m_id), m_state == WslcContainerStateDeleted);

transition = m_transition;
WI_ASSERT(!transition || transition->Kind == TransitionKind::Stop);
Expand Down Expand Up @@ -1358,6 +1403,14 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
// HTTP 304 is returned when the container is already stopped.
if (Kill || e.StatusCode() != 304)
{
lock = m_lock.lock_exclusive();

// A force delete can win the locks released above, so the container may be gone rather than stuck.
THROW_HR_WITH_USER_ERROR_IF(
WSLC_E_CONTAINER_DELETED,
Localization::MessageWslcContainerDeleted(m_id),
m_state == WslcContainerStateDeleted || (m_transition && m_transition->ExpectedEvent == ContainerEvent::Destroy));

THROW_DOCKER_USER_ERROR_MSG(e, "Failed to %hs container '%hs'", Kill ? "kill" : "stop", m_id.c_str());
}
}
Expand Down Expand Up @@ -1396,6 +1449,98 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
}
}

void WSLCContainerImpl::Restart(WSLCSignal Signal, LONG TimeoutSeconds)
{
// The stop phase is skipped when the container is not running, so it cannot be the only validation.
ValidateStopTimeout(TimeoutSeconds, true);

bool wasRunning{};
auto restart = std::make_shared<RestartTransaction>();

{
auto lifecycleLock = m_lifecycleLock.lock_shared();
auto lock = m_lock.lock_exclusive();
WaitForConflictingTransitionToComplete(lock, lifecycleLock);

wasRunning = m_state == WslcContainerStateRunning;
Comment thread
beena352 marked this conversation as resolved.

// N.B. Stop() and Start() each take m_lock, so it cannot be held across both phases. m_restart
// stands them down until the start phase commits Running instead.
m_restart = restart;
}
Comment thread
beena352 marked this conversation as resolved.

// N.B. Nothing between here and restartCleanup below may throw — nothing clears m_restart until it
// is armed, and it must follow failureCleanup so it runs first (see the N.B. on OnFailedRestart).
auto failureCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { OnFailedRestart(); });

{
auto restartCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, restart]() {
{
auto lock = m_lock.lock_exclusive();

// CommitState() clears this once the start phase lands, so a later restart may already own it.
if (m_restart == restart)
{
m_restart.reset();
}
}

restart->Completed.SetEvent();
});

if (wasRunning)
{
StopPhase(Signal, TimeoutSeconds, /* Kill */ false, /* RestartPhase */ true);
}

StartPhase(WSLCContainerStartFlagsNone, nullptr, /* RestartPhase */ true);
}

failureCleanup.release();
}

// N.B. Runs after the restart transaction has been released, so the delete below is no longer
// suppressed by OnStopped().
void WSLCContainerImpl::OnFailedRestart() noexcept
Comment thread
beena352 marked this conversation as resolved.
{
try
{
std::shared_ptr<StateTransition> transition;

{
auto lifecycleLock = m_lifecycleLock.lock_shared();
auto lock = m_lock.lock_exclusive();

// The start phase waits for the start event after Docker has accepted the start, so it can throw
// on a container that is coming up. Leave that container alone; it still owns its resources.
if (m_transition || m_state == WslcContainerStateRunning)
{
return;
}

// The stop phase held these back for a start phase that never landed.
if (m_runtimeResourcesHeld)
{
ReleaseRuntimeResources();
}

if (WI_IsFlagClear(m_containerFlags, WSLCContainerFlagsRm) || m_state != WslcContainerStateExited)
{
return;
}

// N.B. A Start() released by restartCleanup is already queued on m_lock, so the removal is
// requested in the same scope as the checks above rather than through Delete(), which would
// let that Start() bring the container back up before the force delete lands.
RequestDeleteExclusiveLockHeld(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes);
transition = StartTransition(TransitionKind::Delete, ContainerEvent::Destroy);
}

AttachToTransition(transition);
}
CATCH_LOG()
}
Comment thread
beena352 marked this conversation as resolved.

__requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exitCode, std::optional<std::int64_t> stopTimestamp)
{
auto transition = m_transition;
Expand All @@ -1422,7 +1567,12 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exi
}

ReleaseProcesses();
ReleaseRuntimeResources();

// A restart's start phase relies on the container's ports and mounts still being held.
if (!m_restart)
{
ReleaseRuntimeResources();
}

// Ignore duplicate or late Stop events so they do not overwrite an already committed state.
if (m_state == WslcContainerStateRunning)
Expand All @@ -1439,7 +1589,7 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exi
}

// Stop with Rm must initiate Delete.
if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm))
if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm) && !m_restart)
Comment thread
beena352 marked this conversation as resolved.
{
try
{
Expand Down Expand Up @@ -1513,7 +1663,10 @@ void WSLCContainerImpl::Delete(WSLCDeleteFlags Flags)
std::shared_ptr<StateTransition> transition;
auto lifecycleLock = m_lifecycleLock.lock_shared();
auto lock = m_lock.lock_exclusive();
WaitForConflictingTransitionToComplete(lock, lifecycleLock);

// N.B. Unlike Start() and Stop(), this deliberately does not wait for an in-flight restart.
// A remove that lands between the two phases takes effect, and the restart's start phase fails.
WaitForConflictingTransitionToComplete(lock, lifecycleLock, std::nullopt, /* waitForRestart */ false);

Comment thread
beena352 marked this conversation as resolved.
RequestDeleteExclusiveLockHeld(Flags);
transition = StartTransition(TransitionKind::Delete, ContainerEvent::Destroy);
Expand Down Expand Up @@ -2864,6 +3017,8 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::ReleaseRuntimeRes
{
WSL_LOG("ReleaseRuntimeResources", TraceLoggingValue(m_id.c_str(), "ID"));

m_runtimeResourcesHeld = false;

// Release runtime resources (port relays, volume mounts) that were set up at Start().
UnmapPorts();

Expand Down Expand Up @@ -2932,6 +3087,12 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::CommitState(WSLCContainerSt
m_stateGeneration++;
m_stateChangedAt = stateChangedAt.value_or(static_cast<std::int64_t>(std::time(nullptr)));

if (State == WslcContainerStateRunning)
{
// The restart's start phase landed, so a later exit must auto-delete an --rm container again.
m_restart.reset();
}

// Keep the VM alive while this container is Running and release the hold once it leaves that
// state, even when no client holds the wrapper (e.g. a detached `run -d` container). Dropping
// the hold on the transition out of Running is what lets an otherwise-idle VM be torn down; a
Expand Down Expand Up @@ -3070,6 +3231,18 @@ try
}
CATCH_RETURN();

HRESULT WSLCContainer::Restart(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, IWarningCallback* WarningCallback)
try
{
WSLCExecutionContext context(&m_session, WarningCallback);

// Hold a VM lease across both phases: the container is not Running in between, so nothing else
// keeps the VM alive.
auto vmLease = m_session.Runtime().AcquireVmLease();
return CallImpl(&WSLCContainerImpl::Restart, Signal, TimeoutSeconds);
}
CATCH_RETURN();

HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, IWarningCallback* WarningCallback)
try
{
Expand Down
Loading