master
cpp 961 lines 38.8 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 PluginTests.cpp
8
9 Abstract:
10
11 This file contains test cases for the plugin API.
12
13 --*/
14
15 #include "precomp.h"
16 #include "Common.h"
17 #include "registry.hpp"
18 #include "PluginTests.h"
19 #include "wslc.h"
20 #include "WSLCContainerLauncher.h"
21 #include "WSLCProcessLauncher.h"
22 #include "wslc/e2e/WSLCE2EHelpers.h"
23
24 using namespace wsl::windows::common::registry;
25 using WSLCE2ETests::StartLocalRegistry;
26
27 extern std::wstring g_testDistroPath;
28 extern std::wstring g_testDataPath;
29
30 class PluginTests
31 {
32 std::wstring logFile;
33 bool m_initialized = false;
34 std::wstring pluginDll;
35 std::optional<WslConfigChange> config;
36
37 WSL_TEST_CLASS(PluginTests)
38
39 TEST_CLASS_SETUP(TestClassSetup)
40 {
41 VERIFY_ARE_EQUAL(LxsstuInitialize(FALSE), TRUE);
42 m_initialized = true;
43 logFile = wil::GetCurrentDirectoryW<std::wstring>() + L"\\plugin-logs.txt";
44
45 const auto currentDll = std::filesystem::path(wil::GetModuleFileNameW<std::wstring>(wil::GetModuleInstanceHandle()));
46 const auto pluginDllPath = currentDll.parent_path() / L"testplugin.dll";
47 if (!std::filesystem::exists(pluginDllPath))
48 {
49 const std::wstring message = L"Plugin not found in: " + pluginDllPath.wstring();
50 VERIFY_FAIL(message.c_str());
51 return false;
52 }
53
54 pluginDll = pluginDllPath.wstring();
55
56 // Disable VM timeouts during the plugin tests
57 config.emplace(LxssGenerateTestConfig({.vmIdleTimeout = -1}));
58
59 return true;
60 }
61
62 TEST_CLASS_CLEANUP(TestClassCleanup)
63 {
64
65 auto key = OpenLxssMachineKey(KEY_ALL_ACCESS);
66 DeleteKey(key.get(), L"Test");
67
68 key = OpenKey(key.get(), L"Plugins", KEY_SET_VALUE);
69 DeleteValue(key.get(), L"TestPlugin");
70
71 RestartWslService();
72
73 std::wifstream file(logFile);
74 const auto fileContent = std::wstring{std::istreambuf_iterator<wchar_t>(file), {}};
75 LogInfo("Logfile: %ls", fileContent.c_str());
76 file.close();
77
78 StopWslService();
79 if (!DeleteFile(logFile.c_str()))
80 {
81 VERIFY_ARE_EQUAL(ERROR_FILE_NOT_FOUND, GetLastError());
82 }
83
84 if (m_initialized)
85 {
86 LxsstuUninitialize(FALSE);
87 }
88
89 return true;
90 }
91
92 void ConfigurePlugin(PluginTestType testCase) const
93 {
94 StopWslService();
95 if (!DeleteFile(logFile.c_str()))
96 {
97 VERIFY_ARE_EQUAL(ERROR_FILE_NOT_FOUND, GetLastError());
98 }
99
100 const auto testKey = OpenTestRegistryKey(KEY_SET_VALUE);
101 WriteDword(testKey.get(), nullptr, c_testType, static_cast<DWORD>(testCase));
102 WriteString(testKey.get(), nullptr, c_logFile, logFile.c_str());
103
104 const auto lxssKey =
105 CreateKey(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Lxss\\Plugins", KEY_SET_VALUE, nullptr, 0);
106 WriteString(lxssKey.get(), nullptr, L"TestPlugin", pluginDll.c_str());
107
108 RestartWslService();
109 }
110
111 static void StartWsl(int expectedExitCode, const std::wstring& expectedOutput = {})
112 {
113 auto [output, error] = LxsstuLaunchWslAndCaptureOutput(L"echo -n OK", expectedExitCode);
114 if (expectedExitCode == 0)
115 {
116 VERIFY_ARE_EQUAL(output, L"OK");
117 }
118 else
119 {
120 VERIFY_ARE_EQUAL(output, expectedOutput);
121 }
122 }
123
124 void ValidateLogFile(LPCWSTR expected) const
125 {
126 StopWslService();
127
128 std::wifstream file(logFile);
129 auto fileContent = std::wstring{std::istreambuf_iterator<wchar_t>(file), {}};
130 LogInfo("Logfile: %ls", fileContent.c_str());
131
132 auto fileLines = wsl::shared::string::Split<wchar_t>(fileContent, '\n');
133 auto expectedLines = wsl::shared::string::Split<wchar_t>(expected, '\n');
134
135 for (size_t i = 0; i < std::max(fileLines.size(), expectedLines.size()); i++)
136 {
137 if (i >= fileLines.size())
138 {
139 std::wstring message = L"Line is expected but not in log file: " + expectedLines[i];
140 VERIFY_FAIL(message.c_str());
141 }
142 else if (i >= expectedLines.size())
143 {
144 std::wstring message = L"Line is in file but not expected: " + fileLines[i];
145 VERIFY_FAIL(message.c_str());
146 }
147
148 const auto& expected = expectedLines[i];
149 const auto& actual = fileLines[i];
150 if (!PathMatchSpec(actual.c_str(), expected.c_str()))
151 {
152 LogInfo("Plugin log: %ls", fileContent.c_str());
153 std::wstring message = L"Line (" + actual + L") didn't match pattern: " + expected;
154 VERIFY_FAIL(message.c_str());
155 }
156 }
157 }
158
159 WSL2_TEST_METHOD(Success)
160 {
161 constexpr auto ExpectedOutput =
162 LR"(Plugin loaded. TestMode=1
163 VM created (settings->CustomConfigurationFlags=0)
164 Folder mounted (* -> /test-plugin)
165 Process created
166 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
167 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
168 VM Stopping)";
169
170 ConfigurePlugin(PluginTestType::Success);
171 StartWsl(0);
172 ValidateLogFile(ExpectedOutput);
173 }
174
175 WSL2_TEST_METHOD(CustomKernelOverriddenByPolicy)
176 {
177 RegistryKeyChange policy(
178 HKEY_LOCAL_MACHINE, wsl::windows::policies::c_registryKey, wsl::windows::policies::c_allowCustomKernelUserSetting, static_cast<DWORD>(0));
179
180 WslConfigChange config(LxssGenerateTestConfig({.kernel = L"kernel-that-doesn't-exist"}));
181
182 constexpr auto ExpectedOutput =
183 LR"(Plugin loaded. TestMode=1
184 VM created (settings->CustomConfigurationFlags=0)
185 Folder mounted (* -> /test-plugin)
186 Process created
187 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
188 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
189 VM Stopping)";
190
191 ConfigurePlugin(PluginTestType::Success);
192 StartWsl(0);
193 ValidateLogFile(ExpectedOutput);
194 }
195
196 WSL2_TEST_METHOD(DuplicatedPlugin)
197 {
198 constexpr auto ExpectedOutput =
199 LR"(Plugin loaded. TestMode=1
200 VM created (settings->CustomConfigurationFlags=0)
201 Folder mounted (* -> /test-plugin)
202 Process created
203 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
204 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
205 VM Stopping)";
206
207 ConfigurePlugin(PluginTestType::Success);
208
209 // Register the same plugin dll twice. Validate that it's only called once.
210 const auto key =
211 CreateKey(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Lxss\\Plugins", KEY_SET_VALUE, nullptr, 0);
212 WriteString(key.get(), nullptr, L"TestPlugin-duplicated", pluginDll.c_str());
213 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { DeleteValue(key.get(), L"TestPlugin-duplicated"); });
214 RestartWslService();
215
216 StartWsl(0);
217 ValidateLogFile(ExpectedOutput);
218 }
219
220 WSL2_TEST_METHOD(CustomKernel)
221 {
222 constexpr auto ExpectedOutput =
223 LR"(Plugin loaded. TestMode=1
224 VM created (settings->CustomConfigurationFlags=1)
225 OnVmStarted: E_ACCESSDENIED
226 VM Stopping)";
227
228 #ifdef WSL_KERNEL_PATH
229
230 std::wstring kernelPath = WIDEN(WSL_KERNEL_PATH);
231
232 #else
233
234 auto kernelPath = wsl::windows::common::wslutil::GetMsiPackagePath().value_or(L"");
235 VERIFY_IS_FALSE(kernelPath.empty());
236 kernelPath += L"\\tools\\kernel";
237
238 #endif
239
240 WslConfigChange config(LxssGenerateTestConfig({.vmIdleTimeout = 1, .kernel = kernelPath}));
241
242 ConfigurePlugin(PluginTestType::Success);
243 StartWsl(
244 -1,
245 FormatErrorMessage(
246 L"A fatal error was returned by plugin 'TestPlugin'",
247 L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_ACCESSDENIED"));
248
249 ValidateLogFile(ExpectedOutput);
250 }
251
252 WSL2_TEST_METHOD(CustomKernelCommandLine)
253 {
254 constexpr auto ExpectedOutput =
255 LR"(Plugin loaded. TestMode=1
256 VM created (settings->CustomConfigurationFlags=2)
257 Folder mounted (* -> /test-plugin)
258 Process created
259 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
260 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
261 VM Stopping)";
262
263 WslConfigChange config(LxssGenerateTestConfig({.vmIdleTimeout = 1, .kernelCommandLine = L"custom"}));
264
265 ConfigurePlugin(PluginTestType::Success);
266 StartWsl(0);
267 ValidateLogFile(ExpectedOutput);
268 }
269
270 WSL2_TEST_METHOD(DistroIdStaysTheSame)
271 {
272 constexpr auto ExpectedOutput =
273 LR"(Plugin loaded. TestMode=10
274 VM created (settings->CustomConfigurationFlags=0)
275 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
276 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
277 VM Stopping
278 VM created (settings->CustomConfigurationFlags=0)
279 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
280 OnDistroStarted: received same GUID
281 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
282 VM Stopping)";
283
284 ConfigurePlugin(PluginTestType::SameDistroId);
285 StartWsl(0);
286 WslShutdown();
287 StartWsl(0);
288
289 ValidateLogFile(ExpectedOutput);
290 }
291
292 WSL2_TEST_METHOD(InitPidIsDifferent)
293 {
294 constexpr auto ExpectedOutput =
295 LR"(Plugin loaded. TestMode=14
296 VM created (settings->CustomConfigurationFlags=0)
297 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
298 Distribution Stopping, name=test_distro, package=, PidNs=*
299 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
300 Init's pid is different (* ! = *)
301 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
302 VM Stopping)";
303
304 ConfigurePlugin(PluginTestType::InitPidIsDifferent);
305 StartWsl(0);
306 TerminateDistribution();
307 StartWsl(0);
308
309 ValidateLogFile(ExpectedOutput);
310 }
311
312 WSL2_TEST_METHOD(PluginUpdateRequired)
313 {
314 constexpr auto ExpectedOutput =
315 LR"(Plugin loaded. TestMode=9
316 OnLoad: WSL_E_PLUGINREQUIRESUPDATE)";
317
318 ConfigurePlugin(PluginTestType::PluginRequiresUpdate);
319 StartWsl(
320 -1,
321 FormatErrorMessage(
322 L"The plugin 'TestPlugin' requires a newer version of WSL. Please run: wsl.exe --update",
323 L"Wsl/Service/CreateInstance/CreateVm/Plugin/WSL_E_PLUGIN_REQUIRES_UPDATE"));
324
325 ValidateLogFile(ExpectedOutput);
326 }
327
328 WSL2_TEST_METHOD(APIErrors)
329 {
330 constexpr auto ExpectedOutput =
331 LR"(Plugin loaded. TestMode=7
332 VM created (settings->CustomConfigurationFlags=0)
333 API error tests passed
334 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
335 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
336 VM Stopping)";
337
338 ConfigurePlugin(PluginTestType::ApiErrors);
339 StartWsl(0);
340 ValidateLogFile(ExpectedOutput);
341 }
342
343 WSL1_TEST_METHOD(SuccessWSL1)
344 {
345 constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=1)";
346
347 ConfigurePlugin(PluginTestType::Success);
348 StartWsl(0);
349 ValidateLogFile(ExpectedOutput);
350 }
351
352 WSL2_TEST_METHOD(LoadFailureFatalWSL2)
353 {
354 constexpr auto ExpectedOutput =
355 LR"(Plugin loaded. TestMode=2
356 OnLoad: E_UNEXPECTED)";
357
358 ConfigurePlugin(PluginTestType::FailToLoad);
359 StartWsl(
360 -1,
361 FormatErrorMessage(
362 L"A fatal error was returned by plugin 'TestPlugin'", L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_UNEXPECTED"));
363 ValidateLogFile(ExpectedOutput);
364 }
365
366 WSL1_TEST_METHOD(LoadFailureNonFatalWSL1)
367 {
368 constexpr auto ExpectedOutput =
369 LR"(Plugin loaded. TestMode=2
370 OnLoad: E_UNEXPECTED)";
371
372 ConfigurePlugin(PluginTestType::FailToLoad);
373 StartWsl(0);
374 ValidateLogFile(ExpectedOutput);
375 }
376
377 WSL2_TEST_METHOD(VmStartFailure)
378 {
379 constexpr auto ExpectedOutput =
380 LR"(Plugin loaded. TestMode=3
381 VM created (settings->CustomConfigurationFlags=0)
382 OnVmStarted: E_UNEXPECTED
383 VM Stopping)";
384
385 ConfigurePlugin(PluginTestType::FailToStartVm);
386 StartWsl(
387 -1,
388 FormatErrorMessage(
389 L"A fatal error was returned by plugin 'TestPlugin'", L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_UNEXPECTED"));
390 ValidateLogFile(ExpectedOutput);
391 }
392
393 WSL2_TEST_METHOD(VmStartFailureWithPluginErrorTwice)
394 {
395 constexpr auto ExpectedOutput =
396 LR"(Plugin loaded. TestMode=13
397 VM created (settings->CustomConfigurationFlags=0)
398 OnVmStarted: E_UNEXPECTED
399 VM Stopping
400 VM created (settings->CustomConfigurationFlags=0)
401 OnVmStarted: E_UNEXPECTED
402 VM Stopping)";
403
404 ConfigurePlugin(PluginTestType::FailToStartVmWithPluginErrorMessage);
405
406 StartWsl(
407 -1,
408 FormatErrorMessage(
409 L"A fatal error was returned by plugin 'TestPlugin'. Error message: 'Plugin error message'",
410 L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_UNEXPECTED"));
411
412 StartWsl(
413 -1,
414 FormatErrorMessage(
415 L"A fatal error was returned by plugin 'TestPlugin'. Error message: 'Plugin error message'",
416 L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_UNEXPECTED"));
417
418 ValidateLogFile(ExpectedOutput);
419 }
420
421 WSL2_TEST_METHOD(VmStopFailure)
422 {
423 constexpr auto ExpectedOutput =
424 LR"(Plugin loaded. TestMode=5
425 VM created (settings->CustomConfigurationFlags=0)
426 Distribution started, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
427 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
428 VM Stopping
429 OnVmStopping: E_UNEXPECTED)";
430
431 ConfigurePlugin(PluginTestType::FailToStopVm);
432 StartWsl(0);
433 ValidateLogFile(ExpectedOutput);
434 }
435
436 WSL2_TEST_METHOD(DistributionStartFailure)
437 {
438 constexpr auto ExpectedOutput =
439 LR"(Plugin loaded. TestMode=4
440 VM created (settings->CustomConfigurationFlags=0)
441 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
442 OnDistroStarted: E_UNEXPECTED
443 VM Stopping)";
444
445 ConfigurePlugin(PluginTestType::FailToStartDistro);
446 StartWsl(
447 -1,
448 FormatErrorMessage(
449 L"A fatal error was returned by plugin 'TestPlugin'", L"Wsl/Service/CreateInstance/Plugin/E_UNEXPECTED"));
450 ValidateLogFile(ExpectedOutput);
451 }
452
453 WSL2_TEST_METHOD(DistributionStopFailure)
454 {
455 constexpr auto ExpectedOutput =
456 LR"(Plugin loaded. TestMode=6
457 VM created (settings->CustomConfigurationFlags=0)
458 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
459 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
460 OnDistroStopping: E_UNEXPECTED
461 VM Stopping)";
462
463 ConfigurePlugin(PluginTestType::FailToStopDistro);
464 StartWsl(0);
465 ValidateLogFile(ExpectedOutput);
466 }
467
468 WSL2_TEST_METHOD(ErrorMessageStartVm)
469 {
470 constexpr auto ExpectedOutput =
471 LR"(Plugin loaded. TestMode=11
472 VM created (settings->CustomConfigurationFlags=0)
473 OnVmStarted: E_FAIL
474 VM Stopping)";
475
476 ConfigurePlugin(PluginTestType::ErrorMessageStartVm);
477 StartWsl(
478 -1,
479 FormatErrorMessage(
480 L"A fatal error was returned by plugin 'TestPlugin'. Error message: 'StartVm plugin error message'",
481 L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_FAIL"));
482
483 ValidateLogFile(ExpectedOutput);
484 }
485
486 WSL2_TEST_METHOD(ErrorMessageStartDistro)
487 {
488 constexpr auto ExpectedOutput =
489 LR"(Plugin loaded. TestMode=12
490 VM created (settings->CustomConfigurationFlags=0)
491 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
492 OnDistroStarted: E_FAIL
493 VM Stopping)";
494
495 ConfigurePlugin(PluginTestType::ErrorMessageStartDistro);
496 StartWsl(
497 -1,
498 FormatErrorMessage(
499 L"A fatal error was returned by plugin 'TestPlugin'. Error message: 'StartDistro plugin error message'",
500 L"Wsl/Service/CreateInstance/Plugin/E_FAIL"));
501
502 ValidateLogFile(ExpectedOutput);
503 }
504
505 WSL2_TEST_METHOD(RegisterSuccess)
506 {
507 ConfigurePlugin(PluginTestType::Success);
508
509 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--import plugin-test-distro . \"" + g_testDistroPath + L"\" --version 2"), 0L);
510 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister plugin-test-distro"), 0L);
511
512 constexpr auto ExpectedOutput =
513 LR"(Plugin loaded. TestMode=1
514 VM created (settings->CustomConfigurationFlags=0)
515 Folder mounted (* -> /test-plugin)
516 Process created
517 Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13
518 Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13
519 VM Stopping)";
520
521 ValidateLogFile(ExpectedOutput);
522 }
523
524 WSL2_TEST_METHOD(ImportInplaceSuccess)
525 {
526 ConfigurePlugin(PluginTestType::Success);
527
528 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--import plugin-test-distro . \"" + g_testDistroPath + L"\" --version 2"), 0L);
529 WslShutdown();
530 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--export plugin-test-distro plugin-test-distro.vhdx --format vhd"), 0L);
531 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister plugin-test-distro"), 0L);
532 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--import-in-place plugin-test-distro-vhd plugin-test-distro.vhdx"), 0L);
533 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister plugin-test-distro-vhd"), 0L);
534
535 constexpr auto ExpectedOutput =
536 LR"(Plugin loaded. TestMode=1
537 VM created (settings->CustomConfigurationFlags=0)
538 Folder mounted (* -> /test-plugin)
539 Process created
540 Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13
541 VM Stopping
542 Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13
543 VM created (settings->CustomConfigurationFlags=0)
544 Folder mounted (* -> /test-plugin)
545 Process created
546 Distribution registered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=13
547 Distribution unregistered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=13
548 VM Stopping)";
549
550 ValidateLogFile(ExpectedOutput);
551 }
552
553 WSL2_TEST_METHOD(RegisterUnregisterFail)
554 {
555 ConfigurePlugin(PluginTestType::FailToRegisterUnregisterDistro);
556
557 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--import plugin-test-distro . \"" + g_testDistroPath + L"\" --version 2"), 0L);
558 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister plugin-test-distro"), 0L);
559
560 constexpr auto ExpectedOutput =
561 LR"(Plugin loaded. TestMode=15
562 VM created (settings->CustomConfigurationFlags=0)
563 Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13
564 OnDistributionRegistered: E_UNEXPECTED
565 Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13
566 OnDistributionUnregistered: E_UNEXPECTED
567 VM Stopping)";
568
569 ValidateLogFile(ExpectedOutput);
570 }
571
572 WSL2_TEST_METHOD(ExecuteDistroCommand)
573 {
574 ConfigurePlugin(PluginTestType::RunDistroCommand);
575
576 constexpr auto ExpectedOutput =
577 LR"(Plugin loaded. TestMode=16
578 VM created (settings->CustomConfigurationFlags=0)
579 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
580 Process created
581 Failed process launch returned: -2147467259
582 Invalid distro launch returned: -2147220717
583 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
584 VM Stopping)";
585
586 StartWsl(0);
587 ValidateLogFile(ExpectedOutput);
588 }
589
590 WSL2_TEST_METHOD(PluginToken)
591 {
592 ConfigurePlugin(PluginTestType::GetUsername);
593
594 constexpr auto ExpectedOutput =
595 LR"(Plugin loaded. TestMode=17
596 VM created (settings->CustomConfigurationFlags=0)
597 Username: *
598 Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
599 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
600 VM Stopping)";
601
602 StartWsl(0);
603 ValidateLogFile(ExpectedOutput);
604 }
605 static wil::com_ptr<IWSLCSessionManager> OpenWslcSessionManager()
606 {
607 wil::com_ptr<IWSLCSessionManager> sessionManager;
608 VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
609 wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
610 return sessionManager;
611 }
612
613 static wil::com_ptr<IWSLCSession> CreateWslcSession(LPCWSTR Name, WSLCNetworkingMode NetworkingMode = WSLCNetworkingModeNone, LPCWSTR StoragePath = nullptr)
614 {
615 WSLCSessionSettings settings{};
616 settings.DisplayName = Name;
617 settings.CpuCount = 4;
618 settings.MemoryMb = 4096;
619 settings.BootTimeoutMs = 30 * 1000;
620 settings.NetworkingMode = NetworkingMode;
621 settings.StoragePath = StoragePath;
622 settings.MaximumStorageSizeMb = 1024 * 20; // 20GB, only used when StoragePath is set.
623
624 auto manager = OpenWslcSessionManager();
625 wil::com_ptr<IWSLCSession> session;
626 VERIFY_SUCCEEDED(manager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session));
627 wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
628
629 WSLCSessionState state{};
630 VERIFY_SUCCEEDED(session->GetState(&state));
631 VERIFY_ARE_EQUAL(state, WSLCSessionStateRunning);
632
633 return session;
634 }
635
636 WSL2_TEST_METHOD(WslcSuccess)
637 {
638 ConfigurePlugin(PluginTestType::WslcSuccess);
639
640 {
641 auto session = CreateWslcSession(L"plugin-wslc-test");
642
643 LoadTestImage(*session, "debian:latest");
644
645 // Create a container that will have a stuck process so it's still in a running state when the callback is made.
646 wsl::windows::common::WSLCContainerLauncher launcher(
647 "debian:latest", "wslc-plugin-container", {"/bin/sh", "-c", "sleep 120"});
648
649 auto container = launcher.Launch(*session, WSLCContainerStartFlagsAttach);
650 VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
651
652 // Delete the image so we get an ImageDeleted notification before the session goes away.
653 WSLCDeleteImageOptions options{.Image = "debian:latest", .Flags = WSLCDeleteImageFlagsForce};
654 wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
655 VERIFY_SUCCEEDED(session->DeleteImage(&options, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
656 }
657
658 const auto ExpectedOutput = std::format(
659 LR"(Plugin loaded. TestMode=18
660 WSLC Session created, name=plugin-wslc-test, id=*, pid=*, token=set, sid=set
661 Command: 'echo -n stdout-ok && echo -n stderr-ok >&2', status=0, stdout: stdout-ok, stderr: stderr-ok
662 Command: 'cat', status=0, stdout: stdin-ok, stderr:
663 Command: 'exit 12', status=12, stdout: , stderr:
664 Command: 'echo -n $ENV', status=0, stdout: env-ok, stderr:
665 WSLCCreateProcess(does-not-exist): {:x}, errno=2
666 WSLCProcessGetFd(999): {}
667 WSLCProcessGetExitCode(<running>): {}
668 WSLC RW folder mounted at: /mnt/wsl-plugin/plugin-rw-test
669 Command: 'cat /mnt/wsl-plugin/plugin-rw-test/plugin-test.txt', status=0, stdout: Windows-content, stderr:
670 Command: 'cat /mnt/wsl-plugin/plugin-rw-test/plugin-denied.txt', status=1, stdout: , stderr: *
671 WSLC RO folder mounted at: /mnt/wsl-plugin/plugin-ro-test
672 Command: 'echo fail > /mnt/wsl-plugin/plugin-ro-test/should-not-exist.txt', status=1, stdout: , stderr: *
673 WSLCMountFolder(nonexistent): {}
674 WSLCMountFolder(relative): {}
675 Test completed
676 WSLC Image created, session=*, id=sha256:*, name=debian:latest
677 WSLC Container started, session=*, id=*, name=/wslc-plugin-container, image=debian:latest, state=*
678 WSLC Container stopping, session=*, id=*
679 WSLC Image deleted, session=*, id=*
680 WSLC Session stopping, name=plugin-wslc-test, id=*)",
681 static_cast<uint32_t>(E_FAIL),
682 E_INVALIDARG,
683 HRESULT_FROM_WIN32(ERROR_INVALID_STATE),
684 HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND),
685 E_INVALIDARG);
686
687 ValidateLogFile(ExpectedOutput.c_str());
688 }
689
690 WSL2_TEST_METHOD(WslcPullImageNotification)
691 {
692 ConfigurePlugin(PluginTestType::WslcImagePull);
693
694 {
695 auto session = CreateWslcSession(L"plugin-wslc-pull-test", WSLCNetworkingModeConsomme);
696
697 // Load the registry and debian images.
698 LoadTestImage(*session, "debian:latest");
699
700 // Start a local registry container.
701 auto [registryContainer, registryAddress] = StartLocalRegistry(*session);
702
703 // Tag debian:latest for the local registry and push it.
704 auto registryImage = std::format("{}/debian:latest", registryAddress);
705 auto registryRepo = std::format("{}/debian", registryAddress);
706 WSLCTagImageOptions tagOptions{};
707 tagOptions.Image = "debian:latest";
708 tagOptions.Repo = registryRepo.c_str();
709 tagOptions.Tag = "latest";
710 VERIFY_SUCCEEDED(session->TagImage(&tagOptions));
711
712 auto emptyAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
713 VERIFY_SUCCEEDED(session->PushImage(registryImage.c_str(), emptyAuth.c_str(), nullptr, nullptr));
714
715 // Delete the local tagged copy so PullImage actually downloads it.
716 WSLCDeleteImageOptions deleteOpts{.Image = registryImage.c_str(), .Flags = WSLCDeleteImageFlagsNone};
717 wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
718 VERIFY_SUCCEEDED(session->DeleteImage(&deleteOpts, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
719
720 // Pull the image back — this should trigger the ImageCreated plugin callback.
721 VERIFY_SUCCEEDED(session->PullImage(registryImage.c_str(), nullptr, nullptr, nullptr));
722 }
723
724 constexpr auto ExpectedOutput =
725 LR"(Plugin loaded. TestMode=21
726 WSLC Session created, name=plugin-wslc-pull-test, id=*, pid=*, token=set, sid=set
727 WSLC Image created, session=*, id=sha256:*, name=debian:latest
728 WSLC Image created, session=*, id=sha256:*, name=wslc-registry:latest
729 WSLC Container started, session=*, id=*, name=*, image=wslc-registry:latest, state=running
730 WSLC Image created, session=*, id=sha256:*, name=127.0.0.1:5000/debian:latest
731 WSLC Container stopping, session=*, id=*
732 WSLC Session stopping, name=plugin-wslc-pull-test, id=*)";
733
734 ValidateLogFile(ExpectedOutput);
735 }
736
737 WSL2_TEST_METHOD(WslcSessionRejected)
738 {
739 ConfigurePlugin(PluginTestType::WslcSessionRejected);
740
741 WSLCSessionSettings settings{};
742 settings.DisplayName = L"plugin-wslc-rejected";
743 settings.CpuCount = 4;
744 settings.MemoryMb = 2048;
745 settings.BootTimeoutMs = 30 * 1000;
746 settings.MaximumStorageSizeMb = 1024 * 20;
747 settings.NetworkingMode = WSLCNetworkingModeNone;
748
749 auto manager = OpenWslcSessionManager();
750 wil::com_ptr<IWSLCSession> session;
751 const auto hr = manager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session);
752 ValidateCOMErrorMessageContains(L"A fatal error was returned by plugin 'TestPlugin'");
753 VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED));
754
755 constexpr auto ExpectedOutput =
756 LR"(Plugin loaded. TestMode=19
757 WSLC Session created, name=plugin-wslc-rejected, id=*, pid=*, token=set, sid=set
758 OnWslcSessionCreated: ERROR_ACCESS_DENIED)";
759
760 ValidateLogFile(ExpectedOutput);
761 }
762
763 WSL2_TEST_METHOD(WslcContainerRejected)
764 {
765 ConfigurePlugin(PluginTestType::WslcContainerRejected);
766
767 {
768 auto session = CreateWslcSession(L"plugin-wslc-container-rejected");
769
770 LoadTestImage(*session, "debian:latest");
771
772 wsl::windows::common::WSLCContainerLauncher launcher(
773 "debian:latest", "wslc-plugin-rejected-container", {"/bin/sh", "-c", "echo nope"});
774
775 auto [hr, container] = launcher.LaunchNoThrow(*session, WSLCContainerStartFlagsAttach);
776 ValidateCOMErrorMessageContains(L"A fatal error was returned by plugin 'TestPlugin'");
777 VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED));
778 }
779
780 constexpr auto ExpectedOutput =
781 LR"(Plugin loaded. TestMode=20
782 WSLC Session created, name=plugin-wslc-container-rejected, id=*, pid=*, token=set, sid=set
783 WSLC Image created, session=*, id=sha256:*, name=debian:latest
784 WSLC Container started, session=*, id=*, name=*, image=debian:latest, state=*
785 OnWslcContainerStarted: ERROR_ACCESS_DENIED
786 WSLC Session stopping, name=plugin-wslc-container-rejected, id=*)";
787
788 ValidateLogFile(ExpectedOutput);
789 }
790
791 // Validates the VM-lifecycle hooks: OnWslcVmStarted fires each time the VM is (re)created and
792 // OnWslcVmStopping each time it is torn down, decoupled from the once-per-session hooks. Also
793 // proves the started hook can call back into the session (WSLCCreateProcess) without deadlocking.
794 WSL2_TEST_METHOD(WslcVmRestart)
795 {
796 ConfigurePlugin(PluginTestType::WslcVmRestart);
797
798 // Idle termination only tears down storage-backed sessions (tmpfs state is unrecoverable), so
799 // this restart lifecycle test needs a dedicated persistent storage directory.
800 const auto storageDir = std::filesystem::current_path() / "test-storage-wslc-vm-restart";
801 std::error_code storageError;
802 std::filesystem::remove_all(storageDir, storageError);
803 std::filesystem::create_directories(storageDir);
804 auto storageCleanup = wil::scope_exit([&]() {
805 std::error_code ec;
806 std::filesystem::remove_all(storageDir, ec);
807 });
808
809 {
810 auto session = CreateWslcSession(L"plugin-wslc-vm-restart", WSLCNetworkingModeNone, storageDir.c_str());
811
812 // First operation brings the VM up -> OnWslcVmStarted (which reentrantly runs a process).
813 {
814 wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
815 auto process = launcher.Launch(*session);
816 }
817
818 // Force idle teardown of the running VM -> OnWslcVmStopping.
819 BOOL wasAlreadyIdle = TRUE;
820 VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle));
821 VERIFY_IS_FALSE(wasAlreadyIdle);
822
823 // Next operation lazily restarts the VM -> OnWslcVmStarted fires again.
824 {
825 wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
826 auto process = launcher.Launch(*session);
827 }
828
829 // Session teardown tears the second VM down -> OnWslcVmStopping, then OnWslcSessionStopping.
830 }
831
832 constexpr auto ExpectedOutput =
833 LR"(Plugin loaded. TestMode=22
834 WSLC Session created, name=plugin-wslc-vm-restart, id=*, pid=*, token=set, sid=set
835 WSLC VM started, session=*
836 WSLC VM started reentrant WSLCCreateProcess: ok
837 WSLC VM started mount+unmount: ok
838 WSLC VM stopping, session=*
839 WSLC VM stopping reentrant WSLCCreateProcess: ok
840 WSLC VM stopping mount+unmount: ok
841 WSLC VM started, session=*
842 WSLC VM started reentrant WSLCCreateProcess: ok
843 WSLC VM started mount+unmount: ok
844 WSLC VM stopping, session=*
845 WSLC VM stopping reentrant WSLCCreateProcess: failed
846 WSLC VM stopping mount+unmount: skipped
847 WSLC Session stopping, name=plugin-wslc-vm-restart, id=*)";
848
849 ValidateLogFile(ExpectedOutput);
850 }
851
852 // Validates that an announced VM teardown always happens. The plugin leaves a process running when
853 // OnWslcVmStopping returns -- which under the previous design made the runtime abandon the
854 // teardown -- and starts a call from a thread of its own during the notification window. The VM
855 // must stop anyway, the leaked process must die with it, and the windowed call must be served by
856 // the stopping VM rather than blocking on the teardown it cannot influence.
857 WSL2_TEST_METHOD(WslcVmStopCommitted)
858 {
859 ConfigurePlugin(PluginTestType::WslcVmStopCommitted);
860
861 // Idle termination only tears down storage-backed sessions (see WslcVmRestart).
862 const auto storageDir = std::filesystem::current_path() / "test-storage-wslc-vm-stop-committed";
863 std::error_code storageError;
864 std::filesystem::remove_all(storageDir, storageError);
865 std::filesystem::create_directories(storageDir);
866 auto storageCleanup = wil::scope_exit([&]() {
867 std::error_code ec;
868 std::filesystem::remove_all(storageDir, ec);
869 });
870
871 {
872 auto session = CreateWslcSession(L"plugin-wslc-vm-stop-committed", WSLCNetworkingModeNone, storageDir.c_str());
873
874 // Bring the VM up -> OnWslcVmStarted.
875 {
876 wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
877 auto process = launcher.Launch(*session);
878 }
879
880 // The teardown is announced and then carried out, even though the plugin's callback left a
881 // process running. The session is genuinely idle afterwards.
882 BOOL wasAlreadyIdle = TRUE;
883 VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle));
884 VERIFY_IS_FALSE(wasAlreadyIdle);
885
886 // The VM is gone, so this starts a fresh one -> a second OnWslcVmStarted.
887 {
888 wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
889 auto process = launcher.Launch(*session);
890 }
891 }
892
893 // "leaked process died: yes" is the assertion that the announced stop actually happened: the
894 // process the callback left running was killed by the teardown rather than keeping the VM
895 // alive. "stop-window caller: ok" shows a plugin call from another thread was served by the
896 // stopping VM instead of deadlocking against the teardown that callback was holding up. Both
897 // are reported when that thread is joined, at session teardown.
898 constexpr auto ExpectedOutput =
899 LR"(Plugin loaded. TestMode=23
900 WSLC Session created, name=plugin-wslc-vm-stop-committed, id=*, pid=*, token=set, sid=set
901 WSLC VM started, session=*
902 WSLC VM stopping, session=*
903 WSLC VM stopping leaked process: ok
904 WSLC VM started, session=*
905 WSLC stop-window caller: ok
906 WSLC leaked process died: yes
907 WSLC Session stopping, name=plugin-wslc-vm-stop-committed, id=*)";
908
909 ValidateLogFile(ExpectedOutput);
910 }
911
912 WSL2_TEST_METHOD(WslcVmNeverStarted)
913 {
914 ConfigurePlugin(PluginTestType::WslcVmNeverStarted);
915
916 // A session whose VM is never needed. VM bring-up is lazy, so creating and destroying the
917 // session must not produce either VM notification: OnWslcVmStopping is documented to fire
918 // exactly once per OnWslcVmStarted, and a stop for a VM that never existed would break the
919 // pairing every plugin relies on to track VM lifetime. The plugin also issues a call from
920 // OnWslcSessionCreated, which must be rejected rather than bring a VM up.
921 {
922 auto session = CreateWslcSession(L"plugin-wslc-vm-never-started");
923 VERIFY_IS_NOT_NULL(session.get());
924 }
925
926 constexpr auto ExpectedOutput =
927 LR"(Plugin loaded. TestMode=24
928 WSLC Session created, name=plugin-wslc-vm-never-started, id=*, pid=*, token=set, sid=set
929 WSLC no-vm caller: rejected
930 WSLC Session stopping, name=plugin-wslc-vm-never-started, id=*)";
931
932 ValidateLogFile(ExpectedOutput);
933 }
934
935 // This test must run last so it doesn't break test cases that depends on plugin signature.
936 WSL2_TEST_METHOD(InvalidPluginSignature)
937 {
938 if constexpr (!wsl::shared::OfficialBuild)
939 {
940 LogSkipped("This test only applies to signed builds");
941 return;
942 }
943
944 StopWslService();
945
946 // Append one byte at the end of the plugin dll to break its signature
947 wil::unique_handle plugin{CreateFile(pluginDll.c_str(), FILE_APPEND_DATA, 0, nullptr, OPEN_EXISTING, 0, nullptr)};
948 VERIFY_IS_TRUE(plugin.is_valid());
949
950 char c{};
951 VERIFY_IS_TRUE(WriteFile(plugin.get(), &c, sizeof(c), nullptr, nullptr));
952 plugin.reset();
953
954 ConfigurePlugin(PluginTestType::ErrorMessageStartDistro);
955 StartWsl(
956 -1,
957 FormatErrorMessage(
958 L"A fatal error was returned by plugin 'TestPlugin'",
959 L"Wsl/Service/CreateInstance/CreateVm/Plugin/TRUST_E_NOSIGNATURE"));
960 }
961 };