master
cpp 976 lines 34.6 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCSessionRuntime.cpp
8
9 Abstract:
10
11 Contains the implementation for WSLCSessionRuntime.
12
13 --*/
14
15 #include "precomp.h"
16 #include "WSLCSessionRuntime.h"
17 #include "WSLCSession.h"
18 #include "WSLCSessionDefaults.h"
19
20 using wsl::windows::service::wslc::WSLCSessionRuntime;
21
22 namespace {
23
24 constexpr auto c_dockerdReadyLogLine = "API listen on /var/run/docker.sock";
25
26 // How long a VM lease waits for an announced stop to complete before logging that it is still
27 // blocked. Purely diagnostic: the wait itself is unbounded (see VmLease).
28 constexpr DWORD c_vmStopWaitLogIntervalMs = 30 * 1000;
29
30 } // namespace
31
32 namespace wsl::windows::service::wslc {
33
34 WSLCSessionRuntime::WSLCSessionRuntime(WSLCSession& Session) noexcept : m_session(&Session)
35 {
36 }
37
38 void WSLCSessionRuntime::Initialize(
39 DWORD vmFactoryGitCookie,
40 wil::com_ptr<IGlobalInterfaceTable> git,
41 const WSLCSessionInitSettings* settings,
42 std::chrono::milliseconds idleGrace,
43 SessionContext sessionContext,
44 RuntimeHooks hooks)
45 {
46 m_vmFactoryGitCookie = vmFactoryGitCookie;
47 m_git = std::move(git);
48 m_settings = settings;
49 m_id = sessionContext.Id;
50 m_displayName = std::move(sessionContext.DisplayName);
51 m_terminating = sessionContext.Terminating;
52 m_sessionTerminatingEvent = sessionContext.SessionTerminatingEvent;
53 m_sessionTerminatedEvent = sessionContext.SessionTerminatedEvent;
54 m_hooks = std::move(hooks);
55
56 m_idleState->Initialize(idleGrace, [this]() { OnIdleTimer(); });
57
58 // Session-scoped: subscriptions must survive VM restarts. Rebound per-VM in InitializeDockerRuntime.
59 m_eventTracker.emplace(*m_session);
60
61 m_initialized = true;
62 }
63
64 WSLCVirtualMachine& WSLCSessionRuntime::Vm()
65 {
66 WI_ASSERT(m_virtualMachine.has_value());
67 return m_virtualMachine.value();
68 }
69
70 bool WSLCSessionRuntime::HasVm() const noexcept
71 {
72 return m_hasVm.load();
73 }
74
75 IORelay* WSLCSessionRuntime::Relay()
76 {
77 return m_ioRelay ? &m_ioRelay.value() : nullptr;
78 }
79
80 bool WSLCSessionRuntime::HasRelay() const noexcept
81 {
82 return m_ioRelay.has_value();
83 }
84
85 DockerHTTPClient& WSLCSessionRuntime::Docker()
86 {
87 WI_ASSERT(m_dockerClient.has_value());
88 return m_dockerClient.value();
89 }
90
91 bool WSLCSessionRuntime::HasDocker() const noexcept
92 {
93 return m_dockerClient.has_value();
94 }
95
96 DockerEventTracker& WSLCSessionRuntime::Events()
97 {
98 WI_ASSERT(m_eventTracker.has_value());
99 return m_eventTracker.value();
100 }
101
102 bool WSLCSessionRuntime::HasEvents() const noexcept
103 {
104 return m_eventTracker.has_value();
105 }
106
107 WSLCVolumes& WSLCSessionRuntime::Volumes()
108 {
109 WI_ASSERT(m_volumes.has_value());
110 return m_volumes.value();
111 }
112
113 bool WSLCSessionRuntime::HasVolumes() const noexcept
114 {
115 return m_volumes.has_value();
116 }
117
118 wil::rwlock_release_exclusive_scope_exit WSLCSessionRuntime::TryLockExclusive() noexcept
119 {
120 return m_lock.try_lock_exclusive();
121 }
122
123 IdleState& WSLCSessionRuntime::Idle() noexcept
124 {
125 return *m_idleState;
126 }
127
128 std::shared_ptr<IdleState> WSLCSessionRuntime::IdleStateShared() const noexcept
129 {
130 return m_idleState;
131 }
132
133 WSLCSessionRuntime::VmState WSLCSessionRuntime::State() const noexcept
134 {
135 return m_vmState.load();
136 }
137
138 WSLCSessionRuntime::VmExitDisposition WSLCSessionRuntime::ExitDisposition() const noexcept
139 {
140 return m_vmExitDisposition.load();
141 }
142
143 bool WSLCSessionRuntime::VmExited() const noexcept
144 {
145 return m_vmExited.load();
146 }
147
148 void WSLCSessionRuntime::ResetDockerdReady() noexcept
149 {
150 m_dockerdReadyEvent.ResetEvent();
151 }
152
153 void WSLCSessionRuntime::OnProcessLog(const gsl::span<char>& buffer, PCSTR source) noexcept
154 try
155 {
156 if (buffer.empty())
157 {
158 return;
159 }
160
161 std::string entry{buffer.begin(), buffer.end()};
162 WSL_LOG(
163 "ContainerdLog",
164 TraceLoggingValue(source, "Source"),
165 TraceLoggingValue(entry.c_str(), "Content"),
166 TraceLoggingValue(m_displayName.c_str(), "Name"));
167
168 if (!m_dockerdReadyEvent.is_signaled() && entry.find(c_dockerdReadyLogLine) != std::string::npos)
169 {
170 m_dockerdReadyEvent.SetEvent();
171 }
172 }
173 CATCH_LOG()
174
175 void WSLCSessionRuntime::SetContainerdProcess(ServiceRunningProcess&& process)
176 {
177 m_containerdProcess = std::move(process);
178 }
179
180 void WSLCSessionRuntime::SetDockerdProcess(ServiceRunningProcess&& process)
181 {
182 m_dockerdProcess = std::move(process);
183 }
184
185 void WSLCSessionRuntime::SetSwapVhdPath(std::filesystem::path path)
186 {
187 m_swapVhdPath = std::move(path);
188 }
189
190 void WSLCSessionRuntime::SetStorageMounted(bool value) noexcept
191 {
192 m_storageMounted = value;
193 }
194
195 std::mutex& WSLCSessionRuntime::AllocatedPortsLock() noexcept
196 {
197 return m_allocatedPortsLock;
198 }
199
200 std::map<uint16_t, std::pair<std::shared_ptr<VmPortAllocation>, size_t>>& WSLCSessionRuntime::AllocatedPorts() noexcept
201 {
202 return m_allocatedPorts;
203 }
204
205 bool WSLCSessionRuntime::IdleTerminationEnabled() const noexcept
206 {
207 // Only tear the VM down when there is persistent storage to recover from. A tmpfs-backed
208 // session would lose all image/container state on teardown, so its VM is kept alive once started.
209 return m_settings->StoragePath != nullptr;
210 }
211
212 int WSLCSessionRuntime::StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs)
213 {
214 auto signalResult = Process.Get().Signal(WSLCSignalSIGTERM);
215 if (FAILED(signalResult))
216 {
217 LOG_HR_MSG(signalResult, "Failed to terminate process %i", Process.Get().GetPid());
218 return -1;
219 }
220
221 try
222 {
223 return Process.Wait(TerminateTimeoutMs);
224 }
225 catch (...)
226 {
227 LOG_CAUGHT_EXCEPTION();
228 try
229 {
230 LOG_IF_FAILED(Process.Get().Signal(WSLCSignalSIGKILL));
231 return Process.Wait(KillTimeoutMs);
232 }
233 CATCH_LOG();
234 }
235
236 return -1;
237 }
238
239 void WSLCSessionRuntime::EnsureVmRunning()
240 {
241 // Reject leases once the session is terminating/terminated, including on the running fast path:
242 // Shutdown() drops the lock to fire OnVmStopping, and a reentrant lease that finds the VM still
243 // Running must fail here rather than run work against a VM being permanently torn down.
244 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent.is_signaled());
245
246 if (m_vmState.load() == VmState::Running)
247 {
248 return;
249 }
250
251 bool started = false;
252 uint64_t generation = 0;
253 {
254 auto lock = m_lock.lock_exclusive();
255
256 // Re-check under the lock: terminating may have been set since the check above. This also
257 // bounds VmLease's retry loop: a lease that races with Terminate() fails here instead of
258 // restarting a VM that is being permanently torn down.
259 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent.is_signaled());
260
261 if (m_vmState.load() != VmState::Running)
262 {
263 StartVmLockHeld();
264 started = true;
265 }
266
267 generation = m_vmGeneration.load();
268 }
269
270 // Notify plugins that a VM has started, outside the exclusive lock: the handler forwards to the
271 // plugin, which may call back into the session (e.g. WSLCCreateProcess acquires a VM lease and
272 // the exclusive lock), so firing under the lock would deadlock. EnsureVmRunning's only caller
273 // (VmLease) holds an activity reference across this call, so idle teardown cannot race the VM
274 // down in the gap between releasing the lock and notifying.
275 if (started)
276 {
277 NotifyVmStarted(generation);
278 }
279 }
280
281 void WSLCSessionRuntime::NotifyVmStarted(uint64_t Generation)
282 {
283 // Hold m_notifyLock across both the pairing-state flip and the hook invocation, so a concurrent
284 // NotifyVmStopping on another thread cannot deliver its OnVmStopping in between and observe this
285 // OnVmStarted arrive late (which would otherwise leave the plugin with a trailing "started" for a
286 // VM that already stopped). m_notifyLock is recursive: the handler may reentrantly restart the VM
287 // (e.g. WSLCCreateProcess -> NotifyVmStarted/Stopping) on this same thread, which would self-deadlock
288 // under a plain mutex. Suppressed once terminating, else a racing Shutdown leaves OnVmStarted unpaired.
289 // m_notifiedGeneration tracks the VM lifecycle, not whether OnVmStarted is installed, so a hooks
290 // user that sets only OnVmStopping still gets paired stop notifications.
291 auto lock = std::lock_guard(m_notifyLock);
292
293 // Drop the notification if this instance is already gone: between releasing the runtime lock and
294 // getting here it may have been torn down and replaced, and announcing it now would misattribute
295 // the start to whichever instance is running.
296 if (m_terminating->load() || m_vmGeneration.load() != Generation)
297 {
298 return;
299 }
300
301 m_notifiedGeneration.store(Generation);
302 if (m_hooks.OnVmStarted)
303 {
304 m_hooks.OnVmStarted();
305 }
306 }
307
308 void WSLCSessionRuntime::NotifyVmStopping(uint64_t Generation)
309 {
310 // See NotifyVmStarted: hold m_notifyLock across both the pairing check and the hook invocation, so
311 // the two notifications cannot interleave across threads. Recursive because the handler may
312 // reentrantly restart the VM and call NotifyVmStarted on this same thread.
313 //
314 // N.B. The CAS below retires the generation as the stop is announced, so a second stop for the
315 // same VM (e.g. Terminate() racing an idle teardown that has dropped the lock across its
316 // notification) cannot deliver OnVmStopping twice. This is safe because the stop is committed
317 // before it is announced: the VM cannot come back, so nothing needs the generation afterwards.
318 //
319 // Generation 0 is the sentinel for "no VM has been announced": both counters start there, so a
320 // session that is torn down without ever starting a VM must not match, or Shutdown would deliver
321 // an OnVmStopping that no OnVmStarted ever paired with.
322 auto lock = std::lock_guard(m_notifyLock);
323
324 auto expected = Generation;
325 if (Generation != 0 && m_notifiedGeneration.compare_exchange_strong(expected, 0) && m_hooks.OnVmStopping)
326 {
327 m_hooks.OnVmStopping();
328 }
329 }
330
331 void WSLCSessionRuntime::BeginVmStopLockHeld() noexcept
332 {
333 // Reset before publishing the flag, so a lease cannot observe the pending stop while the event is
334 // still signaled from the previous teardown.
335 m_vmStopCompleteEvent.ResetEvent();
336 m_vmStopPending.store(true);
337 }
338
339 void WSLCSessionRuntime::EndVmStop() noexcept
340 {
341 m_vmStopPending.store(false);
342 m_vmStopCompleteEvent.SetEvent();
343 }
344
345 bool WSLCSessionRuntime::TryClaimExpectedStop() noexcept
346 {
347 auto expected = VmExitDisposition::Active;
348 return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::StopRequested);
349 }
350
351 bool WSLCSessionRuntime::TryClaimSpontaneousExit() noexcept
352 {
353 auto expected = VmExitDisposition::Active;
354 return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::ExitClaimed);
355 }
356
357 void WSLCSessionRuntime::StartVmLockHeld()
358 {
359 WI_ASSERT(m_vmState.load() != VmState::Running);
360
361 WSL_LOG("WslcVmStarting", TraceLoggingValue(m_id, "SessionId"));
362
363 m_vmState.store(VmState::Starting);
364 m_vmExitDisposition.store(VmExitDisposition::Active);
365
366 // Identify this instance. Bumped under the runtime lock so a notification that had to drop the
367 // lock to fire can tell whether it still describes the VM it was raised for. The first VM is
368 // generation 1, so the 0 stored by TearDownVmLockHeld never collides with a live instance.
369 m_vmGeneration.fetch_add(1);
370
371 // Tear back down if bring-up fails partway. The VM may have exited on its own during bring-up,
372 // so claim the stop first and only tear down if we win it (TryClaimExpectedStop()); otherwise
373 // OnVmExited() owns the teardown and we just release the lock to let its Terminate() finish.
374 auto startCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
375 if (TryClaimExpectedStop())
376 {
377 TearDownVmLockHeld();
378 m_vmState.store(VmState::None);
379 }
380 else
381 {
382 WSL_LOG("WslcVmExitedDuringStart", TraceLoggingValue(m_id, "SessionId"));
383 }
384 });
385
386 // Create a fresh IO relay for this VM instance. The previous one (if any) was stopped
387 // during teardown and cannot be restarted.
388 m_ioRelay.emplace();
389
390 // Create the VM via the factory. Re-fetch the factory from the GIT so we call it through a
391 // proxy marshalled into this thread's apartment (see m_git). The VM produces crash events;
392 // the session multiplexes them out to any registered ICrashDumpCallback subscribers via
393 // OnCrashDumpWritten.
394 wil::com_ptr<IWSLCVirtualMachineFactory> vmFactory;
395 THROW_IF_FAILED(m_git->GetInterfaceFromGlobal(m_vmFactoryGitCookie, __uuidof(IWSLCVirtualMachineFactory), vmFactory.put_void()));
396
397 wil::com_ptr<IWSLCVirtualMachine> vm;
398 THROW_IF_FAILED(vmFactory->CreateVirtualMachine(&vm));
399
400 m_virtualMachine.emplace(vm.get(), m_settings, m_sessionTerminatingEvent.get(), WSLCVirtualMachine::TOnCrashDump(m_hooks.OnCrashDump));
401
402 // Publish only once the object is constructed. If Initialize() below throws, startCleanup tears
403 // the VM down and clears this again.
404 m_hasVm.store(true);
405
406 m_virtualMachine->Initialize();
407
408 // Get an event from the service that is signaled when the VM exits.
409 m_vmExitedEvent.reset();
410 THROW_IF_FAILED(vm->GetTerminationEvent(&m_vmExitedEvent));
411 m_vmExited.store(false);
412
413 if (m_hooks.BringUp)
414 {
415 m_hooks.BringUp();
416 }
417
418 // Monitor for unexpected VM exit.
419 m_ioRelay->AddHandle(
420 std::make_unique<windows::common::io::EventHandle>(m_vmExitedEvent.get(), std::bind(&WSLCSessionRuntime::OnVmExited, this)));
421
422 if (m_hooks.RecoverState)
423 {
424 m_hooks.RecoverState();
425 }
426
427 m_vmState.store(VmState::Running);
428 startCleanup.release();
429
430 WSL_LOG("WslcVmStarted", TraceLoggingValue(m_id, "SessionId"));
431 }
432
433 void WSLCSessionRuntime::InitializeDockerRuntime(const std::filesystem::path& storagePath)
434 {
435 // Wait for dockerd to be ready before starting the event tracker.
436 THROW_WIN32_IF_MSG(
437 ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(m_settings->BootTimeoutMs), "Timed out waiting for dockerd to start");
438
439 [[maybe_unused]] auto [pid, ptyMaster, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread);
440
441 m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000);
442
443 // (Re)bind the session-scoped event tracker to this VM's docker client and relay. Existing
444 // container subscriptions are preserved across restarts.
445 m_eventTracker->Connect(m_dockerClient.value(), *m_ioRelay);
446
447 m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), storagePath);
448 }
449
450 void WSLCSessionRuntime::StopVmLockHeld()
451 {
452 if (m_vmState.load() != VmState::Running)
453 {
454 return;
455 }
456
457 WSL_LOG("WslcVmIdleStop", TraceLoggingValue(m_id, "SessionId"));
458
459 // N.B. The caller has claimed StopRequested (via TryClaimExpectedStop), so VM/dockerd/containerd
460 // exit callbacks firing from the relay thread during teardown are treated as expected, not as a
461 // crash.
462 m_vmState.store(VmState::Stopping);
463
464 TearDownVmLockHeld();
465
466 m_vmState.store(VmState::None);
467 }
468
469 void WSLCSessionRuntime::TearDownVmLockHeld(bool CaptureTerminationReason)
470 {
471 // The VM is committed to going away from here on, so the plugin's view of it ends here. Retiring
472 // the notified instance up front, before any step below can throw, also covers the case where the
473 // VM is torn down without an OnVmStopping ever being announced (e.g. bring-up failed). Written
474 // under the runtime lock; NotifyVmStarted/NotifyVmStopping read it under m_notifyLock without the
475 // runtime lock, so this store can land while one of them is inside a plugin handler. That is
476 // benign: both compare against a generation they captured earlier, so a store of 0 can only make
477 // them decline to notify, and by this point either the stop has already been announced (its CAS
478 // ran first) or the instance is gone and must not be announced at all.
479 m_notifiedGeneration.store(0);
480
481 // Latch whether the guest is already dead before running session-state cleanup so container
482 // teardown (ReleaseRuntimeResources) can skip VM-dependent calls, e.g. volume unmounts, on a VM
483 // that has exited. A graceful stop reaches here with the VM still alive (is_signaled() false), so
484 // its mounts are unmounted through the live VM; StartVmLockHeld clears this for the next instance.
485 if (m_vmExitedEvent && m_vmExitedEvent.is_signaled())
486 {
487 m_vmExited.store(true);
488 }
489
490 if (m_hooks.TearDownSessionState)
491 {
492 m_hooks.TearDownSessionState(CaptureTerminationReason);
493 }
494
495 m_volumes.reset();
496
497 // Stop the IO relay.
498 // This stops:
499 // - container state monitoring.
500 // - container init process relays
501 // - execs relays
502 // - container logs relays
503 if (m_ioRelay)
504 {
505 m_ioRelay->Stop();
506 }
507
508 {
509 std::lock_guard allocatedPortsLock(m_allocatedPortsLock);
510 m_allocatedPorts.clear();
511 }
512
513 // The session-scoped event tracker is intentionally not reset. Its stream handle dies with the IO
514 // relay above, and InitializeDockerRuntime re-binds it on the next start.
515 m_dockerClient.reset();
516
517 if (CaptureTerminationReason)
518 {
519 // Default: an explicit/graceful teardown is a shutdown (the VM is still alive and we are
520 // bringing it down). Overridden below if the VM exited on its own and recorded a cause.
521 m_lastTerminationReason = WSLCVirtualMachineTerminationReasonShutdown;
522 m_lastTerminationDetails.clear();
523 }
524
525 // Check if the VM has already exited (e.g., killed externally).
526 // If so, skip operations that require a live VM to avoid unnecessary waits.
527 // N.B. m_vmExitedEvent may be uninitialized if teardown runs before GetTerminationEvent() succeeds.
528 if (m_vmExitedEvent && m_vmExitedEvent.is_signaled())
529 {
530 WSL_LOG("SkippingGracefulShutdown_VmDead", TraceLoggingValue(m_id, "SessionId"));
531
532 // The VM exited on its own, so it recorded the cause.
533 if (CaptureTerminationReason && m_virtualMachine)
534 {
535 wil::unique_cotaskmem_string details;
536 LOG_IF_FAILED(m_virtualMachine->GetTerminationReason(&m_lastTerminationReason, &details));
537 m_lastTerminationDetails = details ? details.get() : L"";
538 }
539 }
540 else if (m_virtualMachine)
541 {
542 m_virtualMachine->OnSessionTerminated();
543
544 // Stop dockerd first, then containerd (dockerd is a client of containerd).
545 // N.B. dockerd waits a couple seconds if there are any outstanding HTTP request sockets opened.
546 if (m_dockerdProcess.has_value())
547 {
548 auto dockerdExitCode =
549 StopProcess(m_dockerdProcess.value(), wsl::windows::wslc::ProcessTerminateTimeoutMs, wsl::windows::wslc::ProcessKillTimeoutMs);
550 WSL_LOG("DockerdExit", TraceLoggingValue(dockerdExitCode, "code"));
551 }
552
553 if (m_containerdProcess.has_value())
554 {
555 auto containerdExitCode =
556 StopProcess(m_containerdProcess.value(), wsl::windows::wslc::ProcessTerminateTimeoutMs, wsl::windows::wslc::ProcessKillTimeoutMs);
557 WSL_LOG("ContainerdExit", TraceLoggingValue(containerdExitCode, "code"));
558 }
559
560 // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running.
561 if (m_storageMounted)
562 {
563 try
564 {
565 m_virtualMachine->Unmount(wsl::windows::wslc::ContainerdStorageMountPoint);
566 }
567 CATCH_LOG();
568 }
569 }
570
571 m_dockerdProcess.reset();
572 m_containerdProcess.reset();
573
574 // Retire the lock-free mirror before destroying the object so an unlocked reader never sees
575 // "a VM is present" while it is being torn down.
576 m_hasVm.store(false);
577 m_virtualMachine.reset();
578 m_storageMounted = false;
579
580 // Destroy the relay unless we're on its own thread (~IORelay joins the thread, which would
581 // deadlock). On unexpected-VM-exit path (runs on relay thread), leave it for ~WSLCSession.
582 if (!m_ioRelay || !m_ioRelay->IsRelayThread())
583 {
584 m_ioRelay.reset();
585 m_vmExitedEvent.reset();
586 }
587
588 // Delete the ephemeral swap VHD now that the VM is gone.
589 if (!m_swapVhdPath.empty())
590 {
591 LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str()));
592 m_swapVhdPath.clear();
593 }
594 }
595
596 void WSLCSessionRuntime::OnIdleTimer()
597 try
598 {
599 // Idle teardown releases cross-process COM proxies (the VM and its VM-scoped state), so this
600 // threadpool callback must join the process MTA; otherwise those Release/calls fail with
601 // RPC_E_WRONG_THREAD. The function-try-block keeps this (and everything below) under CATCH_LOG:
602 // the threadpool callback that invokes us is noexcept, so an escaping throw would terminate.
603 const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
604
605 if (m_terminating->load() || !IdleTerminationEnabled())
606 {
607 return;
608 }
609
610 // Non-blocking acquire: a blocking exclusive would queue behind in-flight operations, and
611 // SRW locks favor waiting writers, stalling all new ops. If the lock is held, an operation
612 // is in flight; it holds an activity reference and will re-arm the timer (via the 1->0
613 // transition) when it releases, so there is nothing to do here.
614 auto lock = m_lock.try_lock_exclusive();
615 if (!lock)
616 {
617 return;
618 }
619
620 // Re-check every teardown precondition under the lock. The activity count is the single
621 // source of truth for "the VM is needed"; a 0->1 transition since the timer fired (cancel
622 // raced the callback) is caught here.
623 if (m_terminating->load() || m_vmState.load() != VmState::Running || m_idleState->ActivityCount() != 0)
624 {
625 return;
626 }
627
628 // Claim the stop. If we lose, OnVmExited() owns a spontaneous-exit teardown and is spinning for
629 // this lock, so release it and let that run instead of joining the relay ourselves.
630 if (!TryClaimExpectedStop())
631 {
632 return;
633 }
634
635 // Restore Active on completion (or early exit) so the next StartVmLockHeld starts clean; only
636 // clear our own claim.
637 auto dispositionCleanup = wil::scope_exit([this]() {
638 auto stopRequested = VmExitDisposition::StopRequested;
639 m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active);
640 });
641
642 // Final activity re-check under the lock, and the last point at which the stop can still be called
643 // off: a VmLease that bumped the count since the check above is now blocked on the shared lock we
644 // hold. Back out here -- nothing has been announced yet -- rather than stopping a VM that is about
645 // to be used again.
646 if (m_idleState->ActivityCount() != 0)
647 {
648 return;
649 }
650
651 // Past this point the VM is going away, whatever happens. Publishing the pending stop under the
652 // lock is what makes the announcement below true: an ordinary lease that races in while the lock
653 // is dropped no longer gets this VM, it waits for the teardown and is served by the next one. The
654 // exception is a lease taken by the OnVmStopping handler itself, which must be served or it would
655 // deadlock against the very callback this teardown is waiting on. Work that handler leaves running
656 // dies with the VM -- which is exactly what it was just told would happen.
657 //
658 // N.B. endStop is declared after 'lock' so it runs *before* the lock is released (reverse
659 // declaration order): waiters are released only once the teardown is complete and published.
660 BeginVmStopLockHeld();
661 auto endStop = wil::scope_exit([this]() { EndVmStop(); });
662
663 // Fire OnVmStopping with m_lock dropped so a plugin handler may take a VM lease without
664 // deadlocking, and while the VM is still running so the handler can still use it.
665 //
666 // The notification cannot be allowed to abort the teardown: the stop is already published and its
667 // generation retired, so bailing out here would leave waiters to be served by the VM they were
668 // promised would die, and its eventual teardown would be silent.
669 const auto generation = m_vmGeneration.load();
670
671 lock.reset();
672 try
673 {
674 NotifyVmStopping(generation);
675 }
676 CATCH_LOG();
677 lock = m_lock.lock_exclusive();
678
679 // Unconditional: the stop was announced, so it happens. Reacquiring the exclusive lock first
680 // drains every in-flight operation, so nothing is cut off mid-call. StopVmLockHeld no-ops if a
681 // concurrent Terminate already tore the VM down, and copes with a VM that crashed while the lock
682 // was dropped -- OnVmExited() declined that exit (we hold the expected-stop claim) and its exit
683 // handle is one-shot, so this is the only teardown left to run.
684 StopVmLockHeld();
685 }
686 CATCH_LOG();
687
688 bool WSLCSessionRuntime::TriggerIdleTerminationForTest()
689 {
690 // Mirror OnIdleTimer's MTA context on a dedicated thread: the incoming RPC thread is an STA, so
691 // both re-initializing MTA on it and running teardown inline would fail (RPC_E_CHANGED_MODE /
692 // RPC_E_WRONG_THREAD when releasing the VM's cross-process COM proxies).
693 bool wasAlreadyIdle = false;
694 std::exception_ptr error;
695
696 std::thread worker([&]() {
697 try
698 {
699 const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
700
701 auto lock = m_lock.lock_exclusive();
702
703 if (m_terminating->load() || m_vmState.load() != VmState::Running)
704 {
705 wasAlreadyIdle = true;
706 return;
707 }
708
709 // tmpfs sessions have no persistent storage to recover from, so tearing a running VM down
710 // loses all state. Check this after the VM state so a never-started session still reports
711 // that it was already idle.
712 if (!IdleTerminationEnabled())
713 {
714 return;
715 }
716
717 // Match the production idle timer: an active container or operation keeps the VM alive.
718 if (m_idleState->ActivityCount() != 0)
719 {
720 return;
721 }
722
723 if (!TryClaimExpectedStop())
724 {
725 wasAlreadyIdle = true;
726 return;
727 }
728
729 auto dispositionCleanup = wil::scope_exit([this]() {
730 auto stopRequested = VmExitDisposition::StopRequested;
731 m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active);
732 });
733
734 // Mirror OnIdleTimer's final re-check under the lock: a lease taken while the stop claim
735 // was being made is now blocked on the lock we hold, and must back the teardown out here
736 // rather than announce a stop production code would have suppressed.
737 if (m_idleState->ActivityCount() != 0)
738 {
739 return;
740 }
741
742 // Commit to the stop before announcing it, then fire OnVmStopping without holding m_lock
743 // and tear down unconditionally. See OnIdleTimer for the full rationale.
744 BeginVmStopLockHeld();
745 auto endStop = wil::scope_exit([this]() { EndVmStop(); });
746
747 const auto generation = m_vmGeneration.load();
748
749 lock.reset();
750 try
751 {
752 NotifyVmStopping(generation);
753 }
754 CATCH_LOG();
755 lock = m_lock.lock_exclusive();
756
757 StopVmLockHeld();
758 }
759 catch (...)
760 {
761 error = std::current_exception();
762 }
763 });
764
765 worker.join();
766
767 if (error)
768 {
769 std::rethrow_exception(error);
770 }
771
772 return wasAlreadyIdle;
773 }
774
775 WSLCSessionRuntime::VmLease WSLCSessionRuntime::AcquireVmLease(VmLeasePolicy Policy)
776 {
777 return VmLease(*this, Policy);
778 }
779
780 WSLCSessionRuntime::LockedRuntime WSLCSessionRuntime::Acquire(VmLeasePolicy Policy)
781 {
782 return LockedRuntime(*this, Policy);
783 }
784
785 WSLCSessionRuntime::VmLease::VmLease(WSLCSessionRuntime& Runtime, VmLeasePolicy Policy) : m_runtime(&Runtime)
786 {
787 // Record an in-flight operation before bringing the VM up so idle teardown cannot tear it down
788 // between EnsureVmRunning() and acquiring the shared lock. AddActivity cancels any pending idle
789 // timer.
790 m_runtime->m_idleState->AddActivity();
791
792 auto countCleanup = wil::scope_exit([this]() {
793 m_runtime->m_idleState->ReleaseActivity();
794 m_runtime = nullptr;
795 });
796
797 // Activity increment may race with idle teardown. Retry until we hold the lock with VM running.
798 for (;;)
799 {
800 if (Policy == VmLeasePolicy::Acquire)
801 {
802 m_runtime->EnsureVmRunning();
803 }
804
805 m_lock = m_runtime->m_lock.lock_shared();
806
807 if (m_runtime->m_vmState.load() == VmState::Running)
808 {
809 // An announced stop always happens, so a VM with one pending is unusable even though it is
810 // still Running: wait for the teardown, then retry, which brings up a fresh VM. ExistingOnly
811 // callers are exempt and are served by the stopping VM -- see VmLeasePolicy.
812 if (Policy == VmLeasePolicy::ExistingOnly || !m_runtime->m_vmStopPending.load())
813 {
814 break;
815 }
816
817 // Release the shared lock before waiting. The teardown must be able to take the exclusive
818 // lock, and it is the only thing that will ever signal us.
819 m_lock.reset();
820
821 // The wait is bounded only so that a handler wedging the teardown is visible in traces;
822 // there is no correct way to proceed without a VM, so the retry is unconditional.
823 while (!m_runtime->m_vmStopCompleteEvent.wait(c_vmStopWaitLogIntervalMs))
824 {
825 WSL_LOG(
826 "WslcVmLeaseWaitingForStop",
827 TraceLoggingLevel(WINEVENT_LEVEL_WARNING),
828 TraceLoggingValue(m_runtime->m_id, "SessionId"));
829 }
830
831 continue;
832 }
833
834 // ExistingOnly never starts a VM, so retrying could only spin. Reject the caller instead.
835 THROW_HR_IF(WSLC_E_VM_NOT_RUNNING, Policy == VmLeasePolicy::ExistingOnly);
836
837 WSL_LOG(
838 "WslcVmLeaseRetry",
839 TraceLoggingValue(m_runtime->m_id, "SessionId"),
840 TraceLoggingValue(static_cast<uint32_t>(m_runtime->m_vmState.load()), "VmState"));
841 m_lock.reset();
842 }
843
844 countCleanup.release();
845 }
846
847 WSLCSessionRuntime::VmLease::VmLease(VmLease&& Other) noexcept :
848 m_runtime(std::exchange(Other.m_runtime, nullptr)), m_lock(std::move(Other.m_lock))
849 {
850 }
851
852 WSLCSessionRuntime::VmLease& WSLCSessionRuntime::VmLease::operator=(VmLease&& Other) noexcept
853 {
854 if (this != &Other)
855 {
856 if (m_runtime != nullptr)
857 {
858 // Release the shared lock before the activity reference so that, if this was the last
859 // activity, idle teardown can immediately take the exclusive lock.
860 m_lock.reset();
861 m_runtime->m_idleState->ReleaseActivity();
862 }
863
864 m_runtime = std::exchange(Other.m_runtime, nullptr);
865 m_lock = std::move(Other.m_lock);
866 }
867
868 return *this;
869 }
870
871 WSLCSessionRuntime::VmLease::~VmLease()
872 {
873 if (m_runtime != nullptr)
874 {
875 // Release the shared lock before the activity reference so that, if this was the last
876 // activity, idle teardown can immediately take the exclusive lock. ReleaseActivity arms the
877 // idle timer on the 1->0 transition.
878 m_lock.reset();
879 m_runtime->m_idleState->ReleaseActivity();
880 }
881 }
882
883 WSLCSessionRuntime::LockedRuntime::LockedRuntime(WSLCSessionRuntime& Runtime, VmLeasePolicy Policy) :
884 m_runtime(&Runtime), m_lease(Runtime.AcquireVmLease(Policy))
885 {
886 }
887
888 WSLCVirtualMachine& WSLCSessionRuntime::LockedRuntime::Vm()
889 {
890 return m_runtime->Vm();
891 }
892
893 IORelay* WSLCSessionRuntime::LockedRuntime::Relay()
894 {
895 return m_runtime->Relay();
896 }
897
898 DockerHTTPClient& WSLCSessionRuntime::LockedRuntime::Docker()
899 {
900 return m_runtime->Docker();
901 }
902
903 void WSLCSessionRuntime::OnVmExited()
904 {
905 // A spontaneous exit we must permanently terminate, unless an expected stop already claimed it,
906 // in which case the exit was wanted and we decline.
907 if (!TryClaimSpontaneousExit())
908 {
909 WSL_LOG("WslcVmExitedDuringStop", TraceLoggingValue(m_id, "SessionId"));
910 return;
911 }
912
913 WSL_LOG(
914 "VmExited",
915 TraceLoggingLevel(WINEVENT_LEVEL_WARNING),
916 TraceLoggingValue(m_id, "SessionId"),
917 TraceLoggingValue(m_displayName.c_str(), "Name"),
918 TraceLoggingValue(!m_sessionTerminatingEvent.is_signaled(), "Unexpected"));
919
920 if (m_hooks.OnSpontaneousExit)
921 {
922 m_hooks.OnSpontaneousExit();
923 }
924 }
925
926 void WSLCSessionRuntime::Shutdown(
927 wil::rwlock_release_exclusive_scope_exit& runtimeLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails)
928 {
929 if (!m_initialized)
930 {
931 return;
932 }
933
934 // runtimeLock is an exclusive hold on m_lock, guaranteeing no operation is running.
935 WI_VERIFY(runtimeLock);
936
937 // Permanently disable idle teardown and drain any in-flight timer callback on every exit path,
938 // including an exception escaping the teardown below. The timer callback captures this runtime,
939 // but IdleState outlives it (activity tokens hold shared_ptr copies), so skipping the disarm
940 // would leave a late 1->0 transition able to re-arm a timer that references freed memory.
941 //
942 // The exclusive lock is released first: a timer callback already blocked acquiring it must be
943 // able to obtain it, observe m_terminating, and return, otherwise the drain below deadlocks.
944 auto disarmIdleState = wil::scope_exit([&]() {
945 runtimeLock.reset();
946 m_idleState->Disarm();
947 });
948
949 // Notify with m_lock dropped, then re-lock for the teardown. The handler may call back into the
950 // session (e.g. WSLCCreateProcess) which takes a VM lease and this lock; firing under it would
951 // deadlock. m_terminating is set, so any such reentrant lease fails at EnsureVmRunning's gate
952 // rather than restarting the VM, and the reacquire can't block on it. A throwing handler must not
953 // skip the teardown below -- the session is terminating either way.
954 runtimeLock.reset();
955 try
956 {
957 NotifyVmStopping(m_vmGeneration.load());
958 }
959 CATCH_LOG();
960 runtimeLock = m_lock.lock_exclusive();
961
962 // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason; the
963 // reacquired exclusive hold on m_lock satisfies TearDownVmLockHeld's precondition.
964 TearDownVmLockHeld(/* CaptureTerminationReason */ true);
965
966 m_vmState.store(VmState::None);
967
968 terminationReason = m_lastTerminationReason;
969 terminationDetails = m_lastTerminationDetails;
970
971 // Signal completion last so any observer of the terminated event sees a fully torn-down
972 // session and a populated termination reason.
973 m_sessionTerminatedEvent.SetEvent();
974 }
975
976 } // namespace wsl::windows::service::wslc