Skip to content

wslc: idle-terminate inactive per-user session VMs - #41077

Open
benhillis wants to merge 46 commits into
masterfrom
user/benhill/wslc-runtime-encapsulation
Open

wslc: idle-terminate inactive per-user session VMs#41077
benhillis wants to merge 46 commits into
masterfrom
user/benhill/wslc-runtime-encapsulation

Conversation

@benhillis

@benhillis benhillis commented Jul 14, 2026

Copy link
Copy Markdown
Member

Idle-terminates per-user WSLC session VMs after a period of inactivity, so an idle container session releases its utility VM instead of holding memory indefinitely. Includes the supporting refactor that makes the VM lifecycle safe to reason about.

What this does

  • Adds an idle timer to each per-user WSLC session VM. When no container operation has run for the configured grace period, the VM is torn down; the next operation transparently restarts it.
  • Container operations take an activity lease (Session::BeginContainerOperation) so the VM cannot be idle-terminated mid-operation.
  • Adds OnVmStarted / OnVmStopping plugin hooks, paired correctly across start/stop and VM-restart.

VM lifecycle hardening

  • Extracts VM state management into a dedicated WSLCSessionRuntime state machine (start / stop / teardown / spontaneous-exit), replacing ad-hoc state spread across WSLCSession.
  • Fixes several concurrency issues found during review:
    • reentrant deadlock in start/stop notifications,
    • idle-teardown vs. activity race,
    • stopping-only hook pairing,
    • two VM-lifecycle races found via multi-model review (dead-VM re-pair in the idle-timer abort path; terminating-gate ordering in EnsureVmRunning).
  • Skips guest volume unmount when the VM has already exited.

Encapsulation cleanup

  • Narrows the WSLCSession / WSLCSessionRuntime seam: replaces ~9 accessors that handed out mutable internals with intent-revealing methods (TryLockExclusive, SetContainerdProcess, Set/Reset/Signal/IsDockerdReady, SetSwapVhdPath), and drops dead accessors.
  • Marks the per-operation activity leases [[maybe_unused]] to document that they are held for their scope lifetime.

Testing

Full build (x64 + arm64) + package, then locally: idle-termination (5/5), WslcVmRestart, VmKillTerminatesSession, StuckVmTermination, reentrant-mount plugin hook, and PluginTests::WslcSuccess all pass.

Ben Hillis and others added 30 commits July 7, 2026 08:38
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>
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>
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>
- 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>
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>
- 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>
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>
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>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
…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>
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>
…dle-terminate-vm

# Conflicts:
#	src/windows/wslcsession/WSLCContainer.cpp
#	src/windows/wslcsession/WSLCSession.h
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- 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
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.
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.
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.
…tion

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.
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.
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.
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<bool>: 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 38 out of 38 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/windows/wslcsession/WSLCSessionRuntime.cpp:443

  • In TearDownVmLockHeld, the bool parameter is named CaptureTerminationReason but it is also forwarded as the permanent flag to RuntimeHooks::TearDownSessionState. That dual meaning makes the teardown semantics hard to read and easy to misuse later. Consider introducing a clearly-named local (or renaming the parameter) when invoking the hook so it’s obvious the hook is receiving a “permanent shutdown” signal, not a “capture termination reason” signal.
    if (m_hooks.TearDownSessionState)
    {
        m_hooks.TearDownSessionState(CaptureTerminationReason);
    }

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 38 out of 38 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/windows/wslcsession/WSLCContainer.cpp:2596

  • vmLease is only used for its RAII side effect (keeping the VM alive), but the variable itself is otherwise unused. This commonly triggers MSVC warning C4189 (unused local variable) and can break builds when warnings are treated as errors. Consider marking this (and the other similar vmLease locals in this file) as [[maybe_unused]] to make the intent explicit and keep builds warning-clean.
    auto vmLease = m_session.Runtime().AcquireVmLease();
    return CallImpl(&WSLCContainerImpl::Attach, DetachKeys, Stdin, Stdout, Stderr);

