/*++ Copyright (c) Microsoft. All rights reserved. Module Name: LSWTests.cpp Abstract: This file contains test cases for the LSW API. --*/ #include "precomp.h" #include "Common.h" #include "LSWApi.h" using namespace wsl::windows::common::registry; using unique_vm = wil::unique_any; class LSWTests { WSL_TEST_CLASS(LSWTests) wil::unique_couninitialize_call coinit = wil::CoInitializeEx(); WSADATA Data; std::filesystem::path testVhd; TEST_CLASS_SETUP(TestClassSetup) { THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &Data)); auto distroKey = OpenDistributionKey(LXSS_DISTRO_NAME_TEST_L); auto vhdPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath"); testVhd = std::filesystem::path{vhdPath} / "ext4.vhdx"; WslShutdown(); return true; } TEST_CLASS_CLEANUP(TestClassCleanup) { return true; } TEST_METHOD(GetVersion) { auto coinit = wil::CoInitializeEx(); WSL_VERSION_INFORMATION version{}; VERIFY_SUCCEEDED(WslGetVersion(&version)); VERIFY_ARE_EQUAL(version.Major, WSL_PACKAGE_VERSION_MAJOR); VERIFY_ARE_EQUAL(version.Minor, WSL_PACKAGE_VERSION_MINOR); VERIFY_ARE_EQUAL(version.Revision, WSL_PACKAGE_VERSION_REVISION); } std::tuple LaunchCommand( LSWVirtualMachineHandle vm, const std::vector& command) { auto copiedCommand = command; if (copiedCommand.back() != nullptr) { copiedCommand.push_back(nullptr); } std::vector fds(3); fds[0].Number = 0; fds[1].Number = 1; fds[2].Number = 2; CreateProcessSettings createProcessSettings{}; createProcessSettings.Executable = copiedCommand[0]; createProcessSettings.Arguments = copiedCommand.data(); createProcessSettings.FileDescriptors = fds.data(); createProcessSettings.FdCount = 3; int pid = -1; VERIFY_SUCCEEDED(WslCreateLinuxProcess(vm, &createProcessSettings, &pid)); return std::make_tuple( pid, wil::unique_handle{fds[0].Handle}, wil::unique_handle(fds[1].Handle), wil::unique_handle{fds[2].Handle}); } int RunCommand(LSWVirtualMachineHandle vm, const std::vector& command, int timeout = 600000) { auto [pid, _, __, ___] = LaunchCommand(vm, command); WaitResult result{}; VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm, pid, timeout, &result)); VERIFY_ARE_EQUAL(result.State, ProcessStateExited); return result.Code; } unique_vm CreateVm(const VirtualMachineSettings* settings) { unique_vm vm{}; VERIFY_SUCCEEDED(WslCreateVirtualMachine(settings, &vm)); DiskAttachSettings attachSettings{testVhd.c_str(), true}; AttachedDiskInformation attachedDisk; VERIFY_SUCCEEDED(WslAttachDisk(vm.get(), &attachSettings, &attachedDisk)); MountSettings mountSettings{attachedDisk.Device, "/mnt", "ext4", "ro", MountFlagsChroot | MountFlagsWriteableOverlayFs}; VERIFY_SUCCEEDED(WslMount(vm.get(), &mountSettings)); MountSettings devmountSettings{nullptr, "/dev", "devtmpfs", "", false}; VERIFY_SUCCEEDED(WslMount(vm.get(), &devmountSettings)); MountSettings sysmountSettings{nullptr, "/sys", "sysfs", "", false}; VERIFY_SUCCEEDED(WslMount(vm.get(), &sysmountSettings)); MountSettings procmountSettings{nullptr, "/proc", "proc", "", false}; VERIFY_SUCCEEDED(WslMount(vm.get(), &procmountSettings)); MountSettings ptsMountSettings{nullptr, "/dev/pts", "devpts", "noatime,nosuid,noexec,gid=5,mode=620", false}; VERIFY_SUCCEEDED(WslMount(vm.get(), &ptsMountSettings)); return vm; } TEST_METHOD(AttachDetach) { WSL2_TEST_ONLY(); VirtualMachineSettings settings{}; settings.CPU.CpuCount = 4; settings.DisplayName = L"LSW"; settings.Memory.MemoryMb = 1024; settings.Options.BootTimeoutMs = 30000; auto vm = CreateVm(&settings); #ifdef WSL_DEV_INSTALL_PATH auto vhdPath = std::filesystem::path(WSL_DEV_INSTALL_PATH) / "system.vhd"; #else auto msiPath = wsl::windows::common::wslutil::GetMsiPackagePath(); VERIFY_IS_TRUE(msiPath.has_value()); auto vhdPath = std::filesystem::path(msiPath.value()) / "system.vhd"; #endif auto blockDeviceExists = [&](ULONG Lun) { std::string device = std::format("/sys/bus/scsi/devices/0:0:0:{}", Lun); std::vector cmd{"/usr/bin/test", "-d", device.c_str()}; return RunCommand(vm.get(), cmd) == 0; }; // Attach the disk. DiskAttachSettings attachSettings{vhdPath.c_str(), true}; AttachedDiskInformation attachedDisk{}; VERIFY_SUCCEEDED(WslAttachDisk(vm.get(), &attachSettings, &attachedDisk)); VERIFY_IS_TRUE(blockDeviceExists(attachedDisk.ScsiLun)); // Mount it to /mnt. MountSettings mountSettings{attachedDisk.Device, "/mnt", "ext4", "ro"}; VERIFY_SUCCEEDED(WslMount(vm.get(), &mountSettings)); // Validate that the mountpoint is present. std::vector cmd{"/usr/bin/mountpoint", "/mnt"}; VERIFY_ARE_EQUAL(RunCommand(vm.get(), cmd), 0L); // Unmount /mnt. VERIFY_SUCCEEDED(WslUnmount(vm.get(), "/mnt")); VERIFY_ARE_EQUAL(RunCommand(vm.get(), cmd), 32L); // Verify that unmount fails now. VERIFY_ARE_EQUAL(WslUnmount(vm.get(), "/mnt"), E_FAIL); // Detach the disk VERIFY_SUCCEEDED(WslDetachDisk(vm.get(), attachedDisk.ScsiLun)); VERIFY_IS_FALSE(blockDeviceExists(attachedDisk.ScsiLun)); // Verify that disk can't be detached twice VERIFY_ARE_EQUAL(WslDetachDisk(vm.get(), attachedDisk.ScsiLun), HRESULT_FROM_WIN32(ERROR_NOT_FOUND)); } TEST_METHOD(CustomDmesgOutput) { WSL2_TEST_ONLY(); auto createVmWithDmesg = [this](bool earlyBootLogging) { auto [read, write] = CreateSubprocessPipe(false, false); VirtualMachineSettings settings{}; settings.CPU.CpuCount = 4; settings.DisplayName = L"LSW"; settings.Memory.MemoryMb = 1024; settings.Options.BootTimeoutMs = 30000; settings.Options.Dmesg = write.get(); settings.Options.EnableEarlyBootDmesg = earlyBootLogging; std::vector dmesgContent; auto readDmesg = [read = read.get(), &dmesgContent]() mutable { DWORD Offset = 0; constexpr auto bufferSize = 1024; while (true) { dmesgContent.resize(Offset + bufferSize); DWORD Read{}; if (!ReadFile(read, &dmesgContent[Offset], bufferSize, &Read, nullptr)) { LogInfo("ReadFile() failed: %lu", GetLastError()); } if (Read == 0) { break; } Offset += Read; } }; std::thread thread(readDmesg); auto vm = CreateVm(&settings); auto detach = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { vm.reset(); if (thread.joinable()) { thread.join(); } }); write.reset(); std::vector cmd = {"/bin/bash", "-c", "echo DmesgTest > /dev/kmsg"}; VERIFY_ARE_EQUAL(RunCommand(vm.get(), cmd), 0); VERIFY_ARE_EQUAL(WslShutdownVirtualMachine(vm.get(), 30 * 1000), S_OK); detach.reset(); auto contentString = std::string(dmesgContent.begin(), dmesgContent.end()); VERIFY_ARE_NOT_EQUAL(contentString.find("Run /init as init process"), std::string::npos); VERIFY_ARE_NOT_EQUAL(contentString.find("DmesgTest"), std::string::npos); return contentString; }; auto validateFirstDmesgLine = [](const std::string& dmesg, const char* expected) { auto firstLf = dmesg.find("\n"); VERIFY_ARE_NOT_EQUAL(firstLf, std::string::npos); VERIFY_IS_TRUE(dmesg.find(expected) < firstLf); }; // Dmesg without early boot logging { auto dmesg = createVmWithDmesg(false); // Verify that the first line is "brd: module loaded"; validateFirstDmesgLine(dmesg, "brd: module loaded"); } // Dmesg with early boot logging { auto dmesg = createVmWithDmesg(true); validateFirstDmesgLine(dmesg, "Linux version"); } } TEST_METHOD(TerminationCallback) { WSL2_TEST_ONLY(); std::promise> callbackInfo; auto callback = [](void* context, VirtualMachineTerminationReason reason, LPCWSTR details) -> HRESULT { auto* future = reinterpret_cast>*>(context); future->set_value(std::make_pair(reason, details)); return S_OK; }; VirtualMachineSettings settings{}; settings.CPU.CpuCount = 4; settings.DisplayName = L"LSW"; settings.Memory.MemoryMb = 1024; settings.Options.BootTimeoutMs = 30000; settings.Options.TerminationCallback = callback; settings.Options.TerminationContext = &callbackInfo; auto vm = CreateVm(&settings); VERIFY_SUCCEEDED(WslShutdownVirtualMachine(vm.get(), 30 * 1000)); auto future = callbackInfo.get_future(); auto result = future.wait_for(std::chrono::seconds(10)); auto [reason, details] = future.get(); VERIFY_ARE_EQUAL(reason, VirtualMachineTerminationReasonShutdown); VERIFY_ARE_NOT_EQUAL(details, L""); } TEST_METHOD(CreateVmSmokeTest) { WSL2_TEST_ONLY(); VirtualMachineSettings settings{}; settings.CPU.CpuCount = 4; settings.DisplayName = L"LSW"; settings.Memory.MemoryMb = 1024; settings.Options.BootTimeoutMs = 30000; auto vm = CreateVm(&settings); // Create a process and wait for it to exit { std::vector commandLine{"/bin/sh", "-c", "echo $bar", nullptr}; std::vector fds(3); fds[0].Number = 0; fds[1].Number = 1; fds[2].Number = 2; std::vector env{"bar=foo", nullptr}; CreateProcessSettings createProcessSettings{}; createProcessSettings.Executable = "/bin/sh"; createProcessSettings.Arguments = commandLine.data(); createProcessSettings.FileDescriptors = fds.data(); createProcessSettings.Environment = env.data(); createProcessSettings.FdCount = 3; int pid = -1; VERIFY_SUCCEEDED(WslCreateLinuxProcess(vm.get(), &createProcessSettings, &pid)); LogInfo("pid: %lu", pid); std::vector buffer(100); DWORD bytes{}; if (!ReadFile(createProcessSettings.FileDescriptors[1].Handle, buffer.data(), (DWORD)buffer.size(), &bytes, nullptr)) { LogError("ReadFile: %lu, handle: 0x%x", GetLastError(), createProcessSettings.FileDescriptors[1].Handle); VERIFY_FAIL(); } VERIFY_ARE_EQUAL(buffer.data(), std::string("foo\n")); WaitResult result{}; VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm.get(), pid, 1000, &result)); VERIFY_ARE_EQUAL(result.State, ProcessStateExited); VERIFY_ARE_EQUAL(result.Code, 0); } // Create a 'stuck' process and kill it { std::vector commandLine{"/usr/bin/sleep", "100000", nullptr}; std::vector fds(3); fds[0].Number = 0; fds[1].Number = 1; fds[2].Number = 2; CreateProcessSettings createProcessSettings{}; createProcessSettings.Executable = commandLine[0]; createProcessSettings.Arguments = commandLine.data(); createProcessSettings.FileDescriptors = fds.data(); createProcessSettings.Environment = nullptr; createProcessSettings.FdCount = 3; int pid = -1; VERIFY_SUCCEEDED(WslCreateLinuxProcess(vm.get(), &createProcessSettings, &pid)); // Verify that the process is in a running state WaitResult result{}; VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm.get(), pid, 1000, &result)); VERIFY_ARE_EQUAL(result.State, ProcessStateRunning); // Verify that the process can still be waited for result = {}; VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm.get(), pid, 1000, &result)); VERIFY_ARE_EQUAL(result.State, ProcessStateRunning); result = {}; VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm.get(), pid, 0, &result)); VERIFY_ARE_EQUAL(result.State, ProcessStateRunning); // Verify that it can be killed. VERIFY_SUCCEEDED(WslSignalLinuxProcess(vm.get(), pid, 9)); // Verify that the process is in a running state VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm.get(), pid, 1000, &result)); VERIFY_ARE_EQUAL(result.State, ProcessStateSignaled); VERIFY_ARE_EQUAL(result.Code, 9); } // Test various error paths { std::vector commandLine{"dummy", "100000", nullptr}; std::vector fds(3); fds[0].Number = 0; fds[1].Number = 1; fds[2].Number = 2; CreateProcessSettings createProcessSettings{}; createProcessSettings.Executable = commandLine[0]; createProcessSettings.Arguments = commandLine.data(); createProcessSettings.FileDescriptors = fds.data(); createProcessSettings.Environment = nullptr; createProcessSettings.FdCount = 3; int pid = -1; VERIFY_ARE_EQUAL(WslCreateLinuxProcess(vm.get(), &createProcessSettings, &pid), E_FAIL); WaitResult result{}; VERIFY_ARE_EQUAL(WslWaitForLinuxProcess(vm.get(), 1234, 1000, &result), E_FAIL); VERIFY_ARE_EQUAL(result.State, ProcessStateUnknown); } } TEST_METHOD(InteractiveShell) { WSL2_TEST_ONLY(); VirtualMachineSettings settings{}; settings.CPU.CpuCount = 4; settings.DisplayName = L"LSW"; settings.Memory.MemoryMb = 2048; settings.Options.BootTimeoutMs = 30 * 1000; settings.Options.EnableDebugShell = true; settings.Networking.Mode = NetworkingModeNone; auto vm = CreateVm(&settings); std::vector commandLine{"/bin/sh", nullptr}; std::vector fds(2); fds[0].Number = 0; fds[0].Type = TerminalInput; fds[1].Number = 1; fds[1].Type = TerminalOutput; CreateProcessSettings createProcessSettings{}; createProcessSettings.Executable = "/bin/sh"; createProcessSettings.Arguments = commandLine.data(); createProcessSettings.FileDescriptors = fds.data(); createProcessSettings.FdCount = static_cast(fds.size()); int pid = -1; VERIFY_SUCCEEDED(WslCreateLinuxProcess(vm.get(), &createProcessSettings, &pid)); auto validateTtyOutput = [&](const std::string& expected) { std::string buffer(expected.size(), '\0'); DWORD offset = 0; while (offset < buffer.size()) { DWORD bytesRead{}; VERIFY_IS_TRUE(ReadFile( createProcessSettings.FileDescriptors[1].Handle, buffer.data() + offset, static_cast(buffer.size() - offset), &bytesRead, nullptr)); offset += bytesRead; } buffer.resize(offset); VERIFY_ARE_EQUAL(buffer, expected); }; auto writeTty = [&](const std::string& content) { VERIFY_IS_TRUE(WriteFile( createProcessSettings.FileDescriptors[0].Handle, content.data(), static_cast(content.size()), nullptr, nullptr)); }; // Expect the shell prompt to be displayed validateTtyOutput("#"); writeTty("echo OK\n"); validateTtyOutput(" echo OK\r\nOK"); // Validate that the interactive process successfully starts wil::unique_handle process; VERIFY_SUCCEEDED(WslLaunchInteractiveTerminal( createProcessSettings.FileDescriptors[0].Handle, createProcessSettings.FileDescriptors[1].Handle, &process)); // Exit the shell writeTty("exit\n"); VERIFY_ARE_EQUAL(WaitForSingleObject(process.get(), 30 * 1000), WAIT_OBJECT_0); } TEST_METHOD(NATNetworking) { WSL2_TEST_ONLY(); VirtualMachineSettings settings{}; settings.CPU.CpuCount = 4; settings.DisplayName = L"LSW"; settings.Memory.MemoryMb = 2048; settings.Options.BootTimeoutMs = 30 * 1000; settings.Networking.Mode = NetworkingModeNAT; auto vm = CreateVm(&settings); // Validate that eth0 has an ip address VERIFY_ARE_EQUAL( RunCommand( vm.get(), {"/bin/bash", "-c", "ip a show dev eth0 | grep -iF 'inet ' | grep -E '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}'"}), 0); // Verify that /etc/resolv.conf is configured VERIFY_ARE_EQUAL(RunCommand(vm.get(), {"/bin/grep", "-iF", "nameserver", "/etc/resolv.conf"}), 0); } TEST_METHOD(NATPortMapping) { WSL2_TEST_ONLY(); VirtualMachineSettings settings{}; settings.CPU.CpuCount = 4; settings.DisplayName = L"LSW"; settings.Memory.MemoryMb = 2048; settings.Options.BootTimeoutMs = 30 * 1000; settings.Networking.Mode = NetworkingModeNAT; auto vm = CreateVm(&settings); auto waitForOutput = [](HANDLE Handle, const char* Content) { std::string output; DWORD index = 0; while (true) // TODO: timeout { constexpr auto bufferSize = 100; output.resize(output.size() + bufferSize); DWORD bytesRead = 0; if (!ReadFile(Handle, &output[index], bufferSize, &bytesRead, nullptr)) { LogError("ReadFile failed with %lu", GetLastError()); VERIFY_FAIL(); } output.resize(index + bytesRead); if (bytesRead == 0) { LogError("Process exited, output: %hs", output.c_str()); VERIFY_FAIL(); } index += bytesRead; if (output.find(Content) != std::string::npos) { break; } } }; auto listen = [&](short port, const char* content, bool ipv6) { auto cmd = std::format("echo -n '{}' | /usr/bin/socat -dd TCP{}-LISTEN:{},reuseaddr -", content, ipv6 ? "6" : "", port); auto [pid, in, out, err] = LaunchCommand(vm.get(), {"/bin/bash", "-c", cmd.c_str()}); waitForOutput(err.get(), "listening on"); return pid; }; auto connectAndRead = [&](short port, int family) -> std::string { SOCKADDR_INET addr{}; addr.si_family = family; INETADDR_SETLOOPBACK((PSOCKADDR)&addr); SS_PORT(&addr) = htons(port); wil::unique_socket hostSocket{socket(family, SOCK_STREAM, IPPROTO_TCP)}; THROW_LAST_ERROR_IF(!hostSocket); THROW_LAST_ERROR_IF(connect(hostSocket.get(), reinterpret_cast(&addr), sizeof(addr)) == SOCKET_ERROR); return ReadToString(hostSocket.get()); }; auto expectContent = [&](short port, int family, const char* expected) { auto content = connectAndRead(port, family); VERIFY_ARE_EQUAL(content, expected); }; auto expectNotBound = [&](short port, int family) { auto result = wil::ResultFromException([&]() { connectAndRead(port, family); }); VERIFY_ARE_EQUAL(result, HRESULT_FROM_WIN32(WSAECONNREFUSED)); }; // Map port PortMappingSettings port{1234, 80, AF_INET}; VERIFY_SUCCEEDED(WslMapPort(vm.get(), &port)); // Validate that the same port can't be bound twice VERIFY_ARE_EQUAL(WslMapPort(vm.get(), &port), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)); // Check simple case listen(80, "port80", false); expectContent(1234, AF_INET, "port80"); // Validate that same port mapping can be reused listen(80, "port80", false); expectContent(1234, AF_INET, "port80"); // Validate that the connection is immediately reset if the port is not bound on the linux side expectContent(1234, AF_INET, ""); // Add a ipv6 binding PortMappingSettings portv6{1234, 80, AF_INET6}; VERIFY_SUCCEEDED(WslMapPort(vm.get(), &portv6)); // Validate that ipv6 bindings work as well. listen(80, "port80ipv6", true); expectContent(1234, AF_INET6, "port80ipv6"); // Unmap the ipv4 port VERIFY_SUCCEEDED(WslUnmapPort(vm.get(), &port)); expectNotBound(1234, AF_INET); // Verify that a proper error is returned if the mapping doesn't exist VERIFY_ARE_EQUAL(WslUnmapPort(vm.get(), &port), HRESULT_FROM_WIN32(ERROR_NOT_FOUND)); // Unmap the v6 port VERIFY_SUCCEEDED(WslUnmapPort(vm.get(), &portv6)); expectNotBound(1234, AF_INET6); // Map another port as v6 only PortMappingSettings portv6Only{1235, 81, AF_INET6}; VERIFY_SUCCEEDED(WslMapPort(vm.get(), &portv6Only)); listen(81, "port81ipv6", true); expectContent(1235, AF_INET6, "port81ipv6"); expectNotBound(1235, AF_INET); VERIFY_SUCCEEDED(WslUnmapPort(vm.get(), &portv6Only)); VERIFY_ARE_EQUAL(WslUnmapPort(vm.get(), &portv6Only), HRESULT_FROM_WIN32(ERROR_NOT_FOUND)); expectNotBound(1235, AF_INET6); // Create a forking relay and stress test VERIFY_SUCCEEDED(WslMapPort(vm.get(), &port)); auto [pid, in, out, err] = LaunchCommand(vm.get(), {"/usr/bin/socat", "-dd", "TCP-LISTEN:80,fork,reuseaddr", "system:'echo -n OK'"}); waitForOutput(err.get(), "listening on"); for (auto i = 0; i < 100; i++) { expectContent(1234, AF_INET, "OK"); } VERIFY_SUCCEEDED(WslUnmapPort(vm.get(), &port)); } };