WSL/test/windows/UnitTests.cpp
Ben Hillis 9e9ef6f145
Fix edge cases around .vhd support (#13061)
* Fix edge cases around .vhd support

* PR feedback

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* pr feedback

* remove unneeded scope exit in unit test

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-21 17:00:47 -07:00

6346 lines
260 KiB
C++

/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
UnitTests.cpp
Abstract:
This file contains unit tests for WSL.
--*/
#include "precomp.h"
#include "Common.h"
#include <AclAPI.h>
#include <fstream>
#include <filesystem>
#include "wslservice.h"
#include "registry.hpp"
#include "helpers.hpp"
#include "svccomm.hpp"
#include "lxfsshares.h"
#include <userenv.h>
#include <nlohmann/json.hpp>
#include "Distribution.h"
#include "WslCoreConfigInterface.h"
#include "CommandLine.h"
#define LXSST_TEST_USERNAME L"kerneltest"
#define LXSST_LXFS_TEST_DIR L"lxfstest"
#define LXSST_LXFS_MKDIR_COMMAND_LINE \
L"/bin/bash -c \"mkdir /" LXSST_LXFS_TEST_DIR "; chown 1000:1001 /" LXSST_LXFS_TEST_DIR L"\""
#define LXSST_LXFS_CLEANUP_COMMAND_LINE L"/bin/bash -c \"rm -rf /" LXSST_LXFS_TEST_DIR L"\""
#define LXSST_LXFS_TEST_SUB_DIR L"testdir"
#define LXSST_FSTAB_BACKUP_COMMAND_LINE L"/bin/bash -c 'cp /etc/fstab /etc/fstab.bak'"
#define LXSST_FSTAB_SETUP_COMMAND_LINE L"/bin/bash -c 'echo C:\\\\ /mnt/c drvfs metadata 0 0 >> /etc/fstab'"
#define LXSST_FSTAB_CLEANUP_COMMAND_LINE L"/bin/bash -c \"cp /etc/fstab.bak /etc/fstab\""
#define LXSST_TESTS_INSTALL_COMMAND_LINE L"/bin/bash -c 'cd /data/test; ./build_tests.sh'"
#define LXSST_IMPORT_DISTRO_TEST_DIR L"C:\\importtest\\"
#define LXSST_UID_ROOT 0
#define LXSST_GID_ROOT 0
#define LXSST_USERNAME_ROOT L"root"
#define LXSS_OOBE_COMPLETE_NAME L"OOBEComplete"
constexpr auto c_testDistributionEndpoint = L"http://127.0.0.1:12345/";
constexpr auto c_testDistributionJson =
LR"({
\"Distributions\":[
{
\"Name\": \"Debian\",
\"FriendlyName\": \"Debian\",
\"StoreAppId\": \"Dummy\",
\"Amd64\": true,
\"Arm64\": true,
\"Amd64PackageUrl\": null,
\"Arm64PackageUrl\": null,
\"PackageFamilyName\": \"Dummy\"
}
]})";
using wsl::windows::common::wslutil::GetSystemErrorString;
extern std::wstring g_testDistroPath;
namespace UnitTests {
class UnitTests
{
WSL_TEST_CLASS(UnitTests)
TEST_CLASS_SETUP(TestClassSetup)
{
VERIFY_ARE_EQUAL(LxsstuInitialize(FALSE), TRUE);
// Build the unit tests on the Linux side
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(LXSST_TESTS_INSTALL_COMMAND_LINE), (DWORD)0);
return true;
}
TEST_CLASS_CLEANUP(TestClassCleanup)
{
LxsstuLaunchWsl(LXSST_LXFS_CLEANUP_COMMAND_LINE);
LxsstuUninitialize(FALSE);
return true;
}
TEST_METHOD_CLEANUP(MethodCleanup)
{
LxssLogKernelOutput();
return true;
}
// Note: This test should run first since other test cases create files extended attributes, which causes bdstar to emit warnings during export.
TEST_METHOD(ExportDistro)
{
constexpr auto tarPath = L"exported-test-distro.tar";
constexpr auto vhdPath = L"exported-test-distro.vhdx";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
LOG_IF_WIN32_BOOL_FALSE(DeleteFile(tarPath));
LOG_IF_WIN32_BOOL_FALSE(DeleteFile(vhdPath));
});
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {}", LXSS_DISTRO_NAME_TEST_L, tarPath));
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
VERIFY_ARE_EQUAL(err, L"");
}
// Validate that the file is a valid tar
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"bash -c 'tar tf {} | grep -iF /root/.bashrc'", tarPath));
VERIFY_ARE_EQUAL(out, L"./root/.bashrc\n");
VERIFY_ARE_EQUAL(err, L"");
}
// Validate that gzip compression works
{
auto [out, err] =
LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format tar.gz", LXSS_DISTRO_NAME_TEST_L, tarPath));
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
VERIFY_ARE_EQUAL(err, L"");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"gzip -t {}", tarPath)), 0L);
}
// Verify that xzip compression works
{
auto [out, err] =
LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format tar.xz", LXSS_DISTRO_NAME_TEST_L, tarPath));
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
VERIFY_ARE_EQUAL(err, L"");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"xz -t {}", tarPath)), 0L);
}
// Validate that exporting as vhd works
if (LxsstuVmMode())
{
WslShutdown(); // TODO: detach disk when distribution is stopped to remove this requirement.
auto [out, err] =
LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", LXSS_DISTRO_NAME_TEST_L, vhdPath));
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
VERIFY_ARE_EQUAL(err, L"");
auto [vhdType, _] = LxsstuLaunchPowershellAndCaptureOutput(std::format(L"(Get-VHD '{}').VhdType", vhdPath));
VERIFY_ARE_EQUAL(vhdType, L"Dynamic\r\n");
}
else
{
auto [out, err] =
LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", LXSS_DISTRO_NAME_TEST_L, vhdPath), -1);
VERIFY_ARE_EQUAL(out, L"This operation is only supported by WSL2.\r\nError code: Wsl/Service/WSL_E_WSL2_NEEDED\r\n");
VERIFY_ARE_EQUAL(err, L"");
}
}
TEST_METHOD(SystemdSafeMode)
{
WSL2_TEST_ONLY();
SKIP_TEST_UNSTABLE(); // TODO: Re-enable when this issue is solved in main.
auto revert = EnableSystemd();
// generate a new test config with safe mode enabled
WslConfigChange config(LxssGenerateTestConfig({.safeMode = true}));
// verify that even though systemd is enabled, safe mode prevents it from executing
VERIFY_IS_FALSE(IsSystemdRunning(L"--system", 1));
config.Update(L"");
// disable safe mode and verify that it systemd runs
VERIFY_IS_TRUE(IsSystemdRunning(L"--system"));
}
TEST_METHOD(SystemdDisabled)
{
WSL2_TEST_ONLY();
// tests that systemd does not run without the wsl.conf option enabled
// run and check the output of systemctl --system
VERIFY_IS_FALSE(IsSystemdRunning(L"--system", 1));
}
TEST_METHOD(SystemdSystem)
{
WSL2_TEST_ONLY();
auto cleanup = wil::scope_exit([] {
// clean up wsl.conf file
const std::wstring disableSystemdCmd(LXSST_REMOVE_DISTRO_CONF_COMMAND_LINE);
LxsstuLaunchWsl(disableSystemdCmd);
TerminateDistribution();
});
auto revert = EnableSystemd();
VERIFY_IS_TRUE(IsSystemdRunning(L"--system"));
}
TEST_METHOD(SystemdUser)
{
WSL2_TEST_ONLY();
// enable systemd before creating the user.
// if not called first, the runtime directories needed for --user will not have been created
auto cleanup = EnableSystemd();
// create test user and run test as that user
ULONG TestUid;
ULONG TestGid;
CreateUser(LXSST_TEST_USERNAME, &TestUid, &TestGid);
auto userCleanup = wil::scope_exit([]() { LxsstuLaunchWsl(L"userdel " LXSST_TEST_USERNAME); });
auto validateUserSession = [&]() {
// verify that the user service is running
const std::wstring isServiceActiveCmd =
std::format(L"-u {} systemctl is-active user@{}.service ; exit 0", LXSST_TEST_USERNAME, TestUid);
std::wstring out;
std::wstring err;
try
{
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(isServiceActiveCmd.data());
}
CATCH_LOG();
Trim(out);
if (out.compare(L"active") != 0)
{
LogError(
"Unexpected output from systemd: %ls. Stderr: %ls, cmd: %ls", out.c_str(), err.c_str(), isServiceActiveCmd.c_str());
VERIFY_FAIL();
}
// Verify that /run/user/<uid> is a writable tmpfs mount visible in both mount namespaces.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"touch /run/user/" + std::to_wstring(TestUid) + L"/dummy-test-file"), 0u);
auto command = L"mount | grep -iF 'tmpfs on /run/user/" + std::to_wstring(TestUid) + L" type tmpfs (rw'";
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(command), 0u);
const auto nonElevatedToken = GetNonElevatedToken();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(command, nullptr, nullptr, nullptr, nonElevatedToken.get()), 0u);
};
// Validate user sessions state with gui apps disabled.
{
validateUserSession();
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"echo $DISPLAY", LXSST_TEST_USERNAME));
VERIFY_ARE_EQUAL(out, L"\n");
}
// Validate user sessions state with gui apps enabled.
{
WslConfigChange config(LxssGenerateTestConfig({.guiApplications = true}));
validateUserSession();
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"echo $DISPLAY", LXSST_TEST_USERNAME));
VERIFY_ARE_EQUAL(out, L":0\n");
}
// Create a 'broken' /run/user and validate that the warning is correctly displayed.
{
TerminateDistribution();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"chmod 000 /run/user"), 0L);
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-u {} echo OK", LXSST_TEST_USERNAME));
VERIFY_ARE_EQUAL(out, L"OK\n");
VERIFY_ARE_EQUAL(
err, L"wsl: Failed to start the systemd user session for 'kerneltest'. See journalctl for more details.\n");
}
}
static bool IsSystemdRunning(const std::wstring& SystemdScope, int ExpectedExitCode = 0)
{
// run and check the output of systemctl --system
const auto systemctlCmd = std::format(L"systemctl '{}' is-system-running ; exit 0", SystemdScope);
std::wstring out;
std::wstring error;
// capture the output of systemctl and trim for good measure
try
{
std::tie(out, error) = LxsstuLaunchWslAndCaptureOutput(systemctlCmd.c_str(), ExpectedExitCode);
}
CATCH_LOG()
Trim(out);
// ensure that systemd is either running in a degraded or running state
if ((out.compare(L"degraded") == 0) || (out.compare(L"running") == 0))
{
return true;
}
LogInfo(
"Error when checking if systemd is running: %ls (scope: %ls, stderr: %ls)", out.c_str(), SystemdScope.c_str(), error.c_str());
return false;
}
TEST_METHOD(SystemdNoClearTmpUnit)
{
WSL2_TEST_ONLY();
// ensures that we don't leave state on exit
auto cleanup = EnableSystemd("initTimeout=0");
// Wait for systemd to be started
VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
[&]() { THROW_HR_IF(E_UNEXPECTED, !IsSystemdRunning(L"--system")); }, std::chrono::seconds(1), std::chrono::minutes(1)));
// Validate that the X11 socket has not been deleted
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -d /tmp/.X11-unix"), 0L);
}
TEST_METHOD(SystemdBinfmtIsRestored)
{
WSL2_TEST_ONLY();
// Override WSL's binfmt interpreter
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"echo ':WSLInterop:M::MZ::/bin/echo:PF' > /usr/lib/binfmt.d/dummy.conf"), 0L);
auto cleanupBinfmt = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
LxsstuLaunchWsl(L"rm /usr/lib/binfmt.d/dummy.conf");
WslShutdown(); // Required since this test registers a custom binfmt interpreter.
});
{
// Enable systemd (restarts distro).
auto cleanupSystemd = EnableSystemd();
auto validateBinfmt = []() {
// Validate that WSL's binfmt interpreter is still in place.
auto [cmdOutput, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok");
VERIFY_ARE_EQUAL(cmdOutput, L"ok\r\n");
};
validateBinfmt();
// Validate that this still works after restarting the distribution.
TerminateDistribution();
validateBinfmt();
// Validate that stopping or restarting systemd-binfmt doesn't break interop.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl stop systemd-binfmt.service"), 0u);
validateBinfmt();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl restart systemd-binfmt.service"), 0u);
validateBinfmt();
// Validate that the unit is regenerated after a daemon-reload.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl daemon-reload && systemctl restart systemd-binfmt.service"), 0u);
validateBinfmt();
}
{
// Enable systemd (restarts distro).
auto cleanupSystemd = EnableSystemd("protectBinfmt=false");
// Validate that WSL's binfmt interpreter is overridden
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok");
VERIFY_IS_TRUE(wsl::shared::string::IsEqual(output, L"/mnt/c/Windows/system32/cmd.exe cmd.exe /c echo ok\n", true));
}
}
TEST_METHOD(Dup)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dup", L"Dup"));
}
TEST_METHOD(Epoll)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests epoll", L"Epoll"));
}
TEST_METHOD(EventFd)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests eventfd", L"EventFd"));
}
TEST_METHOD(Flock)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests flock", L"Flock"));
}
TEST_METHOD(Fork)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests fork", L"Fork"));
}
TEST_METHOD(FsCommonLxFs)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests fscommon", L"fscommon_lxfs"));
}
TEST_METHOD(GetSetId)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests get_set_id", L"get_set_id"));
}
TEST_METHOD(Inotify)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests inotify", L"INOTIFY"));
}
#if !defined(_ARM64_)
TEST_METHOD(ResourceLimits)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests resourcelimits", L"resourcelimits"));
}
TEST_METHOD(Select)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests select", L"Select"));
}
#endif
TEST_METHOD(Madvise)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests madvise", L"madvise"));
}
TEST_METHOD(Mprotect)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests mprotect", L"mprotect"));
}
TEST_METHOD(Pipe)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests pipe", L"Pipe"));
}
TEST_METHOD(Sched)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sched", L"sched"));
}
TEST_METHOD(SocketNonblocking)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests socket_nonblock", L"socket_nonblocking"));
}
TEST_METHOD(Splice)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests splice", L"Splice"));
}
TEST_METHOD(Sysfs)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sysfs", L"SysFs"));
}
TEST_METHOD(Tty)
{
WSL1_TEST_ONLY();
auto OriginalHandles = UseOriginalStdHandles();
auto Restore = wil::scope_exit([&OriginalHandles]() { RestoreTestStdHandles(OriginalHandles); });
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests tty", L"tty"));
}
TEST_METHOD(Utimensat)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests utimensat", L"Utimensat"));
}
TEST_METHOD(WaitPid)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests waitpid", L"WaitPid"));
}
TEST_METHOD(Brk)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests brk", L"brk"));
}
TEST_METHOD(Mremap)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests mremap", L"mremap"));
}
TEST_METHOD(VfsAccess)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests vfsaccess", L"vfsaccess"));
}
TEST_METHOD(DevPt)
{
WSL1_TEST_ONLY();
auto OriginalHandles = UseOriginalStdHandles();
auto Restore = wil::scope_exit([&OriginalHandles]() { RestoreTestStdHandles(OriginalHandles); });
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dev_pt", L"dev_pt"));
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dev_pt_2", L"dev_pt_2"));
}
TEST_METHOD(Timer)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests timer", L"timer"));
}
TEST_METHOD(SysInfo)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sysinfo", L"Sysinfo"));
}
TEST_METHOD(TimerFd)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests timerfd", L"timerfd"));
}
TEST_METHOD(Ioprio)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests ioprio", L"Ioprio"));
}
TEST_METHOD(Interop)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests interop", L"interop"));
//
// Run wsl.exe with a very long command line. This ensures that the buffer
// resizing logic that is used by the WSL init daemon is able to correctly
// handle very long messages.
//
// N.B. /bin/true ignores all arguments and always returns 0.
//
std::wstring Command{L"/bin/true "};
Command += std::wstring(0x1000, L'x');
VERIFY_IS_TRUE(LxsstuLaunchWsl(Command.c_str()) == 0);
// Validate that windows executable can run from the linux filesystem. See: https://github.com/microsoft/WSL/issues/10812
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"cp /mnt/c/Program\\ Files/WSL/wsl.exe /tmp"), 0L);
auto [out, _] =
LxsstuLaunchWslAndCaptureOutput(L"WSLENV=WSL_UTF8 WSL_UTF8=1 WSL_INTEROP=/run/WSL/1_interop /tmp/wsl.exe --version");
VERIFY_IS_TRUE(out.find(TEXT(WSL_PACKAGE_VERSION)) != std::string::npos);
}
static std::wstring FormUserCommandLine(_In_ const std::wstring& Username, _In_ ULONG Uid, _In_ ULONG Gid)
{
return std::format(L"/data/test/wsl_unit_tests user {} {} {}", Username, Uid, Gid);
}
TEST_METHOD(User)
{
//
// Create a test user and run the test as that user.
//
ULONG TestUid;
ULONG TestGid;
CreateUser(LXSST_TEST_USERNAME, &TestUid, &TestGid);
std::wstring CommandLine = FormUserCommandLine(LXSST_TEST_USERNAME, TestUid, TestGid);
LogInfo("Running test as user %s", LXSST_TEST_USERNAME);
VERIFY_NO_THROW(LxsstuRunTest(CommandLine.c_str(), L"user", LXSST_TEST_USERNAME));
//
// Add the user to 64 more groups to make sure > 32 groups is supported.
//
{
DistroFileChange groups(L"/etc/group", true);
CommandLine = std::format(L"-- for i in $(seq 1 64); do groupadd group$i; usermod -a -G group$i {}; done", LXSST_TEST_USERNAME);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(CommandLine), (DWORD)0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"{} {} {}", WSL_USER_ARG_LONG, LXSST_TEST_USERNAME, "echo success")), (DWORD)0);
}
//
// Run the test as root.
//
ULONG RootUid;
ULONG RootGid;
CreateUser(LXSST_USERNAME_ROOT, &RootUid, &RootGid);
CommandLine = FormUserCommandLine(LXSST_USERNAME_ROOT, LXSST_UID_ROOT, LXSST_GID_ROOT);
LogInfo("Running test as user %s", LXSST_USERNAME_ROOT);
VERIFY_NO_THROW(LxsstuRunTest(CommandLine.c_str(), L"user", LXSST_USERNAME_ROOT));
//
// Set the default user to the newly created user.
//
// N.B. Modifying the default UID should cause the instance to be recreated and the plan9 server launched as the default user.
//
const auto wslSupport =
wil::CoCreateInstance<LxssUserSession, IWslSupport>(CLSCTX_LOCAL_SERVER | CLSCTX_ENABLE_CLOAKING | CLSCTX_ENABLE_AAA);
ULONG Version;
ULONG DefaultUid;
wil::unique_cotaskmem_array_ptr<wil::unique_cotaskmem_ansistring> DefaultEnvironment{};
ULONG WslFlags;
VERIFY_SUCCEEDED(wslSupport->GetDistributionConfiguration(
LXSS_DISTRO_NAME_TEST_L, &Version, &DefaultUid, DefaultEnvironment.size_address<ULONG>(), &DefaultEnvironment, &WslFlags));
VERIFY_SUCCEEDED(wslSupport->SetDistributionConfiguration(LXSS_DISTRO_NAME_TEST_L, TestUid, WslFlags));
auto cleanup = wil::scope_exit([&] {
try
{
VERIFY_SUCCEEDED(wslSupport->SetDistributionConfiguration(LXSS_DISTRO_NAME_TEST_L, DefaultUid, WslFlags));
}
catch (...)
{
LogError("Error while restoring default user");
}
});
//
// Create a new file using the 9p server.
//
const std::wstring Path = L"\\\\wsl.localhost\\" LXSS_DISTRO_NAME_TEST_L L"\\data\\test\\default_user_test";
const wil::unique_hfile File(CreateFile(
Path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL));
if (!File)
{
LogError("Failed to create file, error=%lu", GetLastError());
VERIFY_FAIL();
}
//
// Ensure the new file was created with the correct uid.
//
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(L"stat -c %U /data/test/default_user_test | grep -iF kerneltest", nullptr, nullptr, nullptr, nullptr), 0u);
}
TEST_METHOD(Execve)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests execve", L"Execve"));
}
TEST_METHOD(Xattr)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests xattr", L"xattr"));
}
TEST_METHOD(Namespace)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests namespace", L"Namespace"));
}
TEST_METHOD(BinFmt)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests binfmt", L"BinFmt"));
//
// Perform a shutdown since the binfmt test modifies the binfmt config.
//
WslShutdown();
}
TEST_METHOD(Cgroup)
{
//
// For WSL1, run the cgroup unit test. For WSL2, ensure the cgroupv2 filesystem is mounted in the expected location.
//
if (!LxsstuVmMode())
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests cgroup", L"cgroup"));
}
else
{
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(
L"mount | grep -iF 'cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)'", nullptr, nullptr, nullptr, nullptr),
0u);
}
}
TEST_METHOD(Netlink)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests netlink", L"Netlink"));
}
TEST_METHOD(Random)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests random", L"random"));
}
TEST_METHOD(Keymgmt)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests keymgmt", L"Keymgmt"));
}
TEST_METHOD(Shm)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests shm", L"shm"));
}
TEST_METHOD(Sem)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sem", L"sem"));
}
TEST_METHOD(Ttys)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests ttys", L"Ttys"));
}
TEST_METHOD(OverlayFs)
{
WSL1_TEST_ONLY();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests overlayfs", L"OverlayFs"));
}
TEST_METHOD(Auxv)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests auxv", L"auxv"));
}
TEST_METHOD(WslInfo)
{
if (LxsstuVmMode())
{
// Ensure the `-n` option to not print newline works by validating newline counts.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | wc -l | grep 1"), 0u);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode -n | wc -l | grep 0"), 0u);
// Ensure various wslinfo functionally works as expected.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'nat'"), 0u);
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::None}));
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'none'"), 0u);
if (AreExperimentalNetworkingFeaturesSupported() && IsHyperVFirewallSupported())
{
config.Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'mirrored'"), 0u);
}
for (const auto enabled : {true, false})
{
config.Update(LxssGenerateTestConfig({.guiApplications = enabled}));
#ifdef WSL_DEV_INSTALL_PATH
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(L"wslinfo --msal-proxy-path | grep -iF $(wslpath '{}')", TEXT(WSL_DEV_INSTALL_PATH))), 0u);
#else
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --msal-proxy-path | grep -iF '/mnt/c/Program Files/WSL/msal.wsl.proxy.exe'"), 0u);
#endif
}
}
else
{
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'wsl1'"), 0u);
}
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"wslinfo --version");
VERIFY_ARE_EQUAL(out, std::format(L"{}\n", WSL_PACKAGE_VERSION));
VERIFY_ARE_EQUAL(err, L"");
}
{
// Ensure the old version query command still works.
const auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"wslinfo --wsl-version");
VERIFY_ARE_EQUAL(out, std::format(L"{}\n", WSL_PACKAGE_VERSION));
VERIFY_ARE_EQUAL(err, L"");
}
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"wslinfo --invalid", 1);
VERIFY_ARE_EQUAL(out, L"");
VERIFY_ARE_EQUAL(
err,
L"Invalid command line argument: --invalid\nPlease use 'wslinfo --help' to get a list of supported "
L"arguments.\n");
}
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"wslinfo --vm-id -n");
VERIFY_ARE_EQUAL(err, L"");
if (LxsstuVmMode())
{
// Ensure that the response from wslinfo has the VM ID.
auto guid = wsl::shared::string::ToGuid(out);
VERIFY_IS_TRUE(guid.has_value());
VERIFY_IS_FALSE(IsEqualGUID(guid.value(), GUID_NULL));
// Validate that the VM ID is not propagated to user commands.
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(L"echo -n \"$WSL2_VM_ID\"");
VERIFY_ARE_EQUAL(out, L"");
VERIFY_ARE_EQUAL(err, L"");
}
else
{
VERIFY_ARE_EQUAL(out, L"wsl1");
}
}
}
TEST_METHOD(WslPath)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests wslpath", L"wslpath"));
}
TEST_METHOD(FsTab)
{
//
// Revert the fstab file and restart the instance so everything is back in
// the default state after this test.
//
auto cleanup = wil::scope_exit([&] {
try
{
LxsstuLaunchWsl(LXSST_FSTAB_CLEANUP_COMMAND_LINE);
TerminateDistribution();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/bin/true"), 0u);
}
catch (...)
{
LogError("Error while cleaning up the fstab");
}
});
//
// Create an entry in the /etc/fstab file to explicitly mount C:.
//
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(LXSST_FSTAB_BACKUP_COMMAND_LINE));
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(LXSST_FSTAB_SETUP_COMMAND_LINE));
TerminateDistribution();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/bin/true"), 0u);
//
// The test will make sure /mnt/c is mounted with the options specified in
// /etc/fstab, and that it's mounted only once.
//
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests fstab", L"fstab"));
}
TEST_METHOD(X11SocketOverTmpMount)
{
if (!LxsstuVmMode())
{
return;
}
auto cleanup = wil::scope_exit([&] {
try
{
LxsstuLaunchWsl(LXSST_FSTAB_CLEANUP_COMMAND_LINE);
TerminateDistribution();
}
catch (...)
{
LogError("Error while cleaning up the fstab");
}
});
WslConfigChange configChange(LxssGenerateTestConfig({.guiApplications = true}));
//
// Create an entry in the /etc/fstab file to add a tmpfs over /tmp.
//
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(LXSST_FSTAB_BACKUP_COMMAND_LINE));
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(L"echo 'tmpfs /tmp tmpfs rw,nodev,nosuid,size=50M 0 0' > /etc/fstab"));
TerminateDistribution();
auto ValidateBindMount = [](HANDLE Token) {
//
// Validate that the bind mount is present.
//
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L" mount | grep -iF 'none on /tmp/.X11-unix type tmpfs'", nullptr, nullptr, nullptr, Token), 0u);
};
//
// Verify that /tmp is mounted in both namespaces.
//
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mount | grep -iF 'tmpfs on /tmp type tmpfs'", nullptr, nullptr, nullptr, nullptr), 0u);
const auto nonElevatedToken = GetNonElevatedToken();
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(L"mount | grep -iF 'tmpfs on /tmp type tmpfs'", nullptr, nullptr, nullptr, nonElevatedToken.get()), 0u);
//
// Validate that the X11 bind mount is present and valid in both namespaces.
//
ValidateBindMount(nullptr);
ValidateBindMount(nonElevatedToken.get());
}
TEST_METHOD(ImportDistro)
{
const auto tarFileName = LXSST_IMPORT_DISTRO_TEST_DIR L"test.tar";
const auto rootfsDirectoryName = LXSST_IMPORT_DISTRO_TEST_DIR L"rootfs";
const auto vhdFileName = LXSST_IMPORT_DISTRO_TEST_DIR L"ext4.vhdx";
auto cleanup = wil::scope_exit([&] {
try
{
VERIFY_IS_TRUE(DeleteFileW(tarFileName));
VERIFY_IS_TRUE(RemoveDirectoryW(rootfsDirectoryName));
VERIFY_IS_TRUE(DeleteFileW(vhdFileName));
VERIFY_IS_TRUE(RemoveDirectoryW(LXSST_IMPORT_DISTRO_TEST_DIR));
}
catch (...)
{
LogError("Error during cleanup")
}
});
//
// Create a dummy tar file, rootfs folder, and vhdx. These will be used
// to ensure that the user cannot import a distribution over an existing one
// even if distro registration registry keys are not present.
//
VERIFY_IS_TRUE(CreateDirectoryW(LXSST_IMPORT_DISTRO_TEST_DIR, NULL));
VERIFY_IS_TRUE(CreateDirectoryW(rootfsDirectoryName, NULL));
{
const wil::unique_hfile tarFile{CreateFileW(
tarFileName, GENERIC_WRITE, (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL)};
VERIFY_IS_FALSE(!tarFile);
const wil::unique_hfile vhdFile{CreateFileW(
vhdFileName, GENERIC_WRITE, (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL)};
VERIFY_IS_FALSE(!vhdFile);
}
auto validateOutput = [](LPCWSTR commandLine, LPCWSTR expectedOutput, DWORD expectedExitCode = -1) {
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(commandLine, expectedExitCode);
VERIFY_ARE_EQUAL(expectedOutput, out);
VERIFY_ARE_EQUAL(L"", err);
};
auto version = LxsstuVmMode() ? 2 : 1;
auto commandLine = std::format(L"--import dummy {} {} --version {}", LXSST_IMPORT_DISTRO_TEST_DIR, tarFileName, version);
validateOutput(
commandLine.c_str(),
L"The supplied install location is already in use.\r\n"
L"Error code: Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS\r\n");
commandLine = std::format(L"--import dummy {} {} --version {}", LXSST_IMPORT_DISTRO_TEST_DIR, vhdFileName, version);
validateOutput(commandLine.c_str(), L"This looks like a VHD file. Use --vhd to import a VHD instead of a tar.\r\n");
if (!LxsstuVmMode())
{
commandLine = std::format(L"--import dummy {} {} --vhd --version 1", LXSST_IMPORT_DISTRO_TEST_DIR, vhdFileName);
validateOutput(
commandLine.c_str(),
L"This operation is only supported by WSL2.\r\n"
L"Error code: Wsl/Service/RegisterDistro/WSL_E_WSL2_NEEDED\r\n");
}
//
// Create and import a new distro that where /bin/sh is an absolute symlink.
//
auto newDistroName = L"symlink_distro";
auto newDistroTar = L"symlink_distro.tar";
validateOutput(
std::format(L"--export {} {}", LXSS_DISTRO_NAME_TEST_L, newDistroTar).c_str(),
L"The operation completed successfully. \r\n",
0);
auto deleteNewDistro = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
VERIFY_IS_TRUE(DeleteFileW(newDistroTar));
LxsstuLaunchWsl(std::format(L"--unregister {}", newDistroName));
});
validateOutput(
std::format(L"--import {} . {} --version {}", newDistroName, newDistroTar, version).c_str(),
L"The operation completed successfully. \r\n",
0);
validateOutput(std::format(L"-d {} -- ln -f -s /bin/bash /bin/sh", newDistroName).c_str(), L"", 0);
validateOutput(
std::format(L"--export {} {}", newDistroName, newDistroTar).c_str(), L"The operation completed successfully. \r\n", 0);
validateOutput(std::format(L"--unregister {}", newDistroName).c_str(), L"The operation completed successfully. \r\n", 0);
validateOutput(
std::format(L"--import {} . {} --version {}", newDistroName, newDistroTar, version).c_str(),
L"The operation completed successfully. \r\n",
0);
}
TEST_METHOD(ImportDistroInvalidTar)
{
const auto commandLine = std::format(
L"--import dummy {} C:\\windows\\system32\\drivers\\etc\\hosts --version {}", LXSST_IMPORT_DISTRO_TEST_DIR, LxsstuVmMode() ? 2 : 1);
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(commandLine.c_str(), -1);
VERIFY_ARE_EQUAL(
out, L"Importing the distribution failed.\r\nError code: Wsl/Service/RegisterDistro/WSL_E_IMPORT_FAILED\r\n");
VERIFY_ARE_EQUAL(err, L"bsdtar: Error opening archive: Unrecognized archive format\n");
}
TEST_METHOD(AppxDistroDeletion)
{
// Create a dummy distro registration
const auto key = wsl::windows::common::registry::CreateKey(
HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\\{baa405ef-1822-4bbe-84e2-30e4c6330d41}");
wsl::windows::common::registry::WriteDword(key.get(), nullptr, L"State", 1);
wsl::windows::common::registry::WriteString(key.get(), nullptr, L"DistributionName", L"DistroToBeDeleted");
wsl::windows::common::registry::WriteString(
key.get(), nullptr, L"PackageFamilyName", L"Microsoft.AppThatIsntInstalledForSure.1.0.0.0_8wekyb3d8bbwe");
wsl::windows::common::registry::WriteDword(key.get(), nullptr, L"Version", 2);
const auto vhdDir = std::filesystem::current_path();
wsl::windows::common::registry::WriteString(key.get(), nullptr, L"BasePath", vhdDir.c_str());
wsl::windows::common::registry::WriteDword(key.get(), nullptr, L"DefaultUid", 0);
wsl::windows::common::registry::WriteDword(key.get(), nullptr, L"Flags", LXSS_DISTRO_FLAGS_VM_MODE);
// Create a dummy vhd
const auto vhdPath = vhdDir.string() + "\\ext4.vhdx";
wil::unique_handle vhdHandle(CreateFileA(vhdPath.c_str(), GENERIC_READ, 0, nullptr, CREATE_ALWAYS, 0, nullptr));
VERIFY_IS_TRUE(vhdHandle.is_valid());
vhdHandle.reset();
wsl::windows::common::SvcComm service;
auto isDistroListed = [&]() {
auto distros = service.EnumerateDistributions();
return std::find_if(distros.begin(), distros.end(), [&](const auto& e) {
return wsl::shared::string::IsEqual(e.DistroName, L"DistroToBeDeleted", false);
}) != distros.end();
};
// The distro should still be there, because the vhd exists.
VERIFY_IS_TRUE(isDistroListed());
// Delete the VHD
VERIFY_IS_TRUE(DeleteFileA(vhdPath.c_str()));
// Now the distro should be deleted.
VERIFY_IS_FALSE(isDistroListed());
}
// Validate that the default distribution is correctly displayed
TEST_METHOD(DefaultDistro)
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--list");
VERIFY_IS_TRUE(out.find(std::format(L"{} (Default)", LXSS_DISTRO_NAME_TEST_L)) != std::wstring::npos);
VERIFY_ARE_EQUAL(err, L"");
}
// TODO: Add test coverage for the Linux => Windows code paths of $WSLENV
TEST_METHOD(WslEnv)
{
auto validateEnv = [&](const std::map<std::wstring, std::wstring>& inputVariables,
const std::map<std::wstring, std::wstring>& expectedOutput) {
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
for (const auto& e : inputVariables)
{
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(e.first.c_str(), nullptr));
}
});
for (const auto& e : inputVariables)
{
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(e.first.c_str(), e.second.c_str()));
}
for (const auto& e : expectedOutput)
{
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"echo -n $" + e.first);
VERIFY_ARE_EQUAL(e.second, output);
}
};
validateEnv({{L"a", L"b"}, {L"c", L"d"}, {L"WSLENV", L"a/u:c/u"}}, {{L"a", L"b"}, {L"c", L"d"}});
validateEnv(
{{L"a", L"C:\\Users"}, {L"b", L"C:\\Users"}, {L"WSLENV", L"a/l:b/p"}},
{{L"a", L"/mnt/c/Users"}, {L"b", L"/mnt/c/Users"}});
validateEnv(
{{L"a", L"C:\\Users;C:\\Windows"},
{L"b", L"C:\\Users;C:\\Windows"},
{L"c", L"C:\\Users;C:\\Windows"},
{L"d", L"C:\\Users;C:\\Windows"},
{L"WSLENV", L"a/l:b/p:c/pl:d/lp"}},
{{L"a", L"/mnt/c/Users:/mnt/c/Windows"},
{L"b", L"/mnt/c/Users:/mnt/c/Windows"},
{L"c", L"/mnt/c/Users:/mnt/c/Windows"},
{L"d", L"/mnt/c/Users:/mnt/c/Windows"}});
validateEnv(
{{L"a", L"C:\\Users;C:\\Windows\\System32"}, {L"b", L"C:\\Users;C:\\Windows"}, {L"WSLENV", L"a/l:b/l:a/l"}},
{{L"a", L"/mnt/c/Users:/mnt/c/Windows/System32"}, {L"b", L"/mnt/c/Users:/mnt/c/Windows"}});
validateEnv(
{{L"a", L"C:\\Users;C:\\Windows\\System32"}, {L"b", L"C:\\Users;C:\\Windows"}, {L"WSLENV", L"a/u:b/u:a/u"}},
{{L"a", L"C:\\Users;C:\\Windows\\System32"}, {L"b", L"C:\\Users;C:\\Windows"}});
validateEnv({{L"a", L"C:\\Users;C:\\Windows\\System32"}, {L"WSLENV", L"a/w"}}, {{L"a", L""}});
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"Empty", nullptr));
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"WSLENV", nullptr));
});
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"Empty", L""));
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"WSLENV", L"Empty/u"));
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"[ -z ${Empty+x} ]"), (DWORD)1);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"[ -z ${SanityCheck+x} ]"), (DWORD)0);
}
static void ValidateErrorMessage(
const std::wstring& Cmd,
const std::wstring& Message,
const std::wstring& Code,
const std::optional<std::wstring>& ExtraConfig = {},
LPCWSTR EntryPoint = WSL_BINARY_NAME,
bool ignoreCasing = false)
{
std::optional<std::wstring> previousConfig;
if (ExtraConfig.has_value())
{
previousConfig = LxssWriteWslConfig(L"[wsl2]\n" + ExtraConfig.value());
RestartWslService();
}
auto revertConfig = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
if (previousConfig.has_value())
{
LxssWriteWslConfig(previousConfig.value());
RestartWslService();
};
});
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(
Cmd.c_str(), wcscmp(EntryPoint, L"bash.exe") == 0 ? 1 : -1, nullptr, nullptr, EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, EntryPoint);
const auto expectedOutput = Message + L"\r\nError code: " + Code + L"\r\n";
if (!wsl::shared::string::IsEqual(output, expectedOutput, ignoreCasing))
{
LogError("Expected error message: '%ls', actual error message: '%ls'", expectedOutput.c_str(), output.c_str());
VERIFY_FAIL();
}
}
static void VerifyOutput(const std::wstring& Cmd, const std::wstring& ExpectedOutput, int ExpectedExitCode = 0, LPCWSTR EntryPoint = WSL_BINARY_NAME)
{
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(
Cmd.c_str(), ExpectedExitCode, nullptr, nullptr, EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, EntryPoint);
VERIFY_ARE_EQUAL(output, ExpectedOutput);
}
TEST_METHOD(ErrorMessages)
{
if (LxsstuVmMode()) // wsl --mount and bridged networking only exist in WSL2.
{
if (!wsl::shared::Arm64 && wsl::windows::common::helpers::GetWindowsVersion().BuildNumber >= 27653)
{
ValidateErrorMessage(
L"--mount DoesNotExist",
L"Failed to attach disk 'DoesNotExist' to WSL2: The system cannot find the file specified. ",
L"Wsl/Service/AttachDisk/MountDisk/HCS/ERROR_FILE_NOT_FOUND");
}
ValidateErrorMessage(
L"--unmount DoesNotExist",
GetSystemErrorString(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)),
L"Wsl/Service/DetachDisk/ERROR_FILE_NOT_FOUND");
ValidateErrorMessage(
WSL_MANAGE_ARG L" " LXSS_DISTRO_NAME_TEST L" " WSL_MANAGE_ARG_SET_SPARSE_OPTION_LONG L" false_",
L"false_ is not a valid boolean, <true|false>",
L"Wsl/E_INVALIDARG");
const std::wstring wslConfigPath = wsl::windows::common::helpers::GetWslConfigPath();
{
// Create a distro registration pointing to a vhdx that doesn't exist and validate that the error message reports that correctly.
const auto userKey = wsl::windows::common::registry::OpenLxssUserKey();
const auto distroKey =
wsl::windows::common::registry::CreateKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
auto revert = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
wsl::windows::common::registry::DeleteKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
});
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"BasePath", L"C:\\DoesNotExit");
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"DistributionName", L"DummyBrokenDistro");
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"DefaultUid", 0);
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Version", LXSS_DISTRO_VERSION_2);
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"State", LxssDistributionStateInstalled);
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Flags", LXSS_DISTRO_FLAGS_VM_MODE);
ValidateErrorMessage(
L"-d DummyBrokenDistro",
L"Failed to attach disk 'C:\\DoesNotExit\\ext4.vhdx' to WSL2: The system cannot find the path "
L"specified. ",
L"Wsl/Service/CreateInstance/MountDisk/HCS/ERROR_PATH_NOT_FOUND");
// Purposefully set an incorrect value type to validate registry error handling.
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"Version", L"Broken");
const auto tokenInfo = wil::get_token_information<TOKEN_USER>();
const auto Sid = std::wstring(wsl::windows::common::wslutil::SidToString(tokenInfo->User.Sid).get());
// N.B. casing is ignored because the 'Software' key is sometimes uppercase, sometimes not.
ValidateErrorMessage(
L"-d DummyBrokenDistro",
L"An error occurred accessing the registry. Path: '\\REGISTRY\\USER\\" + Sid +
L"\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\\{baa405ef-1822-4bbe-84e2-30e4c6330d42}"
L"\\Version'."
L" "
L"Error: Data of this type is not supported. ",
L"Wsl/Service/ReadDistroConfig/ERROR_UNSUPPORTED_TYPE",
{},
L"wsl.exe",
true);
}
ValidateErrorMessage(
L"echo ok",
std::format(L"Invalid mac address 'foo' for key 'wsl2.macAddress' in {}:2", wslConfigPath),
L"Wsl/Service/CreateInstance/CreateVm/ParseConfig/E_INVALIDARG",
L"macAddress=foo");
}
else
{
// wsl.exe --manage --resize requires WSL2.
ValidateErrorMessage(
L"--manage test_distro --resize 10GB",
L"This operation is only supported by WSL2.",
L"Wsl/Service/WSL_E_WSL2_NEEDED");
}
ValidateErrorMessage(
L"--import a b c", GetSystemErrorString(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)), L"Wsl/ERROR_FILE_NOT_FOUND");
ValidateErrorMessage(
L"-d DoesNotExist echo foo",
L"There is no distribution with the supplied name.",
L"Wsl/Service/WSL_E_DISTRO_NOT_FOUND");
ValidateErrorMessage(
L"--export DoesNotExist FileName",
L"There is no distribution with the supplied name.",
L"Wsl/Service/WSL_E_DISTRO_NOT_FOUND");
ValidateErrorMessage(
L"--import-in-place DoesNotExist FileName",
GetSystemErrorString(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)),
L"Wsl/ERROR_FILE_NOT_FOUND");
ValidateErrorMessage(
L"--set-default-version 3",
GetSystemErrorString(HRESULT_FROM_WIN32(ERROR_VERSION_PARSE_ERROR)),
L"Wsl/ERROR_VERSION_PARSE_ERROR");
ValidateErrorMessage(
L"--manage DoesNotExist --resize 10GB",
L"There is no distribution with the supplied name.",
L"Wsl/Service/WSL_E_DISTRO_NOT_FOUND");
ValidateErrorMessage(L"--manage test_distro --resize foo", L"Invalid size: foo", L"Wsl/E_INVALIDARG");
ValidateErrorMessage(
L"--install --distribution debian --no-distribution",
L"Arguments --no-distribution and --distribution can't be specified at same time.",
L"Wsl/E_INVALIDARG");
ValidateErrorMessage(
L"--install debian --from-file foo --distribution foo",
L"Arguments --from-file and --distribution can't be specified at same time.",
L"Wsl/E_INVALIDARG");
ValidateErrorMessage(
L"--install foo --fixed-vhd", L"Argument --fixed-vhd requires the --vhd-size argument.", L"Wsl/E_INVALIDARG");
{
UniqueWebServer server(c_testDistributionEndpoint, c_testDistributionJson);
RegistryKeyChange<std::wstring> keyChange(
HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::distribution::c_distroUrlRegistryValue, c_testDistributionEndpoint);
ValidateErrorMessage(
L"--install -d DoesNotExist",
L"Invalid distribution name: 'DoesNotExist'.\r\nTo get a list of valid distributions, use 'wsl.exe --list "
L"--online'.",
L"Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND");
}
{
const auto lxssKey = wsl::windows::common::registry::OpenLxssMachineKey(KEY_READ | KEY_SET_VALUE);
std::optional<std::wstring> revertValue;
try
{
revertValue = wsl::windows::common::registry::ReadString(
lxssKey.get(), nullptr, wsl::windows::common::distribution::c_distroUrlRegistryValue);
}
catch (...)
{
// Expected if the value isn't set
}
auto revert = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
if (revertValue.has_value())
{
wsl::windows::common::registry::WriteString(
lxssKey.get(), nullptr, wsl::windows::common::distribution::c_distroUrlRegistryValue, revertValue->c_str());
}
else
{
wsl::windows::common::registry::DeleteValue(lxssKey.get(), wsl::windows::common::distribution::c_distroUrlRegistryValue);
}
});
wsl::windows::common::registry::WriteString(
lxssKey.get(), nullptr, wsl::windows::common::distribution::c_distroUrlRegistryValue, L"http://127.0.0.1:6666");
ValidateErrorMessage(
L"--install -d ubuntu",
L"Failed to fetch the list distribution from 'http://127.0.0.1:6666'. " +
GetSystemErrorString(HRESULT_FROM_WIN32(WININET_E_CANNOT_CONNECT)),
L"Wsl/InstallDistro/WININET_E_CANNOT_CONNECT");
ValidateErrorMessage(
L"--list --online",
L"Failed to fetch the list distribution from 'http://127.0.0.1:6666'. " +
GetSystemErrorString(HRESULT_FROM_WIN32(WININET_E_CANNOT_CONNECT)),
L"Wsl/WININET_E_CANNOT_CONNECT");
}
ValidateErrorMessage(
L"/u foo",
L"There is no distribution with the supplied name.",
L"WslConfig/Service/WSL_E_DISTRO_NOT_FOUND",
{},
L"wslconfig.exe");
ValidateErrorMessage(
L"e7bef681-c148-4687-8a0f-8c8be93bac93", // GUID for a distro that's not installed.
L"There is no distribution with the supplied name.",
L"Bash/Service/CreateInstance/ReadDistroConfig/WSL_E_DISTRO_NOT_FOUND",
{},
L"bash.exe");
VerifyOutput(L"--install --no-distribution", L"The operation completed successfully. \r\n");
{
std::wstring expectedUsageMessage;
for (auto e : wsl::shared::Localization::MessageWslUsage())
{
if (e == L'\n')
{
expectedUsageMessage += L'\r';
}
expectedUsageMessage += e;
}
VerifyOutput(L"--manage --move .", expectedUsageMessage + L"\r\n", -1);
}
}
TEST_METHOD(CommandLineParsing)
{
VerifyOutput(L"echo -n \\\"", L"\"");
VerifyOutput(L"echo -n \\\'", L"\'");
VerifyOutput(L"echo -n \" \"", L" ");
VerifyOutput(L"echo -n $USER", L"root");
VerifyOutput(L"echo -n \"$USER\"", L"root");
VerifyOutput(L"echo -n '\"$USER\"'", L"\"$USER\"");
VerifyOutput(L"echo -n '\\\"$USER\\\"'", L"\\\"$USER\\\"");
VerifyOutput(L"echo -n '$USER'", L"$USER");
VerifyOutput(L"echo -n a \" \" b", L"a b");
VerifyOutput(L"echo -n a \"\" b", L"a b");
VerifyOutput(L"echo -n a b \"\"", L"a b ");
VerifyOutput(L"echo -n \"a\"\"b\"", L"ab");
VerifyOutput(L"--exec echo -n \"a\"", L"a");
VerifyOutput(L"--exec echo -n $USER", L"$USER");
VerifyOutput(L"--exec echo -n \\\"a\\\"", L"\"a\"");
VerifyOutput(L"--exec echo -n \\\"a\\\"", L"\"a\"");
VerifyOutput(L"--exec echo -n \"a\"\"b\"", L"a\"b");
VerifyOutput(L"--exec echo -n \\\"", L"\"");
}
// This test validates that the help messages for wsl.exe and wsl.config are correctly displayed.
// Notes:
// - This test will fail if the help messages are changed. If that's the case, simply update the below strings
// - This test assumes that English is the configured language.
TEST_METHOD(UsageMessages)
{
const std::wstring WslHelpMessage =
LR"""(Copyright (c) Microsoft Corporation. All rights reserved.
For privacy information about this product please visit https://aka.ms/privacy.
Usage: wsl.exe [Argument] [Options...] [CommandLine]
Arguments for running Linux binaries:
If no command line is provided, wsl.exe launches the default shell.
--exec, -e <CommandLine>
Execute the specified command without using the default Linux shell.
--shell-type <standard|login|none>
Execute the specified command with the provided shell type.
--
Pass the remaining command line as-is.
Options:
--cd <Directory>
Sets the specified directory as the current working directory.
If ~ is used the Linux user's home path will be used. If the path begins
with a / character, it will be interpreted as an absolute Linux path.
Otherwise, the value must be an absolute Windows path.
--distribution, -d <DistroName>
Run the specified distribution.
--distribution-id <DistroGuid>
Run the specified distribution ID.
--user, -u <UserName>
Run as the specified user.
--system
Launches a shell for the system distribution.
Arguments for managing Windows Subsystem for Linux:
--help
Display usage information.
--debug-shell
Open a WSL2 debug shell for diagnostics purposes.
--install [Distro] [Options...]
Install a Windows Subsystem for Linux distribution.
For a list of valid distributions, use 'wsl.exe --list --online'.
Options:
--enable-wsl1
Enable WSL1 support.
--fixed-vhd
Create a fixed-size disk to store the distribution.
--from-file <Path>
Install a distribution from a local file.
--legacy
Use the legacy distribution manifest.
--location <Location>
Set the install path for the distribution.
--name <Name>
Set the name of the distribution.
--no-distribution
Only install the required optional components, does not install a distribution.
--no-launch, -n
Do not launch the distribution after install.
--version <Version>
Specifies the version to use for the new distribution.
--vhd-size <MemoryString>
Specifies the size of the disk to store the distribution.
--web-download
Download the distribution from the internet instead of the Microsoft Store.
--manage <Distro> <Options...>
Changes distro specific options.
Options:
--move <Location>
Move the distribution to a new location.
--set-sparse, -s <true|false>
Set the VHD of distro to be sparse, allowing disk space to be automatically reclaimed.
--set-default-user <Username>
Set the default user of the distribution.
--resize <MemoryString>
Resize the disk of the distribution to the specified size.
--mount <Disk>
Attaches and mounts a physical or virtual disk in all WSL 2 distributions.
Options:
--vhd
Specifies that <Disk> refers to a virtual hard disk.
--bare
Attach the disk to WSL2, but don't mount it.
--name <Name>
Mount the disk using a custom name for the mountpoint.
--type <Type>
Filesystem to use when mounting a disk, if not specified defaults to ext4.
--options <Options>
Additional mount options.
--partition <Index>
Index of the partition to mount, if not specified defaults to the whole disk.
--set-default-version <Version>
Changes the default install version for new distributions.
--shutdown
Immediately terminates all running distributions and the WSL 2
lightweight utility virtual machine.
Options:
--force
Terminate the WSL 2 virtual machine even if an operation is in progress. Can cause data loss.
--status
Show the status of Windows Subsystem for Linux.
--unmount [Disk]
Unmounts and detaches a disk from all WSL2 distributions.
Unmounts and detaches all disks if called without argument.
--uninstall
Uninstalls the Windows Subsystem for Linux package from this machine.
--update
Update the Windows Subsystem for Linux package.
Options:
--pre-release
Download a pre-release version if available.
--version, -v
Display version information.
Arguments for managing distributions in Windows Subsystem for Linux:
--export <Distro> <FileName> [Options]
Exports the distribution to a tar file.
The filename can be - for stdout.
Options:
--format <Format>
Specifies the export format. Supported values: tar, tar.gz, tar.xz, vhd.
--import <Distro> <InstallLocation> <FileName> [Options]
Imports the specified tar file as a new distribution.
The filename can be - for stdin.
Options:
--version <Version>
Specifies the version to use for the new distribution.
--vhd
Specifies that the provided file is a .vhd or .vhdx file, not a tar file.
This operation makes a copy of the VHD file at the specified install location.
--import-in-place <Distro> <FileName>
Imports the specified VHD file as a new distribution.
This virtual hard disk must be formatted with the ext4 filesystem type.
--list, -l [Options]
Lists distributions.
Options:
--all
List all distributions, including distributions that are
currently being installed or uninstalled.
--running
List only distributions that are currently running.
--quiet, -q
Only show distribution names.
--verbose, -v
Show detailed information about all distributions.
--online, -o
Displays a list of available distributions for install with 'wsl.exe --install'.
--set-default, -s <Distro>
Sets the distribution as the default.
--set-version <Distro> <Version>
Changes the version of the specified distribution.
--terminate, -t <Distro>
Terminates the specified distribution.
--unregister <Distro>
Unregisters the distribution and deletes the root filesystem.
)""";
const std::wstring WslConfigHelpMessage =
LR"""(Performs administrative operations on Windows Subsystem for Linux
Usage:
/l, /list [Option]
Lists registered distributions.
/all - Optionally list all distributions, including distributions that
are currently being installed or uninstalled.
/running - List only distributions that are currently running.
/s, /setdefault <DistributionName>
Sets the distribution as the default.
/t, /terminate <DistributionName>
Terminates the distribution.
/u, /unregister <DistributionName>
Unregisters the distribution and deletes the root filesystem.
)""";
const std::wstring WslInstallHelpMessage =
LR"""(Invalid distribution name: 'foo'.
To get a list of valid distributions, use 'wsl.exe --list --online'.
Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND
)""";
auto AddCrlf = [](const std::wstring& Input) {
std::wstring MessageWithCrlf;
for (const auto e : Input)
{
if (e == '\n')
{
MessageWithCrlf += '\r';
}
MessageWithCrlf += e;
}
return MessageWithCrlf;
};
// Note: There is no easy way to validate wslg's help message, since it displays a blocking
// message box before exiting.
VerifyOutput(L"--help", AddCrlf(WslHelpMessage), -1);
VerifyOutput(L"--help", AddCrlf(WslConfigHelpMessage), -1, L"wslconfig.exe");
UniqueWebServer server(c_testDistributionEndpoint, c_testDistributionJson);
RegistryKeyChange<std::wstring> keyChange(
HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::distribution::c_distroUrlRegistryValue, c_testDistributionEndpoint);
VerifyOutput(L"--install foo", AddCrlf(WslInstallHelpMessage), -1);
}
TEST_METHOD(TestExistingSwapVhd)
{
WSL2_TEST_ONLY();
// Create a 100MB swap vhdx.
auto swapVhd = wil::GetCurrentDirectoryW<std::wstring>() + L"\\TestSwap.vhdx";
VIRTUAL_STORAGE_TYPE storageType{};
storageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX;
storageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT;
CREATE_VIRTUAL_DISK_PARAMETERS createVhdParameters{};
createVhdParameters.Version = CREATE_VIRTUAL_DISK_VERSION_2;
createVhdParameters.Version2.BlockSizeInBytes = 1024 * 1024;
createVhdParameters.Version2.MaximumSize = 100 * 1024 * 1024;
wil::unique_hfile vhd{};
VERIFY_ARE_EQUAL(
::CreateVirtualDisk(
&storageType, swapVhd.c_str(), VIRTUAL_DISK_ACCESS_NONE, nullptr, CREATE_VIRTUAL_DISK_FLAG_SUPPORT_COMPRESSED_VOLUMES, 0, &createVhdParameters, nullptr, &vhd),
0l);
vhd.reset();
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
WslShutdown();
DeleteFile(swapVhd.c_str());
});
// Update .wslconfig. Update the swapVhd path to replace single backslash
// with double backslashes so as to be compatible with .wslconfig parsing.
// The following regex replacement only works as intended if the path contains
// single backslashes. Negative lookahead can be used to handle paths with double
// backslashes but then the negative lookbehind case should also be used but the
// latter is not supported in std::regex.
swapVhd = std::regex_replace(swapVhd, std::wregex(L"\\\\"), L"\\\\");
WslConfigChange configChange(LxssGenerateTestConfig() + L"\nswap=256MB\nswapFile=" + swapVhd);
auto validateSwapSize = [](LPCWSTR Expected) {
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"swapon | awk 'END {print $3}'");
VERIFY_ARE_EQUAL(Expected + std::wstring(L"\n"), output);
};
validateSwapSize(L"256M");
// Validate that the vhdx is resized correctly if the swap size changes
configChange.Update(LxssGenerateTestConfig() + L"\nswap=200MB\nswapFile=" + swapVhd);
validateSwapSize(L"200M");
}
TEST_METHOD(InitDoesntBlockSignals)
{
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"grep -iF SigBlk < /proc/1/status");
VERIFY_ARE_EQUAL(L"SigBlk:\t0000000000000000\n", output);
}
TEST_METHOD(InitReadonly)
{
WSL2_TEST_ONLY();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L" grep '^rootfs /init rootfs ro,' /proc/self/mounts", nullptr, nullptr, nullptr, nullptr), 0u);
}
TEST_METHOD(GpuMounts)
{
WSL2_TEST_ONLY();
auto ValidateGpuMounts = [](HANDLE Token) {
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(
L"mount | grep -iF 'none on /usr/lib/wsl/lib type overlay (rw,nosuid,nodev,noatime,lowerdir=/gpu_" TEXT(LXSS_GPU_PACKAGED_LIB_SHARE) L":/gpu_" TEXT(
LXSS_GPU_INBOX_LIB_SHARE) L",upperdir=/gpu_lib/rw/upper,workdir=/gpu_lib/rw/work,uuid=on)'",
nullptr,
nullptr,
nullptr,
Token),
0u);
// Ensure the lib directory is writable.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L" touch /usr/lib/wsl/lib/foo && rm /usr/lib/wsl/lib/foo", nullptr, nullptr, nullptr, Token), 0u);
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(
L"mount | grep -iF '" TEXT(
LXSS_GPU_DRIVERS_SHARE) L" on /usr/lib/wsl/drivers type 9p (ro,nosuid,nodev,noatime,aname=" TEXT(LXSS_GPU_DRIVERS_SHARE) L";fmask=222;dmask=222,cache=5,access=client,msize=65536,trans=fd,rfd=8,wfd=8)'",
nullptr,
nullptr,
nullptr,
Token),
0u);
};
auto cleanUp = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
// Validate that GPU mounts are present in both namespaces.
const auto nonElevatedToken = GetNonElevatedToken();
WslShutdown();
ValidateGpuMounts(nullptr);
ValidateGpuMounts(nonElevatedToken.get());
// Create a new instance with a non-elevated token as the creator.
WslShutdown();
ValidateGpuMounts(nonElevatedToken.get());
ValidateGpuMounts(nullptr);
}
TEST_METHOD(InteropCornerCases)
{
auto validateInterop = [](const std::wstring& binaryName) {
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(L"rm /tmp/'" + binaryName + L"'"); });
// The "|| echo fail" part is needed because bash will exec instead of forking() of only one non-builtin command is passed.
// If bash exec's then this test is useless since the binfmt interpreter would not be a child of a process with a weird name.
const std::wstring commandLine =
L"cp /bin/bash /tmp/'" + binaryName + L"' && '/tmp/" + binaryName +
L"' -c 'export WSL_INTEROP=\"\" && echo -n $WSL_INTEROP && cmd.exe /c \"echo ok\" || echo fail'";
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(commandLine);
VERIFY_ARE_EQUAL(output, L"ok\r\n");
};
validateInterop(L"bash with spaces");
validateInterop(L"bash )");
validateInterop(L"bash (");
validateInterop(L"(bash)");
validateInterop(L"(bash(");
validateInterop(L"()");
validateInterop(L"(");
validateInterop(L")");
}
TEST_METHOD(InteropPid1)
{
// Validate that interop works as pid 1.
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"unshare -pf --wd $(dirname $(which cmd.exe)) cmd.exe /c echo ok");
VERIFY_ARE_EQUAL(output, L"ok\r\n");
}
TEST_METHOD(Hostname)
{
auto cleanup = wil::scope_exit([] {
LxsstuLaunchWsl(LXSST_REMOVE_DISTRO_CONF_COMMAND_LINE);
TerminateDistribution();
});
auto validate = [](const std::string& input, const std::wstring& expectedOutput) {
LxssWriteWslDistroConfig("[network]\nhostname=" + input);
TerminateDistribution();
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"hostname");
VERIFY_ARE_EQUAL(output, expectedOutput + L"\n");
output = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/hostname").first;
VERIFY_ARE_EQUAL(output, expectedOutput + L"\n");
};
validate("SimpleHostname", L"SimpleHostname");
validate("Simple-Hostname", L"Simple-Hostname");
validate("Simple_Hostname", L"SimpleHostname");
validate("-hostname", L"hostname");
validate("--hostname", L"hostname");
validate("hostname.-", L"hostname");
validate(".hostname", L"hostname");
validate("hostname.", L"hostname");
validate("host.name.", L"host.name");
validate("host..name", L"host.name");
validate("host|name", L"hostname");
validate(".a-", L"a");
validate(".a-b", L"a-b");
validate(".", L"localhost");
validate("-", L"localhost");
validate("-.-", L"localhost");
// Validate hostname is limited to 64 characters.
const std::string longHostName("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz");
validate(longHostName, wsl::shared::string::MultiByteToWide(longHostName.substr(0, 64)));
}
TEST_METHOD(WslConfWarnings)
{
WSL2_TEST_ONLY();
DistroFileChange configChange(L"/etc/wsl.conf", false);
auto validateWarnings = [&configChange](const std::wstring& config, const std::wstring& expectedWarnings) {
configChange.SetContent(config.c_str());
TerminateDistribution();
// This loop is here because of a race condition when starting WSL to get the warnings.
// If a p9rdr distribution startup notification arrives just before wsl.exe calls CreateInstance(),
// the warnings will be 'consumed' before wsl.exe can read them.
// To work around that, loop for up to 2 minutes while we don't get any warnings
const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(2);
while (std::chrono::steady_clock::now() < deadline)
{
auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"-u root echo ok");
VERIFY_ARE_EQUAL(L"ok\n", output);
if (!warnings.empty() || expectedWarnings.empty())
{
VERIFY_ARE_EQUAL(expectedWarnings, warnings);
return;
}
LogInfo("Received empty warnings, trying again");
WslShutdown();
}
LogError("Timed out waiting for warnings. Expected warnings: %ls", expectedWarnings.c_str());
VERIFY_FAIL();
};
validateWarnings(L"[foo]\na=b", L"wsl: Unknown key 'foo.a' in /etc/wsl.conf:2\r\n");
validateWarnings(L"a=a\\m", L"wsl: Invalid escaped character: 'm' in /etc/wsl.conf:1\r\n");
validateWarnings(L"[=b", L"wsl: Invalid section name in /etc/wsl.conf:1\r\n");
validateWarnings(L"\r\n\r\n[foo]\r\na=b", L"wsl: Unknown key 'foo.a' in /etc/wsl.conf:5\r\n");
// Validate that CRLF is correctly handled
{
configChange.SetContent(L"[network]\r\nhostname=foo\r\n");
TerminateDistribution();
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"hostname");
VERIFY_ARE_EQUAL(out, L"foo\n");
VERIFY_ARE_EQUAL(err, L"");
}
}
TEST_METHOD(Warnings)
{
WSL2_TEST_ONLY();
WslConfigChange configChange(LxssGenerateTestConfig());
auto validateWarnings = [&configChange](
const std::wstring& config,
const std::wstring& expectedWarnings,
const std::wstring& prefix = LxssGenerateTestConfig(),
bool fnmatch = false) {
WEX::Logging::Log::Comment(config.c_str());
WEX::Logging::Log::Comment(expectedWarnings.c_str());
configChange.Update(prefix + config);
// This loop is here because of a race condition when starting WSL to get the warnings.
// If a p9rdr distribution startup notification arrives just before wsl.exe calls CreateInstance(),
// the warnings will be 'consumed' before wsl.exe can read them.
// To work around that, loop for up to 2 minutes while we don't get any warnings
const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(2);
while (std::chrono::steady_clock::now() < deadline)
{
auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
VERIFY_ARE_EQUAL(L"ok\n", output);
if (!warnings.empty() || expectedWarnings.empty())
{
if (fnmatch)
{
if (!PathMatchSpec(warnings.c_str(), expectedWarnings.c_str()))
{
LogError("Warning '%ls' didn't match pattern '%ls'", warnings.c_str(), expectedWarnings.c_str());
VERIFY_FAIL();
}
}
else
{
VERIFY_ARE_EQUAL(expectedWarnings, warnings);
}
return;
}
LogInfo("Received empty warnings, trying again");
WslShutdown();
}
LogError("Timed out waiting for warnings. Expected warnings: %ls", expectedWarnings.c_str());
VERIFY_FAIL();
};
const std::wstring wslConfigPath = wsl::windows::common::helpers::GetWslConfigPath();
validateWarnings(L"a=b", std::format(L"wsl: Unknown key 'wsl2.a' in {}:21\r\n", wslConfigPath));
validateWarnings(L"[=b", std::format(L"wsl: Invalid section name in {}:21\r\n", wslConfigPath));
validateWarnings(
L"dhcpTimeout=NotANumber",
std::format(L"wsl: Invalid integer value 'NotANumber' for key 'wsl2.dhcpTimeout' in {}:21\r\n", wslConfigPath));
validateWarnings(L"ipv6=NotABoolean", std::format(L"wsl: Invalid boolean value 'NotABoolean' for key 'wsl2.ipv6' in {}:21\r\n", wslConfigPath));
validateWarnings(L"[sectionNotComplete", std::format(L"wsl: Expected ']' in {}:21\r\n", wslConfigPath));
validateWarnings(L"NoEqual", std::format(L"wsl: Expected '=' in {}:21\r\n", wslConfigPath));
validateWarnings(
L"networkingMode=InvalidMode",
std::format(L"wsl: Invalid value 'InvalidMode' for config key 'wsl2.networkingMode' in {}:2 (Valid values: Bridged, Mirrored, Nat, None, VirtioProxy)\r\n", wslConfigPath),
L"[wsl2]\n");
validateWarnings(
L"networkingMode=a\\m", std::format(L"wsl: Invalid escaped character: 'm' in {}:2\r\n", wslConfigPath), L"[wsl2]\n");
validateWarnings(
L"\nswap=200MB\nswapFile=C:\\\\DoesNotExist\\\\swap.vhdx",
L"wsl: Failed to create the swap disk in 'C:\\DoesNotExist\\swap.vhdx': The system cannot find the path "
L"specified. \r\n");
validateWarnings(L"\nswap=/", std::format(L"wsl: Invalid memory string '/' for .wslconfig entry 'wsl2.swap' in {}:22\r\n", wslConfigPath));
validateWarnings(L"\nswap=0GB", L"");
validateWarnings(L"\nswap=0foo", std::format(L"wsl: Invalid memory string '0foo' for .wslconfig entry 'wsl2.swap' in {}:22\r\n", wslConfigPath));
validateWarnings(L"safeMode=true", L"wsl: SAFE MODE ENABLED - many features will be disabled\r\n", L"[wsl2]\n");
validateWarnings(L"processors=", std::format(L"wsl: Invalid integer value '' for key 'wsl2.processors' in {}:21\r\n", wslConfigPath));
validateWarnings(L"memory=", std::format(L"wsl: Invalid memory string '' for .wslconfig entry 'wsl2.memory' in {}:21\r\n", wslConfigPath));
validateWarnings(L"debugConsole=", std::format(L"wsl: Invalid boolean value '' for key 'wsl2.debugConsole' in {}:21\r\n", wslConfigPath));
validateWarnings(
L"networkingMode=",
std::format(L"wsl: Invalid value '' for config key 'wsl2.networkingMode' in {}:21 (Valid values: Bridged, Mirrored, Nat, None, VirtioProxy)\r\n", wslConfigPath));
validateWarnings(
L"ipv6=true\nipv6=false",
std::format(L"wsl: Duplicated config key 'wsl2.ipv6' in {}:22 (Conflicting key: 'wsl2.ipv6' in {}:21)\r\n", wslConfigPath, wslConfigPath));
validateWarnings(
L"networkingMode=NAT\n[experimental]\nnetworkingMode=Mirrored",
std::format(L"wsl: Duplicated config key 'experimental.networkingMode' in {}:4 (Conflicting key: 'wsl2.networkingMode' in {}:2)\r\n", wslConfigPath, wslConfigPath),
L"[wsl2]\n");
validateWarnings(
L"networkingMode=bridged",
L"wsl: Bridged networking requires wsl2.vmSwitch to be set.\r\n"
L"Error code: CreateInstance/CreateVm/ConfigureNetworking/WSL_E_VMSWITCH_NOT_SET\r\n"
L"wsl: Failed to configure network (networkingMode Bridged), falling back to networkingMode None.\r\n",
L"[wsl2]\n");
validateWarnings(
L"networkingMode=bridged\nvmSwitch=DoesNotExist",
L"wsl: The VmSwitch 'DoesNotExist' was not found. Available switches:*\r\n"
L"Error code: CreateInstance/CreateVm/ConfigureNetworking/WSL_E_VMSWITCH_NOT_FOUND\r\n"
L"wsl: Failed to configure network (networkingMode Bridged), falling back to networkingMode None.\r\n",
L"[wsl2]\n",
true);
if (!AreExperimentalNetworkingFeaturesSupported())
{
validateWarnings(
L"[experimental]\nnetworkingMode=mirrored",
L"wsl: Experimental networking features are not supported, falling back to default settings\r\n",
L"[wsl2]\n");
validateWarnings(
L"[experimental]\ndnsTunneling=true",
L"wsl: Experimental networking features are not supported, falling back to default settings\r\n",
L"[wsl2]\n");
validateWarnings(
L"[experimental]\nfirewall=true",
L"wsl: Experimental networking features are not supported, falling back to default settings\r\n",
L"[wsl2]\n");
}
else
{
if (TryLoadDnsResolverMethods())
{
// Verify DNS tunneling settings are parsed correctly
validateWarnings(L"[experimental]\ndnsTunneling=true\nbestEffortDnsParsing=true", L"");
validateWarnings(L"[experimental]\ndnsTunneling=true\ndnsTunnelingIpAddress=10.255.255.1", L"");
validateWarnings(
L"[experimental]\ndnsTunneling=true\ndnsTunnelingIpAddress=1.2.3",
std::format(L"wsl: Invalid IP value '1.2.3' for key 'experimental.dnsTunnelingIpAddress' in {}:23\r\n", wslConfigPath));
}
}
validateWarnings(
L"[experimental]\nignoredPorts=NotANumber",
std::format(L"wsl: Invalid integer value 'NotANumber' for key 'experimental.ignoredPorts' in {}:22\r\n", wslConfigPath));
validateWarnings(
L"[experimental]\nignoredPorts=65536",
std::format(L"wsl: Invalid integer value '65536' for key 'experimental.ignoredPorts' in {}:22\r\n", wslConfigPath));
// Verify that the vhdSize setting is parsed correctly.
validateWarnings(L"[wsl2]\ndefaultVhdSize=64GB\n", L"");
auto maxProcessorCount = wsl::windows::common::wslutil::GetLogicalProcessorCount();
validateWarnings(
std::format(L"processors={}", maxProcessorCount + 1).c_str(),
std::format(L"wsl: wsl2.processors cannot exceed the number of logical processors on the system ({} > {})\r\n", maxProcessorCount + 1, maxProcessorCount));
// Exclusively open .wslconfig to make it unreadable
const wil::unique_handle wslConfig{
CreateFile(wslConfigPath.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
VERIFY_IS_NOT_NULL(wslConfig);
WslShutdown();
auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
VERIFY_ARE_EQUAL(L"ok\n", output);
VERIFY_ARE_EQUAL(
std::format(L"wsl: Failed to open config file {}, The process cannot access the file because it is being used by another process. \r\n", wslConfigPath),
warnings);
{
DistroFileChange fstab(L"/etc/fstab");
fstab.SetContent(L"invalid fs tab content");
TerminateDistribution();
std::tie(output, warnings) = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
VERIFY_ARE_EQUAL(L"ok\n", output);
VERIFY_ARE_EQUAL(L"wsl: Processing /etc/fstab with mount -a failed.\n", warnings);
}
// Validate that WSL_DISABLE_WARNINGS silence the stderr output
ScopedEnvVariable disableWarnings(L"WSL_DISABLE_WARNINGS", L"1");
WslShutdown();
std::tie(output, warnings) = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
VERIFY_ARE_EQUAL(L"ok\n", output);
VERIFY_ARE_EQUAL(L"", warnings);
}
TEST_METHOD(Processors)
{
WSL2_TEST_ONLY();
WslConfigChange configChange(LxssGenerateTestConfig() + L"\nprocessors=1");
auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"nproc --all");
VERIFY_ARE_EQUAL(L"1\n", output);
VERIFY_ARE_EQUAL(L"", warnings);
}
TEST_METHOD(GuiApplications)
{
WSL2_TEST_ONLY();
auto validateEnvironment = [&](bool systemdEnabled) {
WslConfigChange configChange(LxssGenerateTestConfig({.guiApplications = true}));
// Validate that running the system distro works.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system true"), 0L);
// Validate that $DISPLAY and $WAYLAND_DISPLAY are set
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"env | grep DISPLAY="), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"env | grep WAYLAND_DISPLAY="), 0L);
// Validate the X11 socket is in the expected location and that we can connect to it.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -d /tmp/.X11-unix"), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"socat - UNIX-CONNECT:/tmp/.X11-unix/X0 < /dev/null"), 0L);
// Validate the runtime dir exists and the wayland-0 socket is in the expected location.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"env | grep XDG_RUNTIME_DIR="), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -d $XDG_RUNTIME_DIR"), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -S $XDG_RUNTIME_DIR/wayland-0"), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/wayland-0 < /dev/null"), 0L);
// Validate that WSLg can be disabled.
configChange.Update(LxssGenerateTestConfig({.guiApplications = false}));
// Validate that WSL starts successfully
auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
VERIFY_ARE_EQUAL(L"ok\n", output);
VERIFY_ARE_EQUAL(L"", warnings);
// Validate that WSLg-related environment variables are not present.
//
// N.B. XDG_RUNTIME_DIR is set when systemd is enabled even if GUI apps are disabled.
std::vector<std::wstring> variables = {L"$DISPLAY", L"$WAYLAND_DISPLAY"};
if (!systemdEnabled)
{
variables.emplace_back(L"$XDG_RUNTIME_DIR");
}
for (const auto& variable : variables)
{
std::tie(output, warnings) = LxsstuLaunchWslAndCaptureOutput(L"echo -n " + variable);
VERIFY_ARE_EQUAL(L"", output);
VERIFY_ARE_EQUAL(L"", warnings);
}
// Validate that wsl --system does not start
std::tie(output, warnings) = LxsstuLaunchWslAndCaptureOutput(L"--system echo not ok", -1);
const std::wstring configPath = wsl::windows::common::helpers::GetWslConfigPath();
const auto expectedOutput =
L"GUI application support is disabled via " + configPath +
L" or /etc/wsl.conf.\r\nError code: Wsl/Service/CreateInstance/WSL_E_GUI_APPLICATIONS_DISABLED\r\n";
VERIFY_ARE_EQUAL(output, expectedOutput);
VERIFY_ARE_EQUAL(L"", warnings);
};
LogInfo("Validate WSLg state with systemd disabled.");
validateEnvironment(false);
LogInfo("Validate WSLg state with systemd enabled.");
auto revert = EnableSystemd();
VERIFY_IS_TRUE(IsSystemdRunning(L"--system"));
validateEnvironment(true);
}
TEST_METHOD(GuiApplicationsSystemd)
{
WSL2_TEST_ONLY();
DistroFileChange wslConf(L"/etc/wsl.conf", false);
wslConf.SetContent(L"[boot]\nsystemd=true\n");
WslConfigChange config{LxssGenerateTestConfig({.guiApplications = true})};
auto validateSocketExists = [](bool exists) {
LxsstuLaunchWsl(L"ls -a /tmp/.X11-unix/");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /tmp/.X11-unix/X0"), exists ? 0L : 1L);
};
// Validate that wslg.service restores the socket if it's deleted.
{
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -f /run/systemd/generator/wslg.service"), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /run/systemd/generator/default.target.wants/wslg.service"), 0L);
validateSocketExists(true);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"umount /tmp/.X11-unix"), 0L);
validateSocketExists(false);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl restart wslg.service"), 0L);
validateSocketExists(true);
}
// Validate that the unit isn't create when GUI apps are disabled
{
config.Update(LxssGenerateTestConfig({.guiApplications = false}));
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /run/systemd/generator/wslg.service"), 1L);
}
// Validate that the unit isn't create when GUI apps are disabled inside the distro.
{
wslConf.SetContent(L"[boot]\nsystemd=true\n[general]\nguiApplications=false");
TerminateDistribution();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /run/systemd/generator/wslg.service"), 1L);
}
}
TEST_METHOD(RegistryKeys)
{
auto openKey = [&](LPCWSTR keyName) {
LogInfo("OpenKey(HKEY_LOCAL_MACHINE, %ls, KEY_READ)", keyName);
return wsl::windows::common::registry::OpenKey(HKEY_LOCAL_MACHINE, keyName, KEY_READ);
};
// Keys that are created by the optional component and the service.
const std::vector<LPCWSTR> inboxKeys{
L"SOFTWARE\\Classes\\CLSID\\{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}",
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace\\{B2B4A4D1-2754-4140-A2EB-"
L"9A76D9D7CDC6}",
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\IdListAliasTranslations\\WSL",
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\IdListAliasTranslations\\WSLLegacy",
L"SOFTWARE\\Classes\\Directory\\shell\\WSL",
L"SOFTWARE\\Classes\\Directory\\Background\\shell\\WSL",
L"SOFTWARE\\Classes\\Drive\\shell\\WSL"};
for (const auto* keyName : inboxKeys)
{
auto key = openKey(keyName);
VERIFY_IS_TRUE(!!key);
}
// Keys that are only created by the MSI.
const std::vector<LPCWSTR> serviceKeys{
L"SOFTWARE\\Microsoft\\Terminal Server Client\\Default\\OptionalAddIns\\WSLDVC_PACKAGE",
L"SOFTWARE\\Classes\\CLSID\\{7e6ad219-d1b3-42d5-b8ee-d96324e64ff6}",
L"SOFTWARE\\Classes\\AppID\\{7F82AD86-755B-4870-86B1-D2E68DFE8A49}"};
for (const auto* keyName : serviceKeys)
{
auto key = openKey(keyName);
VERIFY_IS_TRUE(!!key);
}
}
TEST_METHOD(BinariesAreSigned)
{
if (!wsl::shared::OfficialBuild)
{
LogSkipped("Build is not signed, skipping test");
return;
}
auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath();
VERIFY_IS_TRUE(installPath.has_value());
size_t signedFiles = 0;
for (const auto& e : std::filesystem::recursive_directory_iterator(installPath.value()))
{
if (wsl::windows::common::string::IsPathComponentEqual(e.path().extension().native(), L".dll") ||
wsl::windows::common::string::IsPathComponentEqual(e.path().extension().native(), L".exe"))
{
LogInfo("Validating signature for: %ls", e.path().c_str());
wsl::windows::common::wslutil::ValidateFileSignature(e.path().c_str());
signedFiles++;
}
}
// Sanity check
VERIFY_ARE_NOT_EQUAL(signedFiles, 0);
}
TEST_METHOD(CorruptedVhd)
{
WSL2_TEST_ONLY();
// Create a 100MB vhd without a filesystem.
auto distroPath = std::filesystem::weakly_canonical(wil::GetCurrentDirectoryW<std::wstring>());
auto vhdPath = distroPath / L"CorruptedTest.vhdx";
VIRTUAL_STORAGE_TYPE storageType{};
storageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX;
storageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT;
CREATE_VIRTUAL_DISK_PARAMETERS createVhdParameters{};
createVhdParameters.Version = CREATE_VIRTUAL_DISK_VERSION_2;
createVhdParameters.Version2.BlockSizeInBytes = 1024 * 1024;
createVhdParameters.Version2.MaximumSize = 100 * 1024 * 1024;
wil::unique_hfile vhd{};
VERIFY_ARE_EQUAL(
::CreateVirtualDisk(
&storageType, vhdPath.c_str(), VIRTUAL_DISK_ACCESS_NONE, nullptr, CREATE_VIRTUAL_DISK_FLAG_SUPPORT_COMPRESSED_VOLUMES, 0, &createVhdParameters, nullptr, &vhd),
0l);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
vhd.reset();
DeleteFileW(vhdPath.c_str());
});
auto validateOutput = [&](const std::wstring& command, const std::wstring& expectedOutput) {
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(command.c_str(), -1);
VERIFY_ARE_EQUAL(output, expectedOutput);
};
// Attempt to import a vhd with an open handle.
validateOutput(
std::format(L"--import-in-place test-distro-corrupted \"{}\"", vhdPath.wstring()),
std::format(
L"Failed to attach disk '\\\\?\\{}' to WSL2: The process cannot access the file because it is being used by "
L"another process. \r\n"
L"Error code: Wsl/Service/RegisterDistro/MountDisk/HCS/ERROR_SHARING_VIOLATION\r\n",
vhdPath.wstring()));
vhd.reset();
// Create a broken distribution registration
{
const auto userKey = wsl::windows::common::registry::OpenLxssUserKey();
const auto distroKey =
wsl::windows::common::registry::CreateKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
auto revert = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
wsl::windows::common::registry::DeleteKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
});
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"BasePath", distroPath.c_str());
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"VhdFileName", L"CorruptedTest.vhdx");
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"DistributionName", L"BrokenDistro");
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"DefaultUid", 0);
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Version", LXSS_DISTRO_VERSION_2);
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"State", LxssDistributionStateInstalled);
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Flags", LXSS_DISTRO_FLAGS_VM_MODE);
// Validate that starting the distribution fails with the correct error code.
validateOutput(
L"-d BrokenDistro echo ok",
L"The distribution failed to start because its virtual disk is corrupted.\r\n"
L"Error code: Wsl/Service/CreateInstance/WSL_E_DISK_CORRUPTED\r\n");
// Validate that trying to export the distribution fails with the correct error code.
validateOutput(
L"--export BrokenDistro dummy.tar",
L"The distribution failed to start because its virtual disk is corrupted.\r\n"
L"Error code: Wsl/Service/WSL_E_DISK_CORRUPTED\r\n");
// Shutdown WSL to force the disk to detach.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--shutdown"), 0L);
}
// Import a corrupted vhd.
validateOutput(
std::format(L"--import-in-place test-distro-corrupted \"{}\"", vhdPath.wstring()),
L"The distribution failed to start because its virtual disk is corrupted.\r\n"
L"Error code: Wsl/Service/RegisterDistro/WSL_E_DISK_CORRUPTED\r\n");
// Ensure the VHD can be deleted to make sure it was properly ejected from the VM.
VERIFY_ARE_EQUAL(DeleteFileW(vhdPath.c_str()), TRUE);
}
static void ValidateDistributionShortcut(LPCWSTR DistroName, HANDLE ExpectedIcon)
{
auto distroKey = OpenDistributionKey(DistroName);
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
auto shellLink = wil::CoCreateInstance<IShellLink>(CLSID_ShellLink);
auto startMenu = wsl::windows::common::filesystem::GetKnownFolderPath(FOLDERID_StartMenu, KF_FLAG_CREATE);
// Validate that the shortcut is actually in the start menu
VERIFY_IS_TRUE(shortcutPath.find(startMenu) != std::string::npos);
Microsoft::WRL::ComPtr<IPersistFile> storage;
VERIFY_SUCCEEDED(shellLink->QueryInterface(IID_IPersistFile, &storage));
VERIFY_SUCCEEDED(storage->Load(shortcutPath.c_str(), 0));
std::wstring target(MAX_PATH, '\0');
WIN32_FIND_DATA findData{};
VERIFY_SUCCEEDED(shellLink->GetPath(target.data(), static_cast<int>(target.size()), &findData, SLGP_RAWPATH));
target.resize(wcslen(target.c_str()));
static auto wslExePath = wsl::windows::common::wslutil::GetMsiPackagePath().value() + L"wsl.exe";
VERIFY_ARE_EQUAL(target, wslExePath);
std::wstring arguments(MAX_PATH, '\0');
VERIFY_SUCCEEDED(shellLink->GetArguments(arguments.data(), static_cast<int>(arguments.size())));
arguments.resize(wcslen(arguments.c_str()));
auto distroId = GetDistributionId(DistroName);
VERIFY_IS_TRUE(distroId.has_value());
VERIFY_ARE_EQUAL(
std::format(L"{} {} {} {}", WSL_DISTRIBUTION_ID_ARG, wsl::shared::string::GuidToString<wchar_t>(distroId.value()), WSL_CHANGE_DIRECTORY_ARG, WSL_CWD_HOME),
arguments);
std::wstring iconLocation(MAX_PATH, '\0');
int id{};
THROW_IF_FAILED(shellLink->GetIconLocation(iconLocation.data(), static_cast<int>(iconLocation.size()), &id));
iconLocation.resize(wcslen(iconLocation.c_str()));
if (ExpectedIcon == nullptr)
{
VERIFY_ARE_EQUAL(iconLocation, wslExePath);
}
else
{
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
// Validate that the icon is under the distribution folder.
VERIFY_IS_TRUE(iconLocation.find(basePath) != std::string::npos);
// Validate that the icon has the content we expect.
wil::unique_handle distroIcon{CreateFile(iconLocation.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)};
VERIFY_ARE_EQUAL(GetFileSize(ExpectedIcon, nullptr), GetFileSize(distroIcon.get(), nullptr));
}
}
static std::pair<nlohmann::json, std::wstring> ValidateDistributionTerminalProfile(const std::wstring& DistroName, bool defaultIcon)
{
using namespace wsl::windows::common::wslutil;
using namespace wsl::windows::common::string;
auto distroKey = OpenDistributionKey(DistroName.c_str());
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
auto distroId = GetDistributionId(DistroName.c_str());
VERIFY_IS_TRUE(distroId.has_value());
auto distroIdString = wsl::shared::string::GuidToString<wchar_t>(distroId.value());
auto distributionProfileId =
wsl::shared::string::GuidToString<wchar_t>(CreateV5Uuid(WslTerminalNamespace, std::as_bytes(std::span{distroIdString})));
auto profilePath = wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"Microsoft" / L"Windows Terminal" /
L"Fragments" / L"Microsoft.WSL" / (distributionProfileId + L".json");
std::ifstream file{profilePath};
VERIFY_IS_TRUE(file.good());
nlohmann::json json;
VERIFY_IS_TRUE((file >> json).good());
VERIFY_IS_TRUE(json.is_object());
auto profiles = json.find("profiles");
VERIFY_ARE_NOT_EQUAL(profiles, json.end());
VERIFY_IS_TRUE(profiles->is_array());
VERIFY_IS_TRUE(profiles->size() >= 2);
const auto profileHide = profiles->at(0);
auto expectedHideGuid = wsl::shared::string::GuidToString<wchar_t>(
CreateV5Uuid(GeneratedProfilesTerminalNamespace, std::as_bytes(std::span{DistroName})));
VERIFY_ARE_EQUAL(profileHide["updates"], wsl::shared::string::WideToMultiByte(expectedHideGuid));
VERIFY_ARE_EQUAL(profileHide["hidden"], true);
const auto launchProfile = profiles->at(1);
auto expectedId =
wsl::shared::string::GuidToString<wchar_t>(CreateV5Uuid(WslTerminalNamespace, std::as_bytes(std::span{distroIdString})));
VERIFY_ARE_EQUAL(launchProfile["guid"].get<std::string>(), wsl::shared::string::WideToMultiByte(expectedId));
VERIFY_ARE_EQUAL(launchProfile["name"].get<std::string>(), wsl::shared::string::WideToMultiByte(DistroName));
VERIFY_ARE_EQUAL(launchProfile["pathTranslationStyle"].get<std::string>(), "wsl");
std::wstring systemDir;
wil::GetSystemDirectoryW(systemDir);
VERIFY_ARE_EQUAL(
std::format("{}\\{} {} {}", systemDir, WSL_BINARY_NAME, WSL_DISTRIBUTION_ID_ARG, distroIdString),
launchProfile["commandline"].get<std::string>());
// Verify that startingDirectory is set to home directory
VERIFY_ARE_EQUAL(launchProfile["startingDirectory"].get<std::string>(), "~");
auto iconLocation = wsl::shared::string::MultiByteToWide(launchProfile["icon"].get<std::string>());
if (defaultIcon)
{
static auto wslExePath = wsl::windows::common::wslutil::GetMsiPackagePath().value() + L"wsl.exe";
VERIFY_ARE_EQUAL(iconLocation, wslExePath);
}
else
{
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
// Validate that the icon is under the distribution folder.
VERIFY_IS_TRUE(iconLocation.find(basePath) == 0);
}
return std::make_pair(json, profilePath);
}
TEST_METHOD(ConvertDistro)
{
std::wstring originalVersion;
std::wstring targetVersion;
if (LxsstuVmMode())
{
originalVersion = L"2";
targetVersion = L"1";
}
else
{
originalVersion = L"1";
targetVersion = L"2";
}
auto cleanup =
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(L"--set-version test_distro " + originalVersion); });
// Convert the test distribution to the target version and back to the original.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--set-version test_distro " + targetVersion), 0u);
ValidateDistributionShortcut(LXSS_DISTRO_NAME_TEST_L, nullptr);
ValidateDistributionTerminalProfile(LXSS_DISTRO_NAME_TEST_L, true);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--set-version test_distro " + originalVersion), 0u);
ValidateDistributionShortcut(LXSS_DISTRO_NAME_TEST_L, nullptr);
ValidateDistributionTerminalProfile(LXSS_DISTRO_NAME_TEST_L, true);
// Do not convert the test distribution if it is already in the original version.
cleanup.release();
}
TEST_METHOD(ManualDistroShutdown)
{
WSL2_TEST_ONLY();
// Terminate a distribution from within WSL. This command should be terminated by the VM terminating
LxsstuLaunchWsl(L"echo foo > /dev/shm/bar ; reboot -f ; sleep 1d");
// Wait for distribution to be terminated to avoid running the next command as it shuts down
auto pred = []() {
const auto commandLine = LxssGenerateWslCommandLine(L"--list --running");
wsl::windows::common::SubProcess process(nullptr, commandLine.c_str());
// Don't check the exit code since that command returns -1 when no distros are running.
const auto output = process.RunAndCaptureOutput();
THROW_HR_IF(E_ABORT, output.Stdout.find(LXSS_DISTRO_NAME_TEST_L) != std::string::npos);
};
wsl::shared::retry::RetryWithTimeout<void>(pred, std::chrono::seconds(1), std::chrono::minutes(2));
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"test -f /dev/shm/bar2 || echo -n ok");
VERIFY_ARE_EQUAL(out, L"ok");
}
TEST_METHOD(KernelModules)
{
WSL2_TEST_ONLY();
// Get the kernel version and strip off everything after the first dash.
std::wstring kernelVersion{TEXT(KERNEL_VERSION)};
auto position = kernelVersion.find_first_of(L"-");
if (position != kernelVersion.npos)
{
kernelVersion = kernelVersion.substr(0, position);
}
kernelVersion += L"-microsoft-standard-WSL2";
// Ensure the kernel modules folder is mounted correctly.
std::wstring command = std::format(
L"mount | grep -iF 'none on /usr/lib/modules/{} type overlay "
L"(rw,nosuid,nodev,noatime,lowerdir=/modules,upperdir=/lib/modules/{}/rw/upper,workdir=/lib/modules/{}/rw/"
L"work,uuid=on)'",
kernelVersion,
kernelVersion,
kernelVersion);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(command.c_str(), nullptr, nullptr, nullptr, nullptr), 0u);
// Update .wslconfig and ensure an error is displayed if nonexistent kernel or modules is specified.
const std::wstring wslConfigPath = wsl::windows::common::helpers::GetWslConfigPath();
const std::wstring nonExistentFile = L"DoesNotExist";
WslConfigChange configChange(LxssGenerateTestConfig({.kernel = nonExistentFile.c_str()}));
ValidateOutput(
L"echo ok",
std::format(
L"{}\r\nError code: Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND\r\n",
wsl::shared::Localization::MessageCustomKernelNotFound(wslConfigPath, nonExistentFile)),
L"");
configChange.Update(LxssGenerateTestConfig({.kernelModules = nonExistentFile.c_str()}));
ValidateOutput(
L"echo ok",
std::format(
L"{}\r\nError code: Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND\r\n",
wsl::shared::Localization::MessageCustomKernelModulesNotFound(wslConfigPath, nonExistentFile)),
L"");
#ifdef WSL_DEV_INSTALL_PATH
std::wstring kernelPath = WSL_DEV_INSTALL_PATH L"/kernel";
std::wstring kernelModulesPath = WSL_DEV_INSTALL_PATH L"/modules.vhd";
#else
auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath();
VERIFY_IS_TRUE(installPath.has_value());
std::filesystem::path wslInstallPath(installPath.value());
std::wstring kernelPath = wslInstallPath / "tools" / "kernel";
std::wstring kernelModulesPath = wslInstallPath / "tools" / "modules.vhd";
#endif
// Verify that no modules are mounted for a custom kernel with no modules specified.
kernelPath = std::regex_replace(kernelPath, std::wregex(L"\\\\"), L"\\\\");
configChange.Update(LxssGenerateTestConfig({.kernel = kernelPath.c_str()}));
ValidateOutput(command.c_str(), L"", L"", 1);
// Verify the error message if custom kernel modules are used with the default kernel.
kernelModulesPath = std::regex_replace(kernelModulesPath, std::wregex(L"\\\\"), L"\\\\");
configChange.Update(LxssGenerateTestConfig({.kernelModules = kernelModulesPath.c_str()}));
ValidateOutput(
L"echo ok",
std::format(
L"{}\r\nError code: Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND\r\n",
wsl::shared::Localization::MessageMismatchedKernelModulesError()),
L"");
configChange.Update(LxssGenerateTestConfig());
// Validate that tun is loaded by default.
ValidateOutput(L"grep -i '^tun' /proc/modules | wc -l", L"1\n", L"", 0);
// Validate a VM can boot with no extra additional kernel modules.
configChange.Update(LxssGenerateTestConfig({.loadDefaultKernelModules = false}));
ValidateOutput(L"grep -i '^tun' /proc/modules | wc -l", L"0\n", L"", 0);
// Validate that the user can pass additional modules to load at boot.
ValidateOutput(L"grep -iE '^(usb_storage|dm_crypt)' /proc/modules | wc -l", L"0\n", L"", 0);
configChange.Update(LxssGenerateTestConfig({.loadKernelModules = L"usb_storage,dm_crypt"}));
ValidateOutput(L"grep -iE '^(usb_storage|dm_crypt)' /proc/modules | wc -l", L"2\n", L"", 0);
// Validate that failing to load a module shows a warning in dmesg.
configChange.Update(LxssGenerateTestConfig({.loadKernelModules = L"not-found"}));
ValidateOutput(L"dmesg | grep -iF \"failed to load module 'not-found'\" | wc -l", L"1\n", L"", 0);
}
TEST_METHOD(CrashCollection)
{
WSL2_TEST_ONLY();
const auto folder = std::filesystem::absolute(L"test-crash-dumps");
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
std::error_code error;
std::filesystem::remove_all(folder, error);
});
auto countCrashes = [&]() {
std::error_code error;
return std::distance(std::filesystem::directory_iterator{folder, error}, std::filesystem::directory_iterator{});
};
auto waitForCrashes = [&](int expected) {
wsl::shared::retry::RetryWithTimeout<void>(
[&]() { THROW_HR_IF(E_UNEXPECTED, countCrashes() < expected); }, std::chrono::seconds(1), std::chrono::minutes(2));
VERIFY_ARE_EQUAL(countCrashes(), expected);
};
auto crash = []() { LxsstuLaunchWsl(L"kill -SEGV $$"); };
WslConfigChange change(LxssGenerateTestConfig({.crashDumpCount = 2, .CrashDumpFolder = folder.wstring()}));
VERIFY_ARE_EQUAL(countCrashes(), 0);
crash();
waitForCrashes(1);
crash();
waitForCrashes(2);
crash();
waitForCrashes(2);
// Create a dummy file and validate that the file limit logic doesn't remove it.
std::filesystem::remove_all(folder);
std::filesystem::create_directory(folder);
std::ofstream(folder / "dummy").close();
crash();
waitForCrashes(2);
crash();
waitForCrashes(3);
crash();
waitForCrashes(3);
VERIFY_IS_TRUE(std::filesystem::exists(folder / "dummy"));
}
// UnitTests Private Methods
static VOID VerifyCaseSensitiveDirectory(_In_ PCWSTR RelativePath)
{
const std::wstring Path = LxsstuGetLxssDirectory() + L"\\" + RelativePath;
const wil::unique_hfile Directory{CreateFileW(
Path.c_str(),
FILE_READ_ATTRIBUTES,
(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE),
nullptr,
OPEN_EXISTING,
(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT),
nullptr)};
THROW_LAST_ERROR_IF(!Directory);
IO_STATUS_BLOCK IoStatus;
FILE_CASE_SENSITIVE_INFORMATION CaseInfo;
THROW_IF_NTSTATUS_FAILED(NtQueryInformationFile(Directory.get(), &IoStatus, &CaseInfo, sizeof(CaseInfo), FileCaseSensitiveInformation));
VERIFY_ARE_EQUAL(CaseInfo.Flags, (ULONG)FILE_CS_FLAG_CASE_SENSITIVE_DIR);
}
TEST_METHOD(Move)
{
constexpr auto name = L"move-test-distro";
constexpr auto testFolder = L"move-test-test-folder";
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
auto cleanupName = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
LxsstuLaunchWsl(std::format(L"--unregister {}", name));
std::filesystem::remove_all(testFolder);
});
auto validateDistro = []() {
auto [cmdOutput, _] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
VERIFY_ARE_EQUAL(cmdOutput, L"ok\n");
};
// Move the distro to a different folder (relative path)
{
WslShutdown();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, testFolder)), 0L);
// Validate that the distribution still starts
validateDistro();
VERIFY_IS_TRUE(std::filesystem::exists(std::format(L"{}\\ext4.vhdx", testFolder)));
}
auto absolutePath = std::filesystem::weakly_canonical(".").wstring();
// Move the distro to a different folder (absolute path)
{
WslShutdown();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, absolutePath)), 0L);
// Validate that the distribution still starts
validateDistro();
VERIFY_IS_TRUE(std::filesystem::exists(std::format(L"{}\\ext4.vhdx", absolutePath)));
}
// Try to move the distribution to a folder that's already in use
{
WslShutdown();
wil::unique_cotaskmem_string path;
THROW_IF_FAILED(::SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &path));
auto targetPath = std::format(L"{}\\lxss", path.get());
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --move {}", name, targetPath), -1);
VERIFY_ARE_EQUAL(
out,
L"The supplied install location is already in use.\r\nError code: "
L"Wsl/Service/MoveDistro/ERROR_FILE_EXISTS\r\n");
// Validate that the distribution still starts and that the vhd hasn't moved.
validateDistro();
VERIFY_IS_TRUE(std::filesystem::exists(std::format(L"{}\\ext4.vhdx", absolutePath)));
}
// Try to move the distribution to an invalid path
{
WslShutdown();
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --move :", name), -1);
VERIFY_ARE_EQUAL(
out,
L"The filename, directory name, or volume label syntax is incorrect. \r\nError code: "
L"Wsl/Service/MoveDistro/ERROR_INVALID_NAME\r\n");
// Validate that the distribution still starts and that the vhd hasn't moved.
validateDistro();
VERIFY_IS_TRUE(std::filesystem::exists(std::format(L"{}\\ext4.vhdx", absolutePath)));
}
}
TEST_METHOD(Resize)
{
WSL2_TEST_ONLY();
constexpr auto name = L"resize-test-distro";
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
WslShutdown();
auto cleanupName =
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(std::format(L"--unregister {}", name)); });
auto validateDistro = [](LPCWSTR size, LPCWSTR expectedSize, LPCWSTR expectedError = nullptr) {
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --resize {}", name, size), expectedError ? -1 : 0);
if (expectedError)
{
VERIFY_ARE_EQUAL(expectedError, out);
return;
}
std::tie(out, _) = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} df -h / --output=size | sed 1d", name));
VERIFY_ARE_EQUAL(std::format(L" {}\n", expectedSize), out);
WslShutdown();
};
validateDistro(L"1500G", L"1.5T");
validateDistro(L"500G", L"492G");
validateDistro(L"1M", nullptr, L"Failed to resize disk.\r\nError code: Wsl/Service/E_FAIL\r\n");
{
WslKeepAlive keepAlive;
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--manage test_distro --resize 1500GB", -1);
VERIFY_ARE_EQUAL(
L"The operation could not be completed because the VHD is currently in use. To force WSL to stop use: wsl.exe "
L"--shutdown\r\nError code: Wsl/Service/WSL_E_DISTRO_NOT_STOPPED\r\n",
out);
}
}
TEST_METHOD(FileOffsets)
{
WSL2_TEST_ONLY();
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(L"output.txt"); });
std::ofstream file("output.txt");
VERIFY_IS_TRUE(file.good() && file << "previous content\n");
file.close();
std::wstring cmd(L"C:\\windows\\system32\\cmd.exe /c \"wsl.exe echo ok >> output.txt && type output.txt\"");
auto [output, _] = LxsstuLaunchCommandAndCaptureOutput(cmd.data());
VERIFY_ARE_EQUAL(output, L"previous content\r\nok\n");
}
TEST_METHOD(GlobalFlagsOverride)
{
auto isDriveMountingEnabled = []() { return LxsstuLaunchWsl(L"test -d /mnt/c/Windows") == 0; };
VERIFY_IS_TRUE(isDriveMountingEnabled());
{
RegistryKeyChange<DWORD> key(HKEY_LOCAL_MACHINE, LXSS_SERVICE_REGISTRY_PATH, L"DistributionFlags", ~LXSS_DISTRO_FLAGS_ENABLE_DRIVE_MOUNTING);
TerminateDistribution();
VERIFY_IS_FALSE(isDriveMountingEnabled());
}
TerminateDistribution();
VERIFY_IS_TRUE(isDriveMountingEnabled());
}
TEST_METHOD(WriteWslConfig)
{
WSL2_TEST_ONLY();
WSL_SETTINGS_TEST();
auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath();
VERIFY_IS_TRUE(installPath.has_value());
std::filesystem::path wslInstallPath(installPath.value());
std::filesystem::path libWslDllPath = wslInstallPath / "libwsl.dll";
VERIFY_IS_TRUE(std::filesystem::exists(libWslDllPath));
LxssDynamicFunction<decltype(GetWslConfigFilePath)> getWslConfigFilePath(libWslDllPath.c_str(), "GetWslConfigFilePath");
LxssDynamicFunction<decltype(CreateWslConfig)> createWslConfig(libWslDllPath.c_str(), "CreateWslConfig");
LxssDynamicFunction<decltype(FreeWslConfig)> freeWslConfig(libWslDllPath.c_str(), "FreeWslConfig");
LxssDynamicFunction<decltype(GetWslConfigSetting)> getWslConfigSetting(libWslDllPath.c_str(), "GetWslConfigSetting");
LxssDynamicFunction<decltype(SetWslConfigSetting)> setWslConfigSetting(libWslDllPath.c_str(), "SetWslConfigSetting");
// Reset the test config file. The original has already been saved as part of module setup.
auto wslConfigFilePath = getenv("userprofile") + std::string("\\.wslconfig");
WslConfigChange config{L""};
auto apiWslConfigFilePath = getWslConfigFilePath();
VERIFY_IS_TRUE(std::filesystem::path(wslConfigFilePath) == std::filesystem::path(apiWslConfigFilePath));
auto wslConfigDefaults = createWslConfig(nullptr);
VERIFY_IS_NOT_NULL(wslConfigDefaults);
auto wslConfig = createWslConfig(apiWslConfigFilePath);
VERIFY_IS_NOT_NULL(wslConfig);
freeWslConfig(wslConfigDefaults);
freeWslConfig(wslConfig);
WslConfigSetting wslConfigSettingWriteOut;
WslConfigSetting wslConfigSettingReadIn;
auto testLoop = [&](auto& testPlan, auto& updateWslConfigSettingWriteOutValue, auto& verifyWslConfigSettingValueReadEqual) {
wslConfigSettingWriteOut = wslConfigSettingReadIn = WslConfigSetting{};
for (const auto testEntry : testPlan)
{
wslConfigSettingWriteOut = testEntry.first;
for (const auto& test : testEntry.second)
{
const auto& writeValue = test.first;
const auto& expectedValue = test.second;
{
// This scenario tests writing a value to the config file and reading it back. If the write succeeded,
// the written value will be cached in the WslConfig object. The read will then return the cached value.
wslConfig = createWslConfig(apiWslConfigFilePath);
VERIFY_IS_NOT_NULL(wslConfig);
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
updateWslConfigSettingWriteOutValue(wslConfigSettingWriteOut, writeValue);
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
wslConfigSettingReadIn = getWslConfigSetting(wslConfig, wslConfigSettingWriteOut.ConfigEntry);
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.ConfigEntry, wslConfigSettingWriteOut.ConfigEntry);
verifyWslConfigSettingValueReadEqual(wslConfigSettingReadIn, expectedValue);
}
{
// This scenario tests reading a value from the config file. Specifically, it will parse in the
// written value to the wsl config file from the previous scenario. This validates parsing the value
// from the file (e.g. that it was written correctly and then parsed as expected).
wslConfig = createWslConfig(apiWslConfigFilePath);
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
wslConfigSettingReadIn = getWslConfigSetting(wslConfig, wslConfigSettingWriteOut.ConfigEntry);
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.ConfigEntry, wslConfigSettingWriteOut.ConfigEntry);
verifyWslConfigSettingValueReadEqual(wslConfigSettingReadIn, expectedValue);
}
}
}
};
{
// Enable NetworkingMode::Mirrored for IgnoredPorts to be set correctly upon parsing.
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
// std::pair[0] = Written value, std::pair[1] = Actual/Expected value
static const std::vector<std::pair<PCWSTR, PCWSTR>> filePathsToTest{
{L"C:\\DoesNotExit\\ext4.vhdx", L"C:\\DoesNotExit\\ext4.vhdx"},
{L"\\DoesNotExit\\ext4.vhdx", L"\\DoesNotExit\\ext4.vhdx"},
{L"", L""},
};
// tuple: WslConfigSetting, expectedValue, actualValue
std::vector<std::pair<WslConfigSetting, std::vector<std::pair<PCWSTR, PCWSTR>>>> wslConfigSettingStringTestPlan{
{
{.ConfigEntry = WslConfigEntry::SwapFilePath},
filePathsToTest,
},
{
{.ConfigEntry = WslConfigEntry::IgnoredPorts},
{
{L"1,2,300,4455,65535", L"1,2,300,4455,65535"},
{L"10,20,-100,p", L"10,20"},
{L"100,200,notaport", L"100,200"},
{L"1000,2000;3.4", L"1000,2000"},
{L"10000, 20000, 30000,40000 ,50000", L"10000,20000,30000,40000,50000"},
{L"", L""},
{L"notaport", L""},
{L"-5555", L""},
{L"C:\\DoesNotExit\\ext4.vhdx", L""},
},
},
{
{.ConfigEntry = WslConfigEntry::KernelPath},
filePathsToTest,
},
{
{.ConfigEntry = WslConfigEntry::SystemDistroPath},
filePathsToTest,
},
};
auto updateWslConfigSettingWriteOutStringValue = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
wslConfigSettingWriteOut.StringValue = writeValue;
};
auto verifyWslConfigSettingReadStringValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
VERIFY_ARE_EQUAL(std::wstring_view(wslConfigSettingReadIn.StringValue), std::wstring_view(expectedValue));
};
testLoop(wslConfigSettingStringTestPlan, updateWslConfigSettingWriteOutStringValue, verifyWslConfigSettingReadStringValueEqual);
}
{
wslConfigSettingWriteOut = wslConfigSettingReadIn = WslConfigSetting{};
wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::NoEntry;
wslConfig = createWslConfig(apiWslConfigFilePath);
VERIFY_IS_NOT_NULL(wslConfig);
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
wslConfigSettingReadIn = getWslConfigSetting(wslConfig, wslConfigSettingWriteOut.ConfigEntry);
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.ConfigEntry, wslConfigSettingWriteOut.ConfigEntry);
}
SYSTEM_INFO systemInfo{};
GetSystemInfo(&systemInfo);
{
// std::pair[0] = Written value, std::pair[1] = Actual/Expected value
static const std::vector<std::pair<int, int>> timeoutValuesToTest{
{-132445, -132445},
{0, 0},
{1, 1},
{13456, 13456},
{100000000, 100000000},
};
// tuple: WslConfigSetting, expectedValue, actualValue
std::vector<std::pair<WslConfigSetting, std::vector<std::pair<int, int>>>> wslConfigSettingInt32TestPlan{
{
{.ConfigEntry = WslConfigEntry::ProcessorCount},
{
{-123443, systemInfo.dwNumberOfProcessors},
{-1, systemInfo.dwNumberOfProcessors},
{1, 1},
{2, std::min(2, static_cast<int>(systemInfo.dwNumberOfProcessors))},
{systemInfo.dwNumberOfProcessors, systemInfo.dwNumberOfProcessors},
{1234, systemInfo.dwNumberOfProcessors},
},
},
{
{.ConfigEntry = WslConfigEntry::InitialAutoProxyTimeout},
timeoutValuesToTest,
},
{
{.ConfigEntry = WslConfigEntry::VMIdleTimeout},
timeoutValuesToTest,
},
};
auto updateWslConfigSettingWriteOutInt32Value = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
wslConfigSettingWriteOut.Int32Value = writeValue;
};
auto verifyWslConfigSettingReadInt32ValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.Int32Value, expectedValue);
};
testLoop(wslConfigSettingInt32TestPlan, updateWslConfigSettingWriteOutInt32Value, verifyWslConfigSettingReadInt32ValueEqual);
}
{
MEMORYSTATUSEX memInfo{sizeof(MEMORYSTATUSEX)};
THROW_IF_WIN32_BOOL_FALSE(GlobalMemoryStatusEx(&memInfo));
const auto minimumMemorySizeBytes = 256 * _1MB;
const auto maximumMemorySizeBytes = memInfo.ullTotalPhys;
// std::pair[0] = Written value, std::pair[1] = Actual/Expected value
static const std::vector<std::pair<unsigned long long, unsigned long long>> fileSizesBytesToTest{
{0, 0}, {1, 1}, {13456, 13456}, {100000000, 100000000}, {9223372036854775807, 9223372036854775807}};
// tuple: WslConfigSetting, expectedValue, actualValue
std::vector<std::pair<WslConfigSetting, std::vector<std::pair<unsigned long long, unsigned long long>>>> wslConfigSettingUInt64TestPlan{
{
{.ConfigEntry = WslConfigEntry::MemorySizeBytes},
{
{0, maximumMemorySizeBytes / 2},
{minimumMemorySizeBytes / 2, minimumMemorySizeBytes},
{minimumMemorySizeBytes, minimumMemorySizeBytes},
{maximumMemorySizeBytes / 2, maximumMemorySizeBytes / 2},
{maximumMemorySizeBytes, maximumMemorySizeBytes},
{maximumMemorySizeBytes * 2, maximumMemorySizeBytes},
},
},
{
{.ConfigEntry = WslConfigEntry::SwapSizeBytes},
fileSizesBytesToTest,
},
{
{.ConfigEntry = WslConfigEntry::VhdSizeBytes},
fileSizesBytesToTest,
},
};
auto updateWslConfigSettingWriteOutUInt64Value = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
wslConfigSettingWriteOut.UInt64Value = writeValue;
};
auto verifyWslConfigSettingReadUInt64ValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.UInt64Value, expectedValue);
};
testLoop(wslConfigSettingUInt64TestPlan, updateWslConfigSettingWriteOutUInt64Value, verifyWslConfigSettingReadUInt64ValueEqual);
}
{
// Enable NetworkingMode::Mirrored for IgnoredPorts to be set correctly upon parsing.
WslConfigChange config(LxssGenerateTestConfig());
// std::pair[0] = Written value, std::pair[1] = Actual/Expected value
static const std::vector<std::pair<bool, bool>> booleansToTest{{false, false}, {true, true}};
// tuple: WslConfigSetting, expectedValue, actualValue
std::vector<std::pair<WslConfigSetting, std::vector<std::pair<bool, bool>>>> wslConfigSettingBooleanTestPlan{
{
{.ConfigEntry = WslConfigEntry::FirewallEnabled},
booleansToTest,
},
{
{.ConfigEntry = WslConfigEntry::LocalhostForwardingEnabled},
booleansToTest,
},
{
{.ConfigEntry = WslConfigEntry::HostAddressLoopbackEnabled},
// This setting is only enabled when NetworkingMode != Mirrored.
{{false, false}, {true, false}},
},
{
{.ConfigEntry = WslConfigEntry::AutoProxyEnabled},
booleansToTest,
},
{
{.ConfigEntry = WslConfigEntry::DNSProxyEnabled},
booleansToTest,
},
{
{.ConfigEntry = WslConfigEntry::DNSTunnelingEnabled},
// This setting is only enabled when NetworkingMode != Nat && NetworkingMode != Mirrored
booleansToTest,
},
{
{.ConfigEntry = WslConfigEntry::BestEffortDNSParsingEnabled},
// This setting is only enabled when DNSTunnelingEnabled = true
booleansToTest,
},
{
{.ConfigEntry = WslConfigEntry::GUIApplicationsEnabled},
booleansToTest,
},
{
{.ConfigEntry = WslConfigEntry::NestedVirtualizationEnabled},
booleansToTest,
},
{
{.ConfigEntry = WslConfigEntry::SafeModeEnabled},
booleansToTest,
},
{
{.ConfigEntry = WslConfigEntry::SparseVHDEnabled},
booleansToTest,
},
{
{.ConfigEntry = WslConfigEntry::DebugConsoleEnabled},
booleansToTest,
},
{
{.ConfigEntry = WslConfigEntry::HardwarePerformanceCountersEnabled},
// This setting is disabled when SafeModeEnabled = true.
// Since testing SafeModeEnabled is tested earlier and left as
// true (.wslconfig is re-used), this setting should be false.
{{false, false}, {true, false}},
},
};
auto updateWslConfigSettingWriteOutBooleanValue = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
wslConfigSettingWriteOut.BoolValue = writeValue;
};
auto verifyWslConfigSettingReadBooleanValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.BoolValue, expectedValue);
};
testLoop(wslConfigSettingBooleanTestPlan, updateWslConfigSettingWriteOutBooleanValue, verifyWslConfigSettingReadBooleanValueEqual);
}
{
// std::pair[0] = Written value, std::pair[1] = Actual/Expected value
static const std::vector<std::pair<NetworkingConfiguration, NetworkingConfiguration>> networkingConfigurationsToTest{
{NetworkingConfiguration::None, NetworkingConfiguration::None},
{NetworkingConfiguration::Nat, NetworkingConfiguration::Nat},
{NetworkingConfiguration::Bridged, NetworkingConfiguration::Bridged},
{NetworkingConfiguration::Mirrored, NetworkingConfiguration::Mirrored},
{NetworkingConfiguration::VirtioProxy, NetworkingConfiguration::VirtioProxy},
};
// tuple: WslConfigSetting, expectedValue, actualValue
std::vector<std::pair<WslConfigSetting, std::vector<std::pair<NetworkingConfiguration, NetworkingConfiguration>>>> wslConfigSettingNetworkingConfigurationTestPlan{
{
{.ConfigEntry = WslConfigEntry::Networking},
networkingConfigurationsToTest,
},
};
auto updateWslConfigSettingWriteOutNetworkingConfigurationValue = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
wslConfigSettingWriteOut.NetworkingConfigurationValue = writeValue;
};
auto verifyWslConfigSettingReadNetworkingConfigurationValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
VERIFY_ARE_EQUAL(expectedValue, wslConfigSettingReadIn.NetworkingConfigurationValue);
};
testLoop(
wslConfigSettingNetworkingConfigurationTestPlan,
updateWslConfigSettingWriteOutNetworkingConfigurationValue,
verifyWslConfigSettingReadNetworkingConfigurationValueEqual);
}
{
// std::pair[0] = Written value, std::pair[1] = Actual/Expected value
static const std::vector<std::pair<MemoryReclaimConfiguration, MemoryReclaimConfiguration>> memoryReclaimModesToTest{
{MemoryReclaimConfiguration::Disabled, MemoryReclaimConfiguration::Disabled},
{MemoryReclaimConfiguration::Gradual, MemoryReclaimConfiguration::Gradual},
{MemoryReclaimConfiguration::DropCache, MemoryReclaimConfiguration::DropCache},
};
// tuple: WslConfigSetting, expectedValue, actualValue
std::vector<std::pair<WslConfigSetting, std::vector<std::pair<MemoryReclaimConfiguration, MemoryReclaimConfiguration>>>> wslConfigSettingMemoryReclaimModeTestPlan{
{
{.ConfigEntry = WslConfigEntry::AutoMemoryReclaim},
memoryReclaimModesToTest,
},
};
auto updateWslConfigSettingWriteOutMemoryReclaimModeValue = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
wslConfigSettingWriteOut.MemoryReclaimModeValue = writeValue;
};
auto verifyWslConfigSettingReadMemoryReclaimModeValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.MemoryReclaimModeValue, expectedValue);
};
testLoop(wslConfigSettingMemoryReclaimModeTestPlan, updateWslConfigSettingWriteOutMemoryReclaimModeValue, verifyWslConfigSettingReadMemoryReclaimModeValueEqual);
}
{
std::wstring customWslConfigContentOut{
LR"(
[wsl2] # trailing section comment
vmIdleTimeout=200 # property trailing comment
vmIdleTimeout=20000 # property trailing comment
vmIdleTimeout=20000 # property trailing comment
mountDeviceTimeout=120\
000
kernelBootTimeout=120000
# property comment
swapfile=E:\\wsl-b\
uild\\src\\win\
dows\\wslc\
ore\\lib\\swap.vhdx # multi-line property with trailing comment
telemetry=false
safeMode=false
guiApplications=true
earlyBootLogging=false
# comment 1
# comment 2
# \t \b
virtio9p=true # property trailing comment, ensure new property is appended to the section while preserving this comment
# section comment
[experimental]
autoProxy=false
[wsl2]
# end comment
)"};
WslConfigChange config(customWslConfigContentOut);
wslConfig = createWslConfig(apiWslConfigFilePath);
VERIFY_IS_NOT_NULL(wslConfig);
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
// The config contains multiple vmIdleTimeout entries. The first one should be updated/written.
wslConfigSettingWriteOut = WslConfigSetting{};
wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::VMIdleTimeout;
wslConfigSettingWriteOut.Int32Value = 1234;
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
// Replace the swapfile path, which is a multi-line property with a trailing comment.
// The multi-line value should be replaced with the new value and trailing comment preserved.
wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::SwapFilePath;
wslConfigSettingWriteOut.StringValue = LR"(C:\DoesNotExist\swap.vhdx)";
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
// Write out a new setting that doesn't exist in the original config but its section
// does. The new setting should be appended to that section. There are two cases here::
wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::HardwarePerformanceCountersEnabled;
wslConfigSettingWriteOut.BoolValue = true;
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::AutoMemoryReclaim;
wslConfigSettingWriteOut.MemoryReclaimModeValue = MemoryReclaimConfiguration::Gradual;
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
std::wstring customWslConfigContentExpected{
LR"(
[wsl2] # trailing section comment
vmIdleTimeout=1234 # property trailing comment
vmIdleTimeout=20000 # property trailing comment
vmIdleTimeout=20000 # property trailing comment
mountDeviceTimeout=120\
000
kernelBootTimeout=120000
# property comment
swapfile=C:\\DoesNotExist\\swap.vhdx # multi-line property with trailing comment
telemetry=false
safeMode=false
guiApplications=true
earlyBootLogging=false
# comment 1
# comment 2
# \t \b
virtio9p=true # property trailing comment, ensure new property is appended to the section while preserving this comment
# section comment
[experimental]
autoProxy=false
autoMemoryReclaim=Gradual
[wsl2]
# end comment
)"};
std::wifstream configRead(apiWslConfigFilePath);
auto customWslConfigContentActual = std::wstring{std::istreambuf_iterator<wchar_t>(configRead), {}};
configRead.close();
VERIFY_ARE_EQUAL(customWslConfigContentExpected, customWslConfigContentActual);
}
{
// This test contains an invalid line ('babyshark') in the wsl2 section.
// The line should be preserved and no additional spacing/lines should be added.
std::wstring customWslConfigContentOut{
LR"(
[wsl2]
memory=32G
processors=12
hostAddressLoopback=false
dnsTunneling=true
defaultVhdSize=1099511627776
babyshark
localhostForwarding=true
autoProxy=false
)"};
WslConfigChange config(customWslConfigContentOut);
wslConfig = createWslConfig(apiWslConfigFilePath);
VERIFY_IS_NOT_NULL(wslConfig);
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
auto wslConfigSetting = getWslConfigSetting(wslConfig, WslConfigEntry::AutoProxyEnabled);
const auto autoProxyEnabled = false;
VERIFY_ARE_EQUAL(wslConfigSetting.BoolValue, autoProxyEnabled);
wslConfigSetting.BoolValue = !autoProxyEnabled;
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSetting), ERROR_SUCCESS);
std::wstring customWslConfigContentExpected{
LR"(
[wsl2]
memory=32G
processors=12
hostAddressLoopback=false
dnsTunneling=true
defaultVhdSize=1099511627776
babyshark
localhostForwarding=true
)"};
std::wifstream configRead(apiWslConfigFilePath);
auto customWslConfigContentActual = std::wstring{std::istreambuf_iterator<wchar_t>(configRead), {}};
configRead.close();
VERIFY_ARE_EQUAL(customWslConfigContentActual, customWslConfigContentExpected);
}
{
// This test verifies removal of a setting from the .wslconfig when a default value for the particular setting is
// set. This gives wsl control over the default value.
std::wstring customWslConfigContentOut{
LR"(
[wsl2]
memory=32G
processors=12 # property trailing comment
hostAddressLoopback=false
dnsTunneling=true
defaultVhdSize=1099511627776
localhostForwarding=true
autoProxy=false
)"};
WslConfigChange config(customWslConfigContentOut);
wslConfig = createWslConfig(apiWslConfigFilePath);
VERIFY_IS_NOT_NULL(wslConfig);
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
wslConfigDefaults = createWslConfig(nullptr);
VERIFY_IS_NOT_NULL(wslConfigDefaults);
auto cleanupWslConfigDefaults = wil::scope_exit([&] { freeWslConfig(wslConfigDefaults); });
// This setting should be removed from the .wslconfig file.
auto wslConfigDefaultSettingMemorySize = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::MemorySizeBytes);
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingMemorySize), ERROR_SUCCESS);
// This setting should be removed from the .wslconfig file but trailing comment preserved.
auto wslConfigDefaultSettingProcessorCount = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::ProcessorCount);
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingProcessorCount), ERROR_SUCCESS);
// This setting should be preserved with an updated value in the .wslconfig file.
auto wslConfigDefaultSettingVhdSize = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::VhdSizeBytes);
wslConfigDefaultSettingVhdSize.UInt64Value -= 1;
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingVhdSize), ERROR_SUCCESS);
// This setting should be removed from the .wslconfig file.
auto wslConfigDefaultSettingAutoProxy = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::AutoProxyEnabled);
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingAutoProxy), ERROR_SUCCESS);
// This setting should not be written to the .wslconfig file.
auto wslConfigDefaultSettingGuiApplications = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::GUIApplicationsEnabled);
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingGuiApplications), ERROR_SUCCESS);
std::wstring customWslConfigContentExpected{
LR"(
[wsl2]
# property trailing comment
hostAddressLoopback=false
dnsTunneling=true
defaultVhdSize=1099511627775
localhostForwarding=true
)"};
std::wifstream configRead(apiWslConfigFilePath);
auto customWslConfigContentActual = std::wstring{std::istreambuf_iterator<wchar_t>(configRead), {}};
configRead.close();
VERIFY_ARE_EQUAL(customWslConfigContentActual, customWslConfigContentExpected);
}
}
TEST_METHOD(LaunchWslSettingsFromProtocol)
{
WSL_SETTINGS_TEST();
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
SHELLEXECUTEINFOW execInfo{};
execInfo.cbSize = sizeof(execInfo);
execInfo.fMask = SEE_MASK_CLASSNAME | SEE_MASK_FLAG_NO_UI | SEE_MASK_NOCLOSEPROCESS;
execInfo.lpClass = L"wsl-settings";
execInfo.lpFile = L"wsl-settings://";
execInfo.nShow = SW_HIDE;
VERIFY_WIN32_BOOL_SUCCEEDED(ShellExecuteExW(&execInfo));
const wil::unique_process_handle process{execInfo.hProcess};
VERIFY_IS_NOT_NULL(process.get());
auto killProcess = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&process]() {
if (process)
{
LOG_IF_WIN32_BOOL_FALSE(TerminateProcess(process.get(), 0));
}
});
const auto moduleFileName = wil::GetModuleFileNameExW<std::wstring>(process.get(), nullptr);
const auto findExeName = moduleFileName.find(L"wslsettings.exe");
VERIFY_ARE_NOT_EQUAL(findExeName, std::wstring::npos);
}
TEST_METHOD(ManageDefaultUid)
{
const auto distroKey = OpenDistributionKey(LXSS_DISTRO_NAME_TEST_L);
auto assertDefaultUid = [&](ULONG ExpectedUid) {
const auto uid = wsl::windows::common::registry::ReadDword(distroKey.get(), nullptr, L"DefaultUid", 0);
VERIFY_ARE_EQUAL(ExpectedUid, uid);
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"id -u");
while (!out.empty() && (out.back() == '\n' || out.back() == '\r'))
{
out.pop_back();
}
VERIFY_ARE_EQUAL(out, std::to_wstring(ExpectedUid));
};
assertDefaultUid(0);
auto validateUidChange =
[&](const std::wstring& User, ULONG expectedDefaultUid, LPCWSTR ExpectedOutput, const std::wstring& ExpectedError, int ExpectedExitCode) {
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(
std::format(L"--manage {} --set-default-user {}", LXSS_DISTRO_NAME_TEST_L, User), ExpectedExitCode);
VERIFY_ARE_EQUAL(out, ExpectedOutput);
VERIFY_ARE_EQUAL(err, ExpectedError);
assertDefaultUid(expectedDefaultUid);
};
validateUidChange(L"root", 0, L"The operation completed successfully. \r\n", L"", 0);
constexpr auto TestUser = L"testuser";
auto cleanup =
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(std::format(L"-u root userdel -f {}", TestUser)); });
ULONG Uid{};
ULONG Gid{};
CreateUser(TestUser, &Uid, &Gid);
VERIFY_ARE_NOT_EQUAL(Uid, 0);
validateUidChange(L"testuser", Uid, L"The operation completed successfully. \r\n", L"", 0);
validateUidChange(L"root", 0, L"The operation completed successfully. \r\n", L"", 0);
const std::wstring invalidUser = L"Nonexistent";
validateUidChange(invalidUser, 0, L"", L"/usr/bin/id: \u2018" + invalidUser + L"\u2019: no such user\n", 1);
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--manage nonexistent --set-default-user root", -1);
VERIFY_ARE_EQUAL(
out, L"There is no distribution with the supplied name.\r\nError code: Wsl/Service/WSL_E_DISTRO_NOT_FOUND\r\n");
}
TEST_METHOD(PostDistroRegistrationSettingsOOBE)
{
WSL_SETTINGS_TEST();
wsl::windows::common::SvcComm service;
const auto distros = service.EnumerateDistributions();
if (distros.size() != 1)
{
LogSkipped("Test distro as the only distro is required to run this test.");
return;
}
const auto lxssKey = wsl::windows::common::registry::OpenLxssUserKey();
// Test setup should set OOBEComplete
VERIFY_ARE_EQUAL(bool(wsl::windows::common::registry::ReadDword(lxssKey.get(), nullptr, LXSS_OOBE_COMPLETE_NAME, false)), true);
// Delete the OOBEComplete reg value to simulate OOBE not being complete
wsl::windows::common::registry::DeleteValue(lxssKey.get(), LXSS_OOBE_COMPLETE_NAME);
// Restore the OOBEComplete reg value in case of failure
auto restoreOOBEComplete = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
wsl::windows::common::registry::WriteDword(lxssKey.get(), nullptr, LXSS_OOBE_COMPLETE_NAME, true);
});
constexpr auto wslSettingsWindowName = L"Welcome to Windows Subsystem for Linux";
VERIFY_ARE_EQUAL(FindWindowEx(nullptr, nullptr, nullptr, wslSettingsWindowName), nullptr);
auto testDistro = distros.front();
VERIFY_IS_TRUE(wsl::shared::string::IsEqual(testDistro.DistroName, LXSS_DISTRO_NAME_TEST_L, false));
// Get the original BasePath in order to restore the test distro as before.
auto guidStringWithBraces = wsl::shared::string::GuidToString<wchar_t>(testDistro.DistroGuid);
auto testDistroBasePath =
wsl::windows::common::registry::ReadString(lxssKey.get(), guidStringWithBraces.c_str(), L"BasePath", L"");
VERIFY_ARE_NOT_EQUAL(testDistroBasePath, L"");
if (LxsstuVmMode())
{
const auto testDistroVhdPath = std::filesystem::path(testDistroBasePath) / LXSS_VM_MODE_VHD_NAME;
VERIFY_IS_TRUE(std::filesystem::exists(testDistroVhdPath));
const auto testDistroVhdPathExported = std::filesystem::path(testDistroBasePath) / L"exported.vhdx";
WslShutdown();
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(L"--export {} \"{}\" --vhd", testDistro.DistroName, testDistroVhdPathExported.c_str())), 0u);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--unregister {}", testDistro.DistroName)), 0u);
VERIFY_IS_FALSE(std::filesystem::exists(testDistroVhdPath));
VERIFY_IS_TRUE(service.EnumerateDistributions().empty());
std::error_code ec{};
std::filesystem::rename(testDistroVhdPathExported, testDistroVhdPath, ec);
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(L"--import-in-place {} \"{}\"", testDistro.DistroName, testDistroVhdPath.c_str())), 0L);
}
else
{
const auto testDistroRootfsPath = std::filesystem::path(testDistroBasePath) / LXSS_ROOTFS_DIRECTORY;
VERIFY_IS_TRUE(std::filesystem::exists(testDistroRootfsPath));
const auto testDistroExported = std::filesystem::path(testDistroBasePath) / L"exported.tar";
auto deleteTar = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { DeleteFile(testDistroExported.c_str()); });
WslShutdown();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export {} \"{}\"", testDistro.DistroName, testDistroExported.c_str())), 0u);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--unregister {}", testDistro.DistroName)), 0u);
VERIFY_IS_FALSE(std::filesystem::exists(testDistroRootfsPath));
VERIFY_IS_TRUE(service.EnumerateDistributions().empty());
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(
L"--import {} \"{}\" \"{}\" --version 1", testDistro.DistroName, testDistroBasePath, testDistroExported.c_str())),
0L);
}
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--set-default {}", testDistro.DistroName)), 0);
VERIFY_ARE_EQUAL(service.EnumerateDistributions().size(), 1);
HWND wslSettingsWindow{};
const auto findWslSettingsWindowAttempts = 60;
for (auto attempt = 0; attempt < findWslSettingsWindowAttempts; ++attempt)
{
wslSettingsWindow = FindWindowEx(nullptr, nullptr, nullptr, wslSettingsWindowName);
if (wslSettingsWindow)
{
break;
}
Sleep(500);
}
VERIFY_ARE_NOT_EQUAL(wslSettingsWindow, nullptr);
SendMessage(wslSettingsWindow, WM_CLOSE, 0, 0);
VERIFY_ARE_EQUAL(bool(wsl::windows::common::registry::ReadDword(lxssKey.get(), nullptr, LXSS_OOBE_COMPLETE_NAME, false)), true);
}
TEST_METHOD(VersionFlavorParsing)
{
DWORD currentVersion = LxsstuVmMode() ? 2 : 1;
DWORD convertVersion = LxsstuVmMode() ? 1 : 2;
const auto lxssKey = wsl::windows::common::registry::OpenLxssUserKey();
auto validateFlavorVersion = [&](LPCWSTR Distro, LPCWSTR ExpectedFlavor, LPCWSTR ExpectedVersion) {
const auto testDistroId = GetDistributionId(Distro);
VERIFY_IS_TRUE(testDistroId.has_value());
const auto distroId = wsl::shared::string::GuidToString<wchar_t>(testDistroId.value());
TerminateDistribution(Distro);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} cat /etc/os-release || true", Distro).c_str()), 0L);
const auto flavor = wsl::windows::common::registry::ReadString(lxssKey.get(), distroId.c_str(), L"Flavor", L"");
const auto version = wsl::windows::common::registry::ReadString(lxssKey.get(), distroId.c_str(), L"OsVersion", L"");
VERIFY_ARE_EQUAL(ExpectedFlavor, flavor);
VERIFY_ARE_EQUAL(ExpectedVersion, version);
};
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"debian", L"12");
constexpr auto testTar = L"exported-distro.tar";
constexpr auto tmpDistroName = L"tmpdistro";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
DeleteFile(testTar);
LxsstuLaunchWsl(std::format(L"--unregister {}", tmpDistroName));
});
DistroFileChange osRelease(L"/etc/os-release");
{
osRelease.SetContent(
LR"(
ID=Distro
VERSION_ID=Version
)");
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"Distro", L"Version");
}
{
osRelease.SetContent(
LR"(
DISTRO_I=Wrong
ID="DistroWithQuotes"
VERSION_ID="VersionWithQuotes"
Something else
)");
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"DistroWithQuotes", L"VersionWithQuotes");
}
{
osRelease.SetContent(
LR"(
ID="InvalidFormat!"
VERSION_ID="ValidFormat"
)");
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"DistroWithQuotes", L"ValidFormat");
}
{
osRelease.SetContent(
LR"(
ID="Distro-_.,"
VERSION_ID="ValidFormat"
)");
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"Distro-_.,", L"ValidFormat");
}
{
osRelease.SetContent(
LR"(
ID="Invalid|Format"
VERSION_ID="Invalid|Format"
)");
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"Distro-_.,", L"ValidFormat");
}
{
osRelease.Delete(); // Nothing should happen if the file is deleted, but the distro should still work.
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"Distro-_.,", L"ValidFormat");
}
// Validate that importing a distro without os-release works.
{
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export {} {}", LXSS_DISTRO_NAME_TEST_L, testTar).c_str()), 0L);
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(L"--import {} . {} --version {}", tmpDistroName, testTar, currentVersion).c_str()), 0L);
validateFlavorVersion(tmpDistroName, L"", L"");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} echo -e 'VERSION_ID=v' > /etc/os-release", tmpDistroName).c_str()), 0L);
validateFlavorVersion(tmpDistroName, L"", L"v");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--unregister {}", tmpDistroName).c_str()), 0L);
}
// Validate that importing and then converting also behaves correctly when there's no os-release
{
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(L"--import {} . {} --version {}", tmpDistroName, testTar, convertVersion).c_str()), 0L);
validateFlavorVersion(tmpDistroName, L"", L"");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--set-version {} {}", tmpDistroName, currentVersion).c_str()), 0L);
validateFlavorVersion(tmpDistroName, L"", L"");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} echo -e 'VERSION_ID=v2' > /etc/os-release", tmpDistroName).c_str()), 0L);
validateFlavorVersion(tmpDistroName, L"", L"v2");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--unregister {}", tmpDistroName).c_str()), 0L);
}
// Verify that importing a distribution with an os-release as then converting works as well
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(L"--import {} . {} --version {}", tmpDistroName, g_testDistroPath, convertVersion).c_str()), 0L);
validateFlavorVersion(tmpDistroName, L"debian", L"12");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--set-version {} {}", tmpDistroName, currentVersion).c_str()), 0L);
validateFlavorVersion(tmpDistroName, L"debian", L"12");
}
TEST_METHOD(DistributionId)
{
using namespace wsl::windows::common::string;
const auto testDistroId = GetDistributionId(LXSS_DISTRO_NAME_TEST_L);
VERIFY_IS_TRUE(testDistroId.has_value());
auto validateOutput = [](const std::wstring& Cmd, LPCWSTR ExpectedOutput, int ExitCode = 0) {
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(Cmd, ExitCode);
VERIFY_ARE_EQUAL(out, ExpectedOutput);
};
validateOutput(
std::format(
L"--distribution-id {} echo -n OK",
wsl::shared::string::GuidToString<wchar_t>(testDistroId.value(), wsl::shared::string::GuidToStringFlags::None)),
L"OK");
validateOutput(
std::format(
L"--distribution-id {} echo -n OK",
wsl::shared::string::GuidToString<wchar_t>(testDistroId.value(), wsl::shared::string::GuidToStringFlags::AddBraces)),
L"OK");
validateOutput(
std::format(
L"--distribution-id {} echo -n OK",
wsl::shared::string::GuidToString<wchar_t>(testDistroId.value(), wsl::shared::string::GuidToStringFlags::Uppercase)),
L"OK");
validateOutput(L"--distribution-id InvalidGuid", L"The parameter is incorrect. \r\nError code: Wsl/E_INVALIDARG\r\n", -1);
validateOutput(
L"--distribution-id {C13B2B63-F9D5-4840-8105-F6ABECCF46CA}",
L"There is no distribution with the supplied name.\r\nError code: "
L"Wsl/Service/CreateInstance/ReadDistroConfig/WSL_E_DISTRO_NOT_FOUND\r\n",
-1);
}
TEST_METHOD(ModernOOBE)
{
const auto lxssKey = wsl::windows::common::registry::OpenLxssUserKey();
const auto testDistroId = GetDistributionId(LXSS_DISTRO_NAME_TEST_L);
VERIFY_IS_TRUE(testDistroId.has_value());
const auto testDistroIdString = wsl::shared::string::GuidToString<wchar_t>(testDistroId.value());
DistroFileChange distributionconf(L"/etc/wsl-distribution.conf", false);
distributionconf.SetContent(L"[oobe]\ncommand = /bin/bash -c 'echo OOBE'\n");
RegistryKeyChange<DWORD> runOOBE(lxssKey.get(), testDistroIdString.c_str(), L"RunOOBE", 1);
const RegistryKeyChange<DWORD> defaultUid(lxssKey.get(), testDistroIdString.c_str(), L"DefaultUid", 0);
auto validateOutput = [](LPCWSTR Cmd, LPCWSTR ExpectedOutput, LPCWSTR ExpectedWarnings = L"", DWORD ExpectedExitCode = 0) {
auto [read, write] = CreateSubprocessPipe(true, false);
write.reset();
wsl::windows::common::SubProcess process(nullptr, LxssGenerateWslCommandLine(Cmd).c_str());
process.SetStdHandles(read.get(), nullptr, nullptr);
const auto output = process.RunAndCaptureOutput();
VERIFY_ARE_EQUAL(ExpectedExitCode, output.ExitCode);
VERIFY_ARE_EQUAL(ExpectedOutput, output.Stdout);
VERIFY_ARE_EQUAL(ExpectedWarnings, output.Stderr);
};
{
TerminateDistribution();
// Non-interactive commands shouldn't trigger OOBE
validateOutput(L"echo no oobe", L"no oobe\n");
VERIFY_ARE_EQUAL(runOOBE.Get(), 1);
// Interactive shell should trigger OOBE
validateOutput(nullptr, L"OOBE\n");
VERIFY_ARE_EQUAL(runOOBE.Get(), 0);
// OOBE should only trigger once
validateOutput(L"", L"");
}
{
runOOBE.Set(1);
distributionconf.SetContent(L"[oobe]\ncommand = /bin/bash -c 'echo failed OOBE && exit 1'\n");
TerminateDistribution();
constexpr auto expectedStdErr = L"OOBE command \"/bin/bash -c 'echo failed OOBE && exit 1'\" failed, exiting\n";
validateOutput(nullptr, L"failed OOBE\n", expectedStdErr, 1);
VERIFY_ARE_EQUAL(runOOBE.Get(), 1);
// Failed OOBE command should be retried
TerminateDistribution();
validateOutput(nullptr, L"failed OOBE\n", expectedStdErr, 1);
VERIFY_ARE_EQUAL(runOOBE.Get(), 1);
}
{
runOOBE.Set(1);
distributionconf.SetContent(
L"[oobe]\ncommand = /bin/bash -c 'echo OOBE && useradd -u 1010 -m -s /bin/bash user'\n defaultUid = 1010\n");
TerminateDistribution();
validateOutput(nullptr, L"OOBE\n");
VERIFY_ARE_EQUAL(runOOBE.Get(), 0);
// Validate that DefaultUid was set
validateOutput(L"id -u", L"1010\n");
VERIFY_ARE_EQUAL(defaultUid.Get(), 1010);
}
// Verify that the default UID isn't changed if it's not present in wsl-distribution.conf.
{
runOOBE.Set(1);
distributionconf.SetContent(L"[oobe]\ncommand = /bin/bash -c 'echo OOBE'");
TerminateDistribution();
validateOutput(nullptr, L"OOBE\n");
VERIFY_ARE_EQUAL(defaultUid.Get(), 1010);
}
// Verify that OOBE doesn't run if a distribution is installed via wsl --import
{
constexpr auto testDir = L"test-oobe-import";
constexpr auto testDistroName = L"test-oobe-import";
std::filesystem::create_directory(testDir);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() {
LxsstuLaunchWsl(std::format(L"--unregister {}", testDistroName));
std::error_code error;
std::filesystem::remove_all(testDir, error);
});
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export {} {}/exported.tar", LXSS_DISTRO_NAME_TEST_L, testDir)), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} {} {}/exported.tar", testDistroName, testDir, testDistroName)), 0L);
const auto distroKey = OpenDistributionKey(testDistroName);
VERIFY_ARE_EQUAL(wsl::windows::common::registry::ReadDword(distroKey.get(), nullptr, L"RunOOBE", 1), 0);
validateOutput(nullptr, L"");
}
// Make sure the defaultUid is reset for next test case.
TerminateDistribution();
}
static void ValidateDistributionStarts(LPCWSTR Name)
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo -n OK", Name));
VERIFY_ARE_EQUAL(out, L"OK");
}
TEST_METHOD(InstallWithBrokenDefault)
{
// This test case validates that a broken 'DefaultDistribution' value doesn't prevent installing new distributions.
// Create a broken default
RegistryKeyChange defaultDistro(
HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss",
L"DefaultDistribution",
std::wstring{L"{1DB260CB-912D-432A-B898-518DFD0F374E}"});
// Validate that installing a new distribution succeeds.
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(L"--unregister test_new_default"); });
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(L"--install --from-file \"{}\" --no-launch --name test_new_default", g_testDistroPath)), 0L);
auto [out, error] = LxsstuLaunchWslAndCaptureOutput(L"-d test_new_default echo OK");
VERIFY_ARE_EQUAL(out, L"OK\n");
VERIFY_ARE_EQUAL(error, L"");
// Verify that the default distribution is updated
const auto key = wsl::windows::common::registry::OpenLxssUserKey();
const auto defaultValue = wsl::windows::common::registry::ReadString(key.get(), nullptr, L"DefaultDistribution");
VERIFY_ARE_EQUAL(GetDistributionId(L"test_new_default").value_or(GUID_NULL), wsl::shared::string::ToGuid(defaultValue));
}
TEST_METHOD(ModernInstall)
{
using namespace wsl::windows::common::wslutil;
using namespace wsl::windows::common::string;
constexpr auto IconPath = L"test-icon.ico";
auto CreateTarFromManifest = [](LPCWSTR Manifest, LPCWSTR TarName) {
DistroFileChange distributionconf(L"/etc/wsl-distribution.conf", false);
distributionconf.SetContent(Manifest);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export test_distro {}", TarName)), 0L);
};
auto InstallFromTar =
[](LPCWSTR TarName, LPCWSTR ExtraArgs = L"", int ExpectedExitCode = 0, LPCWSTR ExpectedOutput = nullptr, LPCWSTR ExpectedWarnings = nullptr) {
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(
std::format(L"--install --no-launch --from-file {} {}", TarName, ExtraArgs), ExpectedExitCode);
if (ExpectedOutput != nullptr)
{
VERIFY_ARE_EQUAL(ExpectedOutput, out);
}
if (ExpectedWarnings != nullptr)
{
VERIFY_ARE_EQUAL(ExpectedWarnings, err);
}
};
auto installLocation = wsl::windows::common::wslutil::GetMsiPackagePath();
VERIFY_IS_TRUE(installLocation.has_value());
auto wslExePath = installLocation.value() + L"wsl.exe";
wil::unique_hmodule wslExe{LoadLibrary(wslExePath.c_str())};
VERIFY_IS_TRUE(!!wslExe);
auto resource = FindResource(wslExe.get(), MAKEINTRESOURCE(1), RT_ICON);
VERIFY_IS_TRUE(resource != nullptr);
auto loadedResource = LoadResource(wslExe.get(), resource);
const void* iconAddress = LockResource(loadedResource);
wil::unique_handle icon{CreateFile(IconPath, GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, nullptr)};
VERIFY_IS_TRUE(!!icon);
DWORD bytes{};
VERIFY_IS_TRUE(WriteFile(icon.get(), iconAddress, SizeofResource(wslExe.get(), resource), &bytes, nullptr));
LogInfo("Created icon %ls (%lu bytes)", IconPath, bytes);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"cp '{}' /icon.ico", IconPath)), 0L);
// Distribution with default name and icon
{
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
LxsstuLaunchWsl(L"--unregister test-default-name");
DeleteFile(L"distro-default-name-icon.tar");
});
CreateTarFromManifest(
L"[shortcut]\nicon = /icon.ico\n[oobe]\ndefaultName = test-default-name", L"distro-default-name-icon.tar");
//
// Validate that the distribution icon path is also correct when installing via wsl --import.
//
{
constexpr auto distroName = L"TestCustomLocation";
auto currentDirectory = std::filesystem::absolute(std::filesystem::current_path()).wstring();
for (const auto& location : {currentDirectory, std::wstring(L".")})
{
auto cleanup = wil::scope_exit_log(
WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(std::format(L"--unregister {}", distroName)); });
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(
std::format(L"--import {} \"{}\" {}", distroName, location, "distro-default-name-icon.tar")),
0L);
auto [json, profile_path] = ValidateDistributionTerminalProfile(distroName, false);
VERIFY_ARE_EQUAL(
json["profiles"][1]["icon"].get<std::string>(), (std::filesystem::absolute(".") / "shortcut.ico").string());
}
}
InstallFromTar(L"distro-default-name-icon.tar");
ValidateDistributionStarts(L"test-default-name");
// Validate that the distribution was installed under the right name
auto distroKey = OpenDistributionKey(L"test-default-name");
VERIFY_IS_TRUE(!!distroKey);
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
ValidateDistributionShortcut(L"test-default-name", icon.get());
auto [json, profile_path] = ValidateDistributionTerminalProfile(L"test-default-name", false);
VERIFY_IS_TRUE(std::filesystem::exists(profile_path));
cleanup.reset();
// Terminal profile should be removed when the distribution is unregistered.
VERIFY_IS_FALSE(std::filesystem::exists(profile_path));
// Validate that the base path is removed and that the shortcut is gone*
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
}
// Distribution with default name and no icon
{
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
LxsstuLaunchWsl(L"--unregister test-default-name");
DeleteFile(L"distro-default-name-no-icon.tar");
});
CreateTarFromManifest(L"\n[oobe]\ndefaultName = test-default-name", L"distro-default-name-no-icon.tar");
InstallFromTar(L"distro-default-name-no-icon.tar");
ValidateDistributionStarts(L"test-default-name");
// Validate that the distribution was installed under the right name and icon
auto distroKey = OpenDistributionKey(L"test-default-name");
VERIFY_IS_TRUE(!!distroKey);
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
ValidateDistributionShortcut(L"test-default-name", nullptr);
cleanup.reset();
// Validate that the base path is removed and that the shortcut is gone*
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
}
// Distribution with no default name
{
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
LxsstuLaunchWsl(L"--unregister test-distro-no-default-name");
DeleteFile(L"distro-no-default-name.tar");
});
CreateTarFromManifest(L"", L"distro-no-default-name.tar");
// Import should fail without --name
constexpr auto expectedOutput =
L"Installing: distro-no-default-name.tar\r\n\
This distribution doesn't contain a default name. Use --name to chose the distribution name.\r\n\
Error code: Wsl/Service/RegisterDistro/WSL_E_DISTRIBUTION_NAME_NEEDED\r\n";
InstallFromTar(L"distro-no-default-name.tar", L"", -1, expectedOutput);
// And succeed with --name
InstallFromTar(L"distro-no-default-name.tar", L"--name test-distro-no-default-name");
ValidateDistributionStarts(L"test-distro-no-default-name");
auto distroKey = OpenDistributionKey(L"test-distro-no-default-name");
VERIFY_IS_TRUE(!!distroKey);
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
ValidateDistributionShortcut(L"test-distro-no-default-name", nullptr);
cleanup.reset();
// Validate that the base path is removed and that the shortcut is gone*
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
}
// Distribution specifying a VHD size.
auto InstallWithVhdSize = [&](bool FixedVhd) {
constexpr auto distroName = L"distro-vhd-size";
constexpr auto tarFileName = L"distro-vhd-size.tar";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName));
DeleteFile(tarFileName);
});
CreateTarFromManifest(std::format(L"[shortcut]\nicon = /icon.ico\n[oobe]\ndefaultName = {}", distroName).c_str(), tarFileName);
InstallFromTar(tarFileName, std::format(L"--vhd-size 1GB {}", FixedVhd ? L"--fixed-vhd" : L"").c_str());
ValidateDistributionStarts(distroName);
// Terminate the VM to make sure the VHD is not in use.
WslShutdown();
// Validate that the distribution was installed under the right name
auto distroKey = OpenDistributionKey(distroName);
VERIFY_IS_TRUE(!!distroKey);
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
ValidateDistributionShortcut(distroName, icon.get());
auto [json, profile_path] = ValidateDistributionTerminalProfile(distroName, false);
VERIFY_IS_TRUE(std::filesystem::exists(profile_path));
// Verify that the is the correct type.
{
std::filesystem::path vhdFilePath = std::filesystem::path(basePath) / LXSS_VM_MODE_VHD_NAME;
VIRTUAL_STORAGE_TYPE storageType{};
storageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN;
storageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN;
wil::unique_handle disk;
THROW_IF_WIN32_ERROR(OpenVirtualDisk(
&storageType, vhdFilePath.c_str(), VIRTUAL_DISK_ACCESS_GET_INFO, OPEN_VIRTUAL_DISK_FLAG_NONE, nullptr, &disk));
GET_VIRTUAL_DISK_INFO diskInfo{};
diskInfo.Version = GET_VIRTUAL_DISK_INFO_VIRTUAL_STORAGE_TYPE;
ULONG diskInfoSize = sizeof(diskInfo);
THROW_IF_WIN32_ERROR(GetVirtualDiskInformation(disk.get(), &diskInfoSize, &diskInfo, nullptr));
VERIFY_IS_TRUE(diskInfo.VirtualStorageType.DeviceId == VIRTUAL_STORAGE_TYPE_DEVICE_VHDX);
diskInfo.Version = GET_VIRTUAL_DISK_INFO_PROVIDER_SUBTYPE;
diskInfoSize = sizeof(diskInfo);
THROW_IF_WIN32_ERROR(GetVirtualDiskInformation(disk.get(), &diskInfoSize, &diskInfo, nullptr));
VERIFY_ARE_EQUAL(FixedVhd, diskInfo.ProviderSubtype == 2);
}
// Unregister the distribution.
cleanup.reset();
// Terminal profile should be removed when the distribution is unregistered.
VERIFY_IS_FALSE(std::filesystem::exists(profile_path));
// Validate that the base path is removed and that the shortcut is gone*
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
};
InstallWithVhdSize(false);
InstallWithVhdSize(true);
// Distribution imported in place
if (LxsstuVmMode())
{
auto CreateVhdFromManifest = [](LPCWSTR Manifest, LPCWSTR VhdName) {
DistroFileChange distributionconf(L"/etc/wsl-distribution.conf", false);
distributionconf.SetContent(Manifest);
WslShutdown();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export test_distro {} --format vhd", VhdName)), 0L);
};
auto InstallFromVhd =
[](LPCWSTR DistroName, LPCWSTR VhdName, int ExpectedExitCode = 0, LPCWSTR ExpectedOutput = nullptr, LPCWSTR ExpectedWarnings = nullptr) {
auto [out, err] =
LxsstuLaunchWslAndCaptureOutput(std::format(L"--import-in-place {} {}", DistroName, VhdName), ExpectedExitCode);
if (ExpectedOutput != nullptr)
{
VERIFY_ARE_EQUAL(ExpectedOutput, out);
}
if (ExpectedWarnings != nullptr)
{
VERIFY_ARE_EQUAL(ExpectedWarnings, err);
}
};
const auto distroName = L"distro-import-in-place";
const auto vhdName = L"distro-import-in-place.vhdx";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName).c_str());
DeleteFileW(vhdName);
});
CreateVhdFromManifest(L"", vhdName);
InstallFromVhd(distroName, vhdName);
ValidateDistributionStarts(distroName);
// Validate that the distribution was installed under the right name
auto distroKey = OpenDistributionKey(distroName);
VERIFY_IS_TRUE(!!distroKey);
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
ValidateDistributionShortcut(distroName, nullptr);
auto [json, profile_path] = ValidateDistributionTerminalProfile(distroName, true);
VERIFY_IS_TRUE(std::filesystem::exists(profile_path));
cleanup.reset();
// Terminal profile should be removed when the distribution is unregistered.
VERIFY_IS_FALSE(std::filesystem::exists(profile_path));
// Validate that the shortcut is gone
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
}
// Distribution with overridden default location
{
auto cleanup = wil::scope_exit_log(
WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(L"--unregister test-overridden-default-location"); });
auto currentPath = std::filesystem::current_path();
WslConfigChange wslconfig(std::format(L"[general]\ndistributionInstallPath = {}", EscapePath(currentPath.wstring())));
InstallFromTar(g_testDistroPath.c_str(), L"--name test-overridden-default-location");
ValidateDistributionStarts(L"test-overridden-default-location");
auto distroKey = OpenDistributionKey(L"test-overridden-default-location");
VERIFY_IS_TRUE(!!distroKey);
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
// Validate that the distribution was created in the correct path
VERIFY_ARE_EQUAL(std::filesystem::path(basePath).parent_path().string(), currentPath.string());
ValidateDistributionShortcut(L"test-overridden-default-location", nullptr);
cleanup.reset();
// Validate that the base path is removed and that the shortcut is gone*
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
}
// Distribution installed in a custom location
{
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(L"--unregister test-custom-location"); });
InstallFromTar(g_testDistroPath.c_str(), L"--name test-custom-location --location test-distro-folder");
ValidateDistributionStarts(L"test-custom-location");
// Validate that the distribution was installed under the right name
auto distroKey = OpenDistributionKey(L"test-custom-location");
VERIFY_IS_TRUE(!!distroKey);
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
VERIFY_ARE_EQUAL(std::filesystem::absolute("test-distro-folder").wstring(), basePath);
ValidateDistributionShortcut(L"test-custom-location", nullptr);
cleanup.reset();
// Validate that the base path is removed and that the shortcut is gone*
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
}
// Distribution installed from stdin
{
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(L"--unregister test-install-stdin"); });
wil::unique_handle importTar{
CreateFile(g_testDistroPath.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, HANDLE_FLAG_INHERIT, nullptr)};
VERIFY_IS_TRUE(!!importTar);
VERIFY_IS_TRUE(SetHandleInformation(importTar.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--install --no-launch --from-file - --name test-install-stdin", importTar.get()), 0L);
ValidateDistributionStarts(L"test-install-stdin");
// Validate that the distribution was installed under the right name
auto distroKey = OpenDistributionKey(L"test-install-stdin");
VERIFY_IS_TRUE(!!distroKey);
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
ValidateDistributionShortcut(L"test-install-stdin", nullptr);
cleanup.reset();
// Validate that the base path is removed and that the shortcut is gone*
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
}
// Distribution default name conflicts with already installed distribution
{
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(L"conflict.tar"); });
CreateTarFromManifest(L"[oobe]\ndefaultName = test_distro", L"conflict.tar");
constexpr auto expectedOutput =
L"Installing: conflict.tar\r\n\
A distribution with the supplied name already exists. Use --name to chose a different name.\r\n\
Error code: Wsl/Service/RegisterDistro/ERROR_ALREADY_EXISTS\r\n";
InstallFromTar(L"conflict.tar", L"", -1, expectedOutput);
}
// Distribution default name is invalid
{
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(L"invalid.tar"); });
CreateTarFromManifest(L"[oobe]\ndefaultName = invalid!", L"invalid.tar");
constexpr auto expectedOutput =
L"Installing: invalid.tar\r\n\
Invalid distribution name: \"invalid!\".\r\n\
Error code: Wsl/Service/RegisterDistro/E_INVALIDARG\r\n";
InstallFromTar(L"invalid.tar", L"", -1, expectedOutput);
}
// Distribution icon file is too big
{
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
DeleteFile(L"big-icon.tar");
LxsstuLaunchWsl(L"--unregister big-icon");
});
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"fallocate /icon.ico -l 20MB"), 0L);
CreateTarFromManifest(L"[shortcut]\nicon = /icon.ico", L"big-icon.tar");
WslKeepAlive keepAlive;
InstallFromTar(L"big-icon.tar", L"--name big-icon");
ValidateDistributionStarts(L"big-icon");
if (LxsstuVmMode())
{
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"dmesg | grep -iz 'File.*is too big' > /dev/null"), 0L);
}
// Validate that the distribution was installed under the right name
auto distroKey = OpenDistributionKey(L"big-icon");
VERIFY_IS_TRUE(!!distroKey);
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
ValidateDistributionShortcut(L"big-icon", nullptr);
cleanup.reset();
// Validate that the base path is removed and that the shortcut is gone*
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
}
// Distribution icon file doesn't exist
{
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
DeleteFile(L"icon-not-found.tar");
LxsstuLaunchWsl(L"--unregister icon-not-found");
});
CreateTarFromManifest(L"[shortcut]\nicon = /does-not-exist.ico", L"icon-not-found.tar");
InstallFromTar(L"icon-not-found.tar", L"--name icon-not-found");
ValidateDistributionStarts(L"icon-not-found");
// Validate that the distribution was installed under the right name
auto distroKey = OpenDistributionKey(L"icon-not-found");
VERIFY_IS_TRUE(!!distroKey);
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
ValidateDistributionShortcut(L"icon-not-found", nullptr);
cleanup.reset();
// Validate that the base path is removed and that the shortcut is gone*
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
}
// Distribution with a custom terminal profile
{
constexpr auto distroName = L"custom-terminal-profile";
constexpr auto tarName = L"custom-terminal-profile.tar";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
DeleteFile(tarName);
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName));
});
DistroFileChange profileTemplate(L"/terminal.json", false);
constexpr auto templateContent =
LR"(
{
"profiles": [{"custom-field": "custom-value"}],
"schemes": [{"name": "my-scheme"}]
})";
profileTemplate.SetContent(templateContent);
CreateTarFromManifest(L"[windowsterminal]\nprofileTemplate = /terminal.json", tarName);
InstallFromTar(tarName, std::format(L"--name {}", distroName).c_str());
ValidateDistributionStarts(distroName);
auto distroKey = OpenDistributionKey(distroName);
VERIFY_IS_TRUE(!!distroKey);
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
auto [json, profile_path] = ValidateDistributionTerminalProfile(distroName, true);
VERIFY_ARE_EQUAL(json["profiles"][1]["custom-field"].get<std::string>(), "custom-value");
VERIFY_ARE_EQUAL(json["schemes"][0]["name"].get<std::string>(), "my-scheme");
VERIFY_IS_TRUE(std::filesystem::exists(profile_path));
cleanup.reset();
// Terminal profile should be removed when the distribution is unregistered.
VERIFY_IS_FALSE(std::filesystem::exists(profile_path));
}
// Distribution with an invalid terminal profile json
{
constexpr auto distroName = L"custom-terminal-profile-bad-json";
constexpr auto tarName = L"custom-terminal-profile-bad-json.tar";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
DeleteFile(tarName);
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName));
});
DistroFileChange profileTemplate(L"/terminal.json", false);
profileTemplate.SetContent(L"bad-json");
CreateTarFromManifest(L"[windowsterminal]\nprofileTemplate = /terminal.json", tarName);
// Validate the invalid json blob generates a warning.
InstallFromTar(
tarName,
std::format(L"--name {}", distroName).c_str(),
0,
nullptr,
L"wsl: Failed to parse terminal profile while registering distribution: [json.exception.parse_error.101] "
L"parse "
L"error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'b'\r\n");
ValidateDistributionStarts(distroName);
}
// Distribution with a preexisting hide profile.
{
constexpr auto distroName = L"custom-terminal-profile-hide";
constexpr auto tarName = L"custom-terminal-profile-hide.tar";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
DeleteFile(tarName);
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName));
});
auto profileGuid = wsl::shared::string::GuidToString<wchar_t>(
CreateV5Uuid(GeneratedProfilesTerminalNamespace, std::as_bytes(std::span{std::wstring_view{distroName}})));
auto content = std::format(
LR"({{"profiles": [{{ "updates": "{}", "hidden": true, "custom": true}}, {{"name": "my-profile"}}]}})", profileGuid);
DistroFileChange profileTemplate(L"/terminal.json", false);
profileTemplate.SetContent(content.c_str());
CreateTarFromManifest(L"[windowsterminal]\nprofileTemplate = /terminal.json", tarName);
InstallFromTar(tarName, std::format(L"--name {}", distroName).c_str());
ValidateDistributionStarts(distroName);
auto distroKey = OpenDistributionKey(distroName);
VERIFY_IS_TRUE(!!distroKey);
// Validate that the default terminal profile is still generated.
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
auto [json, profile_path] = ValidateDistributionTerminalProfile(distroName, true);
VERIFY_ARE_EQUAL(json["profiles"][0]["custom"].get<bool>(), true);
VERIFY_ARE_EQUAL(json["profiles"].size(), 2);
VERIFY_IS_TRUE(std::filesystem::exists(profile_path));
VERIFY_ARE_EQUAL(
profile_path, wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"TerminalProfilePath", L""));
cleanup.reset();
// Terminal profile should be removed when the distribution is unregistered.
VERIFY_IS_FALSE(std::filesystem::exists(profile_path));
}
// Distribution opting-out of terminal profile generation
{
constexpr auto distroName = L"no-terminal-profile";
constexpr auto tarName = L"no-terminal-profile.tar";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
DeleteFile(tarName);
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName));
});
CreateTarFromManifest(L"[windowsterminal]\nenabled = false", tarName);
InstallFromTar(tarName, std::format(L"--name {}", distroName).c_str());
auto distroKey = OpenDistributionKey(distroName);
VERIFY_IS_TRUE(!!distroKey);
// Validate that no terminal profile is generated.
VERIFY_ARE_EQUAL(
L"", wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"TerminalProfilePath", L""));
}
// Distribution opting-out of shortcut generation
{
constexpr auto distroName = L"no-shortcut";
constexpr auto tarName = L"no-shortcut.tar";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
DeleteFile(tarName);
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName));
});
CreateTarFromManifest(L"[shortcut]\nenabled = false", tarName);
InstallFromTar(tarName, std::format(L"--name {}", distroName).c_str());
auto distroKey = OpenDistributionKey(distroName);
VERIFY_IS_TRUE(!!distroKey);
// Validate that no terminal profile is generated.
VERIFY_ARE_EQUAL(L"", wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L""));
}
}
static auto SetManifest(const std::string& Content, bool Append = false)
{
auto file = wsl::windows::common::filesystem::TempFile(GENERIC_WRITE, FILE_SHARE_READ, OPEN_EXISTING);
VERIFY_IS_TRUE(WriteFile(file.Handle.get(), Content.c_str(), static_cast<DWORD>(Content.size()), nullptr, nullptr));
RegistryKeyChange<std::wstring> manifestOverride{
HKEY_LOCAL_MACHINE,
LXSS_REGISTRY_PATH,
Append ? wsl::windows::common::distribution::c_distroUrlAppendRegistryValue : wsl::windows::common::distribution::c_distroUrlRegistryValue,
L"file://" + file.Path.wstring()};
return std::make_pair(std::move(file), std::move(manifestOverride));
}
static void ValidateInstall(const std::wstring& cmd, LPCWSTR ExpectedOutput = nullptr)
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--install --no-launch {}", cmd));
if (ExpectedOutput != nullptr)
{
VERIFY_ARE_EQUAL(ExpectedOutput, out);
}
}
static void ValidateInstallError(
const std::wstring& cmd, const std::wstring& expectedOutput, const std::wstring& expectedWarnings = L"")
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(cmd, -1);
VERIFY_ARE_EQUAL(expectedOutput, out);
VERIFY_ARE_EQUAL(expectedWarnings, err);
}
static void UnregisterDistribution(LPCWSTR Name)
{
LxsstuLaunchWsl(std::format(L"--unregister {}", Name));
}
TEST_METHOD(FileUrl)
{
auto check = [](LPCWSTR Input, const std::optional<std::filesystem::path>& ExpectedOutput) {
const auto output = wsl::windows::common::filesystem::TryGetPathFromFileUrl(Input);
VERIFY_ARE_EQUAL(output.has_value(), ExpectedOutput.has_value());
if (output.has_value())
{
VERIFY_ARE_EQUAL(output.value(), ExpectedOutput.value());
}
};
check(L"file://C:/File", L"C:\\File");
check(L"file://C:\\File", L"C:\\File");
check(L"file:///C:\\File", L"C:\\File");
check(L"file:///RelativeFile", L"RelativeFile");
check(L"file:///RelativeFile\\SubPath/SubPath", L"RelativeFile\\SubPath\\SubPath");
check(L"notfile:///C:\\File", {});
}
TEST_METHOD(MacAddressParsing)
{
using namespace wsl::shared::string;
auto testParse = [](const std::wstring& Input, const std::optional<MacAddress>& ExpectedOutput, char separator = '\0') {
const auto result = wsl::shared::string::ParseMacAddressNoThrow<wchar_t>(Input, separator);
VERIFY_ARE_EQUAL(result.has_value(), ExpectedOutput.has_value());
if (result.has_value())
{
VERIFY_ARE_EQUAL(ExpectedOutput.value(), result.value());
}
};
testParse(L"", {});
testParse(L"-", {});
testParse(L"00:00:00:00:00:0", {});
testParse(L"00::00:00:00:00:00", {});
testParse(L"000:00:00:00:00:00", {});
testParse(L"000:00:00:00:00:0g", {});
testParse(L"00:00:00:00:00:00", {{0, 0, 0, 0, 0, 0}});
testParse(L"01:23:45:67:89:AB", {{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}});
testParse(L"01-23-45-67-89-AB", {{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}});
testParse(L"01-23-45-67-89-AB", {{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}}, '-');
testParse(L"01-23-45-67-89-AB", {}, ':');
testParse(L"01-23-45-67-89:AB", {});
testParse(L"01,23,45,67,89,AB", {});
VERIFY_ARE_EQUAL(wsl::shared::string::FormatMacAddress({0x01, 0x23, 0x45, 0x67, 0x89, 0xab}, '-'), "01-23-45-67-89-AB");
VERIFY_ARE_EQUAL(wsl::shared::string::FormatMacAddress({0x01, 0x23, 0x45, 0x67, 0x89, 0xab}, ':'), "01:23:45:67:89:AB");
VERIFY_ARE_EQUAL(
wsl::shared::string::FormatMacAddress({0x01, 0x23, 0x45, 0x67, 0x89, 0xab}, L'-'),
wsl::shared::string::MultiByteToWide("01-23-45-67-89-AB"));
VERIFY_ARE_EQUAL(
wsl::shared::string::FormatMacAddress({0x01, 0x23, 0x45, 0x67, 0x89, 0xab}, L':'),
wsl::shared::string::MultiByteToWide("01:23:45:67:89:AB"));
}
TEST_METHOD(ModernDistroInstall)
{
auto tarPath = "file://" + wsl::shared::string::WideToMultiByte(EscapePath(g_testDistroPath));
wil::unique_handle tarHandle{CreateFile(g_testDistroPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)};
VERIFY_IS_TRUE(!!tarHandle);
auto tarHash = wsl::shared::string::WideToMultiByte(
wsl::windows::common::string::BytesToHex(wsl::windows::common::wslutil::HashFile(tarHandle.get(), CALG_SHA_256)));
// Install a modern distribution
{
auto manifest = std::format(
R"({{
"ModernDistributions": {{
"debian": [
{{
"Name": "debian-12",
"FriendlyName": "DebianFriendlyName",
"Default": true,
"Amd64Url": {{
"Url": "{}",
"Sha256": "{}"
}}
}}
]
}}}})",
tarPath,
tarHash);
auto restore = SetManifest(manifest);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { UnregisterDistribution(L"debian-12"); });
ValidateInstall(L"debian --no-launch --name debian-12");
ValidateDistributionStarts(L"debian-12");
UnregisterDistribution(L"debian-12");
ValidateInstall(L"debian-12 --no-launch --name debian-12");
ValidateDistributionStarts(L"debian-12");
ValidateInstallError(
L"--install DoesNotExists",
L"Invalid distribution name: 'DoesNotExists'.\r\n\
To get a list of valid distributions, use 'wsl.exe --list --online'.\r\n\
Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND\r\n");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister debian-12"), 0L);
// Verify that name matching is not case-sensitive on the version.
ValidateInstall(L"Debian-12 --no-launch --name debian-12");
ValidateDistributionStarts(L"debian-12");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister debian-12"), 0L);
// Verify that name matching is not case-sensitive on the flavor.
ValidateInstall(L"Debian --no-launch --name debian-12");
ValidateDistributionStarts(L"debian-12");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister debian-12"), 0L);
// Validate an install with a vhd size.
ValidateInstall(L"Debian --no-launch --name debian-12 --vhd-size 1GB");
ValidateDistributionStarts(L"debian-12");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister debian-12"), 0L);
// Validate an install with a vhd size and fixed vhd.
ValidateInstall(L"Debian --no-launch --name debian-12 --vhd-size 1GB --fixed-vhd");
ValidateDistributionStarts(L"debian-12");
}
// Validate that default works correctly
{
auto manifest = std::format(
R"({{
"Default": "debian",
"ModernDistributions": {{
"debian": [
{{
"Name": "debian-nondefault",
"FriendlyName": "",
"Amd64Url": {{
"Url": "",
"Sha256": ""
}}
}},
{{
"Name": "debian-default",
"FriendlyName": "DebianFriendlyName",
"Default": true,
"Amd64Url": {{
"Url": "{}",
"Sha256": "{}"
}}
}}
],
"ubuntu": [
{{
"Name": "ubuntu-nondefault",
"FriendlyName": "",
"Amd64Url": {{
"Url": "",
"Sha256": ""
}}
}},
{{
"Name": "ubuntu-default",
"FriendlyName": "UbuntuFriendlyName",
"Default": true,
"Amd64Url": {{
"Url": "{}",
"Sha256": "{}"
}}
}}
]
}}}})",
tarPath,
tarHash,
tarPath,
tarHash);
auto restore = SetManifest(manifest);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
UnregisterDistribution(L"debian-default");
UnregisterDistribution(L"ubuntu-default");
});
ValidateInstall(
L"--no-launch --name debian-default",
L"Installing: DebianFriendlyName\r\n\
Distribution successfully installed. It can be launched via 'wsl.exe -d debian-default'\r\n");
ValidateDistributionStarts(L"debian-default");
ValidateInstall(
L"ubuntu --no-launch --name ubuntu-default",
L"Installing: UbuntuFriendlyName\r\n\
Distribution successfully installed. It can be launched via 'wsl.exe -d ubuntu-default'\r\n");
ValidateDistributionStarts(L"ubuntu-default");
// Validate that default can be override via the 'Append' manifest
auto overrideRestore = SetManifest(R"({"Default": "ubuntu"})", true);
UnregisterDistribution(L"ubuntu-default");
ValidateInstall(
L"--no-launch --name ubuntu-default",
L"Installing: UbuntuFriendlyName\r\n\
Distribution successfully installed. It can be launched via 'wsl.exe -d ubuntu-default'\r\n");
ValidateDistributionStarts(L"ubuntu-default");
}
// Install a legacy distribution
{
auto manifest = std::format(
R"({{
"ModernDistributions": {{
"debian": [
{{
"Name": "debian-12",
"FriendlyName": "DebianFriendlyName",
"Amd64Url": {{
"Url": "",
"Sha256": ""
}}
}}
]
}},
"Distributions": [
{{"Name": "legacy",
"FriendlyName": "legacy",
"StoreAppId": "Dummy",
"PackageFamilyName": "Dummy",
"Amd64": true,
"Arm64": true,
"Amd64PackageUrl": "http://127.0.0.1:12/dummyUrl" }}]
}})",
tarPath);
auto restore = SetManifest(manifest);
// There's no easy way to automate the appx package installation, but verify that we take the legacy path
ValidateInstallError(
L"--install legacy --no-launch --web-download",
L"Downloading: legacy\r\n\
A connection with the server could not be established \r\n\
Error code: Wsl/InstallDistro/WININET_E_CANNOT_CONNECT\r\n",
L"wsl: Using legacy distribution registration. Consider using a tar based distribution instead.\r\n");
ValidateInstallError(
L"--install legacy --no-launch --web-download --legacy",
L"Downloading: legacy\r\n\
A connection with the server could not be established \r\n\
Error code: Wsl/InstallDistro/WININET_E_CANNOT_CONNECT\r\n",
L"wsl: Using legacy distribution registration. Consider using a tar based distribution instead.\r\n");
}
// Validate that modern distros takes precedences, but can be overridden.
{
auto manifest = std::format(
R"({{
"ModernDistributions": {{
"debian": [
{{
"Name": "debian-12",
"FriendlyName": "DebianFriendlyName",
"Amd64Url": {{
"Url": "{}",
"Sha256": "{}"
}}
}}
]
}},
"Distributions": [
{{"Name": "debian-12",
"FriendlyName": "debian-12",
"StoreAppId": "Dummy",
"PackageFamilyName": "Dummy",
"Amd64": true,
"Arm64": true,
"Amd64PackageUrl": "http://127.0.0.1:12/dummyUrl" }}]
}})",
tarPath,
tarHash);
auto restore = SetManifest(manifest);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { UnregisterDistribution(L"debian-12"); });
ValidateInstall(L"debian-12 --no-launch --name debian-12");
ValidateDistributionStarts(L"debian-12");
// Validate that --legacy takes the appx path.
ValidateInstallError(
L"--install debian-12 --no-launch --web-download --legacy",
L"Downloading: debian-12\r\n\
A connection with the server could not be established \r\n\
Error code: Wsl/InstallDistro/WININET_E_CANNOT_CONNECT\r\n",
L"wsl: Using legacy distribution registration. Consider using a tar based distribution instead.\r\n");
}
// Validate that distribution can be overridden
{
auto manifest = std::format(
R"({{
"ModernDistributions": {{
"debian": [
{{
"Name": "debian-12",
"FriendlyName": "DebianFriendlyName",
"Amd64Url": {{
"Url": "{}",
"Sha256": "{}"
}}
}},
{{
"Name": "debian-base",
"FriendlyName": "DebianFriendlyName",
"Default": true,
"Amd64Url": {{
"Url": "{}",
"Sha256": ""
}}
}}
]
}},
"Distributions": [{{"Name": "Dummy", "FriendlyName": "Dummy", "StoreAppId": "Dummy", "Amd64": true, "Arm64": true }}]
}})",
"DoesNotExist",
tarPath,
tarHash);
auto overrideManifest = std::format(
R"({{
"ModernDistributions": {{
"debian": [
{{
"Name": "debian-12",
"FriendlyName": "DebianFriendlyNameOverridden",
"Amd64Url": {{
"Url": "{}",
"Sha256": "{}"
}}
}}
]
}}
}})",
tarPath,
tarHash);
auto restore = SetManifest(manifest);
auto override = SetManifest(overrideManifest, true);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
UnregisterDistribution(L"debian-12");
UnregisterDistribution(L"debian-base");
});
ValidateInstall(L"debian-12 --no-launch --name debian-12");
// Validate that distros coming from the 'main' manifest can still be installed.
ValidateInstall(L"debian-12 --no-launch --name debian-base");
}
// Validate that the distribution default name comes from the manifest, event if oobe.defaultName isn't set
{
auto manifest = std::format(
R"({{
"ModernDistributions": {{
"debian": [
{{
"Name": "test-default-manifest-name",
"FriendlyName": "DebianFriendlyName",
"Amd64Url": {{
"Url": "{}",
"Sha256": "{}"
}}
}}
]
}}
}})",
tarPath,
tarHash);
auto restore = SetManifest(manifest);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { UnregisterDistribution(L"test-default-manifest-name"); });
ValidateInstall(L"test-default-manifest-name");
ValidateDistributionStarts(L"test-default-manifest-name");
}
// Validate that install fails if hash doesn't match
{
auto manifest = std::format(
R"({{
"ModernDistributions": {{
"debian": [
{{
"Name": "debian-12",
"FriendlyName": "DebianFriendlyName",
"Amd64Url": {{
"Url": "{}",
"Sha256": "0x12"
}}
}}
]
}}
}})",
tarPath);
auto restore = SetManifest(manifest);
ValidateInstallError(
L"--install debian-12",
std::format(
L"Installing: DebianFriendlyName\r\n\
The distribution hash doesn't match. Expected: 0x12, actual hash: {}\r\n\
Error code: Wsl/InstallDistro/VerifyChecksum/TRUST_E_BAD_DIGEST\r\n",
wsl::shared::string::MultiByteToWide(tarHash)),
L"");
}
// Validate that we fail if the hash format is incorrect
{
auto manifest = std::format(
R"({{
"ModernDistributions": {{
"debian": [
{{
"Name": "debian-12",
"FriendlyName": "DebianFriendlyName",
"Amd64Url": {{
"Url": "{}",
"Sha256": "wrongformat"
}}
}}
]
}}
}})",
tarPath);
auto restore = SetManifest(manifest);
ValidateInstallError(
L"--install debian-12",
L"Installing: DebianFriendlyName\r\n\
Invalid hex string: wrongformat\r\n\
Error code: Wsl/InstallDistro/VerifyChecksum/E_INVALIDARG\r\n",
L"");
}
// Validate various command line error paths
{
auto manifest =
R"({
"Distributions": [
{"Name": "debian-12",
"FriendlyName": "debian-12",
"StoreAppId": "Dummy",
"PackageFamilyName": "Dummy",
"Amd64": true,
"Arm64": true,
"Amd64PackageUrl": "" }]
})";
auto restore = SetManifest(manifest);
ValidateInstallError(
L"--install debian-12 --location foo",
L"'--location' is not supported when installing legacy distributions.\r\n",
L"");
ValidateInstallError(
L"--install debian-12 --name foo", L"'--name' is not supported when installing legacy distributions.\r\n", L"");
ValidateInstallError(
L"--install debian-12 --vhd-size 1GB",
L"'--vhd-size' is not supported when installing legacy distributions.\r\n",
L"");
ValidateInstallError(
L"--install invalid",
L"Invalid distribution name: 'invalid'.\r\n\
To get a list of valid distributions, use 'wsl.exe --list --online'.\r\n\
Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND\r\n",
L"");
}
// Validate that a distribution isn't downloaded if its name is already in use.
{
auto manifest = std::format(
R"({{
"ModernDistributions": {{
"debian": [
{{
"Name": "{}",
"FriendlyName": "DebianFriendlyName",
"Amd64Url": {{
"Url": "file://nonexistent",
"Sha256": ""
}}
}},
{{
"Name": "dummy",
"FriendlyName": "dummy",
"Amd64Url": {{
"Url": "file://nonexistent",
"Sha256": ""
}}
}}
]
}}
}})",
LXSS_DISTRO_NAME_TEST);
auto restore = SetManifest(manifest);
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--install {}", LXSS_DISTRO_NAME_TEST_L), -1);
VERIFY_ARE_EQUAL(
out,
L"A distribution with the supplied name already exists. Use --name to chose a different name.\r\n"
L"Error code: Wsl/InstallDistro/ERROR_ALREADY_EXISTS\r\n");
VERIFY_ARE_EQUAL(err, L"");
}
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--install dummy --name {}", LXSS_DISTRO_NAME_TEST_L), -1);
VERIFY_ARE_EQUAL(
out,
L"A distribution with the supplied name already exists. Use --name to chose a different name.\r\n"
L"Error code: Wsl/InstallDistro/ERROR_ALREADY_EXISTS\r\n");
VERIFY_ARE_EQUAL(err, L"");
}
}
// Validate handling of case where no default install distro is configured.
{
auto manifest =
R"({
"ModernDistributions": {
"debian": [
{
"Name": "debian-12",
"FriendlyName": "DebianFriendlyName",
"Amd64Url": {
"Url": "",
"Sha256": ""
}
}
]
}
})";
auto restore = SetManifest(manifest);
ValidateInstallError(
L"--install",
L"No default distribution has been configured. Please provide a distribution to install.\r\n\
Error code: Wsl/InstallDistro/E_UNEXPECTED\r\n",
L"");
}
// Validate that invalid json errors are correctly handled.
{
auto restore = SetManifest("Bad json");
ValidateInstallError(
L"--install debian",
L"Invalid JSON document. Parse error: [json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'B'\r\n\
Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n",
L"");
}
// Validate that url parameters are correctly handled.
{
constexpr auto tarEndpoint = L"http://127.0.0.1:6667/";
UniqueWebServer fileServer(tarEndpoint, std::filesystem::path(g_testDistroPath));
wil::unique_handle tarHandle{CreateFile(g_testDistroPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)};
VERIFY_IS_TRUE(!!tarHandle);
auto manifest = std::format(
R"({{
"ModernDistributions": {{
"test": [
{{
"Name": "test-url-download",
"FriendlyName": "FriendlyName",
"Default": true,
"Amd64Url": {{
"Url": "{}/distro.tar?foo=bar&key=value",
"Sha256": "{}"
}}
}}
]
}}}})",
tarEndpoint,
tarHash);
auto restore = SetManifest(manifest);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { UnregisterDistribution(L"test-url-download"); });
auto [output, error] = LxsstuLaunchWslAndCaptureOutput(L"--install --no-launch test-url-download");
VERIFY_ARE_EQUAL(
output,
L"Downloading: FriendlyName\r\nInstalling: FriendlyName\r\nDistribution successfully installed. It can be "
L"launched via 'wsl.exe -d test-url-download'\r\n");
VERIFY_ARE_EQUAL(error, L"");
}
// Validate that manifest distribution ordering is preserved.
{
auto validateOrder = [](const std::vector<LPCWSTR>& expected) {
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--list --online");
auto lines = wsl::shared::string::Split<wchar_t>(out, '\n');
for (size_t i = 0; i < expected.size(); i++)
{
auto end = lines[i + 4].find_first_of(L" \t");
VERIFY_ARE_NOT_EQUAL(end, std::wstring::npos);
auto distro = lines[i + 4].substr(0, end);
VERIFY_ARE_EQUAL(expected[i], distro);
}
};
{
auto manifest =
R"({
"ModernDistributions": {
"distro1": [
{
"Name": "distro1",
"FriendlyName": "distro1Name",
"Amd64Url": {"Url": "","Sha256": ""}
}
],
"distro2": [
{
"Name": "distro2",
"FriendlyName": "distro2Name",
"Amd64Url": {"Url": "","Sha256": ""}
}
]
}
})";
auto restore = SetManifest(manifest);
validateOrder({L"distro1", L"distro2"});
}
{
auto manifest =
R"({
"ModernDistributions": {
"distro2": [
{
"Name": "distro2",
"FriendlyName": "distro2Name",
"Amd64Url": {"Url": "","Sha256": ""}
}
],
"distro1": [
{
"Name": "distro1",
"FriendlyName": "distro1Name",
"Amd64Url": {"Url": "","Sha256": ""}
}
]
}
})";
auto restore = SetManifest(manifest);
validateOrder({L"distro2", L"distro1"});
}
}
}
TEST_METHOD(ModernInstallEndToEnd)
{
constexpr auto tarName = L"end2end.tar";
DistroFileChange distributionconf(L"/etc/wsl-distribution.conf", false);
distributionconf.SetContent(
L"[oobe]\ncommand = /bin/bash -c 'echo OOBE && useradd -u 1011 -m -s /bin/bash myuser'\n defaultUid = 1011\n");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export test_distro {}", tarName)), 0L);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
DeleteFile(tarName);
LxsstuLaunchWsl(L"--unregister end2end");
});
wil::unique_handle tarHandle{CreateFile(tarName, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)};
VERIFY_IS_TRUE(!!tarHandle);
auto tarHash = wsl::windows::common::string::BytesToHex(wsl::windows::common::wslutil::HashFile(tarHandle.get(), CALG_SHA_256));
constexpr auto manifestEndpoint = L"http://127.0.0.1:6666/";
constexpr auto tarEndpoint = L"http://127.0.0.1:6667/";
auto manifest = std::format(
LR"({{
\"ModernDistributions\": {{
\"end2end\": [
{{
\"Name\": \"end2end\",
\"FriendlyName\": \"FriendlyName\",
\"Default\": true,
\"Amd64Url\": {{
\"Url\": \"{}/distro.tar\",
\"Sha256\": \"{}\"
}}
}}
]
}}}})",
tarEndpoint,
tarHash);
UniqueWebServer apiServer(manifestEndpoint, manifest.c_str());
UniqueWebServer fileServer(tarEndpoint, std::filesystem::path(tarName));
RegistryKeyChange<std::wstring> manifestOverride{
HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::distribution::c_distroUrlRegistryValue, manifestEndpoint};
{
auto [output, error] = LxsstuLaunchWslAndCaptureOutput(L"--install --no-launch end2end");
VERIFY_ARE_EQUAL(
output,
L"Downloading: FriendlyName\r\nInstalling: FriendlyName\r\nDistribution successfully installed. It can be "
L"launched via 'wsl.exe -d end2end'\r\n");
VERIFY_ARE_EQUAL(error, L"");
}
// Check that OOBE runs
{
auto [read, write] = CreateSubprocessPipe(true, false);
write.reset();
wsl::windows::common::SubProcess process(nullptr, LxssGenerateWslCommandLine(L"-d end2end").c_str());
process.SetStdHandles(read.get(), nullptr, nullptr);
auto oobeResult = process.RunAndCaptureOutput();
VERIFY_ARE_EQUAL(oobeResult.Stdout, L"OOBE\n");
VERIFY_ARE_EQUAL(oobeResult.Stderr, L"");
VERIFY_ARE_EQUAL(oobeResult.ExitCode, 0);
}
// Run the command again to check that oobe doesn't run twice
{
auto [read, write] = CreateSubprocessPipe(true, false);
write.reset();
wsl::windows::common::SubProcess process(nullptr, LxssGenerateWslCommandLine(L"-d end2end").c_str());
process.SetStdHandles(read.get(), nullptr, nullptr);
auto oobeResult = process.RunAndCaptureOutput();
VERIFY_ARE_EQUAL(oobeResult.Stdout, L"");
VERIFY_ARE_EQUAL(oobeResult.Stderr, L"");
VERIFY_ARE_EQUAL(oobeResult.ExitCode, 0);
}
// Validate UID
auto [output, error] = LxsstuLaunchWslAndCaptureOutput(L"-d end2end id -u");
VERIFY_ARE_EQUAL(output, L"1011\n");
VERIFY_ARE_EQUAL(error, L"");
}
TEST_METHOD(DistroTarFormats)
{
auto version = LxsstuVmMode() ? L"2" : L"1";
auto convert = [](LPCWSTR Command, LPCWSTR FileName) {
const wil::unique_handle output{CreateFile(FileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr)};
VERIFY_IS_TRUE(!!output);
wsl::windows::common::helpers::SetHandleInheritable(output.get());
LxsstuLaunchWsl(std::format(L"xz -d -c $(wslpath '{}') | {}", g_testDistroPath, Command), nullptr, output.get());
return wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [FileName]() { std::filesystem::remove(FileName); });
};
auto importAndTest = [&version](LPCWSTR FileName) {
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [FileName]() { LxsstuLaunchWsl(L"--unregister test-format"); });
LxsstuLaunchWsl(std::format(L"--install --no-launch --from-file {} --name test-format --version {}", FileName, version));
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"-d test-format echo OK");
VERIFY_ARE_EQUAL(out, L"OK\n");
};
// Tar bz2
{
auto cleanup = convert(L"bzip2", L"test-distro.tar.bz2");
importAndTest(L"test-distro.tar.bz2");
}
// Tar gz
{
auto cleanup = convert(L"gzip", L"test-distro.tar.gz");
importAndTest(L"test-distro.tar.gz");
}
// N.B. tar xz is already covered since it's the format of the test distro.
VERIFY_IS_TRUE(wsl::shared::string::EndsWith(g_testDistroPath, std::wstring_view{L".xz"}));
}
TEST_METHOD(InnerCommandLineParsing)
{
using namespace wsl::windows::common;
using namespace wsl::shared;
constexpr auto entryPoint = L"dummy";
auto parse = [&](ArgumentParser& Parser, LPCWSTR ExpectedError = nullptr) {
const ExecutionContext context(Context::Wsl);
std::optional<std::wstring> error;
try
{
Parser.Parse();
}
catch (...)
{
if (context.ReportedError().has_value())
{
error = wslutil::ErrorToString(context.ReportedError().value()).Message;
}
else
{
error = wslutil::ErrorCodeToString(wil::ResultFromCaughtException());
THROW_HR(wil::ResultFromCaughtException());
}
}
if (error.has_value())
{
VERIFY_ARE_EQUAL(ExpectedError, error.value());
}
else
{
VERIFY_IS_NULL(ExpectedError);
}
};
{
ArgumentParser parser(L"--a b --c d pos-value", entryPoint, 0);
std::wstring a;
std::wstring c;
std::wstring e;
std::wstring pos;
parser.AddArgument(a, L"--a");
parser.AddArgument(c, L"--c");
parser.AddArgument(e, L"--e");
parser.AddPositionalArgument(pos, 0);
parse(parser);
VERIFY_ARE_EQUAL(a, L"b");
VERIFY_ARE_EQUAL(c, L"d");
VERIFY_ARE_EQUAL(pos, L"pos-value");
VERIFY_ARE_EQUAL(e, L"");
}
{
ArgumentParser parser(L"--a b -- --c", entryPoint, 0);
std::wstring a;
std::wstring c;
std::wstring e;
std::wstring pos;
parser.AddArgument(a, L"--a");
parser.AddArgument(e, L"--e");
parser.AddPositionalArgument(pos, 0);
parse(parser);
VERIFY_ARE_EQUAL(a, L"b");
VERIFY_ARE_EQUAL(pos, L"--c");
VERIFY_ARE_EQUAL(e, L"");
}
{
GUID expectedGuid = {0x12345678, 0x1234, 0x1234, {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}};
auto commandLine = std::format(
L"--flag b --arg value pos-arg2 pos-arg3 --flag3 --flag4 value4 --guid {}",
wsl::shared::string::GuidToString<wchar_t>(expectedGuid));
ArgumentParser parser(commandLine.c_str(), entryPoint, 0);
bool flag{};
std::wstring arg;
std::wstring pos1;
std::wstring pos2;
std::wstring pos3;
bool flag3{};
std::wstring value4;
bool dummy{};
GUID parsedGuid;
parser.AddArgument(flag, L"--flag");
parser.AddArgument(arg, L"--arg");
parser.AddPositionalArgument(pos1, 0);
parser.AddPositionalArgument(pos2, 1);
parser.AddPositionalArgument(pos3, 2);
parser.AddArgument(flag3, L"--flag3");
parser.AddArgument(value4, L"--flag4");
parser.AddArgument(dummy, L"--dummy");
parser.AddArgument(parsedGuid, L"--guid");
parse(parser);
VERIFY_IS_TRUE(flag);
VERIFY_ARE_EQUAL(arg, L"value");
VERIFY_ARE_EQUAL(pos1, L"b");
VERIFY_ARE_EQUAL(pos2, L"pos-arg2");
VERIFY_ARE_EQUAL(pos3, L"pos-arg3");
VERIFY_IS_TRUE(flag3);
VERIFY_ARE_EQUAL(L"value4", value4);
VERIFY_IS_FALSE(dummy);
VERIFY_ARE_EQUAL(expectedGuid, parsedGuid);
}
{
ArgumentParser parser(L"--a", entryPoint, 0);
std::wstring a;
parser.AddArgument(a, L"--a");
parse(
parser,
std::format(
L"Command line argument --a requires a value.\n"
"Please use '{} --help' to get a list of supported arguments.",
entryPoint)
.c_str());
}
{
ArgumentParser parser(L"--does-not-exist --a b -- --c", entryPoint, 0);
parser.AddArgument(NoOp{}, L"--a");
parser.AddArgument(NoOp{}, L"--e");
parser.AddPositionalArgument(NoOp{}, 0);
parse(
parser,
std::format(
L"Invalid command line argument: --does-not-exist\n"
"Please use '{} --help' to get a list of supported arguments.",
entryPoint)
.c_str());
}
{
ArgumentParser parser(L"--guid foo", entryPoint, 0);
GUID guid;
parser.AddArgument(guid, L"--guid");
parse(parser, L"Invalid GUID format: 'foo'");
}
{
ArgumentParser parser(L"-abc pos-value", entryPoint, 0);
bool aLong{};
bool a{};
bool b{};
bool c{};
bool d{};
std::wstring pos;
parser.AddArgument(aLong, L"--a");
parser.AddArgument(a, nullptr, 'a');
parser.AddArgument(b, nullptr, 'b');
parser.AddArgument(c, nullptr, 'c');
parser.AddArgument(d, nullptr, 'd');
parser.AddPositionalArgument(pos, 0);
parse(parser);
VERIFY_IS_TRUE(a);
VERIFY_IS_TRUE(b);
VERIFY_IS_TRUE(c);
VERIFY_IS_FALSE(d);
VERIFY_IS_FALSE(aLong);
VERIFY_ARE_EQUAL(pos, L"pos-value");
}
{
ArgumentParser parser(L"-abc", entryPoint, 0);
parser.AddArgument(NoOp{}, nullptr, 'a');
parser.AddArgument(NoOp{}, nullptr, 'c');
parse(
parser,
std::format(
L"Invalid command line argument: -abc\n"
"Please use '{} --help' to get a list of supported arguments.",
entryPoint)
.c_str());
}
{
ArgumentParser parser(L"- --", entryPoint, 0);
parse(
parser,
std::format(
L"Invalid command line argument: -\n"
"Please use '{} --help' to get a list of supported arguments.",
entryPoint)
.c_str());
}
{
ArgumentParser parser(L"--foo -", entryPoint, 0);
bool a{};
std::wstring pos;
parser.AddArgument(a, L"--foo");
parser.AddPositionalArgument(pos, 0);
parse(parser);
VERIFY_IS_TRUE(a);
VERIFY_ARE_EQUAL(pos, L"-");
}
{
constexpr auto testDir = "wslpath-test-dir";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { std::filesystem::remove_all(testDir); });
std::filesystem::create_directory(testDir);
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"wslpath -aw {}", testDir));
VERIFY_ARE_EQUAL((std::filesystem::canonical(std::filesystem::current_path()) / testDir).wstring() + L"\n", out);
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"wslpath -wa {}", testDir));
VERIFY_ARE_EQUAL((std::filesystem::canonical(std::filesystem::current_path()) / testDir).wstring() + L"\n", out);
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"wslpath {}", testDir));
VERIFY_ARE_EQUAL(std::format(L"{}\n", testDir), out);
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"wslpath -a {}", testDir));
VERIFY_IS_TRUE(out.find(L"/mnt/") == 0);
}
}
TEST_METHOD(CaseSensitivity)
{
auto setCaseSensitivity = [](const std::wstring& Path, bool enable) {
auto cmd = std::format(L"fsutil.exe file setCaseSensitiveInfo \"{}\" {}", Path.c_str(), enable ? L"enable" : L"disable");
LxsstuLaunchCommandAndCaptureOutput(cmd.data());
};
auto getCaseSensitivity = [](const std::wstring& Path) {
auto cmd = std::format(L"fsutil.exe file queryCaseSensitiveInfo \"{}\"", Path);
auto [out, _] = LxsstuLaunchCommandAndCaptureOutput(cmd.data());
if (out.find(L"is disabled") != std::string::npos)
{
return false;
}
else if (out.find(L"is enabled") != std::string::npos)
{
return true;
}
LogError("Failed to parse fsutil output: %ls", out.c_str());
VERIFY_FAIL();
return true;
};
constexpr auto testDir = L"case-test";
constexpr auto flags = wsl::windows::common::filesystem::c_case_sensitive_folders_only | LXSS_CREATE_INSTANCE_FLAGS_ALLOW_FS_UPGRADE;
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { std::filesystem::remove_all(testDir); });
std::filesystem::create_directories(testDir);
setCaseSensitivity(testDir, false);
VERIFY_IS_FALSE(getCaseSensitivity(testDir));
wsl::windows::common::filesystem::EnsureCaseSensitiveDirectory(testDir, flags);
VERIFY_IS_TRUE(getCaseSensitivity(testDir));
setCaseSensitivity(testDir, false);
std::filesystem::create_directories(std::format(L"{}/l1/l2/l3", testDir));
setCaseSensitivity(std::format(L"{}/l1/l2/l3", testDir), false);
setCaseSensitivity(std::format(L"{}/l1/l2", testDir), false);
std::filesystem::create_directories(std::format(L"{}/l1/l2/l3-other", testDir));
setCaseSensitivity(std::format(L"{}/l1/l2/l3-other", testDir), false);
VERIFY_IS_FALSE(getCaseSensitivity(std::format(L"{}/l1/l2", testDir)));
VERIFY_IS_FALSE(getCaseSensitivity(std::format(L"{}/l1/l2/l3", testDir)));
VERIFY_IS_FALSE(getCaseSensitivity(std::format(L"{}/l1/l2/l3-other", testDir)));
wsl::windows::common::filesystem::EnsureCaseSensitiveDirectory(testDir, flags);
VERIFY_IS_TRUE(getCaseSensitivity(std::format(L"{}/l1/l2/l3", testDir)));
VERIFY_IS_TRUE(getCaseSensitivity(std::format(L"{}/l1/l2/l3-other", testDir)));
VERIFY_IS_TRUE(getCaseSensitivity(std::format(L"{}/l1/l2", testDir)));
VERIFY_IS_TRUE(getCaseSensitivity(std::format(L"{}/l1", testDir)));
VERIFY_IS_TRUE(getCaseSensitivity(testDir));
}
TEST_METHOD(AutomountRespectedWithElevation)
{
DistroFileChange distributionconf(L"/etc/wsl.conf", false);
distributionconf.SetContent(L"[automount]\nenabled=false\n");
DistroFileChange distributionFstab(L"/etc/fstab", false);
distributionFstab.SetContent(L"");
TerminateDistribution();
const auto nonElevatedToken = GetNonElevatedToken();
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(L"echo dummy", nullptr, nullptr, nullptr, nonElevatedToken.get()));
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"mountpoint /mnt/c", 32u);
VERIFY_ARE_EQUAL(out, L"/mnt/c is not a mountpoint\n");
}
TEST_METHOD(FstabRespectedWithElevationAndAutomountDisabled)
{
DistroFileChange distributionconf(L"/etc/wsl.conf", false);
distributionconf.SetContent(L"[automount]\nenabled=false\n");
DistroFileChange distributionFstab(L"/etc/fstab", false);
distributionFstab.SetContent(L"C:\\\\ /mnt/c drvfs metadata 0 0");
TerminateDistribution();
const auto nonElevatedToken = GetNonElevatedToken();
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(L"echo dummy", nullptr, nullptr, nullptr, nonElevatedToken.get()));
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"mountpoint /mnt/c", 0u);
VERIFY_ARE_EQUAL(out, L"/mnt/c is a mountpoint\n");
}
// This test case validates that the pipeline doesn't get stuck when both stdout & stdin are a pipe.
// See: https://github.com/microsoft/WSL/issues/12523
TEST_METHOD(DualPipeRelay)
{
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { DeleteFile(L"compressed.gz"); });
wsl::windows::common::SubProcess process{
nullptr, L"cmd /c type \"C:\\Program Files\\WSL\\wsl.exe\" | wsl gzip > compressed.gz"};
VERIFY_ARE_EQUAL(process.Run(), 0L);
wil::unique_handle file{CreateFile(L"compressed.gz", GENERIC_READ, 0, nullptr, OPEN_EXISTING, 0, nullptr)};
VERIFY_IS_TRUE(!!file);
wsl::windows::common::helpers::SetHandleInheritable(file.get());
// Validate that the relay didn't get stuck, and that its output is correct.
auto [expandedHash, _] = LxsstuLaunchWslAndCaptureOutput(L"gzip -d -| md5sum -", 0, file.get());
auto [expectedHash, __] =
LxsstuLaunchWslAndCaptureOutput(L"cat \"$(wslpath 'C:\\Program Files\\WSL\\wsl.exe')\" | md5sum - ");
VERIFY_ARE_EQUAL(expandedHash, expectedHash);
}
TEST_METHOD(EtcHosts)
{
{
// Verify that setting network.generateHosts=false doesn't create /etc/hosts
DistroFileChange wslConf{L"/etc/wsl.conf", false};
wslConf.SetContent(L"[network]\ngenerateHosts=false");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"rm /etc/hosts"), 0L);
TerminateDistribution();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"! test -f /etc/hosts"), 0L);
}
{
// Verify that /etc/hosts generation is correct.
TerminateDistribution();
auto [content, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/hosts");
auto [hostname, domain] = wsl::windows::common::filesystem::GetHostAndDomainNames();
const auto lines = wsl::shared::string::Split(content, L'\n');
VERIFY_IS_TRUE(lines.size() > 4);
VERIFY_ARE_EQUAL(lines[0] + L"\n", WIDEN(LX_INIT_AUTO_GENERATED_FILE_HEADER));
VERIFY_ARE_EQUAL(lines[1], L"# [network]");
VERIFY_ARE_EQUAL(lines[2], L"# generateHosts = false");
VERIFY_ARE_EQUAL(lines[3], L"127.0.0.1\tlocalhost");
VERIFY_ARE_EQUAL(lines[4], std::format(L"127.0.1.1\t{}.{}\t{}", hostname, domain, hostname));
}
}
TEST_METHOD(ExecEmptyArg)
{
// See: https://github.com/microsoft/WSL/issues/12649
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--exec echo \"\"");
VERIFY_ARE_EQUAL(out, L"\n");
VERIFY_ARE_EQUAL(err, L"");
}
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--exec echo foo \"\" bar");
VERIFY_ARE_EQUAL(out, L"foo bar\n"); // Two spaces because echo adds one between each argument.
VERIFY_ARE_EQUAL(err, L"");
}
}
TEST_METHOD(DistroTimeout)
{
WslConfigChange config(LxssGenerateTestConfig() + L"[general]\ninstanceIdleTimeout=-1");
auto distroId = GetDistributionId(LXSS_DISTRO_NAME_TEST_L);
auto getDistroState = [&]() {
wsl::windows::common::SvcComm service;
for (const auto& e : service.EnumerateDistributions())
{
if (wsl::shared::string::IsEqual(e.DistroName, LXSS_DISTRO_NAME_TEST_L))
{
return e.State;
}
}
return LxssDistributionStateInvalid;
};
// Validate that distributions don't time out when timeout is -1
{
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"echo OK"), 0L);
std::this_thread::sleep_for(std::chrono::seconds(20));
VERIFY_ARE_EQUAL(getDistroState(), LxssDistributionStateRunning);
}
// Validate that distributions time out when timeout value is > 0
{
config.Update(LxssGenerateTestConfig() + L"[general]\ninstanceIdleTimeout=2000");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"echo OK"), 0L);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(1);
unsigned long iterations = 0;
while (std::chrono::steady_clock::now() < deadline)
{
if (getDistroState() == LxssDistributionStateInstalled)
{
LogInfo("Distribution stopped after %lu iterations", iterations);
return;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
iterations++;
}
LogError("Distribution failed to time out after %lu iterations. State: %i", iterations, getDistroState());
VERIFY_FAIL();
}
}
TEST_METHOD(WslUpdate)
{
// Test the regular wsl --update logic
{
auto json =
LR"(
{
"name": "2.4.12",
"assets": [
{
"url": "http://arm-url",
"id": 1,
"name": "wsl.2.4.12.0.arm64.msi"
},
{
"url": "http://x64-url",
"id": 2,
"name": "wsl.2.4.12.0.x64.msi"
}]})";
auto [version, asset] = wsl::windows::common::wslutil::GetLatestGitHubRelease(false, json);
VERIFY_ARE_EQUAL(version, L"2.4.12");
VERIFY_ARE_EQUAL(asset.id, 2);
VERIFY_ARE_EQUAL(asset.url, L"http://x64-url");
VERIFY_ARE_EQUAL(asset.name, L"wsl.2.4.12.0.x64.msi");
}
// Test wsl --update --pre-release
{
auto json =
LR"([
{
"name": "2.4.12"
},
{
"name": "2.5.1",
"assets": [
{
"url": "http://arm-url",
"id": 1,
"name": "wsl.2.5.1.0.arm64.msi"
},
{
"url": "http://x64-url",
"id": 2,
"name": "wsl.2.5.1.0.x64.msi"
}
]
},
{
"name": "2.4.13"
}])";
auto [version, asset] = wsl::windows::common::wslutil::GetLatestGitHubRelease(true, json);
VERIFY_ARE_EQUAL(version, L"2.5.1");
VERIFY_ARE_EQUAL(asset.id, 2);
VERIFY_ARE_EQUAL(asset.url, L"http://x64-url");
VERIFY_ARE_EQUAL(asset.name, L"wsl.2.5.1.0.x64.msi");
}
}
TEST_METHOD(CustomModulesVhd)
{
WSL2_TEST_ONLY();
#ifdef WSL_DEV_INSTALL_PATH
auto modulesPath = std::format(L"{}\\modules.vhd", WSL_DEV_INSTALL_PATH);
auto kernelPath = std::format(L"{}\\kernel", WSL_DEV_INSTALL_PATH);
#else
auto modulesPath = std::format(L"{}\\tools\\modules.vhd", wsl::windows::common::wslutil::GetMsiPackagePath().value());
auto kernelPath = std::format(L"{}\\tools\\kernel", wsl::windows::common::wslutil::GetMsiPackagePath().value());
#endif
// Create a copy of the modules vhd
auto testModules = std::filesystem::current_path() / "test-modules.vhd";
VERIFY_IS_TRUE(CopyFile(modulesPath.c_str(), testModules.c_str(), false));
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove(testModules); });
auto cmd = std::format(
LR"($acl = Get-Acl '{}' ; $acl.RemoveAccessRuleAll((New-Object System.Security.AccessControl.FileSystemAccessRule(\"Everyone\", \"Read\", \"None\", \"None\", \"Allow\"))); Set-Acl -Path '{}' -AclObject $acl)",
testModules,
testModules);
LxsstuLaunchPowershellAndCaptureOutput(cmd);
// Update .wslconfig to point to the copied kernel
WslConfigChange config{LxssGenerateTestConfig({.kernel = kernelPath, .kernelModules = testModules.wstring()})};
// Validate that WSL starts correctly
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"echo OK");
VERIFY_ARE_EQUAL(out, L"OK\n");
VERIFY_ARE_EQUAL(err, L"");
}
TEST_METHOD(BrokenDistroImport)
{ // Validate that importing an empty tar fails.
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--import broken-test-distro . NUL", -1);
VERIFY_ARE_EQUAL(
out,
L"The imported file is not a valid Linux distribution.\r\nError code: "
L"Wsl/Service/RegisterDistro/WSL_E_NOT_A_LINUX_DISTRO\r\n");
// TODO: Uncomment once SetVersionDebug is removed from the tests .wslconfig.
// VERIFY_ARE_EQUAL(err, L"");
}
// Validate that importing an empty tar via wsl --install fails.
{
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--install --from-file NUL --name broken-test-distro", -1);
VERIFY_ARE_EQUAL(
out,
L"Installing: NUL\r\nThe imported file is not a valid Linux distribution.\r\nError code: "
L"Wsl/Service/RegisterDistro/WSL_E_NOT_A_LINUX_DISTRO\r\n");
// TODO: Uncomment once SetVersionDebug is removed from the tests .wslconfig.
// VERIFY_ARE_EQUAL(err, L"");
}
// Validate that importing an empty VHDX fails.
if (LxsstuVmMode())
{
constexpr auto testVhd = L"EmptyVhd.vhdx";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(testVhd); });
LxsstuLaunchPowershellAndCaptureOutput(std::format(L"New-Vhd {} -SizeBytes 20MB", testVhd));
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--mount {} --vhd --bare", testVhd)), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mkfs.ext4 /dev/sde"), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unmount"), 0L);
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--import-in-place broken-test-distro {}", testVhd), -1);
VERIFY_ARE_EQUAL(
out,
L"The imported file is not a valid Linux distribution.\r\nError code: "
L"Wsl/Service/RegisterDistro/WSL_E_NOT_A_LINUX_DISTRO\r\n");
// TODO: Uncomment once SetVersionDebug is removed from the tests .wslconfig.
// VERIFY_ARE_EQUAL(err, L"");
}
// Validate that tars containing /etc, but not /bin/sh are accepted.
if (LxsstuVmMode())
{
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(L"--unregister empty-distro"); });
DistroFileChange conf(L"/etc/wsl.conf", false);
conf.SetContent(L"");
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(
L"tar cf - /etc/wsl.conf | wsl.exe --install --from-file - --name empty-distro --no-launch "
L"--version 2");
}
}
TEST_METHOD(ImportExportStdout)
{
constexpr auto test_distro = L"import-test-distro";
auto cleanup =
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(std::format(L"--unregister {}", test_distro)); });
// The below logline makes it easier to find the bsdtar output when debugging this test case.
fprintf(stderr, "Starting ImportExportStdout test case\n");
auto commandLine = std::format(L"cmd.exe /c wsl --export {} - | wsl --import {} . -", LXSS_DISTRO_NAME_TEST_L, test_distro);
VERIFY_ARE_EQUAL(LxsstuRunCommand(commandLine.data()), 0L);
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", test_distro));
VERIFY_ARE_EQUAL(out, L"ok\n");
VERIFY_ARE_EQUAL(err, L"");
}
TEST_METHOD(EtcHostsParsing)
{
constexpr auto inputFileName = L"test-etc-hosts.txt";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(inputFileName); });
auto validate = [](const std::string& Input, const std::string& ExpectedOutput) {
wil::unique_handle inputFile{CreateFile(inputFileName, GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, 0, nullptr)};
VERIFY_IS_TRUE(WriteFile(inputFile.get(), Input.c_str(), static_cast<DWORD>(Input.size()), nullptr, nullptr));
auto output = wsl::windows::common::filesystem::GetWindowsHosts(inputFileName);
VERIFY_ARE_EQUAL(ExpectedOutput, output);
};
validate("127.0.0.1 microsoft.com", "127.0.0.1\tmicrosoft.com\n");
validate("\xEF\xBB\xBF 127.0.0.1 microsoft.com", "127.0.0.1\tmicrosoft.com\n"); // Validate that BOM headers are ignored.
validate("#Comment 127.0.0.1 microsoft.com windows.microsoft.com\n#AnotherComment", "");
validate(
"#Comment 127.0.0.1 microsoft.com windows.microsoft.com\n#AnotherComment\n127.0.0.1 wsl.dev", "127.0.0.1\twsl.dev\n");
}
// Validate that a distribution can be unregistered even if its BasePath doesn't exist.
// See https://github.com/microsoft/WSL/issues/13004
TEST_METHOD(BrokenDistroUnregister)
{
const auto userKey = wsl::windows::common::registry::OpenLxssUserKey();
const auto distroKey = wsl::windows::common::registry::CreateKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
auto revert = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
wsl::windows::common::registry::DeleteKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
});
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"BasePath", L"C:\\DoesNotExit");
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"DistributionName", L"DummyBrokenDistro");
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"DefaultUid", 0);
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Version", LXSS_DISTRO_VERSION_2);
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"State", LxssDistributionStateInstalled);
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Flags", LXSS_DISTRO_FLAGS_VM_MODE);
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--unregister DummyBrokenDistro");
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
VERIFY_ARE_EQUAL(err, L"");
}
// Validate that calling the binfmt interpreter with tty fd's but not controlling terminal doesn't display a warning.
// See https://github.com/microsoft/WSL/issues/13173.
TEST_METHOD(SetSidNoWarning)
{
auto [out, err] =
LxsstuLaunchWslAndCaptureOutput(L"socat - 'EXEC:setsid --wait cmd.exe /c echo OK',pty,setsid,ctty,stderr");
VERIFY_ARE_EQUAL(out, L"OK\r\r\n");
VERIFY_ARE_EQUAL(err, L"");
}
TEST_METHOD(WslDebug)
{
WSL2_TEST_ONLY();
// Verify that hvsocket debug events are logged to dmesg.
WslConfigChange config(LxssGenerateTestConfig({.kernelCommandLine = L"WSL_DEBUG=hvsocket"}));
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"dmesg | grep -iF 'vmbus_send_tl_connect_request'"), 0L);
}
TEST_METHOD(CGroupv1)
{
WSL2_TEST_ONLY();
auto expectedMount = [](const char* path, const wchar_t* expected) {
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"findmnt -ln '{}' || true", path));
VERIFY_ARE_EQUAL(out, expected);
};
// Validate that cgroupv2 is mounted by default.
expectedMount("/sys/fs/cgroup", L"/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate\n");
// Validate that setting cgroup=v1 causes unified cgroups to be mounted.
DistroFileChange wslConf(L"/etc/wsl.conf", false);
wslConf.SetContent(L"[automount]\ncgroups=v1");
TerminateDistribution();
expectedMount(
"/sys/fs/cgroup/unified", L"/sys/fs/cgroup/unified cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate\n");
// Validate that the cgroupv1 mounts are present.
expectedMount("/sys/fs/cgroup/cpu", L"/sys/fs/cgroup/cpu cgroup cgroup rw,nosuid,nodev,noexec,relatime,cpu\n");
// Validate that having cgroup_no_v1=all causes the distribution to fall back to v2.
WslConfigChange wslConfig(LxssGenerateTestConfig({.kernelCommandLine = L"cgroup_no_v1=all"}));
expectedMount("/sys/fs/cgroup/unified", L"");
expectedMount("/sys/fs/cgroup", L"/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate\n");
auto [dmesg, __] = LxsstuLaunchWslAndCaptureOutput(L"dmesg");
VERIFY_ARE_NOT_EQUAL(
dmesg.find(
L"Distribution has cgroupv1 enabled, but kernel command line has cgroup_no_v1=all. Falling back to cgroupv2"),
std::wstring::npos);
}
TEST_METHOD(InitPermissions)
{
WSL2_TEST_ONLY();
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"stat -c %a /init");
VERIFY_ARE_EQUAL(out, L"755\n");
}
TEST_METHOD(ExportImportVhd)
{
WSL2_TEST_ONLY();
WslShutdown();
constexpr auto vhdPath = L"exported-test-distro.vhd";
constexpr auto vhdxPath = L"exported-test-distro.vhdx";
constexpr auto exportedVhdPath = L"exported-vhd.vhd";
constexpr auto newDistroName = L"imported-test-distro";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
LOG_IF_WIN32_BOOL_FALSE(DeleteFile(vhdPath));
LOG_IF_WIN32_BOOL_FALSE(DeleteFile(vhdxPath));
LOG_IF_WIN32_BOOL_FALSE(DeleteFile(exportedVhdPath));
LxsstuLaunchWsl(std::format(L"--unregister {}", newDistroName));
});
// Attempt to export the distribution to a .vhd (should fail).
auto [out, err] =
LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", LXSS_DISTRO_NAME_TEST_L, vhdPath), -1);
VERIFY_ARE_EQUAL(
out, L"The specified file must have the .vhdx file extension.\r\nError code: Wsl/Service/WSL_E_EXPORT_FAILED\r\n");
VERIFY_ARE_EQUAL(err, L"");
// Export the distribution to a .vhdx.
std::tie(out, err) =
LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", LXSS_DISTRO_NAME_TEST_L, vhdxPath));
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
VERIFY_ARE_EQUAL(err, L"");
// Convert the .vhdx to .vhd.
LxsstuLaunchPowershellAndCaptureOutput(std::format(L"Convert-VHD -Path '{}' -DestinationPath '{}'", vhdxPath, vhdPath));
// Import a new distribution from the .vhd file.
std::tie(out, err) =
LxsstuLaunchWslAndCaptureOutput(std::format(L"--import {} {} {} --vhd", newDistroName, newDistroName, vhdPath));
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
VERIFY_ARE_EQUAL(err, L"");
// Export the newly imported distribution to another .vhd file.
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", newDistroName, exportedVhdPath));
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
VERIFY_ARE_EQUAL(err, L"");
// Attempt to export to a .vhdx (should fail).
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", newDistroName, vhdxPath), -1);
VERIFY_ARE_EQUAL(
out, L"The specified file must have the .vhd file extension.\r\nError code: Wsl/Service/WSL_E_EXPORT_FAILED\r\n");
VERIFY_ARE_EQUAL(err, L"");
// Attempt to import to a non VHD file.
auto tempFile = wsl::windows::common::filesystem::TempFile(
GENERIC_ALL, 0, CREATE_ALWAYS, wsl::windows::common::filesystem::TempFileFlags::None, L"txt");
tempFile.Handle.reset();
constexpr auto negativeVariationDistro = L"negative-variation-distro";
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(
std::format(L"--import {} {} {} --vhd", negativeVariationDistro, negativeVariationDistro, tempFile.Path), -1);
VERIFY_ARE_EQUAL(
out,
L"The specified file must have the .vhd or .vhdx file extension.\r\nError code: "
L"Wsl/Service/RegisterDistro/WSL_E_IMPORT_FAILED\r\n");
VERIFY_ARE_EQUAL(err, L"");
}
}; // namespace UnitTests
} // namespace UnitTests