src/windows/wslcsession/WSLCIdleState.h:22

  • WSLCIdleState.h uses WIL result/diagnostics macros (THROW_LAST_ERROR_IF, FAIL_FAST_IF, CATCH_LOG), but the header only includes <wil/resource.h>. This makes the header non-self-contained and can break compilation if it’s included from a TU that doesn’t pull in <wil/result.h> first (e.g. outside the precompiled header). Include <wil/result.h> directly here to ensure the required macros are always available.
#include <atomic>
#include <chrono>
#include <functional>
#include <memory>
#include <utility>
#include <wil/resource.h>

Copilot AI review requested due to automatic review settings July 29, 2026 16:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 38 out of 38 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/windows/wslcsession/WSLCSessionRuntime.h:169

  • TearDownVmLockHeld’s parameter name CaptureTerminationReason doesn’t match its documented/actual semantics: it’s passed through as the permanent argument to RuntimeHooks::TearDownSessionState. This is easy to misread as “only capture reason” and accidentally call it with the wrong value, which could clear/retain container wrappers incorrectly on future refactors. Consider renaming this parameter (and corresponding uses) to reflect permanence (e.g. permanentShutdown).
    _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();

OnWslcVmStopping has to fire while the VM is still alive -- its purpose is to warn
a plugin that the VM it is using is going away, while it can still act on it.
Delivering it means dropping the runtime lock, because a plugin handler may call
back into the session, and that opens a window in which new work can arrive.
Previously that work could keep the VM alive, so an announced stop could silently
not happen and the plugin was left holding a warning that never came true.

Decide the stop under the exclusive lock and publish it before announcing it, so
the notification states a fact rather than a forecast:

- The last silent back-out is the ActivityCount() check immediately before
  BeginVmStopLockHeld(). From there the teardown runs unconditionally.
- A lease arriving during the window releases its shared lock, waits on
  m_vmStopCompleteEvent and retries, so it is served by a fresh VM rather than
  keeping a promised-dead one alive. Releasing the lock before waiting is
  required: the teardown must be able to reacquire it exclusively.
- EndVmStop() is a scope_exit declared after the lock guard, so reverse-order
  destruction runs it while the lock is still held and a waiter can only wake
  into a fully torn-down, published state.
- Calls the stopping handler makes itself must not wait for the teardown they are
  holding up. wslservice marks the thread running the callout and tags those
  calls with FromVmLifecycleCallback, so the session serves them from the VM that
  is going away. A plugin calling from a thread of its own is untagged and simply
  waits for the next VM.
- NotifyVmStopping retires the notified generation as it announces the stop, so a
  Terminate() racing an idle teardown cannot deliver OnWslcVmStopping twice.

Work a plugin leaves running when its handler returns now dies with the VM, which
is exactly what the handler was just told would happen. Documented in
WslPluginApi.h along with the rule that the handler must not block waiting on
another thread calling into the session.

Also publish VM presence in an atomic, so ~WSLCContainerImpl -- which deliberately
holds no VM lease -- can query it without racing the teardown's reset().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33
Copilot AI review requested due to automatic review settings July 30, 2026 16:14
@benhillis
benhillis force-pushed the user/benhill/wslc-runtime-encapsulation branch from f2dbad2 to c79438a Compare July 30, 2026 16:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

test/windows/testplugin/Plugin.cpp:659

  • WSLCProcessGetExitEvent returns a duplicated handle (caller-owned). The current code stores it in g_leakedProcessExitEvent and waits on it, but never closes it, leaking a kernel handle each run of this test path.
    test/windows/testplugin/Plugin.cpp:654
  • This comment block is duplicated verbatim (the same 5 lines appear twice), which makes the intent harder to read and looks accidental. Please remove the duplicate copy.

This issue also appears on line 655 of the same file.
test/windows/testplugin/Plugin.cpp:644

  • The stop-window worker thread writes to g_logfile (e.g. "WSLC stop-window caller" / "WSLC leaked process died") while the callback thread is also writing to the same std::ofstream. Concurrent access to the same iostream without synchronization is a data race (UB) and can lead to flaky/corrupted logs. Prefer having the worker thread only compute/store results and emit the log lines after join (e.g., in OnWslcSessionStopping), or add a mutex that is consistently taken by all g_logfile writes.

