master
cpp 4,143 lines 149 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCSession.cpp
8
9 Abstract:
10
11 This file contains the implementation of the WSLCSession COM class.
12
13 --*/
14
15 #include "precomp.h"
16 #include "WSLCSession.h"
17 #include "WSLCExecutionContext.h"
18 #include "WSLCContainer.h"
19 #include "WSLCNetworkMetadata.h"
20 #include "ContainerNameGenerator.h"
21 #include "ServiceProcessLauncher.h"
22 #include "WindowsCertStore.h"
23 #include "WslCoreFilesystem.h"
24 #include "WSLCSessionDefaults.h"
25 #include "wslpolicies.h"
26 #include "APICompat.h"
27 #include "WSLCContainerEntry.h"
28
29 using namespace wsl::windows::common;
30 using io::MultiHandleWait;
31 using io::OverlappedIOHandle;
32 using io::WriteHandle;
33 using wsl::shared::Localization;
34 using wsl::windows::common::string::FormatHumanReadableSize;
35 using wsl::windows::service::wslc::UserCOMCallback;
36 using wsl::windows::service::wslc::UserHandle;
37 using wsl::windows::service::wslc::WSLCExecutionContext;
38 using wsl::windows::service::wslc::WSLCSession;
39 using wsl::windows::service::wslc::WSLCVirtualMachine;
40
41 constexpr auto c_containerdSocket = "/run/containerd/containerd.sock";
42 constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName;
43 constexpr uint32_t c_progressPrecision = 4;
44 constexpr auto c_containerCreateEventTimeout = std::chrono::seconds{60};
45
46 // Default grace period to keep an otherwise-idle VM running before tearing it down (used when the
47 // session's IdleTimeoutSec setting is 0/unset). This avoids thrashing the VM (repeated
48 // teardown/recreate) when containers are created and destroyed, or operations issued, in quick
49 // succession. The clock restarts whenever the VM is observed to be non-idle, so a full grace period
50 // of continuous idleness is required before teardown.
51 constexpr auto c_vmIdleGracePeriod = std::chrono::seconds(30);
52
53 namespace {
54
55 // Validates the target path for a NEW session (one with no existing storage VHD): if the path
56 // already exists it must be an empty directory, so session storage is never mixed with unrelated
57 // user files. A non-existent path is fine (it will be created). Enforced eagerly at session
58 // creation and again when the storage VHD is lazily created.
59 void ValidateNewSessionStorageDirectory(const std::filesystem::path& StoragePath)
60 {
61 // status's error_code distinguishes "doesn't exist yet" (OK, we'll create it) from other I/O errors.
62 std::error_code ec;
63 const auto status = std::filesystem::status(StoragePath, ec);
64 if (ec && ec.value() != ERROR_FILE_NOT_FOUND && ec.value() != ERROR_PATH_NOT_FOUND)
65 {
66 THROW_IF_WIN32_ERROR_MSG(ec.value(), "status failed for %ls", StoragePath.c_str());
67 }
68
69 if (!std::filesystem::exists(status))
70 {
71 return;
72 }
73
74 THROW_HR_WITH_USER_ERROR_IF(
75 E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeDirectory(StoragePath.c_str()), !std::filesystem::is_directory(status));
76
77 const bool empty = std::filesystem::is_empty(StoragePath, ec);
78 THROW_IF_WIN32_ERROR_MSG(ec.value(), "is_empty failed for %ls", StoragePath.c_str());
79 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeEmpty(StoragePath.c_str()), !empty);
80 }
81
82 // Group policy: WSLContainerRegistryAllowlist restricts which container-image
83 // registries can be pulled from or pushed to. The check is enforced here at the
84 // service boundary so it covers ALL callers (wslc.exe CLI, the WslcSDK C API, and
85 // any other COM client). Callers pass the parsed repository so no reference is
86 // parsed twice.
87 void EnforceRegistryAllowlist(const wslutil::RepositoryReference& Repository)
88 {
89 const auto policiesKey = wsl::windows::policies::OpenPoliciesKey();
90 const auto serverWide = wsl::shared::string::MultiByteToWide(Repository.Server);
91
92 if (wsl::windows::policies::IsRegistryAllowed(policiesKey.get(), serverWide))
93 {
94 return;
95 }
96
97 THROW_HR_WITH_USER_ERROR(WSLC_E_REGISTRY_BLOCKED_BY_POLICY, Localization::MessageRegistryBlockedByPolicy(serverWide));
98 }
99
100 std::string IndentLines(const std::string& input, const std::string& prefix, bool prefixFirstLine = true)
101 {
102 if (input.empty())
103 {
104 return {};
105 }
106
107 std::string result = prefixFirstLine ? prefix : "";
108 for (size_t i = 0; i < input.size(); i++)
109 {
110 result.push_back(input[i]);
111 if (i + 1 < input.size())
112 {
113 if (input[i] == '\n' || (input[i] == '\r' && input[i + 1] != '\n'))
114 {
115 result.append(prefix);
116 }
117 }
118 }
119
120 return result;
121 }
122
123 void ValidateName(LPCSTR Name, size_t maxLength)
124 {
125 const auto& locale = std::locale::classic();
126 size_t i = 0;
127
128 for (; Name[i] != '\0'; i++)
129 {
130 if (!std::isalnum(Name[i], locale) && Name[i] != '_' && Name[i] != '-' && Name[i] != '.')
131 {
132 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcInvalidName(Name));
133 }
134 }
135
136 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcInvalidName(Name), i == 0 || i > maxLength);
137 }
138
139 wslc_schema::InspectImage ConvertInspectImage(const docker_schema::InspectImage& dockerInspect)
140 {
141 wslc_schema::InspectImage wslcInspect{};
142
143 // Direct field mappings
144 wslcInspect.Id = dockerInspect.Id;
145 wslcInspect.RepoTags = dockerInspect.RepoTags;
146 wslcInspect.RepoDigests = dockerInspect.RepoDigests;
147 wslcInspect.Parent = dockerInspect.Parent;
148 wslcInspect.Comment = dockerInspect.Comment;
149 wslcInspect.Created = dockerInspect.Created;
150 wslcInspect.Author = dockerInspect.Author;
151 wslcInspect.Architecture = dockerInspect.Architecture;
152 wslcInspect.Os = dockerInspect.Os;
153 wslcInspect.Size = dockerInspect.Size;
154 wslcInspect.Metadata = dockerInspect.Metadata;
155
156 // Convert Config from docker_schema to wslc_schema
157 if (dockerInspect.Config.has_value())
158 {
159 wslc_schema::ImageConfig wslcConfig{};
160 const auto& dockerConfig = dockerInspect.Config.value();
161
162 wslcConfig.Cmd = dockerConfig.Cmd;
163 wslcConfig.Entrypoint = dockerConfig.Entrypoint;
164 wslcConfig.Env = dockerConfig.Env;
165 wslcConfig.Labels = dockerConfig.Labels;
166 wslcConfig.StopSignal = dockerConfig.StopSignal;
167 wslcConfig.User = dockerConfig.User;
168 wslcConfig.WorkingDir = dockerConfig.WorkingDir;
169
170 if (dockerConfig.ExposedPorts.has_value())
171 {
172 std::map<std::string, wslc_schema::EmptyObject> ports;
173 for (const auto& [port, _] : dockerConfig.ExposedPorts.value())
174 {
175 ports.emplace(port, wslc_schema::EmptyObject{});
176 }
177 wslcConfig.ExposedPorts = std::move(ports);
178 }
179
180 if (dockerConfig.Volumes.has_value())
181 {
182 std::map<std::string, wslc_schema::EmptyObject> volumes;
183 for (const auto& [path, _] : dockerConfig.Volumes.value())
184 {
185 volumes.emplace(path, wslc_schema::EmptyObject{});
186 }
187 wslcConfig.Volumes = std::move(volumes);
188 }
189
190 wslcInspect.Config = wslcConfig;
191 }
192
193 if (dockerInspect.RootFS.has_value())
194 {
195 const auto& dockerRootFS = dockerInspect.RootFS.value();
196 wslc_schema::ImageRootFS wslcRootFS{};
197 wslcRootFS.Type = dockerRootFS.Type;
198 wslcRootFS.Layers = dockerRootFS.Layers;
199 wslcInspect.RootFS = std::move(wslcRootFS);
200 }
201
202 return wslcInspect;
203 }
204
205 using wsl::windows::service::wslc::c_descriptors;
206 using wsl::windows::service::wslc::c_mountains;
207
208 // Generate a random container name in the format "descriptor_mountain".
209 // When retry > 0, appends a random digit (0-9) to reduce collisions.
210 std::string GenerateContainerName(int retry)
211 {
212 std::mt19937 gen(std::random_device{}());
213
214 std::uniform_int_distribution<size_t> leftDist(0, c_descriptors.size() - 1);
215 std::uniform_int_distribution<size_t> rightDist(0, c_mountains.size() - 1);
216
217 auto name = std::format("{}_{}", c_descriptors[leftDist(gen)], c_mountains[rightDist(gen)]);
218
219 if (retry > 0)
220 {
221 std::uniform_int_distribution<int> digitDist(0, 9);
222 name += std::to_string(digitDist(gen));
223 }
224
225 return name;
226 }
227
228 } // namespace
229
230 namespace wsl::windows::service::wslc {
231
232 // COM object returned by WSLCSession::RegisterCrashDumpCallback. Holds a strong reference to the
233 // owning session so the session COM facade cannot be destroyed before all subscriptions are
234 // released. When the last reference is dropped, the destructor removes the matching entry from
235 // the session's callback list. Safe to release in any order with respect to the session pointer
236 // that the client also holds.
237 //
238 // The subscription is returned to callers as a bare IUnknown -- the type is an opaque lifetime
239 // handle with no methods of its own, so there is no need for a dedicated COM interface.
240 class CrashDumpSubscription
241 : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IUnknown, IFastRundown>
242 {
243 public:
244 HRESULT RuntimeClassInitialize(Microsoft::WRL::ComPtr<WSLCSession> Session, WSLCSession::CrashDumpCallbackList::iterator It)
245 {
246 m_session = std::move(Session);
247 m_iterator = It;
248 return S_OK;
249 }
250
251 ~CrashDumpSubscription()
252 {
253 if (m_session)
254 {
255 m_session->RemoveCrashDumpCallback(m_iterator);
256 }
257 }
258
259 private:
260 Microsoft::WRL::ComPtr<WSLCSession> m_session;
261 WSLCSession::CrashDumpCallbackList::iterator m_iterator{};
262 };
263
264 UserHandle::UserHandle(WSLCSession& Session, HANDLE handle) : m_session(&Session), m_handle(handle)
265 {
266 WI_ASSERT(!!m_handle);
267 }
268
269 UserHandle::UserHandle(UserHandle&& Other)
270 {
271 *this = std::move(Other);
272 }
273
274 UserHandle& UserHandle::operator=(UserHandle&& Other)
275 {
276 if (this != &Other)
277 {
278 Reset();
279 m_session = Other.m_session;
280 m_handle = Other.m_handle;
281
282 Other.m_handle = nullptr;
283 Other.m_session = nullptr;
284 }
285 return *this;
286 }
287
288 void UserHandle::Reset()
289 {
290 if (m_handle != nullptr)
291 {
292 WI_ASSERT(m_session != nullptr);
293
294 m_session->ReleaseUserHandle(m_handle);
295 m_handle = nullptr;
296 }
297 }
298
299 UserHandle::~UserHandle()
300 {
301 Reset();
302 }
303
304 HANDLE UserHandle::Get() const noexcept
305 {
306 return m_handle;
307 }
308
309 UserCOMCallback::UserCOMCallback(WSLCSession& Session) noexcept : m_session(&Session), m_threadId(GetCurrentThreadId())
310 {
311 }
312
313 UserCOMCallback::UserCOMCallback(UserCOMCallback&& Other) noexcept
314 {
315 *this = std::move(Other);
316 }
317
318 UserCOMCallback& UserCOMCallback::operator=(UserCOMCallback&& Other) noexcept
319 {
320 if (this != &Other)
321 {
322 Reset();
323 m_session = Other.m_session;
324 m_threadId = Other.m_threadId;
325
326 Other.m_threadId = 0;
327 Other.m_session = nullptr;
328 }
329 return *this;
330 }
331
332 void UserCOMCallback::Reset() noexcept
333 {
334 if (m_threadId != 0)
335 {
336 WI_ASSERT(m_session != nullptr);
337
338 m_session->UnregisterUserCOMCallback(m_threadId);
339 m_threadId = 0;
340
341 LOG_IF_FAILED(CoDisableCallCancellation(nullptr));
342 }
343 }
344
345 UserCOMCallback::~UserCOMCallback() noexcept
346 {
347 Reset();
348 }
349
350 HRESULT WSLCSession::GetProcessHandle(_Out_ HANDLE* ProcessHandle)
351 try
352 {
353 RETURN_HR_IF_NULL(E_POINTER, ProcessHandle);
354
355 *ProcessHandle = wslutil::DuplicateHandle(GetCurrentProcess(), PROCESS_SET_QUOTA | PROCESS_TERMINATE);
356 return S_OK;
357 }
358 CATCH_RETURN();
359
360 HRESULT WSLCSession::Initialize(
361 _In_ const WSLCSessionInitSettings* Settings,
362 _In_ IWSLCVirtualMachineFactory* VmFactory,
363 _In_ IWSLCPluginNotifier* PluginNotifier,
364 _In_opt_ IWarningCallback* WarningCallback)
365 try
366 {
367 RETURN_HR_IF(E_POINTER, Settings == nullptr || VmFactory == nullptr);
368 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED), m_vmFactoryGitCookie != 0);
369
370 THROW_HR_IF_MSG(
371 E_INVALIDARG, WI_IsAnyFlagSet(Settings->FeatureFlags, ~WSLCFeatureFlagsValid), "Invalid feature flags: 0x%x", Settings->FeatureFlags);
372 THROW_HR_IF_MSG(
373 E_INVALIDARG,
374 WI_IsAnyFlagSet(Settings->StorageFlags, ~WSLCSessionStorageFlagsValid),
375 "Invalid storage flags: 0x%x",
376 Settings->StorageFlags);
377
378 // Set up a warning context for the duration of initialization so that non-fatal
379 // failures are streamed to the CLI.
380 WSLCExecutionContext warningContext(this, WarningCallback);
381
382 // The VM (and storage VHD) is created lazily on the first operation. Validate the storage
383 // configuration eagerly here so misconfiguration is reported at session creation rather than
384 // surfacing later on the first VM-starting operation.
385 if (Settings->StoragePath != nullptr)
386 {
387 const std::filesystem::path storagePath{Settings->StoragePath};
388 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Settings->StoragePath), !storagePath.is_absolute());
389
390 const auto vhdPath = storagePath / c_storageVhdFilename;
391 std::error_code existsError;
392 const bool vhdExists = std::filesystem::exists(vhdPath, existsError);
393 THROW_IF_WIN32_ERROR_MSG(existsError.value(), "exists failed for %ls", vhdPath.c_str());
394
395 if (WI_IsFlagSet(Settings->StorageFlags, WSLCSessionStorageFlagsNoCreate))
396 {
397 // The storage VHD must already exist (ConfigureStorage will not create it).
398 THROW_HR_WITH_USER_ERROR_IF(
399 HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), Localization::MessageWslcSessionStorageNotFound(Settings->StoragePath), !vhdExists);
400 }
401 else if (!vhdExists)
402 {
403 // New session: the target path (if it exists) must be an empty directory.
404 ValidateNewSessionStorageDirectory(storagePath);
405 }
406 }
407
408 // N.B. No locking is required because Initialize() is always called before the session is returned to the caller.
409 m_id = Settings->SessionId;
410 m_displayName = Settings->DisplayName ? Settings->DisplayName : L"";
411 m_creatorProcessName = Settings->CreatorProcessName ? Settings->CreatorProcessName : L"";
412 m_featureFlags = Settings->FeatureFlags;
413 m_pluginNotifier = PluginNotifier;
414
415 // Park the VM factory in the Global Interface Table. It is supplied here (on the call that
416 // creates the session) but used on demand from other threads/apartments; storing the raw
417 // proxy and calling it later would raise RPC_E_WRONG_THREAD.
418 m_git = wil::CoCreateInstance<IGlobalInterfaceTable>(CLSID_StdGlobalInterfaceTable, CLSCTX_INPROC_SERVER);
419 THROW_IF_FAILED(m_git->RegisterInterfaceInGlobal(VmFactory, __uuidof(IWSLCVirtualMachineFactory), &m_vmFactoryGitCookie));
420
421 // Persist a deep copy of the settings (and the creating user's SID) required to
422 // (re)create the VM on demand.
423 const auto tokenInfo = wil::get_token_information<TOKEN_USER>(GetCurrentProcessToken());
424 PersistSettings(*Settings, tokenInfo->User.Sid);
425
426 WSL_LOG(
427 "SessionInitialized",
428 TraceLoggingValue(m_id, "SessionId"),
429 TraceLoggingValue(m_displayName.c_str(), "DisplayName"),
430 TraceLoggingValue(m_creatorProcessName.c_str(), "CreatorProcess"));
431
432 const auto idleGracePeriod = m_settings.IdleTimeoutSec > 0 ? std::chrono::seconds(m_settings.IdleTimeoutSec) : c_vmIdleGracePeriod;
433
434 WSLCSessionRuntime::RuntimeHooks hooks;
435 hooks.BringUp = [this]() {
436 // Configure storage.
437 ConfigureStorage(m_settings, m_userSid.empty() ? nullptr : reinterpret_cast<PSID>(m_userSid.data()));
438
439 // Mirror the host's trusted root CAs into the VM before dockerd starts.
440 InstallTrustedRootCertificates();
441
442 // Launch containerd first, then dockerd with the external containerd socket.
443 StartContainerd();
444
445 // Reset the readiness event before (re)starting dockerd so a stale signal from a prior
446 // VM instance is not observed.
447 m_runtime.ResetDockerdReady();
448 StartDockerd();
449
450 m_runtime.InitializeDockerRuntime(m_storageVhdPath.parent_path());
451 };
452
453 hooks.RecoverState = [this]() {
454 RecoverExistingNetworks();
455 RecoverExistingContainers();
456 };
457
458 hooks.TearDownSessionState = [this](bool permanent) {
459 std::lock_guard containersLock(m_containersLock);
460 std::lock_guard networksLock(m_networksLock);
461
462 // Network metadata is rebuilt from dockerd on every VM start, so it is always dropped.
463 m_networks.clear();
464
465 // Container wrappers are kept alive across idle teardown (only cleared on permanent shutdown)
466 // so client COM references stay valid; RecoverState reattaches them to the restarted VM.
467 if (permanent)
468 {
469 m_containers.clear();
470 }
471 };
472
473 hooks.OnSpontaneousExit = [this]() { LOG_IF_FAILED(Terminate()); };
474
475 // Forward VM start/stop to plugins. Both are best-effort: errors are logged and ignored so a
476 // misbehaving plugin cannot abort VM startup or the operation that triggered it.
477 hooks.OnVmStarted = [this]() {
478 if (m_pluginNotifier)
479 {
480 LOG_IF_FAILED(m_pluginNotifier->OnVmStarted());
481 }
482 };
483
484 hooks.OnVmStopping = [this]() {
485 if (m_pluginNotifier)
486 {
487 LOG_IF_FAILED(m_pluginNotifier->OnVmStopping());
488 }
489 };
490
491 hooks.OnCrashDump = std::bind(
492 &WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5);
493
494 WSLCSessionRuntime::SessionContext sessionContext;
495 sessionContext.Id = m_id;
496 sessionContext.DisplayName = m_displayName;
497 sessionContext.Terminating = &m_terminating;
498 sessionContext.SessionTerminatingEvent = m_sessionTerminatingEvent;
499 sessionContext.SessionTerminatedEvent = m_sessionTerminatedEvent;
500
501 m_runtime.Initialize(m_vmFactoryGitCookie, m_git, &m_settings, idleGracePeriod, std::move(sessionContext), std::move(hooks));
502
503 m_containerEventTracking = m_runtime.Events().RegisterContainerCreate(
504 std::bind(&WSLCSession::OnContainerCreated, this, std::placeholders::_1, std::placeholders::_2));
505
506 return S_OK;
507 }
508 CATCH_RETURN()
509
510 void WSLCSession::PersistSettings(const WSLCSessionInitSettings& Settings, PSID UserSid)
511 {
512 m_settings = Settings;
513
514 // Repoint the string fields at storage owned by the session so they outlive the caller's buffers.
515 m_settings.DisplayName = m_displayName.c_str();
516
517 if (Settings.CreatorProcessName != nullptr)
518 {
519 m_settingsCreatorProcessName = Settings.CreatorProcessName;
520 m_settings.CreatorProcessName = m_settingsCreatorProcessName->c_str();
521 }
522 else
523 {
524 m_settings.CreatorProcessName = nullptr;
525 }
526
527 if (Settings.StoragePath != nullptr)
528 {
529 m_settingsStoragePath = Settings.StoragePath;
530 m_settings.StoragePath = m_settingsStoragePath->c_str();
531 }
532 else
533 {
534 m_settings.StoragePath = nullptr;
535 }
536
537 if (Settings.RootVhdTypeOverride != nullptr)
538 {
539 m_settingsRootVhdTypeOverride = Settings.RootVhdTypeOverride;
540 m_settings.RootVhdTypeOverride = m_settingsRootVhdTypeOverride->c_str();
541 }
542 else
543 {
544 m_settings.RootVhdTypeOverride = nullptr;
545 }
546
547 THROW_HR_IF(E_UNEXPECTED, UserSid == nullptr);
548
549 const auto length = GetLengthSid(UserSid);
550 const auto* bytes = reinterpret_cast<const BYTE*>(UserSid);
551 m_userSid.assign(bytes, bytes + length);
552 }
553
554 WSLCSession::VmLease WSLCSession::AcquireLease(WSLCSessionRuntime::VmLeasePolicy Policy)
555 {
556 return m_runtime.AcquireVmLease(Policy);
557 }
558
559 WSLCSession::~WSLCSession()
560 {
561 WSL_LOG("SessionTerminated", TraceLoggingValue(m_id, "SessionId"), TraceLoggingValue(m_displayName.c_str(), "DisplayName"));
562
563 LOG_IF_FAILED(Terminate());
564
565 if (m_destructionCallback)
566 {
567 m_destructionCallback();
568 }
569 }
570
571 void WSLCSession::SetDestructionCallback(std::function<void()>&& callback)
572 {
573 m_destructionCallback = std::move(callback);
574 }
575
576 void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID UserSid)
577 {
578 if (Settings.StoragePath == nullptr)
579 {
580 // If no storage path is specified, use a tmpfs for convenience.
581 m_runtime.Vm().Mount("", wsl::windows::wslc::ContainerdStorageMountPoint, "tmpfs", "", 0);
582 m_runtime.SetStorageMounted(true);
583 return;
584 }
585
586 std::filesystem::path storagePath{Settings.StoragePath};
587 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Settings.StoragePath), !storagePath.is_absolute());
588
589 m_storageVhdPath = storagePath / c_storageVhdFilename;
590
591 std::string diskDevice;
592 std::optional<ULONG> diskLun{};
593 bool vhdCreated = false;
594
595 auto deleteVhdOnFailure = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
596 if (vhdCreated)
597 {
598 if (diskLun.has_value())
599 {
600 m_runtime.Vm().DetachDisk(diskLun.value());
601 }
602
603 LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_storageVhdPath.c_str()));
604 }
605 });
606
607 auto result =
608 wil::ResultFromException([&]() { diskDevice = m_runtime.Vm().AttachDisk(m_storageVhdPath.c_str(), false).second; });
609
610 if (FAILED(result))
611 {
612 THROW_HR_IF_MSG(
613 result,
614 result != HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND) && result != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND),
615 "Failed to attach vhd: %ls",
616 m_storageVhdPath.c_str());
617
618 // No existing VHD — this is a new session. Reject if the caller forbade creation.
619 THROW_HR_WITH_USER_ERROR_IF(
620 HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND),
621 Localization::MessageWslcSessionStorageNotFound(Settings.StoragePath),
622 WI_IsFlagSet(Settings.StorageFlags, WSLCSessionStorageFlagsNoCreate));
623
624 // Reject any non-empty existing path so we don't mix user files with session storage.
625 ValidateNewSessionStorageDirectory(storagePath);
626
627 // If the VHD wasn't found, create it.
628 WSL_LOG("CreateStorageVhd", TraceLoggingValue(m_storageVhdPath.c_str(), "StorageVhdPath"));
629
630 if (WI_IsFlagSet(Settings.StorageFlags, WSLCSessionStorageFlagsWarnCustomLocation))
631 {
632 EMIT_USER_WARNING(Localization::MessageWslcSessionStorageCustomLocation(storagePath.c_str()));
633 }
634
635 std::filesystem::create_directories(storagePath);
636 wsl::core::filesystem::CreateVhd(m_storageVhdPath.c_str(), Settings.MaximumStorageSizeMb * _1MB, UserSid, false, false);
637 vhdCreated = true;
638
639 // Then attach the new disk.
640 std::tie(diskLun, diskDevice) = m_runtime.Vm().AttachDisk(m_storageVhdPath.c_str(), false);
641
642 // Then format it.
643 m_runtime.Vm().Ext4Format(diskDevice);
644 }
645
646 // Mount the device to /root.
647 m_runtime.Vm().Mount(diskDevice.c_str(), wsl::windows::wslc::ContainerdStorageMountPoint, "ext4", "discard", 0);
648 m_runtime.SetStorageMounted(true);
649
650 // Configure swap on a separate ephemeral VHD.
651 if (Settings.SwapSizeMb > 0)
652 {
653 try
654 {
655 std::filesystem::path swapVhdPath = storagePath / "swap.vhdx";
656 m_runtime.SetSwapVhdPath(swapVhdPath);
657 DeleteFileW(swapVhdPath.c_str()); // Remove stale swap from prior run
658 wsl::core::filesystem::CreateVhd(swapVhdPath.c_str(), static_cast<ULONGLONG>(Settings.SwapSizeMb) * _1MB, UserSid, false, false);
659
660 auto [_, swapDevice] = m_runtime.Vm().AttachDisk(swapVhdPath.c_str(), false);
661
662 // Fire-and-forget: mkswap + swapon runs asynchronously since swap is best-effort.
663 auto cmd = std::format("/usr/sbin/mkswap {0} && /usr/sbin/swapon {0}", swapDevice);
664 ServiceProcessLauncher launcher("/bin/sh", {"/bin/sh", "-c", cmd});
665 launcher.Launch(m_runtime.Vm());
666 }
667 catch (...)
668 {
669 LOG_CAUGHT_EXCEPTION();
670 EMIT_USER_WARNING(Localization::MessageWslcSwapInitFailed());
671 }
672 }
673
674 deleteVhdOnFailure.release();
675 }
676
677 HRESULT WSLCSession::GetId(ULONG* Id)
678 {
679 RETURN_HR_IF_NULL(E_POINTER, Id);
680
681 *Id = m_id;
682
683 return S_OK;
684 }
685
686 HRESULT WSLCSession::GetDisplayName(_Out_ LPWSTR* DisplayName)
687 try
688 {
689 RETURN_HR_IF_NULL(E_POINTER, DisplayName);
690 *DisplayName = nullptr;
691
692 *DisplayName = wil::make_unique_string<wil::unique_cotaskmem_string>(m_displayName.c_str()).release();
693 return S_OK;
694 }
695 CATCH_RETURN();
696
697 void WSLCSession::OnDockerdExited()
698 {
699 if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDisposition() != WSLCSessionRuntime::VmExitDisposition::StopRequested)
700 {
701 WSL_LOG("UnexpectedDockerdExit", TraceLoggingValue(m_displayName.c_str(), "Name"));
702 }
703 }
704
705 void WSLCSession::OnContainerdExited()
706 {
707 if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDisposition() != WSLCSessionRuntime::VmExitDisposition::StopRequested)
708 {
709 WSL_LOG("UnexpectedContainerdExit", TraceLoggingValue(m_displayName.c_str(), "Name"));
710 }
711 }
712
713 ServiceRunningProcess WSLCSession::StartProcess(
714 const std::string& Executable, const std::vector<std::string>& Args, PCSTR LogSource, std::function<void()>&& ExitCallback)
715 {
716 ServiceProcessLauncher launcher{Executable, Args, {{"PATH=/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin"}}};
717
718 auto process = launcher.Launch(m_runtime.Vm());
719
720 m_runtime.Relay()->AddHandle(std::make_unique<windows::common::io::LineBasedReadHandle>(
721 process.GetStdHandle(1), [this, LogSource](const auto& data) { m_runtime.OnProcessLog(data, LogSource); }, false));
722
723 m_runtime.Relay()->AddHandle(std::make_unique<windows::common::io::LineBasedReadHandle>(
724 process.GetStdHandle(2), [this, LogSource](const auto& data) { m_runtime.OnProcessLog(data, LogSource); }, false));
725
726 m_runtime.Relay()->AddHandle(std::make_unique<windows::common::io::EventHandle>(process.GetExitEvent(), std::move(ExitCallback)));
727
728 return process;
729 }
730
731 void WSLCSession::StartContainerd()
732 {
733 constexpr auto c_containerdRoot = "/var/lib/docker/containerd/daemon";
734 constexpr auto c_containerdState = "/run/docker/containerd/daemon";
735
736 std::vector<std::string> args{"/usr/bin/containerd", "--address", c_containerdSocket, "--root", c_containerdRoot, "--state", c_containerdState};
737
738 if (WI_IsFlagSet(m_featureFlags, WslcFeatureFlagsDebug))
739 {
740 args.emplace_back("--log-level");
741 args.emplace_back("debug");
742 }
743
744 m_runtime.SetContainerdProcess(StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this)));
745 WSL_LOG("ContainerdStarted");
746 }
747
748 void WSLCSession::StartDockerd()
749 {
750 std::vector<std::string> args{"/usr/bin/dockerd", "--containerd", c_containerdSocket};
751
752 if (WI_IsFlagSet(m_featureFlags, WslcFeatureFlagsDebug))
753 {
754 args.emplace_back("--debug");
755 }
756
757 m_runtime.SetDockerdProcess(StartProcess("/usr/bin/dockerd", args, "dockerd", std::bind(&WSLCSession::OnDockerdExited, this)));
758 WSL_LOG("DockerdStarted");
759 }
760
761 void WSLCSession::InstallTrustedRootCertificates()
762 try
763 {
764 const auto pem = CollectTrustedRootCertificatesPem();
765 if (pem.empty())
766 {
767 WSL_LOG("InstallTrustedRootCertificatesSkipped");
768 return;
769 }
770
771 // dockerd and containerd read the certificates found in /etc/ssl/certs into
772 // their default system certificate pool.
773 constexpr auto c_certPath = "/etc/ssl/certs/wsl-windows-roots.pem";
774 const auto script = std::format("cat > '{}'", c_certPath);
775
776 ServiceProcessLauncher launcher("/bin/sh", {"/bin/sh", "--norc", "-c", script}, {}, WSLCProcessFlagsStdin);
777 auto process = launcher.Launch(m_runtime.Vm());
778
779 std::unique_ptr<OverlappedIOHandle> writeStdin(
780 new WriteHandle(process.GetStdHandle(WSLCFDStdin), std::vector<char>{pem.begin(), pem.end()}));
781 std::vector<std::unique_ptr<OverlappedIOHandle>> extraHandles;
782 extraHandles.emplace_back(std::move(writeStdin));
783
784 const auto result = process.WaitAndCaptureOutput(60000UL, std::move(extraHandles));
785 THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str());
786
787 WSL_LOG(
788 "InstalledTrustedRootCertificates",
789 TraceLoggingValue(c_certPath, "Path"),
790 TraceLoggingValue(static_cast<uint64_t>(pem.size()), "BundleBytes"));
791 }
792 catch (...)
793 {
794 // Best-effort: failing to install the host's trusted roots must not prevent the session from starting.
795 LOG_CAUGHT_EXCEPTION_MSG("Failed to install trusted root certificates into the VM");
796 EMIT_USER_WARNING(Localization::MessageWslcInstallCertsFailed(wslutil::GetErrorString(wil::ResultFromCaughtException())));
797 }
798
799 void WSLCSession::StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback)
800 {
801 auto io = CreateIOContext();
802
803 struct Response
804 {
805 boost::beast::http::status result;
806 bool isJson = false;
807 };
808
809 std::optional<UserCOMCallback> comCall;
810 if (ProgressCallback != nullptr)
811 {
812 comCall = RegisterUserCOMCallback();
813 }
814
815 std::optional<Response> httpResponse;
816
817 auto onHttpResponse = [&](const boost::beast::http::message<false, boost::beast::http::buffer_body>& response) {
818 WSL_LOG(
819 "ImageOperationHttpResponse",
820 TraceLoggingValue(OperationName, "Operation"),
821 TraceLoggingValue(static_cast<int>(response.result()), "StatusCode"));
822
823 auto it = response.find(boost::beast::http::field::content_type);
824 httpResponse.emplace(response.result(), it != response.end() && it->value().starts_with("application/json"));
825 };
826
827 std::string errorJson;
828 std::optional<std::string> reportedError;
829 auto onChunk = [&](const gsl::span<char>& Content) {
830 if (httpResponse.has_value() && httpResponse->result != boost::beast::http::status::ok)
831 {
832 // If the status code is an error, then this is an error message, not a progress update.
833 errorJson.append(Content.data(), Content.size());
834 return;
835 }
836
837 std::string contentString{Content.begin(), Content.end()};
838 WSL_LOG(
839 "ImageOperationProgress",
840 TraceLoggingValue(OperationName, "Operation"),
841 TraceLoggingValue(Image, "Image"),
842 TraceLoggingValue(contentString.c_str(), "Content"));
843
844 auto parsed = wsl::shared::FromJson<docker_schema::CreateImageProgress>(contentString.c_str());
845
846 if (parsed.errorDetail.has_value())
847 {
848 if (reportedError.has_value())
849 {
850 LOG_HR_MSG(
851 E_UNEXPECTED,
852 "Received multiple error messages during image %hs. Previous: %hs, New: %hs",
853 OperationName,
854 reportedError->c_str(),
855 parsed.errorDetail->message.c_str());
856 EMIT_USER_WARNING(wsl::shared::string::MultiByteToWide(*reportedError));
857 }
858
859 reportedError = FormatDockerEngineError(parsed.errorDetail->message);
860 return;
861 }
862
863 if (ProgressCallback != nullptr)
864 {
865 THROW_IF_FAILED(ProgressCallback->OnProgress(
866 parsed.status.c_str(), parsed.id.c_str(), parsed.progressDetail.current, parsed.progressDetail.total));
867 }
868 };
869
870 io.AddHandle(std::make_unique<DockerHTTPClient::DockerHttpResponseHandle>(requestContext, std::move(onHttpResponse), std::move(onChunk)));
871
872 io.Run({});
873
874 THROW_HR_IF(E_UNEXPECTED, !httpResponse.has_value());
875
876 if (httpResponse->result != boost::beast::http::status::ok)
877 {
878 std::string errorMessage;
879 if (httpResponse->isJson)
880 {
881 // operation failed, parse the error message.
882 errorMessage = FormatDockerEngineError(wsl::shared::FromJson<docker_schema::ErrorResponse>(errorJson.c_str()).message);
883 }
884 else
885 {
886 // If no error message was explicitly returned, use the response body, if any.
887 errorMessage = errorJson;
888 }
889
890 if (httpResponse->result == boost::beast::http::status::not_found)
891 {
892 THROW_HR_WITH_USER_ERROR(WSLC_E_IMAGE_NOT_FOUND, errorMessage);
893 }
894 else if (httpResponse->result == boost::beast::http::status::bad_request)
895 {
896 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, errorMessage);
897 }
898 else
899 {
900 THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
901 }
902 }
903 else if (reportedError.has_value())
904 {
905 // Can happen if an error is returned during progress after receiving an OK status.
906 THROW_HR_WITH_USER_ERROR(E_FAIL, reportedError.value().c_str());
907 }
908 }
909
910 void WSLCSession::OnImageCreated(const std::string& ImageNameOrId) noexcept
911 try
912 {
913 LOG_IF_FAILED(m_pluginNotifier->OnImageCreated(InspectImageLockHeld(ImageNameOrId).c_str()));
914 }
915 CATCH_LOG()
916
917 void WSLCSession::OnImageDeleted(const std::string& ImageId) noexcept
918 try
919 {
920 LOG_IF_FAILED(m_pluginNotifier->OnImageDeleted(ImageId.c_str()));
921 }
922 CATCH_LOG()
923
924 HRESULT WSLCSession::PullImage(LPCSTR Image, LPCSTR RegistryAuthenticationInformation, IProgressCallback* ProgressCallback, IWarningCallback* WarningCallback)
925 try
926 {
927 WSLCExecutionContext context(this, WarningCallback);
928
929 RETURN_HR_IF_NULL(E_POINTER, Image);
930
931 const auto reference = wslutil::ImageReference::Parse(Image);
932 const auto& repo = reference.Repository;
933 auto tagOrDigest = reference.TagOrDigest();
934 EnforceRegistryAllowlist(repo);
935
936 auto runtime = m_runtime.Acquire();
937 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
938
939 if (!tagOrDigest.has_value())
940 {
941 tagOrDigest = "latest";
942 }
943
944 std::optional<std::string> registryAuth;
945
946 if (RegistryAuthenticationInformation != nullptr && *RegistryAuthenticationInformation != '\0')
947 {
948 registryAuth = std::string(RegistryAuthenticationInformation);
949 }
950
951 auto requestContext = runtime.Docker().PullImage(repo.Name, tagOrDigest, registryAuth);
952 StreamImageOperation(*requestContext, Image, "Pull", ProgressCallback);
953
954 OnImageCreated(Image);
955
956 return S_OK;
957 }
958 CATCH_RETURN();
959
960 HRESULT WSLCSession::BuildImage(const WSLCBuildImageOptions* Options, IProgressCallback* ProgressCallback, HANDLE CancelEvent)
961 try
962 {
963 WSLCExecutionContext context(this);
964
965 RETURN_HR_IF_NULL(E_POINTER, Options);
966 RETURN_HR_IF_NULL(E_POINTER, Options->ContextPath);
967 RETURN_HR_IF(E_INVALIDARG, *Options->ContextPath == L'\0');
968 RETURN_HR_IF(E_INVALIDARG, Options->Tags.Count > 0 && Options->Tags.Values == nullptr);
969 RETURN_HR_IF(E_INVALIDARG, Options->BuildArgs.Count > 0 && Options->BuildArgs.Values == nullptr);
970 RETURN_HR_IF(E_INVALIDARG, Options->Labels.Count > 0 && Options->Labels.Values == nullptr);
971 RETURN_HR_IF(E_INVALIDARG, Options->Secrets.Count > 0 && Options->Secrets.Values == nullptr);
972 THROW_HR_IF_MSG(
973 E_INVALIDARG,
974 WI_IsAnyFlagSet(static_cast<WSLCBuildImageFlags>(Options->Flags), ~WSLCBuildImageFlagsValid),
975 "Invalid flags: 0x%x",
976 Options->Flags);
977
978 auto buildFileHandle = OpenUserHandle(Options->DockerfileHandle);
979
980 std::optional<UserCOMCallback> comCall;
981 if (ProgressCallback != nullptr)
982 {
983 comCall = RegisterUserCOMCallback();
984 }
985
986 auto runtime = m_runtime.Acquire();
987
988 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
989
990 const auto policyState = runtime.Vm().GetBuildKitPolicyState();
991
992 // Track every Windows folder we mount into the VM during this build so a single scope_exit
993 // unmounts them all on success or on any throw partway through the loop below.
994 std::vector<std::string> mountedPaths;
995 auto unmountAll = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
996 for (const auto& path : mountedPaths)
997 {
998 // Best-effort but not silent: a failed unmount can leave a file-secret share mounted in the
999 // guest, so log it. Never throw here.
1000 LOG_IF_FAILED(runtime.Vm().UnmountWindowsFolder(path.c_str()));
1001 }
1002 });
1003 auto mountInVm = [&](LPCWSTR windowsPath, BOOL readOnly, std::string_view guestBase = "/mnt") -> std::string {
1004 GUID id{};
1005 THROW_IF_FAILED(CoCreateGuid(&id));
1006 auto vmPath = std::format("{}/{}", guestBase, wsl::shared::string::GuidToString<char>(id));
1007 THROW_IF_FAILED(runtime.Vm().MountWindowsFolder(windowsPath, vmPath.c_str(), readOnly));
1008 mountedPaths.push_back(std::move(vmPath));
1009 return mountedPaths.back();
1010 };
1011
1012 // Reserve up front so mountInVm's push_back can never reallocate-and-throw after a successful
1013 // MountWindowsFolder, which would leak a mount the scope_exit hasn't recorded yet. At most the build
1014 // context (1), the single-file exporter output destination (1), the --iidfile destination (1), and
1015 // one parent directory per file secret are mounted.
1016 mountedPaths.reserve(static_cast<size_t>(3) + Options->Secrets.Count);
1017
1018 // Environment for the docker process. Env/in-memory secrets are delivered as variables here so their
1019 // values never touch disk; kept off telemetry (only buildArgs is logged).
1020 std::vector<std::string> buildEnv;
1021
1022 if (policyState == WSLCVirtualMachine::BuildKitPolicyState::Configured)
1023 {
1024 buildEnv.emplace_back(std::string{"EXPERIMENTAL_BUILDKIT_SOURCE_POLICY="} + WSLCVirtualMachine::c_buildKitPolicyPath);
1025 }
1026
1027 auto mountPath = mountInVm(Options->ContextPath, TRUE);
1028
1029 // Progress is requested as JSON so it can be parsed into the formatted progress messages sent to the
1030 // client. The raw JSON is a docker implementation detail and is never forwarded.
1031 std::vector<std::string> buildArgs{"/usr/bin/docker", "buildx", "build", "--builder", "default", "--progress=rawjson"};
1032 if (WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsNoCache))
1033 {
1034 buildArgs.push_back("--no-cache");
1035 }
1036 if (WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsPull))
1037 {
1038 buildArgs.push_back("--pull");
1039 }
1040 if (Options->Target != nullptr && Options->Target[0] != '\0')
1041 {
1042 buildArgs.push_back("--target");
1043 buildArgs.push_back(Options->Target);
1044 }
1045 // Docker-style --output routing. Three cases, distinguished by what the client set:
1046 // * OutputHandle set (dest=- stdout): the client stripped dest= and expects the exporter output
1047 // streamed back. The exporter writes to the build process's stdout, which is relayed to the
1048 // client handle as the build runs, so the output never touches the VM's disk.
1049 // * OutputMountPath set (single-file exporter with a real destination): the client stripped dest=
1050 // and passed the destination file's parent directory, mounted read-write into the VM so buildx
1051 // writes the file (at OutputMountFile within the mount) in place - nothing is streamed back.
1052 // * Neither set: the spec is forwarded verbatim and the build runs entirely in the VM.
1053 // Directory exporters (type=local, or oci/docker with tar=false) are rejected by the client while
1054 // parsing --output: a Linux tree cannot be written faithfully to a Windows destination.
1055 const bool streamOutput = Options->OutputHandle.Type != WSLCHandleTypeUnknown;
1056 const bool mountOutput = Options->OutputMountPath != nullptr && Options->OutputMountPath[0] != L'\0';
1057 // Streaming or mounting the exporter output requires a non-empty Output spec to route from, and the
1058 // two destinations are mutually exclusive. Reject the mismatched combinations at the boundary rather
1059 // than later failing to assign a dest path.
1060 RETURN_HR_IF(E_INVALIDARG, (streamOutput || mountOutput) && (Options->Output == nullptr || Options->Output[0] == '\0'));
1061 RETURN_HR_IF(E_INVALIDARG, streamOutput && mountOutput);
1062
1063 if (Options->Output != nullptr && Options->Output[0] != '\0')
1064 {
1065 std::string outputSpec = Options->Output;
1066 if (streamOutput)
1067 {
1068 // buildx writes the exporter tarball to stdout for dest=-, which is relayed to the client
1069 // handle below. With no image to load, buildx prints no image ID, so stdout carries only
1070 // the tarball.
1071 outputSpec += ",dest=-";
1072 }
1073 else if (mountOutput)
1074 {
1075 // Mount the client's destination directory read-write and point the exporter at the temp file
1076 // to write within it, so buildx writes the single-file output straight to the Windows target.
1077 auto guestMountPath = mountInVm(Options->OutputMountPath, FALSE);
1078 std::string dest = guestMountPath;
1079 if (Options->OutputMountFile != nullptr && Options->OutputMountFile[0] != L'\0')
1080 {
1081 dest += '/';
1082 dest += wsl::shared::string::WideToMultiByte(Options->OutputMountFile);
1083 }
1084 outputSpec += std::format(",dest={}", dest);
1085 }
1086 buildArgs.push_back("--output");
1087 buildArgs.push_back(outputSpec);
1088 }
1089
1090 // Docker-style --iidfile. The destination's parent directory is mounted read-write into the VM so
1091 // buildx writes the image ID straight to the client's --iidfile path.
1092 if (Options->IidFilePath != nullptr && Options->IidFilePath[0] != L'\0')
1093 {
1094 std::filesystem::path iidPath(Options->IidFilePath);
1095 // The client and server have different current directories, so a relative path is ambiguous.
1096 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Options->IidFilePath), !iidPath.is_absolute());
1097
1098 auto iidParent = iidPath.parent_path();
1099 auto iidFileNameUtf8 = wsl::shared::string::WideToMultiByte(iidPath.filename().wstring());
1100 RETURN_HR_IF(E_INVALIDARG, iidParent.empty() || iidFileNameUtf8.empty());
1101
1102 auto iidMountPath = mountInVm(iidParent.c_str(), FALSE);
1103 buildArgs.push_back("--iidfile");
1104 buildArgs.push_back(std::format("{}/{}", iidMountPath, iidFileNameUtf8));
1105 }
1106 for (ULONG i = 0; i < Options->Tags.Count; i++)
1107 {
1108 RETURN_HR_IF_NULL(E_INVALIDARG, Options->Tags.Values[i]);
1109 RETURN_HR_IF(E_INVALIDARG, strlen(Options->Tags.Values[i]) > WSLC_MAX_IMAGE_NAME_LENGTH);
1110 buildArgs.push_back("-t");
1111 buildArgs.push_back(Options->Tags.Values[i]);
1112 }
1113 for (ULONG i = 0; i < Options->BuildArgs.Count; i++)
1114 {
1115 RETURN_HR_IF_NULL(E_INVALIDARG, Options->BuildArgs.Values[i]);
1116 RETURN_HR_IF(E_INVALIDARG, Options->BuildArgs.Values[i][0] == '-');
1117 buildArgs.push_back("--build-arg");
1118 buildArgs.push_back(Options->BuildArgs.Values[i]);
1119 }
1120 for (ULONG i = 0; i < Options->Labels.Count; i++)
1121 {
1122 RETURN_HR_IF_NULL(E_INVALIDARG, Options->Labels.Values[i]);
1123 RETURN_HR_IF(E_INVALIDARG, Options->Labels.Values[i][0] == '-');
1124 buildArgs.push_back("--label");
1125 buildArgs.push_back(Options->Labels.Values[i]);
1126 }
1127
1128 // Deliver each secret to the build without ever writing the value to disk on host or guest, keeping
1129 // it off argv/telemetry and re-readable across RUN steps - matching Docker's secret semantics. Two
1130 // kinds of secret are handled:
1131 //
1132 // * File (src=) secrets carry the resolved host path. We mount the file's *parent directory* into
1133 // the VM read-only and reference the file in place, so the bytes are never copied off their
1134 // original (possibly EFS-encrypted) location. Secrets sharing a directory reuse one mount.
1135 //
1136 // * Env/in-memory secrets carry raw bytes (there is no source file). We hand the value to BuildKit
1137 // through an environment variable of the docker process (id=<id>,env=<var>); nothing is written
1138 // to disk, so there is nothing to clean up.
1139 if (Options->Secrets.Count > 0)
1140 {
1141 // Guest tmpfs base for file-secret directory mounts: keeping them under /run means the secret
1142 // contents never hit the guest disk and leave nothing to clean up if the session crashes.
1143 constexpr std::string_view c_secretMountBase = "/run/build-secrets";
1144
1145 // (id, source spec) pairs - the source spec is docker's "src=<path>" or "env=<var>" token -
1146 // emitted as --secret arguments once every secret is prepared.
1147 std::vector<std::pair<std::string, std::string>> secretArgs;
1148 secretArgs.reserve(Options->Secrets.Count);
1149
1150 // Dedup file-secret parent-directory mounts: secrets from the same host directory share a mount.
1151 std::map<std::filesystem::path, std::string> fileSecretDirMounts;
1152
1153 for (ULONG i = 0; i < Options->Secrets.Count; i++)
1154 {
1155 const auto& secret = Options->Secrets.Values[i];
1156 RETURN_HR_IF_MSG(E_INVALIDARG, secret.Id == nullptr, "Secret %u has a null id", i);
1157 RETURN_HR_IF_MSG(E_INVALIDARG, secret.Id[0] == '\0', "Secret %u has an empty id", i);
1158 RETURN_HR_IF_MSG(E_INVALIDARG, secret.Id[0] == '-', "Invalid secret id '%hs'", secret.Id);
1159 // Id is interpolated into docker's comma/'='-delimited --secret spec below, so reject any
1160 // ',' or '=' a malicious caller could use to inject extra options.
1161 RETURN_HR_IF_MSG(
1162 E_INVALIDARG,
1163 std::string_view(secret.Id).find_first_of(",=") != std::string_view::npos,
1164 "Invalid secret id '%hs'",
1165 secret.Id);
1166
1167 if (secret.SourcePath != nullptr)
1168 {
1169 // File secret: mount the file's parent directory read-only and reference the file in
1170 // place - the bytes are never copied. Mounting the whole directory (not just the file) is
1171 // inherent to virtiofs sharing a directory tree; sibling files are exposed to this user's
1172 // own build VM read-only for the build's duration only.
1173 std::filesystem::path sourcePath(secret.SourcePath);
1174 // The client and server may have different current directories, so a relative path is
1175 // ambiguous - require an absolute path. An empty SourcePath is not absolute, so a
1176 // malformed file secret fails here rather than being treated as an env secret.
1177 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(secret.SourcePath), !sourcePath.is_absolute());
1178 auto parent = sourcePath.parent_path();
1179 auto fileNameUtf8 = sourcePath.filename().string();
1180 RETURN_HR_IF(E_INVALIDARG, parent.empty() || fileNameUtf8.empty());
1181 // The filename is interpolated into the CSV --secret spec; a ',' or '"' would corrupt it.
1182 RETURN_HR_IF(E_INVALIDARG, fileNameUtf8.find_first_of(",\"") != std::string::npos);
1183
1184 auto it = fileSecretDirMounts.find(parent);
1185 if (it == fileSecretDirMounts.end())
1186 {
1187 it = fileSecretDirMounts.emplace(parent, mountInVm(parent.c_str(), TRUE, c_secretMountBase)).first;
1188 }
1189 secretArgs.emplace_back(secret.Id, std::format("src={}/{}", it->second, fileNameUtf8));
1190 }
1191 else
1192 {
1193 // Env/in-memory secret: hand the value to BuildKit through an environment variable of the
1194 // docker process. BuildKit reads it (id=<id>,env=<var>) and streams it to the daemon, so
1195 // the value never touches disk on host or guest and needs no cleanup.
1196 RETURN_HR_IF(E_INVALIDARG, secret.ValueSize != 0 && secret.Value == nullptr);
1197 std::string_view value;
1198 if (secret.ValueSize != 0)
1199 {
1200 value = std::string_view(reinterpret_cast<const char*>(secret.Value), secret.ValueSize);
1201 }
1202 // An environment variable value cannot contain a NUL; reject rather than silently
1203 // truncate the secret.
1204 RETURN_HR_IF(E_INVALIDARG, value.find('\0') != std::string_view::npos);
1205
1206 auto varName = std::format("WSLC_SECRET_{}", std::to_string(i));
1207
1208 buildEnv.push_back(std::format("{}={}", varName, value));
1209 secretArgs.emplace_back(secret.Id, std::format("env={}", varName));
1210 }
1211 }
1212
1213 for (const auto& [id, source] : secretArgs)
1214 {
1215 buildArgs.push_back("--secret");
1216 buildArgs.push_back(std::format("id={},{}", id, source));
1217 }
1218 }
1219
1220 buildArgs.push_back("-f");
1221 buildArgs.push_back("-");
1222 buildArgs.push_back(mountPath);
1223
1224 WSL_LOG("BuildImageStart", TraceLoggingValue(wsl::shared::string::Join(buildArgs, ' ').c_str(), "Command"));
1225
1226 ServiceProcessLauncher buildLauncher(buildArgs[0], buildArgs, buildEnv, WSLCProcessFlagsStdin);
1227 auto buildProcess = buildLauncher.Launch(runtime.Vm());
1228
1229 // Opened before the IO context so it outlives the relay registered on it below.
1230 std::optional<UserHandle> userHandle;
1231 if (streamOutput)
1232 {
1233 userHandle.emplace(OpenUserHandle(Options->OutputHandle));
1234 }
1235
1236 auto io = CreateIOContext();
1237
1238 io.AddHandle(
1239 std::make_unique<io::RelayHandle<io::ReadHandle>>(buildFileHandle.Get(), common::io::HandleWrapper{buildProcess.GetStdHandle(WSLCFDStdin)}),
1240 MultiHandleWait::NeedNotComplete,
1241 [&buildProcess]() {
1242 // If we receive an error relaying stdin, it could be because the process exited.
1243 // Wait up to one second for the process to exit so errors in this relay don't override the actual build result.
1244 if (!buildProcess.GetExitEvent().wait(1000))
1245 {
1246 // Otherwise, throw the error and cancel the build.
1247 throw;
1248 }
1249 });
1250
1251 bool verbose = WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsVerbose);
1252 std::string allOutput;
1253 std::string pendingJson;
1254 std::set<std::string> reportedSteps;
1255 std::set<std::string> reportedCached;
1256 std::set<std::string> reportedErrors;
1257 std::map<std::string, std::string> digestToStageName;
1258 bool needsNewline = false; // true when the last log chunk didn't end with \n
1259 std::string lastLogVertex; // digest of the vertex that produced the last log output
1260
1261 // Extract the named build stage from a BuildKit vertex name. Vertices within the same named stage
1262 // (e.g. "[builder 1/3]" and "[builder 2/3]") share a key. Returns empty for unnamed stages.
1263 auto getStageName = [](const std::string& name) -> std::string {
1264 if (name.size() < 2 || name[0] != '[')
1265 {
1266 return {};
1267 }
1268
1269 auto close = name.find(']');
1270 if (close == std::string::npos)
1271 {
1272 return {};
1273 }
1274
1275 // Pattern: "[name N/M]" or "[N/M]". The stage name is the part before "N/M".
1276 std::string content = name.substr(1, close - 1);
1277 auto slash = content.find('/');
1278 if (slash != std::string::npos)
1279 {
1280 auto space = content.rfind(' ', slash);
1281 if (space != std::string::npos)
1282 {
1283 return content.substr(0, space);
1284 }
1285 }
1286
1287 return {};
1288 };
1289
1290 // Returns the leading step token from a BuildKit vertex name, e.g. "[2/3]" from "[2/3] RUN make".
1291 // Falls back to the full name when there is no bracketed prefix.
1292 auto getStepToken = [](const std::string& name) -> std::string {
1293 if (name.empty() || name[0] != '[')
1294 {
1295 return name;
1296 }
1297
1298 auto close = name.find(']');
1299 if (close == std::string::npos)
1300 {
1301 return name;
1302 }
1303
1304 return name.substr(0, close + 1);
1305 };
1306
1307 auto logPrefix = [](const std::string& name) -> std::string {
1308 if (name.empty())
1309 {
1310 return " | ";
1311 }
1312 return " [" + name + "] ";
1313 };
1314
1315 auto reportProgress = [&](const std::string& message, const char* id = "", ULONGLONG current = 0, ULONGLONG total = 0) {
1316 if (ProgressCallback != nullptr)
1317 {
1318 THROW_IF_FAILED(ProgressCallback->OnProgress(message.c_str(), id, current, total));
1319 }
1320 };
1321
1322 static constexpr char c_logId[] = "log";
1323
1324 auto flushLine = [&]() {
1325 if (needsNewline)
1326 {
1327 reportProgress("\n", c_logId);
1328 needsNewline = false;
1329 }
1330 };
1331
1332 // Accumulate lines and use accept() to detect complete JSON objects. Check for non-JSON lines between JSON objects and add
1333 // them to the output in case they contain helpful information about the build.
1334 auto captureOutput = [&](const gsl::span<char>& content) {
1335 std::string line{content.begin(), content.end()};
1336
1337 pendingJson.append(line);
1338
1339 if (!nlohmann::json::accept(pendingJson))
1340 {
1341 if (pendingJson.empty() || pendingJson[0] != '{')
1342 {
1343 allOutput.append(pendingJson).append("\n");
1344 pendingJson.clear();
1345 }
1346
1347 return;
1348 }
1349
1350 auto json = nlohmann::json::parse(pendingJson);
1351 pendingJson.clear();
1352
1353 docker_schema::BuildKitSolveStatus status{};
1354 from_json(json, status);
1355
1356 // Process vertices before logs so digestToStageName is populated for log correlation.
1357 for (const auto& vertex : status.vertexes)
1358 {
1359 if (!verbose && vertex.name.find("[internal]") != std::string::npos)
1360 {
1361 continue;
1362 }
1363
1364 digestToStageName.try_emplace(vertex.digest, getStageName(vertex.name));
1365
1366 if (!vertex.started.empty() && reportedSteps.insert(vertex.digest).second)
1367 {
1368 flushLine();
1369 reportProgress(vertex.name + "\n");
1370 }
1371
1372 if (vertex.cached && reportedCached.insert(vertex.digest).second)
1373 {
1374 auto stepToken = getStepToken(vertex.name);
1375 if (!stepToken.empty())
1376 {
1377 flushLine();
1378 reportProgress(stepToken + " CACHED\n");
1379 }
1380 }
1381
1382 if (!vertex.error.empty() && reportedErrors.insert(vertex.digest).second)
1383 {
1384 flushLine();
1385 reportProgress(vertex.error + "\n");
1386 }
1387 }
1388
1389 for (const auto& log : status.logs)
1390 {
1391 if (auto it = digestToStageName.find(log.vertex); it != digestToStageName.end() && !log.data.empty())
1392 {
1393 std::string decoded = wslutil::Base64Decode(log.data);
1394 if (!decoded.empty())
1395 {
1396 // The first character of this chunk begins a new line (and so needs a stage prefix) unless it
1397 // continues an unterminated line from the same vertex.
1398 bool continuingLine = needsNewline && log.vertex == lastLogVertex;
1399
1400 if (log.vertex != lastLogVertex && decoded[0] != '\n')
1401 {
1402 flushLine();
1403 }
1404
1405 // When continuing an unterminated line, emit the leading \n or \r directly
1406 // so it terminates/overwrites cleanly without a spurious prefix.
1407 if (needsNewline && (decoded[0] == '\n' || decoded[0] == '\r'))
1408 {
1409 reportProgress(decoded.substr(0, 1), c_logId);
1410 decoded.erase(0, 1);
1411
1412 continuingLine = false;
1413 }
1414
1415 if (!decoded.empty())
1416 {
1417 reportProgress(IndentLines(decoded, logPrefix(it->second), !continuingLine), c_logId);
1418 }
1419
1420 needsNewline = !decoded.empty() && decoded.back() != '\n';
1421 lastLogVertex = log.vertex;
1422 }
1423 }
1424 }
1425
1426 for (const auto& entry : status.statuses)
1427 {
1428 auto it = digestToStageName.find(entry.vertex);
1429 if (it == digestToStageName.end() || entry.id.empty())
1430 {
1431 continue;
1432 }
1433
1434 if (entry.total > 0)
1435 {
1436 auto currentBytes = static_cast<ULONGLONG>(std::max<int64_t>(entry.current, 0));
1437 auto totalBytes = static_cast<ULONGLONG>(std::max<int64_t>(entry.total, 0));
1438 auto current = FormatHumanReadableSize(currentBytes, c_progressPrecision);
1439 auto total = FormatHumanReadableSize(totalBytes, c_progressPrecision);
1440 reportProgress(std::format("{}{} {} / {}", logPrefix(it->second), entry.id, current, total), entry.id.c_str(), currentBytes, totalBytes);
1441 }
1442 else if (reportedSteps.insert(entry.id).second)
1443 {
1444 flushLine();
1445 reportProgress(logPrefix(it->second) + entry.id + "\n");
1446 }
1447 }
1448 };
1449
1450 // Docker writes progress to stderr and the final image ID to stdout on success (empty on failure).
1451 //
1452 // For dest=- the exporter tarball is written to stdout, so it is relayed to the client handle as the
1453 // build runs. RelayHandle is an overlapped handle, so a slow client only marks the relay pending and
1454 // stderr keeps draining in the same IO loop.
1455 if (streamOutput)
1456 {
1457 io.AddHandle(std::make_unique<io::RelayHandle<io::ReadHandle>>(
1458 common::io::HandleWrapper{buildProcess.GetStdHandle(1)}, userHandle->Get()));
1459 }
1460 else
1461 {
1462 io.AddHandle(std::make_unique<io::ReadHandle>(
1463 buildProcess.GetStdHandle(1), [&](const auto& content) { allOutput.append(content.begin(), content.end()); }));
1464 }
1465
1466 io.AddHandle(std::make_unique<io::LineBasedReadHandle>(buildProcess.GetStdHandle(2), captureOutput, false));
1467
1468 // Handle cancellation within the IO loop (NeedNotComplete) so pipes keep draining.
1469 bool cancelled = false;
1470 wil::unique_handle killTimer;
1471 if (CancelEvent != nullptr)
1472 {
1473 killTimer.reset(CreateWaitableTimer(nullptr, TRUE, nullptr));
1474 THROW_LAST_ERROR_IF_NULL(killTimer);
1475
1476 io.AddHandle(
1477 std::make_unique<io::EventHandle>(
1478 CancelEvent,
1479 [&]() {
1480 cancelled = true;
1481 LOG_IF_FAILED(buildProcess.Get().Signal(WSLCSignalSIGTERM));
1482 LARGE_INTEGER dueTime{.QuadPart = -10LL * 10 * 1000 * 1000}; // 10 seconds
1483 THROW_IF_WIN32_BOOL_FALSE(SetWaitableTimer(killTimer.get(), &dueTime, 0, nullptr, nullptr, FALSE));
1484 }),
1485 io::MultiHandleWait::NeedNotComplete);
1486
1487 io.AddHandle(
1488 std::make_unique<io::EventHandle>(
1489 killTimer.get(), [&]() { LOG_IF_FAILED(buildProcess.Get().Signal(WSLCSignalSIGKILL)); }),
1490 io::MultiHandleWait::NeedNotComplete);
1491 }
1492
1493 try
1494 {
1495 io.Run({});
1496 }
1497 catch (...)
1498 {
1499 flushLine();
1500 LOG_IF_FAILED(buildProcess.Get().Signal(WSLCSignalSIGTERM));
1501 try
1502 {
1503 buildProcess.Wait(10 * 1000);
1504 }
1505 catch (...)
1506 {
1507 if (wil::ResultFromCaughtException() == HRESULT_FROM_WIN32(ERROR_TIMEOUT))
1508 {
1509 LOG_IF_FAILED(buildProcess.Get().Signal(WSLCSignalSIGKILL));
1510 try
1511 {
1512 buildProcess.Wait(10 * 1000);
1513 }
1514 catch (...)
1515 {
1516 LOG_CAUGHT_EXCEPTION_MSG("Build process did not exit after SIGKILL");
1517 }
1518 }
1519 }
1520 throw;
1521 }
1522
1523 flushLine();
1524
1525 THROW_HR_IF_MSG(E_ABORT, cancelled, "Cancellation handle was signaled");
1526
1527 int exitCode = buildProcess.Wait();
1528 WSL_LOG("BuildImageComplete", TraceLoggingValue(exitCode, "ExitCode"));
1529 // Strip \r from the error output. The captured docker output sometimes contains
1530 // \r\n line endings (e.g., in the Dockerfile context BuildKit prints on failure).
1531 // When the CRT writes stderr in text mode it translates each \n to \r\n, turning
1532 // \r\n into \r\r\n. cmd.exe's 2> writes that as-is (one line break), but
1533 // PowerShell's 2> treats it as two line breaks and double-spaces the output.
1534 // Stripping \r normalizes to plain \n which becomes \r\n once via text-mode
1535 // translation.
1536 std::erase(allOutput, '\r');
1537 THROW_HR_WITH_USER_ERROR_IF(E_FAIL, allOutput, exitCode != 0);
1538
1539 return S_OK;
1540 }
1541 CATCH_RETURN();
1542
1543 HRESULT WSLCSession::LoadImage(const WSLCHandle ImageHandle, ULONGLONG ContentSize, IWarningCallback* WarningCallback, IImageLoadCallback* LoadCallback)
1544 try
1545 {
1546 WSLCExecutionContext context(this, WarningCallback);
1547
1548 auto lock = AcquireLease();
1549
1550 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1551
1552 auto requestContext = m_runtime.Docker().LoadImage(ContentSize);
1553
1554 std::ignore = ImportImageImpl(*requestContext, ImageHandle, LoadCallback);
1555
1556 return S_OK;
1557 }
1558 CATCH_RETURN();
1559
1560 HRESULT WSLCSession::ImportImage(const WSLCHandle ImageHandle, LPCSTR ImageName, ULONGLONG ContentSize, IWarningCallback* WarningCallback, LPSTR* ImageId)
1561 try
1562 {
1563 WSLCExecutionContext context(this, WarningCallback);
1564
1565 RETURN_HR_IF_NULL(E_POINTER, ImageId);
1566 *ImageId = nullptr;
1567
1568 std::string repo;
1569 std::string tag;
1570
1571 if (ImageName != nullptr)
1572 {
1573 RETURN_HR_IF(E_INVALIDARG, strlen(ImageName) > WSLC_MAX_IMAGE_NAME_LENGTH);
1574
1575 auto reference = wslutil::ImageReference::Parse(ImageName);
1576 auto tagOrDigest = reference.TagOrDigest();
1577 THROW_HR_IF_MSG(E_INVALIDARG, !tagOrDigest.has_value(), "Expected tag for image import: %hs", ImageName);
1578 repo = reference.Repository.Name;
1579 tag = tagOrDigest.value();
1580 }
1581
1582 auto lock = AcquireLease();
1583
1584 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1585
1586 auto requestContext = m_runtime.Docker().ImportImage(repo, tag, ContentSize);
1587
1588 auto imageId = ImportImageImpl(*requestContext, ImageHandle);
1589 THROW_HR_IF_MSG(E_UNEXPECTED, !imageId.has_value(), "Docker import succeeded but did not return an image ID");
1590
1591 if (ImageName != nullptr && strlen(ImageName) > 0)
1592 {
1593 OnImageCreated(ImageName);
1594 }
1595 else
1596 {
1597 OnImageCreated(imageId->c_str());
1598 }
1599
1600 *ImageId = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(imageId->c_str()).release();
1601
1602 return S_OK;
1603 }
1604 CATCH_RETURN();
1605
1606 std::optional<std::string> WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle, IImageLoadCallback* LoadCallback)
1607 {
1608 auto userHandle = OpenUserHandle(ImageHandle);
1609
1610 std::optional<UserCOMCallback> comCall;
1611 if (LoadCallback != nullptr)
1612 {
1613 comCall = RegisterUserCOMCallback();
1614 }
1615
1616 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1617
1618 auto io = CreateIOContext();
1619
1620 std::optional<std::string> pendingErrorJson;
1621 std::optional<std::string> imageId;
1622 auto onHttpResponse = [&](const boost::beast::http::message<false, boost::beast::http::buffer_body>& response) {
1623 WSL_LOG("ImageImportHttpResponse", TraceLoggingValue(static_cast<int>(response.result()), "StatusCode"));
1624
1625 if (response.result_int() != 200)
1626 {
1627 auto it = response.find(boost::beast::http::field::content_type);
1628
1629 THROW_HR_IF_MSG(
1630 E_UNEXPECTED,
1631 it == response.end() || !it->value().starts_with("application/json"),
1632 "Received HTTP %i but Content-Type is not json",
1633 response.result_int());
1634
1635 pendingErrorJson.emplace();
1636 }
1637 };
1638
1639 std::optional<std::string> errorMessage;
1640 auto onProgress = [&](const gsl::span<char>& buffer) {
1641 if (pendingErrorJson.has_value())
1642 {
1643 // If we received a non-200 status code, then the response body is an error message. Accumulate to the error message.
1644 pendingErrorJson->append(buffer.data(), buffer.size());
1645 return;
1646 }
1647
1648 auto parsed = shared::FromJson<docker_schema::ImageLoadResult>(std::string(buffer.begin(), buffer.end()).c_str());
1649
1650 if (parsed.errorDetail.has_value())
1651 {
1652 if (errorMessage.has_value())
1653 {
1654 LOG_HR_MSG(
1655 E_UNEXPECTED,
1656 "Overriding previous error message '%hs' with new message '%hs'",
1657 errorMessage->c_str(),
1658 parsed.errorDetail->message.c_str());
1659 EMIT_USER_WARNING(wsl::shared::string::MultiByteToWide(*errorMessage));
1660 }
1661
1662 errorMessage = FormatDockerEngineError(parsed.errorDetail->message);
1663 }
1664 else if (parsed.stream.has_value())
1665 {
1666 WSL_LOG("ImageImportProgress", TraceLoggingValue(parsed.stream->c_str(), "Content"));
1667
1668 {
1669 static constexpr std::string_view c_loadedImagePrefix = "Loaded image: ";
1670 static constexpr std::string_view c_loadedImageIdPrefix = "Loaded image ID: ";
1671
1672 for (const auto& entry : shared::string::Split(*parsed.stream, '\n'))
1673 {
1674 std::string name;
1675 EnumReferenceFormat format = EnumReferenceFormatNone;
1676 if (entry.starts_with(c_loadedImagePrefix))
1677 {
1678 name = entry.substr(c_loadedImagePrefix.size());
1679 format = EnumReferenceFormatTag;
1680 }
1681 else if (entry.starts_with(c_loadedImageIdPrefix))
1682 {
1683 name = entry.substr(c_loadedImageIdPrefix.size());
1684 format = EnumReferenceFormatDigest;
1685 }
1686
1687 if (!name.empty())
1688 {
1689 OnImageCreated(name);
1690
1691 if (LoadCallback != nullptr)
1692 {
1693 THROW_IF_FAILED(LoadCallback->OnImageLoaded(name.c_str(), format));
1694 }
1695 }
1696 }
1697 }
1698 }
1699 else if (parsed.status.has_value())
1700 {
1701 WSL_LOG("ImageImportProgress", TraceLoggingValue(parsed.status->c_str(), "Status"));
1702 if (parsed.status->starts_with("sha256:"))
1703 {
1704 THROW_HR_IF_MSG(E_UNEXPECTED, imageId.has_value(), "Received duplicate image ID in import status");
1705 imageId = *parsed.status;
1706 }
1707 }
1708 else
1709 {
1710 LOG_HR_MSG(E_UNEXPECTED, "Failed to parse import progress: %.*hs", static_cast<int>(buffer.size()), buffer.data());
1711 EMIT_USER_WARNING(Localization::MessageWslcImportProgressParseFailed());
1712 }
1713 };
1714
1715 // Shutdown the Docker stream's write side when the user pipe is closed.
1716 // This is required for Docker to know when the request body is complete.
1717 auto onInputComplete = [socket = Request.stream.native_handle()]() {
1718 LOG_LAST_ERROR_IF(shutdown(socket, SD_SEND) == SOCKET_ERROR);
1719 };
1720
1721 io.AddHandle(
1722 std::make_unique<io::RelayHandle<io::ReadHandle>>(
1723 common::io::HandleWrapper{userHandle.Get(), std::move(onInputComplete)}, common::io::HandleWrapper{Request.stream.native_handle()}),
1724 MultiHandleWait::NeedNotComplete);
1725
1726 io.AddHandle(std::make_unique<DockerHTTPClient::DockerHttpResponseHandle>(Request, std::move(onHttpResponse), std::move(onProgress)));
1727
1728 io.Run({});
1729
1730 // Look for an error message returned as an HTTP response (non HTTP 200)
1731 if (pendingErrorJson.has_value())
1732 {
1733 auto error = wsl::shared::FromJson<docker_schema::ErrorResponse>(pendingErrorJson->c_str());
1734
1735 THROW_HR_WITH_USER_ERROR(E_FAIL, FormatDockerEngineError(error.message));
1736 }
1737
1738 // Otherwise look for an error message returned via the progress stream (HTTP 200 followed by a stream error).
1739 THROW_HR_WITH_USER_ERROR_IF(E_FAIL, errorMessage.value(), errorMessage.has_value());
1740
1741 return imageId;
1742 }
1743
1744 HRESULT WSLCSession::SaveImage(WSLCHandle OutHandle, LPCSTR ImageNameOrID, IProgressCallback* ProgressCallback, HANDLE CancelEvent)
1745 try
1746 {
1747 UNREFERENCED_PARAMETER(ProgressCallback);
1748
1749 WSLCExecutionContext context(this);
1750
1751 RETURN_HR_IF_NULL(E_POINTER, ImageNameOrID);
1752 RETURN_HR_IF(E_INVALIDARG, strlen(ImageNameOrID) > WSLC_MAX_IMAGE_NAME_LENGTH);
1753 auto lock = AcquireLease();
1754
1755 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1756
1757 auto retVal = m_runtime.Docker().SaveImage(ImageNameOrID);
1758 SaveImageImpl(retVal, OutHandle, CancelEvent);
1759 return S_OK;
1760 }
1761 CATCH_RETURN();
1762
1763 HRESULT WSLCSession::SaveImages(WSLCHandle OutHandle, const WSLCStringArray* ImageNames, IProgressCallback* ProgressCallback, HANDLE CancelEvent)
1764 try
1765 {
1766 UNREFERENCED_PARAMETER(ProgressCallback);
1767
1768 COMServiceExecutionContext context;
1769
1770 RETURN_HR_IF_NULL(E_POINTER, ImageNames);
1771 RETURN_HR_IF(E_INVALIDARG, ImageNames->Count == 0);
1772 RETURN_HR_IF(E_INVALIDARG, ImageNames->Count > WSLC_MAX_SAVE_IMAGES_COUNT);
1773 RETURN_HR_IF_NULL(E_INVALIDARG, ImageNames->Values);
1774
1775 std::vector<std::string> names;
1776 names.reserve(ImageNames->Count);
1777 for (ULONG i = 0; i < ImageNames->Count; i += 1)
1778 {
1779 RETURN_HR_IF_NULL(E_INVALIDARG, ImageNames->Values[i]);
1780 const size_t length = strlen(ImageNames->Values[i]);
1781 RETURN_HR_IF(E_INVALIDARG, length == 0);
1782 RETURN_HR_IF(E_INVALIDARG, length > WSLC_MAX_IMAGE_NAME_LENGTH);
1783 names.emplace_back(ImageNames->Values[i]);
1784 }
1785
1786 auto lock = AcquireLease();
1787
1788 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1789
1790 auto retVal = m_runtime.Docker().SaveImages(names);
1791 SaveImageImpl(retVal, OutHandle, CancelEvent);
1792 return S_OK;
1793 }
1794 CATCH_RETURN();
1795
1796 void WSLCSession::SaveImageImpl(std::pair<uint32_t, wil::unique_socket>& SocketCodePair, WSLCHandle OutputHandle, HANDLE CancelEvent)
1797 {
1798 auto userHandle = OpenUserHandle(OutputHandle);
1799
1800 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1801
1802 auto io = CreateIOContext(CancelEvent);
1803
1804 std::string errorJson;
1805
1806 if (SocketCodePair.first != 200)
1807 {
1808 auto accumulateError = [&](const gsl::span<char>& buffer) {
1809 // If the save failed, accumulate the error message.
1810 errorJson.append(buffer.data(), buffer.size());
1811 };
1812
1813 io.AddHandle(std::make_unique<io::ReadHandle>(common::io::HandleWrapper{std::move(SocketCodePair.second)}, std::move(accumulateError)));
1814 }
1815 else
1816 {
1817 io.AddHandle(std::make_unique<io::RelayHandle<io::HTTPChunkBasedReadHandle>>(
1818 common::io::HandleWrapper{std::move(SocketCodePair.second)}, userHandle.Get()));
1819 }
1820
1821 io.Run({});
1822
1823 if (SocketCodePair.first != 200)
1824 {
1825 // Save failed, parse the error message.
1826 auto error = wsl::shared::FromJson<docker_schema::ErrorResponse>(errorJson.c_str());
1827 const auto errorMessage = FormatDockerEngineError(error.message);
1828 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_IMAGE_NOT_FOUND, errorMessage, SocketCodePair.first == 404);
1829 THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
1830 }
1831 }
1832
1833 HRESULT WSLCSession::ListImages(const WSLCListImagesOptions* Options, WSLCImageInformation** Images, ULONG* Count)
1834 try
1835 {
1836 WSLCExecutionContext context(this);
1837
1838 RETURN_HR_IF_NULL(E_POINTER, Images);
1839 RETURN_HR_IF_NULL(E_POINTER, Count);
1840
1841 *Count = 0;
1842 *Images = nullptr;
1843
1844 bool all = false;
1845 bool digests = false;
1846 bool containerCounts = false;
1847 std::map<std::string, std::vector<std::string>> filters;
1848
1849 if (Options != nullptr)
1850 {
1851 THROW_HR_IF_MSG(
1852 E_INVALIDARG,
1853 WI_IsAnyFlagSet(static_cast<WSLCListImagesFlags>(Options->Flags), ~WSLCListImagesFlagsValid),
1854 "Invalid flags: 0x%lx",
1855 Options->Flags);
1856
1857 all = WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsAll);
1858 digests = WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsDigests);
1859 containerCounts = WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsContainerCounts);
1860
1861 filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount);
1862 }
1863
1864 auto lock = AcquireLease();
1865
1866 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1867
1868 // The container count is gathered under the container lock alongside the image list so that no
1869 // container can be created or removed in between, which would report counts for a set of images
1870 // that no longer matches the listing.
1871 std::unique_lock<std::mutex> containersLock;
1872 if (containerCounts)
1873 {
1874 containersLock = std::unique_lock{m_containersLock};
1875 }
1876
1877 std::vector<docker_schema::Image> images;
1878 try
1879 {
1880 images = m_runtime.Docker().ListImages(all, digests, filters);
1881 }
1882 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list images");
1883
1884 // Stopped containers are included, matching docker.
1885 std::map<std::string, LONGLONG> containersByImage;
1886 if (containerCounts)
1887 {
1888 try
1889 {
1890 for (const auto& container : m_runtime.Docker().ListContainers(true))
1891 {
1892 containersByImage[container.ImageID]++;
1893 }
1894 }
1895 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list containers");
1896 }
1897
1898 const auto containersForImage = [&](const std::string& id) {
1899 if (!containerCounts)
1900 {
1901 return -1LL;
1902 }
1903
1904 const auto it = containersByImage.find(id);
1905 return it == containersByImage.end() ? 0LL : it->second;
1906 };
1907
1908 // Compute the number of entries - one entry per tag, or one per image if no tags
1909 auto entries = std::accumulate(images.begin(), images.end(), size_t{0}, [](auto sum, const auto& e) {
1910 return sum + (e.RepoTags.empty() ? 1 : e.RepoTags.size());
1911 });
1912
1913 auto output = wil::make_unique_cotaskmem<WSLCImageInformation[]>(entries);
1914
1915 size_t index = 0;
1916 for (const auto& e : images)
1917 {
1918 // Build a map from repo name to digest for this image
1919 // RepoDigests format: "repo@sha256:digest"
1920 std::map<std::string, std::string> repoToDigest;
1921 for (const auto& repoDigest : e.RepoDigests)
1922 {
1923 size_t atPos = repoDigest.find('@');
1924 THROW_HR_IF(E_UNEXPECTED, atPos == std::string::npos || atPos == 0);
1925 std::string repoName = repoDigest.substr(0, atPos);
1926 repoToDigest[repoName] = repoDigest;
1927 }
1928
1929 if (e.RepoTags.empty())
1930 {
1931 // Image has no tags (dangling image)
1932 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, "<none>:<none>") != 0);
1933 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Hash, e.Id.c_str()) != 0);
1934
1935 // Set digest if available
1936 if (!e.RepoDigests.empty())
1937 {
1938 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Digest, e.RepoDigests[0].c_str()) != 0);
1939 }
1940 else
1941 {
1942 output[index].Digest[0] = '\0';
1943 }
1944
1945 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].ParentId, e.ParentId.c_str()) != 0);
1946 output[index].Size = e.Size;
1947 output[index].Created = e.Created;
1948 output[index].Containers = containersForImage(e.Id);
1949 index++;
1950 }
1951 else
1952 {
1953 // Image has tags - create one entry per tag
1954 for (const auto& tag : e.RepoTags)
1955 {
1956 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, tag.c_str()) != 0);
1957 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Hash, e.Id.c_str()) != 0);
1958
1959 // Extract repo name from tag (format: "repo:tag")
1960 // and lookup corresponding digest from the map
1961 auto repoName = wslutil::ImageReference::Parse(tag).Repository.Name;
1962 auto it = repoToDigest.find(repoName);
1963 if (it != repoToDigest.end())
1964 {
1965 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Digest, it->second.c_str()) != 0);
1966 }
1967 else
1968 {
1969 output[index].Digest[0] = '\0';
1970 }
1971
1972 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].ParentId, e.ParentId.c_str()) != 0);
1973 output[index].Size = e.Size;
1974 output[index].Created = e.Created;
1975 output[index].Containers = containersForImage(e.Id);
1976 index++;
1977 }
1978 }
1979 }
1980
1981 WI_ASSERT(index == entries);
1982
1983 *Count = static_cast<ULONG>(entries);
1984 *Images = output.release();
1985 return S_OK;
1986 }
1987 CATCH_RETURN();
1988
1989 HRESULT WSLCSession::DeleteImage(const WSLCDeleteImageOptions* Options, WSLCDeletedImageInformation** DeletedImages, ULONG* Count)
1990 try
1991 {
1992 WSLCExecutionContext context(this);
1993
1994 RETURN_HR_IF_NULL(E_POINTER, Options);
1995 RETURN_HR_IF_NULL(E_POINTER, Options->Image);
1996 RETURN_HR_IF(E_INVALIDARG, strlen(Options->Image) > WSLC_MAX_IMAGE_NAME_LENGTH);
1997 THROW_HR_IF_MSG(
1998 E_INVALIDARG,
1999 WI_IsAnyFlagSet(static_cast<WSLCDeleteImageFlags>(Options->Flags), ~WSLCDeleteImageFlagsValid),
2000 "Invalid flags: 0x%x",
2001 Options->Flags);
2002 RETURN_HR_IF_NULL(E_POINTER, DeletedImages);
2003 RETURN_HR_IF_NULL(E_POINTER, Count);
2004
2005 *DeletedImages = nullptr;
2006 *Count = 0;
2007
2008 auto lock = AcquireLease();
2009
2010 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2011
2012 std::vector<docker_schema::DeletedImage> deletedImages;
2013 try
2014 {
2015 deletedImages = m_runtime.Docker().DeleteImage(
2016 Options->Image, WI_IsFlagSet(Options->Flags, WSLCDeleteImageFlagsForce), WI_IsFlagSet(Options->Flags, WSLCDeleteImageFlagsNoPrune));
2017 }
2018 catch (const DockerHTTPException& e)
2019 {
2020 std::string errorMessage;
2021 if ((e.StatusCode() >= 400 && e.StatusCode() < 500))
2022 {
2023 errorMessage = FormatDockerEngineError(e.DockerMessage<docker_schema::ErrorResponse>().message);
2024 }
2025
2026 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_IMAGE_NOT_FOUND, errorMessage, e.StatusCode() == 404);
2027 THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), errorMessage, e.StatusCode() == 409);
2028 THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
2029 }
2030
2031 THROW_HR_IF_MSG(E_FAIL, deletedImages.empty(), "Failed to delete image: %hs", Options->Image);
2032
2033 auto output = wil::make_unique_cotaskmem<WSLCDeletedImageInformation[]>(deletedImages.size());
2034
2035 size_t index = 0;
2036 for (const auto& image : deletedImages)
2037 {
2038 THROW_HR_IF(E_UNEXPECTED, (image.Deleted.empty() && image.Untagged.empty()) || (!image.Deleted.empty() && !image.Untagged.empty()));
2039
2040 if (!image.Deleted.empty())
2041 {
2042 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, image.Deleted.c_str()) != 0);
2043 output[index].Type = WSLCDeletedImageTypeDeleted;
2044 }
2045 else
2046 {
2047 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, image.Untagged.c_str()) != 0);
2048 output[index].Type = WSLCDeletedImageTypeUntagged;
2049 }
2050
2051 index++;
2052 }
2053
2054 *Count = static_cast<ULONG>(deletedImages.size());
2055 *DeletedImages = output.release();
2056
2057 // Notify plugin manager of all deleted image IDs.
2058 for (const auto& image : deletedImages)
2059 {
2060 if (!image.Deleted.empty())
2061 {
2062 OnImageDeleted(image.Deleted);
2063 }
2064 }
2065
2066 return S_OK;
2067 }
2068 CATCH_RETURN();
2069
2070 HRESULT WSLCSession::TagImage(const WSLCTagImageOptions* Options)
2071 try
2072 {
2073 WSLCExecutionContext context(this);
2074
2075 RETURN_HR_IF_NULL(E_POINTER, Options);
2076 RETURN_HR_IF_NULL(E_POINTER, Options->Image);
2077 RETURN_HR_IF(E_INVALIDARG, strlen(Options->Image) > WSLC_MAX_IMAGE_NAME_LENGTH);
2078 RETURN_HR_IF_NULL(E_POINTER, Options->Repo);
2079 RETURN_HR_IF_NULL(E_POINTER, Options->Tag);
2080 RETURN_HR_IF(E_INVALIDARG, strlen(Options->Repo) + strlen(Options->Tag) + 1 > WSLC_MAX_IMAGE_NAME_LENGTH);
2081
2082 auto lock = AcquireLease();
2083
2084 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2085
2086 try
2087 {
2088 m_runtime.Docker().TagImage(Options->Image, Options->Repo, Options->Tag);
2089 }
2090 catch (const DockerHTTPException& e)
2091 {
2092 std::string errorMessage;
2093 if ((e.StatusCode() >= 400 && e.StatusCode() < 500))
2094 {
2095 errorMessage = FormatDockerEngineError(e.DockerMessage<docker_schema::ErrorResponse>().message);
2096 }
2097
2098 THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_BAD_ARGUMENTS), errorMessage, e.StatusCode() == 400);
2099 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_IMAGE_NOT_FOUND, errorMessage, e.StatusCode() == 404);
2100 THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), errorMessage, e.StatusCode() == 409);
2101 THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
2102 }
2103
2104 return S_OK;
2105 }
2106 CATCH_RETURN();
2107
2108 HRESULT WSLCSession::PushImage(LPCSTR Image, LPCSTR RegistryAuthenticationInformation, IProgressCallback* ProgressCallback, IWarningCallback* WarningCallback)
2109 try
2110 {
2111 WSLCExecutionContext context(this, WarningCallback);
2112
2113 RETURN_HR_IF_NULL(E_POINTER, Image);
2114 RETURN_HR_IF_NULL(E_POINTER, RegistryAuthenticationInformation);
2115
2116 const auto reference = wslutil::ImageReference::Parse(Image);
2117 const auto& repo = reference.Repository;
2118 auto tagOrDigest = reference.TagOrDigest();
2119 EnforceRegistryAllowlist(repo);
2120
2121 auto lock = AcquireLease();
2122 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2123
2124 auto requestContext = m_runtime.Docker().PushImage(repo.Name, tagOrDigest, RegistryAuthenticationInformation);
2125 StreamImageOperation(*requestContext, Image, "Push", ProgressCallback);
2126
2127 return S_OK;
2128 }
2129 CATCH_RETURN();
2130
2131 HRESULT WSLCSession::InspectImage(_In_ LPCSTR ImageNameOrId, _Out_ LPSTR* Output)
2132 try
2133 {
2134 WSLCExecutionContext context(this);
2135
2136 RETURN_HR_IF_NULL(E_POINTER, ImageNameOrId);
2137 RETURN_HR_IF(E_INVALIDARG, strlen(ImageNameOrId) > WSLC_MAX_IMAGE_NAME_LENGTH);
2138 RETURN_HR_IF_NULL(E_POINTER, Output);
2139
2140 *Output = nullptr;
2141
2142 auto lock = AcquireLease();
2143 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2144
2145 *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(InspectImageLockHeld(ImageNameOrId).c_str()).release();
2146
2147 return S_OK;
2148 }
2149 CATCH_RETURN();
2150
2151 std::string WSLCSession::InspectImageLockHeld(const std::string& NameOrId)
2152 {
2153 docker_schema::InspectImage dockerInspect;
2154 try
2155 {
2156 dockerInspect = m_runtime.Docker().InspectImage(NameOrId);
2157 }
2158 catch (const DockerHTTPException& e)
2159 {
2160 std::string errorMessage = "Failed to inspect image";
2161 if (e.HasErrorMessage())
2162 {
2163 errorMessage = FormatDockerEngineError(e.DockerMessage<docker_schema::ErrorResponse>().message);
2164 }
2165
2166 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_IMAGE_NOT_FOUND, errorMessage, e.StatusCode() == 404);
2167 THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_BAD_ARGUMENTS), errorMessage, e.StatusCode() == 400);
2168 THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
2169 }
2170
2171 // Convert to WSLC schema
2172 auto wslcInspect = ConvertInspectImage(dockerInspect);
2173
2174 // Serialize to JSON
2175 return wsl::shared::ToJson(wslcInspect);
2176 }
2177
2178 HRESULT WSLCSession::Authenticate(_In_ LPCSTR ServerAddress, _In_ LPCSTR Username, _In_ LPCSTR Password, _Out_ LPSTR* IdentityToken)
2179 try
2180 {
2181 WSLCExecutionContext context(this);
2182
2183 RETURN_HR_IF_NULL(E_POINTER, ServerAddress);
2184 RETURN_HR_IF_NULL(E_POINTER, Username);
2185 RETURN_HR_IF_NULL(E_POINTER, Password);
2186 RETURN_HR_IF_NULL(E_POINTER, IdentityToken);
2187
2188 *IdentityToken = nullptr;
2189
2190 auto lock = AcquireLease();
2191 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2192
2193 wil::unique_cotaskmem_ansistring token;
2194
2195 try
2196 {
2197 auto response = m_runtime.Docker().Authenticate(ServerAddress, Username, Password);
2198 token = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(response.c_str());
2199 }
2200 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to authenticate with registry: %hs", ServerAddress);
2201
2202 *IdentityToken = token.release();
2203 return S_OK;
2204 }
2205 CATCH_RETURN();
2206
2207 HRESULT WSLCSession::PruneImages(
2208 const WSLCFilter* Filters, ULONG FiltersCount, WSLCDeletedImageInformation** DeletedImages, ULONG* DeletedImagesCount, ULONGLONG* SpaceReclaimed)
2209 try
2210 {
2211 WSLCExecutionContext context(this);
2212
2213 RETURN_HR_IF_NULL(E_POINTER, DeletedImages);
2214 RETURN_HR_IF_NULL(E_POINTER, DeletedImagesCount);
2215 RETURN_HR_IF_NULL(E_POINTER, SpaceReclaimed);
2216 *DeletedImages = nullptr;
2217 *DeletedImagesCount = 0;
2218 *SpaceReclaimed = 0;
2219
2220 auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
2221
2222 auto lock = AcquireLease();
2223 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2224
2225 docker_schema::PruneImageResult pruneResult;
2226 try
2227 {
2228 pruneResult = m_runtime.Docker().PruneImages(filters);
2229 }
2230 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune images");
2231
2232 *SpaceReclaimed = pruneResult.SpaceReclaimed;
2233
2234 if (pruneResult.ImagesDeleted.has_value() && !pruneResult.ImagesDeleted->empty())
2235 {
2236 auto output = wil::make_unique_cotaskmem<WSLCDeletedImageInformation[]>(pruneResult.ImagesDeleted->size());
2237 size_t index = 0;
2238 for (const auto& image : pruneResult.ImagesDeleted.value())
2239 {
2240 THROW_HR_IF(
2241 E_UNEXPECTED, (image.Deleted.empty() && image.Untagged.empty()) || (!image.Deleted.empty() && !image.Untagged.empty()));
2242
2243 if (!image.Deleted.empty())
2244 {
2245 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, image.Deleted.c_str()) != 0);
2246 output[index].Type = WSLCDeletedImageTypeDeleted;
2247 }
2248 else
2249 {
2250 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, image.Untagged.c_str()) != 0);
2251 output[index].Type = WSLCDeletedImageTypeUntagged;
2252 }
2253
2254 index++;
2255 }
2256
2257 *DeletedImages = output.release();
2258 *DeletedImagesCount = static_cast<ULONG>(pruneResult.ImagesDeleted->size());
2259 }
2260
2261 return S_OK;
2262 }
2263 CATCH_RETURN();
2264
2265 HRESULT WSLCSession::CreateContainer(const WSLCContainerOptions* containerOptions, IWarningCallback* WarningCallback, IWSLCContainer** Container)
2266 try
2267 {
2268 WSLCExecutionContext context(this, WarningCallback);
2269 THROW_HR_IF_NULL(E_POINTER, containerOptions);
2270 THROW_HR_IF_NULL(E_POINTER, Container);
2271 THROW_HR_IF_NULL(E_POINTER, containerOptions->Image);
2272 THROW_HR_IF_MSG(
2273 E_INVALIDARG,
2274 WI_IsAnyFlagSet(containerOptions->Flags, ~WSLCContainerFlagsValid),
2275 "Invalid container flags: 0x%x",
2276 containerOptions->Flags);
2277 THROW_HR_IF_MSG(
2278 E_INVALIDARG,
2279 WI_IsAnyFlagSet(containerOptions->InitProcessOptions.Flags, ~WSLCProcessFlagsValid),
2280 "Invalid process flags: 0x%x",
2281 containerOptions->InitProcessOptions.Flags);
2282
2283 auto lock = AcquireLease();
2284
2285 auto result = wil::ResultFromException([&]() { CreateContainerImpl(containerOptions, Container); });
2286
2287 // This telemetry event is used to keep track of the container creation failure rate and surface unexpected errors.
2288 WSL_LOG(
2289 "WSLCCreateContainer",
2290 TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage),
2291 TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA),
2292 TraceLoggingValue(result, "Result"),
2293 TraceLoggingValue(WSL_PACKAGE_VERSION, "wslVersion"),
2294 TraceLoggingValue(containerOptions->Image, "Image"),
2295 TraceLoggingValue(m_displayName.c_str(), "SessionName"),
2296 TraceLoggingValue(m_creatorProcessName.c_str(), "CreatorProcess"));
2297
2298 return result;
2299 }
2300 CATCH_RETURN();
2301
2302 void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptions, IWSLCContainer** Container)
2303 {
2304 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
2305 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasEvents());
2306 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2307 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes());
2308
2309 // Validate that name & images are valid.
2310 if (containerOptions->Name != nullptr && containerOptions->Name[0] != '\0')
2311 {
2312 ValidateName(containerOptions->Name, WSLC_MAX_CONTAINER_NAME_LENGTH);
2313 }
2314
2315 THROW_HR_IF(E_INVALIDARG, strlen(containerOptions->Image) > WSLC_MAX_IMAGE_NAME_LENGTH);
2316
2317 try
2318 {
2319 std::unique_lock containersLock{m_containersLock};
2320 WaitForConflictingCreateToComplete(containersLock);
2321 std::unique_lock networksLock{m_networksLock};
2322
2323 // Generate a unique container name if the user didn't provide one.
2324 std::string containerName;
2325 if (containerOptions->Name != nullptr && containerOptions->Name[0] != '\0')
2326 {
2327 containerName = containerOptions->Name;
2328 }
2329 else
2330 {
2331 constexpr int c_maxNameRetries = 6;
2332 for (int attempt = 0; attempt < c_maxNameRetries; attempt++)
2333 {
2334 auto randomName = GenerateContainerName(attempt);
2335 if (std::ranges::none_of(m_containers, [&](const auto& entry) { return entry.second->Name() == randomName; }))
2336 {
2337 containerName = randomName;
2338 break;
2339 }
2340 }
2341
2342 // Fallback to a GUID name.
2343 if (containerName.empty())
2344 {
2345 WSL_LOG("GenerateGuidContainerName");
2346 GUID guid{};
2347 THROW_IF_FAILED(CoCreateGuid(&guid));
2348 containerName = wsl::shared::string::GuidToString<char>(guid, wsl::shared::string::GuidToStringFlags::None);
2349 }
2350 }
2351
2352 auto container = WSLCContainerImpl::Create(
2353 *containerOptions,
2354 containerName,
2355 *this,
2356 m_runtime,
2357 m_pluginNotifier.get(),
2358 m_networks,
2359 std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
2360 m_eventStore);
2361
2362 auto pendingCreate = StartPendingCreate(container);
2363
2364 containersLock.unlock();
2365 networksLock.unlock();
2366
2367 // m_pendingCreate is published under m_containersLock before the event thread can observe it, so
2368 // OnContainerCreated() is guaranteed to complete this create unless the session tears down first.
2369 WaitForPendingCreateCompletion(pendingCreate);
2370
2371 if (pendingCreate->Exception)
2372 {
2373 std::rethrow_exception(pendingCreate->Exception);
2374 }
2375
2376 container->CopyTo(Container);
2377 }
2378 catch (const DockerHTTPException& e)
2379 {
2380 std::string errorMessage;
2381 if ((e.StatusCode() >= 400 && e.StatusCode() < 500))
2382 {
2383 errorMessage = FormatDockerEngineError(e.DockerMessage<docker_schema::ErrorResponse>().message);
2384 }
2385
2386 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_IMAGE_NOT_FOUND, errorMessage, e.StatusCode() == 404);
2387 THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), errorMessage, e.StatusCode() == 409);
2388 THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
2389 }
2390 }
2391
2392 __requires_lock_held(m_containersLock) std::shared_ptr<WSLCSession::PendingContainerCreate> WSLCSession::StartPendingCreate(std::shared_ptr<WSLCContainerImpl> Container)
2393 {
2394 WI_ASSERT(!m_pendingCreate);
2395
2396 m_pendingCreate = std::make_shared<PendingContainerCreate>();
2397 m_pendingCreate->Container = std::move(Container);
2398
2399 return m_pendingCreate;
2400 }
2401
2402 void WSLCSession::WaitForPendingCreateCompletion(const std::shared_ptr<PendingContainerCreate>& PendingCreate)
2403 {
2404 auto io = CreateIOContext();
2405 io.AddHandle(std::make_unique<io::EventHandle>(PendingCreate->Completed.get()));
2406
2407 try
2408 {
2409 io.Run(c_containerCreateEventTimeout);
2410 }
2411 catch (...)
2412 {
2413 if (wil::ResultFromCaughtException() != HRESULT_FROM_WIN32(ERROR_TIMEOUT))
2414 {
2415 throw;
2416 }
2417
2418 // Fail this create rather than leaving m_pendingCreate set, which would wedge every later one.
2419 // Any container docker did manage to create is left behind; the same broken event stream makes
2420 // deleting it unreliable, and a late create event is ignored once m_pendingCreate is cleared.
2421 std::lock_guard containersLock{m_containersLock};
2422 if (m_pendingCreate == PendingCreate)
2423 {
2424 CompletePendingCreate(PendingCreate, std::current_exception());
2425 }
2426 }
2427
2428 WI_ASSERT(PendingCreate->Completed.is_signaled());
2429 }
2430
2431 __requires_lock_held(m_containersLock) void WSLCSession::CompletePendingCreate(
2432 const std::shared_ptr<PendingContainerCreate>& PendingCreate, std::exception_ptr Exception) noexcept
2433 {
2434 WI_ASSERT(m_pendingCreate == PendingCreate);
2435 PendingCreate->Exception = std::move(Exception);
2436 m_pendingCreate.reset();
2437 PendingCreate->Completed.SetEvent();
2438 }
2439
2440 void WSLCSession::WaitForConflictingCreateToComplete(std::unique_lock<std::mutex>& ContainersLock)
2441 {
2442 while (m_pendingCreate)
2443 {
2444 auto pendingCreate = m_pendingCreate;
2445 ContainersLock.unlock();
2446
2447 WaitForPendingCreateCompletion(pendingCreate);
2448
2449 ContainersLock.lock();
2450 }
2451 }
2452
2453 void WSLCSession::OnContainerCreated(const std::string& ContainerId, std::int64_t Time) noexcept
2454 try
2455 {
2456 std::lock_guard containersLock{m_containersLock};
2457
2458 // Containers created behind our back (BuildKit, for instance) have no pending create to match.
2459 if (!m_pendingCreate || m_pendingCreate->Container->ID() != ContainerId)
2460 {
2461 return;
2462 }
2463
2464 auto pendingCreate = m_pendingCreate;
2465 std::exception_ptr exception;
2466
2467 try
2468 {
2469 // Key the map by Docker's container ID, which is set in the WSLCContainerImpl constructor and stable for its lifetime.
2470 WI_VERIFY(m_containers.emplace(ContainerId, pendingCreate->Container).second);
2471 pendingCreate->Container->RecordEvent("create", Time);
2472 }
2473 catch (...)
2474 {
2475 // Hand the failure to the waiting create rather than letting it return a container the session isn't tracking.
2476 exception = std::current_exception();
2477 }
2478
2479 CompletePendingCreate(pendingCreate, std::move(exception));
2480 }
2481 CATCH_LOG()
2482
2483 HRESULT WSLCSession::OpenContainer(LPCSTR Id, IWSLCContainer** Container)
2484 try
2485 {
2486 WSLCExecutionContext context(this);
2487
2488 RETURN_HR_IF_NULL(E_POINTER, Id);
2489 RETURN_HR_IF_NULL(E_POINTER, Container);
2490
2491 ValidateName(Id, WSLC_MAX_CONTAINER_NAME_LENGTH);
2492
2493 // Look for an exact ID match first.
2494 auto lock = AcquireLease();
2495 std::lock_guard containersLock{m_containersLock};
2496
2497 // Purge containers that were auto-deleted via OnEvent (--rm).
2498 std::erase_if(m_containers, [](const auto& entry) { return entry.second->State() == WslcContainerStateDeleted; });
2499 auto it = m_containers.find(Id);
2500
2501 // If no match is found, call Inspect() so that partial IDs and names are matched.
2502 if (it == m_containers.end())
2503 {
2504 // TODO: consider a trimmed down version of inspect to avoid parsing the full response.
2505 docker_schema::InspectContainer inspectResult;
2506
2507 try
2508 {
2509 inspectResult = m_runtime.Docker().InspectContainer(Id);
2510 }
2511 catch (DockerHTTPException& e)
2512 {
2513 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_FOUND, Localization::MessageWslcContainerNotFound(Id), e.StatusCode() == 404);
2514 RETURN_HR_IF_MSG(WSLC_E_CONTAINER_PREFIX_AMBIGUOUS, e.StatusCode() == 400, "Ambiguous prefix: '%hs'", Id);
2515
2516 THROW_HR_MSG(E_FAIL, "Unexpected error inspecting container '%hs': %hs", Id, e.what());
2517 }
2518
2519 it = m_containers.find(inspectResult.Id);
2520 RETURN_HR_IF_MSG(
2521 E_UNEXPECTED, it == m_containers.end(), "Resolved container ID (%hs -> %hs) not found", Id, inspectResult.Id.c_str());
2522 }
2523
2524 auto result = wil::ResultFromException([&]() { it->second->CopyTo(Container); });
2525
2526 // Return WSLC_E_CONTAINER_NOT_FOUND if the container was found, but is being deleted for consistency.
2527 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_FOUND, Localization::MessageWslcContainerNotFound(Id), result == RPC_E_DISCONNECTED);
2528
2529 return result;
2530 }
2531 CATCH_RETURN();
2532
2533 namespace {
2534
2535 // Activity token holds an activity reference to prevent idle VM teardown while client holds it.
2536 // Implements IFastRundown so crashed clients reclaim stub promptly instead of slow default rundown.
2537 class ContainerOperation
2538 : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IUnknown, IFastRundown>
2539 {
2540 public:
2541 // Adopts an activity reference from CreateActivityToken; callback releases it.
2542 void Initialize(std::function<void()>&& onRelease) noexcept
2543 {
2544 m_onRelease = std::move(onRelease);
2545 }
2546
2547 ~ContainerOperation() override
2548 {
2549 if (m_onRelease)
2550 {
2551 m_onRelease();
2552 }
2553 }
2554
2555 private:
2556 std::function<void()> m_onRelease;
2557 };
2558
2559 } // namespace
2560
2561 Microsoft::WRL::ComPtr<IUnknown> WSLCSession::CreateActivityToken()
2562 {
2563 // Record the in-flight activity up front so the VM cannot idle-terminate before the caller
2564 // takes ownership of the returned token.
2565 m_runtime.Idle().AddActivity();
2566 auto countCleanup = wil::scope_exit([this]() { m_runtime.Idle().ReleaseActivity(); });
2567
2568 auto operation = Microsoft::WRL::Make<ContainerOperation>();
2569 THROW_IF_NULL_ALLOC(operation.Get());
2570
2571 // Capture shared idle state so token can outlive session and release activity without keeping session alive.
2572 std::shared_ptr<IdleState> idleState = m_runtime.IdleStateShared();
2573 operation->Initialize([idleState = std::move(idleState)]() { idleState->ReleaseActivity(); });
2574
2575 // The token now owns the activity-count reference and will release it on destruction.
2576 countCleanup.release();
2577
2578 Microsoft::WRL::ComPtr<IUnknown> token;
2579 THROW_IF_FAILED(operation.As(&token));
2580 return token;
2581 }
2582
2583 HRESULT WSLCSession::BeginContainerOperation(IUnknown** Operation)
2584 try
2585 {
2586 WSLCExecutionContext context(this);
2587
2588 RETURN_HR_IF_NULL(E_POINTER, Operation);
2589 *Operation = nullptr;
2590
2591 // Do not start a new operation (which would hold the VM alive) once the session is terminating
2592 // or has terminated. Mirrors the gate in EnsureVmRunning().
2593 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating.load() || m_sessionTerminatedEvent.is_signaled());
2594
2595 // Record the in-flight operation up front so the VM cannot idle-terminate before the client
2596 // resolves the container and issues the operation (and streams any output).
2597 auto token = CreateActivityToken();
2598
2599 RETURN_IF_FAILED(token.CopyTo(Operation));
2600 return S_OK;
2601 }
2602 CATCH_RETURN();
2603
2604 HRESULT WSLCSession::ListContainers(
2605 const WSLCListContainersOptions* Options, WSLCContainerEntry** Containers, ULONG* Count, WSLCContainerPortMapping** Ports, ULONG* PortsCount)
2606 try
2607 {
2608 WSLCExecutionContext context(this);
2609
2610 RETURN_HR_IF_NULL(E_POINTER, Containers);
2611 RETURN_HR_IF_NULL(E_POINTER, Count);
2612 RETURN_HR_IF_NULL(E_POINTER, Ports);
2613 RETURN_HR_IF_NULL(E_POINTER, PortsCount);
2614
2615 *Count = 0;
2616 *Containers = nullptr;
2617 *Ports = nullptr;
2618 *PortsCount = 0;
2619
2620 bool all = false;
2621 int limit = -1;
2622 std::map<std::string, std::vector<std::string>> filters;
2623
2624 if (Options != nullptr)
2625 {
2626 THROW_HR_IF_MSG(
2627 E_INVALIDARG,
2628 WI_IsAnyFlagSet(static_cast<WSLCListContainersFlags>(Options->Flags), ~WSLCListContainersFlagsValid),
2629 "Invalid flags: 0x%x",
2630 Options->Flags);
2631
2632 all = WI_IsFlagSet(Options->Flags, WSLCListContainersFlagsAll);
2633 limit = static_cast<int>(Options->Limit);
2634
2635 filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount);
2636 }
2637
2638 auto lock = AcquireLease();
2639 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2640
2641 std::vector<docker_schema::ContainerInfo> dockerContainers;
2642 try
2643 {
2644 dockerContainers = m_runtime.Docker().ListContainers(all, limit, filters);
2645 }
2646 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list containers");
2647
2648 std::lock_guard containersLock{m_containersLock};
2649
2650 // Purge containers that were auto-deleted via OnEvent (--rm).
2651 std::erase_if(m_containers, [](const auto& entry) { return entry.second->State() == WslcContainerStateDeleted; });
2652
2653 // Allocate up to the Docker result count. The actual count (tracked via index) may be smaller
2654 // if some IDs returned by Docker aren't in m_containers (e.g. created externally), but in the
2655 // common case the two should match.
2656 auto output = wil::make_unique_cotaskmem<WSLCContainerEntry[]>(dockerContainers.size());
2657 auto freeStrings = wil::scope_exit([&] {
2658 for (size_t i = 0; i < dockerContainers.size(); ++i)
2659 {
2660 wsl::windows::common::wslc::FreeContainerEntryStrings(&output[i]);
2661 }
2662 });
2663
2664 std::vector<WSLCContainerPortMapping> allPorts;
2665
2666 size_t index = 0;
2667 for (const auto& dockerContainer : dockerContainers)
2668 {
2669 auto it = m_containers.find(dockerContainer.Id);
2670 if (it == m_containers.end())
2671 {
2672 continue;
2673 }
2674
2675 auto* e = it->second.get();
2676 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, e->Image().c_str()) != 0);
2677 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Name, e->Name().c_str()) != 0);
2678 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Id, e->ID().c_str()) != 0);
2679
2680 // Commands and status descriptions have no bound imposed by the runtime, so they are
2681 // allocated rather than copied into a fixed buffer.
2682 output[index].Command = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(dockerContainer.Command.c_str()).release();
2683 output[index].Status = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(dockerContainer.Status.c_str()).release();
2684
2685 // Labels, networks and mounts are reported the way the docker CLI renders them: a comma
2686 // separated list. Like the command and status above they are unbounded.
2687 std::vector<std::string> labels;
2688 for (const auto& [key, value] : dockerContainer.Labels)
2689 {
2690 labels.push_back(std::format("{}={}", key, value));
2691 }
2692
2693 std::vector<std::string> networks;
2694 for (const auto& [name, _] : dockerContainer.NetworkSettings.Networks)
2695 {
2696 networks.push_back(name);
2697 }
2698
2699 std::vector<std::string> mounts;
2700 ULONG localVolumes = 0;
2701 for (const auto& mount : dockerContainer.Mounts)
2702 {
2703 // Named volumes report a name, bind mounts only report the host path.
2704 mounts.push_back(mount.Name.empty() ? mount.Source : mount.Name);
2705 if (mount.Type == "volume")
2706 {
2707 localVolumes++;
2708 }
2709 }
2710
2711 const auto joinedLabels = wsl::shared::string::Join(labels, ',');
2712 const auto joinedNetworks = wsl::shared::string::Join(networks, ',');
2713 const auto joinedMounts = wsl::shared::string::Join(mounts, ',');
2714
2715 output[index].Labels = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(joinedLabels.c_str()).release();
2716 output[index].Networks = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(joinedNetworks.c_str()).release();
2717 output[index].Mounts = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(joinedMounts.c_str()).release();
2718 output[index].LocalVolumes = localVolumes;
2719
2720 e->GetState(&output[index].State);
2721 e->GetStateChangedAt(&output[index].StateChangedAt);
2722 e->GetCreatedAt(&output[index].CreatedAt);
2723
2724 for (const auto& port : e->GetPorts())
2725 {
2726 WSLCContainerPortMapping mapping{};
2727 THROW_HR_IF(E_UNEXPECTED, strcpy_s(mapping.Id, e->ID().c_str()) != 0);
2728 mapping.PortMapping.HostPort = port.HostPort;
2729 mapping.PortMapping.ContainerPort = port.ContainerPort;
2730 mapping.PortMapping.Family = port.Family;
2731 mapping.PortMapping.Protocol = port.Protocol;
2732 THROW_HR_IF(E_UNEXPECTED, port.BindingAddress.size() > WSLC_MAX_BINDING_ADDRESS_LENGTH);
2733 THROW_HR_IF(E_UNEXPECTED, strcpy_s(mapping.PortMapping.BindingAddress, port.BindingAddress.c_str()) != 0);
2734 allPorts.push_back(mapping);
2735 }
2736
2737 index++;
2738 }
2739
2740 // Finish every allocation before transferring ownership so nothing can throw once the caller
2741 // owns the results.
2742 wil::unique_cotaskmem_ptr<WSLCContainerPortMapping[]> portsOutput;
2743 if (!allPorts.empty())
2744 {
2745 portsOutput = wil::make_unique_cotaskmem<WSLCContainerPortMapping[]>(allPorts.size());
2746 memcpy(portsOutput.get(), allPorts.data(), allPorts.size() * sizeof(WSLCContainerPortMapping));
2747 }
2748
2749 freeStrings.release();
2750 *Count = static_cast<ULONG>(index);
2751 *Containers = output.release();
2752
2753 if (portsOutput)
2754 {
2755 *PortsCount = static_cast<ULONG>(allPorts.size());
2756 *Ports = portsOutput.release();
2757 }
2758
2759 return S_OK;
2760 }
2761 CATCH_RETURN();
2762
2763 HRESULT WSLCSession::PruneContainers(_In_opt_ const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Out_ WSLCPruneContainersResults* Result)
2764 try
2765 {
2766 WSLCExecutionContext context(this);
2767
2768 RETURN_HR_IF_NULL(E_POINTER, Result);
2769 ZeroMemory(Result, sizeof(*Result));
2770
2771 auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
2772
2773 auto lock = AcquireLease();
2774 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2775
2776 std::lock_guard containersLock{m_containersLock};
2777
2778 docker_schema::PruneContainerResult pruneResult;
2779
2780 try
2781 {
2782 pruneResult = m_runtime.Docker().PruneContainers(filters);
2783 }
2784 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune containers");
2785
2786 Result->SpaceReclaimed = pruneResult.SpaceReclaimed;
2787
2788 if (pruneResult.ContainersDeleted.has_value() && pruneResult.ContainersDeleted->size() > 0)
2789 {
2790 // Remove deleted containers from m_containers.
2791 size_t erased = 0;
2792 for (const auto& deletedId : pruneResult.ContainersDeleted.value())
2793 {
2794 erased += m_containers.erase(deletedId);
2795 }
2796
2797 LOG_HR_IF_MSG(
2798 E_UNEXPECTED,
2799 erased != pruneResult.ContainersDeleted->size(),
2800 "Expected to erase %zu containers, but erased %zu",
2801 pruneResult.ContainersDeleted->size(),
2802 erased);
2803
2804 auto containers = wil::make_unique_cotaskmem<WSLCContainerId[]>(pruneResult.ContainersDeleted->size());
2805
2806 for (size_t i = 0; i < pruneResult.ContainersDeleted->size(); ++i)
2807 {
2808 THROW_HR_IF_MSG(
2809 E_UNEXPECTED,
2810 strcpy_s(containers[i], pruneResult.ContainersDeleted.value()[i].c_str()) != 0,
2811 "Unexpected container name: %hs",
2812 pruneResult.ContainersDeleted.value()[i].c_str());
2813 }
2814
2815 Result->Containers = containers.release();
2816 Result->ContainersCount = static_cast<DWORD>(pruneResult.ContainersDeleted->size());
2817 }
2818 else
2819 {
2820 Result->Containers = nullptr;
2821 Result->ContainersCount = 0;
2822 }
2823
2824 return S_OK;
2825 }
2826 CATCH_RETURN();
2827
2828 HRESULT WSLCSession::CreateRootNamespaceProcess(
2829 LPCSTR Executable, const WSLCProcessOptions* Options, ULONG TtyRows, ULONG TtyColumns, BOOL AcquireVmLease, IWSLCProcess** Process, int* Errno)
2830 try
2831 {
2832 WSLCExecutionContext context(this);
2833
2834 THROW_HR_IF_NULL(E_POINTER, Executable);
2835 THROW_HR_IF_NULL(E_POINTER, Options);
2836 THROW_HR_IF_NULL(E_POINTER, Process);
2837 THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Options->Flags, ~WSLCProcessFlagsValid), "Invalid flags: 0x%x", Options->Flags);
2838
2839 if (Errno != nullptr)
2840 {
2841 *Errno = -1; // Make sure not to return 0 if something fails.
2842 }
2843
2844 auto runtime = m_runtime.Acquire(LeasePolicyFor(AcquireVmLease));
2845 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
2846
2847 auto process = runtime.Vm().CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno);
2848
2849 // The VmLease above is released when this call returns, but the process keeps running in the
2850 // VM and the client holds the returned proxy. A root-namespace process is not tracked as a
2851 // container, so attach an activity token bound to the process's lifetime; this keeps the VM
2852 // alive for as long as the client holds the process, preventing the idle worker from tearing
2853 // the VM down and killing the process out from under the client.
2854 //
2855 // Not for a plugin-originated call: it was served by whatever VM was already running, possibly
2856 // one already committed to stopping, and a plugin must never extend a VM's life. Attaching a
2857 // token anyway would keep counting activity for as long as the plugin holds the proxy and would
2858 // block idle termination of every subsequent VM in this session.
2859 if (AcquireVmLease)
2860 {
2861 process->SetKeepAliveToken(CreateActivityToken());
2862 }
2863
2864 THROW_IF_FAILED(process.CopyTo(Process));
2865
2866 return S_OK;
2867 }
2868 CATCH_RETURN();
2869
2870 void WSLCSession::Ext4Format(const std::string& Device)
2871 {
2872 constexpr auto mkfsPath = "/usr/sbin/mkfs.ext4";
2873 ServiceProcessLauncher launcher(mkfsPath, {mkfsPath, Device});
2874 auto result = launcher.Launch(m_runtime.Vm()).WaitAndCaptureOutput();
2875
2876 THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str());
2877 }
2878
2879 HRESULT WSLCSession::FormatVirtualDisk(LPCWSTR Path)
2880 try
2881 {
2882 WSLCExecutionContext context(this);
2883
2884 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Path), !std::filesystem::path(Path).is_absolute());
2885
2886 auto lock = AcquireLease();
2887 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
2888
2889 // Attach the disk to the VM (AttachDisk() performs the access check for the VHD file).
2890 auto [lun, device] = m_runtime.Vm().AttachDisk(Path, false);
2891
2892 // N.B. DetachDisk calls sync() before detaching.
2893 auto detachDisk = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, lun]() { m_runtime.Vm().DetachDisk(lun); });
2894
2895 // Format it to ext4.
2896 m_runtime.Vm().Ext4Format(device);
2897
2898 return S_OK;
2899 }
2900 CATCH_RETURN();
2901
2902 HRESULT WSLCSession::CreateVolume(const WSLCVolumeOptions* Options, WSLCVolumeInformation* VolumeInfo)
2903 try
2904 {
2905 WSLCExecutionContext context(this);
2906
2907 RETURN_HR_IF_NULL(E_POINTER, Options);
2908 RETURN_HR_IF_NULL(E_POINTER, VolumeInfo);
2909 ZeroMemory(VolumeInfo, sizeof(*VolumeInfo));
2910
2911 auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount);
2912 auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCVolumeMetadataLabel);
2913
2914 auto lock = AcquireLease();
2915 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes());
2916
2917 if (Options->Name != nullptr && Options->Name[0] != '\0')
2918 {
2919 ValidateName(Options->Name, WSLC_MAX_VOLUME_NAME_LENGTH);
2920 }
2921
2922 *VolumeInfo = m_runtime.Volumes().CreateVolume(Options->Name, Options->Driver, std::move(driverOpts), std::move(labels));
2923 return S_OK;
2924 }
2925 CATCH_RETURN();
2926
2927 HRESULT WSLCSession::DeleteVolume(LPCSTR Name)
2928 try
2929 {
2930 WSLCExecutionContext context(this);
2931
2932 RETURN_HR_IF_NULL(E_POINTER, Name);
2933
2934 auto lock = AcquireLease();
2935 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes());
2936
2937 m_runtime.Volumes().DeleteVolume(Name);
2938 return S_OK;
2939 }
2940 CATCH_RETURN();
2941
2942 HRESULT WSLCSession::ListVolumes(const WSLCFilter* Filters, ULONG FiltersCount, LPSTR* Output)
2943 try
2944 {
2945 WSLCExecutionContext context(this);
2946
2947 RETURN_HR_IF_NULL(E_POINTER, Output);
2948
2949 *Output = nullptr;
2950
2951 auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
2952
2953 auto lock = AcquireLease();
2954 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes());
2955
2956 auto volumeList = m_runtime.Volumes().ListVolumes(std::move(filters));
2957
2958 std::string json = wsl::shared::ToJson(volumeList);
2959 *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
2960
2961 return S_OK;
2962 }
2963 CATCH_RETURN();
2964
2965 HRESULT WSLCSession::InspectVolume(LPCSTR Name, LPSTR* Output)
2966 try
2967 {
2968 WSLCExecutionContext context(this);
2969
2970 RETURN_HR_IF_NULL(E_POINTER, Name);
2971 RETURN_HR_IF_NULL(E_POINTER, Output);
2972
2973 *Output = nullptr;
2974
2975 std::string name = Name;
2976 ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH);
2977
2978 auto lock = AcquireLease();
2979 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes());
2980
2981 std::string json = m_runtime.Volumes().InspectVolume(name);
2982 *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
2983
2984 return S_OK;
2985 }
2986 CATCH_RETURN();
2987
2988 HRESULT WSLCSession::PruneVolumes(
2989 const WSLCFilter* Filters, ULONG FiltersCount, IWarningCallback* WarningCallback, WSLCVolumeName** Volumes, ULONG* VolumesCount, ULONGLONG* SpaceReclaimed)
2990 try
2991 {
2992 WSLCExecutionContext context(this, WarningCallback);
2993
2994 RETURN_HR_IF_NULL(E_POINTER, Volumes);
2995 RETURN_HR_IF_NULL(E_POINTER, VolumesCount);
2996 RETURN_HR_IF_NULL(E_POINTER, SpaceReclaimed);
2997 *Volumes = nullptr;
2998 *VolumesCount = 0;
2999 *SpaceReclaimed = 0;
3000
3001 auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
3002
3003 auto lock = AcquireLease();
3004 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes());
3005
3006 WSLCVolumes::PruneVolumesResult pruneResult;
3007 try
3008 {
3009 pruneResult = m_runtime.Volumes().PruneVolumes(filters);
3010 }
3011 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune volumes");
3012
3013 *SpaceReclaimed = pruneResult.SpaceReclaimed;
3014
3015 if (!pruneResult.Volumes.empty())
3016 {
3017 auto output = wil::make_unique_cotaskmem<WSLCVolumeName[]>(pruneResult.Volumes.size());
3018 for (size_t i = 0; i < pruneResult.Volumes.size(); ++i)
3019 {
3020 THROW_HR_IF_MSG(
3021 E_UNEXPECTED,
3022 strcpy_s(output[i], pruneResult.Volumes[i].c_str()) != 0,
3023 "Unexpected volume name length: %hs",
3024 pruneResult.Volumes[i].c_str());
3025 }
3026
3027 *Volumes = output.release();
3028 *VolumesCount = static_cast<ULONG>(pruneResult.Volumes.size());
3029 }
3030
3031 return S_OK;
3032 }
3033 CATCH_RETURN();
3034
3035 // Network management.
3036
3037 HRESULT WSLCSession::CreateNetwork(const WSLCNetworkOptions* Options, IWarningCallback* WarningCallback)
3038 try
3039 {
3040 WSLCExecutionContext context(this, WarningCallback);
3041
3042 RETURN_HR_IF_NULL(E_POINTER, Options);
3043 RETURN_HR_IF_NULL(E_POINTER, Options->Name);
3044
3045 std::string name = Options->Name;
3046 std::string driver = Options->Driver != nullptr ? Options->Driver : WSLCBridgeNetworkDriver;
3047
3048 ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH);
3049
3050 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcInvalidName(name), IsReservedNetworkName(name));
3051 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcInvalidNetworkDriver(driver), driver != WSLCBridgeNetworkDriver);
3052
3053 auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount);
3054 auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCNetworkManagedLabel);
3055
3056 auto lock = AcquireLease();
3057 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
3058 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3059
3060 std::lock_guard networksLock(m_networksLock);
3061 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_networks.contains(name));
3062
3063 docker_schema::CreateNetwork request;
3064 request.Name = name;
3065 request.Driver = driver;
3066 request.Labels = labels;
3067 request.Labels[WSLCNetworkManagedLabel] = "true";
3068
3069 request.Internal = static_cast<bool>(Options->Internal);
3070
3071 THROW_HR_WITH_USER_ERROR_IF(
3072 E_INVALIDARG, Localization::MessageWslcGatewayRequiresSubnet(), Options->Gateway != nullptr && Options->Subnet == nullptr);
3073
3074 THROW_HR_WITH_USER_ERROR_IF(
3075 E_INVALIDARG, Localization::MessageWslcIpRangeRequiresSubnet(), Options->IpRange != nullptr && Options->Subnet == nullptr);
3076
3077 if (Options->Subnet != nullptr)
3078 {
3079 docker_schema::IPAMConfig ipamConfig;
3080 ipamConfig.Subnet = Options->Subnet;
3081
3082 if (Options->Gateway != nullptr)
3083 {
3084 ipamConfig.Gateway = Options->Gateway;
3085 }
3086
3087 if (Options->IpRange != nullptr)
3088 {
3089 ipamConfig.IPRange = Options->IpRange;
3090 }
3091
3092 auto& ipam = request.IPAM.emplace();
3093 ipam.Driver = "default";
3094 ipam.Config.emplace().push_back(std::move(ipamConfig));
3095 }
3096
3097 if (!driverOpts.empty())
3098 {
3099 request.Options = std::move(driverOpts);
3100 }
3101
3102 docker_schema::CreateNetworkResponse createResult;
3103 try
3104 {
3105 createResult = m_runtime.Docker().CreateNetwork(request);
3106 }
3107 catch (const DockerHTTPException& e)
3108 {
3109 THROW_HR_WITH_USER_ERROR_IF(
3110 HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), Localization::MessageWslcNetworkAlreadyExists(name), e.StatusCode() == 409);
3111 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to create network '%hs'", name.c_str());
3112 }
3113
3114 if (!createResult.Warning.empty())
3115 {
3116 EMIT_USER_WARNING(wsl::shared::string::MultiByteToWide(createResult.Warning));
3117 }
3118
3119 auto removeNetworkCleanup =
3120 wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_runtime.Docker().RemoveNetwork(name); });
3121
3122 // Inspect the newly created network to cache full properties (IPAM, Scope, etc.)
3123 // since CreateNetworkResponse only returns {Id, Warning}.
3124 docker_schema::Network full;
3125 try
3126 {
3127 full = m_runtime.Docker().InspectNetwork(name);
3128 }
3129 catch (const DockerHTTPException& e)
3130 {
3131 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to inspect newly created network '%hs'", name.c_str());
3132 }
3133
3134 NetworkEntry entry;
3135 entry.Id = full.Id;
3136 entry.Driver = full.Driver;
3137 entry.Scope = full.Scope;
3138 entry.Internal = full.Internal;
3139 entry.Labels = full.Labels;
3140 entry.Options = full.Options;
3141 entry.IPAM.Driver = full.IPAM.Driver;
3142 if (full.IPAM.Config)
3143 {
3144 auto& cfgs = entry.IPAM.Config.emplace();
3145 for (const auto& c : *full.IPAM.Config)
3146 {
3147 cfgs.push_back({c.Subnet, c.Gateway, c.IPRange});
3148 }
3149 }
3150
3151 auto [it, inserted] = m_networks.insert({name, std::move(entry)});
3152 WI_VERIFY(inserted);
3153
3154 WSL_LOG("NetworkCreated", TraceLoggingValue(name.c_str(), "NetworkName"), TraceLoggingValue(full.Id.c_str(), "NetworkId"));
3155
3156 removeNetworkCleanup.release();
3157
3158 return S_OK;
3159 }
3160 CATCH_RETURN();
3161
3162 HRESULT WSLCSession::DeleteNetwork(LPCSTR Name)
3163 try
3164 {
3165 WSLCExecutionContext context(this);
3166
3167 RETURN_HR_IF_NULL(E_POINTER, Name);
3168 std::string name = Name;
3169 ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH);
3170
3171 auto lock = AcquireLease();
3172 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
3173 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3174
3175 std::lock_guard networksLock(m_networksLock);
3176
3177 auto it = m_networks.find(name);
3178 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), it == m_networks.end());
3179
3180 try
3181 {
3182 m_runtime.Docker().RemoveNetwork(name);
3183 }
3184 catch (const DockerHTTPException& e)
3185 {
3186 // Docker returns 403 when the network has active endpoints.
3187 THROW_HR_WITH_USER_ERROR_IF(
3188 HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), Localization::MessageWslcNetworkInUse(name), e.StatusCode() == 403);
3189 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), e.StatusCode() == 404);
3190 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to delete network '%hs'", name.c_str());
3191 }
3192
3193 m_networks.erase(it);
3194 WSL_LOG("NetworkDeleted", TraceLoggingValue(name.c_str(), "NetworkName"));
3195
3196 return S_OK;
3197 }
3198 CATCH_RETURN();
3199
3200 HRESULT WSLCSession::ListNetworks(const WSLCFilter* Filters, ULONG FiltersCount, LPSTR* Output)
3201 try
3202 {
3203 WSLCExecutionContext context(this);
3204
3205 RETURN_HR_IF_NULL(E_POINTER, Output);
3206
3207 *Output = nullptr;
3208
3209 auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
3210
3211 auto lock = AcquireLease();
3212 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
3213 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3214
3215 std::vector<docker_schema::Network> dockerNetworks;
3216 try
3217 {
3218 dockerNetworks = m_runtime.Docker().ListNetworks(filters);
3219 }
3220 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list networks");
3221
3222 std::vector<wslc_schema::NetworkListEntry> networks;
3223 networks.reserve(dockerNetworks.size());
3224 for (const auto& network : dockerNetworks)
3225 {
3226 wslc_schema::NetworkListEntry entry;
3227 entry.Id = network.Id;
3228 entry.Name = network.Name;
3229 entry.Driver = network.Driver;
3230 entry.Scope = network.Scope;
3231 entry.Created = network.Created;
3232 entry.EnableIPv4 = network.EnableIPv4;
3233 entry.EnableIPv6 = network.EnableIPv6;
3234 entry.Internal = network.Internal;
3235 entry.Labels = network.Labels;
3236 entry.Labels.erase(WSLCNetworkManagedLabel);
3237
3238 networks.push_back(std::move(entry));
3239 }
3240
3241 std::string json = wsl::shared::ToJson(networks);
3242 *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
3243
3244 return S_OK;
3245 }
3246 CATCH_RETURN();
3247
3248 HRESULT WSLCSession::InspectNetwork(LPCSTR Name, LPSTR* Output)
3249 try
3250 {
3251 WSLCExecutionContext context(this);
3252
3253 RETURN_HR_IF_NULL(E_POINTER, Name);
3254 RETURN_HR_IF_NULL(E_POINTER, Output);
3255
3256 *Output = nullptr;
3257
3258 std::string name = Name;
3259 ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH);
3260
3261 auto lock = AcquireLease();
3262 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
3263 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3264
3265 docker_schema::Network network;
3266 try
3267 {
3268 network = m_runtime.Docker().InspectNetwork(name);
3269 }
3270 catch (const DockerHTTPException& e)
3271 {
3272 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), e.StatusCode() == 404);
3273 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to inspect network '%hs'", name.c_str());
3274 }
3275
3276 wslc_schema::Network result;
3277 result.Id = network.Id;
3278 result.Name = network.Name;
3279 result.Created = network.Created;
3280 result.Driver = network.Driver;
3281 result.Scope = network.Scope;
3282 result.EnableIPv4 = network.EnableIPv4;
3283 result.EnableIPv6 = network.EnableIPv6;
3284 result.Internal = network.Internal;
3285 result.Attachable = network.Attachable;
3286 result.Ingress = network.Ingress;
3287 result.ConfigOnly = network.ConfigOnly;
3288 result.ConfigFrom.Network = network.ConfigFrom.Network;
3289 result.Options = network.Options;
3290 result.Labels = network.Labels;
3291 result.Labels.erase(WSLCNetworkManagedLabel);
3292 result.Status = network.Status;
3293
3294 result.IPAM.Driver = network.IPAM.Driver;
3295 result.IPAM.Options = network.IPAM.Options;
3296 if (network.IPAM.Config)
3297 {
3298 auto& configs = result.IPAM.Config.emplace();
3299 for (const auto& cfg : *network.IPAM.Config)
3300 {
3301 wslc_schema::IPAMConfig inspectCfg;
3302 inspectCfg.Subnet = cfg.Subnet;
3303 inspectCfg.Gateway = cfg.Gateway;
3304 inspectCfg.IPRange = cfg.IPRange;
3305 configs.push_back(std::move(inspectCfg));
3306 }
3307 }
3308
3309 for (const auto& [id, container] : network.Containers)
3310 {
3311 wslc_schema::NetworkContainer inspectContainer;
3312 inspectContainer.Name = container.Name;
3313 inspectContainer.EndpointID = container.EndpointID;
3314 inspectContainer.MacAddress = container.MacAddress;
3315 inspectContainer.IPv4Address = container.IPv4Address;
3316 inspectContainer.IPv6Address = container.IPv6Address;
3317 result.Containers.emplace(id, std::move(inspectContainer));
3318 }
3319
3320 std::string json = wsl::shared::ToJson(result);
3321 *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
3322
3323 return S_OK;
3324 }
3325 CATCH_RETURN();
3326
3327 HRESULT WSLCSession::PruneNetworks(const WSLCFilter* Filters, ULONG FiltersCount, WSLCNetworkName** Networks, ULONG* NetworksCount)
3328 try
3329 {
3330 WSLCExecutionContext context(this);
3331
3332 RETURN_HR_IF_NULL(E_POINTER, Networks);
3333 RETURN_HR_IF_NULL(E_POINTER, NetworksCount);
3334 *Networks = nullptr;
3335 *NetworksCount = 0;
3336
3337 auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
3338
3339 // Scope the prune to WSLC-managed networks.
3340 filters["label"].push_back(WSLCNetworkManagedLabel);
3341
3342 auto lock = AcquireLease();
3343 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
3344 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3345
3346 std::lock_guard networksLock(m_networksLock);
3347
3348 docker_schema::PruneNetworkResult pruneResult;
3349 try
3350 {
3351 pruneResult = m_runtime.Docker().PruneNetworks(filters);
3352 }
3353 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune networks");
3354
3355 if (!pruneResult.NetworksDeleted.has_value() || pruneResult.NetworksDeleted->empty())
3356 {
3357 return S_OK;
3358 }
3359
3360 std::vector<std::string> deleted;
3361 deleted.reserve(pruneResult.NetworksDeleted->size());
3362 for (const auto& name : *pruneResult.NetworksDeleted)
3363 {
3364 // Only report networks that we manage.
3365 if (!m_networks.contains(name))
3366 {
3367 WSL_LOG("PrunedUnknownNetwork", TraceLoggingValue(name.c_str(), "NetworkName"));
3368 continue;
3369 }
3370 deleted.push_back(name);
3371 }
3372
3373 if (deleted.empty())
3374 {
3375 return S_OK;
3376 }
3377
3378 // Erase before marshalling: docker has already pruned these, so m_networks must stay in sync.
3379 for (const auto& name : deleted)
3380 {
3381 m_networks.erase(name);
3382 }
3383
3384 WSL_LOG("NetworksPruned", TraceLoggingValue(static_cast<ULONG>(deleted.size()), "Count"));
3385
3386 auto output = wil::make_unique_cotaskmem<WSLCNetworkName[]>(deleted.size());
3387 for (size_t i = 0; i < deleted.size(); ++i)
3388 {
3389 THROW_HR_IF_MSG(
3390 E_UNEXPECTED, strcpy_s(output[i], deleted[i].c_str()) != 0, "Unexpected network name length: %hs", deleted[i].c_str());
3391 }
3392
3393 *Networks = output.release();
3394 *NetworksCount = static_cast<ULONG>(deleted.size());
3395
3396 return S_OK;
3397 }
3398 CATCH_RETURN();
3399
3400 bool WSLCSession::WaitForEventOrSessionTerminating(HANDLE Event, std::chrono::milliseconds Timeout) const
3401 {
3402 const HANDLE waitHandles[] = {Event, m_sessionTerminatingEvent.get()};
3403 const DWORD waitResult = WaitForMultipleObjects(RTL_NUMBER_OF(waitHandles), waitHandles, FALSE, gsl::narrow<DWORD>(Timeout.count()));
3404
3405 switch (waitResult)
3406 {
3407 case WAIT_OBJECT_0:
3408 return true;
3409 case WAIT_OBJECT_0 + 1:
3410 THROW_HR_MSG(E_ABORT, "Session %lu is terminating.", m_id);
3411 break;
3412 case WAIT_TIMEOUT:
3413 return false;
3414 default:
3415 THROW_LAST_ERROR();
3416 }
3417 }
3418
3419 HRESULT WSLCSession::Terminate()
3420 try
3421 {
3422 // Ensure only one Terminate() runs. This must be checked before taking the runtime's exclusive
3423 // lock because OnVmExited() is called from the IORelay thread — if an external Terminate()
3424 // holds that lock and calls m_runtime.Relay()->Stop(), the relay thread must not re-enter
3425 // Terminate() and deadlock on it.
3426 if (m_terminating.exchange(true))
3427 {
3428 return S_OK;
3429 }
3430
3431 wil::rwlock_release_exclusive_scope_exit sessionLock;
3432
3433 // Because it's not possible to synchronize CancelIoEx() with ReadFile() calls, keep attempting to acquire the session lock while cancelling IO & callbacks.
3434 // This is required because calling CancelIoEx() between two ReadFile() calls does nothing, and therefore could still allow another thread to get stuck doing synchronous IO.
3435 bool retrying = false;
3436 while (!sessionLock)
3437 {
3438 // If this isn't the first iteration, sleep to prevent this loop from burning too much CPU.
3439 if (retrying)
3440 {
3441 std::this_thread::sleep_for(std::chrono::milliseconds(10));
3442 }
3443
3444 {
3445 std::lock_guard lock(m_userHandlesLock);
3446
3447 // m_sessionTerminatingEvent is always valid, so it can be signalled without holding the runtime lock.
3448 // This allows a session to be unblocked if a stuck operation is holding the runtime lock.
3449 // N.B. This must happen under m_userHandlesLock to synchronize with potentially running operations.
3450 if (!m_sessionTerminatingEvent.is_signaled())
3451 {
3452 m_sessionTerminatingEvent.SetEvent();
3453
3454 // Wake any readers parked in an event stream so they abort instead of waiting forever.
3455 m_eventStore.OnSessionTerminating();
3456 }
3457
3458 // Cancel any pending IO on user-provided handles to unblock operations
3459 // in case the handles don't support overlapped IO.
3460 CancelUserHandleIO();
3461 }
3462
3463 {
3464 std::lock_guard comLock(m_userCOMCallbacksLock);
3465
3466 // Cancel any pending outgoing COM callback calls (e.g. IProgressCallback::OnProgress)
3467 // to unblock operations waiting for cross-process COM responses.
3468 CancelUserCOMCallbacks();
3469 }
3470
3471 sessionLock = m_runtime.TryLockExclusive();
3472 retrying = true;
3473 }
3474
3475 m_runtime.Shutdown(sessionLock, m_terminationReason, m_terminationDetails);
3476
3477 // Idle teardown is disabled and no operation can run past termination, so the parked VM
3478 // factory can no longer be re-fetched; revoke it from the GIT.
3479 if (m_vmFactoryGitCookie != 0)
3480 {
3481 LOG_IF_FAILED(m_git->RevokeInterfaceFromGlobal(m_vmFactoryGitCookie));
3482 m_vmFactoryGitCookie = 0;
3483 }
3484
3485 return S_OK;
3486 }
3487 CATCH_RETURN();
3488
3489 HRESULT WSLCSession::RegisterCrashDumpCallback(_In_ ICrashDumpCallback* Callback, _Out_ IUnknown** Subscription)
3490 try
3491 {
3492 RETURN_HR_IF(E_POINTER, Callback == nullptr || Subscription == nullptr);
3493 *Subscription = nullptr;
3494
3495 CrashDumpCallbackList::iterator it;
3496 {
3497 auto lock = m_crashDumpLock.lock_exclusive();
3498 it = m_crashDumpCallbacks.emplace(m_crashDumpCallbacks.end(), Callback);
3499 }
3500
3501 // Roll back the registration if creating the subscription object fails so we don't leak it.
3502 auto removeOnFailure = wil::scope_exit([&]() { RemoveCrashDumpCallback(it); });
3503
3504 // The subscription holds a strong reference to this session, which guarantees that
3505 // RemoveCrashDumpCallback is safe to call from the subscription destructor regardless of the
3506 // order in which the client releases its session and subscription pointers.
3507 Microsoft::WRL::ComPtr<CrashDumpSubscription> subscription;
3508 RETURN_IF_FAILED(Microsoft::WRL::MakeAndInitialize<CrashDumpSubscription>(&subscription, Microsoft::WRL::ComPtr<WSLCSession>{this}, it));
3509
3510 RETURN_IF_FAILED(subscription.CopyTo(Subscription));
3511
3512 removeOnFailure.release();
3513 return S_OK;
3514 }
3515 CATCH_RETURN();
3516
3517 void WSLCSession::RemoveCrashDumpCallback(CrashDumpCallbackList::iterator It) noexcept
3518 {
3519 auto lock = m_crashDumpLock.lock_exclusive();
3520 m_crashDumpCallbacks.erase(It);
3521 }
3522
3523 void WSLCSession::OnCrashDumpWritten(const std::wstring& DumpPath, const std::string& ProcessName, ULONG Pid, ULONG Signal, ULONGLONG Timestamp)
3524 try
3525 {
3526 // Snapshot the callback list under the lock so that cross-process callback invocations don't
3527 // hold m_crashDumpLock (and can't deadlock with Register/Remove on the same thread that the
3528 // callback might in turn use).
3529 std::vector<wil::com_ptr<ICrashDumpCallback>> snapshot;
3530 {
3531 auto lock = m_crashDumpLock.lock_shared();
3532 snapshot.assign(m_crashDumpCallbacks.begin(), m_crashDumpCallbacks.end());
3533 }
3534
3535 auto comCall = RegisterUserCOMCallback();
3536
3537 for (const auto& callback : snapshot)
3538 {
3539 LOG_IF_FAILED(callback->OnCrashDump(DumpPath.c_str(), ProcessName.c_str(), Pid, Signal, Timestamp));
3540 }
3541 }
3542 CATCH_LOG();
3543
3544 HRESULT WSLCSession::MountWindowsFolder(LPCWSTR WindowsPath, LPCSTR LinuxPath, BOOL ReadOnly, BOOL AcquireVmLease)
3545 try
3546 {
3547 WSLCExecutionContext context(this);
3548
3549 RETURN_HR_IF_NULL(E_POINTER, WindowsPath);
3550 RETURN_HR_IF_NULL(E_POINTER, LinuxPath);
3551
3552 auto lock = AcquireLease(LeasePolicyFor(AcquireVmLease));
3553 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3554
3555 return m_runtime.Vm().MountWindowsFolder(WindowsPath, LinuxPath, ReadOnly);
3556 }
3557 CATCH_RETURN();
3558
3559 HRESULT WSLCSession::UnmountWindowsFolder(LPCSTR LinuxPath, BOOL AcquireVmLease)
3560 try
3561 {
3562 WSLCExecutionContext context(this);
3563
3564 RETURN_HR_IF_NULL(E_POINTER, LinuxPath);
3565
3566 auto lock = AcquireLease(LeasePolicyFor(AcquireVmLease));
3567 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3568
3569 return m_runtime.Vm().UnmountWindowsFolder(LinuxPath);
3570 }
3571 CATCH_RETURN();
3572
3573 HRESULT WSLCSession::MapVmPort(int Family, unsigned short WindowsPort, unsigned short LinuxPort)
3574 try
3575 {
3576 WSLCExecutionContext context(this);
3577
3578 auto lock = AcquireLease();
3579 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3580
3581 std::lock_guard allocatedPortsLock(m_runtime.AllocatedPortsLock());
3582
3583 // Look for an existing allocation first.
3584 auto& allocatedPorts = m_runtime.AllocatedPorts();
3585 auto it = allocatedPorts.find(LinuxPort);
3586
3587 bool inserted = false;
3588 auto cleanup = wil::scope_exit([&]() {
3589 if (inserted)
3590 {
3591 allocatedPorts.erase(it);
3592 }
3593 });
3594
3595 if (it == allocatedPorts.end())
3596 {
3597 // No existing port allocation, create a new one.
3598 auto allocated = std::make_pair(m_runtime.Vm().TryAllocatePort(LinuxPort, Family, IPPROTO_TCP), static_cast<size_t>(0));
3599 THROW_HR_IF(HRESULT_FROM_WIN32(WSAEADDRINUSE), allocated.first == nullptr);
3600
3601 it = allocatedPorts.emplace(LinuxPort, allocated).first;
3602 inserted = true;
3603 }
3604
3605 auto mapping = VMPortMapping::LocalhostTcpMapping(Family, WindowsPort);
3606 mapping.AssignVmPort(it->second.first);
3607
3608 m_runtime.Vm().MapPort(mapping);
3609
3610 // Increase usage count.
3611 it->second.second++;
3612
3613 mapping.Release();
3614 cleanup.release();
3615
3616 return S_OK;
3617 }
3618 CATCH_RETURN();
3619
3620 HRESULT WSLCSession::UnmapVmPort(int Family, unsigned short WindowsPort, unsigned short LinuxPort)
3621 try
3622 {
3623 WSLCExecutionContext context(this);
3624
3625 auto lock = AcquireLease();
3626 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3627
3628 std::lock_guard allocatedPortsLock(m_runtime.AllocatedPortsLock());
3629
3630 auto& allocatedPorts = m_runtime.AllocatedPorts();
3631 auto it = allocatedPorts.find(LinuxPort);
3632 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == allocatedPorts.end());
3633
3634 auto mapping = VMPortMapping::LocalhostTcpMapping(Family, WindowsPort);
3635 mapping.AssignVmPort(it->second.first);
3636 mapping.Attach(m_runtime.Vm());
3637
3638 auto cleanup = wil::scope_exit([&]() { mapping.Release(); });
3639
3640 m_runtime.Vm().UnmapPort(mapping);
3641
3642 it->second.second--;
3643
3644 // If usage count drops to 0, release the port allocation.
3645 if (it->second.second == 0)
3646 {
3647 allocatedPorts.erase(it);
3648 }
3649
3650 return S_OK;
3651 }
3652 CATCH_RETURN();
3653
3654 HRESULT WSLCSession::TriggerIdleTermination(BOOL* WasAlreadyIdle)
3655 try
3656 {
3657 WSLCExecutionContext context(this);
3658
3659 THROW_HR_IF_NULL(E_POINTER, WasAlreadyIdle);
3660
3661 *WasAlreadyIdle = m_runtime.TriggerIdleTerminationForTest() ? TRUE : FALSE;
3662
3663 return S_OK;
3664 }
3665 CATCH_RETURN();
3666
3667 HRESULT WSLCSession::InterfaceSupportsErrorInfo(REFIID riid)
3668 {
3669 return riid == __uuidof(IWSLCSession) || riid == __uuidof(IWSLCCompatSession) ? S_OK : S_FALSE;
3670 }
3671
3672 HRESULT WSLCSession::PullImage(LPCSTR Image, LPCSTR RegistryAuthenticationInformation, IWSLCCompatProgressCallback* ProgressCallback, IWSLCCompatWarningCallback* WarningCallback)
3673 {
3674 const auto progress = apicompat::Convert(ProgressCallback);
3675 const auto warning = apicompat::Convert(WarningCallback);
3676
3677 return PullImage(Image, RegistryAuthenticationInformation, progress.Get(), warning.Get());
3678 }
3679
3680 HRESULT WSLCSession::LoadImage(WSLCCompatHandle ImageHandle, IWSLCCompatProgressCallback*, ULONGLONG ContentLength, IWSLCCompatWarningCallback* WarningCallback)
3681 {
3682 const auto handle = apicompat::Convert(ImageHandle);
3683 const auto warning = apicompat::Convert(WarningCallback);
3684
3685 return LoadImage(handle, ContentLength, warning.Get(), nullptr);
3686 }
3687
3688 HRESULT WSLCSession::ImportImage(
3689 WSLCCompatHandle ImageHandle, LPCSTR ImageName, IWSLCCompatProgressCallback*, ULONGLONG ContentLength, IWSLCCompatWarningCallback* WarningCallback, LPSTR* ImageId)
3690 {
3691 const auto handle = apicompat::Convert(ImageHandle);
3692 const auto warning = apicompat::Convert(WarningCallback);
3693
3694 return ImportImage(handle, ImageName, ContentLength, warning.Get(), ImageId);
3695 }
3696
3697 HRESULT WSLCSession::ListImages(const WSLCCompatListImagesOptions* Options, WSLCCompatImageInformation** Images, ULONG* Count)
3698 try
3699 {
3700 RETURN_HR_IF_NULL(E_POINTER, Images);
3701 RETURN_HR_IF_NULL(E_POINTER, Count);
3702
3703 *Images = nullptr;
3704 *Count = 0;
3705
3706 wil::unique_cotaskmem_array_ptr<WSLCImageInformation> imagesImpl;
3707
3708 if (Options == nullptr)
3709 {
3710 RETURN_IF_FAILED(ListImages(static_cast<const WSLCListImagesOptions*>(nullptr), &imagesImpl, imagesImpl.size_address<ULONG>()));
3711 }
3712 else
3713 {
3714 const auto options = apicompat::Convert(*Options);
3715 RETURN_IF_FAILED(ListImages(options.Get(), &imagesImpl, imagesImpl.size_address<ULONG>()));
3716 }
3717
3718 if (imagesImpl.size() > 0)
3719 {
3720 auto converted = wil::make_unique_cotaskmem_nothrow<WSLCCompatImageInformation[]>(imagesImpl.size());
3721 RETURN_IF_NULL_ALLOC(converted);
3722
3723 for (size_t index = 0; index < imagesImpl.size(); index++)
3724 {
3725 converted[index] = apicompat::Convert(imagesImpl[index]);
3726 }
3727
3728 *Images = converted.release();
3729 }
3730
3731 *Count = static_cast<ULONG>(imagesImpl.size());
3732 return S_OK;
3733 }
3734 CATCH_RETURN();
3735
3736 HRESULT WSLCSession::DeleteImage(const WSLCCompatDeleteImageOptions* Options, WSLCCompatDeletedImageInformation** DeletedImages, ULONG* Count)
3737 try
3738 {
3739 RETURN_HR_IF_NULL(E_POINTER, Options);
3740 RETURN_HR_IF_NULL(E_POINTER, DeletedImages);
3741 RETURN_HR_IF_NULL(E_POINTER, Count);
3742
3743 *DeletedImages = nullptr;
3744 *Count = 0;
3745
3746 const auto options = apicompat::Convert(*Options);
3747
3748 wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> imagesImpl;
3749
3750 RETURN_IF_FAILED(DeleteImage(&options, &imagesImpl, imagesImpl.size_address<ULONG>()));
3751
3752 if (imagesImpl.size() > 0)
3753 {
3754 auto converted = wil::make_unique_cotaskmem_nothrow<WSLCCompatDeletedImageInformation[]>(imagesImpl.size());
3755 RETURN_IF_NULL_ALLOC(converted);
3756
3757 for (size_t index = 0; index < imagesImpl.size(); index++)
3758 {
3759 converted[index] = apicompat::Convert(imagesImpl[index]);
3760 }
3761
3762 *DeletedImages = converted.release();
3763 }
3764
3765 *Count = static_cast<ULONG>(imagesImpl.size());
3766 return S_OK;
3767 }
3768 CATCH_RETURN();
3769
3770 HRESULT WSLCSession::TagImage(const WSLCCompatTagImageOptions* Options)
3771 try
3772 {
3773 RETURN_HR_IF_NULL(E_POINTER, Options);
3774
3775 const auto options = apicompat::Convert(*Options);
3776 return TagImage(&options);
3777 }
3778 CATCH_RETURN();
3779
3780 HRESULT WSLCSession::PushImage(LPCSTR Image, LPCSTR RegistryAuthenticationInformation, IWSLCCompatProgressCallback* ProgressCallback, IWSLCCompatWarningCallback* WarningCallback)
3781 {
3782 const auto progress = apicompat::Convert(ProgressCallback);
3783 const auto warning = apicompat::Convert(WarningCallback);
3784
3785 return PushImage(Image, RegistryAuthenticationInformation, progress.Get(), warning.Get());
3786 }
3787
3788 HRESULT WSLCSession::CreateContainer(const WSLCCompatContainerOptions* Options, IWSLCCompatWarningCallback* WarningCallback, IWSLCCompatContainer** Container)
3789 try
3790 {
3791 RETURN_HR_IF_NULL(E_POINTER, Options);
3792 RETURN_HR_IF_NULL(E_POINTER, Container);
3793 *Container = nullptr;
3794
3795 const auto warning = apicompat::Convert(WarningCallback);
3796 const auto options = apicompat::Convert(*Options);
3797
3798 Microsoft::WRL::ComPtr<IWSLCContainer> container;
3799 RETURN_IF_FAILED(CreateContainer(options.Get(), warning.Get(), &container));
3800 RETURN_HR_IF_NULL(E_UNEXPECTED, container);
3801
3802 return container.CopyTo(Container);
3803 }
3804 CATCH_RETURN();
3805
3806 HRESULT WSLCSession::OpenContainer(LPCSTR NameOrId, IWSLCCompatContainer** Container)
3807 try
3808 {
3809 RETURN_HR_IF_NULL(E_POINTER, NameOrId);
3810 RETURN_HR_IF_NULL(E_POINTER, Container);
3811 *Container = nullptr;
3812
3813 Microsoft::WRL::ComPtr<IWSLCContainer> container;
3814 RETURN_IF_FAILED(OpenContainer(NameOrId, &container));
3815 RETURN_HR_IF_NULL(E_UNEXPECTED, container);
3816
3817 return container.CopyTo(Container);
3818 }
3819 CATCH_RETURN();
3820
3821 HRESULT WSLCSession::CreateVolume(const WSLCCompatVolumeOptions* Options, WSLCCompatVolumeInformation* VolumeInfo)
3822 try
3823 {
3824 RETURN_HR_IF_NULL(E_POINTER, Options);
3825
3826 const auto options = apicompat::Convert(*Options);
3827
3828 WSLCVolumeInformation info{};
3829 WSLCVolumeInformation* internalInfo = (VolumeInfo != nullptr) ? &info : nullptr;
3830 RETURN_IF_FAILED(CreateVolume(options.Get(), internalInfo));
3831
3832 if (VolumeInfo != nullptr)
3833 {
3834 *VolumeInfo = apicompat::Convert(info);
3835 }
3836
3837 return S_OK;
3838 }
3839 CATCH_RETURN();
3840
3841 HRESULT WSLCSession::RegisterCrashDumpCallback(IWSLCCompatCrashDumpCallback* Callback, IUnknown** Subscription)
3842 {
3843 const auto callback = apicompat::Convert(Callback);
3844
3845 return RegisterCrashDumpCallback(callback.Get(), Subscription);
3846 }
3847
3848 MultiHandleWait WSLCSession::CreateIOContext(HANDLE CancelHandle)
3849 {
3850 io::MultiHandleWait io;
3851
3852 // Cancel with E_ABORT if the session is terminating.
3853 io.AddHandle(
3854 std::make_unique<io::EventHandle>(
3855 m_sessionTerminatingEvent.get(), [this]() { THROW_HR_MSG(E_ABORT, "Session %lu is terminating", m_id); }),
3856 io::MultiHandleWait::NeedNotComplete);
3857
3858 // Cancel with E_ABORT if the client process exits.
3859 io.AddHandle(
3860 std::make_unique<io::EventHandle>(
3861 wslutil::OpenCallingProcess(SYNCHRONIZE), [this]() { THROW_HR_MSG(E_ABORT, "Client process has exited"); }),
3862 io::MultiHandleWait::NeedNotComplete);
3863
3864 if (CancelHandle != nullptr)
3865 {
3866 io.AddHandle(
3867 std::make_unique<io::EventHandle>(CancelHandle, []() { THROW_HR_MSG(E_ABORT, "Cancellation handle was signaled"); }),
3868 io::MultiHandleWait::NeedNotComplete);
3869 }
3870
3871 return io;
3872 }
3873
3874 UserHandle WSLCSession::OpenUserHandle(WSLCHandle Handle)
3875 {
3876 std::lock_guard lock(m_userHandlesLock);
3877
3878 // Don't allow new handles to be added to the list if the session is terminating.
3879 // N.B. This check must happen under m_userHandlesLock to synchronize with Terminate().
3880
3881 THROW_HR_IF_MSG(
3882 E_ABORT, m_sessionTerminatingEvent.is_signaled(), "Refusing to open a user handle while the session is terminating.");
3883
3884 auto userHandle = common::wslutil::FromCOMInputHandle(Handle);
3885
3886 m_userHandles.emplace_back(userHandle);
3887
3888 return UserHandle{*this, userHandle};
3889 }
3890
3891 void WSLCSession::ReleaseUserHandle(HANDLE Handle)
3892 {
3893 std::lock_guard lock(m_userHandlesLock);
3894
3895 auto it = std::ranges::find(m_userHandles, Handle);
3896 WI_ASSERT(it != m_userHandles.end());
3897
3898 m_userHandles.erase(it);
3899 }
3900
3901 void WSLCSession::CancelUserHandleIO()
3902 {
3903 for (auto handle : m_userHandles)
3904 {
3905 // Cancel all IO on the handle.
3906 // N.B. This only cancels IO happening in this process.
3907 if (!CancelIoEx(handle, nullptr))
3908 {
3909 LOG_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND);
3910 }
3911 }
3912 }
3913
3914 UserCOMCallback WSLCSession::RegisterUserCOMCallback()
3915 {
3916 std::lock_guard lock(m_userCOMCallbacksLock);
3917
3918 // Don't allow new COM calls if the session is terminating.
3919 // N.B. This check must happen under m_userCOMCallbacksLock to synchronize with Terminate().
3920 THROW_HR_IF_MSG(
3921 E_ABORT, m_sessionTerminatingEvent.is_signaled(), "Refusing to make a COM callback while the session is terminating.");
3922
3923 THROW_IF_FAILED(CoEnableCallCancellation(nullptr));
3924
3925 auto threadId = GetCurrentThreadId();
3926 auto it = m_userCOMCallbackThreads.find(threadId);
3927 WI_VERIFY(it == m_userCOMCallbackThreads.end() || it->second > 0);
3928
3929 if (it == m_userCOMCallbackThreads.end())
3930 {
3931 m_userCOMCallbackThreads.insert({threadId, 1});
3932 }
3933 else
3934 {
3935 it->second++;
3936 }
3937
3938 return UserCOMCallback{*this};
3939 }
3940
3941 void WSLCSession::UnregisterUserCOMCallback(DWORD ThreadId)
3942 {
3943 std::lock_guard lock(m_userCOMCallbacksLock);
3944
3945 auto it = m_userCOMCallbackThreads.find(ThreadId);
3946 WI_VERIFY(it != m_userCOMCallbackThreads.end() && it->second > 0);
3947
3948 if (it->second > 1)
3949 {
3950 it->second--;
3951 }
3952 else
3953 {
3954 m_userCOMCallbackThreads.erase(it);
3955 }
3956 }
3957
3958 void WSLCSession::CancelUserCOMCallbacks()
3959 {
3960 for (auto threadId : std::views::keys(m_userCOMCallbackThreads))
3961 {
3962 LOG_IF_FAILED(CoCancelCall(threadId, 0));
3963 }
3964 }
3965
3966 void WSLCSession::OnContainerDeleted(const WSLCContainerImpl* Container)
3967 {
3968 // N.B. Invoked only from WSLCContainer::Delete, which already holds a VmLease (the shared
3969 // session lock). The lease prevents a concurrent idle teardown from clearing m_containers,
3970 // so this only needs m_containersLock. It must NOT re-acquire the shared session lock here:
3971 // doing so while the idle worker is queued for the exclusive lock would deadlock (recursive
3972 // shared acquire behind a pending writer).
3973 std::lock_guard containersLock(m_containersLock);
3974
3975 // N.B. once a container transitions to a 'Deleted' state, a call to ListContainers() can remove it from m_containers.
3976 // Therefore it's possible that the container is already removed when the callback from Delete() is invoked.
3977 m_containers.erase(Container->ID());
3978 }
3979
3980 HRESULT WSLCSession::GetState(_Out_ WSLCSessionState* State)
3981 {
3982 RETURN_HR_IF_NULL(E_POINTER, State);
3983
3984 *State = m_sessionTerminatedEvent.is_signaled() ? WSLCSessionStateTerminated : WSLCSessionStateRunning;
3985 return S_OK;
3986 }
3987
3988 HRESULT WSLCSession::GetTerminationEvent(_Out_ HANDLE* Event)
3989 try
3990 {
3991 RETURN_HR_IF(E_POINTER, Event == nullptr);
3992
3993 *Event = nullptr;
3994
3995 // Duplicate the "terminated" event. The caller owns the returned handle, which stays valid even after the session is released.
3996 *Event = wsl::windows::common::wslutil::DuplicateHandle(m_sessionTerminatedEvent.get(), SYNCHRONIZE);
3997
3998 return S_OK;
3999 }
4000 CATCH_RETURN();
4001
4002 HRESULT WSLCSession::GetTerminationReason(_Out_ WSLCVirtualMachineTerminationReason* Reason, _Out_ LPWSTR* Details)
4003 try
4004 {
4005 RETURN_HR_IF(E_POINTER, Reason == nullptr || Details == nullptr);
4006
4007 *Reason = WSLCVirtualMachineTerminationReasonUnknown;
4008 *Details = nullptr;
4009
4010 // m_terminationReason/m_terminationDetails are written once before m_sessionTerminatedEvent is
4011 // signaled and never modified afterward, so observing the signaled event safely publishes them.
4012 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_sessionTerminatedEvent.is_signaled());
4013
4014 *Reason = m_terminationReason;
4015 *Details = wil::make_cotaskmem_string(m_terminationDetails.c_str()).release();
4016
4017 return S_OK;
4018 }
4019 CATCH_RETURN();
4020
4021 HRESULT WSLCSession::GetEvents(LONGLONG SinceTime, LONGLONG UntilTime, const WSLCFilter* Filters, ULONG FiltersCount, IWSLCEventStream** Stream)
4022 try
4023 {
4024 WSLCExecutionContext context(this);
4025
4026 RETURN_HR_IF_NULL(E_POINTER, Stream);
4027
4028 *Stream = nullptr;
4029
4030 auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
4031 auto stream = m_eventStore.CreateStream(Microsoft::WRL::ComPtr<WSLCSession>{this}, SinceTime, UntilTime, std::move(filters));
4032
4033 *Stream = stream.Detach();
4034 return S_OK;
4035 }
4036 CATCH_RETURN();
4037
4038 void WSLCSession::RecoverExistingContainers()
4039 {
4040 WI_ASSERT(m_runtime.HasDocker());
4041 WI_ASSERT(m_runtime.HasEvents());
4042 WI_ASSERT(m_runtime.HasVm());
4043
4044 auto containers = m_runtime.Docker().ListContainers(true); // all=true to include stopped containers
4045
4046 std::lock_guard containersLock(m_containersLock);
4047 for (const auto& dockerContainer : containers)
4048 {
4049 // Keep existing wrappers and their client COM references in place, then re-register their
4050 // ports against the restarted VM.
4051 if (auto existing = m_containers.find(dockerContainer.Id); existing != m_containers.end())
4052 {
4053 // Isolate recovery failures to this container so one bad container cannot fail lazy start
4054 // for every client, mirroring the Open() failure path below.
4055 try
4056 {
4057 existing->second->RecoverPorts(dockerContainer);
4058 }
4059 catch (...)
4060 {
4061 LOG_CAUGHT_EXCEPTION_MSG("Failed to recover container state: %hs", dockerContainer.Id.c_str());
4062 EMIT_USER_WARNING(
4063 Localization::MessageWslcFailedToRecoverContainer(wsl::shared::string::MultiByteToWide(dockerContainer.Id)));
4064 }
4065 continue;
4066 }
4067
4068 try
4069 {
4070 auto container = WSLCContainerImpl::Open(
4071 dockerContainer, *this, m_runtime, m_pluginNotifier.get(), std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), m_eventStore);
4072
4073 auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container));
4074 WI_ASSERT(inserted);
4075 }
4076 catch (...)
4077 {
4078 LOG_CAUGHT_EXCEPTION_MSG("Failed to recover container: %hs", dockerContainer.Id.c_str());
4079 EMIT_USER_WARNING(
4080 Localization::MessageWslcFailedToRecoverContainer(wsl::shared::string::MultiByteToWide(dockerContainer.Id)));
4081 }
4082 }
4083
4084 WSL_LOG(
4085 "ContainersRecovered",
4086 TraceLoggingValue(m_displayName.c_str(), "SessionName"),
4087 TraceLoggingValue(m_containers.size(), "ContainerCount"));
4088 }
4089
4090 void WSLCSession::RecoverExistingNetworks()
4091 {
4092 WI_ASSERT(m_runtime.HasDocker());
4093 WI_ASSERT(m_runtime.HasVm());
4094
4095 auto networks = m_runtime.Docker().ListNetworks();
4096
4097 std::lock_guard networksLock(m_networksLock);
4098
4099 for (const auto& network : networks)
4100 {
4101 if (!network.Labels.contains(WSLCNetworkManagedLabel))
4102 {
4103 continue;
4104 }
4105
4106 try
4107 {
4108 WI_ASSERT(!m_networks.contains(network.Name));
4109
4110 NetworkEntry entry;
4111 entry.Id = network.Id;
4112 entry.Driver = network.Driver;
4113 entry.Scope = network.Scope;
4114 entry.Internal = network.Internal;
4115 entry.Labels = network.Labels;
4116 entry.Options = network.Options;
4117 entry.IPAM.Driver = network.IPAM.Driver;
4118 if (network.IPAM.Config)
4119 {
4120 auto& cfgs = entry.IPAM.Config.emplace();
4121 for (const auto& c : *network.IPAM.Config)
4122 {
4123 cfgs.push_back({c.Subnet, c.Gateway, c.IPRange});
4124 }
4125 }
4126
4127 auto [_, inserted] = m_networks.insert({network.Name, std::move(entry)});
4128 WI_VERIFY(inserted);
4129 }
4130 catch (...)
4131 {
4132 LOG_CAUGHT_EXCEPTION_MSG("Failed to recover network: %hs", network.Name.c_str());
4133 EMIT_USER_WARNING(Localization::MessageWslcFailedToRecoverNetwork(wsl::shared::string::MultiByteToWide(network.Name)));
4134 }
4135 }
4136
4137 WSL_LOG(
4138 "NetworksRecovered",
4139 TraceLoggingValue(m_displayName.c_str(), "SessionName"),
4140 TraceLoggingValue(m_networks.size(), "NetworkCount"));
4141 }
4142
4143 } // namespace wsl::windows::service::wslc