master
cpp 1,514 lines 52.1 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCVirtualMachine.cpp
8
9 Abstract:
10
11 Client-side class for WSLC virtual machine operations.
12 The VM is created via IWSLCVirtualMachine (running in the SYSTEM service).
13 This class connects to the existing VM for unprivileged operations
14 and delegates privileged operations back to IWSLCVirtualMachine.
15
16 --*/
17
18 #include "precomp.h"
19 #include "WSLCVirtualMachine.h"
20 #include <format>
21 #include <filesystem>
22 #include <nlohmann/json.hpp>
23 #include "ServiceProcessLauncher.h"
24 #include "wslutil.h"
25 #include "lxinitshared.h"
26
27 using namespace wsl::windows::common;
28 using wsl::windows::common::io::HandleWrapper;
29 using wsl::windows::service::wslc::TypedHandle;
30 using wsl::windows::service::wslc::VmPortAllocation;
31 using wsl::windows::service::wslc::VMPortMapping;
32 using wsl::windows::service::wslc::WSLCProcess;
33 using wsl::windows::service::wslc::WSLCVirtualMachine;
34 namespace wslutil = wsl::windows::common::wslutil;
35
36 constexpr auto CONTAINER_PORT_RANGE = std::pair<uint16_t, uint16_t>(20002, 65535);
37
38 static_assert(c_ephemeralPortRange.second < CONTAINER_PORT_RANGE.first);
39
40 namespace {
41
42 // Escapes regex metacharacters in `input` so a literal hostname can be embedded into a BuildKit
43 // source-policy regex identifier (e.g. `myreg:5000` stays a literal match).
44 std::string EscapeRegexMetacharacters(std::string_view input)
45 {
46 static constexpr std::string_view c_metacharacters = R"(\.+*?()|[]{}^$)";
47 std::string escaped;
48 escaped.reserve(input.size());
49 for (const char ch : input)
50 {
51 if (c_metacharacters.find(ch) != std::string_view::npos)
52 {
53 escaped.push_back('\\');
54 }
55 escaped.push_back(ch);
56 }
57 return escaped;
58 }
59
60 // DENY-all first, then per-host ALLOW: BuildKit evaluates rules in order and last match wins.
61 // Hosts are lowercased because BuildKit normalises identifiers before regex matching.
62 // https://github.com/moby/buildkit/blob/master/docs/sourcepolicy.md
63 std::string BuildBuildKitSourcePolicyJson(const std::vector<std::string>& allowedHosts)
64 {
65 nlohmann::json rules = nlohmann::json::array();
66 rules.push_back({{"action", "DENY"}, {"selector", {{"identifier", "docker-image://.*"}, {"match_type", "REGEX"}}}});
67
68 for (const auto& host : allowedHosts)
69 {
70 std::string lowered;
71 lowered.reserve(host.size());
72 std::transform(host.begin(), host.end(), std::back_inserter(lowered), [](unsigned char ch) {
73 return static_cast<char>(std::tolower(ch));
74 });
75
76 const auto identifier = "docker-image://" + EscapeRegexMetacharacters(lowered) + "/.*";
77 rules.push_back({{"action", "ALLOW"}, {"selector", {{"identifier", identifier}, {"match_type", "REGEX"}}}});
78 }
79
80 nlohmann::json document;
81 document["rules"] = std::move(rules);
82 return document.dump();
83 }
84
85 } // namespace
86
87 VmPortAllocation::VmPortAllocation(uint16_t port, int family, int protocol, std::weak_ptr<VmPortReservations> reservations) :
88 m_port(port), m_family(family), m_protocol(protocol), m_reservations(std::move(reservations))
89 {
90 }
91
92 VmPortAllocation::VmPortAllocation(VmPortAllocation&& Other)
93 {
94 *this = std::move(Other);
95 }
96
97 VmPortAllocation& VmPortAllocation::operator=(VmPortAllocation&& Other)
98 {
99 if (this != &Other)
100 {
101 Reset();
102 m_port = Other.m_port;
103 m_family = Other.m_family;
104 m_protocol = Other.m_protocol;
105 m_reservations = Other.m_reservations;
106
107 Other.Release();
108 }
109 return *this;
110 }
111
112 VmPortAllocation::~VmPortAllocation()
113 {
114 Reset();
115 }
116
117 void VmPortAllocation::Reset()
118 {
119 // Release the reservation only if the owning VM (and its table) is still alive. If the VM was torn
120 // down the table is already gone and lock() returns null, so a surviving allocation is a safe no-op.
121 if (auto reservations = m_reservations.lock())
122 {
123 std::lock_guard lock{reservations->Mutex};
124 LOG_HR_IF(E_UNEXPECTED, reservations->Ports.erase(m_port) != 1);
125 }
126
127 Release();
128 }
129
130 void VmPortAllocation::Release()
131 {
132 m_reservations.reset();
133 m_port = 0;
134 m_family = 0;
135 m_protocol = 0;
136 }
137
138 uint16_t VmPortAllocation::Port() const
139 {
140 return m_port;
141 }
142
143 int VmPortAllocation::Family() const
144 {
145 return m_family;
146 }
147
148 int VmPortAllocation::Protocol() const
149 {
150 return m_protocol;
151 }
152
153 VMPortMapping::VMPortMapping(int protocol, int Family, uint16_t Port, const char* Address) : Protocol(protocol)
154 {
155 THROW_HR_IF_MSG(E_INVALIDARG, Protocol != IPPROTO_TCP && Protocol != IPPROTO_UDP, "Invalid protocol: %i", Protocol);
156 THROW_HR_IF(E_POINTER, Address == nullptr);
157 if (Family == AF_INET)
158 {
159 common::wslutil::ParseIpv4Address(Address, BindAddress.Ipv4.sin_addr);
160 BindAddress.Ipv4.sin_port = htons(Port);
161 }
162 else if (Family == AF_INET6)
163 {
164 common::wslutil::ParseIpv6Address(Address, BindAddress.Ipv6.sin6_addr);
165 BindAddress.Ipv6.sin6_port = htons(Port);
166 }
167 else
168 {
169 THROW_HR_MSG(E_INVALIDARG, "Invalid address family: %i", Family);
170 }
171
172 // Must be assigned after parsing is done, since inet_pton writes to the family field as well.
173 BindAddress.si_family = Family;
174 }
175
176 VMPortMapping::~VMPortMapping()
177 {
178 try
179 {
180 Unmap();
181 }
182 CATCH_LOG();
183 }
184
185 VMPortMapping::VMPortMapping(VMPortMapping&& Other)
186 {
187 *this = std::move(Other);
188 }
189
190 void VMPortMapping::AssignVmPort(const std::shared_ptr<VmPortAllocation>& Port)
191 {
192 WI_ASSERT(!VmPort);
193
194 VmPort = Port;
195 }
196
197 void VMPortMapping::Unmap()
198 {
199 if (Vm)
200 {
201 auto clearVm = wil::scope_exit([&] { Vm = nullptr; });
202 Vm->UnmapPort(*this);
203 }
204 }
205
206 void VMPortMapping::Release()
207 {
208 Vm = nullptr;
209 VmPort.reset();
210 }
211
212 bool VMPortMapping::IsLocalhost() const
213 {
214 if (BindAddress.si_family == AF_INET6)
215 {
216 return IN6_IS_ADDR_LOOPBACK(&BindAddress.Ipv6.sin6_addr);
217 }
218 else
219 {
220 return IN4ADDR_ISLOOPBACK(&BindAddress.Ipv4);
221 }
222 }
223
224 bool VMPortMapping::IsIPv6() const
225 {
226 return BindAddress.si_family == AF_INET6;
227 }
228
229 uint16_t VMPortMapping::HostPort() const
230 {
231 if (BindAddress.si_family == AF_INET6)
232 {
233 return ntohs(BindAddress.Ipv6.sin6_port);
234 }
235 else
236 {
237 WI_ASSERT(BindAddress.si_family == AF_INET);
238 return ntohs(BindAddress.Ipv4.sin_port);
239 }
240 }
241
242 void VMPortMapping::SetHostPort(uint16_t port)
243 {
244 if (BindAddress.si_family == AF_INET6)
245 {
246 BindAddress.Ipv6.sin6_port = htons(port);
247 }
248 else
249 {
250 WI_ASSERT(BindAddress.si_family == AF_INET);
251 BindAddress.Ipv4.sin_port = htons(port);
252 }
253 }
254
255 std::string VMPortMapping::BindingAddressString() const
256 {
257 char buffer[INET6_ADDRSTRLEN]{};
258 if (BindAddress.Ipv4.sin_family == AF_INET6)
259 {
260 THROW_LAST_ERROR_IF(inet_ntop(AF_INET6, &BindAddress.Ipv6.sin6_addr, buffer, sizeof(buffer)) == nullptr);
261 }
262 else
263 {
264 THROW_LAST_ERROR_IF(inet_ntop(AF_INET, &BindAddress.Ipv4.sin_addr, buffer, sizeof(buffer)) == nullptr);
265 }
266
267 return buffer;
268 }
269
270 void VMPortMapping::Attach(WSLCVirtualMachine& Vm)
271 {
272 WI_ASSERT(this->Vm == nullptr);
273
274 this->Vm = &Vm;
275 }
276
277 void VMPortMapping::Detach()
278 {
279 WI_ASSERT(Vm != nullptr);
280
281 this->Vm = nullptr;
282 }
283
284 VMPortMapping VMPortMapping::LocalhostTcpMapping(int Family, uint16_t WindowsPort)
285 {
286 WI_ASSERT(Family == AF_INET || Family == AF_INET6);
287
288 return VMPortMapping(IPPROTO_TCP, Family, WindowsPort, Family == AF_INET ? "127.0.0.1" : "::1");
289 }
290
291 VMPortMapping VMPortMapping::FromWSLCPortMapping(const ::WSLCPortMapping& Mapping)
292 {
293 return VMPortMapping(Mapping.Protocol, Mapping.Family, Mapping.HostPort, Mapping.BindingAddress);
294 }
295
296 VMPortMapping VMPortMapping::FromContainerMetaData(const wslc::WSLCPortMapping& Mapping)
297 {
298 return VMPortMapping(Mapping.Protocol, Mapping.Family, Mapping.HostPort, Mapping.BindingAddress.c_str());
299 }
300
301 VMPortMapping& VMPortMapping::operator=(VMPortMapping&& Other)
302 {
303 if (this != &Other)
304 {
305 Unmap();
306 Protocol = Other.Protocol;
307 VmPort = std::move(Other.VmPort);
308 BindAddress = Other.BindAddress;
309 Vm = Other.Vm;
310
311 Other.Protocol = 0;
312 ZeroMemory(&Other.BindAddress, sizeof(Other.BindAddress));
313 Other.Vm = nullptr;
314 }
315 return *this;
316 }
317
318 WSLCVirtualMachine::WSLCVirtualMachine(
319 _In_ IWSLCVirtualMachine* Vm, _In_ const WSLCSessionInitSettings* Settings, _In_ HANDLE SessionTerminatingEvent, _In_ TOnCrashDump&& OnCrashDump) :
320 m_vm(Vm),
321 m_featureFlags(static_cast<WSLCFeatureFlags>(Settings->FeatureFlags)),
322 m_networkingMode(Settings->NetworkingMode),
323 m_bootTimeoutMs(Settings->BootTimeoutMs),
324 m_rootVhdType(Settings->RootVhdTypeOverride ? Settings->RootVhdTypeOverride : "ext4"),
325 m_onCrashDump(std::move(OnCrashDump)),
326 m_sessionTerminatingEvent(SessionTerminatingEvent)
327 {
328 // N.B. The constructor should not run any operation that could throw, so the destructor runs even if the VM fails to boot.
329 }
330
331 void WSLCVirtualMachine::Initialize()
332 {
333 THROW_IF_FAILED(m_vm->GetId(&m_vmId));
334
335 // Create a job object that will terminate child processes (wslrelay.exe)
336 // when the VM is destroyed.
337 m_processJobObject = wsl::windows::common::helpers::CreateKillOnCloseJob();
338
339 // Start crash dump collection thread.
340 auto crashDumpSocket = hvsocket::Listen(m_vmId, LX_INIT_UTILITY_VM_CRASH_DUMP_PORT);
341 THROW_LAST_ERROR_IF(!crashDumpSocket);
342
343 m_crashDumpThread = std::thread{[this, socket = std::move(crashDumpSocket)]() mutable { CollectCrashDumps(std::move(socket)); }};
344
345 // Establish a socket channel with mini_init in the VM.
346 wil::unique_socket socket;
347 THROW_IF_FAILED(m_vm->AcceptConnection(reinterpret_cast<HANDLE*>(&socket)));
348
349 m_initChannel = wsl::shared::SocketChannel{std::move(socket), "mini_init", {m_vmTerminatingEvent.get(), m_sessionTerminatingEvent}};
350
351 // Create a thread to watch for exited processes.
352 auto [__, ___, childChannel] = Fork(WSLC_FORK::Thread);
353 childChannel.SetExitEvents({m_vmTerminatingEvent.get()});
354
355 WSLC_WATCH_PROCESSES watchMessage{};
356 auto watchTransaction = childChannel.StartTransaction(m_initChannelTimeout);
357 watchTransaction.Send(watchMessage);
358
359 THROW_HR_IF(E_FAIL, watchTransaction.Receive<RESULT_MESSAGE<uint32_t>>().Result != 0);
360
361 m_processExitThread = std::thread(std::bind(&WSLCVirtualMachine::WatchForExitedProcesses, this, std::move(childChannel)));
362
363 // Mount VHDs
364 const auto rootDevice = GetVhdDevicePath(0);
365 Mount(m_initChannel, rootDevice.c_str(), "/mnt", m_rootVhdType.c_str(), "ro", WSLC_MOUNT::Chroot | WSLC_MOUNT::OverlayFs);
366
367 const auto modulesDevice = GetVhdDevicePath(1);
368 MountModules(m_initChannel, modulesDevice.c_str());
369
370 // Discover the per-VM guest capabilities (currently the hv_pci swiotlb pool) and forward them
371 // to the service before virtiofs shares or Consomme networking devices are created.
372 ReadGuestCapabilities();
373
374 // Configure GPU mounts if enabled
375 MountGpuLibraries(c_gpuLibrariesPath, c_gpuDriversPath);
376
377 // Snapshot the container-registry allowlist and, if configured, hand the BuildKit source-policy
378 // JSON to init. Done at boot rather than per build so a compromised user process cannot bypass
379 // enforcement by racing the write.
380 ConfigureBuildKitPolicy();
381
382 // Configure networking. This must happen after all filesystems are mounted since /gns needs to access /sys.
383 ConfigureNetworking();
384 }
385
386 WSLCVirtualMachine::~WSLCVirtualMachine()
387 {
388 WSL_LOG("WSLCTerminateVmStart");
389
390 m_vmTerminatingEvent.SetEvent();
391
392 m_initChannel.Close();
393
394 // Terminate the VM.
395 m_vm.reset();
396
397 if (m_processExitThread.joinable())
398 {
399 m_processExitThread.join();
400 }
401
402 if (m_crashDumpThread.joinable())
403 {
404 m_crashDumpThread.join();
405 }
406
407 // Clear the state of all remaining processes now that the VM has exited.
408 for (auto& e : m_trackedProcesses)
409 {
410 if (auto locked = e.lock())
411 {
412 locked->OnVmTerminated();
413 }
414 }
415 }
416
417 void WSLCVirtualMachine::ConfigureNetworking()
418 {
419 if (m_networkingMode == WSLCNetworkingModeNone)
420 {
421 return;
422 }
423
424 // Launch /gns with auto-allocated file descriptors for the GNS channel (and DNS channel if enabled).
425 std::vector<WSLCProcessFd> fds;
426 fds.emplace_back(WSLCProcessFd{.Fd = -1, .Type = WSLCFdType::WSLCFdTypeDefault});
427
428 // Consomme forwards DNS via the host proxy, so the DNS channel and /gns args are only needed for NAT mode.
429 const bool enableDnsTunneling = FeatureEnabled(WslcFeatureFlagsDnsTunneling) && m_networkingMode != WSLCNetworkingModeConsomme;
430 if (enableDnsTunneling)
431 {
432 fds.emplace_back(WSLCProcessFd{.Fd = -1, .Type = WSLCFdType::WSLCFdTypeDefault});
433 }
434
435 // Because the file descriptor numbers aren't known in advance, the command line needs to be generated after the
436 // file descriptors are allocated.
437 std::vector<const char*> cmd{"/gns", LX_INIT_GNS_SOCKET_ARG};
438 std::string gnsSocketFdArg;
439 std::string dnsSocketFdArg;
440 int gnsChannelFd = -1;
441 int dnsChannelFd = -1;
442
443 WSLCProcessOptions options{};
444 auto prepareCommandLine = [&](const auto& sockets) {
445 gnsChannelFd = sockets[0].Fd;
446 gnsSocketFdArg = std::to_string(gnsChannelFd);
447 cmd.push_back(gnsSocketFdArg.c_str());
448
449 if (enableDnsTunneling)
450 {
451 dnsChannelFd = sockets[1].Fd;
452 dnsSocketFdArg = std::to_string(dnsChannelFd);
453 cmd.push_back(LX_INIT_GNS_DNS_SOCKET_ARG);
454 cmd.push_back(dnsSocketFdArg.c_str());
455 cmd.push_back(LX_INIT_GNS_DNS_TUNNELING_IP);
456 cmd.push_back(LX_INIT_DNS_TUNNELING_IP_ADDRESS);
457 }
458
459 options.CommandLine = {.Values = cmd.data(), .Count = static_cast<ULONG>(cmd.size())};
460 };
461
462 auto process = CreateLinuxProcessImpl("/init", options, fds, 0, 0, nullptr, prepareCommandLine);
463
464 // Call back to the service to configure the networking engine.
465 auto gnsHandle = process->GetStdHandle(gnsChannelFd);
466
467 HandleWrapper dnsHandle;
468 HANDLE dnsSocketHandle = nullptr;
469 if (enableDnsTunneling)
470 {
471 dnsHandle = process->GetStdHandle(dnsChannelFd);
472 dnsSocketHandle = dnsHandle.Get();
473 }
474
475 THROW_IF_FAILED(m_vm->ConfigureNetworking(gnsHandle.Get(), enableDnsTunneling ? &dnsSocketHandle : nullptr));
476
477 // Launch port relay for port forwarding
478 LaunchPortRelay();
479 }
480
481 void WSLCVirtualMachine::ReadGuestCapabilities()
482 {
483 WSLC_GET_GUEST_CAPABILITIES message{};
484 const auto& response = m_initChannel.Transaction(message, nullptr, m_initChannelTimeout);
485
486 m_hvPciSwiotlbBase = response.HvPciSwiotlbBase;
487 m_hvPciSwiotlbSize = response.HvPciSwiotlbSize;
488
489 WSL_LOG(
490 "WSLCReadGuestCapabilities",
491 TraceLoggingValue(m_hvPciSwiotlbBase, "HvPciSwiotlbBase"),
492 TraceLoggingValue(m_hvPciSwiotlbSize, "HvPciSwiotlbSize"));
493
494 // Forward the values to the service so AddShare and ConfigureNetworking can configure
495 // wsldevicehost. Passing zero for both means the guest kernel does not support hv_pci swiotlb.
496 WSLCGuestCapabilities capabilities{};
497 capabilities.HvPciSwiotlbBase = m_hvPciSwiotlbBase;
498 capabilities.HvPciSwiotlbSize = m_hvPciSwiotlbSize;
499 THROW_IF_FAILED(m_vm->ApplyGuestCapabilities(&capabilities));
500 }
501
502 void WSLCVirtualMachine::ConfigureBuildKitPolicy()
503 {
504 const auto snapshot = wsl::windows::policies::ReadRegistryAllowlistSnapshotFromPoliciesRoot();
505
506 if (snapshot.State == wsl::windows::policies::RegistryAllowlistState::NotConfigured)
507 {
508 m_buildKitPolicyState = BuildKitPolicyState::NotConfigured;
509 return;
510 }
511
512 std::vector<std::string> hosts;
513 hosts.reserve(snapshot.Hosts.size());
514 std::ranges::transform(snapshot.Hosts, std::back_inserter(hosts), [](const std::wstring& host) {
515 return wsl::shared::string::WideToMultiByte(host);
516 });
517
518 const auto policyJson = BuildBuildKitSourcePolicyJson(hosts);
519
520 // Linux <fcntl.h> flags for open().
521 constexpr int c_lxOWriteOnly = 0x1;
522 constexpr int c_lxOCreate = 0x40;
523 constexpr int c_lxOTruncate = 0x200;
524 constexpr int c_lxOCloseOnExec = 0x80000;
525 constexpr int c_lxONoFollow = 0x20000;
526
527 auto message = wsl::shared::MessageWriter<WSLC_WRITE_FILE>{};
528 message.WriteString(message->PathIndex, c_buildKitPolicyPath);
529 message->ContentLength = static_cast<unsigned int>(policyJson.size());
530 gsl::copy(
531 gsl::as_bytes(gsl::make_span(policyJson.data(), policyJson.size())),
532 message.InsertBuffer(message->ContentIndex, policyJson.size()));
533 message->OpenFlags = c_lxOWriteOnly | c_lxOCreate | c_lxOTruncate | c_lxOCloseOnExec | c_lxONoFollow;
534 message->Permissions = 0644;
535
536 const auto& response = m_initChannel.Transaction<WSLC_WRITE_FILE>(message.Span(), nullptr, m_initChannelTimeout);
537 THROW_HR_IF_MSG(E_FAIL, response.Result != 0, "Guest failed to write %hs: %d", c_buildKitPolicyPath, response.Result);
538
539 m_buildKitPolicyState = BuildKitPolicyState::Configured;
540 }
541
542 bool WSLCVirtualMachine::FeatureEnabled(WSLCFeatureFlags Value) const
543 {
544 return static_cast<ULONG>(m_featureFlags) & static_cast<ULONG>(Value);
545 }
546
547 WSLCNetworkingMode WSLCVirtualMachine::NetworkingMode() const
548 {
549 return m_networkingMode;
550 }
551
552 bool WSLCVirtualMachine::UseWslRelayPortForwarding() const
553 {
554 return m_networkingMode == WSLCNetworkingModeNAT ||
555 (m_networkingMode == WSLCNetworkingModeConsomme && FeatureEnabled(WslcFeatureFlagsPortRelayWslRelay));
556 }
557
558 void WSLCVirtualMachine::WatchForExitedProcesses(wsl::shared::SocketChannel& Channel)
559 try
560 {
561 // TODO: Terminate the VM if this thread exits unexpectedly.
562 while (true)
563 {
564 auto [message, _] = Channel.ReceiveMessageOrClosed<WSLC_PROCESS_EXITED>();
565 if (message == nullptr)
566 {
567 break; // Channel has been closed, exit
568 }
569
570 WSL_LOG(
571 "ProcessExited",
572 TraceLoggingValue(message->Pid, "Pid"),
573 TraceLoggingValue(message->Code, "Code"),
574 TraceLoggingValue(message->Signaled, "Signaled"));
575
576 // Signal the exited process, if it's been monitored.
577 // N.B. Lock weak_ptr under lock, then call OnExited outside it to avoid
578 // deadlock with the destructor's m_lock -> m_trackedProcessesLock ordering.
579 std::shared_ptr<VMProcessControl> exited;
580 {
581 std::lock_guard lock{m_trackedProcessesLock};
582
583 for (auto& e : m_trackedProcesses)
584 {
585 auto locked = e.lock();
586 if (locked && locked->GetPid() == message->Pid)
587 {
588 exited = std::move(locked);
589 break;
590 }
591 }
592 }
593
594 if (exited)
595 {
596 try
597 {
598 exited->OnExited(message->Signaled ? 128 + message->Code : message->Code);
599 }
600 CATCH_LOG();
601 }
602 }
603 }
604 CATCH_LOG();
605
606 std::pair<ULONG, std::string> WSLCVirtualMachine::AttachDisk(_In_ PCWSTR Path, _In_ BOOL ReadOnly)
607 {
608 std::lock_guard lock{m_lock};
609
610 ULONG Lun{};
611 std::string Device;
612
613 // Delegate to IWSLCVirtualMachine for the privileged HCS operation
614 THROW_IF_FAILED(m_vm->AttachDisk(Path, ReadOnly, &Lun));
615
616 // Detach on failure so the service-side state stays consistent.
617 auto detachOnFailure = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(m_vm->DetachDisk(Lun)); });
618
619 // Query the guest for the device path
620 Device = GetVhdDevicePath(Lun);
621
622 WSL_LOG(
623 "WSLCAttachDisk",
624 TraceLoggingValue(Path, "Path"),
625 TraceLoggingValue(ReadOnly, "ReadOnly"),
626 TraceLoggingValue(Device.c_str(), "Device"),
627 TraceLoggingValue(Lun, "Lun"));
628
629 m_attachedDisks.emplace(Lun, AttachedDisk{Path, Device});
630
631 detachOnFailure.release();
632
633 return {Lun, Device};
634 }
635
636 void WSLCVirtualMachine::Ext4Format(const std::string& Device, std::optional<uint32_t> Uid, std::optional<uint32_t> Gid)
637 {
638 constexpr auto mkfsPath = "/usr/sbin/mkfs.ext4";
639
640 // Uid/Gid must be paired; the named-volume parser enforces this for user
641 // input — this guards future internal callers that bypass it.
642 THROW_HR_IF(E_UNEXPECTED, Uid.has_value() != Gid.has_value());
643
644 std::vector<std::string> args = {mkfsPath};
645 std::string rootOwner;
646 if (Uid.has_value() && Gid.has_value())
647 {
648 rootOwner = std::format("root_owner={}:{}", *Uid, *Gid);
649 args.push_back("-E");
650 args.push_back(rootOwner);
651 }
652 args.push_back(Device);
653
654 ServiceProcessLauncher launcher(mkfsPath, args);
655 auto result = launcher.Launch(*this).WaitAndCaptureOutput();
656
657 THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str());
658 }
659
660 void WSLCVirtualMachine::RemoveDirectory(const std::string& Path)
661 {
662 // rmdir only removes an empty directory, so callers can rely on it to leave
663 // a non-empty directory untouched.
664 constexpr auto rmdirPath = "/bin/rmdir";
665
666 std::vector<std::string> args = {rmdirPath, Path};
667
668 ServiceProcessLauncher launcher(rmdirPath, args);
669 auto result = launcher.Launch(*this).WaitAndCaptureOutput();
670
671 THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str());
672 }
673
674 std::vector<std::string> WSLCVirtualMachine::ListDirectory(const std::string& Path)
675 {
676 wsl::shared::MessageWriter<WSLC_LISTDIR> message;
677 message.WriteString(Path);
678
679 gsl::span<gsl::byte> responseSpan;
680 const auto& response = m_initChannel.Transaction<WSLC_LISTDIR>(message.Span(), &responseSpan, m_initChannelTimeout);
681
682 THROW_HR_IF_MSG(E_FAIL, response.Result != 0, "Failed to list directory '%hs', init returned: %d", Path.c_str(), response.Result);
683
684 return wsl::shared::string::ArrayFromSpan(responseSpan, response.EntriesIndex);
685 }
686
687 void WSLCVirtualMachine::Unmount(_In_ const char* Path)
688 {
689 auto [pid, _, subChannel] = Fork(WSLC_FORK::Thread);
690
691 wsl::shared::MessageWriter<WSLC_UNMOUNT> message;
692 message.WriteString(Path);
693
694 const auto& response = subChannel.Transaction<WSLC_UNMOUNT>(message.Span(), nullptr, m_initChannelTimeout);
695
696 // TODO: Return errno to caller
697 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), response.Result == EINVAL);
698 THROW_HR_IF(E_FAIL, response.Result != 0);
699 }
700
701 void WSLCVirtualMachine::DetachDisk(_In_ ULONG Lun)
702 {
703 std::lock_guard lock{m_lock};
704
705 // Find the disk
706 auto it = m_attachedDisks.find(Lun);
707 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_attachedDisks.end());
708
709 // Detach it from the guest
710 WSLC_DETACH message;
711 message.Lun = Lun;
712 const auto& response = m_initChannel.Transaction(message, nullptr, m_initChannelTimeout);
713
714 // TODO: Return errno to caller
715 THROW_HR_IF(E_FAIL, response.Result != 0);
716
717 // Remove it from the VM
718 THROW_IF_FAILED(m_vm->DetachDisk(Lun));
719
720 m_attachedDisks.erase(it);
721 }
722
723 std::tuple<int32_t, int32_t, wsl::shared::SocketChannel> WSLCVirtualMachine::Fork(enum WSLC_FORK::ForkType Type)
724 {
725 std::lock_guard lock{m_lock};
726 return Fork(m_initChannel, Type);
727 }
728
729 std::tuple<int32_t, int32_t, wsl::shared::SocketChannel> WSLCVirtualMachine::Fork(
730 wsl::shared::SocketChannel& Channel, enum WSLC_FORK::ForkType Type, ULONG TtyRows, ULONG TtyColumns)
731 {
732 uint32_t port{};
733 int32_t pid{};
734 int32_t ptyMaster{};
735 {
736 WSLC_FORK message;
737 message.ForkType = Type;
738 message.TtyColumns = static_cast<uint16_t>(TtyColumns);
739 message.TtyRows = static_cast<uint16_t>(TtyRows);
740 const auto& response = Channel.Transaction(message, nullptr, m_initChannelTimeout);
741 port = response.Port;
742 pid = response.Pid;
743 ptyMaster = response.PtyMasterFd;
744 }
745
746 THROW_HR_IF_MSG(E_FAIL, pid <= 0, "fork() returned %i", pid);
747
748 auto socket = wsl::windows::common::hvsocket::Connect(m_vmId, port, m_vmTerminatingEvent.get(), m_initChannelTimeout);
749
750 return std::make_tuple(
751 pid, ptyMaster, wsl::shared::SocketChannel{std::move(socket), std::to_string(pid), std::vector<HANDLE>(Channel.GetExitEvents())});
752 }
753
754 WSLCVirtualMachine::ConnectedSocket WSLCVirtualMachine::ConnectSocket(wsl::shared::SocketChannel& Channel, int32_t Fd)
755 {
756 WSLC_ACCEPT message{};
757 message.Fd = Fd;
758
759 auto transaction = Channel.StartTransaction(m_initChannelTimeout);
760 transaction.Send(message);
761 const auto& response = transaction.Receive<WSLC_ACCEPT::TResponse>();
762
763 ConnectedSocket socket;
764 socket.Socket = wsl::windows::common::hvsocket::Connect(m_vmId, response.Result, m_vmTerminatingEvent.get(), m_initChannelTimeout);
765
766 // If the FD was unspecified, read the Linux file descriptor from the guest.
767 if (Fd == -1)
768 {
769 socket.Fd = transaction.Receive<RESULT_MESSAGE<int32_t>>().Result;
770 }
771 else
772 {
773 socket.Fd = Fd;
774 }
775
776 return socket;
777 }
778
779 std::string WSLCVirtualMachine::GetVhdDevicePath(ULONG Lun)
780 {
781 WSLC_GET_DISK message{};
782 message.Header.MessageSize = sizeof(message);
783 message.Header.MessageType = WSLC_GET_DISK::Type;
784 message.ScsiLun = Lun;
785 const auto& response = m_initChannel.Transaction(message, nullptr, m_initChannelTimeout);
786 THROW_HR_IF_MSG(E_FAIL, response.Result != 0, "Failed to get disk path, init returned: %lu", response.Result);
787
788 return response.Buffer;
789 }
790
791 Microsoft::WRL::ComPtr<WSLCProcess> WSLCVirtualMachine::CreateLinuxProcess(
792 _In_ LPCSTR Executable, _In_ const WSLCProcessOptions& Options, ULONG TtyRows, ULONG TtyColumns, int* Errno, const TPrepareCommandLine& PrepareCommandLine)
793 {
794 // Check if this is a tty or not
795 std::vector<WSLCProcessFd> fds;
796 if (WI_IsFlagSet(Options.Flags, WSLCProcessFlagsTty))
797 {
798 fds.emplace_back(WSLCProcessFd{.Fd = WSLCFDTty, .Type = WSLCFdType::WSLCFdTypeTty});
799 fds.emplace_back(WSLCProcessFd{.Fd = 0, .Type = WSLCFdType::WSLCFdTypeTtyControl});
800 }
801 else
802 {
803 if (WI_IsFlagSet(Options.Flags, WSLCProcessFlagsStdin))
804 {
805 fds.emplace_back(WSLCProcessFd{.Fd = WSLCFDStdin, .Type = WSLCFdType::WSLCFdTypeDefault});
806 }
807
808 fds.emplace_back(WSLCProcessFd{.Fd = WSLCFDStdout, .Type = WSLCFdType::WSLCFdTypeDefault});
809 fds.emplace_back(WSLCProcessFd{.Fd = WSLCFDStderr, .Type = WSLCFdType::WSLCFdTypeDefault});
810 }
811
812 return CreateLinuxProcessImpl(Executable, Options, fds, TtyRows, TtyColumns, Errno, PrepareCommandLine);
813 }
814
815 Microsoft::WRL::ComPtr<WSLCProcess> WSLCVirtualMachine::CreateLinuxProcessImpl(
816 LPCSTR Executable, const WSLCProcessOptions& Options, const std::vector<WSLCProcessFd>& Fds, ULONG TtyRows, ULONG TtyColumns, int* Errno, const TPrepareCommandLine& PrepareCommandLine)
817 {
818 // N.B This check is there to prevent processes from being started before the VM is done initializing.
819 // to avoid potential deadlocks, since the processExitThread is required to signal the process exit events.
820 // std::thread::joinable() is const, so this can be called without acquiring the lock.
821 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_processExitThread.joinable());
822
823 THROW_WIN32_IF_MSG(
824 ERROR_NOT_SUPPORTED, Options.User != nullptr, "Custom users are not supported for root namespace processes");
825
826 auto setErrno = [Errno](int Error) {
827 if (Errno != nullptr)
828 {
829 *Errno = Error;
830 }
831 };
832
833 // Check if this is a tty or not
834 const WSLCProcessFd* tty = nullptr;
835 auto [pid, _, childChannel] = Fork(WSLC_FORK::Process);
836
837 std::vector<WSLCVirtualMachine::ConnectedSocket> sockets;
838 ConnectedSocket ttyControlhandle;
839 for (const auto& e : Fds)
840 {
841
842 if (e.Type == WSLCFdTypeTtyControl)
843 {
844 THROW_HR_IF_MSG(E_INVALIDARG, ttyControlhandle.Fd != -1, "Multiple terminal control fds specified");
845
846 ttyControlhandle = ConnectSocket(childChannel, e.Fd);
847 }
848 else
849 {
850 if (e.Type == WSLCFdTypeTty)
851 {
852 THROW_HR_IF_MSG(E_INVALIDARG, tty != nullptr, "Multiple terminal fds specified");
853 tty = &e;
854 }
855
856 sockets.emplace_back(ConnectSocket(childChannel, e.Fd));
857 }
858 }
859
860 PrepareCommandLine(sockets);
861
862 wsl::shared::MessageWriter<WSLC_EXEC> Message;
863
864 Message.WriteString(Message->ExecutableIndex, Executable);
865 Message.WriteString(Message->CurrentDirectoryIndex, Options.CurrentDirectory ? Options.CurrentDirectory : "/");
866 Message.WriteStringArray(Message->CommandLineIndex, Options.CommandLine.Values, Options.CommandLine.Count);
867 Message.WriteStringArray(Message->EnvironmentIndex, Options.Environment.Values, Options.Environment.Count);
868
869 // N.B. The process control needs to be registered before the actual exec message is sent. Otherwise, if the process exits quickly, we might receive the exit notification before registering it.
870 std::shared_ptr<VMProcessControl> control;
871 auto registerProcess = [&](int processPid) {
872 control = std::make_shared<VMProcessControl>(*this, processPid, std::move(ttyControlhandle.Socket));
873 {
874 std::lock_guard lock{m_trackedProcessesLock};
875 m_trackedProcesses.emplace_back(control);
876 }
877 };
878
879 // If this is an interactive tty, we need a relay process
880 if (tty != nullptr)
881 {
882 auto [grandChildPid, ptyMaster, grandChildChannel] = Fork(childChannel, WSLC_FORK::Pty, TtyRows, TtyColumns);
883 WSLC_TTY_RELAY relayMessage{};
884 relayMessage.TtyMaster = ptyMaster;
885 relayMessage.Socket = tty->Fd;
886 relayMessage.TtyControl = ttyControlhandle.Fd; // N.B. Fd is set to -1 if unset.
887 {
888 auto relayTransaction = childChannel.StartTransaction(m_initChannelTimeout);
889 relayTransaction.Send(relayMessage);
890 }
891
892 auto result = ExpectClosedChannelOrError(childChannel);
893 if (result != 0)
894 {
895 setErrno(result);
896 THROW_HR_MSG(E_FAIL, "errno: %i", result);
897 }
898
899 registerProcess(grandChildPid);
900
901 {
902 auto execTransaction = grandChildChannel.StartTransaction(m_initChannelTimeout);
903 execTransaction.Send<WSLC_EXEC>(Message.Span());
904 auto [execResponse, execSpan] = execTransaction.ReceiveOrClosed<RESULT_MESSAGE<int32_t>>();
905 result = execResponse != nullptr ? execResponse->Result : 0;
906 }
907 if (result != 0)
908 {
909 setErrno(result);
910 THROW_HR_MSG(E_FAIL, "errno: %i", result);
911 }
912
913 pid = grandChildPid;
914 }
915 else
916 {
917 registerProcess(pid);
918
919 auto execTransaction = childChannel.StartTransaction(m_initChannelTimeout);
920 execTransaction.Send<WSLC_EXEC>(Message.Span());
921 auto [execResponse, execSpan] = execTransaction.ReceiveOrClosed<RESULT_MESSAGE<int32_t>>();
922 auto result = execResponse != nullptr ? execResponse->Result : 0;
923 if (result != 0)
924 {
925 setErrno(result);
926 THROW_HR_MSG(E_FAIL, "errno: %i", result);
927 }
928 }
929
930 std::map<ULONG, TypedHandle> stdHandles;
931 for (auto& [fd, handle] : sockets)
932 {
933 stdHandles.emplace(fd, TypedHandle{std::move(handle), WSLCHandleTypeSocket});
934 }
935
936 auto io = std::make_unique<VMProcessIO>(std::move(stdHandles));
937
938 auto process = wil::MakeOrThrow<WSLCProcess>(std::move(control), std::move(io), Options.Flags);
939
940 setErrno(0);
941
942 return process;
943 }
944
945 void WSLCVirtualMachine::Mount(LPCSTR Source, LPCSTR Target, LPCSTR Type, LPCSTR Options, ULONG Flags)
946 {
947 std::lock_guard lock{m_lock};
948
949 Mount(m_initChannel, Source, Target, Type, Options, Flags);
950 }
951
952 void WSLCVirtualMachine::Mount(shared::SocketChannel& Channel, LPCSTR Source, LPCSTR Target, LPCSTR Type, LPCSTR Options, ULONG Flags)
953 {
954 static_assert(WSLCMountFlagsNone == WSLC_MOUNT::None);
955 static_assert(WSLCMountFlagsReadOnly == WSLC_MOUNT::ReadOnly);
956 static_assert(WSLCMountFlagsChroot == WSLC_MOUNT::Chroot);
957 static_assert(WSLCMountFlagsWriteableOverlayFs == WSLC_MOUNT::OverlayFs);
958
959 wsl::shared::MessageWriter<WSLC_MOUNT> message;
960
961 auto optionalAdd = [&](auto value, unsigned int& index) {
962 if (value != nullptr)
963 {
964 message.WriteString(index, value);
965 }
966 };
967
968 optionalAdd(Source, message->SourceIndex);
969 optionalAdd(Target, message->DestinationIndex);
970 optionalAdd(Type, message->TypeIndex);
971 optionalAdd(Options, message->OptionsIndex);
972 message->Flags = Flags;
973
974 const auto& response = Channel.Transaction<WSLC_MOUNT>(message.Span());
975
976 WSL_LOG(
977 "WSLCMount",
978 TraceLoggingValue(Source == nullptr ? "<null>" : Source, "Source"),
979 TraceLoggingValue(Target == nullptr ? "<null>" : Target, "Target"),
980 TraceLoggingValue(Type == nullptr ? "<null>" : Type, "Type"),
981 TraceLoggingValue(Options == nullptr ? "<null>" : Options, "Options"),
982 TraceLoggingValue(Flags, "Flags"),
983 TraceLoggingValue(response.Result, "Result"));
984
985 THROW_HR_IF(E_FAIL, response.Result != 0);
986 }
987
988 void WSLCVirtualMachine::MountModules(shared::SocketChannel& Channel, LPCSTR Source)
989 {
990 wsl::shared::MessageWriter<WSLC_MOUNT_MODULES> message;
991 message.WriteString(message->SourceIndex, Source);
992
993 const auto& response = Channel.Transaction<WSLC_MOUNT_MODULES>(message.Span());
994
995 WSL_LOG("WSLCMountModules", TraceLoggingValue(Source, "Source"), TraceLoggingValue(response.Result, "Result"));
996
997 THROW_HR_IF(E_FAIL, response.Result != 0);
998 }
999
1000 void WSLCVirtualMachine::MountVirtioFsChild(shared::SocketChannel& Channel, LPCSTR Source, LPCSTR ChildName, LPCSTR Target, LPCSTR Options, ULONG Flags)
1001 {
1002 wsl::shared::MessageWriter<WSLC_MOUNT_VIRTIOFS> message;
1003 message.WriteString(message->SourceIndex, Source);
1004 message.WriteString(message->ChildNameIndex, ChildName);
1005 message.WriteString(message->DestinationIndex, Target);
1006 message.WriteString(message->TypeIndex, "virtiofs");
1007 message.WriteString(message->OptionsIndex, Options);
1008 message->Flags = Flags;
1009
1010 const auto& response = Channel.Transaction<WSLC_MOUNT_VIRTIOFS>(message.Span());
1011
1012 WSL_LOG(
1013 "WSLCMountVirtioFsChild",
1014 TraceLoggingValue(Source, "Source"),
1015 TraceLoggingValue(ChildName, "ChildName"),
1016 TraceLoggingValue(Target, "Target"),
1017 TraceLoggingValue(Options, "Options"),
1018 TraceLoggingValue(Flags, "Flags"),
1019 TraceLoggingValue(response.Result, "Result"));
1020
1021 THROW_HR_IF(E_FAIL, response.Result != 0);
1022 }
1023
1024 int32_t WSLCVirtualMachine::ExpectClosedChannelOrError(wsl::shared::SocketChannel& Channel)
1025 {
1026 auto [response, span] = Channel.ReceiveMessageOrClosed<RESULT_MESSAGE<int32_t>>();
1027 if (response != nullptr)
1028 {
1029 return response->Result;
1030 }
1031 else
1032 {
1033 return 0;
1034 }
1035 }
1036
1037 void WSLCVirtualMachine::Signal(_In_ LONG Pid, _In_ int Signal)
1038 {
1039 std::lock_guard lock(m_lock);
1040
1041 WSLC_SIGNAL message;
1042 message.Pid = Pid;
1043 message.Signal = Signal;
1044 const auto& response = m_initChannel.Transaction(message, nullptr, m_initChannelTimeout);
1045
1046 THROW_HR_IF(E_FAIL, response.Result != 0);
1047 }
1048
1049 void WSLCVirtualMachine::LaunchPortRelay()
1050 {
1051 WI_ASSERT(!m_portRelayChannelRead);
1052
1053 auto [_, __, channel] = Fork(WSLC_FORK::ForkType::Process);
1054
1055 std::lock_guard lock(m_portRelaylock);
1056 auto relayPort = channel.Transaction<WSLC_PORT_RELAY>();
1057
1058 wil::unique_handle readPipe;
1059 wil::unique_handle writePipe;
1060 THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&readPipe, &m_portRelayChannelWrite, nullptr, 0));
1061 THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&m_portRelayChannelRead, &writePipe, nullptr, 0));
1062
1063 // TODO: move the port relay infra into this process. Create a thread, pass handle ownership to thread, refactor and remove
1064 // wslrelaymode. wsl::windows::wslrelay::localhost::RunWSLCPortRelay(
1065 // readPipe.release(), writePipe.release(), m_vmId, relayPort.Result, m_vmTerminatingEvent.get());
1066
1067 wsl::windows::common::helpers::SetHandleInheritable(readPipe.get());
1068 wsl::windows::common::helpers::SetHandleInheritable(writePipe.get());
1069 wsl::windows::common::helpers::SetHandleInheritable(m_vmTerminatingEvent.get());
1070
1071 auto path = wsl::windows::common::wslutil::GetBasePath() / L"wslrelay.exe";
1072
1073 auto cmd = std::format(
1074 L"\"{}\" {} {} {} {} {} {} {} {}",
1075 path,
1076 wslrelay::mode_option,
1077 static_cast<int>(wslrelay::RelayMode::WSLCPortRelay),
1078 wslrelay::exit_event_option,
1079 HandleToUlong(m_vmTerminatingEvent.get()),
1080 wslrelay::port_option,
1081 relayPort.Result,
1082 wslrelay::vm_id_option,
1083 m_vmId);
1084
1085 WSL_LOG("LaunchWslRelay", TraceLoggingValue(cmd.c_str(), "cmd"));
1086
1087 wsl::windows::common::SubProcess process{nullptr, cmd.c_str()};
1088 process.SetStdHandles(readPipe.get(), writePipe.get(), nullptr);
1089 process.InheritHandle(m_vmTerminatingEvent.get());
1090 process.SetJobObject(m_processJobObject.get());
1091 process.Start();
1092
1093 readPipe.release();
1094 writePipe.release();
1095 }
1096
1097 void WSLCVirtualMachine::MapRelayPort(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort, _In_ bool Remove)
1098 {
1099 std::lock_guard lock(m_portRelaylock);
1100
1101 THROW_HR_IF(E_ILLEGAL_STATE_CHANGE, !m_portRelayChannelWrite);
1102
1103 WSLC_MAP_PORT message;
1104 message.WindowsPort = WindowsPort;
1105 message.LinuxPort = LinuxPort;
1106 message.AddressFamily = Family;
1107 message.Stop = Remove;
1108
1109 DWORD bytesTransfered{};
1110 THROW_IF_WIN32_BOOL_FALSE(WriteFile(m_portRelayChannelWrite.get(), &message, sizeof(message), &bytesTransfered, nullptr));
1111 THROW_HR_IF_MSG(E_UNEXPECTED, bytesTransfered != sizeof(message), "%u bytes transfered", bytesTransfered);
1112
1113 HRESULT result = E_UNEXPECTED;
1114 THROW_IF_WIN32_BOOL_FALSE(ReadFile(m_portRelayChannelRead.get(), &result, sizeof(result), &bytesTransfered, nullptr));
1115
1116 THROW_HR_IF(E_UNEXPECTED, bytesTransfered != sizeof(result));
1117 THROW_IF_FAILED_MSG(result, "Failed to map port: WindowsPort=%d, LinuxPort=%d, Family=%d, Remove=%d", WindowsPort, LinuxPort, Family, Remove);
1118 }
1119
1120 void WSLCVirtualMachine::MapPort(VMPortMapping& Mapping)
1121 {
1122 THROW_HR_IF_MSG(E_INVALIDARG, !Mapping.VmPort, "Can't map a VM port without an allocated port");
1123
1124 if (m_networkingMode == WSLCNetworkingModeNone)
1125 {
1126 THROW_HR_MSG(E_ILLEGAL_STATE_CHANGE, "Port mapping is not supported with the current networking mode");
1127 }
1128 else if (UseWslRelayPortForwarding())
1129 {
1130 THROW_HR_IF_MSG(
1131 HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
1132 !Mapping.IsLocalhost() || Mapping.Protocol != IPPROTO_TCP,
1133 "Unsupported port mapping for the wslrelay port relay: %hs, protocol: %i",
1134 Mapping.BindingAddressString().c_str(),
1135 Mapping.Protocol);
1136
1137 MapRelayPort(Mapping.BindAddress.si_family, Mapping.HostPort(), Mapping.VmPort->Port(), false);
1138 }
1139 else if (m_networkingMode == WSLCNetworkingModeConsomme)
1140 {
1141 USHORT allocatedHostPort = 0;
1142 auto result = m_vm->MapVirtioNetPort(
1143 Mapping.HostPort(), Mapping.VmPort->Port(), Mapping.Protocol, Mapping.BindingAddressString().c_str(), &allocatedHostPort);
1144
1145 if (FAILED(result))
1146 {
1147 auto portString = std::format(
1148 "{}:{}/{}",
1149 Mapping.IsIPv6() ? std::format("[{}]", Mapping.BindingAddressString()) : Mapping.BindingAddressString(),
1150 Mapping.HostPort(),
1151 Mapping.Protocol == IPPROTO_TCP ? "tcp" : "udp");
1152
1153 THROW_HR_WITH_USER_ERROR(result, shared::Localization::MessageFailedToMapPort(portString, common::wslutil::GetErrorString(result)));
1154 }
1155
1156 // For anonymous binds, write back the allocated host port.
1157 if (Mapping.HostPort() == WSLC_EPHEMERAL_PORT)
1158 {
1159 WSL_LOG(
1160 "AllocatedHostPort",
1161 TraceLoggingValue(allocatedHostPort, "HostPort"),
1162 TraceLoggingValue(Mapping.VmPort->Port(), "GuestPort"));
1163 Mapping.SetHostPort(allocatedHostPort);
1164 }
1165 }
1166 else
1167 {
1168 THROW_HR_MSG(E_UNEXPECTED, "Unexpected networking mode: %i", m_networkingMode);
1169 }
1170
1171 Mapping.Attach(*this);
1172 }
1173
1174 void WSLCVirtualMachine::UnmapPort(VMPortMapping& Mapping)
1175 {
1176 THROW_HR_IF_MSG(E_INVALIDARG, !Mapping.VmPort, "Can't unmap a VM port without an allocated port");
1177
1178 if (m_networkingMode == WSLCNetworkingModeNone)
1179 {
1180 THROW_HR_MSG(E_ILLEGAL_STATE_CHANGE, "Port mapping is not supported with the current networking mode");
1181 }
1182 else if (UseWslRelayPortForwarding())
1183 {
1184 MapRelayPort(Mapping.BindAddress.si_family, Mapping.HostPort(), Mapping.VmPort->Port(), true);
1185 }
1186 else if (m_networkingMode == WSLCNetworkingModeConsomme)
1187 {
1188 THROW_IF_FAILED(m_vm->UnmapVirtioNetPort(
1189 Mapping.HostPort(), Mapping.VmPort->Port(), Mapping.Protocol, Mapping.BindingAddressString().c_str()));
1190 }
1191 else
1192 {
1193 THROW_HR_MSG(E_UNEXPECTED, "Unexpected networking mode: %i", m_networkingMode);
1194 }
1195
1196 Mapping.Detach();
1197 }
1198
1199 HRESULT WSLCVirtualMachine::MountWindowsFolder(_In_ LPCWSTR WindowsPath, _In_ LPCSTR LinuxPath, _In_ BOOL ReadOnly)
1200 {
1201 return MountWindowsFolderImpl(WindowsPath, LinuxPath, ReadOnly ? WSLCMountFlagsReadOnly : WSLCMountFlagsNone);
1202 }
1203
1204 HRESULT WSLCVirtualMachine::MountWindowsFolderImpl(_In_ LPCWSTR WindowsPath, _In_ LPCSTR LinuxPath, _In_ WSLCMountFlags Flags)
1205 try
1206 {
1207 std::filesystem::path path(WindowsPath);
1208 THROW_HR_IF_MSG(E_INVALIDARG, !path.is_absolute(), "Path is not absolute: '%ls'", WindowsPath);
1209 THROW_HR_IF_MSG(
1210 HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), !std::filesystem::is_directory(path), "Path is not a directory: '%ls'", WindowsPath);
1211
1212 THROW_HR_IF_MSG(E_INVALIDARG, LinuxPath[0] != '/', "Mountpoint is not absolute: '%hs'", LinuxPath);
1213
1214 const bool readOnly = WI_IsFlagSet(Flags, WSLCMountFlagsReadOnly);
1215 GUID shareGuid{};
1216
1217 {
1218 std::lock_guard lock(m_lock);
1219
1220 // Verify that this folder isn't already mounted.
1221 auto it = m_mountedWindowsFolders.find(LinuxPath);
1222 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), it != m_mountedWindowsFolders.end());
1223
1224 // Delegate to IWSLCVirtualMachine for the privileged share creation.
1225 THROW_IF_FAILED(m_vm->AddShare(WindowsPath, readOnly, &shareGuid));
1226
1227 m_mountedWindowsFolders.emplace(LinuxPath, shareGuid);
1228 }
1229
1230 auto deleteOnFailure = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1231 std::lock_guard lock(m_lock);
1232 auto mountIt = m_mountedWindowsFolders.find(LinuxPath);
1233 if (WI_VERIFY(mountIt != m_mountedWindowsFolders.end()))
1234 {
1235 m_mountedWindowsFolders.erase(mountIt);
1236 LOG_IF_FAILED(m_vm->RemoveShare(shareGuid));
1237 }
1238 });
1239
1240 // Create the guest mount
1241 auto shareName = shared::string::GuidToString<char>(shareGuid, shared::string::None);
1242 if (!FeatureEnabled(WslcFeatureFlagsVirtioFs))
1243 {
1244 auto [_, __, channel] = Fork(WSLC_FORK::Process);
1245
1246 WSLC_CONNECT message;
1247 message.HostPort = LX_INIT_UTILITY_VM_PLAN9_PORT;
1248
1249 auto fd = channel.Transaction(message).Result;
1250 THROW_HR_IF_MSG(E_FAIL, fd < 0, "WSLC_CONNECT failed with %i", fd);
1251
1252 auto mountOptions = std::format(
1253 "{},msize={},trans=fd,rfdno={},wfdno={},aname={},cache=mmap", readOnly ? "ro" : "rw", LX_INIT_UTILITY_VM_PLAN9_BUFFER_SIZE, fd, fd, shareName);
1254
1255 Mount(channel, shareName.c_str(), LinuxPath, "9p", mountOptions.c_str(), Flags);
1256 }
1257 else
1258 {
1259 std::string options = readOnly ? "ro" : "rw";
1260 MountVirtioFsChild(m_initChannel, LX_INIT_DRVFS_VIRTIO_TAG, shareName.c_str(), LinuxPath, options.c_str(), Flags);
1261 }
1262
1263 deleteOnFailure.release();
1264
1265 return S_OK;
1266 }
1267 CATCH_RETURN();
1268
1269 HRESULT WSLCVirtualMachine::UnmountWindowsFolder(_In_ LPCSTR LinuxPath)
1270 try
1271 {
1272 std::lock_guard lock(m_lock);
1273
1274 // Verify that this folder is mounted.
1275 auto it = m_mountedWindowsFolders.find(LinuxPath);
1276 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_mountedWindowsFolders.end());
1277
1278 // Unmount the folder from the guest.
1279 auto result = wil::ResultFromException([&]() { Unmount(LinuxPath); });
1280 THROW_HR_IF(result, FAILED(result) && result != HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
1281
1282 auto shareId = it->second;
1283
1284 // Delegate to IWSLCVirtualMachine for the privileged share removal.
1285 THROW_IF_FAILED(m_vm->RemoveShare(shareId));
1286
1287 m_mountedWindowsFolders.erase(it);
1288
1289 return S_OK;
1290 }
1291 CATCH_RETURN();
1292
1293 void WSLCVirtualMachine::MountGpuLibraries(_In_ LPCSTR LibrariesMountPoint, _In_ LPCSTR DriversMountpoint)
1294 {
1295 if (!FeatureEnabled(WslcFeatureFlagsGPU))
1296 {
1297 return;
1298 }
1299
1300 auto windowsPath = wil::GetWindowsDirectoryW<std::wstring>();
1301
1302 // Mount drivers.
1303 THROW_IF_FAILED(MountWindowsFolderImpl(
1304 std::format(L"{}\\System32\\DriverStore\\FileRepository", windowsPath).c_str(), DriversMountpoint, WSLCMountFlagsReadOnly));
1305
1306 // Mount the inbox libraries.
1307 auto inboxLibPath = std::format(L"{}\\System32\\lxss\\lib", windowsPath);
1308 std::optional<std::string> inboxLibMountPoint;
1309 if (std::filesystem::is_directory(inboxLibPath))
1310 {
1311 inboxLibMountPoint = std::format("{}/inbox", LibrariesMountPoint);
1312 THROW_IF_FAILED(MountWindowsFolderImpl(inboxLibPath.c_str(), inboxLibMountPoint->c_str(), WSLCMountFlagsReadOnly));
1313 }
1314
1315 // Mount the packaged libraries.
1316 #ifdef WSL_GPU_LIB_PATH
1317
1318 auto packagedLibPath = std::filesystem::path(TEXT(WSL_GPU_LIB_PATH));
1319
1320 #else
1321
1322 auto packagedLibPath = wslutil::GetBasePath() / L"lib";
1323
1324 #endif
1325
1326 if (inboxLibMountPoint.has_value())
1327 {
1328 // Mount an overlay containing both inbox and packaged libraries (the packaged mount takes precedence).
1329 auto packagedLibMountPoint = std::format("{}/packaged", LibrariesMountPoint);
1330 THROW_IF_FAILED(MountWindowsFolderImpl(packagedLibPath.c_str(), packagedLibMountPoint.c_str(), WSLCMountFlagsReadOnly));
1331
1332 Mount(
1333 m_initChannel,
1334 "none",
1335 LibrariesMountPoint,
1336 "overlay",
1337 std::format("lowerdir={}:{}", packagedLibMountPoint, inboxLibMountPoint.value()).c_str(),
1338 0);
1339 }
1340 else
1341 {
1342 // If the inbox libraries are not present, mount the packaged libraries directly at final location (no overlay needed).
1343 THROW_IF_FAILED(MountWindowsFolderImpl(packagedLibPath.c_str(), LibrariesMountPoint, WSLCMountFlagsReadOnly));
1344 }
1345 }
1346 void WSLCVirtualMachine::OnProcessReleased(int Pid)
1347 {
1348 std::lock_guard lock{m_trackedProcessesLock};
1349
1350 std::erase_if(m_trackedProcesses, [Pid](const auto& e) {
1351 auto locked = e.lock();
1352 return !locked || locked->GetPid() == Pid;
1353 });
1354 }
1355
1356 void WSLCVirtualMachine::OnSessionTerminated()
1357 {
1358 std::lock_guard lock{m_lock};
1359
1360 // Don't cancel init transactions on the session termination event, since that event is set.
1361 m_initChannel.SetExitEvents({m_vmTerminatingEvent.get()});
1362
1363 // Set a lower timeout for init transactions since we're terminating.
1364 m_initChannelTimeout = 15 * 1000;
1365 }
1366
1367 std::shared_ptr<VmPortAllocation> WSLCVirtualMachine::TryAllocatePort(uint16_t Port, int Family, int Protocol)
1368 {
1369 std::lock_guard lock{m_reservations->Mutex};
1370
1371 WSL_LOG("AllocatePort", TraceLoggingValue(Port, "Port"));
1372
1373 if (!m_reservations->Ports.insert(Port).second)
1374 {
1375 return {};
1376 }
1377
1378 // Roll the reservation back if the allocation object can't be created: nothing owns the port
1379 // until the shared_ptr exists, so it would otherwise stay marked in use for the VM's lifetime.
1380 auto reservationCleanup = wil::scope_exit([&]() { m_reservations->Ports.erase(Port); });
1381 auto allocation = std::make_shared<VmPortAllocation>(Port, Family, Protocol, m_reservations);
1382 reservationCleanup.release();
1383
1384 return allocation;
1385 }
1386
1387 std::shared_ptr<VmPortAllocation> WSLCVirtualMachine::AllocatePort(int Family, int Protocol)
1388 {
1389 std::lock_guard lock{m_reservations->Mutex};
1390
1391 for (uint32_t i = CONTAINER_PORT_RANGE.first; i <= CONTAINER_PORT_RANGE.second; i++)
1392 {
1393 uint16_t port = static_cast<uint16_t>(i);
1394 if (!m_reservations->Ports.contains(port))
1395 {
1396 WI_VERIFY(m_reservations->Ports.insert(port).second);
1397
1398 auto reservationCleanup = wil::scope_exit([&]() { m_reservations->Ports.erase(port); });
1399 auto allocation = std::make_shared<VmPortAllocation>(port, Family, Protocol, m_reservations);
1400 reservationCleanup.release();
1401
1402 return allocation;
1403 }
1404 }
1405
1406 // Fail if we couldn't find a port.
1407 THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_NO_SYSTEM_RESOURCES), "Failed to allocate port");
1408 }
1409
1410 wil::unique_socket WSLCVirtualMachine::ConnectUnixSocket(const char* Path)
1411 {
1412 auto [_, __, channel] = Fork(WSLC_FORK::Thread);
1413
1414 shared::MessageWriter<WSLC_UNIX_CONNECT> message;
1415 message.WriteString(message->PathOffset, Path);
1416
1417 auto result = channel.Transaction<WSLC_UNIX_CONNECT>(message.Span());
1418
1419 THROW_HR_IF_MSG(E_FAIL, result.Result < 0, "Failed to connect to unix socket: '%hs', %i", Path, result.Result);
1420
1421 return channel.Release();
1422 }
1423
1424 void WSLCVirtualMachine::CollectCrashDumps(wil::unique_socket&& listenSocket)
1425 {
1426 // No impersonation needed - the session process already runs as the user.
1427 wslutil::SetThreadDescription(L"CrashDumpCollection");
1428
1429 const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
1430
1431 const auto crashDumpFolder = filesystem::GetTempFolderPath(GetCurrentProcessToken()) / L"wslc-crashes";
1432
1433 while (!m_vmTerminatingEvent.is_signaled())
1434 {
1435 try
1436 {
1437 auto socket = wsl::windows::common::socket::CancellableAccept(listenSocket.get(), INFINITE, m_vmTerminatingEvent.get());
1438 if (!socket)
1439 {
1440 // VM is exiting.
1441 break;
1442 }
1443
1444 constexpr DWORD timeout = 30 * 1000;
1445 THROW_LAST_ERROR_IF(setsockopt(socket->get(), SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)) == SOCKET_ERROR);
1446
1447 auto channel = wsl::shared::SocketChannel{
1448 std::move(socket.value()), "crash_dump", {m_vmTerminatingEvent.get(), m_sessionTerminatingEvent}};
1449
1450 auto transaction = channel.ReceiveTransaction();
1451 gsl::span<gsl::byte> responseSpan;
1452 const auto& message = transaction.Receive<LX_PROCESS_CRASH>(&responseSpan);
1453
1454 const auto bufferSize = responseSpan.size_bytes() - offsetof(LX_PROCESS_CRASH, Buffer);
1455 const std::string process(message.Buffer, strnlen(message.Buffer, bufferSize));
1456
1457 const auto crashPid = message.Pid;
1458 const auto crashSignal = message.Signal;
1459 const auto crashTimestamp = message.Timestamp;
1460
1461 constexpr auto dumpExtension = ".dmp";
1462 constexpr auto dumpPrefix = "wsl-crash";
1463
1464 auto filename = std::format("{}-{}-{}-{}-{}{}", dumpPrefix, crashTimestamp, crashPid, process, crashSignal, dumpExtension);
1465
1466 std::replace_if(
1467 filename.begin(),
1468 filename.end(),
1469 [](char e) { return !std::isalnum(static_cast<unsigned char>(e)) && e != '.' && e != '-'; },
1470 '_');
1471
1472 auto fullPath = crashDumpFolder / filename;
1473
1474 WSL_LOG(
1475 "WSLCLinuxCrash",
1476 TraceLoggingValue(fullPath.c_str(), "FullPath"),
1477 TraceLoggingValue(crashPid, "Pid"),
1478 TraceLoggingValue(crashSignal, "Signal"),
1479 TraceLoggingValue(process.c_str(), "process"));
1480
1481 filesystem::EnsureDirectory(crashDumpFolder.c_str());
1482
1483 // Only delete files that:
1484 // - have the temporary flag set
1485 // - start with 'wsl-crash'
1486 // - end in .dmp
1487 //
1488 // This logic is here to prevent accidental user file deletion
1489 auto pred = [&dumpExtension, &dumpPrefix](const auto& e) {
1490 return WI_IsFlagSet(GetFileAttributes(e.path().c_str()), FILE_ATTRIBUTE_TEMPORARY) && e.path().has_extension() &&
1491 e.path().extension() == dumpExtension && e.path().has_filename() &&
1492 e.path().filename().string().find(dumpPrefix) == 0;
1493 };
1494
1495 wslutil::EnforceFileLimit(crashDumpFolder.c_str(), 10, pred);
1496
1497 wil::unique_hfile file{CreateFileW(fullPath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY, nullptr)};
1498 THROW_LAST_ERROR_IF(!file);
1499
1500 transaction.SendResultMessage<std::int32_t>(0);
1501 relay::InterruptableRelay(reinterpret_cast<HANDLE>(channel.Socket()), file.get(), nullptr);
1502
1503 file.reset();
1504
1505 // Notify the session that a crash dump has been fully written. The session fans out
1506 // to any registered ICrashDumpCallback subscribers. Failures are caller-handled.
1507 if (m_onCrashDump)
1508 {
1509 m_onCrashDump(fullPath.wstring(), process, crashPid, crashSignal, crashTimestamp);
1510 }
1511 }
1512 CATCH_LOG()
1513 }
1514 }