NotifyVmStopping retires the notified generation with a compare-exchange against
the generation being stopped. Both m_vmGeneration and m_notifiedGeneration start
at 0, so a session torn down without ever starting a VM -- bring-up is lazy, a VM
is only created on the first operation that needs one -- reached Shutdown with
generation 0 and the CAS matched, delivering an OnWslcVmStopping that no
OnWslcVmStarted ever paired with. Treat 0 as the 'nothing announced' sentinel it
already is everywhere else. Covered by a new WslcVmNeverStarted plugin test.

Also from review:

- Don't attach a process keep-alive activity token for a call made from the
  OnVmStopping handler. That VM is committed to stopping, so the token cannot do
  its job, but it would keep counting activity for as long as the plugin held the
  proxy and block idle termination of every later VM in the session.
- Deliver the notification under CATCH_LOG at all three sites. The stop is already
  published and its generation retired by then, so a throwing handler must not be
  able to skip the teardown and leave waiters to be served by the VM they were
  promised would die.
- Correct three comments: TearDownVmLockHeld's claim that the notify paths never
  run mid-teardown, WslPluginApi.h's claim that blocked callers are always served
  by a next VM (there is none when the session itself is terminating), and the
  IDL's 'must be set only by the service' for FromVmLifecycleCallback, which the
  session cannot enforce across the process boundary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33
Copilot AI review requested due to automatic review settings July 30, 2026 17:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

test/windows/wslc/e2e/WSLCE2EWarningTests.cpp:114

  • This test now silently passes if RunWslc fails to capture stderr (Stderr == nullopt). Since the assertion here is specifically that the recovery warning is not emitted, the test should still require that stderr capture is present and then assert the warning substring is absent; otherwise a broken capture path could mask a regression.
    test/windows/testplugin/Plugin.cpp:667
  • The stop-window test caches a raw Win32 HANDLE (g_leakedProcessExitEvent) but never closes it. Even in test code, leaking handles can accumulate across test runs and cause unrelated failures. Consider closing the handle once the waiting thread is done with it (and clearing the atomic) to keep resource usage bounded.

…sert

IWSLCProcess::GetExitEvent marshals its handle as [out, system_handle(sh_event)],
so the caller owns the duplicate. The stop-window thread waited on it and dropped
it -- a leak, and the test plugin is reference code plugin authors copy.

WSLCE2E_Warning_ContainerRecoveryNotPrintedOnStderr guarded its only assertion on
Stderr.has_value(), so the test would pass vacuously if stderr capture broke --
exactly when it would stop proving anything. No stderr at all already satisfies
'the warning was not printed', so assert against an empty string instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33
Copilot AI review requested due to automatic review settings July 30, 2026 19:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

test/windows/testplugin/Plugin.cpp:523

  • g_leakedProcess / g_leakedProcessExitEvent are intentionally retained for the stop-committed scenario, but they are never cleaned up afterwards, which leaks the service-side process wrapper (and can also leak the duplicated exit-event handle if the stop-window thread doesn’t reach the exchange/CloseHandle path). Consider releasing the wrapper and closing any remaining exit event once the stop-window thread is drained at session teardown.
    test/windows/wslc/e2e/WSLCE2EWarningTests.cpp:116
  • The new stderr check can still pass vacuously if stderr capture breaks: value_or(std::wstring{}) yields an empty string when result.Stderr is nullopt, and an empty string will never contain the warning. If the intent is to ensure capture is working while asserting the warning is not printed, require result.Stderr.has_value() before searching it.

…e leaked exit event

RunWslc always populates Stderr, so guarding the search on has_value() -- or
searching value_or({}) -- lets the test pass without proving anything if capture
ever breaks. Assert has_value() first, matching the other wslc e2e tests.

