Files
63fdf785d2 Convert EnsureCaseSensitiveDirectoryRecursive to iterative traversal (#40347)
* Convert EnsureCaseSensitiveDirectoryRecursive to iterative traversal

Replace unbounded recursive depth-first traversal with an iterative
approach using an explicit stack. A deeply nested directory tree could
overflow the thread stack during the recursive calls.

The iterative version collects directories in reverse post-order during
traversal, then marks them bottom-up (children before parents),
preserving the ordering invariant that EnsureCaseSensitiveDirectory
relies on for its early-exit optimization.

Bug: 61964147, 61974992, 61911308, 61463090

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

* Add deep-nesting regression test for case-sensitive recursion

The original recursive EnsureCaseSensitiveDirectoryRecursive could blow
the Windows 1 MB thread stack at a few hundred levels of nesting because
each frame carried a FILE_ID_BOTH_DIR_INFORMATION enumeration buffer.
The iterative implementation in this PR moves that state to the heap, so
a very deep tree must complete without crashing.

The test creates a 1024-level chain via the \\\\?\\ long-path prefix
(cumulative path length exceeds MAX_PATH) and then calls
EnsureCaseSensitiveDirectory on the root. Cleanup goes through the same
long-path prefix so remove_all can see every component.

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

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-07 13:32:23 -07:00

8871 lines
383 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 "install.h"
#include <AclAPI.h>
#include <fstream>
#include <sstream>
#include <filesystem>
#include "wslservice.h"
#include "registry.hpp"
#include "helpers.hpp"
#include "lxinitshared.h"
#include "svccomm.hpp"
#include "relay.hpp"
#include "ConsoleState.h"
#include "lxfsshares.h"
#include <userenv.h>
#include <nlohmann/json.hpp>
#include "Distribution.h"
#include "WslCoreConfigInterface.h"
#include "WslCoreFilesystem.h"
#include "WslSecurity.h"
#include "CommandLine.h"
#include "retryshared.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_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, FormatErrorMessage(L"This operation is only supported by WSL2.", L"Wsl/Service/WSL_E_WSL2_NEEDED"));
VERIFY_ARE_EQUAL(err, L"");
}
VerifyInvalidUsage(std::format(L"--export {} {} --format tar.gz --format tar.xz", LXSS_DISTRO_NAME_TEST_L, tarPath));
VerifyInvalidUsage(std::format(L"--export {} {} --format tar.xz --vhd", LXSS_DISTRO_NAME_TEST_L, tarPath));
}
WSL2_TEST_METHOD(SystemdSafeMode)
{
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"));
}
WSL2_TEST_METHOD(SystemdDisabled)
{
// 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));
}
WSL2_TEST_METHOD(SystemdSystem)
{
auto revert = EnableSystemd();
VERIFY_IS_TRUE(IsSystemdRunning(L"--system"));
// Validate that systemd-networkd-wait-online.service is masked.
std::wstring out;
std::wstring err;
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(L"systemctl show -p LoadState systemd-networkd-wait-online.service");
VERIFY_ARE_EQUAL(out, L"LoadState=masked\n");
// Validate that NetworkManager-wait-online.service is masked.
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(L"systemctl show -p LoadState NetworkManager-wait-online.service");
VERIFY_ARE_EQUAL(out, L"LoadState=masked\n");
// Validate that console-getty.service is masked (tty devices are shared at VM level across distros).
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(L"systemctl show -p LoadState console-getty.service");
VERIFY_ARE_EQUAL(out, L"LoadState=masked\n");
}
WSL2_TEST_METHOD(SystemdUser)
{
// 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.
WslConfigChange config(LxssGenerateTestConfig({.guiApplications = false}));
{
validateUserSession();
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $DISPLAY", LXSST_TEST_USERNAME));
VERIFY_ARE_EQUAL(out, L"\n");
// N.B. The XDG_RUNTIME_DIR variable is always set by init even if gui apps are disabled.
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $XDG_RUNTIME_DIR", LXSST_TEST_USERNAME));
VERIFY_ARE_EQUAL(out, std::format(L"/run/user/{}\n", TestUid));
}
// Validate user sessions state with gui apps enabled.
{
config.Update(LxssGenerateTestConfig({.guiApplications = true}));
validateUserSession();
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $DISPLAY", LXSST_TEST_USERNAME));
VERIFY_ARE_EQUAL(out, L":0\n");
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $XDG_RUNTIME_DIR", LXSST_TEST_USERNAME));
VERIFY_ARE_EQUAL(out, std::format(L"/run/user/{}\n", TestUid));
}
// 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;
}
WSL2_TEST_METHOD(SystemdNoClearTmpUnit)
{
// The X11 socket is only created when gui applications are enabled.
WslConfigChange config(LxssGenerateTestConfig({.guiApplications = true}));
// 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);
}
WSL2_TEST_METHOD(SystemdBinfmtIsRestored)
{
// Override WSL's binfmt interpreter
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mkdir -p /usr/lib/binfmt.d && 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 = []() {
const auto status = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/fs/binfmt_misc/status").first;
VERIFY_ARE_EQUAL(status, L"enabled\n");
// 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();
auto restartBinfmt = [&]() {
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl restart systemd-binfmt.service"), 0u);
validateBinfmt();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl is-active --quiet systemd-binfmt.service"), 0u);
const auto result =
LxsstuLaunchWslAndCaptureOutput(L"systemctl show --property=Result --value systemd-binfmt.service").first;
VERIFY_ARE_EQUAL(result, L"success\n");
};
restartBinfmt();
// Validate that the unit is regenerated after a daemon-reload.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl daemon-reload"), 0u);
restartBinfmt();
// Exercise both hooks independently of the distro's systemd-binfmt exit behavior.
constexpr auto overridePath = L"/run/systemd/system/systemd-binfmt.service.d/wsl-test.conf";
auto cleanupOverride = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
LxsstuLaunchWsl(std::format(L"rm -f {} && systemctl daemon-reload", overridePath));
});
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mkdir -p /run/systemd/system/systemd-binfmt.service.d"), 0u);
for (const bool failStartup : {false, true})
{
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(
L"printf '[Service]\\nExecStart=\\nExecStart=/bin/{}\\n' > {} && systemctl daemon-reload",
failStartup ? L"false" : L"true",
overridePath)),
0u);
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(L"echo -1 > /proc/sys/fs/binfmt_misc/WSLInterop && "
L"echo ':WSLInterop:M::MZ::/bin/echo:PF' > /proc/sys/fs/binfmt_misc/register"),
0u);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl restart systemd-binfmt.service"), failStartup ? 1u : 0u);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl is-failed --quiet systemd-binfmt.service"), failStartup ? 0u : 1u);
validateBinfmt();
if (!failStartup)
{
// A normal stop must not re-register the entry.
const auto inodeBefore = LxsstuLaunchWslAndCaptureOutput(L"stat -c %i /proc/sys/fs/binfmt_misc/WSLInterop").first;
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl stop systemd-binfmt.service"), 0u);
const auto inodeAfter = LxsstuLaunchWslAndCaptureOutput(L"stat -c %i /proc/sys/fs/binfmt_misc/WSLInterop").first;
VERIFY_ARE_EQUAL(inodeBefore, inodeAfter);
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));
}
}
WSL2_TEST_METHOD(BinfmtStatusIsProtected)
{
//
// Validates the protection mechanism for the cross-distro binfmt wipe bug.
//
// Fix: per-distro init bind-mounts a writable regular file over
// /proc/sys/fs/binfmt_misc/status before exec'ing the distro's init
// (see ProtectBinfmtStatus in src/linux/init/init.cpp). systemd-shutdown's
// disable_binfmt() writes "-1" to that file to clear the kernel-global
// binfmt_misc table at shutdown; with the bind-mount in place the write
// only changes the shadow file, so entries shared with other running distros
// survive. Per-entry operations (registering new entries via /register,
// unregistering individual entries via the entry file) are unaffected.
//
// Default: bind-mount must be in place.
{
// EnableSystemd raises /proc/sys/fs/nr_open VM-wide; without a full
// VM teardown that bumped value persists across distro restarts and
// breaks later tests like ResourceLimits that assume the kernel default.
auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
auto cleanupSystemd = EnableSystemd();
// /status is its own mount point.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mountpoint -q /proc/sys/fs/binfmt_misc/status"), 0u);
// Runtime registration via /register still works (we only shadow /status).
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sh -c 'echo \":wsltestbinfmt:M::WSLTESTMAGIC::/bin/echo:\" > /proc/sys/fs/binfmt_misc/register'"), 0L);
// binfmt_misc is VM-global, so a leftover wsltestbinfmt entry would
// cascade into later tests. Always remove it on scope exit.
auto cleanupTestEntry = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
LxsstuLaunchWsl(L"sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/wsltestbinfmt 2>/dev/null || true'");
});
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/wsltestbinfmt"), 0L);
// Clear/disable/enable writes succeed but affect only the shadow file.
for (const auto* value : {L"-1", L"0", L"1"})
{
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"echo {} > /proc/sys/fs/binfmt_misc/status", value)), 0L);
const auto status = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/fs/binfmt_misc/status").first;
VERIFY_ARE_EQUAL(status, std::format(L"{}\n", value));
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/WSLInterop"), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/wsltestbinfmt"), 0L);
const auto cmd = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok").first;
VERIFY_ARE_EQUAL(cmd, L"ok\r\n");
}
// Per-entry unregister (writing -1 to the entry file, not /status) still works.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/wsltestbinfmt'"), 0L);
VERIFY_ARE_NOT_EQUAL(LxsstuLaunchWsl(L"test -e /proc/sys/fs/binfmt_misc/wsltestbinfmt"), 0L);
cleanupTestEntry.release();
// Interop still works.
{
auto [cmd, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok");
VERIFY_ARE_EQUAL(cmd, L"ok\r\n");
}
}
// protectBinfmt=false: bind-mount must NOT be installed (kill switch).
// EnableSystemd's cleanup re-launches the distro to revert wsl.conf and
// then terminates it; that termination invokes systemd-shutdown's
// disable_binfmt() which wipes the kernel-global table because
// protectBinfmt=false exposes the real kernel /status. WslShutdown registered
// FIRST (runs LAST in LIFO unwind) ensures the VM is fully torn down
// after the wipe, so the next test starts a fresh VM where mini_init
// re-registers WSLInterop.
{
auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
auto cleanupSystemd = EnableSystemd("protectBinfmt=false");
VERIFY_ARE_NOT_EQUAL(LxsstuLaunchWsl(L"mountpoint -q /proc/sys/fs/binfmt_misc/status"), 0L);
}
}
static std::wstring GetWslInitPid()
{
auto [pid, _] = LxsstuLaunchWslAndCaptureOutput(
L"/bin/sh -c \"for pid in \\$(cat /proc/1/task/1/children); do "
L"case \\$(cat /proc/\\$pid/comm 2>/dev/null) in init-systemd*) echo \\$pid; break;; esac; done\"");
while (!pid.empty() && (pid.back() == L'\n' || pid.back() == L'\r'))
{
pid.pop_back();
}
VERIFY_IS_FALSE(pid.empty());
return pid;
}
WSL2_TEST_METHOD(SystemdKillInitTerminatesDistro)
{
WslConfigChange config(LxssGenerateTestConfig() + L"[general]\ninstanceIdleTimeout=-1");
auto revert = EnableSystemd("initTimeout=0");
// Wait for systemd to start
VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
[&]() { THROW_HR_IF(E_UNEXPECTED, !IsSystemdRunning(L"--system")); }, std::chrono::seconds(1), std::chrono::minutes(1)));
// Kill the WSL init process without relying on an incidental PID allocation.
const auto wslInitPid = GetWslInitPid();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"kill -9 {}", wslInitPid)), 0L);
// Wait for the distro to exit.
VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
[&]() { THROW_HR_IF(E_ABORT, GetDistroState() == LxssDistributionStateRunning); }, std::chrono::seconds(1), std::chrono::seconds(30)));
// Verify that a new WSL command succeeds (the distro restarts cleanly).
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"echo hello");
VERIFY_ARE_EQUAL(out, L"hello\n");
}
WSL2_TEST_METHOD(BinfmtSurvivesDistroTermination)
{
//
// Regression test for the "Exec format error" bug: binfmt_misc registrations
// (most importantly WSLInterop) must survive when a peer systemd-enabled distro
// terminates. Before this fix, systemd-shutdown's disable_binfmt() wrote `-1`
// to /proc/sys/fs/binfmt_misc/status, which clears the entire binfmt_misc
// entry table. binfmt_misc itself is a single kernel-global registry — it is
// not isolated per distro — so that one write wiped WSLInterop for every
// running distro and broke Windows interop everywhere.
//
constexpr auto peerDistroName = L"binfmt-peer-test";
// EnableSystemd raises /proc/sys/fs/nr_open VM-wide; without a full
// VM teardown that bumped value persists across distro restarts and
// breaks later tests like ResourceLimits that assume the kernel default.
auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
// Enable systemd on the primary test distro.
auto cleanupSystemd = EnableSystemd();
// Import a second distro from the same tarball as the test distro.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", peerDistroName, g_testDistroPath)), 0L);
auto cleanupPeer =
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(std::format(L"--unregister {}", peerDistroName)); });
auto cleanupPeerSystemd = EnableSystemd("", peerDistroName);
// Verify interop works in both distros (this also starts the peer with systemd).
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo alive");
VERIFY_ARE_EQUAL(out, L"alive\r\n");
}
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} -- cmd.exe /c echo alive", peerDistroName));
VERIFY_ARE_EQUAL(out, L"alive\r\n");
}
// Terminate the peer distro — this triggers systemd shutdown. Without
// the fix, systemd-shutdown's disable_binfmt() would clear the kernel-
// global binfmt_misc table for every running distro.
TerminateDistribution(peerDistroName);
// Verify interop still works in the primary distro.
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo survived");
VERIFY_ARE_EQUAL(out, L"survived\r\n");
}
// Verify the binfmt entry still exists and carries the F (fix-binary) flag.
// The F flag is required so the kernel resolves the interpreter at
// registration time, making the entry independent of mount-namespace state.
{
auto [flags, _] = LxsstuLaunchWslAndCaptureOutput(L"grep ^flags /proc/sys/fs/binfmt_misc/WSLInterop");
VERIFY_IS_TRUE(flags.find(L"F") != std::wstring::npos);
}
}
WSL2_TEST_METHOD(SharedMountSurvivesDistroTermination)
{
constexpr auto peerDistroName = L"mount-guard-peer-test";
auto validate = [&](const std::string& automountRoot) {
const auto extraConfig = automountRoot.empty() ? "" : std::format("[automount]\nroot={}\n", automountRoot);
const auto effectiveAutomountRoot = automountRoot.empty() ? "/mnt" : automountRoot;
const auto mountPoint = std::format(L"{}/wsl/mount-guard-test", wsl::shared::string::MultiByteToWide(effectiveAutomountRoot));
auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
auto cleanupSystemd = EnableSystemd(extraConfig);
LxsstuLaunchWsl(std::format(L"--unregister {}", peerDistroName));
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", peerDistroName, g_testDistroPath)), 0L);
auto cleanupPeer = wil::scope_exit_log(
WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(std::format(L"--unregister {}", peerDistroName)); });
auto cleanupPeerSystemd = EnableSystemd(extraConfig, peerDistroName);
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(L"-d {} -- sh -c \"systemctl is-system-running | grep -Eq 'running|degraded'\"", peerDistroName)), 0L);
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(
L"sh -c 'mkdir -p {0} && mount -t tmpfs -o size=4M mount-guard-test {0} && echo survived > {0}/marker'", mountPoint)),
0L);
auto cleanupMount = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
LxsstuLaunchWsl(std::format(L"sh -c 'umount {0} 2>/dev/null || true; rmdir {0} 2>/dev/null || true'", mountPoint));
});
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} -- findmnt -n {}", peerDistroName, mountPoint)), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} -- grep -qx survived {}/marker", peerDistroName, mountPoint)), 0L);
TerminateDistribution(peerDistroName);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"findmnt -n {}", mountPoint)), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"grep -qx survived {}/marker", mountPoint)), 0L);
};
validate("");
validate("/wsl-test-mount");
}
WSL2_TEST_METHOD(ConfigUpdateLanguage)
{
// Validates that init populates $LANG from the distro locale configuration file.
// ConfigUpdateLanguage reads /etc/default/locale first, then /etc/locale.conf, and uses
// the first file that exists. See ConfigUpdateLanguage in src/linux/init/config.cpp.
DistroFileChange defaultLocale(L"/etc/default/locale", LxsstuLaunchWsl(L"test -f /etc/default/locale") == 0);
DistroFileChange localeConf(L"/etc/locale.conf", LxsstuLaunchWsl(L"test -f /etc/locale.conf") == 0);
const auto readLang = []() { return LxsstuLaunchWslAndCaptureOutput(L"printenv LANG").first; };
// Only /etc/default/locale is present (Debian/Ubuntu).
{
defaultLocale.Delete();
localeConf.Delete();
defaultLocale.SetContent(L"LANG=de_DE.UTF-8\n");
TerminateDistribution();
VERIFY_ARE_EQUAL(readLang(), L"de_DE.UTF-8\n");
}
// Only /etc/locale.conf is present (Fedora, Arch, openSUSE, ...).
{
defaultLocale.Delete();
localeConf.Delete();
localeConf.SetContent(L"LANG=\"fr_FR.UTF-8\"\n");
TerminateDistribution();
VERIFY_ARE_EQUAL(readLang(), L"fr_FR.UTF-8\n");
}
// Both files are present: /etc/default/locale takes precedence because it is read first.
{
defaultLocale.Delete();
localeConf.Delete();
defaultLocale.SetContent(L"LANG=ja_JP.UTF-8\n");
localeConf.SetContent(L"LANG=en_US.UTF-8\n");
TerminateDistribution();
VERIFY_ARE_EQUAL(readLang(), L"ja_JP.UTF-8\n");
}
}
TEST_METHOD(Dup)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dup", L"Dup"));
}
WSL1_TEST_METHOD(Epoll)
{
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"));
}
WSL1_TEST_METHOD(Fork)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests fork", L"Fork"));
}
WSL1_TEST_METHOD(FsCommonLxFs)
{
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"));
}
WSL1_TEST_METHOD(Inotify)
{
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"));
}
WSL1_TEST_METHOD(Pipe)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests pipe", L"Pipe"));
}
WSL1_TEST_METHOD(Sched)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sched", L"sched"));
}
WSL1_TEST_METHOD(SocketNonblocking)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests socket_nonblock", L"socket_nonblocking"));
}
WSL1_TEST_METHOD(Splice)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests splice", L"Splice"));
}
WSL1_TEST_METHOD(Sysfs)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sysfs", L"SysFs"));
}
WSL1_TEST_METHOD(Tty)
{
auto OriginalHandles = UseOriginalStdHandles();
auto Restore = wil::scope_exit([&OriginalHandles]() { RestoreTestStdHandles(OriginalHandles); });
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests tty", L"tty"));
}
WSL1_TEST_METHOD(Utimensat)
{
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)
{
// This is disabled because of intermittent test failures in WSL1 mode.
// TODO: Enable this test once the underlying issue is resolved:
SKIP_TEST_UNSTABLE();
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"));
}
WSL1_TEST_METHOD(DevPt)
{
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"));
}
WSL1_TEST_METHOD(Timer)
{
// This is disabled because of intermittent test failures.
// TODO: Enable this test once the underlying issue is resolved.
SKIP_TEST_UNSTABLE();
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests timer", L"timer"));
}
WSL1_TEST_METHOD(SysInfo)
{
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"));
}
WSL1_TEST_METHOD(Ioprio)
{
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);
wsl::windows::common::security::ConfigureForCOMImpersonation(wslSupport.get());
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);
}
WSL1_TEST_METHOD(Execve)
{
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"));
}
WSL1_TEST_METHOD(Namespace)
{
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);
}
}
WSL1_TEST_METHOD(Netlink)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests netlink", L"Netlink"));
}
WSL1_TEST_METHOD(Random)
{
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"));
}
WSL1_TEST_METHOD(Shm)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests shm", L"shm"));
}
WSL1_TEST_METHOD(Sem)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sem", L"sem"));
}
WSL1_TEST_METHOD(Ttys)
{
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests ttys", L"Ttys"));
}
WSL1_TEST_METHOD(OverlayFs)
{
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 (IsMirroredNetworkingSupported())
{
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(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, const std::wstring& 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);
if (LxsstuVmMode())
{
validateOutput(
commandLine.c_str(),
FormatErrorMessage(
std::format(L"Failed to create disk '{}ext4.vhdx': The file exists. ", LXSST_IMPORT_DISTRO_TEST_DIR),
L"Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS"));
}
else
{
validateOutput(
commandLine.c_str(), FormatErrorMessage(L"The file exists. ", L"Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS"));
}
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(),
FormatErrorMessage(L"This operation is only supported by WSL2.", L"Wsl/Service/RegisterDistro/WSL_E_WSL2_NEEDED"));
}
//
// Verify that importing a distribution with a different name into the same path as an
// already registered distribution (test_distro) returns the path-already-exists error.
//
{
const auto distroKey = OpenDistributionKey(LXSS_DISTRO_NAME_TEST_L);
VERIFY_IS_TRUE(!!distroKey);
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
VERIFY_IS_FALSE(basePath.empty());
commandLine = std::format(L"--import path-conflict-distro \"{}\" \"{}\" --version {}", basePath, tarFileName, version);
validateOutput(
commandLine.c_str(),
FormatErrorMessage(
L"The supplied install location is already in use.", L"Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS"));
}
//
// 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, FormatErrorMessage(L"Importing the distribution failed.", L"Wsl/Service/RegisterDistro/WSL_E_IMPORT_FAILED"));
constexpr auto expectedError = L"bsdtar: Error opening archive: Unrecognized archive format\n";
if (LxsstuVmMode())
{
VERIFY_ARE_EQUAL(err, expectedError);
}
else
{
// lxcore.sys can close the stderr pipe before bsdtar has finished writing.
VERIFY_IS_TRUE(std::wstring_view{expectedError}.starts_with(err));
}
}
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 = FormatErrorMessage(Message, Code);
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);
}
static std::wstring ExpectedUsageMessage()
{
std::wstring expectedUsageMessage;
for (auto e : wsl::shared::Localization::MessageWslUsage())
{
if (e == L'\n')
{
expectedUsageMessage += L'\r';
}
expectedUsageMessage += e;
}
return expectedUsageMessage + L"\r\n";
}
static void VerifyInvalidUsage(const std::wstring& Cmd)
{
auto [output, error] = LxsstuLaunchWslAndCaptureOutput(Cmd.c_str(), -1);
VERIFY_ARE_EQUAL(ExpectedUsageMessage(), output);
VERIFY_ARE_EQUAL(error, L"");
}
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/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'. 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");
// wsl.exe --manage --compact requires WSL2.
ValidateErrorMessage(
L"--manage test_distro --compact", 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 distribution list 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 distribution list 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");
VerifyInvalidUsage(L"--manage --move .");
}
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"\"");
}
TEST_METHOD(CommandsRejectExtraArguments)
{
for (const auto* command :
{L"--debug-shell",
L"--help",
L"--status",
L"--version",
L"-v",
L"--set-default DoesNotExist",
L"--setdefault DoesNotExist",
L"-s DoesNotExist",
L"--terminate DoesNotExist",
L"-t DoesNotExist",
L"--unregister DoesNotExist",
L"--set-default-version 2",
L"--set-version DoesNotExist 2"})
{
for (const auto& [arguments, invalidArgument] :
{std::pair{L"extra", L"extra"},
std::pair{L"extra another", L"extra"},
std::pair{L"--unexpected", L"--unexpected"},
std::pair{L"\"extra argument\"", L"\"extra argument\""},
std::pair{L"\"\"", L"\"\""}})
{
auto [output, error] = LxsstuLaunchWslAndCaptureOutput(std::format(L"{} {}", command, arguments), -1);
VERIFY_ARE_EQUAL(
FormatErrorMessage(
std::format(
L"Invalid command line argument: {}\r\n"
L"Please use 'wsl.exe --help' to get a list of supported arguments.",
invalidArgument),
L"Wsl/E_INVALIDARG"),
output);
VERIFY_ARE_EQUAL(L"", error);
}
}
for (const auto* command : {L"--status", L"--version", L"-v"})
{
const auto [output, error] = LxsstuLaunchWslAndCaptureOutput(command);
VerifyOutput(std::format(L"{} \t ", command), output);
}
VerifyOutput(L"--help \t ", ExpectedUsageMessage(), -1);
}
TEST_METHOD(ManageInvalidUsage)
{
VerifyInvalidUsage(L"--manage " LXSS_DISTRO_NAME_TEST_L L" --compact --resize 10GB");
}
// 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.
--compact
Compact the VHDX file of a WSL 2 distribution.
--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.
)""";
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",
FormatErrorMessage(
L"Invalid distribution name: 'foo'.\r\nTo get a list of valid distributions, use 'wsl.exe --list --online'.",
L"Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND"),
-1);
}
WSL2_TEST_METHOD(TestExistingSwapVhd)
{
// Create a 100MB swap vhdx.
const auto swapVhd = wil::GetCurrentDirectoryW<std::wstring>() + L"\\TestSwap.vhdx";
const auto tokenUser = wil::get_token_information<TOKEN_USER>(GetCurrentProcessToken());
wsl::core::filesystem::CreateVhd(swapVhd.c_str(), 100 * _1MB, tokenUser->User.Sid, false, false);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
WslShutdown();
DeleteFile(swapVhd.c_str());
});
// Update .wslconfig. Escape 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.
const auto escapedSwapVhd = std::regex_replace(swapVhd, std::wregex(L"\\\\"), L"\\\\");
WslConfigChange configChange(LxssGenerateTestConfig() + L"\nswap=256MB\nswapFile=" + escapedSwapVhd);
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=" + escapedSwapVhd);
validateSwapSize(L"200M");
WslShutdown();
VerifyNoVmAccessToVhd(swapVhd.c_str());
}
TEST_METHOD(InitDoesntBlockSignals)
{
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"grep -iF SigBlk < /proc/1/status");
VERIFY_ARE_EQUAL(L"SigBlk:\t0000000000000000\n", output);
}
WSL2_TEST_METHOD(InitReadonly)
{
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L" grep '^rootfs /init rootfs ro,' /proc/self/mounts", nullptr, nullptr, nullptr, nullptr), 0u);
}
WSL2_TEST_METHOD(GpuMounts)
{
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=0x5,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([] { TerminateDistribution(); });
DistroFileChange config(L"/etc/wsl.conf", false);
auto validate = [&config](const std::string& input, const std::wstring& expectedOutput) {
config.SetContent(wsl::shared::string::MultiByteToWide("[network]\nhostname=" + input).c_str());
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)));
}
WSL2_TEST_METHOD(WslConfWarnings)
{
DistroFileChange configChange(L"/etc/wsl.conf", false);
auto validateWarnings = [&configChange](
const std::wstring& config,
const std::wstring& expectedWarnings,
const std::wstring& command = L"-u root echo ok",
const std::wstring& expectedOutput = L"ok\n") {
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(command);
VERIFY_ARE_EQUAL(expectedOutput, 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"a=a\\\r\n", L"wsl: Invalid escaped character: '\r' in /etc/wsl.conf:1\r\n");
validateWarnings(L"[=b", L"wsl: Invalid section name in /etc/wsl.conf:1\r\n");
validateWarnings(
L"[=b\n[network]\nhostname=foo", L"wsl: Invalid section name in /etc/wsl.conf:1\r\n", L"hostname", L"foo\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"");
}
}
WSL2_TEST_METHOD(Warnings)
{
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();
const auto dnsTunnelingDisabledWarning =
wsl::windows::common::helpers::IsServiceRunning(L"GlobalSecureAccessTunnelingService")
? std::format(L"wsl: {}\r\n", wsl::shared::Localization::MessageDnsTunnelingDisabled())
: L"";
validateWarnings(L"a=b", std::format(L"wsl: Unknown key 'wsl2.a' in {}:22\r\n", wslConfigPath));
validateWarnings(L"[=b", std::format(L"wsl: Invalid section name in {}:22\r\n", wslConfigPath));
validateWarnings(
L"dhcpTimeout=NotANumber",
std::format(L"wsl: Invalid integer value 'NotANumber' for key 'wsl2.dhcpTimeout' in {}:22\r\n", wslConfigPath));
validateWarnings(L"ipv6=NotABoolean", std::format(L"wsl: Invalid boolean value 'NotABoolean' for key 'wsl2.ipv6' in {}:22\r\n", wslConfigPath));
validateWarnings(L"[sectionNotComplete", std::format(L"wsl: Expected ']' in {}:22\r\n", wslConfigPath));
validateWarnings(L"NoEqual", std::format(L"wsl: Expected '=' in {}:22\r\n", wslConfigPath));
validateWarnings(
L"networkingMode=InvalidMode",
std::format(L"wsl: Invalid value 'InvalidMode' for config key 'wsl2.networkingMode' in {}:2 (Valid values: Bridged, Consomme, 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 {}:23\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 {}:23\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 {}:22\r\n", wslConfigPath));
validateWarnings(L"memory=", std::format(L"wsl: Invalid memory string '' for .wslconfig entry 'wsl2.memory' in {}:22\r\n", wslConfigPath));
validateWarnings(L"debugConsole=", std::format(L"wsl: Invalid boolean value '' for key 'wsl2.debugConsole' in {}:22\r\n", wslConfigPath));
validateWarnings(
L"networkingMode=",
std::format(L"wsl: Invalid value '' for config key 'wsl2.networkingMode' in {}:22 (Valid values: Bridged, Consomme, Mirrored, Nat, None, VirtioProxy)\r\n", wslConfigPath));
validateWarnings(
L"ipv6=true\nipv6=false",
std::format(L"wsl: Duplicated config key 'wsl2.ipv6' in {}:23 (Conflicting key: 'wsl2.ipv6' in {}:22)\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: " +
FormatErrorMessage(
L"Bridged networking requires wsl2.vmSwitch to be set.",
L"CreateInstance/CreateVm/ConfigureNetworking/WSL_E_VMSWITCH_NOT_SET") +
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: " +
FormatErrorMessage(
L"The VmSwitch 'DoesNotExist' was not found. Available switches:*",
L"CreateInstance/CreateVm/ConfigureNetworking/WSL_E_VMSWITCH_NOT_FOUND") +
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", dnsTunnelingDisabledWarning);
validateWarnings(L"[experimental]\ndnsTunneling=true\ndnsTunnelingIpAddress=10.255.255.1", dnsTunnelingDisabledWarning);
validateWarnings(
L"[experimental]\ndnsTunneling=true\ndnsTunnelingIpAddress=1.2.3",
std::format(L"wsl: Invalid IP value '1.2.3' for key 'experimental.dnsTunnelingIpAddress' in {}:24\r\n", wslConfigPath));
}
}
validateWarnings(
L"[experimental]\nignoredPorts=NotANumber",
std::format(L"wsl: Invalid integer value 'NotANumber' for key 'experimental.ignoredPorts' in {}:23\r\n", wslConfigPath));
validateWarnings(
L"[experimental]\nignoredPorts=65536",
std::format(L"wsl: Invalid integer value '65536' for key 'experimental.ignoredPorts' in {}:23\r\n", wslConfigPath));
// Verify experimental.swiotlb parsing and validation.
//
// With wsl2.virtio enabled (the default), a valid swiotlb value is accepted silently.
validateWarnings(L"[experimental]\nswiotlb=64M", L"");
constexpr auto expectedWarning =
wsl::shared::Arm64 ? L"wsl: The running kernel is missing a patch that significantly improves virtio device "
L"performance. Update to a more recent WSL kernel to enable this optimization.\r\n"
: L"";
validateWarnings(L"[experimental]\nswiotlb=4096K", expectedWarning);
// Malformed values are rejected by the parser; only the parser warning is reported.
validateWarnings(
L"[experimental]\nswiotlb=garbage",
std::format(L"wsl: Invalid memory string 'garbage' for .wslconfig entry 'experimental.swiotlb' in {}:23\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);
}
WSL2_TEST_METHOD(Processors)
{
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);
}
WSL2_TEST_METHOD(DmesgCollection)
{
const auto dmesgLogFile = std::filesystem::current_path() / L"test-dmesg.txt";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { DeleteFile(dmesgLogFile.c_str()); });
WslConfigChange config(LxssGenerateTestConfig({}));
auto readDmesgLog = [&](uint64_t offset) -> std::string {
wil::unique_hfile file(CreateFileW(
dmesgLogFile.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
if (!file)
{
return {};
}
LARGE_INTEGER fileOffset{};
fileOffset.QuadPart = static_cast<LONGLONG>(offset);
THROW_LAST_ERROR_IF(!SetFilePointerEx(file.get(), fileOffset, nullptr, FILE_BEGIN));
return ReadToString(file.get());
};
auto fileSize = [&]() -> uint64_t {
WIN32_FILE_ATTRIBUTE_DATA attributes{};
if (!GetFileAttributesExW(dmesgLogFile.c_str(), GetFileExInfoStandard, &attributes))
{
return 0;
}
return (static_cast<uint64_t>(attributes.nFileSizeHigh) << 32) | attributes.nFileSizeLow;
};
auto expectInDmesg = [&](bool earlyBootLogging, const std::string_view& expectedLine) -> std::string {
config.Update(LxssGenerateTestConfig({.earlyBootLogging = earlyBootLogging, .debugConsoleLogFile = dmesgLogFile}));
const auto offset = fileSize();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/bin/true"), 0L);
return wsl::shared::retry::RetryWithTimeout<std::string>(
[&]() {
auto content = readDmesgLog(offset);
THROW_HR_IF_MSG(E_FAIL, content.find(expectedLine) == std::string::npos, "%hs", content.c_str());
return content;
},
std::chrono::milliseconds(100),
std::chrono::seconds(120));
};
// 'Linux version' is printed during early boot. 'brd: module loaded' is printed after transitioning to the virtio console.
{
// N.B. brd is only loaded on X64.
auto dmesg = expectInDmesg(true, wsl::shared::Arm64 ? "Linux version" : "brd: module loaded");
VERIFY_ARE_NOT_EQUAL(dmesg.find("Linux version"), std::string::npos);
}
// N.B. Early boot logging is always enabled on ARM64.
if constexpr (!wsl::shared::Arm64)
{
auto dmesg = expectInDmesg(false, "brd: module loaded");
VERIFY_ARE_EQUAL(dmesg.find("Linux version"), std::string::npos);
}
}
WSL2_TEST_METHOD(GuiApplications)
{
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 that distro-provided tmpfiles rules cannot modify the read-only X11 mount.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -f /run/tmpfiles.d/x11.conf"), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemd-tmpfiles --create --boot x11.conf"), 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);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test ! -e /run/tmpfiles.d/x11.conf"), 0L);
// 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 = FormatErrorMessage(
L"GUI application support is disabled via " + configPath + L" or /etc/wsl.conf.",
L"Wsl/Service/CreateInstance/WSL_E_GUI_APPLICATIONS_DISABLED");
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);
}
WSL2_TEST_METHOD(GuiApplicationsSystemd)
{
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\\AppID\\{17696EAC-9568-4CF5-BB8C-82515AAD6C09}",
L"SOFTWARE\\Classes\\CLSID\\{2C3E9A41-7B5D-4F18-93D6-A8C2E4F7B1D9}\\InProcServer32",
L"SOFTWARE\\Classes\\CLSID\\{E3146082-A0DA-43A7-813B-A89EEE8C7628}\\InProcServer32",
L"SOFTWARE\\Classes\\CLSID\\{4F9C8B23-D6E1-4A85-BF2A-E7C5D8F931A6}\\InProcServer32",
L"SOFTWARE\\Classes\\CLSID\\{9C9C7131-D756-48FA-BD49-734E75AF37C0}\\InProcServer32",
L"SOFTWARE\\Classes\\CLSID\\{6D32A4B7-9E1F-4C82-A573-F8B1C4D29E60}\\InProcServer32",
L"SOFTWARE\\Classes\\Interface\\{27394DCF-6383-4E4E-BB0A-C13D4E5F6071}\\ProxyStubClsid32",
L"SOFTWARE\\Classes\\Interface\\{D2F47B8A-1E3C-4D9F-A6B5-7C8E9F0A1B2C}\\ProxyStubClsid32",
L"SOFTWARE\\Classes\\Interface\\{E3F58C9B-2F4D-4E0A-B7C6-8D9F0A1B2C3D}\\ProxyStubClsid32",
L"SOFTWARE\\Classes\\Interface\\{F406DACB-3050-4F1B-A8D7-9E0A1B2C3D4E}\\ProxyStubClsid32",
L"SOFTWARE\\Classes\\Interface\\{05172EBD-4161-4C2C-99E8-AF1B2C3D4E5F}\\ProxyStubClsid32",
L"SOFTWARE\\Classes\\Interface\\{16283FCE-5272-4D3D-AAF9-B02C3D4E5F60}\\ProxyStubClsid32"};
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::install::ValidateFileSignature(e.path().c_str());
signedFiles++;
}
}
// Sanity check
VERIFY_ARE_NOT_EQUAL(signedFiles, 0);
}
WSL2_TEST_METHOD(CorruptedVhd)
{
// Create a 100MB vhd without a filesystem.
auto distroPath = wsl::windows::common::filesystem::GetCanonicalPath(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()),
FormatErrorMessage(
std::format(
L"Failed to attach disk '\\\\?\\{}' to WSL2: The process cannot access the file because it is being used by "
L"another process. ",
vhdPath.wstring()),
L"Wsl/Service/RegisterDistro/MountDisk/HCS/ERROR_SHARING_VIOLATION"));
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",
FormatErrorMessage(
L"The distribution failed to start because its virtual disk is corrupted.",
L"Wsl/Service/CreateInstance/WSL_E_DISK_CORRUPTED"));
// Validate that trying to export the distribution fails with the correct error code.
validateOutput(
L"--export BrokenDistro dummy.tar",
FormatErrorMessage(
L"The distribution failed to start because its virtual disk is corrupted.",
L"Wsl/Service/WSL_E_DISK_CORRUPTED"));
// 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()),
FormatErrorMessage(
L"The distribution failed to start because its virtual disk is corrupted.",
L"Wsl/Service/RegisterDistro/WSL_E_DISK_CORRUPTED"));
// 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);
auto storage = shellLink.query<IPersistFile>();
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();
}
WSL2_TEST_METHOD(ManualDistroShutdown)
{
// 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");
}
WSL2_TEST_METHOD(KernelModules)
{
// 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/{}/modules,upperdir=/lib/modules/{}/rw/upper,workdir=/lib/modules/{}/rw/"
L"work,uuid=on)'",
kernelVersion,
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",
FormatErrorMessage(
wsl::shared::Localization::MessageCustomKernelNotFound(wslConfigPath, nonExistentFile),
L"Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND"),
L"");
configChange.Update(LxssGenerateTestConfig({.kernelModules = nonExistentFile.c_str()}));
ValidateOutput(
L"echo ok",
FormatErrorMessage(
wsl::shared::Localization::MessageCustomKernelModulesNotFound(wslConfigPath, nonExistentFile),
L"Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND"),
L"");
#ifdef WSL_DEV_INSTALL_PATH
std::wstring kernelPath = WSL_DEV_INSTALL_PATH L"/kernel";
std::wstring kernelModulesPath = WSL_DEV_INSTALL_PATH L"/artifacts.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" / "artifacts.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",
FormatErrorMessage(
wsl::shared::Localization::MessageMismatchedKernelModulesError(),
L"Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND"),
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);
}
WSL2_TEST_METHOD(KernelArtifacts)
{
// The unified kernel artifacts VHD provides the kernel headers and the perf tooling
// alongside the kernel modules. Headers are mounted at /usr/src/linux-headers-$(uname -r)
// with /lib/modules/$(uname -r)/build symlinked to that directory; perf is mounted at
// /usr/lib/linux-tools/$(uname -r) and exposed via $PATH.
// Headers: the build symlink and a representative uapi header are present.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -L /lib/modules/$(uname -r)/build", nullptr, nullptr, nullptr, nullptr), 0u);
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(L"test -s /lib/modules/$(uname -r)/build/include/linux/version.h", nullptr, nullptr, nullptr, nullptr), 0u);
// Headers are usable: compile and run a tiny program that includes recent uapi headers. The
// identifiers below fail to compile if the headers are missing or too old (BPF_PROG_TYPE_NETFILTER
// added in 6.4, IORING_OP_FUTEX_WAKE added in 6.7). Their numeric values are not a stable API
// contract, so the program only checks that <linux/version.h> matches the running kernel.
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(
LR"BASH(bash -ec '
d=$(mktemp -d)
trap "rm -rf $d" EXIT
cat > "$d/t.c" <<EOF
#include <stdio.h>
#include <linux/version.h>
#include <linux/bpf.h>
#include <linux/io_uring.h>
int main(void){
(void)BPF_PROG_TYPE_NETFILTER;
(void)IORING_OP_FUTEX_WAKE;
printf("%u.%u.%u\n",
LINUX_VERSION_MAJOR, LINUX_VERSION_PATCHLEVEL, LINUX_VERSION_SUBLEVEL);
return 0;
}
EOF
cc -isystem /lib/modules/$(uname -r)/build/include -o "$d/t" "$d/t.c"
v=$("$d/t")
case "$(uname -r)" in "$v"*) exit 0 ;; *) exit 8 ;; esac
')BASH",
nullptr,
nullptr,
nullptr,
nullptr),
0u);
// perf: leave no trace in the distro's file system and make the versioned binary reachable
// via $PATH, with PERF_EXEC_PATH pointing at its helper scripts.
//
// N.B. The test distro does not ship perf, so nothing should be created at /usr/bin/perf.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -x /usr/lib/linux-tools/$(uname -r)/bin/perf", nullptr, nullptr, nullptr, nullptr), 0u);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test ! -e /usr/bin/perf", nullptr, nullptr, nullptr, nullptr), 0u);
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(L"test \"$(command -v perf)\" = \"/usr/lib/linux-tools/$(uname -r)/bin/perf\"", nullptr, nullptr, nullptr, nullptr), 0u);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"perf --version", nullptr, nullptr, nullptr, nullptr), 0u);
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(L"test \"$(perf --exec-path)\" = \"/usr/lib/linux-tools/$(uname -r)/libexec/perf-core\"", nullptr, nullptr, nullptr, nullptr),
0u);
// Stale distro-provided artifacts are replaced or hidden after the VM restarts. A distro
// provided perf is shadowed by the binary matching the running kernel.
//
// N.B. The cleanup is registered before the distro's file system is modified so that a
// failure can't leave a perf binary or a broken build symlink behind, which would
// break subsequent runs.
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
LxsstuLaunchWsl(
L"umount /usr/bin/distro-perf 2>/dev/null; rm -f /usr/bin/perf /usr/bin/distro-perf; ln -snf"
L" /usr/src/linux-headers-$(uname -r) /lib/modules/$(uname -r)/build",
nullptr,
nullptr,
nullptr,
nullptr);
});
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(
L"rm /lib/modules/$(uname -r)/build && ln -s /tmp /lib/modules/$(uname -r)/build && printf old-perf >"
L" /usr/bin/distro-perf && ln -s distro-perf /usr/bin/perf",
nullptr,
nullptr,
nullptr,
nullptr),
0u);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--shutdown"), 0u);
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(L"test \"$(readlink /lib/modules/$(uname -r)/build)\" = \"/usr/src/linux-headers-$(uname -r)\"", nullptr, nullptr, nullptr, nullptr),
0u);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test \"$(readlink /usr/bin/perf)\" = \"distro-perf\"", nullptr, nullptr, nullptr, nullptr), 0u);
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(
L"test \"$(stat -Lc %d:%i /usr/bin/perf)\" = \"$(stat -Lc %d:%i /usr/lib/linux-tools/$(uname -r)/bin/perf)\"", nullptr, nullptr, nullptr, nullptr),
0u);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/usr/bin/perf --version", nullptr, nullptr, nullptr, nullptr), 0u);
}
WSL2_TEST_METHOD(CrashCollection)
{
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, [name]() {
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 = wsl::windows::common::filesystem::GetCanonicalPath(".").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,
FormatErrorMessage(
L"The supplied install location is already in use.", L"Wsl/Service/MoveDistro/ERROR_FILE_EXISTS"));
// 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,
FormatErrorMessage(
L"The filename, directory name, or volume label syntax is incorrect. ",
L"Wsl/Service/MoveDistro/ERROR_INVALID_NAME"));
// 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(CreateVhdPermissions)
{
const auto path = std::filesystem::path(L"wsl-test-vhd-permissions.vhdx");
const auto tokenUser = wil::get_token_information<TOKEN_USER>();
auto [administratorsSid, administratorsSidBuffer] =
wsl::windows::common::security::CreateSid(SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS);
for (const auto fixed : {false, true})
{
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove(path); });
wsl::core::filesystem::CreateVhd(path.c_str(), 16 * _1MB, tokenUser->User.Sid, false, fixed);
PSID owner{};
PACL dacl{};
wil::unique_hlocal descriptor;
THROW_IF_WIN32_ERROR(GetNamedSecurityInfoW(
path.c_str(), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, &owner, nullptr, &dacl, nullptr, &descriptor));
VERIFY_IS_TRUE(EqualSid(owner, tokenUser->User.Sid));
VERIFY_IS_NOT_NULL(dacl);
VERIFY_ARE_EQUAL(2, dacl->AceCount);
SECURITY_DESCRIPTOR_CONTROL control{};
DWORD revision{};
THROW_IF_WIN32_BOOL_FALSE(GetSecurityDescriptorControl(descriptor.get(), &control, &revision));
VERIFY_IS_TRUE(WI_IsFlagSet(control, SE_DACL_PROTECTED));
bool foundUser = false;
bool foundAdministrators = false;
for (DWORD index = 0; index < dacl->AceCount; ++index)
{
void* ace{};
THROW_IF_WIN32_BOOL_FALSE(GetAce(dacl, index, &ace));
auto* allowed = static_cast<ACCESS_ALLOWED_ACE*>(ace);
VERIFY_ARE_EQUAL(ACCESS_ALLOWED_ACE_TYPE, allowed->Header.AceType);
VERIFY_ARE_EQUAL(0, allowed->Header.AceFlags);
VERIFY_ARE_EQUAL(FILE_ALL_ACCESS, allowed->Mask);
if (EqualSid(&allowed->SidStart, tokenUser->User.Sid))
{
foundUser = true;
}
else
{
VERIFY_IS_TRUE(EqualSid(&allowed->SidStart, administratorsSid));
foundAdministrators = true;
}
}
VERIFY_IS_TRUE(foundUser);
VERIFY_IS_TRUE(foundAdministrators);
VERIFY_IS_TRUE(std::filesystem::remove(path));
}
}
WSL2_TEST_METHOD(SetSparseWithProtectedVhd)
{
constexpr auto name = L"sparse-protected-test-distro";
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
auto cleanup =
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() { LxsstuLaunchWsl(std::format(L"--unregister {}", name)); });
WslShutdown();
const auto distroKey = OpenDistributionKey(name);
VERIFY_IS_NOT_NULL(distroKey.get());
const auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
const auto vhdFileName =
wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"VhdFileName", L"ext4.vhdx");
auto vhdPath = (std::filesystem::path(basePath) / vhdFileName).wstring();
// Remove SYSTEM and Administrators access so success requires impersonating the user.
const auto tokenUser = wil::get_token_information<TOKEN_USER>();
EXPLICIT_ACCESS access{};
access.grfAccessMode = SET_ACCESS;
access.grfAccessPermissions = FILE_ALL_ACCESS;
access.grfInheritance = NO_INHERITANCE;
BuildTrusteeWithSid(&access.Trustee, tokenUser->User.Sid);
wsl::windows::common::security::unique_acl acl;
THROW_IF_WIN32_ERROR(SetEntriesInAcl(1, &access, nullptr, &acl));
THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW(
vhdPath.data(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, nullptr, nullptr, acl.get(), nullptr));
// Restrict administrator groups before converting to a primary token for process creation.
const auto nonElevatedToken = GetNonElevatedToken(TokenImpersonation);
VERIFY_IS_FALSE(wsl::windows::common::security::IsTokenElevated(nonElevatedToken.get()));
const auto tokenGroups = wil::get_token_information<TOKEN_GROUPS_AND_PRIVILEGES>(nonElevatedToken.get());
for (DWORD index = 0; index < tokenGroups->SidCount; ++index)
{
const auto& group = tokenGroups->Sids[index];
if (IsWellKnownSid(group.Sid, WinBuiltinAdministratorsSid))
{
// Sanity check.
VERIFY_IS_FALSE(WI_IsFlagSet(group.Attributes, SE_GROUP_ENABLED));
}
}
for (const auto elevated : {true, false})
{
for (const auto sparse : {true, false})
{
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(
std::format(L"--manage {} --set-sparse {} --allow-unsafe", name, sparse ? L"true" : L"false"),
nullptr,
nullptr,
nullptr,
elevated ? nullptr : nonElevatedToken.get()),
0L);
const auto attributes = GetFileAttributesW(vhdPath.c_str());
VERIFY_ARE_NOT_EQUAL(INVALID_FILE_ATTRIBUTES, attributes);
VERIFY_ARE_EQUAL(sparse, WI_IsFlagSet(attributes, FILE_ATTRIBUTE_SPARSE_FILE));
}
}
}
WSL2_TEST_METHOD(MoveVhdOwnership)
{
constexpr auto name = L"move-owner-test-distro";
constexpr auto moveElevatedFolder = L"move-owner-elevated";
constexpr auto moveNonElevatedFolder = L"move-owner-non-elevated";
// Import a WSL2 distro.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() {
LxsstuLaunchWsl(std::format(L"--unregister {}", name));
std::filesystem::remove_all(moveElevatedFolder);
std::filesystem::remove_all(moveNonElevatedFolder);
});
auto verifyVhdOwner = [](const std::wstring& path) {
PSID ownerSid = nullptr;
wil::unique_hlocal descriptor;
THROW_IF_WIN32_ERROR(GetNamedSecurityInfoW(
path.c_str(), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &ownerSid, nullptr, nullptr, nullptr, &descriptor));
auto userToken = wil::open_current_access_token(TOKEN_QUERY);
auto tokenUser = wil::get_token_information<TOKEN_USER>(userToken.get());
VERIFY_IS_TRUE(EqualSid(ownerSid, tokenUser->User.Sid));
};
const auto nonElevatedToken = GetNonElevatedToken();
// Move as elevated, launch as non-elevated.
// This is the primary bug scenario: MoveFileEx sets owner to BUILTIN\Administrators,
// then HcsGrantVmAccess fails with E_ACCESSDENIED when impersonating the non-elevated user.
{
WslShutdown();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, moveElevatedFolder)), 0L);
auto vhdPath = std::format(L"{}\\ext4.vhdx", moveElevatedFolder);
VERIFY_IS_TRUE(std::filesystem::exists(vhdPath));
verifyVhdOwner(vhdPath);
WslShutdown();
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name), 0, nullptr, nonElevatedToken.get());
VERIFY_ARE_EQUAL(out, L"ok\n");
}
// Move as non-elevated, launch as elevated.
{
WslShutdown();
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(
std::format(L"--manage {} --move {}", name, moveNonElevatedFolder),
nullptr,
nullptr,
nullptr,
nonElevatedToken.get()),
0L);
auto vhdPath = std::format(L"{}\\ext4.vhdx", moveNonElevatedFolder);
VERIFY_IS_TRUE(std::filesystem::exists(vhdPath));
verifyVhdOwner(vhdPath);
WslShutdown();
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name));
VERIFY_ARE_EQUAL(out, L"ok\n");
}
// Also launch as non-elevated after the non-elevated move.
{
WslShutdown();
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name), 0, nullptr, nonElevatedToken.get());
VERIFY_ARE_EQUAL(out, L"ok\n");
}
}
WSL2_TEST_METHOD(MoveVhdWithAdminOwner)
{
// Regression test for #40716: a same-volume move must succeed when the VHD
// is already owned by BUILTIN\Administrators.
constexpr auto name = L"move-admin-owner-test-distro";
constexpr auto firstFolder = L"move-admin-owner-first";
constexpr auto secondFolder = L"move-admin-owner-second";
// Import a WSL2 distro.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() {
LxsstuLaunchWsl(std::format(L"--unregister {}", name));
std::filesystem::remove_all(firstFolder);
std::filesystem::remove_all(secondFolder);
});
// Move to first folder so we know where the VHD is.
WslShutdown();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, firstFolder)), 0L);
auto vhdPath = std::format(L"{}\\ext4.vhdx", firstFolder);
VERIFY_IS_TRUE(std::filesystem::exists(vhdPath));
// Simulate cross-volume MoveFileEx side-effect: change VHD owner to BUILTIN\Administrators.
{
BYTE adminsSidBuffer[SECURITY_MAX_SID_SIZE];
DWORD sidSize = sizeof(adminsSidBuffer);
THROW_IF_WIN32_BOOL_FALSE(CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, adminsSidBuffer, &sidSize));
THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW(
const_cast<LPWSTR>(vhdPath.c_str()), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, adminsSidBuffer, nullptr, nullptr, nullptr));
// Verify it took effect.
PSID ownerSid = nullptr;
wil::unique_hlocal descriptor;
THROW_IF_WIN32_ERROR(GetNamedSecurityInfoW(
vhdPath.c_str(), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &ownerSid, nullptr, nullptr, nullptr, &descriptor));
VERIFY_IS_TRUE(EqualSid(ownerSid, adminsSidBuffer));
}
// Now move again as non-elevated. Before the fix, this would fail with E_ACCESSDENIED
// because CreateFileW(WRITE_OWNER) was called under user impersonation.
const auto nonElevatedToken = GetNonElevatedToken();
WslShutdown();
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, secondFolder), nullptr, nullptr, nullptr, nonElevatedToken.get()), 0L);
auto newVhdPath = std::format(L"{}\\ext4.vhdx", secondFolder);
VERIFY_IS_TRUE(std::filesystem::exists(newVhdPath));
// A same-volume move preserves the VHD owner.
{
PSID ownerSid = nullptr;
wil::unique_hlocal descriptor;
THROW_IF_WIN32_ERROR(GetNamedSecurityInfoW(
newVhdPath.c_str(), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &ownerSid, nullptr, nullptr, nullptr, &descriptor));
BYTE adminsSidCheck[SECURITY_MAX_SID_SIZE] = {};
DWORD sidSize = sizeof(adminsSidCheck);
THROW_IF_WIN32_BOOL_FALSE(CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, adminsSidCheck, &sidSize));
VERIFY_IS_TRUE(EqualSid(ownerSid, adminsSidCheck));
}
// Validate distro still works.
WslShutdown();
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name), 0, nullptr, nonElevatedToken.get());
VERIFY_ARE_EQUAL(out, L"ok\n");
}
WSL2_TEST_METHOD(Resize)
{
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, [name]() { LxsstuLaunchWsl(std::format(L"--unregister {}", name)); });
auto validateDistro = [name](LPCWSTR size, LPCWSTR expectedSize, const std::wstring& expectedError = {}) {
auto [out, _] =
LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --resize {}", name, size), expectedError.empty() ? 0 : -1);
if (!expectedError.empty())
{
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, FormatErrorMessage(L"Failed to resize disk.", L"Wsl/Service/E_FAIL"));
{
WslKeepAlive keepAlive;
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--manage test_distro --resize 1500GB", -1);
VERIFY_ARE_EQUAL(
FormatErrorMessage(
L"The operation could not be completed because the VHD is currently in use. To force WSL "
L"to stop use: wsl.exe --shutdown",
L"Wsl/Service/WSL_E_DISTRO_NOT_STOPPED"),
out);
}
}
// Verifies that VHD-mutating manage operations (--resize, --set-sparse, --move) are rejected while a
// long-running conversion/export holds the distribution lock, rather than racing with it on the VHD.
WSL2_TEST_METHOD(ManageRejectedWhileLocked)
{
constexpr auto name = L"manage-locked-test-distro";
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
auto cleanupName =
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() { LxsstuLaunchWsl(std::format(L"--unregister {}", name)); });
WslShutdown();
// Start an export to a pipe we deliberately don't drain. Use a tiny buffer so the export blocks
// as soon as it writes any data, regardless of the test distro's size, deterministically holding
// the distribution in the "Exporting" locked state.
auto [readPipe, writePipe] = CreateSubprocessPipe(false, true, 1);
std::thread exportThread([&]() { LxsstuLaunchWsl(std::format(L"--export {} -", name), nullptr, writePipe.get()); });
auto joinExport = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
// Close the read end so the blocked export fails with a broken pipe and returns, then join.
readPipe.reset();
if (exportThread.joinable())
{
exportThread.join();
}
});
// Wait until the service reports the distribution as Exporting (i.e. the lock is held), retrying for up
// to two minutes so a slow machine doesn't flake before the export acquires the lock.
wsl::shared::retry::RetryWithTimeout<void>(
[&]() {
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--list --verbose");
bool locked = false;
std::wistringstream stream(out);
for (std::wstring line; std::getline(stream, line);)
{
if (line.find(name) != std::wstring::npos && line.find(L"Exporting") != std::wstring::npos)
{
locked = true;
break;
}
}
THROW_HR_IF(E_ABORT, !locked);
},
std::chrono::milliseconds(100),
std::chrono::minutes(2),
[] { return wil::ResultFromCaughtException() == E_ABORT; });
// Each VHD-mutating manage operation must be rejected with E_ILLEGAL_STATE_CHANGE while the lock is held.
auto verifyRejected = [&](const std::wstring& command) {
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(command, -1);
VERIFY_IS_TRUE(out.find(L"E_ILLEGAL_STATE_CHANGE") != std::wstring::npos);
};
verifyRejected(std::format(L"--manage {} --resize 2GB", name));
verifyRejected(std::format(L"--manage {} --set-sparse false", name));
const auto moveTarget = std::filesystem::absolute(L"manage-locked-move-target").wstring();
verifyRejected(std::format(L"--manage {} --move \"{}\"", name, moveTarget));
}
WSL2_TEST_METHOD(Compact)
{
constexpr auto name = L"compact-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, [name]() { LxsstuLaunchWsl(std::format(L"--unregister {}", name)); });
const auto distroKey = OpenDistributionKey(name);
VERIFY_IS_NOT_NULL(distroKey.get());
const auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
const auto vhdFileName =
wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"VhdFileName", L"ext4.vhdx");
const auto vhdPath = std::filesystem::path(basePath) / vhdFileName;
VERIFY_IS_TRUE(std::filesystem::exists(vhdPath));
auto getVhdSizeOnDisk = [](const std::filesystem::path& path) {
DWORD highPart{};
SetLastError(NO_ERROR);
const auto lowPart = GetCompressedFileSizeW(path.c_str(), &highPart);
THROW_LAST_ERROR_IF(lowPart == INVALID_FILE_SIZE && GetLastError() != NO_ERROR);
ULARGE_INTEGER size{};
size.LowPart = lowPart;
size.HighPart = highPart;
return size.QuadPart;
};
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --compact", name));
VERIFY_ARE_EQUAL(err, L"");
constexpr auto minimumCompactionDelta = 32ull * 1024 * 1024;
const auto sizeBeforeWrite = getVhdSizeOnDisk(vhdPath);
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(
L"-d {} -u root -- sh -c 'mkdir -p /root/vhdx-compact-test && "
L"dd if=/dev/zero bs=1M count=128 2>/dev/null | base64 -w 0 > /root/vhdx-compact-test/nonzero.bin && sync'",
name));
VERIFY_ARE_EQUAL(err, L"");
WslShutdown();
const auto sizeAfterWrite = getVhdSizeOnDisk(vhdPath);
VERIFY_IS_TRUE(sizeAfterWrite >= sizeBeforeWrite + minimumCompactionDelta);
// Delete the file but do NOT trim from inside the guest: reclaiming the freed blocks now
// depends on the trim that '--compact' performs on the host before compacting the VHD.
std::tie(out, err) =
LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} -u root -- sh -c 'rm /root/vhdx-compact-test/nonzero.bin && sync'", name));
VERIFY_ARE_EQUAL(err, L"");
WslShutdown();
const auto sizeBeforeCompact = getVhdSizeOnDisk(vhdPath);
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --compact", name));
VERIFY_ARE_EQUAL(err, L"");
const auto sizeAfterCompact = getVhdSizeOnDisk(vhdPath);
LogInfo(
"Compact test VHD size on disk: before write=%llu, after write=%llu, before compact=%llu, after compact=%llu",
static_cast<unsigned long long>(sizeBeforeWrite),
static_cast<unsigned long long>(sizeAfterWrite),
static_cast<unsigned long long>(sizeBeforeCompact),
static_cast<unsigned long long>(sizeAfterCompact));
VERIFY_IS_TRUE(sizeBeforeCompact >= sizeAfterCompact);
VERIFY_IS_TRUE(sizeAfterWrite >= sizeAfterCompact + minimumCompactionDelta);
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --compact", name));
VERIFY_ARE_EQUAL(err, L"");
VerifyNoVmAccessToVhd(vhdPath.c_str());
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name));
VERIFY_ARE_EQUAL(out, L"ok\n");
VERIFY_ARE_EQUAL(err, L"");
WslShutdown();
VerifyNoVmAccessToVhd(vhdPath.c_str());
}
WSL2_TEST_METHOD(FileOffsets)
{
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");
}
WSL2_TEST_METHOD(MergedOutputFileOffsets)
{
constexpr auto c_outputPath = L"merged-output.txt";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(c_outputPath); });
constexpr int c_byteCount = 100;
const auto command = std::format(
L"cmd.exe /d /s /c \"wsl.exe -d {} --exec /bin/sh -c "
L"\"for index in $(seq 1 {}); do printf O; printf E >&2; done\" > {} 2>&1\"",
LXSS_DISTRO_NAME_TEST_L,
c_byteCount,
c_outputPath);
wsl::windows::common::SubProcess process(nullptr, command.c_str());
VERIFY_ARE_EQUAL(process.Run(), 0L);
const auto actual = ReadFileContent(c_outputPath);
VERIFY_ARE_EQUAL(2 * c_byteCount, actual.size());
VERIFY_ARE_EQUAL(c_byteCount, std::count(actual.begin(), actual.end(), L'O'));
VERIFY_ARE_EQUAL(c_byteCount, std::count(actual.begin(), actual.end(), L'E'));
}
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());
}
WSL2_TEST_METHOD(WriteWslConfig)
{
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""},
// Verifies that reloading preserves BMP Chinese characters and the rocket's UTF-16 surrogate pair (U+1F680).
{L"C:\\中文🚀\\ext4.vhdx", L"C:\\中文🚀\\ext4.vhdx"},
};
// 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::Consomme, NetworkingConfiguration::Consomme},
};
// 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);
}
// Regression test for GitHub issue #12671:
// Ensure that section headers always appear BEFORE their key-value pairs.
// Bug: WSL Settings GUI was writing keys before the section header, causing "Unknown key" errors.
{
std::wstring bugScenarioConfig =
LR"([wsl2]
[experimental]
[wsl2]
)";
WslConfigChange config{bugScenarioConfig.c_str()};
wslConfig = createWslConfig(apiWslConfigFilePath);
VERIFY_IS_NOT_NULL(wslConfig);
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
// Write memory setting - this should NOT appear before the first [wsl2]
WslConfigSetting memorySetting{};
memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
memorySetting.UInt64Value = 17825792000ULL; // Value from bug report
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
// Read and verify
std::wifstream configRead(apiWslConfigFilePath);
std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
configRead.close();
// Find FIRST occurrence of [wsl2] and memory=
auto firstWsl2Pos = fileContent.find(L"[wsl2]");
auto memoryPos = fileContent.find(L"memory=");
VERIFY_ARE_NOT_EQUAL(firstWsl2Pos, std::wstring::npos);
VERIFY_ARE_NOT_EQUAL(memoryPos, std::wstring::npos);
// The critical assertion: memory= must NOT appear before [wsl2]
VERIFY_IS_TRUE(firstWsl2Pos < memoryPos);
// Additional check: memory should appear after the first [wsl2], not after line 1
auto firstLineEnd = fileContent.find(L'\n');
VERIFY_IS_TRUE(memoryPos > firstLineEnd);
}
// Test: Empty file - should create proper [wsl2] section structure
{
std::wofstream emptyConfig(apiWslConfigFilePath, std::ios::trunc);
emptyConfig.close();
wslConfig = createWslConfig(apiWslConfigFilePath);
VERIFY_IS_NOT_NULL(wslConfig);
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
WslConfigSetting memorySetting{};
memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
memorySetting.UInt64Value = 4294967296ULL; // 4GB
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
std::wifstream configRead(apiWslConfigFilePath);
std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
configRead.close();
// Should create [wsl2] section and add memory key
VERIFY_IS_TRUE(fileContent.find(L"[wsl2]") != std::wstring::npos);
VERIFY_IS_TRUE(fileContent.find(L"memory=") != std::wstring::npos);
// Verify [wsl2] comes before memory=
VERIFY_IS_TRUE(fileContent.find(L"[wsl2]") < fileContent.find(L"memory="));
}
// Test: Multiple same-section instances - should update first occurrence
{
std::wofstream configFile(apiWslConfigFilePath, std::ios::trunc);
configFile << L"[wsl2]\n";
configFile << L"processors=4\n";
configFile << L"\n";
configFile << L"[experimental]\n";
configFile << L"autoProxy=true\n";
configFile << L"\n";
configFile << L"[wsl2]\n"; // Second [wsl2] section
configFile << L"swap=0\n";
configFile.close();
wslConfig = createWslConfig(apiWslConfigFilePath);
VERIFY_IS_NOT_NULL(wslConfig);
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
WslConfigSetting memorySetting{};
memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
memorySetting.UInt64Value = 8589934592ULL; // 8GB
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
std::wifstream configRead(apiWslConfigFilePath);
std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
configRead.close();
// Find first and second [wsl2]
auto firstWsl2 = fileContent.find(L"[wsl2]");
auto secondWsl2 = fileContent.find(L"[wsl2]", firstWsl2 + 1);
auto memoryPos = fileContent.find(L"memory=");
VERIFY_ARE_NOT_EQUAL(firstWsl2, std::wstring::npos);
VERIFY_ARE_NOT_EQUAL(secondWsl2, std::wstring::npos);
VERIFY_ARE_NOT_EQUAL(memoryPos, std::wstring::npos);
// Memory should be added to FIRST [wsl2] section, not second
VERIFY_IS_TRUE(memoryPos > firstWsl2);
VERIFY_IS_TRUE(memoryPos < secondWsl2);
}
// Test: EOF without trailing newline
{
std::wofstream configFile(apiWslConfigFilePath, std::ios::trunc);
configFile << L"[wsl2]\n";
configFile << L"processors=2"; // No trailing newline
configFile.close();
wslConfig = createWslConfig(apiWslConfigFilePath);
VERIFY_IS_NOT_NULL(wslConfig);
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
WslConfigSetting memorySetting{};
memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
memorySetting.UInt64Value = 3221225472ULL; // 3GB
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
std::wifstream configRead(apiWslConfigFilePath);
std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
configRead.close();
// Should properly append memory key even without trailing newline on last line
VERIFY_IS_TRUE(fileContent.find(L"processors=2") != std::wstring::npos);
VERIFY_IS_TRUE(fileContent.find(L"memory=") != std::wstring::npos);
// Verify both keys are in the same section
auto wsl2Pos = fileContent.find(L"[wsl2]");
auto processorsPos = fileContent.find(L"processors=2");
auto memoryPos = fileContent.find(L"memory=");
VERIFY_IS_TRUE(wsl2Pos < processorsPos);
VERIFY_IS_TRUE(wsl2Pos < memoryPos);
// Memory should come after processors in the same section
VERIFY_IS_TRUE(processorsPos < memoryPos);
}
// Test: Empty section followed by another section
{
std::wofstream configFile(apiWslConfigFilePath, std::ios::trunc);
configFile << L"[wsl2]\n";
configFile << L"[experimental]\n";
configFile << L"autoProxy=true\n";
configFile.close();
wslConfig = createWslConfig(apiWslConfigFilePath);
VERIFY_IS_NOT_NULL(wslConfig);
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
WslConfigSetting memorySetting{};
memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
memorySetting.UInt64Value = 5368709120ULL; // 5GB
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
std::wifstream configRead(apiWslConfigFilePath);
std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
configRead.close();
// Should insert memory into empty [wsl2] section before [experimental]
auto wsl2Pos = fileContent.find(L"[wsl2]");
auto memoryPos = fileContent.find(L"memory=");
auto experimentalPos = fileContent.find(L"[experimental]");
VERIFY_ARE_NOT_EQUAL(wsl2Pos, std::wstring::npos);
VERIFY_ARE_NOT_EQUAL(memoryPos, std::wstring::npos);
VERIFY_ARE_NOT_EQUAL(experimentalPos, std::wstring::npos);
// Order should be: [wsl2], memory=, [experimental]
VERIFY_IS_TRUE(wsl2Pos < memoryPos);
VERIFY_IS_TRUE(memoryPos < experimentalPos);
}
// Test: Section header at EOF with no content
{
std::wofstream configFile(apiWslConfigFilePath, std::ios::trunc);
configFile << L"[wsl2]"; // Section at EOF, no newline, no content
configFile.close();
wslConfig = createWslConfig(apiWslConfigFilePath);
VERIFY_IS_NOT_NULL(wslConfig);
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
WslConfigSetting memorySetting{};
memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
memorySetting.UInt64Value = 6442450944ULL; // 6GB
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
std::wifstream configRead(apiWslConfigFilePath);
std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
configRead.close();
// Should properly add key to section at EOF
VERIFY_IS_TRUE(fileContent.find(L"[wsl2]") != std::wstring::npos);
VERIFY_IS_TRUE(fileContent.find(L"memory=") != std::wstring::npos);
VERIFY_IS_TRUE(fileContent.find(L"[wsl2]") < fileContent.find(L"memory="));
}
}
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, [TestUser]() { 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"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, FormatErrorMessage(L"There is no distribution with the supplied name.", L"Wsl/Service/WSL_E_DISTRO_NOT_FOUND"));
constexpr auto injectionMarker = L"/tmp/wsl-manage-default-user-injection";
LxsstuLaunchWsl(std::format(L"-u root -e /usr/bin/rm -f {}", injectionMarker));
auto cleanupInjectionMarker = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [injectionMarker]() {
LxsstuLaunchWsl(std::format(L"-u root -e /usr/bin/rm -f {}", injectionMarker));
});
const auto injectionUsername = std::format(L"' || touch {} || '", injectionMarker);
const std::array<std::wstring_view, 4> injectionArguments{
WSL_MANAGE_ARG, LXSS_DISTRO_NAME_TEST_L, WSL_MANAGE_ARG_SET_DEFAULT_USER_OPTION_LONG, injectionUsername};
const auto injectionCommand = wil::ArgvToCommandLine(injectionArguments, wil::ArgvToCommandLineFlags::FirstArgumentIsNotPath);
auto injectionCommandLine = LxssGenerateWslCommandLine(injectionCommand.c_str());
const auto injectionExitCode = LxsstuRunCommand(injectionCommandLine.data());
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-u root -e /usr/bin/test ! -e {}", injectionMarker)), 0L);
VERIFY_ARE_EQUAL(injectionExitCode, 1L);
}
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"13");
constexpr auto testTar = L"exported-distro.tar";
constexpr auto tmpDistroName = L"tmpdistro";
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [tmpDistroName]() {
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"13");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--set-version {} {}", tmpDistroName, currentVersion).c_str()), 0L);
validateFlavorVersion(tmpDistroName, L"debian", L"13");
}
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, const std::wstring& 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", FormatErrorMessage(L"The parameter is incorrect. ", L"Wsl/E_INVALIDARG"), -1);
validateOutput(
L"--distribution-id {C13B2B63-F9D5-4840-8105-F6ABECCF46CA}",
FormatErrorMessage(
L"There is no distribution with the supplied name.",
L"Wsl/Service/CreateInstance/ReadDistroConfig/WSL_E_DISTRO_NOT_FOUND"),
-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);
constexpr auto manifest =
L"[oobe]\n"
L"command = /bin/bash -c 'echo OOBE'\n"
L"defaultUid = 0\n"
L"defaultName = test-default-name\n"
L"[shortcut]\n"
L"icon = /icon.ico\n"
L"enabled = false\n"
L"[windowsterminal]\n"
L"ProfileTemplate = /terminal.json\n"
L"enabled = false\n";
distributionconf.SetContent(manifest);
GUID runId;
THROW_IF_FAILED(CoCreateGuid(&runId));
const auto drvFsTestPath = std::filesystem::temp_directory_path() /
std::format(L"wsl-oobe-drvfs-test-{}", wsl::shared::string::GuidToString<wchar_t>(runId));
std::filesystem::create_directory(drvFsTestPath);
auto cleanupDrvFsTestPath = wil::scope_exit([&]() {
std::error_code ignored;
std::filesystem::remove_all(drvFsTestPath, ignored);
});
auto cleanupUser = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
TerminateDistribution();
const auto exitCode = LxsstuLaunchWsl(L"userdel -f -r user");
// The test may fail before OOBE creates the user (userdel exits with 6 if it does not exist).
VERIFY_IS_TRUE(exitCode == 0 || exitCode == 6);
});
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 without warnings for any supported manifest keys.
validateOutput(nullptr, L"OOBE\n");
VERIFY_ARE_EQUAL(runOOBE.Get(), 0);
// OOBE should only trigger once
validateOutput(L"", L"");
}
{
runOOBE.Set(1);
distributionconf.SetContent((std::wstring(manifest) + L"[unknown]\nkey = value\n").c_str());
TerminateDistribution();
validateOutput(nullptr, L"OOBE\n", L"wsl: Unknown key 'unknown.key' in /etc/wsl-distribution.conf:12\n");
VERIFY_ARE_EQUAL(runOOBE.Get(), 0);
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");
constexpr auto userMountPoint = L"/run/wsl-oobe-user-mount";
const auto userMountIdCommand = std::format(L"findmnt -n -o ID -M {}", userMountPoint);
const auto userMountOwnerCommand = std::format(L"stat -c %u:%g {}", userMountPoint);
std::wstring originalUserMountId;
std::optional<DistroFileChange> fstab;
auto cleanupUserMount = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
if (fstab.has_value())
{
fstab.reset();
TerminateDistribution();
}
});
if (LxsstuVmMode())
{
fstab.emplace(L"/etc/fstab", false);
fstab->SetContent(
std::format(L"{} {} drvfs uid=2000,gid=2001,x-mount.mkdir 0 0\n", drvFsTestPath.root_path().generic_wstring(), userMountPoint)
.c_str());
}
TerminateDistribution();
if (LxsstuVmMode())
{
auto [mountId, warnings] = LxsstuLaunchWslAndCaptureOutput(userMountIdCommand);
VERIFY_IS_FALSE(mountId.empty());
VERIFY_ARE_EQUAL(L"", warnings);
originalUserMountId = std::move(mountId);
validateOutput(userMountOwnerCommand.c_str(), L"2000:2001\n");
VERIFY_ARE_EQUAL(1, runOOBE.Get());
}
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);
if (LxsstuVmMode())
{
// DrvFs mounts created before OOBE should be refreshed to use the new default user.
validateOutput(
std::format(
L"bash -c 'path=$(wslpath -u \"{}\") && mountpoint=$(findmnt -n -o TARGET -T \"$path\") && "
L"test \"$(stat -c %u:%g \"$mountpoint\")\" = \"$(id -u):$(id -g)\" && touch \"$path/probe\" && "
L"chmod 0644 \"$path/probe\"'",
drvFsTestPath.wstring())
.c_str(),
L"");
validateOutput(userMountOwnerCommand.c_str(), L"2000:2001\n");
validateOutput(userMountIdCommand.c_str(), originalUserMountId.c_str());
}
// New file should be created with the correct uid.
const std::wstring testFilePathLinux = L"/tmp/oobe_file_test";
const std::wstring testFilePathWindows = L"\\\\wsl.localhost\\" LXSS_DISTRO_NAME_TEST_L L"\\tmp\\oobe_file_test";
const wil::unique_hfile file(CreateFile(
testFilePathWindows.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
VERIFY_IS_TRUE(file.is_valid());
validateOutput(std::format(L"stat -c %u {}", testFilePathLinux).c_str(), L"1010\n");
}
if (LxsstuVmMode())
{
runOOBE.Set(1);
const auto mountIdCommand =
std::format(L"path=$(wslpath -u \"{}\") && findmnt -n -o ID -T \"$path\"", drvFsTestPath.generic_wstring());
distributionconf.SetContent(
std::format(L"[oobe]\ncommand = /bin/bash -c '{} > \"$path/mount-id\"'\ndefaultUid = 1010\n", mountIdCommand).c_str());
TerminateDistribution();
validateOutput(nullptr, L"");
VERIFY_ARE_EQUAL(runOOBE.Get(), 0);
VERIFY_ARE_EQUAL(defaultUid.Get(), 1010);
validateOutput(std::format(L"bash -c '{} | cmp -s - \"$path/mount-id\"'", mountIdCommand).c_str(), L"");
}
// 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, testDistroName]() {
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"");
}
}
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
const auto expectedOutput = L"Installing: distro-no-default-name.tar\r\n" +
FormatErrorMessage(
L"This distribution doesn't contain a default name. Use --name to choose the "
L"distribution name.",
L"Wsl/Service/RegisterDistro/WSL_E_DISTRIBUTION_NAME_NEEDED");
InstallFromTar(L"distro-no-default-name.tar", L"", -1, expectedOutput.c_str());
// 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);
{
WslConfigChange config(LxssGenerateTestConfig({.sparse = true}));
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");
const auto expectedOutput = L"Installing: conflict.tar\r\n" +
FormatErrorMessage(
L"A distribution with the supplied name already exists. Use --name to choose a "
L"different name.",
L"Wsl/Service/RegisterDistro/ERROR_ALREADY_EXISTS");
InstallFromTar(L"conflict.tar", L"", -1, expectedOutput.c_str());
}
// 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");
const auto expectedOutput =
L"Installing: invalid.tar\r\n" +
FormatErrorMessage(L"Invalid distribution name: \"invalid!\".", L"Wsl/Service/RegisterDistro/E_INVALIDARG");
InstallFromTar(L"invalid.tar", L"", -1, expectedOutput.c_str());
}
// 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, [distroName]() {
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, [distroName]() {
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, [distroName]() {
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, [distroName]() {
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, [distroName]() {
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": "{0}",
"Sha256": "{1}"
}},
"Arm64Url": {{
"Url": "{0}",
"Sha256": "{1}"
}}
}}
]
}}}})",
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",
FormatErrorMessage(
L"Invalid distribution name: 'DoesNotExists'.\r\nTo get a list of valid distributions, use 'wsl.exe "
L"--list --online'.",
L"Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND"));
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": ""
}},
"Arm64Url": {{
"Url": "",
"Sha256": ""
}}
}},
{{
"Name": "debian-default",
"FriendlyName": "DebianFriendlyName",
"Default": true,
"Amd64Url": {{
"Url": "{0}",
"Sha256": "{1}"
}},
"Arm64Url": {{
"Url": "{0}",
"Sha256": "{1}"
}}
}}
],
"ubuntu": [
{{
"Name": "ubuntu-nondefault",
"FriendlyName": "",
"Amd64Url": {{
"Url": "",
"Sha256": ""
}},
"Arm64Url": {{
"Url": "",
"Sha256": ""
}}
}},
{{
"Name": "ubuntu-default",
"FriendlyName": "UbuntuFriendlyName",
"Default": true,
"Amd64Url": {{
"Url": "{2}",
"Sha256": "{3}"
}},
"Arm64Url": {{
"Url": "{2}",
"Sha256": "{3}"
}}
}}
]
}}}})",
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": ""
}},
"Arm64Url": {{
"Url": "",
"Sha256": ""
}}
}}
]
}},
"Distributions": [
{{"Name": "legacy",
"FriendlyName": "legacy",
"StoreAppId": "Dummy",
"PackageFamilyName": "Dummy",
"Amd64": true,
"Arm64": true,
"Amd64PackageUrl": "http://127.0.0.1:12/dummyUrl",
"Arm64PackageUrl": "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" +
FormatErrorMessage(
L"A connection with the server could not be established ", L"Wsl/InstallDistro/WININET_E_CANNOT_CONNECT"),
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" +
FormatErrorMessage(
L"A connection with the server could not be established ", L"Wsl/InstallDistro/WININET_E_CANNOT_CONNECT"),
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": "{0}",
"Sha256": "{1}"
}},
"Arm64Url": {{
"Url": "{0}",
"Sha256": "{1}"
}}
}}
]
}},
"Distributions": [
{{"Name": "debian-12",
"FriendlyName": "debian-12",
"StoreAppId": "Dummy",
"PackageFamilyName": "Dummy",
"Amd64": true,
"Arm64": true,
"Amd64PackageUrl": "http://127.0.0.1:12/dummyUrl",
"Arm64PackageUrl": "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" +
FormatErrorMessage(
L"A connection with the server could not be established ", L"Wsl/InstallDistro/WININET_E_CANNOT_CONNECT"),
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": "{0}",
"Sha256": "{1}"
}},
"Arm64Url": {{
"Url": "{0}",
"Sha256": "{1}"
}}
}},
{{
"Name": "debian-base",
"FriendlyName": "DebianFriendlyName",
"Default": true,
"Amd64Url": {{
"Url": "{2}",
"Sha256": ""
}},
"Arm64Url": {{
"Url": "{2}",
"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": "{0}",
"Sha256": "{1}"
}},
"Arm64Url": {{
"Url": "{0}",
"Sha256": "{1}"
}}
}}
]
}}
}})",
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": "{0}",
"Sha256": "{1}"
}},
"Arm64Url": {{
"Url": "{0}",
"Sha256": "{1}"
}}
}}
]
}}
}})",
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": "{0}",
"Sha256": "0x12"
}},
"Arm64Url": {{
"Url": "{0}",
"Sha256": "0x12"
}}
}}
]
}}
}})",
tarPath);
auto restore = SetManifest(manifest);
ValidateInstallError(
L"--install debian-12",
L"Installing: DebianFriendlyName\r\n" +
FormatErrorMessage(
std::format(L"The distribution hash doesn't match. Expected: 0x12, actual hash: {}", wsl::shared::string::MultiByteToWide(tarHash)),
L"Wsl/InstallDistro/VerifyChecksum/TRUST_E_BAD_DIGEST"),
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": "{0}",
"Sha256": "wrongformat"
}},
"Arm64Url": {{
"Url": "{0}",
"Sha256": "wrongformat"
}}
}}
]
}}
}})",
tarPath);
auto restore = SetManifest(manifest);
ValidateInstallError(
L"--install debian-12",
L"Installing: DebianFriendlyName\r\n" +
FormatErrorMessage(L"Invalid hex string: wrongformat", L"Wsl/InstallDistro/VerifyChecksum/E_INVALIDARG"),
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": "",
"Arm64PackageUrl": "" }]
})";
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",
FormatErrorMessage(
L"Invalid distribution name: 'invalid'.\r\nTo get a list of valid distributions, use 'wsl.exe --list "
L"--online'.",
L"Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND"),
L"");
}
// Validate that a distribution isn't downloaded if its name is already in use.
{
auto manifest = std::format(
R"({{
"ModernDistributions": {{
"debian": [
{{
"Name": "{0}",
"FriendlyName": "DebianFriendlyName",
"Amd64Url": {{
"Url": "file://nonexistent",
"Sha256": ""
}},
"Arm64Url": {{
"Url": "file://nonexistent",
"Sha256": ""
}}
}},
{{
"Name": "dummy",
"FriendlyName": "dummy",
"Amd64Url": {{
"Url": "file://nonexistent",
"Sha256": ""
}},
"Arm64Url": {{
"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,
FormatErrorMessage(
L"Cannot create a file when that file already exists. ", L"Wsl/InstallDistro/ERROR_ALREADY_EXISTS"));
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,
FormatErrorMessage(
L"Cannot create a file when that file already exists. ", L"Wsl/InstallDistro/ERROR_ALREADY_EXISTS"));
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": ""
},
"Arm64Url": {
"Url": "",
"Sha256": ""
}
}
]
}
})";
auto restore = SetManifest(manifest);
ValidateInstallError(
L"--install",
FormatErrorMessage(
L"No default distribution has been configured. Please provide a distribution to install.",
L"Wsl/InstallDistro/E_UNEXPECTED"),
L"");
}
// Validate that invalid json errors are correctly handled.
{
auto restore = SetManifest("Bad json");
ValidateInstallError(
L"--install debian",
FormatErrorMessage(
L"Invalid JSON document. Parse error: [json.exception.parse_error.101] parse error at line 1, column 1: "
L"syntax error while parsing value - invalid literal; last read: 'B'",
L"Wsl/InstallDistro/WSL_E_INVALID_JSON"),
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": "{0}/distro.tar?foo=bar&key=value",
"Sha256": "{1}"
}},
"Arm64Url": {{
"Url": "{0}/distro.tar?foo=bar&key=value",
"Sha256": "{1}"
}}
}}
]
}}}})",
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\": \"{0}/distro.tar\",
\"Sha256\": \"{1}\"
}},
\"Arm64Url\": {{
\"Url\": \"{0}/distro.tar\",
\"Sha256\": \"{1}\"
}}
}}
]
}}}})",
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"-");
}
}
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(CaseSensitivityDeepNesting)
{
// Regression test for stack overflow in EnsureCaseSensitiveDirectoryRecursive on deeply
// nested directory trees. The original recursive DFS implementation could blow the
// 1 MB Windows thread stack at a few hundred levels of nesting (each frame held a
// FILE_ID_BOTH_DIR_INFORMATION buffer plus locals); the iterative implementation must
// succeed at depths well beyond that without consuming caller stack space.
constexpr auto testDir = L"deep-case-test";
constexpr int depth = 1024;
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, []() {
// The deep tree exceeds MAX_PATH; remove it via the long-path prefix so the
// remove walk can see every component.
std::error_code ec;
std::filesystem::remove_all(std::format(L"\\\\?\\{}\\{}", std::filesystem::current_path().wstring(), testDir), ec);
});
// Build the deep chain via the \\?\ long-path prefix because the cumulative path
// length goes well past MAX_PATH.
auto deepPath = std::format(L"\\\\?\\{}\\{}", std::filesystem::current_path().wstring(), testDir);
for (int i = 0; i < depth; ++i)
{
deepPath += std::format(L"\\d{}", i);
}
std::filesystem::create_directories(deepPath);
// Should not crash with a stack overflow regardless of tree depth.
wsl::windows::common::filesystem::EnsureCaseSensitiveDirectory(testDir, flags);
}
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"");
}
}
static LxssDistributionState 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;
}
TEST_METHOD(DistroTimeout)
{
WslConfigChange config(LxssGenerateTestConfig() + L"[general]\ninstanceIdleTimeout=-1");
auto distroId = GetDistributionId(LXSS_DISTRO_NAME_TEST_L);
// 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, wsl::shared::Arm64 ? 1 : 2);
VERIFY_ARE_EQUAL(asset.url, wsl::shared::Arm64 ? L"http://arm-url" : L"http://x64-url");
VERIFY_ARE_EQUAL(asset.name, wsl::shared::Arm64 ? L"wsl.2.4.12.0.arm64.msi" : 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, wsl::shared::Arm64 ? 1 : 2);
VERIFY_ARE_EQUAL(asset.url, wsl::shared::Arm64 ? L"http://arm-url" : L"http://x64-url");
VERIFY_ARE_EQUAL(asset.name, wsl::shared::Arm64 ? L"wsl.2.5.1.0.arm64.msi" : L"wsl.2.5.1.0.x64.msi");
}
}
WSL2_TEST_METHOD(CustomVhdsInUserProfile)
{
// Regression: HCS fails with E_ACCESSDENIED when user-supplied kernelModules or
// systemDistro VHDs live under the user profile and VMWP wasn't granted access.
#ifdef WSL_DEV_INSTALL_PATH
const auto modulesPath = std::format(L"{}\\artifacts.vhd", WSL_DEV_INSTALL_PATH);
const auto kernelPath = std::format(L"{}\\kernel", WSL_DEV_INSTALL_PATH);
const auto systemDistroPath = std::format(L"{}\\system.vhd", WSL_DEV_INSTALL_PATH);
#else
const auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath().value();
const auto modulesPath = std::format(L"{}\\tools\\artifacts.vhd", installPath);
const auto kernelPath = std::format(L"{}\\tools\\kernel", installPath);
const auto systemDistroPath = std::format(L"{}\\system.vhd", installPath);
#endif
// Unique folder under %TEMP% so parallel runs don't collide.
GUID runId;
THROW_IF_FAILED(CoCreateGuid(&runId));
const auto testFolder =
std::filesystem::temp_directory_path() /
std::format(L"wsl-test-vhd-grant-{}", wsl::shared::string::GuidToString<wchar_t>(runId, wsl::shared::string::GuidToStringFlags::None));
const auto testModules = testFolder / L"test-modules.vhd";
const auto testSystemDistro = testFolder / L"test-system.vhd";
// Construct the cleanup scope before any filesystem mutations so a failed copy or
// VERIFY does not leak the directory across runs.
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
std::error_code ignored;
std::filesystem::remove_all(testFolder, ignored);
});
std::filesystem::create_directories(testFolder);
VERIFY_IS_TRUE(CopyFile(modulesPath.c_str(), testModules.c_str(), false));
VERIFY_IS_TRUE(CopyFile(systemDistroPath.c_str(), testSystemDistro.c_str(), false));
for (const auto& path : {testModules, testSystemDistro})
{
auto cmd = std::format(L"icacls.exe \"{}\" /remove Everyone /Q", path.wstring());
LxsstuLaunchCommandAndCaptureOutput(cmd.data());
}
WslConfigChange config{LxssGenerateTestConfig(
{.kernel = kernelPath, .kernelModules = testModules.wstring(), .systemDistro = testSystemDistro.wstring()})};
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"echo OK");
VERIFY_ARE_EQUAL(out, L"OK\n");
VERIFY_ARE_EQUAL(err, L"");
}
WSL2_TEST_METHOD(CustomVhdsAccessibleViaInheritedAcls)
{
// Regression: VHDs reachable to VMWP via inherited ACLs must boot even when the
// impersonated user lacks WRITE_DAC for HcsGrantVmAccess.
#ifdef WSL_DEV_INSTALL_PATH
const auto modulesPath = std::format(L"{}\\artifacts.vhd", WSL_DEV_INSTALL_PATH);
const auto kernelPath = std::format(L"{}\\kernel", WSL_DEV_INSTALL_PATH);
const auto systemDistroPath = std::format(L"{}\\system.vhd", WSL_DEV_INSTALL_PATH);
#else
const auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath().value();
const auto modulesPath = std::format(L"{}\\tools\\artifacts.vhd", installPath);
const auto kernelPath = std::format(L"{}\\tools\\kernel", installPath);
const auto systemDistroPath = std::format(L"{}\\system.vhd", installPath);
#endif
WslConfigChange config{LxssGenerateTestConfig({.kernel = kernelPath, .kernelModules = modulesPath, .systemDistro = systemDistroPath})};
// Non-elevated launch so impersonation cannot WRITE_DAC the SYSTEM-owned VHD.
const auto nonElevatedToken = GetNonElevatedToken();
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"echo OK", 0, nullptr, nonElevatedToken.get());
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,
FormatErrorMessage(
L"The imported file is not a valid Linux distribution.",
L"Wsl/Service/RegisterDistro/WSL_E_NOT_A_LINUX_DISTRO"));
// 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\n" + FormatErrorMessage(
L"The imported file is not a valid Linux distribution.",
L"Wsl/Service/RegisterDistro/WSL_E_NOT_A_LINUX_DISTRO"));
// 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";
constexpr auto c_testVhdSize = 20 * _1MB;
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(testVhd); });
LxsstuLaunchPowershellAndCaptureOutput(std::format(L"New-Vhd {} -SizeBytes {}", testVhd, c_testVhdSize));
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--mount {} --vhd --bare", testVhd)), 0L);
const auto disk = GetBlockDeviceInWsl(c_testVhdSize);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"mkfs.ext4 {}", disk)), 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,
FormatErrorMessage(
L"The imported file is not a valid Linux distribution.",
L"Wsl/Service/RegisterDistro/WSL_E_NOT_A_LINUX_DISTRO"));
// 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, [test_distro]() { 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 [invalidOutput, invalidError] = LxsstuLaunchWslAndCaptureOutput(L"--unregister DummyBrokenDistro extra another", -1);
VERIFY_ARE_EQUAL(
FormatErrorMessage(
L"Invalid command line argument: extra\r\n"
L"Please use 'wsl.exe --help' to get a list of supported arguments.",
L"Wsl/E_INVALIDARG"),
invalidOutput);
VERIFY_ARE_EQUAL(L"", invalidError);
VERIFY_IS_TRUE(GetDistributionId(L"DummyBrokenDistro").has_value());
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,stderr");
VERIFY_ARE_EQUAL(out, L"OK\r\r\n");
VERIFY_ARE_EQUAL(err, L"");
}
WSL2_TEST_METHOD(WslDebug)
{
// 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);
}
WSL2_TEST_METHOD(CGroupv1)
{
// cgroupv1 conflicts with the per-distro cgroup hierarchy
WslConfigChange config(LxssGenerateTestConfig({.isolateDistroCgroup = false}));
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.
config.Update(LxssGenerateTestConfig({.kernelCommandLine = L"cgroup_no_v1=all", .isolateDistroCgroup = false}));
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);
}
WSL2_TEST_METHOD(InitPermissions)
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"stat -c %a /init");
VERIFY_ARE_EQUAL(out, L"755\n");
}
WSL2_TEST_METHOD(ExportImportVhd)
{
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,
FormatErrorMessage(L"The specified file must have the .vhdx file extension.", L"Wsl/Service/WSL_E_EXPORT_FAILED"));
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, FormatErrorMessage(L"The specified file must have the .vhd file extension.", L"Wsl/Service/WSL_E_EXPORT_FAILED"));
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,
FormatErrorMessage(
L"The specified file must have the .vhd or .vhdx file extension.",
L"Wsl/Service/RegisterDistro/WSL_E_IMPORT_FAILED"));
VERIFY_ARE_EQUAL(err, L"");
}
TEST_METHOD(BytesToHex)
{
using wsl::windows::common::string::BytesToHex;
VERIFY_ARE_EQUAL(BytesToHex({}), L"0x");
VERIFY_ARE_EQUAL(BytesToHex({0x0F}), L"0x0f");
VERIFY_ARE_EQUAL(BytesToHex({0xDE, 0xAD, 0xBE, 0xEF}), L"0xdeadbeef");
VERIFY_ARE_EQUAL(BytesToHex({0x00, 0x00}), L"0x0000");
VERIFY_ARE_EQUAL(BytesToHex({0xFF, 0xFF}), L"0xffff");
}
TEST_METHOD(HexToBytes)
{
using wsl::windows::common::string::BytesToHex;
using wsl::windows::common::string::HexToBytes;
using ByteVec = std::vector<BYTE>;
// Wide string with 0x prefix
VERIFY_ARE_EQUAL(HexToBytes(L"0xdeadbeef"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
// Narrow string with 0x prefix
VERIFY_ARE_EQUAL(HexToBytes("0xdeadbeef"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
// Wide string without prefix
VERIFY_ARE_EQUAL(HexToBytes(L"deadbeef"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
// Narrow string without prefix
VERIFY_ARE_EQUAL(HexToBytes("deadbeef"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
// Empty string
VERIFY_ARE_EQUAL(HexToBytes(L""), (ByteVec{}));
// Single byte
VERIFY_ARE_EQUAL(HexToBytes(L"0x0f"), (ByteVec{0x0F}));
// Uppercase hex digits
VERIFY_ARE_EQUAL(HexToBytes(L"0xDEADBEEF"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
// Round-trip: BytesToHex -> HexToBytes
const ByteVec original = {0x01, 0x23, 0xAB};
VERIFY_ARE_EQUAL(HexToBytes(BytesToHex(original)), original);
// Odd-length string (after stripping "0x") throws E_INVALIDARG
bool threw = false;
try
{
HexToBytes(L"0xabc");
}
catch (const wil::ResultException& e)
{
VERIFY_ARE_EQUAL(e.GetErrorCode(), E_INVALIDARG);
threw = true;
}
VERIFY_IS_TRUE(threw);
// Invalid hex character throws E_INVALIDARG
threw = false;
try
{
HexToBytes(L"0xZZ");
}
catch (const wil::ResultException& e)
{
VERIFY_ARE_EQUAL(e.GetErrorCode(), E_INVALIDARG);
threw = true;
}
VERIFY_IS_TRUE(threw);
}
WSL2_TEST_METHOD(InteractiveMount)
{
// Add a fake interactive mount helper.
DistroFileChange mountHelper(L"/sbin/mount.hang", false);
mountHelper.SetContent(
L"#!/bin/sh\n"
L"read pass < /dev/tty\n");
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"chmod +x /sbin/mount.hang"), (DWORD)0);
// Don't keep the original fstab as it can be missing on the pipeline.
DistroFileChange fstab(L"/etc/fstab", false);
fstab.SetContent(L"none /mnt/ttytest hang 0 0\n");
// Restart the distro with this mount.
WslShutdown();
wsl::windows::common::SubProcess process(nullptr, LxssGenerateWslCommandLine(L"echo booted").c_str());
auto result = process.RunAndCaptureOutput(60 * 1000);
VERIFY_ARE_EQUAL(result.Stdout, L"booted\n");
VERIFY_ARE_EQUAL(result.ExitCode, 0);
}
TEST_METHOD(UninstallRejectsArguments)
{
VerifyOutput(
L"--uninstall Distro",
wsl::shared::Localization::MessageUninstallNoArguments(WSL_UNINSTALL_ARG, WSL_UNREGISTER_ARG) + L"\r\n",
-1);
}
TEST_METHOD(ReadSocketMessageHandle)
{
// Drive a ReadSocketMessageHandle until completion and return the bytes delivered to its
// OnMessage callback. If a non-success HRESULT is supplied, the call is expected to throw
// that HRESULT instead, and the OnMessage callback must not be invoked.
auto readMessage = [](wil::unique_socket&& server, HRESULT expectedHr = S_OK, std::vector<gsl::byte> pendingBytes = {}) {
std::vector<gsl::byte> buffer;
bool callbackInvoked = false;
std::vector<gsl::byte> message;
wsl::windows::common::io::MultiHandleWait io;
io.AddHandle(std::make_unique<wsl::windows::common::io::ReadSocketMessageHandle>(
wsl::windows::common::io::HandleWrapper{std::move(server)},
buffer,
pendingBytes,
[&callbackInvoked, &message](const gsl::span<gsl::byte>& received) {
callbackInvoked = true;
message.assign(received.begin(), received.end());
}));
const auto hr = wil::ResultFromException([&]() { io.Run(std::chrono::seconds(60)); });
VERIFY_ARE_EQUAL(hr, expectedHr);
VERIFY_ARE_EQUAL(callbackInvoked, SUCCEEDED(expectedHr));
return message;
};
// Scenario 1: A complete header-only message is delivered intact.
{
auto [client, server] = MakeSocketPair();
MESSAGE_HEADER header{};
header.MessageType = LxMiniInitMessageAny;
header.MessageSize = sizeof(header);
header.TransactionId = 7;
header.TransactionStep = 1;
WriteSocket(client.get(), &header, sizeof(header));
client.reset();
const auto message = readMessage(std::move(server));
VERIFY_ARE_EQUAL(message.size(), sizeof(header));
VERIFY_IS_TRUE(std::memcmp(message.data(), &header, sizeof(header)) == 0);
}
// Scenario 2: A complete message with a payload body is delivered intact.
{
auto [client, server] = MakeSocketPair();
constexpr size_t bodySize = 128;
std::vector<gsl::byte> payload(sizeof(MESSAGE_HEADER) + bodySize);
auto* header = reinterpret_cast<MESSAGE_HEADER*>(payload.data());
header->MessageType = LxMiniInitMessageAny;
header->MessageSize = gsl::narrow_cast<unsigned int>(payload.size());
header->TransactionId = 42;
header->TransactionStep = 2;
for (size_t i = 0; i < bodySize; ++i)
{
payload[sizeof(MESSAGE_HEADER) + i] = static_cast<gsl::byte>(i & 0xFF);
}
WriteSocket(client.get(), payload.data(), payload.size());
client.reset();
const auto message = readMessage(std::move(server));
VERIFY_ARE_EQUAL(message.size(), payload.size());
VERIFY_IS_TRUE(std::memcmp(message.data(), payload.data(), payload.size()) == 0);
}
// Scenario 3: Sender closes without writing any bytes. The reader should observe a clean
// end-of-stream and signal completion with an empty span (no exception).
{
auto [client, server] = MakeSocketPair();
client.reset();
const auto message = readMessage(std::move(server));
VERIFY_ARE_EQUAL(message.size(), static_cast<size_t>(0));
}
// Scenario 4: Sender closes after sending fewer bytes than a full header.
// The reader should treat this as a protocol error and throw E_UNEXPECTED.
{
auto [client, server] = MakeSocketPair();
std::array<gsl::byte, sizeof(MESSAGE_HEADER) - 1> partialHeader{};
std::memset(partialHeader.data(), 0xCC, partialHeader.size());
WriteSocket(client.get(), partialHeader.data(), partialHeader.size());
client.reset();
readMessage(std::move(server), E_UNEXPECTED);
}
// Scenario 5: Sender provides a complete header but closes after sending only part of the body.
// The reader should treat this as a protocol error and throw E_UNEXPECTED.
{
auto [client, server] = MakeSocketPair();
constexpr size_t fullBodySize = 64;
constexpr size_t partialBodySize = 16;
MESSAGE_HEADER header{};
header.MessageType = LxMiniInitMessageAny;
header.MessageSize = gsl::narrow_cast<unsigned int>(sizeof(header) + fullBodySize);
header.TransactionId = 11;
header.TransactionStep = 1;
WriteSocket(client.get(), &header, sizeof(header));
std::array<gsl::byte, partialBodySize> partialBody{};
std::memset(partialBody.data(), 0x55, partialBody.size());
WriteSocket(client.get(), partialBody.data(), partialBody.size());
client.reset();
readMessage(std::move(server), E_UNEXPECTED);
}
// Scenario 6: PendingBytes carries a complete header-only message left over from a
// previous aborted receive. The reader should deliver it without touching the socket.
{
auto [client, server] = MakeSocketPair();
client.reset(); // close the peer; we should still complete from PendingBytes alone.
MESSAGE_HEADER header{};
header.MessageType = LxMiniInitMessageAny;
header.MessageSize = sizeof(header);
header.TransactionId = 77;
header.TransactionStep = 1;
const auto* headerBytes = reinterpret_cast<const gsl::byte*>(&header);
std::vector<gsl::byte> pendingBytes(headerBytes, headerBytes + sizeof(header));
const auto message = readMessage(std::move(server), S_OK, std::move(pendingBytes));
VERIFY_ARE_EQUAL(message.size(), sizeof(header));
VERIFY_IS_TRUE(std::memcmp(message.data(), &header, sizeof(header)) == 0);
}
// Scenario 7: PendingBytes carries a complete message with a body left over from a
// previous aborted receive. The reader should deliver it without touching the socket.
{
auto [client, server] = MakeSocketPair();
client.reset();
constexpr size_t bodySize = 32;
std::vector<gsl::byte> payload(sizeof(MESSAGE_HEADER) + bodySize);
auto* header = reinterpret_cast<MESSAGE_HEADER*>(payload.data());
header->MessageType = LxMiniInitMessageAny;
header->MessageSize = gsl::narrow_cast<unsigned int>(payload.size());
header->TransactionId = 81;
header->TransactionStep = 3;
for (size_t i = 0; i < bodySize; ++i)
{
payload[sizeof(MESSAGE_HEADER) + i] = static_cast<gsl::byte>(i ^ 0xA5);
}
std::vector<gsl::byte> pendingBytes(payload.begin(), payload.end());
const auto message = readMessage(std::move(server), S_OK, std::move(pendingBytes));
VERIFY_ARE_EQUAL(message.size(), payload.size());
VERIFY_IS_TRUE(std::memcmp(message.data(), payload.data(), payload.size()) == 0);
}
// Scenario 8: PendingBytes carries only part of a header. The reader must fill in the
// rest of the header (and the body) from the socket and deliver the assembled message.
{
auto [client, server] = MakeSocketPair();
constexpr size_t bodySize = 48;
constexpr size_t prebufferedBytes = 6; // less than sizeof(MESSAGE_HEADER) = 16
std::vector<gsl::byte> payload(sizeof(MESSAGE_HEADER) + bodySize);
auto* header = reinterpret_cast<MESSAGE_HEADER*>(payload.data());
header->MessageType = LxMiniInitMessageAny;
header->MessageSize = gsl::narrow_cast<unsigned int>(payload.size());
header->TransactionId = 91;
header->TransactionStep = 4;
for (size_t i = 0; i < bodySize; ++i)
{
payload[sizeof(MESSAGE_HEADER) + i] = static_cast<gsl::byte>(0xC3);
}
std::vector<gsl::byte> pendingBytes(payload.begin(), payload.begin() + prebufferedBytes);
WriteSocket(client.get(), payload.data() + prebufferedBytes, payload.size() - prebufferedBytes);
client.reset();
const auto message = readMessage(std::move(server), S_OK, std::move(pendingBytes));
VERIFY_ARE_EQUAL(message.size(), payload.size());
VERIFY_IS_TRUE(std::memcmp(message.data(), payload.data(), payload.size()) == 0);
}
// Scenario 9: PendingBytes carries the full header plus part of the body. The reader
// must read the remaining body bytes from the socket and deliver the assembled message.
{
auto [client, server] = MakeSocketPair();
constexpr size_t bodySize = 64;
constexpr size_t prebufferedBodyBytes = 12;
std::vector<gsl::byte> payload(sizeof(MESSAGE_HEADER) + bodySize);
auto* header = reinterpret_cast<MESSAGE_HEADER*>(payload.data());
header->MessageType = LxMiniInitMessageAny;
header->MessageSize = gsl::narrow_cast<unsigned int>(payload.size());
header->TransactionId = 92;
header->TransactionStep = 5;
for (size_t i = 0; i < bodySize; ++i)
{
payload[sizeof(MESSAGE_HEADER) + i] = static_cast<gsl::byte>(i & 0xFF);
}
const size_t prebufferedBytes = sizeof(MESSAGE_HEADER) + prebufferedBodyBytes;
std::vector<gsl::byte> pendingBytes(payload.begin(), payload.begin() + prebufferedBytes);
WriteSocket(client.get(), payload.data() + prebufferedBytes, payload.size() - prebufferedBytes);
client.reset();
const auto message = readMessage(std::move(server), S_OK, std::move(pendingBytes));
VERIFY_ARE_EQUAL(message.size(), payload.size());
VERIFY_IS_TRUE(std::memcmp(message.data(), payload.data(), payload.size()) == 0);
}
// Scenario 10: PendingBytes contains an invalid (too-small) message size. The
// IO should detect this and throw E_UNEXPECTED without invoking OnMessage.
{
auto [client, server] = MakeSocketPair();
client.reset();
MESSAGE_HEADER header{};
header.MessageType = LxMiniInitMessageAny;
header.MessageSize = sizeof(header) - 1; // invalid: smaller than the header itself
header.TransactionId = 99;
header.TransactionStep = 1;
const auto* headerBytes = reinterpret_cast<const gsl::byte*>(&header);
std::vector<gsl::byte> pendingBytes{headerBytes, headerBytes + sizeof(header)};
std::vector<gsl::byte> buffer;
bool callbackInvoked = false;
const auto hr = wil::ResultFromException([&]() {
wsl::windows::common::io::MultiHandleWait io;
io.AddHandle(std::make_unique<wsl::windows::common::io::ReadSocketMessageHandle>(
wsl::windows::common::io::HandleWrapper{std::move(server)},
buffer,
pendingBytes,
[&callbackInvoked](const gsl::span<gsl::byte>&) { callbackInvoked = true; }));
io.Run(std::chrono::seconds(60));
});
VERIFY_ARE_EQUAL(hr, E_UNEXPECTED);
VERIFY_IS_FALSE(callbackInvoked);
}
}
TEST_METHOD(MultiHandleWaitAboveMaximumWaitObjects)
{
// Validate that MultiHandleWait can wait on more than MAXIMUM_WAIT_OBJECTS (64) handles.
constexpr size_t handleCount = 100;
static_assert(handleCount > MAXIMUM_WAIT_OBJECTS);
// Scenario 1: signal every event before Run(); all callbacks must fire and Run() must return.
{
std::vector<wil::unique_event> events;
events.reserve(handleCount);
for (size_t i = 0; i < handleCount; ++i)
{
events.emplace_back(wil::EventOptions::ManualReset);
}
std::vector<bool> fired(handleCount, false);
std::atomic<size_t> firedCount{0};
std::mutex firedLock;
wsl::windows::common::io::MultiHandleWait io;
for (size_t i = 0; i < handleCount; ++i)
{
io.AddHandle(std::make_unique<wsl::windows::common::io::EventHandle>(
wsl::windows::common::io::HandleWrapper{events[i].get()}, [&fired, &firedCount, &firedLock, i]() {
std::lock_guard lock{firedLock};
VERIFY_IS_FALSE(fired[i]);
fired[i] = true;
firedCount.fetch_add(1);
}));
}
for (auto& e : events)
{
e.SetEvent();
}
VERIFY_IS_TRUE(io.Run(std::chrono::seconds(60)));
VERIFY_ARE_EQUAL(firedCount.load(), handleCount);
for (size_t i = 0; i < handleCount; ++i)
{
VERIFY_IS_TRUE(fired[i]);
}
}
// Scenario 2: signal events one at a time from another thread while Run() processes them.
{
std::vector<wil::unique_event> events;
events.reserve(handleCount);
for (size_t i = 0; i < handleCount; ++i)
{
events.emplace_back(wil::EventOptions::ManualReset);
}
std::vector<bool> fired(handleCount, false);
std::atomic<size_t> firedCount{0};
std::mutex firedLock;
wsl::windows::common::io::MultiHandleWait io;
for (size_t i = 0; i < handleCount; ++i)
{
io.AddHandle(std::make_unique<wsl::windows::common::io::EventHandle>(
wsl::windows::common::io::HandleWrapper{events[i].get()}, [&fired, &firedCount, &firedLock, i]() {
std::lock_guard lock{firedLock};
VERIFY_IS_FALSE(fired[i]);
fired[i] = true;
firedCount.fetch_add(1);
}));
}
std::thread signaller([&events]() {
for (auto& e : events)
{
e.SetEvent();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
});
VERIFY_IS_TRUE(io.Run(std::chrono::seconds(60)));
signaller.join();
VERIFY_ARE_EQUAL(firedCount.load(), handleCount);
for (size_t i = 0; i < handleCount; ++i)
{
VERIFY_IS_TRUE(fired[i]);
}
}
}
TEST_METHOD(SocketChannel)
{
// Read exactly `size` bytes from a raw socket into the destination buffer.
auto recvAll = [](SOCKET socket, void* destination, size_t size) {
auto* cursor = static_cast<char*>(destination);
size_t total = 0;
while (total < size)
{
const auto received = recv(socket, cursor + total, gsl::narrow_cast<int>(size - total), 0);
VERIFY_IS_TRUE(received > 0);
total += static_cast<size_t>(received);
}
};
// Scenario 1: SendMessage produces the expected wire format on the peer socket.
// The header should carry the auto-stamped TransactionId (1 for the first non-transaction
// message) and a NONE transaction step, and the payload bytes should match exactly.
{
auto [client, server] = MakeSocketPair();
wsl::shared::SocketChannel channel{std::move(client), "client"};
RESULT_MESSAGE<int32_t> message{};
message.Header.MessageType = RESULT_MESSAGE<int32_t>::Type;
message.Header.MessageSize = sizeof(message);
message.Result = static_cast<int32_t>(0xCAFEBABE);
channel.SendMessage(message);
std::array<gsl::byte, sizeof(message)> received{};
recvAll(server.get(), received.data(), received.size());
const auto* header = reinterpret_cast<const MESSAGE_HEADER*>(received.data());
VERIFY_ARE_EQUAL(header->MessageType, RESULT_MESSAGE<int32_t>::Type);
VERIFY_ARE_EQUAL(header->MessageSize, gsl::narrow_cast<unsigned int>(sizeof(message)));
VERIFY_ARE_EQUAL(header->TransactionId, 1u);
VERIFY_ARE_EQUAL(header->TransactionStep, static_cast<unsigned int>(TRANSACTION_STEP::NONE));
const auto* payload = reinterpret_cast<const RESULT_MESSAGE<int32_t>*>(received.data());
VERIFY_ARE_EQUAL(payload->Result, static_cast<int32_t>(0xCAFEBABE));
}
// Scenario 2: Two channels can round-trip a typed message end-to-end.
{
auto [a, b] = MakeSocketPair();
wsl::shared::SocketChannel sender{std::move(a), "sender"};
wsl::shared::SocketChannel receiver{std::move(b), "receiver"};
RESULT_MESSAGE<int32_t> message{};
message.Header.MessageType = RESULT_MESSAGE<int32_t>::Type;
message.Header.MessageSize = sizeof(message);
message.Result = 1234;
sender.SendMessage(message);
auto& received = receiver.ReceiveMessage<RESULT_MESSAGE<int32_t>>();
VERIFY_ARE_EQUAL(received.Header.MessageType, RESULT_MESSAGE<int32_t>::Type);
VERIFY_ARE_EQUAL(received.Header.MessageSize, gsl::narrow_cast<unsigned int>(sizeof(message)));
VERIFY_ARE_EQUAL(received.Header.TransactionId, 1u);
VERIFY_ARE_EQUAL(received.Result, 1234);
}
// Scenario 3: An exit event signaled while ReceiveMessage is waiting causes the call to
// throw E_ABORT. Pre-signaling avoids racing a worker thread with the call.
{
auto [client, server] = MakeSocketPair();
wil::unique_event exitEvent{wil::EventOptions::ManualReset};
exitEvent.SetEvent();
wsl::shared::SocketChannel channel{std::move(server), "server", std::vector<HANDLE>{exitEvent.get()}};
const auto hr = wil::ResultFromException([&]() { channel.ReceiveMessage<RESULT_MESSAGE<int32_t>>(); });
VERIFY_ARE_EQUAL(hr, E_ABORT);
}
// Scenario 4: ReceiveMessageOrClosed on a peer that closed the socket without sending
// any data returns {nullptr, empty span} and does not throw.
{
auto [client, server] = MakeSocketPair();
wsl::shared::SocketChannel channel{std::move(server), "server"};
client.reset();
auto [message, span] = channel.ReceiveMessageOrClosed<RESULT_MESSAGE<int32_t>>();
VERIFY_IS_NULL(message);
VERIFY_ARE_EQUAL(span.size(), static_cast<size_t>(0));
}
// Scenario 5: A message arriving with a TransactionId other than the next expected
// sequence number (the first message must have id 1) is rejected with E_UNEXPECTED.
{
auto [client, server] = MakeSocketPair();
wsl::shared::SocketChannel channel{std::move(server), "server"};
RESULT_MESSAGE<int32_t> message{};
message.Header.MessageType = RESULT_MESSAGE<int32_t>::Type;
message.Header.MessageSize = sizeof(message);
message.Header.TransactionId = 99;
message.Header.TransactionStep = static_cast<unsigned int>(TRANSACTION_STEP::NONE);
message.Result = 0;
WriteSocket(client.get(), &message, sizeof(message));
const auto hr = wil::ResultFromException([&]() { channel.ReceiveMessage<RESULT_MESSAGE<int32_t>>(); });
VERIFY_ARE_EQUAL(hr, E_UNEXPECTED);
}
// Scenario 6: A transaction-tagged message arriving on a channel waiting for a
// non-transaction message is rejected with E_UNEXPECTED.
{
auto [client, server] = MakeSocketPair();
wsl::shared::SocketChannel channel{std::move(server), "server"};
RESULT_MESSAGE<int32_t> message{};
message.Header.MessageType = RESULT_MESSAGE<int32_t>::Type;
message.Header.MessageSize = sizeof(message);
message.Header.TransactionId = 1;
message.Header.TransactionStep = static_cast<unsigned int>(TRANSACTION_STEP::REQUEST);
message.Result = 0;
WriteSocket(client.get(), &message, sizeof(message));
const auto hr = wil::ResultFromException([&]() { channel.ReceiveMessage<RESULT_MESSAGE<int32_t>>(); });
VERIFY_ARE_EQUAL(hr, E_UNEXPECTED);
}
// Scenario 7: A header-only message arriving when a larger message type is expected
// is rejected with E_UNEXPECTED because the received span is too small for the type.
{
auto [client, server] = MakeSocketPair();
wsl::shared::SocketChannel channel{std::move(server), "server"};
MESSAGE_HEADER header{};
header.MessageType = RESULT_MESSAGE<int32_t>::Type;
header.MessageSize = sizeof(header);
header.TransactionId = 1;
header.TransactionStep = static_cast<unsigned int>(TRANSACTION_STEP::NONE);
WriteSocket(client.get(), &header, sizeof(header));
const auto hr = wil::ResultFromException([&]() { channel.ReceiveMessage<RESULT_MESSAGE<int32_t>>(); });
VERIFY_ARE_EQUAL(hr, E_UNEXPECTED);
}
// Scenario 8: ReceiveMessage with a finite timeout on an idle socket throws
// HRESULT_FROM_WIN32(ERROR_TIMEOUT) once the timeout elapses.
{
auto [client, server] = MakeSocketPair();
wsl::shared::SocketChannel channel{std::move(server), "server"};
const auto hr = wil::ResultFromException([&]() { channel.ReceiveMessage<RESULT_MESSAGE<int32_t>>(nullptr, 100); });
VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_TIMEOUT));
}
// Scenario 9: Variable-length string fields must be terminated within the received message.
{
auto [a, b] = MakeSocketPair();
wsl::shared::SocketChannel sender{std::move(a), "sender"};
wsl::shared::SocketChannel receiver{std::move(b), "receiver"};
wsl::shared::MessageWriter<LX_INIT_GUEST_CAPABILITIES> message;
std::array<gsl::byte, 4> unterminated{
static_cast<gsl::byte>('1'), static_cast<gsl::byte>('.'), static_cast<gsl::byte>('2'), static_cast<gsl::byte>('3')};
message.WriteSpan(gsl::make_span(unterminated));
sender.SendMessage<LX_INIT_GUEST_CAPABILITIES>(message.Span());
gsl::span<gsl::byte> span;
receiver.ReceiveMessage<LX_INIT_GUEST_CAPABILITIES>(&span);
const auto hr =
wil::ResultFromException([&]() { wsl::shared::string::FromMessageBuffer<LX_INIT_GUEST_CAPABILITIES>(span); });
VERIFY_ARE_EQUAL(hr, E_INVALIDARG);
}
{
auto [a, b] = MakeSocketPair();
wsl::shared::SocketChannel sender{std::move(a), "sender"};
wsl::shared::SocketChannel receiver{std::move(b), "receiver"};
wsl::shared::MessageWriter<WSLC_GET_DISK_RESULT> message;
std::array<gsl::byte, 4> unterminated{
static_cast<gsl::byte>('/'), static_cast<gsl::byte>('d'), static_cast<gsl::byte>('e'), static_cast<gsl::byte>('v')};
message.WriteSpan(gsl::make_span(unterminated));
sender.SendMessage<WSLC_GET_DISK_RESULT>(message.Span());
gsl::span<gsl::byte> span;
receiver.ReceiveMessage<WSLC_GET_DISK_RESULT>(&span);
const auto hr = wil::ResultFromException([&]() { wsl::shared::string::FromMessageBuffer<WSLC_GET_DISK_RESULT>(span); });
VERIFY_ARE_EQUAL(hr, E_INVALIDARG);
}
}
TEST_METHOD(DownloadToHiddenSystemTempFolder)
{
// Avoid contaminating the real temp folder.
const auto testTempFolder = std::filesystem::temp_directory_path() / L"wsl-download-test";
std::filesystem::create_directories(testTempFolder);
auto cleanupTempFolder = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
std::error_code error;
std::filesystem::remove_all(testTempFolder, error);
});
const auto originalAttributes = GetFileAttributesW(testTempFolder.c_str());
VERIFY_IS_TRUE(originalAttributes != INVALID_FILE_ATTRIBUTES);
VERIFY_IS_TRUE(SetFileAttributesW(testTempFolder.c_str(), originalAttributes | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM));
ScopedEnvVariable temp(L"TEMP", testTempFolder.wstring());
ScopedEnvVariable tmp(L"TMP", testTempFolder.wstring());
VERIFY_IS_TRUE(std::filesystem::equivalent(std::filesystem::temp_directory_path(), testTempFolder));
constexpr USHORT port = 6666;
const auto endpoint = std::format(L"http://127.0.0.1:{}/", port);
constexpr auto fileName = L"downloaded-file.bin";
constexpr auto fileContent = L"wsl download test content";
UniqueWebServer server(endpoint.c_str(), fileContent);
const auto url = endpoint + fileName;
const auto noProgress = [](uint64_t, uint64_t) {};
wsl::shared::retry::RetryWithTimeout<void>(
[&]() {
wil::unique_socket probe{socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)};
THROW_LAST_ERROR_IF(!probe);
sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_port = htons(port);
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
THROW_LAST_ERROR_IF(connect(probe.get(), reinterpret_cast<const sockaddr*>(&address), sizeof(address)) == SOCKET_ERROR);
},
std::chrono::milliseconds(500),
std::chrono::seconds(5));
const auto firstPath = wsl::windows::common::wslutil::DownloadFileImpl(url, L"", noProgress);
auto readFile = [](const std::filesystem::path& Path) {
std::ifstream file(Path, std::ios::binary);
VERIFY_IS_TRUE(file.good());
return std::string{std::istreambuf_iterator<char>(file), {}};
};
VERIFY_ARE_EQUAL(std::filesystem::path(firstPath).parent_path(), testTempFolder);
VERIFY_ARE_EQUAL(std::filesystem::path(firstPath).filename().wstring(), std::wstring(fileName));
VERIFY_IS_TRUE(std::filesystem::exists(firstPath));
VERIFY_ARE_EQUAL(readFile(firstPath), wsl::shared::string::WideToMultiByte(fileContent));
const auto secondPath = wsl::windows::common::wslutil::DownloadFileImpl(url, L"", noProgress);
VERIFY_ARE_EQUAL(std::filesystem::path(secondPath).parent_path(), testTempFolder);
VERIFY_ARE_EQUAL(std::filesystem::path(secondPath).filename().wstring(), std::wstring(L"downloaded-file (2).bin"));
VERIFY_IS_TRUE(std::filesystem::exists(firstPath));
VERIFY_IS_TRUE(std::filesystem::exists(secondPath));
VERIFY_ARE_EQUAL(readFile(secondPath), wsl::shared::string::WideToMultiByte(fileContent));
}
void ValidateIsolatedCgroupLayout(bool systemd, bool requestCgroupV1 = false)
{
VERIFY_IS_FALSE(systemd && requestCgroupV1, L"Invalid test parameters: systemd and cgroup v1 cannot both be requested.");
auto getOutput = [](const std::wstring& command) {
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(command);
return out;
};
constexpr auto secondDistroName = L"cgroup-test-distro";
// Ensure no stale state from a previous run.
LxsstuLaunchWsl(std::format(L"--terminate {}", secondDistroName));
LxsstuLaunchWsl(std::format(L"--unregister {}", secondDistroName));
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
LxsstuLaunchWsl(std::format(L"--terminate {}", secondDistroName));
LxsstuLaunchWsl(std::format(L"--unregister {}", secondDistroName));
});
// Import the second distro.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", secondDistroName, g_testDistroPath)), 0L);
std::optional<decltype(EnableSystemd())> systemdCleanup;
std::optional<decltype(EnableSystemd())> systemdCleanup2;
std::optional<DistroFileChange> cgroupConfig;
std::optional<DistroFileChange> cgroupConfig2;
if (systemd)
{
systemdCleanup.emplace(EnableSystemd());
systemdCleanup2.emplace(EnableSystemd("", secondDistroName));
}
if (requestCgroupV1)
{
cgroupConfig.emplace(L"/etc/wsl.conf", false);
cgroupConfig->SetContent(L"[automount]\ncgroups=v1\n");
cgroupConfig2.emplace(L"/etc/wsl.conf", false, secondDistroName);
cgroupConfig2->SetContent(L"[automount]\ncgroups=v1\n");
TerminateDistribution();
TerminateDistribution(secondDistroName);
for (const auto* distro : {LXSS_DISTRO_NAME_TEST_L, secondDistroName})
{
const auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} /bin/true", distro));
VERIFY_ARE_EQUAL(output, std::wstring{});
LogInfo("%ls startup warnings: %ls", distro, warnings.c_str());
VERIFY_IS_TRUE(warnings.find(wsl::shared::Localization::MessageCgroupV1IncompatibleWithDistroIsolation()) != std::wstring::npos);
}
}
WslKeepAlive keepAlive(nullptr, LXSS_DISTRO_NAME_TEST_L);
WslKeepAlive keepAlive2(nullptr, secondDistroName);
if (requestCgroupV1)
{
for (const auto* distro : {LXSS_DISTRO_NAME_TEST_L, secondDistroName})
{
VERIFY_ARE_EQUAL(getOutput(std::format(L"-d {} findmnt -n -o FSTYPE /sys/fs/cgroup", distro)), std::wstring(L"cgroup2\n"));
}
}
const auto cgroup1 = getOutput(std::format(L"-d {} cat -e /proc/self/cgroup", LXSS_DISTRO_NAME_TEST_L));
const auto cgroup2 = getOutput(std::format(L"-d {} cat -e /proc/self/cgroup", secondDistroName));
VERIFY_ARE_EQUAL(cgroup1, std::wstring(L"0::/non-systemd$\n"));
VERIFY_ARE_EQUAL(cgroup2, std::wstring(L"0::/non-systemd$\n"));
const auto cgroupNamespace1 = getOutput(std::format(L"-d {} readlink /proc/self/ns/cgroup", LXSS_DISTRO_NAME_TEST_L));
const auto cgroupNamespace2 = getOutput(std::format(L"-d {} readlink /proc/self/ns/cgroup", secondDistroName));
VERIFY_ARE_NOT_EQUAL(cgroupNamespace1, cgroupNamespace2);
const auto systemDistroArguments = std::format(L"-d {} --system ", LXSS_DISTRO_NAME_TEST_L);
const auto systemDistroCgroup = getOutput(systemDistroArguments + L"cat -e /proc/self/cgroup");
const auto systemDistroCgroupNamespace = getOutput(systemDistroArguments + L"readlink /proc/self/ns/cgroup");
const auto systemDistroCgroupRoot = getOutput(systemDistroArguments + L"findmnt -n -o FSROOT /sys/fs/cgroup");
VERIFY_ARE_EQUAL(systemDistroCgroup, cgroup1);
VERIFY_ARE_EQUAL(systemDistroCgroupNamespace, cgroupNamespace1);
VERIFY_ARE_EQUAL(systemDistroCgroupRoot, std::wstring(L"/\n"));
const auto cgroupRoot = getOutput(L"findmnt -n -o FSROOT /sys/fs/cgroup");
VERIFY_ARE_EQUAL(cgroupRoot, std::wstring(L"/\n"));
const auto rootProcesses = getOutput(L"cat /sys/fs/cgroup/cgroup.procs");
VERIFY_ARE_EQUAL(rootProcesses, std::wstring{});
const auto wslInitPid = systemd ? GetWslInitPid() : L"1";
const auto wslInitCgroup = getOutput(std::format(L"cat /proc/{}/cgroup", wslInitPid));
VERIFY_ARE_EQUAL(wslInitCgroup, std::wstring(L"0::/../..\n"));
const auto wslInitCgroupNamespace = getOutput(std::format(L"readlink /proc/{}/ns/cgroup", wslInitPid));
VERIFY_ARE_NOT_EQUAL(wslInitCgroupNamespace, cgroupNamespace1);
if (systemd)
{
const auto systemdCgroup = getOutput(L"cat /proc/1/cgroup");
VERIFY_ARE_EQUAL(systemdCgroup, std::wstring(L"0::/init.scope\n"));
const auto systemdCgroupNamespace = getOutput(L"readlink /proc/1/ns/cgroup");
VERIFY_ARE_EQUAL(systemdCgroupNamespace, cgroupNamespace1);
}
else
{
VERIFY_ARE_EQUAL(getOutput(L"cat /sys/fs/cgroup/cgroup.subtree_control"), std::wstring{});
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"-u root /bin/sh -c \"echo +cpu +memory > /sys/fs/cgroup/cgroup.subtree_control\""), 0L);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"-u root mkdir /sys/fs/cgroup/wsl-test-workload"), 0L);
auto cleanupWorkload = wil::scope_exit_log(
WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(L"-u root rmdir /sys/fs/cgroup/wsl-test-workload"); });
VERIFY_ARE_EQUAL(
LxsstuLaunchWsl(L"-u root /bin/sh -c \"echo 67108864 > /sys/fs/cgroup/wsl-test-workload/memory.max && "
L"echo '100000 100000' > /sys/fs/cgroup/wsl-test-workload/cpu.max && "
L"echo 0 > /sys/fs/cgroup/wsl-test-workload/cgroup.procs && "
L"grep -qx 67108864 /sys/fs/cgroup/wsl-test-workload/memory.max && "
L"grep -qx '100000 100000' /sys/fs/cgroup/wsl-test-workload/cpu.max && "
L"grep -qx '0::/wsl-test-workload' /proc/self/cgroup\""),
0L);
}
keepAlive.Reset();
TerminateDistribution(LXSS_DISTRO_NAME_TEST_L);
auto [debugInput, debugWrite] = CreateSubprocessPipe(true, false);
auto [debugRead, debugOutput] = CreateSubprocessPipe(false, true);
wil::unique_handle debugJob{CreateJobObjectW(nullptr, nullptr)};
VERIFY_IS_NOT_NULL(debugJob.get());
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobLimits{};
jobLimits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
VERIFY_WIN32_BOOL_SUCCEEDED(SetInformationJobObject(debugJob.get(), JobObjectExtendedLimitInformation, &jobLimits, sizeof(jobLimits)));
wsl::windows::common::SubProcess debugShell(nullptr, LxssGenerateWslCommandLine(L"--debug-shell").c_str());
debugShell.SetStdHandles(debugInput.get(), debugOutput.get(), debugOutput.get());
debugShell.SetJobObject(debugJob.get());
const auto debugProcess = debugShell.Start();
debugInput.reset();
debugOutput.reset();
// The debug shell echoes the script. Build "PASS" from "PA" and "SS" at runtime so the echoed command
// cannot satisfy the success check below.
constexpr auto c_checkRemainingCgroupsCommand = R"(
{
if timeout 30 sh -c '
while true; do
set -- /sys/fs/cgroup/wsl-user/distro-*
if [ "$#" -eq 1 ] && [ -d "$1" ]; then
exit 0
fi
sleep 1
done
'; then
printf '\n%s%s\n' PA SS
else
printf '\n%s\n' FAIL
fi
busybox poweroff -f
}
)";
DWORD bytesWritten{};
VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(
debugWrite.get(), c_checkRemainingCgroupsCommand, static_cast<DWORD>(strlen(c_checkRemainingCgroupsCommand)), &bytesWritten, nullptr));
VERIFY_ARE_EQUAL(bytesWritten, strlen(c_checkRemainingCgroupsCommand));
debugWrite.reset();
VERIFY_ARE_EQUAL(WaitForSingleObject(debugProcess.get(), 60 * 1000), WAIT_OBJECT_0);
// Ensure a clean WSL state after shutting down the VM from the debug shell.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--shutdown --force"), 0L);
keepAlive2.Reset();
const auto cgroupCheckOutput = ReadToString(debugRead.get());
VERIFY_IS_TRUE(cgroupCheckOutput.find("PASS") != std::string::npos);
TerminateDistribution(secondDistroName);
}
WSL2_TEST_METHOD(IsolatedCgroupLayout)
{
ValidateIsolatedCgroupLayout(false);
}
WSL2_TEST_METHOD(IsolatedCgroupLayoutSystemd)
{
ValidateIsolatedCgroupLayout(true);
}
WSL2_TEST_METHOD(IsolatedCgroupLayoutOverridesV1)
{
ValidateIsolatedCgroupLayout(false, true);
}
WSL2_TEST_METHOD(IsolatedCgroupLayoutDisabled)
{
WslConfigChange config(LxssGenerateTestConfig({.isolateDistroCgroup = false}));
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/self/cgroup");
while (!out.empty() && (out.back() == L'\n' || out.back() == L'\r'))
{
out.pop_back();
}
LogInfo("cgroup with isolateDistroCgroup=false: %ls", out.c_str());
VERIFY_ARE_EQUAL(out, std::wstring(L"0::/"));
auto [exists, __] =
LxsstuLaunchWslAndCaptureOutput(L"/bin/sh -c \"[ -d /sys/fs/cgroup/wsl-user ] && echo yes || echo no\"");
while (!exists.empty() && (exists.back() == L'\n' || exists.back() == L'\r'))
{
exists.pop_back();
}
VERIFY_ARE_EQUAL(exists, std::wstring(L"no"));
}
TEST_METHOD(ConsoleState_SetOutputCodePageUtf8)
{
// 437 (OEM-US) and 850 (OEM Multilingual) are built-in Windows code pages that are always
// available. 437 is the baseline the helper must restore; 850 stands in for another
// component changing the code page after the helper first ran.
constexpr UINT baselineCodePage = 437;
constexpr UINT intermediateCodePage = 850;
const UINT originalCodePage = GetConsoleOutputCP();
auto restore = wil::scope_exit([originalCodePage]() { SetConsoleOutputCP(originalCodePage); });
// A settable console output code page requires an attached console, which CI and service
// contexts often lack. Skip the test when the code page cannot be set so the suite stays stable.
if (!SetConsoleOutputCP(baselineCodePage))
{
LogSkipped("Skipping test: no attached console with a settable output code page");
return;
}
VERIFY_ARE_EQUAL(baselineCodePage, GetConsoleOutputCP());
{
wsl::windows::common::ConsoleState console;
console.SetOutputCodePageUtf8();
VERIFY_ARE_EQUAL(
static_cast<UINT>(CP_UTF8),
GetConsoleOutputCP(),
L"SetOutputCodePageUtf8 sets the console output code page to UTF-8");
// Another component changes the code page; a repeated call must re-assert UTF-8.
VERIFY_IS_TRUE(static_cast<bool>(SetConsoleOutputCP(intermediateCodePage)));
console.SetOutputCodePageUtf8();
VERIFY_ARE_EQUAL(
static_cast<UINT>(CP_UTF8), GetConsoleOutputCP(), L"A repeated call re-asserts UTF-8 after the code page changed");
}
VERIFY_ARE_EQUAL(baselineCodePage, GetConsoleOutputCP(), L"Destruction restores the code page saved on the first call");
}
TEST_METHOD(PrettyPrintOutOfBoundsFields)
{
{
alignas(LX_INIT_NETWORK_INFORMATION) std::array<char, offsetof(LX_INIT_NETWORK_INFORMATION, Buffer) + 1> storage{};
auto* message = reinterpret_cast<LX_INIT_NETWORK_INFORMATION*>(storage.data());
message->Header.MessageSize = offsetof(LX_INIT_NETWORK_INFORMATION, Buffer);
message->FileHeaderIndex = message->Header.MessageSize + 1;
const auto output = message->PrettyPrint();
const auto expected = std::format(
"Header = {{MessageType = LxMiniInitMessageAny\n"
"MessageSize = {}\n"
"TransactionId = 0\n"
"TransactionStep = 0\n"
"}}\n"
"FileHeaderIndex = <out-of-bounds>\n"
"FileContentsIndex = <empty>\n",
message->Header.MessageSize);
VERIFY_ARE_EQUAL(expected, output);
}
{
alignas(LX_INIT_QUERY_ENVIRONMENT_VARIABLE) std::array<char, offsetof(LX_INIT_QUERY_ENVIRONMENT_VARIABLE, Buffer) + 1> storage{};
auto* message = reinterpret_cast<LX_INIT_QUERY_ENVIRONMENT_VARIABLE*>(storage.data());
message->Header.MessageSize = offsetof(LX_INIT_QUERY_ENVIRONMENT_VARIABLE, Buffer);
const auto output = message->PrettyPrint();
const auto expected = std::format(
"Header = {{MessageType = LxMiniInitMessageAny\n"
"MessageSize = {}\n"
"TransactionId = 0\n"
"TransactionStep = 0\n"
"}}\n"
"Buffer = <out-of-bounds>\n",
message->Header.MessageSize);
VERIFY_ARE_EQUAL(expected, output);
}
{
wsl::shared::MessageWriter<WSLC_LISTDIR_RESULT> message;
const auto verifyEntries = [&](std::string_view entries) {
const auto expected = std::format(
"Header = {{MessageType = LxMessageWSLCListDirResult\n"
"MessageSize = {}\n"
"TransactionId = 0\n"
"TransactionStep = 0\n"
"}}\n"
"Result = 0\n"
"EntriesIndex = {}\n",
message->Header.MessageSize,
entries);
VERIFY_ARE_EQUAL(expected, message->PrettyPrint());
};
message->EntriesIndex = message->Header.MessageSize + 0x1000;
verifyEntries("<out-of-bounds>");
message->EntriesIndex = 0;
verifyEntries("<empty>");
}
}
WSL2_TEST_METHOD(SystemdBootTimeout)
{
auto cleanupVm = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
auto cleanupSystemd = EnableSystemd("initTimeout=1000");
DistroFileChange bootService(L"/etc/systemd/system/wsl-test-boot-timeout.service", false);
bootService.SetContent(
L"[Unit]\n"
L"DefaultDependencies=no\n"
L"Before=multi-user.target\n"
L"[Service]\n"
L"Type=oneshot\n"
L"ExecStart=/bin/sleep infinity\n"
L"TimeoutStartSec=infinity\n"
L"[Install]\n"
L"WantedBy=multi-user.target\n");
DistroFileChange enabledService(L"/etc/systemd/system/multi-user.target.wants/wsl-test-boot-timeout.service", false);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"-u root systemctl enable wsl-test-boot-timeout.service"), 0L);
WslShutdown();
wsl::windows::common::SubProcess process(nullptr, LxssGenerateWslCommandLine(L"-u root echo booted").c_str());
const auto result = process.RunAndCaptureOutput(30 * 1000);
VERIFY_ARE_EQUAL(result.ExitCode, 0L);
VERIFY_ARE_EQUAL(result.Stdout, L"booted\n");
const auto dmesg = LxsstuLaunchWslAndCaptureOutput(L"-u root dmesg").first;
VERIFY_ARE_NOT_EQUAL(dmesg.find(L"failed to start within 1000ms"), std::wstring::npos);
const auto cgroup = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/self/cgroup").first;
VERIFY_ARE_EQUAL(cgroup, L"0::/non-systemd\n");
}
TEST_METHOD(WriteInstallLog)
{
const auto directory = std::filesystem::current_path() / L"install-log-test";
VERIFY_IS_TRUE(std::filesystem::create_directory(directory));
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove_all(directory); });
const auto verifyRejected = [](const std::filesystem::path& path) {
VERIFY_THROWS_SPECIFIC(
wsl::windows::common::install::WriteInstallLogImpl(path.wstring(), "must not be written"),
wil::ResultException,
[](const wil::ResultException& e) { return e.GetErrorCode() == E_ACCESSDENIED; });
};
verifyRejected(directory);
VERIFY_IS_TRUE(std::filesystem::is_empty(directory));
const auto target = directory / L"target.txt";
const auto link = directory / L"log.txt";
wsl::windows::common::install::WriteInstallLogImpl(target.wstring(), "original content");
const auto original = ReadFileContent(target.wstring());
VERIFY_IS_TRUE(original.ends_with(L": original content\n"));
VERIFY_IS_TRUE(CreateSymbolicLinkW(link.c_str(), target.c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE));
verifyRejected(link);
VERIFY_ARE_EQUAL(original, ReadFileContent(target.wstring()));
VERIFY_IS_TRUE(std::filesystem::remove(link));
VERIFY_WIN32_BOOL_SUCCEEDED(CreateHardLinkW(link.c_str(), target.c_str(), nullptr));
verifyRejected(link);
VERIFY_ARE_EQUAL(original, ReadFileContent(target.wstring()));
}
};
} // namespace UnitTests