master
cpp 1,011 lines 34.5 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 HcsVirtualMachine.cpp
8
9 Abstract:
10
11 Implementation of IWSLCVirtualMachine - represents a single HCS-based VM instance.
12
13 --*/
14
15 #include "HcsVirtualMachine.h"
16 #include <format>
17 #include <fstream>
18 #include <string>
19 #include <string_view>
20 #include "hcs_schema.h"
21 #include "ConsommeNetworking.h"
22 #include "NatNetworking.h"
23 #include "wslsecurity.h"
24 #include "wslutil.h"
25 #include "lxinitshared.h"
26 #include "DnsResolver.h"
27 #include "string.hpp"
28
29 using namespace wsl::windows::common;
30 using helpers::WindowsBuildNumbers;
31 using wsl::windows::service::wslc::HcsVirtualMachine;
32
33 constexpr auto MAX_VM_CRASH_FILES = 3;
34 constexpr auto SAVED_STATE_FILE_EXTENSION = L".vmrs";
35 constexpr auto SAVED_STATE_FILE_PREFIX = L"saved-state-";
36
37 namespace {
38
39 SOCKADDR_INET CreateListenAddress(LPCSTR Address, uint16_t HostPort)
40 {
41 auto listenAddr = wsl::windows::common::string::StringToSockAddrInet(wsl::shared::string::MultiByteToWide(Address));
42
43 if (listenAddr.si_family == AF_INET)
44 {
45 listenAddr.Ipv4.sin_port = HostPort;
46 }
47 else if (listenAddr.si_family == AF_INET6)
48 {
49 listenAddr.Ipv6.sin6_port = HostPort;
50 }
51 else
52 {
53 THROW_HR_MSG(E_INVALIDARG, "Unsupported address family: %d", listenAddr.si_family);
54 }
55
56 return listenAddr;
57 }
58
59 // Replace any character outside the conservative ASCII allowlist with '_' so the
60 // result is safe to use as the HCS HostingProcessNameSuffix (which becomes the
61 // vmmem-XXX process name visible in Task Manager and parsed by various tooling).
62 std::wstring SanitizeHostingProcessNameSuffix(std::wstring_view name)
63 {
64 constexpr std::wstring_view c_allowed = L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
65 std::wstring sanitized{name};
66 for (auto& c : sanitized)
67 {
68 if (c_allowed.find(c) == std::wstring_view::npos)
69 {
70 c = L'_';
71 }
72 }
73
74 return sanitized;
75 }
76
77 } // namespace
78
79 HcsVirtualMachine::HcsVirtualMachine(_In_ const WSLCSessionSettings* Settings)
80 {
81 THROW_HR_IF(E_POINTER, Settings == nullptr);
82
83 // Store the user token.
84 m_userToken = wil::shared_handle{wsl::windows::common::security::GetUserToken(TokenImpersonation).release()};
85 m_crashDumpFolder = GetCrashDumpFolder();
86
87 std::lock_guard lock(m_lock);
88
89 THROW_IF_FAILED(CoCreateGuid(&m_vmId));
90 m_vmIdString = wsl::shared::string::GuidToString<wchar_t>(m_vmId, wsl::shared::string::GuidToStringFlags::Uppercase);
91 m_featureFlags = Settings->FeatureFlags;
92 m_networkingMode = Settings->NetworkingMode;
93 m_hostLoopback = Settings->HostLoopback ? Settings->HostLoopback : "";
94 m_bootTimeoutMs = Settings->BootTimeoutMs;
95
96 // Build HCS settings
97 hcs::ComputeSystem systemSettings{};
98 systemSettings.Owner = Settings->DisplayName ? Settings->DisplayName : L"WSLC";
99 systemSettings.ShouldTerminateOnLastHandleClosed = true;
100
101 // Determine which schema version to use based on the Windows version. Windows 10 does not support
102 // newer schema versions and some features may be disabled as a result.
103 if (wsl::windows::common::helpers::IsWindows11OrAbove())
104 {
105 systemSettings.SchemaVersion.Major = 2;
106 systemSettings.SchemaVersion.Minor = 7;
107 }
108 else
109 {
110 systemSettings.SchemaVersion.Major = 2;
111 systemSettings.SchemaVersion.Minor = 3;
112 }
113
114 hcs::VirtualMachine vmSettings{};
115 vmSettings.StopOnReset = true;
116 vmSettings.Chipset.UseUtc = true;
117
118 // Ensure the 2MB granularity enforced by HCS.
119 vmSettings.ComputeTopology.Memory.SizeInMB = Settings->MemoryMb & ~0x1;
120 vmSettings.ComputeTopology.Memory.AllowOvercommit = true;
121 vmSettings.ComputeTopology.Memory.EnableDeferredCommit = true;
122 vmSettings.ComputeTopology.Memory.EnableColdDiscardHint = true;
123 vmSettings.ComputeTopology.Processor.Count = Settings->CpuCount;
124
125 // Configure backing page size, fault cluster shift size, and page reporting order to favor density (lower vmmem usage).
126 //
127 // N.B. Page reporting order must be >= fault cluster size shift.
128 const auto windowsVersion = wsl::windows::common::helpers::GetWindowsVersion();
129 int pageReportingOrder;
130 if (windowsVersion.BuildNumber >= WindowsBuildNumbers::Germanium)
131 {
132 vmSettings.ComputeTopology.Memory.BackingPageSize = hcs::MemoryBackingPageSize::Small;
133 vmSettings.ComputeTopology.Memory.FaultClusterSizeShift = 4;
134 vmSettings.ComputeTopology.Memory.DirectMapFaultClusterSizeShift = 4;
135 pageReportingOrder = 5; // 128k
136 }
137 else
138 {
139 pageReportingOrder = 9; // 2MB
140 }
141
142 if (helpers::IsVmemmSuffixSupported() && Settings->DisplayName)
143 {
144 // The vmmem-XXX process name shown in Task Manager (and parsed by tooling)
145 // can't tolerate spaces / unicode / etc., so sanitize before use. Note that
146 // Settings->DisplayName itself (e.g. the HCS Owner) is left untouched.
147 vmSettings.ComputeTopology.Memory.HostingProcessNameSuffix = SanitizeHostingProcessNameSuffix(Settings->DisplayName);
148 }
149
150 #ifdef _AMD64_
151
152 HV_X64_HYPERVISOR_HARDWARE_FEATURES hardwareFeatures{};
153 __cpuid(reinterpret_cast<int*>(&hardwareFeatures), HvCpuIdFunctionMsHvHardwareFeatures);
154 vmSettings.ComputeTopology.Processor.EnablePerfmonPmu = hardwareFeatures.ChildPerfmonPmuSupported != 0;
155 vmSettings.ComputeTopology.Processor.EnablePerfmonLbr = hardwareFeatures.ChildPerfmonLbrSupported != 0;
156
157 #endif
158
159 // Compute a swiotlb size that fits this VM's RAM for the kernel command line.
160 // Only needed when a virtio device that requires bounce buffers will be attached.
161 ULONG64 swiotlbSizeBytes = 0;
162 if (FeatureEnabled(WslcFeatureFlagsVirtioFs) || m_networkingMode == WSLCNetworkingModeConsomme)
163 {
164 swiotlbSizeBytes = helpers::ComputeDefaultSwiotlbConfig(static_cast<UINT64>(Settings->MemoryMb) * _1MB);
165 }
166
167 // Initialize kernel command line.
168 std::wstring kernelCmdLine = L"initrd=\\" LXSS_VM_MODE_INITRD_NAME L" " TEXT(WSLC_ROOT_INIT_ENV) L"=1 panic=-1";
169 helpers::AppendCommonKernelCommandLine(kernelCmdLine, pageReportingOrder, swiotlbSizeBytes, Settings->CpuCount);
170
171 // Setup dmesg collector with optional DmesgOutput handle.
172 // TODO: move dmesg collector to user session process.
173 // N.B. 'DmesgOutput' needs to be duplicated since COM will close it when this call completes.
174 wil::unique_handle dmesgOutputHandle;
175 if (Settings->DmesgOutput.Handle.File != nullptr && Settings->DmesgOutput.Handle.File != INVALID_HANDLE_VALUE)
176 {
177 dmesgOutputHandle.reset(wslutil::DuplicateHandle(wslutil::FromCOMInputHandle(Settings->DmesgOutput), GENERIC_WRITE | SYNCHRONIZE));
178 }
179
180 m_dmesgCollector = DmesgCollector::Create(
181 m_vmId, m_vmExitEvent.get(), true, false, L"", FeatureEnabled(WslcFeatureFlagsEarlyBootDmesg), std::move(dmesgOutputHandle));
182
183 if (FeatureEnabled(WslcFeatureFlagsEarlyBootDmesg))
184 {
185 if constexpr (!wsl::shared::Arm64)
186 {
187 kernelCmdLine += L" earlycon=uart8250,io,0x3f8,115200";
188 }
189 else
190 {
191 kernelCmdLine += L" earlycon=pl011,0xeffec000,115200";
192 }
193
194 vmSettings.Devices.ComPorts["0"] = hcs::ComPort{m_dmesgCollector->EarlyConsoleName()};
195 }
196
197 if (helpers::IsVirtioSerialConsoleSupported())
198 {
199 kernelCmdLine += L" console=hvc0 debug";
200 vmSettings.Devices.VirtioSerial.emplace();
201 hcs::VirtioSerialPort virtioPort{};
202 virtioPort.Name = L"hvc0";
203 virtioPort.NamedPipe = m_dmesgCollector->VirtioConsoleName();
204 virtioPort.ConsoleSupport = true;
205 vmSettings.Devices.VirtioSerial->Ports["0"] = std::move(virtioPort);
206 }
207
208 // Set up boot params.
209 //
210 // N.B. Linux kernel direct boot is not yet supported on ARM64.
211 auto basePath = wslutil::GetBasePath();
212
213 #ifdef WSL_KERNEL_PATH
214 auto kernelPath = std::filesystem::path(WSL_KERNEL_PATH);
215 #else
216 auto kernelPath = std::filesystem::path(basePath) / L"tools" / LXSS_VM_MODE_KERNEL_NAME;
217 #endif
218
219 if constexpr (!wsl::shared::Arm64)
220 {
221 vmSettings.Chipset.LinuxKernelDirect.emplace();
222 vmSettings.Chipset.LinuxKernelDirect->KernelFilePath = kernelPath.wstring();
223 vmSettings.Chipset.LinuxKernelDirect->InitRdPath = (basePath / L"tools" / LXSS_VM_MODE_INITRD_NAME).c_str();
224 vmSettings.Chipset.LinuxKernelDirect->KernelCmdLine = kernelCmdLine;
225 }
226 else
227 {
228 auto bootThis = hcs::UefiBootEntry{};
229 bootThis.DeviceType = hcs::UefiBootDevice::VmbFs;
230 bootThis.VmbFsRootPath = (basePath / L"tools").c_str();
231 bootThis.DevicePath = L"\\" LXSS_VM_MODE_KERNEL_NAME;
232 bootThis.OptionalData = kernelCmdLine;
233 hcs::Uefi uefiSettings{};
234 uefiSettings.BootThis = std::move(bootThis);
235 vmSettings.Chipset.Uefi = std::move(uefiSettings);
236 }
237
238 #ifdef WSL_KERNEL_MODULES_PATH
239 auto kernelModulesPath = std::filesystem::path(TEXT(WSL_KERNEL_MODULES_PATH));
240 #else
241 auto kernelModulesPath = basePath / L"tools" / L"artifacts.vhd";
242 #endif
243
244 // Get root VHD path
245 std::filesystem::path rootVhdPath;
246 if (Settings->RootVhdOverride != nullptr)
247 {
248 rootVhdPath = Settings->RootVhdOverride;
249 }
250 else
251 {
252 #ifdef WSL_SYSTEM_DISTRO_PATH
253 rootVhdPath = TEXT(WSL_SYSTEM_DISTRO_PATH);
254 #else
255 rootVhdPath = std::filesystem::path(wslutil::GetMsiPackagePath().value()) / L"system.vhd";
256 #endif
257 }
258
259 // Setup boot VHDs
260 hcs::Scsi scsiController{};
261 auto attachScsiDisk = [&](PCWSTR path, bool grantUserAccess) {
262 const ULONG lun = AllocateLun();
263 hcs::Attachment disk{};
264 disk.Type = hcs::AttachmentType::VirtualDisk;
265 disk.Path = path;
266 disk.ReadOnly = true;
267 disk.SupportCompressedVolumes = true;
268 disk.AlwaysAllowSparseFiles = true;
269 disk.SupportEncryptedFiles = true;
270 scsiController.Attachments[std::to_string(lun)] = std::move(disk);
271
272 DiskInfo diskInfo{path};
273
274 if (grantUserAccess)
275 {
276 auto runAsUser = wil::impersonate_token(m_userToken.get());
277 hcs::GrantVmAccess(m_vmIdString.c_str(), path);
278 diskInfo.AccessGranted = true;
279 }
280
281 m_attachedDisks.emplace(lun, std::move(diskInfo));
282 };
283
284 attachScsiDisk(rootVhdPath.c_str(), Settings->RootVhdOverride != nullptr);
285 attachScsiDisk(kernelModulesPath.c_str(), false);
286
287 vmSettings.Devices.Scsi["0"] = std::move(scsiController);
288
289 // Setup HvSocket security
290 auto tokenUser = wil::get_token_information<TOKEN_USER>(m_userToken.get());
291 wil::unique_hlocal_string userSidString;
292 THROW_LAST_ERROR_IF(!ConvertSidToStringSidW(tokenUser->User.Sid, &userSidString));
293
294 std::wstring securityDescriptor = std::format(L"D:P(A;;FA;;;SY)(A;;FA;;;{})", userSidString.get());
295 hcs::HvSocket hvSocketConfig{};
296 hvSocketConfig.HvSocketConfig.DefaultBindSecurityDescriptor = securityDescriptor;
297 hvSocketConfig.HvSocketConfig.DefaultConnectSecurityDescriptor = securityDescriptor;
298 vmSettings.Devices.HvSocket = std::move(hvSocketConfig);
299
300 // Enable .vmrs dump collection if supported.
301 if (wsl::windows::common::helpers::IsWindows11OrAbove())
302 {
303 CreateVmSavedStateFile(m_userToken.get());
304 if (!m_vmSavedStateFile.empty())
305 {
306 hcs::DebugOptions debugOptions{};
307 debugOptions.BugcheckSavedStateFileName = m_vmSavedStateFile;
308 vmSettings.DebugOptions = std::move(debugOptions);
309 }
310 }
311
312 systemSettings.VirtualMachine = std::move(vmSettings);
313 auto json = wsl::shared::ToJsonW(systemSettings);
314
315 WSL_LOG("CreateWSLCVirtualMachine", TraceLoggingValue(json.c_str(), "json"));
316
317 // Create and start compute system
318 m_computeSystem = hcs::CreateComputeSystem(m_vmIdString.c_str(), json.c_str());
319
320 if (FeatureEnabled(WslcFeatureFlagsVirtioFs) || m_networkingMode == WSLCNetworkingModeConsomme)
321 {
322 m_guestDeviceManager = std::make_shared<::GuestDeviceManager>(m_vmIdString, m_vmId);
323 }
324
325 hcs::RegisterCallback(m_computeSystem.get(), &HcsVirtualMachine::OnVmExitCallback, this);
326
327 // Create a listening socket for mini_init to connect to once the VM is running.
328 m_listenSocket = wsl::windows::common::hvsocket::Listen(m_vmId, LX_INIT_UTILITY_VM_INIT_PORT);
329
330 // Start the virtual machine
331 hcs::StartComputeSystem(m_computeSystem.get(), json.c_str());
332
333 // Add GPU to the VM if requested
334 if (FeatureEnabled(WslcFeatureFlagsGPU))
335 {
336 hcs::ModifySettingRequest<hcs::GpuConfiguration> gpuRequest{};
337 gpuRequest.ResourcePath = L"VirtualMachine/ComputeTopology/Gpu";
338 gpuRequest.RequestType = hcs::ModifyRequestType::Update;
339 gpuRequest.Settings.AssignmentMode = hcs::GpuAssignmentMode::Mirror;
340 gpuRequest.Settings.AllowVendorExtension = true;
341 if (wsl::windows::common::hcs::IsDisableVgpuSettingsSupported())
342 {
343 gpuRequest.Settings.DisableGdiAcceleration = true;
344 gpuRequest.Settings.DisablePresentation = true;
345 }
346
347 hcs::ModifyComputeSystem(m_computeSystem.get(), wsl::shared::ToJsonW(gpuRequest).c_str());
348 }
349 }
350
351 HcsVirtualMachine::~HcsVirtualMachine()
352 {
353 // Do not hold m_lock: waiting on m_vmExitEvent and closing the compute system below both block
354 // on in-flight HCS exit/crash callbacks, which may themselves need m_lock. OnExit() is lock-free,
355 // and closing the compute system drains all callbacks, so the rest of teardown needs no lock.
356
357 // Wait up to 5 seconds for the VM to terminate gracefully.
358 bool forceTerminate = false;
359 if (!m_vmExitEvent.wait(5000))
360 {
361 forceTerminate = true;
362 try
363 {
364 hcs::TerminateComputeSystem(m_computeSystem.get());
365 }
366 CATCH_LOG()
367 }
368
369 WSL_LOG("WSLCTerminateVm", TraceLoggingValue(forceTerminate, "forced"));
370
371 // N.B. Destruction order matters: the networking engine and device manager must be torn down
372 // before the compute system handle is closed. The networking engine holds a shared_ptr to
373 // GuestDeviceManager, so it must be released first for the device manager reset to be effective.
374 m_networkEngine.reset();
375 m_guestDeviceManager.reset();
376 if (m_plan9Server)
377 {
378 LOG_IF_FAILED(m_plan9Server->Teardown());
379 m_plan9Server.reset();
380 }
381 m_computeSystem.reset();
382
383 // Revoke VM access for attached disks
384 for (const auto& e : m_attachedDisks)
385 {
386 try
387 {
388 if (e.second.AccessGranted)
389 {
390 hcs::RevokeVmAccess(m_vmIdString.c_str(), e.second.Path.c_str());
391 }
392 }
393 CATCH_LOG()
394 }
395
396 // If the VM did not crash, the saved state file should be empty, so we can remove it.
397 if (!m_vmSavedStateFile.empty() && !m_vmSavedStateCaptured)
398 {
399 try
400 {
401 auto runAsUser = wil::impersonate_token(m_userToken.get());
402 WI_ASSERT(std::filesystem::is_empty(m_vmSavedStateFile));
403 std::filesystem::remove(m_vmSavedStateFile);
404 }
405 CATCH_LOG()
406 }
407 }
408
409 bool HcsVirtualMachine::FeatureEnabled(WSLCFeatureFlags Value) const
410 {
411 return static_cast<ULONG>(m_featureFlags) & static_cast<ULONG>(Value);
412 }
413
414 HRESULT HcsVirtualMachine::GetId(_Out_ GUID* VmId)
415 try
416 {
417 RETURN_HR_IF_NULL(E_POINTER, VmId);
418
419 *VmId = m_vmId;
420 return S_OK;
421 }
422 CATCH_RETURN()
423
424 HRESULT HcsVirtualMachine::AcceptConnection(_Out_ HANDLE* Socket)
425 try
426 {
427 RETURN_HR_IF_NULL(E_POINTER, Socket);
428
429 auto socket = wsl::windows::common::socket::CancellableAccept(m_listenSocket.get(), m_bootTimeoutMs, m_vmExitEvent.get());
430 THROW_HR_IF(E_ABORT, !socket.has_value());
431
432 *Socket = reinterpret_cast<HANDLE>(socket->release());
433 return S_OK;
434 }
435 CATCH_RETURN()
436
437 HRESULT HcsVirtualMachine::ConfigureNetworking(_In_ HANDLE GnsSocket, _In_opt_ HANDLE* DnsSocket)
438 try
439 {
440 std::lock_guard lock(m_lock);
441 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED), m_networkEngine != nullptr);
442
443 if (m_networkingMode == WSLCNetworkingModeNone)
444 {
445 return S_OK;
446 }
447
448 // Duplicate the socket handles - COM manages the lifetime of the marshalled handles,
449 // so we need our own copies to take ownership.
450 wil::unique_socket gnsSocketHandle{reinterpret_cast<SOCKET>(wslutil::DuplicateHandle(GnsSocket))};
451 wil::unique_socket dnsSocketHandle;
452
453 // The DNS hvsocket is only allocated for NAT mode.
454 THROW_HR_IF(E_INVALIDARG, (FeatureEnabled(WslcFeatureFlagsDnsTunneling) && m_networkingMode == WSLCNetworkingModeNAT) != (DnsSocket != nullptr));
455
456 // The check still applies to Consomme because the host Consomme NAT uses the same Windows DNS APIs.
457 if (FeatureEnabled(WslcFeatureFlagsDnsTunneling))
458 {
459 const auto result = wsl::core::networking::DnsResolver::LoadDnsResolverMethods();
460 if (FAILED(result))
461 {
462 LOG_HR_MSG(result, "Failed to load DNS resolver methods, DNS tunneling will be disabled");
463 WI_ClearFlag(m_featureFlags, WslcFeatureFlagsDnsTunneling);
464 }
465 }
466
467 if (DnsSocket != nullptr && FeatureEnabled(WslcFeatureFlagsDnsTunneling))
468 {
469 dnsSocketHandle.reset(reinterpret_cast<SOCKET>(wslutil::DuplicateHandle(*DnsSocket)));
470 }
471
472 if (m_networkingMode == WSLCNetworkingModeNAT)
473 {
474 // TODO: refactor this to avoid using wsl config
475 m_natConfig.emplace(nullptr);
476 if (!wsl::core::NatNetworking::IsHyperVFirewallSupported(*m_natConfig))
477 {
478 m_natConfig->FirewallConfig.reset();
479 }
480
481 // Enable DNS tunneling if a DNS socket was provided
482 if (FeatureEnabled(WslcFeatureFlagsDnsTunneling))
483 {
484 WI_ASSERT(dnsSocketHandle);
485
486 m_natConfig->EnableDnsTunneling = true;
487 in_addr address{};
488 WI_VERIFY(inet_pton(AF_INET, LX_INIT_DNS_TUNNELING_IP_ADDRESS, &address) == 1);
489 m_natConfig->DnsTunnelingIpAddress = address.S_un.S_addr;
490 }
491
492 m_networkEngine = std::make_unique<wsl::core::NatNetworking>(
493 m_computeSystem.get(),
494 wsl::core::NatNetworking::CreateNetwork(*m_natConfig),
495 wsl::core::GnsChannel(std::move(gnsSocketHandle)),
496 *m_natConfig,
497 std::move(dnsSocketHandle),
498 nullptr);
499 }
500 else if (m_networkingMode == WSLCNetworkingModeConsomme)
501 {
502 wsl::core::ConsommeNetworkingFlags flags = wsl::core::ConsommeNetworkingFlags::Ipv6;
503 if (FeatureEnabled(WslcFeatureFlagsDnsTunneling))
504 {
505 WI_SetFlag(flags, wsl::core::ConsommeNetworkingFlags::DnsTunneling);
506 }
507
508 if (!FeatureEnabled(WslcFeatureFlagsPortRelayWslRelay))
509 {
510 WI_SetFlag(flags, wsl::core::ConsommeNetworkingFlags::LocalhostRelay);
511 }
512
513 m_networkEngine = std::make_unique<wsl::core::ConsommeNetworking>(
514 wsl::core::GnsChannel(std::move(gnsSocketHandle)), flags, nullptr, m_hostLoopback.c_str(), m_guestDeviceManager, m_userToken);
515 }
516 else
517 {
518 THROW_HR_MSG(E_INVALIDARG, "Invalid networking mode: %lu", m_networkingMode);
519 }
520
521 m_networkEngine->Initialize();
522
523 return S_OK;
524 }
525 CATCH_RETURN()
526
527 HRESULT HcsVirtualMachine::AttachDisk(_In_ LPCWSTR Path, _In_ BOOL ReadOnly, _Out_ ULONG* Lun)
528 try
529 {
530 RETURN_HR_IF(E_POINTER, Path == nullptr || Lun == nullptr);
531
532 std::lock_guard lock(m_lock);
533
534 DiskInfo disk{Path};
535 const ULONG allocatedLun = AllocateLun();
536
537 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
538 if (disk.AccessGranted)
539 {
540 hcs::RevokeVmAccess(m_vmIdString.c_str(), disk.Path.c_str());
541 }
542
543 FreeLun(allocatedLun);
544 });
545
546 auto grantDiskAccess = [&]() {
547 auto runAsUser = wil::impersonate_token(m_userToken.get());
548 hcs::GrantVmAccess(m_vmIdString.c_str(), Path);
549 disk.AccessGranted = true;
550 };
551
552 if (!ReadOnly)
553 {
554 grantDiskAccess();
555 }
556
557 auto result = wil::ResultFromException([&]() { hcs::AddVhd(m_computeSystem.get(), Path, allocatedLun, ReadOnly); });
558
559 if (result == HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED) && !disk.AccessGranted)
560 {
561 grantDiskAccess();
562 hcs::AddVhd(m_computeSystem.get(), Path, allocatedLun, ReadOnly);
563 }
564 else
565 {
566 THROW_IF_FAILED(result);
567 }
568
569 m_attachedDisks.emplace(allocatedLun, std::move(disk));
570
571 cleanup.release();
572
573 *Lun = allocatedLun;
574 return S_OK;
575 }
576 CATCH_RETURN()
577
578 HRESULT HcsVirtualMachine::DetachDisk(_In_ ULONG Lun)
579 try
580 {
581 std::lock_guard lock(m_lock);
582
583 auto it = m_attachedDisks.find(Lun);
584 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_attachedDisks.end());
585
586 hcs::RemoveScsiDisk(m_computeSystem.get(), Lun);
587
588 FreeLun(Lun);
589
590 if (it->second.AccessGranted)
591 {
592 hcs::RevokeVmAccess(m_vmIdString.c_str(), it->second.Path.c_str());
593 }
594
595 m_attachedDisks.erase(it);
596
597 return S_OK;
598 }
599 CATCH_RETURN()
600
601 HRESULT HcsVirtualMachine::AddShare(_In_ LPCWSTR WindowsPath, _In_ BOOL ReadOnly, _Out_ GUID* ShareId)
602 try
603 {
604 RETURN_HR_IF(E_POINTER, WindowsPath == nullptr || ShareId == nullptr);
605
606 std::lock_guard lock(m_lock);
607
608 GUID shareIdLocal;
609 THROW_IF_FAILED(CoCreateGuid(&shareIdLocal));
610 auto shareName = wsl::shared::string::GuidToString<wchar_t>(shareIdLocal, wsl::shared::string::None);
611
612 // Add the share entry upfront so the emplace cannot fail after the device is created.
613 auto it = m_shares.emplace(shareIdLocal, std::nullopt).first;
614 auto cleanup = wil::scope_exit([&]() { m_shares.erase(it); });
615
616 if (!FeatureEnabled(WslcFeatureFlagsVirtioFs))
617 {
618 auto runAsUser = wil::impersonate_token(m_userToken.get());
619 if (!m_plan9Server)
620 {
621 auto server =
622 wsl::windows::common::wslutil::CreateComServerAsUser<p9fs::Plan9FileSystem, IPlan9FileSystem>(m_userToken.get());
623 THROW_IF_FAILED(server->Init(&m_vmId, LX_INIT_UTILITY_VM_PLAN9_PORT));
624 THROW_IF_FAILED(server->Resume());
625 m_plan9Server = std::move(server);
626 }
627
628 auto flags = hcs::Plan9ShareFlags::AllowOptions;
629 WI_SetFlagIf(flags, hcs::Plan9ShareFlags::ReadOnly, ReadOnly);
630 THROW_IF_FAILED(m_plan9Server->AddSharePath(shareName.c_str(), WindowsPath, static_cast<UINT32>(flags)));
631 }
632 else
633 {
634 std::wstring options = ReadOnly ? L"ro" : L"";
635
636 if (!m_virtioFsDevice.has_value())
637 {
638 VirtioFsShareOptions aggregateOptions{.Kind = VirtiofsShareKind_Aggregate};
639 m_virtioFsDevice =
640 m_guestDeviceManager->AddVirtiofsDevice(TEXT(LX_INIT_DRVFS_VIRTIO_TAG), L"", L"", m_userToken.get(), aggregateOptions);
641 }
642
643 m_guestDeviceManager->AddVirtiofsChild(m_virtioFsDevice.value(), shareName.c_str(), options.c_str(), WindowsPath);
644 it->second = m_virtioFsDevice;
645 }
646
647 cleanup.release();
648
649 *ShareId = shareIdLocal;
650 return S_OK;
651 }
652 CATCH_RETURN()
653
654 HRESULT HcsVirtualMachine::RemoveShare(_In_ REFGUID ShareId)
655 try
656 {
657 std::lock_guard lock(m_lock);
658
659 auto it = m_shares.find(ShareId);
660 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_shares.end());
661
662 if (!it->second.has_value())
663 {
664 auto runAsUser = wil::impersonate_token(m_userToken.get());
665 auto shareName = wsl::shared::string::GuidToString<wchar_t>(it->first, wsl::shared::string::None);
666 THROW_IF_FAILED(m_plan9Server->RemoveShare(shareName.c_str()));
667 }
668 else
669 {
670 auto shareName = wsl::shared::string::GuidToString<wchar_t>(it->first, wsl::shared::string::None);
671 m_guestDeviceManager->RemoveVirtiofsChild(it->second.value(), shareName.c_str());
672 }
673
674 m_shares.erase(it);
675
676 return S_OK;
677 }
678 CATCH_RETURN()
679
680 HRESULT HcsVirtualMachine::ApplyGuestCapabilities(_In_ const WSLCGuestCapabilities* Capabilities)
681 try
682 {
683 RETURN_HR_IF_NULL(E_POINTER, Capabilities);
684
685 std::lock_guard lock(m_lock);
686
687 THROW_HR_IF(E_INVALIDARG, m_swiotlbConfigured);
688
689 if (Capabilities->HvPciSwiotlbBase != 0 && Capabilities->HvPciSwiotlbSize != 0)
690 {
691 if (m_guestDeviceManager)
692 {
693 m_guestDeviceManager->SetSwiotlb(Capabilities->HvPciSwiotlbBase, Capabilities->HvPciSwiotlbSize);
694 }
695
696 m_swiotlbConfigured = true;
697 }
698
699 WSL_LOG(
700 "WSLCApplyGuestCapabilities",
701 TraceLoggingValue(Capabilities->HvPciSwiotlbBase, "HvPciSwiotlbBase"),
702 TraceLoggingValue(Capabilities->HvPciSwiotlbSize, "HvPciSwiotlbSize"));
703
704 return S_OK;
705 }
706 CATCH_RETURN()
707
708 HRESULT HcsVirtualMachine::GetTerminationEvent(_Out_ HANDLE* Event)
709 try
710 {
711 RETURN_HR_IF_NULL(E_POINTER, Event);
712
713 *Event = wslutil::DuplicateHandle(m_vmExitEvent.get());
714
715 return S_OK;
716 }
717 CATCH_RETURN()
718
719 HRESULT HcsVirtualMachine::MapVirtioNetPort(_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ LPCSTR ListenAddress, _Out_ USHORT* AllocatedHostPort)
720 try
721 {
722 RETURN_HR_IF(E_POINTER, AllocatedHostPort == nullptr || ListenAddress == nullptr);
723
724 *AllocatedHostPort = 0;
725
726 std::lock_guard lock(m_lock);
727
728 auto* consommeNet = dynamic_cast<wsl::core::ConsommeNetworking*>(m_networkEngine.get());
729 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), consommeNet == nullptr);
730
731 return consommeNet->MapPort(CreateListenAddress(ListenAddress, HostPort), GuestPort, Protocol, AllocatedHostPort);
732 }
733 CATCH_RETURN()
734
735 HRESULT HcsVirtualMachine::UnmapVirtioNetPort(_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ LPCSTR ListenAddress)
736 try
737 {
738 RETURN_HR_IF(E_POINTER, ListenAddress == nullptr);
739
740 std::lock_guard lock(m_lock);
741
742 auto* consommeNet = dynamic_cast<wsl::core::ConsommeNetworking*>(m_networkEngine.get());
743 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), consommeNet == nullptr);
744
745 return consommeNet->UnmapPort(CreateListenAddress(ListenAddress, HostPort), GuestPort, Protocol);
746 }
747 CATCH_RETURN()
748
749 void CALLBACK HcsVirtualMachine::OnVmExitCallback(HCS_EVENT* Event, void* Context)
750 try
751 {
752 WSL_LOG(
753 "OnVmExitCallback",
754 TraceLoggingValue(Event->EventData, "details"),
755 TraceLoggingValue(static_cast<int>(Event->Type), "type"));
756
757 auto* vm = reinterpret_cast<HcsVirtualMachine*>(Context);
758 if (Event->Type == HcsEventSystemExited)
759 {
760 vm->OnExit(Event);
761 }
762 else if (Event->Type == HcsEventSystemCrashInitiated || Event->Type == HcsEventSystemCrashReport)
763 {
764 vm->OnCrash(Event);
765 }
766 }
767 CATCH_LOG()
768
769 void HcsVirtualMachine::OnExit(const HCS_EVENT* Event)
770 {
771 const auto exitStatus = wsl::shared::FromJson<wsl::windows::common::hcs::SystemExitStatus>(Event->EventData);
772
773 auto reason = WSLCVirtualMachineTerminationReasonUnknown;
774
775 if (exitStatus.ExitType.has_value())
776 {
777 switch (exitStatus.ExitType.value())
778 {
779 case hcs::NotificationType::ForcedExit:
780 case hcs::NotificationType::GracefulExit:
781 reason = WSLCVirtualMachineTerminationReasonShutdown;
782 break;
783 case hcs::NotificationType::UnexpectedExit:
784 reason = WSLCVirtualMachineTerminationReasonCrashed;
785 break;
786 default:
787 reason = WSLCVirtualMachineTerminationReasonUnknown;
788 break;
789 }
790 }
791
792 // Cache the termination reason and details before signaling the exit event. These fields are
793 // written once here (OnExit fires once and m_vmExitEvent is never reset) and published to readers
794 // by the SetEvent below; GetTerminationReason only reads them after observing the signaled event.
795 m_terminationReason = reason;
796 m_terminationDetails = Event->EventData;
797
798 m_vmExitEvent.SetEvent();
799 }
800
801 HRESULT HcsVirtualMachine::GetTerminationReason(_Out_ WSLCVirtualMachineTerminationReason* Reason, _Out_ LPWSTR* Details)
802 try
803 {
804 RETURN_HR_IF(E_POINTER, Reason == nullptr || Details == nullptr);
805
806 *Reason = WSLCVirtualMachineTerminationReasonUnknown;
807 *Details = nullptr;
808
809 // m_terminationReason/m_terminationDetails are written once in OnExit before m_vmExitEvent is
810 // signaled and never modified afterward, so observing the signaled event safely publishes them.
811 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_vmExitEvent.is_signaled());
812
813 *Reason = m_terminationReason;
814 *Details = wil::make_cotaskmem_string(m_terminationDetails.c_str()).release();
815
816 return S_OK;
817 }
818 CATCH_RETURN()
819
820 void HcsVirtualMachine::OnCrash(const HCS_EVENT* Event)
821 {
822 if (m_crashLogCaptured.load() && m_vmSavedStateCaptured.load())
823 {
824 return;
825 }
826
827 const auto crashReport = wsl::shared::FromJson<wsl::windows::common::hcs::CrashReport>(Event->EventData);
828
829 if (crashReport.GuestCrashSaveInfo.has_value() && crashReport.GuestCrashSaveInfo->SaveStateFile.has_value())
830 {
831 if (!m_vmSavedStateCaptured.exchange(true))
832 {
833 auto resetFlag = wil::scope_exit([&]() noexcept { m_vmSavedStateCaptured.store(false); });
834 EnforceVmSavedStateFileLimit();
835 resetFlag.release();
836 }
837 }
838
839 if (!crashReport.CrashLog.empty())
840 {
841 if (!m_crashLogCaptured.exchange(true))
842 {
843 auto resetFlag = wil::scope_exit([&]() noexcept { m_crashLogCaptured.store(false); });
844 WriteCrashLog(crashReport.CrashLog);
845 resetFlag.release();
846 }
847 }
848 }
849
850 std::filesystem::path HcsVirtualMachine::GetCrashDumpFolder()
851 {
852 auto tempPath = wsl::windows::common::filesystem::GetTempFolderPath(m_userToken.get());
853 return tempPath / L"wslc-crashes";
854 }
855
856 void HcsVirtualMachine::CreateVmSavedStateFile(HANDLE InUserToken)
857 {
858 auto runAsUser = wil::impersonate_token(InUserToken);
859
860 const auto filename = std::format(L"saved-state-{}-{}.vmrs", std::time(nullptr), m_vmIdString);
861 auto savedStateFile = m_crashDumpFolder / filename;
862
863 wsl::windows::common::filesystem::EnsureDirectory(m_crashDumpFolder.c_str());
864
865 wil::unique_handle file{CreateFileW(savedStateFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY, nullptr)};
866 THROW_LAST_ERROR_IF(!file);
867
868 hcs::GrantVmAccess(m_vmIdString.c_str(), savedStateFile.c_str());
869 m_vmSavedStateFile = savedStateFile;
870 }
871
872 void HcsVirtualMachine::EnforceVmSavedStateFileLimit()
873 {
874 auto runAsUser = wil::impersonate_token(m_userToken.get());
875
876 auto pred = [](const auto& e) {
877 return WI_IsFlagSet(GetFileAttributes(e.path().c_str()), FILE_ATTRIBUTE_TEMPORARY) && e.path().has_extension() &&
878 e.path().extension() == SAVED_STATE_FILE_EXTENSION && e.path().has_filename() &&
879 e.path().filename().wstring().find(SAVED_STATE_FILE_PREFIX) == 0 && e.file_size() > 0;
880 };
881
882 wsl::windows::common::wslutil::EnforceFileLimit(m_crashDumpFolder.c_str(), MAX_VM_CRASH_FILES + 1, pred);
883 }
884
885 void HcsVirtualMachine::WriteCrashLog(const std::wstring& crashLog)
886 {
887 auto runAsUser = wil::impersonate_token(m_userToken.get());
888
889 constexpr auto c_extension = L".txt";
890 constexpr auto c_prefix = L"kernel-panic-";
891 const auto filename = std::format(L"{}{}-{}{}", c_prefix, std::time(nullptr), m_vmIdString, c_extension);
892 auto filePath = m_crashDumpFolder / filename;
893
894 WI_ASSERT(std::filesystem::exists(m_crashDumpFolder));
895 WI_ASSERT(std::filesystem::is_directory(m_crashDumpFolder));
896
897 auto pred = [&c_extension, &c_prefix](const auto& e) {
898 return WI_IsFlagSet(GetFileAttributes(e.path().c_str()), FILE_ATTRIBUTE_TEMPORARY) && e.path().has_extension() &&
899 e.path().extension() == c_extension && e.path().has_filename() && e.path().filename().wstring().find(c_prefix) == 0;
900 };
901
902 wsl::windows::common::wslutil::EnforceFileLimit(m_crashDumpFolder.c_str(), MAX_VM_CRASH_FILES, pred);
903
904 {
905 std::wofstream outputFile(filePath.wstring());
906 THROW_HR_IF(E_UNEXPECTED, !outputFile.is_open());
907
908 outputFile << crashLog;
909 THROW_HR_IF(E_UNEXPECTED, outputFile.fail());
910 }
911
912 THROW_IF_WIN32_BOOL_FALSE(SetFileAttributesW(filePath.c_str(), FILE_ATTRIBUTE_TEMPORARY));
913 }
914
915 ULONG HcsVirtualMachine::AllocateLun()
916 {
917 for (ULONG index = 0; index < gsl::narrow_cast<ULONG>(m_lunBitmap.size()); index += 1)
918 {
919 if (!m_lunBitmap[index])
920 {
921 m_lunBitmap[index] = true;
922 return index;
923 }
924 }
925
926 THROW_HR(WSL_E_TOO_MANY_DISKS_ATTACHED);
927 }
928
929 void HcsVirtualMachine::FreeLun(ULONG Lun)
930 {
931 THROW_HR_IF(E_BOUNDS, Lun >= m_lunBitmap.size());
932 THROW_HR_IF(E_INVALIDARG, !m_lunBitmap[Lun]);
933
934 m_lunBitmap[Lun] = false;
935 }
936
937 namespace wsl::windows::service::wslc {
938
939 WSLCVirtualMachineFactory::WSLCVirtualMachineFactory(_In_ const WSLCSessionSettings* Settings)
940 {
941 THROW_HR_IF(E_POINTER, Settings == nullptr);
942
943 m_displayName = Settings->DisplayName ? Settings->DisplayName : L"";
944 m_storagePath = Settings->StoragePath ? Settings->StoragePath : L"";
945
946 if (Settings->RootVhdOverride != nullptr)
947 {
948 m_rootVhdOverride.emplace(Settings->RootVhdOverride);
949 }
950
951 if (Settings->RootVhdTypeOverride != nullptr)
952 {
953 m_rootVhdTypeOverride.emplace(Settings->RootVhdTypeOverride);
954 }
955
956 // Keep our own duplicate of the dmesg sink so recreated VMs can reuse it.
957 if (Settings->DmesgOutput.Handle.File != nullptr && Settings->DmesgOutput.Handle.File != INVALID_HANDLE_VALUE)
958 {
959 m_dmesgOutput.reset(wslutil::DuplicateHandle(wslutil::FromCOMInputHandle(Settings->DmesgOutput), GENERIC_WRITE | SYNCHRONIZE));
960 }
961
962 m_maximumStorageSizeMb = Settings->MaximumStorageSizeMb;
963 m_cpuCount = Settings->CpuCount;
964 m_memoryMb = Settings->MemoryMb;
965 m_bootTimeoutMs = Settings->BootTimeoutMs;
966 m_networkingMode = Settings->NetworkingMode;
967 m_featureFlags = Settings->FeatureFlags;
968 m_hostLoopback = Settings->HostLoopback ? Settings->HostLoopback : "";
969 m_storageFlags = Settings->StorageFlags;
970 }
971
972 WSLCSessionSettings WSLCVirtualMachineFactory::BuildSettings()
973 {
974 WSLCSessionSettings settings{};
975 settings.DisplayName = m_displayName.c_str();
976 settings.StoragePath = m_storagePath.empty() ? nullptr : m_storagePath.c_str();
977 settings.MaximumStorageSizeMb = m_maximumStorageSizeMb;
978 settings.CpuCount = m_cpuCount;
979 settings.MemoryMb = m_memoryMb;
980 settings.BootTimeoutMs = m_bootTimeoutMs;
981 settings.NetworkingMode = m_networkingMode;
982 settings.FeatureFlags = m_featureFlags;
983 settings.HostLoopback = m_hostLoopback.empty() ? nullptr : m_hostLoopback.c_str();
984 settings.StorageFlags = m_storageFlags;
985 settings.RootVhdOverride = m_rootVhdOverride ? m_rootVhdOverride->c_str() : nullptr;
986 settings.RootVhdTypeOverride = m_rootVhdTypeOverride ? m_rootVhdTypeOverride->c_str() : nullptr;
987
988 if (m_dmesgOutput)
989 {
990 settings.DmesgOutput = wslutil::ToCOMInputHandle(m_dmesgOutput.get());
991 }
992
993 return settings;
994 }
995
996 HRESULT WSLCVirtualMachineFactory::CreateVirtualMachine(_Out_ IWSLCVirtualMachine** Vm)
997 try
998 {
999 RETURN_HR_IF(E_POINTER, Vm == nullptr);
1000 *Vm = nullptr;
1001
1002 const auto settings = BuildSettings();
1003 auto vm = Microsoft::WRL::Make<HcsVirtualMachine>(&settings);
1004 THROW_IF_NULL_ALLOC(vm);
1005
1006 *Vm = vm.Detach();
1007 return S_OK;
1008 }
1009 CATCH_RETURN()
1010
1011 } // namespace wsl::windows::service::wslc