/*++ Copyright (c) Microsoft. All rights reserved. Module Name: PluginTests.cpp Abstract: This file contains test cases for the plugin API. --*/ #include "precomp.h" #include "Common.h" #include "registry.hpp" #include "PluginTests.h" #include "wslc.h" #include "WSLCContainerLauncher.h" #include "WSLCProcessLauncher.h" #include "wslc/e2e/WSLCE2EHelpers.h" using namespace wsl::windows::common::registry; using WSLCE2ETests::StartLocalRegistry; extern std::wstring g_testDistroPath; extern std::wstring g_testDataPath; class PluginTests { std::wstring logFile; bool m_initialized = false; std::wstring pluginDll; std::optional config; WSL_TEST_CLASS(PluginTests) TEST_CLASS_SETUP(TestClassSetup) { VERIFY_ARE_EQUAL(LxsstuInitialize(FALSE), TRUE); m_initialized = true; logFile = wil::GetCurrentDirectoryW() + L"\\plugin-logs.txt"; const auto currentDll = std::filesystem::path(wil::GetModuleFileNameW(wil::GetModuleInstanceHandle())); const auto pluginDllPath = currentDll.parent_path() / L"testplugin.dll"; if (!std::filesystem::exists(pluginDllPath)) { const std::wstring message = L"Plugin not found in: " + pluginDllPath.wstring(); VERIFY_FAIL(message.c_str()); return false; } pluginDll = pluginDllPath.wstring(); // Disable VM timeouts during the plugin tests config.emplace(LxssGenerateTestConfig({.vmIdleTimeout = -1})); return true; } TEST_CLASS_CLEANUP(TestClassCleanup) { auto key = OpenLxssMachineKey(KEY_ALL_ACCESS); DeleteKey(key.get(), L"Test"); key = OpenKey(key.get(), L"Plugins", KEY_SET_VALUE); DeleteValue(key.get(), L"TestPlugin"); RestartWslService(); std::wifstream file(logFile); const auto fileContent = std::wstring{std::istreambuf_iterator(file), {}}; LogInfo("Logfile: %ls", fileContent.c_str()); file.close(); StopWslService(); if (!DeleteFile(logFile.c_str())) { VERIFY_ARE_EQUAL(ERROR_FILE_NOT_FOUND, GetLastError()); } if (m_initialized) { LxsstuUninitialize(FALSE); } return true; } void ConfigurePlugin(PluginTestType testCase, LPCWSTR mountFolder = L"") const { StopWslService(); if (!DeleteFile(logFile.c_str())) { VERIFY_ARE_EQUAL(ERROR_FILE_NOT_FOUND, GetLastError()); } const auto testKey = OpenTestRegistryKey(KEY_SET_VALUE); WriteDword(testKey.get(), nullptr, c_testType, static_cast(testCase)); WriteString(testKey.get(), nullptr, c_logFile, logFile.c_str()); WriteString(testKey.get(), nullptr, c_mountFolder, mountFolder); const auto lxssKey = CreateKey(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Lxss\\Plugins", KEY_SET_VALUE, nullptr, 0); WriteString(lxssKey.get(), nullptr, L"TestPlugin", pluginDll.c_str()); RestartWslService(); } static void StartWsl(int expectedExitCode, const std::wstring& expectedOutput = {}) { auto [output, error] = LxsstuLaunchWslAndCaptureOutput(L"echo -n OK", expectedExitCode); if (expectedExitCode == 0) { VERIFY_ARE_EQUAL(output, L"OK"); } else { VERIFY_ARE_EQUAL(output, expectedOutput); } } void ValidateLogFile(LPCWSTR expected) const { StopWslService(); std::wifstream file(logFile); auto fileContent = std::wstring{std::istreambuf_iterator(file), {}}; LogInfo("Logfile: %ls", fileContent.c_str()); auto fileLines = wsl::shared::string::Split(fileContent, '\n'); auto expectedLines = wsl::shared::string::Split(expected, '\n'); for (size_t i = 0; i < std::max(fileLines.size(), expectedLines.size()); i++) { if (i >= fileLines.size()) { std::wstring message = L"Line is expected but not in log file: " + expectedLines[i]; VERIFY_FAIL(message.c_str()); } else if (i >= expectedLines.size()) { std::wstring message = L"Line is in file but not expected: " + fileLines[i]; VERIFY_FAIL(message.c_str()); } const auto& expected = expectedLines[i]; const auto& actual = fileLines[i]; if (!PathMatchSpec(actual.c_str(), expected.c_str())) { LogInfo("Plugin log: %ls", fileContent.c_str()); std::wstring message = L"Line (" + actual + L") didn't match pattern: " + expected; VERIFY_FAIL(message.c_str()); } } } WSL2_TEST_METHOD(Success) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=1 VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::Success); StartWsl(0); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(MountFolderAccess) { const auto testFolder = std::filesystem::current_path() / "deny-write"; std::filesystem::remove_all(testFolder); VERIFY_IS_TRUE(std::filesystem::create_directory(testFolder)); VERIFY_IS_TRUE(std::filesystem::create_directory(testFolder / "allowed")); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove_all(testFolder); }); const auto user = wil::get_token_information(); EXPLICIT_ACCESSW access{}; access.grfAccessPermissions = FILE_ADD_FILE; access.grfAccessMode = DENY_ACCESS; access.grfInheritance = NO_INHERITANCE; access.Trustee.TrusteeForm = TRUSTEE_IS_SID; access.Trustee.ptstrName = static_cast(user->User.Sid); PACL originalAcl = nullptr; wil::unique_hlocal originalDescriptor; THROW_IF_WIN32_ERROR(GetNamedSecurityInfoW( testFolder.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &originalAcl, nullptr, &originalDescriptor)); auto restoreAcl = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW( const_cast(testFolder.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, originalAcl, nullptr)); }); wsl::windows::common::security::unique_acl deniedAcl; THROW_IF_WIN32_ERROR(SetEntriesInAclW(1, &access, originalAcl, &deniedAcl)); THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW( const_cast(testFolder.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, deniedAcl.get(), nullptr)); const auto testFile = testFolder / L"plugin-test.txt"; wil::unique_hfile deniedFile{CreateFileW(testFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr)}; VERIFY_IS_TRUE(!deniedFile); VERIFY_ARE_EQUAL(ERROR_ACCESS_DENIED, GetLastError()); ConfigurePlugin(PluginTestType::MountFolderAccess, testFolder.c_str()); constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=25 VM created (settings->CustomConfigurationFlags=0) *Permission denied Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; StartWsl(0); VERIFY_IS_TRUE(std::filesystem::exists(testFolder / "allowed" / "plugin-allowed.txt")); VERIFY_IS_FALSE(std::filesystem::exists(testFile)); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(CustomKernelOverriddenByPolicy) { RegistryKeyChange policy( HKEY_LOCAL_MACHINE, wsl::windows::policies::c_registryKey, wsl::windows::policies::c_allowCustomKernelUserSetting, static_cast(0)); WslConfigChange config(LxssGenerateTestConfig({.kernel = L"kernel-that-doesn't-exist"})); constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=1 VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::Success); StartWsl(0); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(DuplicatedPlugin) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=1 VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::Success); // Register the same plugin dll twice. Validate that it's only called once. const auto key = CreateKey(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Lxss\\Plugins", KEY_SET_VALUE, nullptr, 0); WriteString(key.get(), nullptr, L"TestPlugin-duplicated", pluginDll.c_str()); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { DeleteValue(key.get(), L"TestPlugin-duplicated"); }); RestartWslService(); StartWsl(0); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(CustomKernel) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=1 VM created (settings->CustomConfigurationFlags=1) OnVmStarted: E_ACCESSDENIED VM Stopping)"; #ifdef WSL_KERNEL_PATH std::wstring kernelPath = WIDEN(WSL_KERNEL_PATH); #else auto kernelPath = wsl::windows::common::wslutil::GetMsiPackagePath().value_or(L""); VERIFY_IS_FALSE(kernelPath.empty()); kernelPath += L"\\tools\\kernel"; #endif WslConfigChange config(LxssGenerateTestConfig({.vmIdleTimeout = 1, .kernel = kernelPath})); ConfigurePlugin(PluginTestType::Success); StartWsl( -1, FormatErrorMessage( L"A fatal error was returned by plugin 'TestPlugin'", L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_ACCESSDENIED")); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(CustomKernelCommandLine) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=1 VM created (settings->CustomConfigurationFlags=2) Folder mounted (* -> /test-plugin) Process created Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; WslConfigChange config(LxssGenerateTestConfig({.vmIdleTimeout = 1, .kernelCommandLine = L"custom"})); ConfigurePlugin(PluginTestType::Success); StartWsl(0); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(DistroIdStaysTheSame) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=10 VM created (settings->CustomConfigurationFlags=0) Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping VM created (settings->CustomConfigurationFlags=0) Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 OnDistroStarted: received same GUID Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::SameDistroId); StartWsl(0); WslShutdown(); StartWsl(0); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(InitPidIsDifferent) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=14 VM created (settings->CustomConfigurationFlags=0) Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=* Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Init's pid is different (* ! = *) Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::InitPidIsDifferent); StartWsl(0); TerminateDistribution(); StartWsl(0); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(PluginUpdateRequired) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=9 OnLoad: WSL_E_PLUGINREQUIRESUPDATE)"; ConfigurePlugin(PluginTestType::PluginRequiresUpdate); StartWsl( -1, FormatErrorMessage( L"The plugin 'TestPlugin' requires a newer version of WSL. Please run: wsl.exe --update", L"Wsl/Service/CreateInstance/CreateVm/Plugin/WSL_E_PLUGIN_REQUIRES_UPDATE")); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(APIErrors) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=7 VM created (settings->CustomConfigurationFlags=0) API error tests passed Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::ApiErrors); StartWsl(0); ValidateLogFile(ExpectedOutput); } WSL1_TEST_METHOD(SuccessWSL1) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=1)"; ConfigurePlugin(PluginTestType::Success); StartWsl(0); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(LoadFailureFatalWSL2) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=2 OnLoad: E_UNEXPECTED)"; ConfigurePlugin(PluginTestType::FailToLoad); StartWsl( -1, FormatErrorMessage( L"A fatal error was returned by plugin 'TestPlugin'", L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_UNEXPECTED")); ValidateLogFile(ExpectedOutput); } WSL1_TEST_METHOD(LoadFailureNonFatalWSL1) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=2 OnLoad: E_UNEXPECTED)"; ConfigurePlugin(PluginTestType::FailToLoad); StartWsl(0); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(VmStartFailure) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=3 VM created (settings->CustomConfigurationFlags=0) OnVmStarted: E_UNEXPECTED VM Stopping)"; ConfigurePlugin(PluginTestType::FailToStartVm); StartWsl( -1, FormatErrorMessage( L"A fatal error was returned by plugin 'TestPlugin'", L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_UNEXPECTED")); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(VmStartFailureWithPluginErrorTwice) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=13 VM created (settings->CustomConfigurationFlags=0) OnVmStarted: E_UNEXPECTED VM Stopping VM created (settings->CustomConfigurationFlags=0) OnVmStarted: E_UNEXPECTED VM Stopping)"; ConfigurePlugin(PluginTestType::FailToStartVmWithPluginErrorMessage); StartWsl( -1, FormatErrorMessage( L"A fatal error was returned by plugin 'TestPlugin'. Error message: 'Plugin error message'", L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_UNEXPECTED")); StartWsl( -1, FormatErrorMessage( L"A fatal error was returned by plugin 'TestPlugin'. Error message: 'Plugin error message'", L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_UNEXPECTED")); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(VmStopFailure) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=5 VM created (settings->CustomConfigurationFlags=0) Distribution started, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping OnVmStopping: E_UNEXPECTED)"; ConfigurePlugin(PluginTestType::FailToStopVm); StartWsl(0); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(DistributionStartFailure) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=4 VM created (settings->CustomConfigurationFlags=0) Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 OnDistroStarted: E_UNEXPECTED VM Stopping)"; ConfigurePlugin(PluginTestType::FailToStartDistro); StartWsl( -1, FormatErrorMessage( L"A fatal error was returned by plugin 'TestPlugin'", L"Wsl/Service/CreateInstance/Plugin/E_UNEXPECTED")); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(DistributionStopFailure) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=6 VM created (settings->CustomConfigurationFlags=0) Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 OnDistroStopping: E_UNEXPECTED VM Stopping)"; ConfigurePlugin(PluginTestType::FailToStopDistro); StartWsl(0); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(ErrorMessageStartVm) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=11 VM created (settings->CustomConfigurationFlags=0) OnVmStarted: E_FAIL VM Stopping)"; ConfigurePlugin(PluginTestType::ErrorMessageStartVm); StartWsl( -1, FormatErrorMessage( L"A fatal error was returned by plugin 'TestPlugin'. Error message: 'StartVm plugin error message'", L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_FAIL")); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(ErrorMessageStartDistro) { constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=12 VM created (settings->CustomConfigurationFlags=0) Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 OnDistroStarted: E_FAIL VM Stopping)"; ConfigurePlugin(PluginTestType::ErrorMessageStartDistro); StartWsl( -1, FormatErrorMessage( L"A fatal error was returned by plugin 'TestPlugin'. Error message: 'StartDistro plugin error message'", L"Wsl/Service/CreateInstance/Plugin/E_FAIL")); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(RegisterSuccess) { ConfigurePlugin(PluginTestType::Success); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--import plugin-test-distro . \"" + g_testDistroPath + L"\" --version 2"), 0L); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister plugin-test-distro"), 0L); constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=1 VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13 Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13 VM Stopping)"; ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(ImportInplaceSuccess) { ConfigurePlugin(PluginTestType::Success); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--import plugin-test-distro . \"" + g_testDistroPath + L"\" --version 2"), 0L); WslShutdown(); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--export plugin-test-distro plugin-test-distro.vhdx --format vhd"), 0L); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister plugin-test-distro"), 0L); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--import-in-place plugin-test-distro-vhd plugin-test-distro.vhdx"), 0L); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister plugin-test-distro-vhd"), 0L); constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=1 VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13 VM Stopping Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13 VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created Distribution registered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=13 Distribution unregistered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=13 VM Stopping)"; ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(RegisterUnregisterFail) { ConfigurePlugin(PluginTestType::FailToRegisterUnregisterDistro); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--import plugin-test-distro . \"" + g_testDistroPath + L"\" --version 2"), 0L); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister plugin-test-distro"), 0L); constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=15 VM created (settings->CustomConfigurationFlags=0) Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13 OnDistributionRegistered: E_UNEXPECTED Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13 OnDistributionUnregistered: E_UNEXPECTED VM Stopping)"; ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(ExecuteDistroCommand) { ConfigurePlugin(PluginTestType::RunDistroCommand); constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=16 VM created (settings->CustomConfigurationFlags=0) Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Process created Failed process launch returned: -2147467259 Invalid distro launch returned: -2147220717 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; StartWsl(0); ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(PluginToken) { ConfigurePlugin(PluginTestType::GetUsername); constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=17 VM created (settings->CustomConfigurationFlags=0) Username: * Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; StartWsl(0); ValidateLogFile(ExpectedOutput); } static wil::com_ptr OpenWslcSessionManager() { wil::com_ptr sessionManager; VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager))); wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get()); return sessionManager; } static wil::com_ptr CreateWslcSession(LPCWSTR Name, WSLCNetworkingMode NetworkingMode = WSLCNetworkingModeNone, LPCWSTR StoragePath = nullptr) { WSLCSessionSettings settings{}; settings.DisplayName = Name; settings.CpuCount = 4; settings.MemoryMb = 4096; settings.BootTimeoutMs = 30 * 1000; settings.NetworkingMode = NetworkingMode; settings.StoragePath = StoragePath; settings.MaximumStorageSizeMb = 1024 * 20; // 20GB, only used when StoragePath is set. auto manager = OpenWslcSessionManager(); wil::com_ptr session; VERIFY_SUCCEEDED(manager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); WSLCSessionState state{}; VERIFY_SUCCEEDED(session->GetState(&state)); VERIFY_ARE_EQUAL(state, WSLCSessionStateRunning); return session; } WSL2_TEST_METHOD(WslcSuccess) { ConfigurePlugin(PluginTestType::WslcSuccess); { auto session = CreateWslcSession(L"plugin-wslc-test"); LoadTestImage(*session, "debian:latest"); // Create a container that will have a stuck process so it's still in a running state when the callback is made. wsl::windows::common::WSLCContainerLauncher launcher( "debian:latest", "wslc-plugin-container", {"/bin/sh", "-c", "sleep 120"}); auto container = launcher.Launch(*session, WSLCContainerStartFlagsAttach); VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0)); // Delete the image so we get an ImageDeleted notification before the session goes away. WSLCDeleteImageOptions options{.Image = "debian:latest", .Flags = WSLCDeleteImageFlagsForce}; wil::unique_cotaskmem_array_ptr deletedImages; VERIFY_SUCCEEDED(session->DeleteImage(&options, deletedImages.addressof(), deletedImages.size_address())); } const auto ExpectedOutput = std::format( LR"(Plugin loaded. TestMode=18 WSLC Session created, name=plugin-wslc-test, id=*, pid=*, token=set, sid=set Command: 'echo -n stdout-ok && echo -n stderr-ok >&2', status=0, stdout: stdout-ok, stderr: stderr-ok Command: 'cat', status=0, stdout: stdin-ok, stderr: Command: 'exit 12', status=12, stdout: , stderr: Command: 'echo -n $ENV', status=0, stdout: env-ok, stderr: WSLCCreateProcess(does-not-exist): {:x}, errno=2 WSLCProcessGetFd(999): {} WSLCProcessGetExitCode(): {} WSLC RW folder mounted at: /mnt/wsl-plugin/plugin-rw-test Command: 'cat /mnt/wsl-plugin/plugin-rw-test/plugin-test.txt', status=0, stdout: Windows-content, stderr: Command: 'cat /mnt/wsl-plugin/plugin-rw-test/plugin-denied.txt', status=1, stdout: , stderr: * WSLC RO folder mounted at: /mnt/wsl-plugin/plugin-ro-test Command: 'echo fail > /mnt/wsl-plugin/plugin-ro-test/should-not-exist.txt', status=1, stdout: , stderr: * WSLCMountFolder(nonexistent): {} WSLCMountFolder(relative): {} Test completed WSLC Image created, session=*, id=sha256:*, name=debian:latest WSLC Container started, session=*, id=*, name=/wslc-plugin-container, image=debian:latest, state=* WSLC Container stopping, session=*, id=* WSLC Image deleted, session=*, id=* WSLC Session stopping, name=plugin-wslc-test, id=*)", static_cast(E_FAIL), E_INVALIDARG, HRESULT_FROM_WIN32(ERROR_INVALID_STATE), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), E_INVALIDARG); ValidateLogFile(ExpectedOutput.c_str()); } WSL2_TEST_METHOD(WslcPullImageNotification) { ConfigurePlugin(PluginTestType::WslcImagePull); { auto session = CreateWslcSession(L"plugin-wslc-pull-test", WSLCNetworkingModeConsomme); // Load the registry and debian images. LoadTestImage(*session, "debian:latest"); // Start a local registry container. auto [registryContainer, registryAddress] = StartLocalRegistry(*session); // Tag debian:latest for the local registry and push it. auto registryImage = std::format("{}/debian:latest", registryAddress); auto registryRepo = std::format("{}/debian", registryAddress); WSLCTagImageOptions tagOptions{}; tagOptions.Image = "debian:latest"; tagOptions.Repo = registryRepo.c_str(); tagOptions.Tag = "latest"; VERIFY_SUCCEEDED(session->TagImage(&tagOptions)); auto emptyAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", ""); VERIFY_SUCCEEDED(session->PushImage(registryImage.c_str(), emptyAuth.c_str(), FALSE, nullptr, nullptr)); // Delete the local tagged copy so PullImage actually downloads it. WSLCDeleteImageOptions deleteOpts{.Image = registryImage.c_str(), .Flags = WSLCDeleteImageFlagsNone}; wil::unique_cotaskmem_array_ptr deletedImages; VERIFY_SUCCEEDED(session->DeleteImage(&deleteOpts, deletedImages.addressof(), deletedImages.size_address())); // Pull the image back — this should trigger the ImageCreated plugin callback. VERIFY_SUCCEEDED(session->PullImage(registryImage.c_str(), nullptr, FALSE, nullptr, nullptr)); // Publish two distinct images into one repository, one of them under two tags. An --all-tags // pull reports a digest per tag, so this covers both that every image the pull created is // notified and that two tags resolving to one image are notified once. const auto versionedRepo = std::format("{}/debian-versioned", registryAddress); auto publish = [&](LPCSTR image, LPCSTR tag) { const auto reference = std::format("{}:{}", versionedRepo, tag); tagOptions.Image = image; tagOptions.Repo = versionedRepo.c_str(); tagOptions.Tag = tag; VERIFY_SUCCEEDED(session->TagImage(&tagOptions)); VERIFY_SUCCEEDED(session->PushImage(reference.c_str(), emptyAuth.c_str(), FALSE, nullptr, nullptr)); // Drop the local tag so the pull is the only thing that can report it. WSLCDeleteImageOptions deleteOptions{.Image = reference.c_str(), .Flags = WSLCDeleteImageFlagsNone}; wil::unique_cotaskmem_array_ptr deleted; VERIFY_SUCCEEDED(session->DeleteImage(&deleteOptions, deleted.addressof(), deleted.size_address())); }; publish("debian:latest", "v1"); publish("debian:latest", "v2"); publish("wslc-registry:latest", "v3"); VERIFY_SUCCEEDED(session->PullImage(versionedRepo.c_str(), nullptr, TRUE, nullptr, nullptr)); } constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=21 WSLC Session created, name=plugin-wslc-pull-test, id=*, pid=*, token=set, sid=set WSLC Image created, session=*, id=sha256:*, name=debian:latest WSLC Image created, session=*, id=sha256:*, name=wslc-registry:latest WSLC Container started, session=*, id=*, name=*, image=wslc-registry:latest, state=running WSLC Image created, session=*, id=sha256:*, name=127.0.0.1:5000/debian:latest WSLC Image created, session=*, id=sha256:*, name=* WSLC Image created, session=*, id=sha256:*, name=* WSLC Container stopping, session=*, id=* WSLC Session stopping, name=plugin-wslc-pull-test, id=*)"; ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(WslcSessionRejected) { ConfigurePlugin(PluginTestType::WslcSessionRejected); WSLCSessionSettings settings{}; settings.DisplayName = L"plugin-wslc-rejected"; settings.CpuCount = 4; settings.MemoryMb = 2048; settings.BootTimeoutMs = 30 * 1000; settings.MaximumStorageSizeMb = 1024 * 20; settings.NetworkingMode = WSLCNetworkingModeNone; auto manager = OpenWslcSessionManager(); wil::com_ptr session; const auto hr = manager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session); ValidateCOMErrorMessageContains(L"A fatal error was returned by plugin 'TestPlugin'"); VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED)); constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=19 WSLC Session created, name=plugin-wslc-rejected, id=*, pid=*, token=set, sid=set OnWslcSessionCreated: ERROR_ACCESS_DENIED)"; ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(WslcContainerRejected) { ConfigurePlugin(PluginTestType::WslcContainerRejected); { auto session = CreateWslcSession(L"plugin-wslc-container-rejected"); LoadTestImage(*session, "debian:latest"); wsl::windows::common::WSLCContainerLauncher launcher( "debian:latest", "wslc-plugin-rejected-container", {"/bin/sh", "-c", "echo nope"}); auto [hr, container] = launcher.LaunchNoThrow(*session, WSLCContainerStartFlagsAttach); ValidateCOMErrorMessageContains(L"A fatal error was returned by plugin 'TestPlugin'"); VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED)); } constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=20 WSLC Session created, name=plugin-wslc-container-rejected, id=*, pid=*, token=set, sid=set WSLC Image created, session=*, id=sha256:*, name=debian:latest WSLC Container started, session=*, id=*, name=*, image=debian:latest, state=* OnWslcContainerStarted: ERROR_ACCESS_DENIED WSLC Session stopping, name=plugin-wslc-container-rejected, id=*)"; ValidateLogFile(ExpectedOutput); } // Validates the VM-lifecycle hooks: OnWslcVmStarted fires each time the VM is (re)created and // OnWslcVmStopping each time it is torn down, decoupled from the once-per-session hooks. Also // proves the started hook can call back into the session (WSLCCreateProcess) without deadlocking. WSL2_TEST_METHOD(WslcVmRestart) { ConfigurePlugin(PluginTestType::WslcVmRestart); // Idle termination only tears down storage-backed sessions (tmpfs state is unrecoverable), so // this restart lifecycle test needs a dedicated persistent storage directory. const auto storageDir = std::filesystem::current_path() / "test-storage-wslc-vm-restart"; std::error_code storageError; std::filesystem::remove_all(storageDir, storageError); std::filesystem::create_directories(storageDir); auto storageCleanup = wil::scope_exit([&]() { std::error_code ec; std::filesystem::remove_all(storageDir, ec); }); { auto session = CreateWslcSession(L"plugin-wslc-vm-restart", WSLCNetworkingModeNone, storageDir.c_str()); // First operation brings the VM up -> OnWslcVmStarted (which reentrantly runs a process). { wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); auto process = launcher.Launch(*session); } // Force idle teardown of the running VM -> OnWslcVmStopping. BOOL wasAlreadyIdle = TRUE; VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); VERIFY_IS_FALSE(wasAlreadyIdle); // Next operation lazily restarts the VM -> OnWslcVmStarted fires again. { wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); auto process = launcher.Launch(*session); } // Session teardown tears the second VM down -> OnWslcVmStopping, then OnWslcSessionStopping. } constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=22 WSLC Session created, name=plugin-wslc-vm-restart, id=*, pid=*, token=set, sid=set WSLC VM started, session=* WSLC VM started reentrant WSLCCreateProcess: ok WSLC VM started mount+unmount: ok WSLC VM stopping, session=* WSLC VM stopping reentrant WSLCCreateProcess: ok WSLC VM stopping mount+unmount: ok WSLC VM started, session=* WSLC VM started reentrant WSLCCreateProcess: ok WSLC VM started mount+unmount: ok WSLC VM stopping, session=* WSLC VM stopping reentrant WSLCCreateProcess: failed WSLC VM stopping mount+unmount: skipped WSLC Session stopping, name=plugin-wslc-vm-restart, id=*)"; ValidateLogFile(ExpectedOutput); } // Validates that an announced VM teardown always happens. The plugin leaves a process running when // OnWslcVmStopping returns -- which under the previous design made the runtime abandon the // teardown -- and starts a call from a thread of its own during the notification window. The VM // must stop anyway, the leaked process must die with it, and the windowed call must be served by // the stopping VM rather than blocking on the teardown it cannot influence. WSL2_TEST_METHOD(WslcVmStopCommitted) { ConfigurePlugin(PluginTestType::WslcVmStopCommitted); // Idle termination only tears down storage-backed sessions (see WslcVmRestart). const auto storageDir = std::filesystem::current_path() / "test-storage-wslc-vm-stop-committed"; std::error_code storageError; std::filesystem::remove_all(storageDir, storageError); std::filesystem::create_directories(storageDir); auto storageCleanup = wil::scope_exit([&]() { std::error_code ec; std::filesystem::remove_all(storageDir, ec); }); { auto session = CreateWslcSession(L"plugin-wslc-vm-stop-committed", WSLCNetworkingModeNone, storageDir.c_str()); // Bring the VM up -> OnWslcVmStarted. { wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); auto process = launcher.Launch(*session); } // The teardown is announced and then carried out, even though the plugin's callback left a // process running. The session is genuinely idle afterwards. BOOL wasAlreadyIdle = TRUE; VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); VERIFY_IS_FALSE(wasAlreadyIdle); // The VM is gone, so this starts a fresh one -> a second OnWslcVmStarted. { wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); auto process = launcher.Launch(*session); } } // "leaked process died: yes" is the assertion that the announced stop actually happened: the // process the callback left running was killed by the teardown rather than keeping the VM // alive. "stop-window caller: ok" shows a plugin call from another thread was served by the // stopping VM instead of deadlocking against the teardown that callback was holding up. Both // are reported when that thread is joined, at session teardown. constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=23 WSLC Session created, name=plugin-wslc-vm-stop-committed, id=*, pid=*, token=set, sid=set WSLC VM started, session=* WSLC VM stopping, session=* WSLC VM stopping leaked process: ok WSLC VM started, session=* WSLC stop-window caller: ok WSLC leaked process died: yes WSLC Session stopping, name=plugin-wslc-vm-stop-committed, id=*)"; ValidateLogFile(ExpectedOutput); } WSL2_TEST_METHOD(WslcVmNeverStarted) { ConfigurePlugin(PluginTestType::WslcVmNeverStarted); // A session whose VM is never needed. VM bring-up is lazy, so creating and destroying the // session must not produce either VM notification: OnWslcVmStopping is documented to fire // exactly once per OnWslcVmStarted, and a stop for a VM that never existed would break the // pairing every plugin relies on to track VM lifetime. The plugin also issues a call from // OnWslcSessionCreated, which must be rejected rather than bring a VM up. { auto session = CreateWslcSession(L"plugin-wslc-vm-never-started"); VERIFY_IS_NOT_NULL(session.get()); } constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=24 WSLC Session created, name=plugin-wslc-vm-never-started, id=*, pid=*, token=set, sid=set WSLC no-vm caller: rejected WSLC Session stopping, name=plugin-wslc-vm-never-started, id=*)"; ValidateLogFile(ExpectedOutput); } // This test must run last so it doesn't break test cases that depends on plugin signature. WSL2_TEST_METHOD(InvalidPluginSignature) { if constexpr (!wsl::shared::OfficialBuild) { LogSkipped("This test only applies to signed builds"); return; } StopWslService(); // Append one byte at the end of the plugin dll to break its signature wil::unique_handle plugin{CreateFile(pluginDll.c_str(), FILE_APPEND_DATA, 0, nullptr, OPEN_EXISTING, 0, nullptr)}; VERIFY_IS_TRUE(plugin.is_valid()); char c{}; VERIFY_IS_TRUE(WriteFile(plugin.get(), &c, sizeof(c), nullptr, nullptr)); plugin.reset(); ConfigurePlugin(PluginTestType::ErrorMessageStartDistro); StartWsl( -1, FormatErrorMessage( L"A fatal error was returned by plugin 'TestPlugin'", L"Wsl/Service/CreateInstance/CreateVm/Plugin/TRUST_E_NOSIGNATURE")); } };