Also close the duplicated exit event at session teardown when the stop-window
thread did not get far enough to claim it. The leaked process wrapper is left
alone deliberately: it holds a COM proxy marshalled to the OnWslcVmStopping
callback's thread, so releasing it from the session-stopping thread risks the same
RPC_E_WRONG_THREAD hazard that forced the exit event to be cached as a plain
handle, and it is one wrapper for the lifetime of a test process.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33
Copilot AI review requested due to automatic review settings July 30, 2026 22:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

src/windows/wslcsession/WSLCVirtualMachine.cpp:1285

  • Same exception-safety issue as TryAllocatePort(): if std::make_shared<VmPortAllocation> throws after inserting port into m_reservations->Ports, the port remains reserved forever. Roll back the reservation in the failure path to keep the reservation table consistent.
    test/windows/testplugin/Plugin.cpp:666
  • g_logfile is written from both the plugin callback thread and g_stopWindowCaller's worker thread (e.g. this line runs in the worker), but there is no synchronization around the shared std::ofstream. Concurrent operator<< on the same stream is a data race and can corrupt the log or make the expected-output matching flaky.

Consider adding a shared synchronization mechanism (e.g. std::mutex + helper that serializes all g_logfile writes, or std::osyncstream for line-at-a-time atomic writes) and using it for all log output that can come from multiple threads (this worker thread, the callback thread, and the existing async thread in OnVmStarted).
test/windows/testplugin/Plugin.cpp:587

  • This comment is misleading: the WslcVmNeverStarted test expects OnWslcVmStarted to never fire, but this branch logs a "VM started" line if it does fire (which is how the test would detect the regression). Consider rewording to reflect that this is an assertion/diagnostic path, not expected behavior.
    src/windows/wslcsession/WSLCVirtualMachine.cpp:1271
  • If std::make_shared<VmPortAllocation> throws after the port is inserted into m_reservations->Ports, the reservation is leaked (the port stays marked in-use), which can cause spurious ERROR_ALREADY_EXISTS / allocation failures later in the VM lifetime. The reservation update should be exception-safe by rolling back the set insert on failure.

This issue also appears on line 1281 of the same file.

// it outside that context is self-defeating rather than dangerous: it opts the caller into a VM
// that is about to be torn down, so its work is destroyed and only that caller's session is
// affected.
HRESULT CreateRootNamespaceProcess([in, ref] LPCSTR Executable, [in, ref] const WSLCProcessOptions* Options, [in] ULONG TtyRows, [in] ULONG TtyColumns, [in] BOOL FromVmLifecycleCallback, [out] IWSLCProcess** Process, [out] int* Errno);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd rename to AcquireVmLease or similar to describe what this flag does instead of when it's set by the caller


auto session = ResolveWslcSession(Session);
auto result = session->MountWindowsFolder(WindowsPath, Mountpoint, ReadOnly);
auto result = session->MountWindowsFolder(WindowsPath, Mountpoint, ReadOnly, FromVmLifecycleCallback(Session));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should reject the caller if a plugin tries to mount a folder / execute a process while the VM is not running (regardless of whether the VM is terminating or not).

The easiest way I could think of doing that would be to have flags in that call to determine if the caller should obtain a VM lease or not, and in the case of plugins, that flag should always be set to false

m_comWrapper->Initialize(weak_from_this());
}

WSLCVirtualMachine& WSLCContainerImpl::Vm() const

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think we can probably remove these methods, they don't feel like they add much value

…n failure

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33
Copilot AI review requested due to automatic review settings July 30, 2026 22:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

test/windows/testplugin/Plugin.cpp:696

  • Avoid calling CloseHandle directly here as well; wrap the duplicated event in wil::unique_handle and wait on the wrapper. This keeps the close reliably paired even if additional early-returns/throws are introduced later in the thread body.
    test/windows/testplugin/Plugin.cpp:539
  • Avoid calling CloseHandle directly; use a WIL RAII wrapper so the handle is always closed even if this function later changes to return/throw early. This aligns with the rest of the codebase’s handle-management pattern and keeps teardown logic exception-safe.

This issue also appears on line 692 of the same file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants