WSL/test/windows/WSLATests.cpp
2025-11-06 18:57:03 -08:00

1171 lines
45 KiB
C++

/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
WSLATests.cpp
Abstract:
This file contains test cases for the WSLA API.
--*/
#include "precomp.h"
#include "Common.h"
#include "WSLAApi.h"
#include "wslaservice.h"
using namespace wsl::windows::common::registry;
using unique_vm = wil::unique_any<WslVirtualMachineHandle, decltype(WslReleaseVirtualMachine), &WslReleaseVirtualMachine>;
class WSLATests
{
WSL_TEST_CLASS(WSLATests)
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<int, wil::unique_handle, wil::unique_handle, wil::unique_handle> LaunchCommand(
WslVirtualMachineHandle vm, const std::vector<const char*>& command)
{
auto copiedCommand = command;
if (copiedCommand.back() != nullptr)
{
copiedCommand.push_back(nullptr);
}
std::vector<WslProcessFileDescriptorSettings> fds(3);
fds[0].Number = 0;
fds[1].Number = 1;
fds[2].Number = 2;
WslCreateProcessSettings WslCreateProcessSettings{};
WslCreateProcessSettings.Executable = copiedCommand[0];
WslCreateProcessSettings.Arguments = copiedCommand.data();
WslCreateProcessSettings.FileDescriptors = fds.data();
WslCreateProcessSettings.FdCount = 3;
int pid = -1;
VERIFY_SUCCEEDED(WslCreateLinuxProcess(vm, &WslCreateProcessSettings, &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(WslVirtualMachineHandle vm, const std::vector<const char*>& command, int timeout = 600000)
{
auto [pid, _, __, ___] = LaunchCommand(vm, command);
WslWaitResult result{};
VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm, pid, timeout, &result));
VERIFY_ARE_EQUAL(result.State, WslProcessStateExited);
return result.Code;
}
unique_vm CreateVm(const WslVirtualMachineSettings* settings, const std::optional<LPCWSTR> rootfs = {})
{
unique_vm vm{};
VERIFY_SUCCEEDED(WslCreateVirtualMachine(settings, &vm));
WslDiskAttachSettings attachSettings{rootfs.value_or(testVhd.c_str()), true};
WslAttachedDiskInformation attachedDisk;
VERIFY_SUCCEEDED(WslAttachDisk(vm.get(), &attachSettings, &attachedDisk));
WslMountSettings mountSettings{attachedDisk.Device, "/mnt", "ext4", "ro", WslMountFlagsChroot | WslMountFlagsWriteableOverlayFs};
VERIFY_SUCCEEDED(WslMount(vm.get(), &mountSettings));
WslMountSettings devMountSettings{nullptr, "/dev", "devtmpfs", "", false};
VERIFY_SUCCEEDED(WslMount(vm.get(), &devMountSettings));
WslMountSettings sysMountSettings{nullptr, "/sys", "sysfs", "", false};
VERIFY_SUCCEEDED(WslMount(vm.get(), &sysMountSettings));
WslMountSettings procMountSettings{nullptr, "/proc", "proc", "", false};
VERIFY_SUCCEEDED(WslMount(vm.get(), &procMountSettings));
WslMountSettings 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();
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
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<const char*> cmd{"/usr/bin/test", "-d", device.c_str()};
return RunCommand(vm.get(), cmd) == 0;
};
// Attach the disk.
WslDiskAttachSettings attachSettings{vhdPath.c_str(), true};
WslAttachedDiskInformation attachedDisk{};
VERIFY_SUCCEEDED(WslAttachDisk(vm.get(), &attachSettings, &attachedDisk));
VERIFY_IS_TRUE(blockDeviceExists(attachedDisk.ScsiLun));
// Mount it to /mnt.
WslMountSettings mountSettings{attachedDisk.Device, "/mnt", "ext4", "ro"};
VERIFY_SUCCEEDED(WslMount(vm.get(), &mountSettings));
// Validate that the mountpoint is present.
std::vector<const char*> 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"), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
// 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));
// Validate that invalid flags return E_INVALIDARG.
WslMountSettings invalidFlagSettings{"/dev/sda", "/mnt", "ext4", "ro", 0x4};
VERIFY_ARE_EQUAL(WslMount(vm.get(), &invalidFlagSettings), E_INVALIDARG);
invalidFlagSettings.Flags = 0xff;
VERIFY_ARE_EQUAL(WslMount(vm.get(), &invalidFlagSettings), E_INVALIDARG);
}
TEST_METHOD(CustomDmesgOutput)
{
WSL2_TEST_ONLY();
auto createVmWithDmesg = [this](bool earlyBootLogging) {
auto [read, write] = CreateSubprocessPipe(false, false);
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
settings.Memory.MemoryMb = 1024;
settings.Options.BootTimeoutMs = 30000;
settings.Options.Dmesg = write.get();
settings.Options.EnableEarlyBootDmesg = earlyBootLogging;
std::vector<char> 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<const char*> 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<std::pair<WslVirtualMachineTerminationReason, std::wstring>> callbackInfo;
auto callback = [](void* context, WslVirtualMachineTerminationReason reason, LPCWSTR details) -> HRESULT {
auto* future = reinterpret_cast<std::promise<std::pair<WslVirtualMachineTerminationReason, std::wstring>>*>(context);
future->set_value(std::make_pair(reason, details));
return S_OK;
};
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
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, WslVirtualMachineTerminationReasonShutdown);
VERIFY_ARE_NOT_EQUAL(details, L"");
}
TEST_METHOD(CreateVmSmokeTest)
{
WSL2_TEST_ONLY();
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
settings.Memory.MemoryMb = 1024;
settings.Options.BootTimeoutMs = 30000;
auto vm = CreateVm(&settings);
// Create a process and wait for it to exit
{
std::vector<const char*> commandLine{"/bin/sh", "-c", "echo $bar", nullptr};
std::vector<WslProcessFileDescriptorSettings> fds(3);
fds[0].Number = 0;
fds[1].Number = 1;
fds[2].Number = 2;
std::vector<const char*> env{"bar=foo", nullptr};
WslCreateProcessSettings WslCreateProcessSettings{};
WslCreateProcessSettings.Executable = "/bin/sh";
WslCreateProcessSettings.Arguments = commandLine.data();
WslCreateProcessSettings.FileDescriptors = fds.data();
WslCreateProcessSettings.Environment = env.data();
WslCreateProcessSettings.FdCount = 3;
int pid = -1;
VERIFY_SUCCEEDED(WslCreateLinuxProcess(vm.get(), &WslCreateProcessSettings, &pid));
LogInfo("pid: %lu", pid);
std::vector<char> buffer(100);
DWORD bytes{};
if (!ReadFile(WslCreateProcessSettings.FileDescriptors[1].Handle, buffer.data(), (DWORD)buffer.size(), &bytes, nullptr))
{
LogError("ReadFile: %lu, handle: 0x%x", GetLastError(), WslCreateProcessSettings.FileDescriptors[1].Handle);
VERIFY_FAIL();
}
VERIFY_ARE_EQUAL(buffer.data(), std::string("foo\n"));
WslWaitResult result{};
VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm.get(), pid, 1000, &result));
VERIFY_ARE_EQUAL(result.State, WslProcessStateExited);
VERIFY_ARE_EQUAL(result.Code, 0);
}
// Create a 'stuck' process and kill it
{
std::vector<const char*> commandLine{"/usr/bin/sleep", "100000", nullptr};
std::vector<WslProcessFileDescriptorSettings> fds(3);
fds[0].Number = 0;
fds[1].Number = 1;
fds[2].Number = 2;
WslCreateProcessSettings WslCreateProcessSettings{};
WslCreateProcessSettings.Executable = commandLine[0];
WslCreateProcessSettings.Arguments = commandLine.data();
WslCreateProcessSettings.FileDescriptors = fds.data();
WslCreateProcessSettings.Environment = nullptr;
WslCreateProcessSettings.FdCount = 3;
int pid = -1;
VERIFY_SUCCEEDED(WslCreateLinuxProcess(vm.get(), &WslCreateProcessSettings, &pid));
// Verify that the process is in a running state
WslWaitResult result{};
VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm.get(), pid, 1000, &result));
VERIFY_ARE_EQUAL(result.State, WslProcessStateRunning);
// Verify that the process can still be waited for
result = {};
VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm.get(), pid, 1000, &result));
VERIFY_ARE_EQUAL(result.State, WslProcessStateRunning);
result = {};
VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm.get(), pid, 0, &result));
VERIFY_ARE_EQUAL(result.State, WslProcessStateRunning);
// 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, WslProcessStateSignaled);
VERIFY_ARE_EQUAL(result.Code, 9);
}
// Test various error paths
{
std::vector<const char*> commandLine{"dummy", "100000", nullptr};
std::vector<WslProcessFileDescriptorSettings> fds(3);
fds[0].Number = 0;
fds[1].Number = 1;
fds[2].Number = 2;
WslCreateProcessSettings WslCreateProcessSettings{};
WslCreateProcessSettings.Executable = commandLine[0];
WslCreateProcessSettings.Arguments = commandLine.data();
WslCreateProcessSettings.FileDescriptors = fds.data();
WslCreateProcessSettings.Environment = nullptr;
WslCreateProcessSettings.FdCount = 3;
int pid = -1;
VERIFY_ARE_EQUAL(WslCreateLinuxProcess(vm.get(), &WslCreateProcessSettings, &pid), E_FAIL);
WslWaitResult result{};
VERIFY_ARE_EQUAL(WslWaitForLinuxProcess(vm.get(), 1234, 1000, &result), E_FAIL);
VERIFY_ARE_EQUAL(result.State, WslProcessStateUnknown);
}
}
TEST_METHOD(InteractiveShell)
{
WSL2_TEST_ONLY();
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
settings.Memory.MemoryMb = 2048;
settings.Options.BootTimeoutMs = 30 * 1000;
settings.Options.EnableDebugShell = true;
settings.Networking.Mode = WslNetworkingModeNone;
auto vm = CreateVm(&settings);
std::vector<const char*> commandLine{"/bin/sh", nullptr};
std::vector<WslProcessFileDescriptorSettings> fds(3);
fds[0].Number = 0;
fds[0].Type = WslFdTypeTerminalInput;
fds[1].Number = 1;
fds[1].Type = WslFdTypeTerminalOutput;
fds[2].Number = 2;
fds[2].Type = WslFdTypeTerminalControl;
const char* env[] = {"TERM=xterm-256color", nullptr};
WslCreateProcessSettings WslCreateProcessSettings{};
WslCreateProcessSettings.Executable = "/bin/sh";
WslCreateProcessSettings.Arguments = commandLine.data();
WslCreateProcessSettings.FileDescriptors = fds.data();
WslCreateProcessSettings.FdCount = static_cast<ULONG>(fds.size());
WslCreateProcessSettings.Environment = env;
int pid = -1;
VERIFY_SUCCEEDED(WslCreateLinuxProcess(vm.get(), &WslCreateProcessSettings, &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(
WslCreateProcessSettings.FileDescriptors[1].Handle, buffer.data() + offset, static_cast<DWORD>(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(
WslCreateProcessSettings.FileDescriptors[0].Handle, content.data(), static_cast<DWORD>(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(
WslCreateProcessSettings.FileDescriptors[0].Handle,
WslCreateProcessSettings.FileDescriptors[1].Handle,
WslCreateProcessSettings.FileDescriptors[2].Handle,
&process));
// Exit the shell
writeTty("exit\n");
VERIFY_ARE_EQUAL(WaitForSingleObject(process.get(), 30000 * 1000), WAIT_OBJECT_0);
}
TEST_METHOD(NATNetworking)
{
WSL2_TEST_ONLY();
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
settings.Memory.MemoryMb = 2048;
settings.Options.BootTimeoutMs = 30 * 1000;
settings.Networking.Mode = WslNetworkingModeNAT;
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(NATNetworkingWithDnsTunneling)
{
WSL2_TEST_ONLY();
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
settings.Memory.MemoryMb = 2048;
settings.Options.BootTimeoutMs = 30 * 1000;
settings.Networking.Mode = WslNetworkingModeNAT;
settings.Networking.DnsTunneling = true;
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 correctly configured.
auto [pid, in, out, err] = LaunchCommand(vm.get(), {"/bin/grep", "-iF", "nameserver ", "/etc/resolv.conf"});
auto output = ReadToString((SOCKET)out.get());
VERIFY_ARE_EQUAL(output, std::format("nameserver {}\n", LX_INIT_DNS_TUNNELING_IP_ADDRESS));
}
TEST_METHOD(OpenFiles)
{
WSL2_TEST_ONLY();
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
settings.Memory.MemoryMb = 2048;
settings.Options.BootTimeoutMs = 30 * 1000;
auto vm = CreateVm(&settings);
struct FileFd
{
int Fd;
WslFdType Flags;
const char* Path;
};
auto createProcess = [&](std::vector<const char*> Args, const std::vector<FileFd>& Fds, HRESULT expectedError = S_OK) {
Args.emplace_back(nullptr);
std::vector<WslProcessFileDescriptorSettings> fds;
for (const auto& e : Fds)
{
fds.emplace_back(WslProcessFileDescriptorSettings{e.Fd, e.Flags, e.Path, nullptr});
}
WslCreateProcessSettings WslCreateProcessSettings{};
WslCreateProcessSettings.Executable = Args[0];
WslCreateProcessSettings.Arguments = Args.data();
WslCreateProcessSettings.FileDescriptors = fds.data();
WslCreateProcessSettings.FdCount = static_cast<DWORD>(fds.size());
int pid{};
VERIFY_ARE_EQUAL(WslCreateLinuxProcess(vm.get(), &WslCreateProcessSettings, &pid), expectedError);
std::vector<wil::unique_handle> handles;
for (const auto& e : fds)
{
handles.emplace_back(e.Handle);
}
return std::make_pair(std::move(handles), pid);
};
auto wait = [&](int pid) {
WslWaitResult result{};
VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm.get(), pid, INFINITE, &result));
VERIFY_ARE_EQUAL(result.State, WslProcessStateExited);
return result.Code;
};
{
auto [fds, pid] =
createProcess({"/bin/cat"}, {{0, WslFdTypeLinuxFileInput, "/proc/self/comm"}, {1, WslFdTypeDefault, nullptr}});
VERIFY_ARE_EQUAL(ReadToString((SOCKET)fds[1].get()), "cat\n");
VERIFY_ARE_EQUAL(wait(pid), 0);
}
{
auto read = [&]() {
auto [readFds, readPid] =
createProcess({"/bin/cat"}, {{0, WslFdTypeLinuxFileInput, "/tmp/output"}, {1, WslFdTypeDefault, nullptr}});
VERIFY_ARE_EQUAL(wait(readPid), 0);
auto content = ReadToString((SOCKET)readFds[1].get());
return content;
};
// Write to a new file.
auto [fds, pid] = createProcess(
{"/bin/cat"},
{{0, WslFdTypeDefault, nullptr},
{1, static_cast<WslFdType>(WslFdTypeLinuxFileOutput | WslFdTypeLinuxFileCreate), "/tmp/output"}});
constexpr auto content = "TestOutput";
VERIFY_IS_TRUE(WriteFile(fds[0].get(), content, static_cast<DWORD>(strlen(content)), nullptr, nullptr));
fds.clear();
VERIFY_ARE_EQUAL(wait(pid), 0);
VERIFY_ARE_EQUAL(read(), content);
// Append content to the same file
auto [appendFds, appendPid] = createProcess(
{"/bin/cat"},
{{0, WslFdTypeDefault, nullptr},
{1, static_cast<WslFdType>(WslFdTypeLinuxFileOutput | WslFdTypeLinuxFileAppend), "/tmp/output"}});
VERIFY_IS_TRUE(WriteFile(appendFds[0].get(), content, static_cast<DWORD>(strlen(content)), nullptr, nullptr));
appendFds.clear();
VERIFY_ARE_EQUAL(wait(appendPid), 0);
VERIFY_ARE_EQUAL(read(), std::format("{}{}", content, content));
// Truncate the file
auto [truncFds, truncPid] = createProcess(
{"/bin/cat"},
{{0, WslFdTypeDefault, nullptr}, {1, static_cast<WslFdType>(WslFdTypeLinuxFileOutput), "/tmp/output"}});
VERIFY_IS_TRUE(WriteFile(truncFds[0].get(), content, static_cast<DWORD>(strlen(content)), nullptr, nullptr));
truncFds.clear();
VERIFY_ARE_EQUAL(wait(truncPid), 0);
VERIFY_ARE_EQUAL(read(), content);
}
// Test various error paths
{
createProcess({"/bin/cat"}, {{0, static_cast<WslFdType>(WslFdTypeLinuxFileOutput), "/tmp/DoesNotExist"}}, E_FAIL);
createProcess({"/bin/cat"}, {{0, static_cast<WslFdType>(WslFdTypeLinuxFileOutput), nullptr}}, E_INVALIDARG);
createProcess({"/bin/cat"}, {{0, static_cast<WslFdType>(WslFdTypeDefault), "should-be-null"}}, E_INVALIDARG);
createProcess({"/bin/cat"}, {{0, static_cast<WslFdType>(WslFdTypeDefault | WslFdTypeLinuxFileOutput), nullptr}}, E_INVALIDARG);
createProcess({"/bin/cat"}, {{0, static_cast<WslFdType>(WslFdTypeLinuxFileAppend), nullptr}}, E_INVALIDARG);
createProcess({"/bin/cat"}, {{0, static_cast<WslFdType>(WslFdTypeLinuxFileInput | WslFdTypeLinuxFileAppend), nullptr}}, E_INVALIDARG);
}
// Validate that read & write modes are respected
{
auto [fds, pid] = createProcess(
{"/bin/cat"},
{{0, WslFdTypeLinuxFileInput, "/proc/self/comm"}, {1, WslFdTypeLinuxFileInput, "/tmp/output"}, {2, WslFdTypeDefault, nullptr}});
VERIFY_ARE_EQUAL(ReadToString((SOCKET)fds[2].get()), "/bin/cat: write error: Bad file descriptor\n");
VERIFY_ARE_EQUAL(wait(pid), 1);
}
{
auto [fds, pid] = createProcess({"/bin/cat"}, {{0, WslFdTypeLinuxFileOutput, "/tmp/output"}, {2, WslFdTypeDefault, nullptr}});
VERIFY_ARE_EQUAL(ReadToString((SOCKET)fds[1].get()), "/bin/cat: standard output: Bad file descriptor\n");
VERIFY_ARE_EQUAL(wait(pid), 1);
}
}
TEST_METHOD(NATPortMapping)
{
WSL2_TEST_ONLY();
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
settings.Memory.MemoryMb = 2048;
settings.Options.BootTimeoutMs = 30 * 1000;
settings.Networking.Mode = WslNetworkingModeNAT;
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<SOCKADDR*>(&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
WslPortMappingSettings 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
WslPortMappingSettings 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
WslPortMappingSettings 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));
}
TEST_METHOD(StuckVmTermination)
{
WSL2_TEST_ONLY();
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
settings.Memory.MemoryMb = 2048;
settings.Options.BootTimeoutMs = 30 * 1000;
settings.Networking.Mode = WslNetworkingModeNone;
auto vm = CreateVm(&settings);
auto [pid, stdinFd, _, __] = LaunchCommand(vm.get(), {"/bin/cat"});
// Create a 'stuck' thread, waiting for cat to exit
std::thread stuckThread([&]() {
WslWaitResult result{};
WslWaitForLinuxProcess(vm.get(), pid, INFINITE, &result);
});
// Stop the service
StopWslaService();
// Verify that the thread is unstuck
stuckThread.join();
}
TEST_METHOD(WindowsMounts)
{
WSL2_TEST_ONLY();
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
settings.Memory.MemoryMb = 2048;
settings.Options.BootTimeoutMs = 30 * 1000;
settings.Networking.Mode = WslNetworkingModeNone;
auto vm = CreateVm(&settings);
auto expectMount = [&](const std::string& target, const std::optional<std::string>& options) {
auto cmd = std::format("set -o pipefail ; findmnt '{}' | tail -n 1", target);
auto [pid, in, out, err] = LaunchCommand(vm.get(), {"/bin/bash", "-c", cmd.c_str()});
auto output = ReadToString((SOCKET)out.get());
auto error = ReadToString((SOCKET)err.get());
WslWaitResult result{};
VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm.get(), pid, INFINITE, &result));
if (result.Code != (options.has_value() ? 0 : 1))
{
LogError("%hs failed. code=%i, output: %hs, error: %hs", cmd.c_str(), result.Code, output.c_str(), error.c_str());
VERIFY_FAIL();
}
if (options.has_value() && !PathMatchSpecA(output.c_str(), options->c_str()))
{
std::wstring message = std::format(L"Output: '{}' didn't match pattern: '{}'", output, options.value());
VERIFY_FAIL(message.c_str());
}
};
auto testFolder = std::filesystem::current_path() / "test-folder";
std::filesystem::create_directories(testFolder);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove_all(testFolder); });
// Validate writeable mount.
{
VERIFY_SUCCEEDED(WslMountWindowsFolder(vm.get(), testFolder.c_str(), "/win-path", false));
expectMount("/win-path", "/win-path*9p*rw,relatime,aname=*,cache=5,access=client,msize=65536,trans=fd,rfd=*,wfd=*");
// Validate that mount can't be stacked on each other
VERIFY_ARE_EQUAL(WslMountWindowsFolder(vm.get(), testFolder.c_str(), "/win-path", false), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
// Validate that folder is writeable from linux
VERIFY_ARE_EQUAL(RunCommand(vm.get(), {"/bin/bash", "-c", "echo -n content > /win-path/file.txt && sync"}), 0);
VERIFY_ARE_EQUAL(ReadFileContent(testFolder / "file.txt"), L"content");
VERIFY_SUCCEEDED(WslUnmountWindowsFolder(vm.get(), "/win-path"));
expectMount("/win-path", {});
}
// Validate read-only mount.
{
VERIFY_SUCCEEDED(WslMountWindowsFolder(vm.get(), testFolder.c_str(), "/win-path", true));
expectMount("/win-path", "/win-path*9p*rw,relatime,aname=*,cache=5,access=client,msize=65536,trans=fd,rfd=*,wfd=*");
// Validate that folder is not writeable from linux
VERIFY_ARE_EQUAL(RunCommand(vm.get(), {"/bin/bash", "-c", "echo -n content > /win-path/file.txt"}), 1);
VERIFY_SUCCEEDED(WslUnmountWindowsFolder(vm.get(), "/win-path"));
expectMount("/win-path", {});
}
// Validate various error paths
{
VERIFY_ARE_EQUAL(WslMountWindowsFolder(vm.get(), L"relative-path", "/win-path", true), E_INVALIDARG);
VERIFY_ARE_EQUAL(WslMountWindowsFolder(vm.get(), L"C:\\does-not-exist", "/win-path", true), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND));
VERIFY_ARE_EQUAL(WslUnmountWindowsFolder(vm.get(), "/not-mounted"), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
VERIFY_ARE_EQUAL(WslUnmountWindowsFolder(vm.get(), "/proc"), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
// Validate that folders that are manually unmounted from the guest are handled properly
VERIFY_SUCCEEDED(WslMountWindowsFolder(vm.get(), testFolder.c_str(), "/win-path", true));
expectMount("/win-path", "/win-path*9p*rw,relatime,aname=*,cache=5,access=client,msize=65536,trans=fd,rfd=*,wfd=*");
VERIFY_ARE_EQUAL(RunCommand(vm.get(), {"/usr/bin/umount", "/win-path"}), 0);
VERIFY_SUCCEEDED(WslUnmountWindowsFolder(vm.get(), "/win-path"));
}
}
// This test case validates that no file descriptors are leaked to user processes.
TEST_METHOD(Fd)
{
WSL2_TEST_ONLY();
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
settings.Memory.MemoryMb = 2048;
settings.Options.BootTimeoutMs = 30 * 1000;
settings.Networking.Mode = WslNetworkingModeNone;
auto vm = CreateVm(&settings);
std::vector<WslProcessFileDescriptorSettings> fds(1);
fds[0].Number = 1;
fds[0].Type = WslFdTypeDefault;
const char* args[] = {"/bin/bash", "-c", "echo /proc/self/fd/* && readlink /proc/self/fd/*", nullptr};
WslCreateProcessSettings WslCreateProcessSettings{};
WslCreateProcessSettings.Executable = "/bin/bash";
WslCreateProcessSettings.Arguments = args;
WslCreateProcessSettings.FileDescriptors = fds.data();
WslCreateProcessSettings.FdCount = 1;
int pid = -1;
VERIFY_SUCCEEDED(WslCreateLinuxProcess(vm.get(), &WslCreateProcessSettings, &pid));
wil::unique_socket output{(SOCKET)fds[0].Handle};
auto result = ReadToString(output.get());
// Note: fd/0 is opened readlink to read the actual content of /proc/fd.
if (!PathMatchSpecA(result.c_str(), "/proc/self/fd/0 /proc/self/fd/1\nsocket:[*]\n"))
{
LogInfo("Found additional fds: %hs", result.c_str());
VERIFY_FAIL();
}
}
TEST_METHOD(GPU)
{
WSL2_TEST_ONLY();
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
settings.Memory.MemoryMb = 2048;
settings.Options.BootTimeoutMs = 30 * 1000;
settings.Networking.Mode = WslNetworkingModeNAT;
settings.GPU.Enable = true;
auto vm = CreateVm(&settings);
// Validate that the GPU device is available.
VERIFY_ARE_EQUAL(RunCommand(vm.get(), {"/bin/bash", "-c", "test -c /dev/dxg"}), 0);
// Validate that invalid flags return E_INVALIDARG
{
VERIFY_ARE_EQUAL(WslMountGpuLibraries(vm.get(), "/usr/lib/wsl/lib", "/usr/lib/wsl/drivers", WslMountFlagsChroot), E_INVALIDARG);
VERIFY_ARE_EQUAL(WslMountGpuLibraries(vm.get(), "/usr/lib/wsl/lib", "/usr/lib/wsl/drivers", static_cast<WslMountFlags>(1024)), E_INVALIDARG);
}
// Validate GPU mounts
VERIFY_SUCCEEDED(WslMountGpuLibraries(vm.get(), "/usr/lib/wsl/lib", "/usr/lib/wsl/drivers", WslMountFlagsNone));
auto expectMount = [&](const std::string& target, const std::optional<std::string>& options) {
auto cmd = std::format("set -o pipefail ; findmnt '{}' | tail -n 1", target);
auto [pid, in, out, err] = LaunchCommand(vm.get(), {"/bin/bash", "-c", cmd.c_str()});
auto output = ReadToString((SOCKET)out.get());
auto error = ReadToString((SOCKET)err.get());
WslWaitResult result{};
VERIFY_SUCCEEDED(WslWaitForLinuxProcess(vm.get(), pid, INFINITE, &result));
if (result.Code != (options.has_value() ? 0 : 1))
{
LogError("%hs failed. code=%i, output: %hs, error: %hs", cmd.c_str(), result.Code, output.c_str(), error.c_str());
VERIFY_FAIL();
}
if (options.has_value() && !PathMatchSpecA(output.c_str(), options->c_str()))
{
std::wstring message = std::format(L"Output: '{}' didn't match pattern: '{}'", output, options.value());
VERIFY_FAIL(message.c_str());
}
};
expectMount(
"/usr/lib/wsl/drivers",
"/usr/lib/wsl/drivers*9p*relatime,aname=*,cache=5,access=client,msize=65536,trans=fd,rfd=*,wfd=*");
expectMount("/usr/lib/wsl/lib", "/usr/lib/wsl/lib none*overlay ro,relatime,lowerdir=/usr/lib/wsl/lib/packaged*");
// Validate that the mount points arenot writeable.
VERIFY_ARE_EQUAL(RunCommand(vm.get(), {"/usr/bin/touch", "/usr/lib/wsl/drivers/test"}), 1L);
VERIFY_ARE_EQUAL(RunCommand(vm.get(), {"/usr/bin/touch", "/usr/lib/wsl/lib/test"}), 1L);
// Create a writeable mount point.
VERIFY_SUCCEEDED(WslMountGpuLibraries(vm.get(), "/usr/lib/wsl/lib-rw", "/usr/lib/wsl/drivers-rw", WslMountFlagsWriteableOverlayFs));
expectMount(
"/usr/lib/wsl/drivers-rw",
"/usr/lib/wsl/drivers-rw "
"none*overlay*rw,relatime,lowerdir=/usr/lib/wsl/drivers-rw,upperdir=/usr/lib/wsl/drivers-rw-rw/rw/upper,workdir=/usr/"
"lib/wsl/drivers-rw-rw/rw/work*");
expectMount(
"/usr/lib/wsl/lib-rw",
"/usr/lib/wsl/lib-rw none*overlay "
"rw,relatime,lowerdir=/usr/lib/wsl/lib-rw,upperdir=/usr/lib/wsl/lib-rw-rw/rw/upper,workdir=/usr/lib/wsl/lib-rw-rw/rw/"
"work*");
// Verify that the mountpoints are actually writeable.
VERIFY_ARE_EQUAL(RunCommand(vm.get(), {"/usr/bin/touch", "/usr/lib/wsl/lib-rw/test"}), 0L);
VERIFY_ARE_EQUAL(RunCommand(vm.get(), {"/usr/bin/touch", "/usr/lib/wsl/drivers-rw/test"}), 0L);
// Validate that trying to mount the shares without GPU support disabled fails.
{
settings.GPU.Enable = false;
auto vm = CreateVm(&settings);
VERIFY_ARE_EQUAL(
WslMountGpuLibraries(vm.get(), "/usr/lib/wsl/lib", "/usr/lib/wsl/drivers", WslMountFlagsNone),
HRESULT_FROM_WIN32(ERROR_INVALID_CONFIG_VALUE));
}
}
TEST_METHOD(Modules)
{
WSL2_TEST_ONLY();
WslVirtualMachineSettings settings{};
settings.CPU.CpuCount = 4;
settings.DisplayName = L"WSLA";
settings.Memory.MemoryMb = 2048;
settings.Options.BootTimeoutMs = 30 * 1000;
settings.Networking.Mode = WslNetworkingModeNone;
// Use the system distro vhd for modprobe & lsmod.
#ifdef WSL_SYSTEM_DISTRO_PATH
auto rootfs = std::filesystem::path(TEXT(WSL_SYSTEM_DISTRO_PATH));
#else
auto rootfs = std::filesystem::path(wsl::windows::common::wslutil::GetMsiPackagePath().value()) / L"system.vhd";
#endif
auto vm = CreateVm(&settings, rootfs.c_str());
// Sanity check.
VERIFY_ARE_EQUAL(RunCommand(vm.get(), {"/bin/bash", "-c", "lsmod | grep ^xsk_diag"}), 1);
// Validate that modules can be loaded.
VERIFY_ARE_EQUAL(RunCommand(vm.get(), {"/usr/sbin/modprobe", "xsk_diag"}), 0);
// Validate that xsk_diag is now loaded.
VERIFY_ARE_EQUAL(RunCommand(vm.get(), {"/bin/bash", "-c", "lsmod | grep ^xsk_diag"}), 0);
}
TEST_METHOD(CreateSessionSmokeTest)
{
wil::com_ptr<IWSLAUserSession> userSession;
VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLAUserSession), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&userSession)));
wsl::windows::common::security::ConfigureForCOMImpersonation(userSession.get());
WSLA_SESSION_SETTINGS settings{L"my-display-name"};
wil::com_ptr<IWSLASession> session;
VIRTUAL_MACHINE_SETTINGS vmSettings{};
vmSettings.BootTimeoutMs = 30 * 1000;
vmSettings.DisplayName = L"WSLA";
vmSettings.MemoryMb = 2048;
vmSettings.CpuCount = 4;
vmSettings.NetworkingMode = WslNetworkingModeNone;
vmSettings.EnableDebugShell = true;
VERIFY_SUCCEEDED(userSession->CreateSession(&settings, &vmSettings, &session));
wil::unique_cotaskmem_string returnedDisplayName;
VERIFY_SUCCEEDED(session->GetDisplayName(&returnedDisplayName));
VERIFY_ARE_EQUAL(returnedDisplayName.get(), std::wstring(L"my-display-name"));
}
TEST_METHOD(WiringSmokeTest)
{
wil::com_ptr<IWSLAUserSession> userSession;
VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLAUserSession), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&userSession)));
wsl::windows::common::security::ConfigureForCOMImpersonation(userSession.get());
WSLA_SESSION_SETTINGS settings{L"my-display-name"};
wil::com_ptr<IWSLASession> session;
VIRTUAL_MACHINE_SETTINGS vmSettings{};
vmSettings.BootTimeoutMs = 30 * 1000;
vmSettings.DisplayName = L"WSLA";
vmSettings.MemoryMb = 2048;
vmSettings.CpuCount = 4;
vmSettings.NetworkingMode = WslNetworkingModeNone;
vmSettings.EnableDebugShell = true;
VERIFY_SUCCEEDED(userSession->CreateSession(&settings, &vmSettings, &session));
wil::com_ptr<IWSLAContainer> container;
WSLA_CONTAINER_OPTIONS containerOptions{};
containerOptions.Image = "dummy";
containerOptions.Name = "dummy";
VERIFY_SUCCEEDED(session->CreateContainer(&containerOptions, &container));
wil::com_ptr<IWSLAProcess> process;
WSLA_PROCESS_OPTIONS processOptions{};
processOptions.Executable = "dummy";
VERIFY_SUCCEEDED(container->Exec(&processOptions, &process));
wil::unique_handle exitEvent;
VERIFY_SUCCEEDED(process->GetExitEvent(reinterpret_cast<ULONG*>(exitEvent.addressof())));
// Verify that the event handle is valid.
VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent.get(), 0), WAIT_TIMEOUT);
}
};