/*++ 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 #include #include #include #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 #include #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/ 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( [&]() { 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( [&]() { 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( [&]() { 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(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 DefaultEnvironment{}; ULONG WslFlags; VERIFY_SUCCEEDED(wslSupport->GetDistributionConfiguration( LXSS_DISTRO_NAME_TEST_L, &Version, &DefaultUid, DefaultEnvironment.size_address(), &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& inputVariables, const std::map& 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& ExtraConfig = {}, LPCWSTR EntryPoint = WSL_BINARY_NAME, bool ignoreCasing = false) { std::optional 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, ", 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(); 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 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 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 Execute the specified command without using the default Linux shell. --shell-type Execute the specified command with the provided shell type. -- Pass the remaining command line as-is. Options: --cd 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 Run the specified distribution. --distribution-id Run the specified distribution ID. --user, -u 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 Install a distribution from a local file. --legacy Use the legacy distribution manifest. --location Set the install path for the distribution. --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 Specifies the version to use for the new distribution. --vhd-size Specifies the size of the disk to store the distribution. --web-download Download the distribution from the internet instead of the Microsoft Store. --manage Changes distro specific options. Options: --move Move the distribution to a new location. --set-sparse, -s Set the VHD of distro to be sparse, allowing disk space to be automatically reclaimed. --set-default-user Set the default user of the distribution. --resize Resize the disk of the distribution to the specified size. --compact Compact the VHDX file of a WSL 2 distribution. --mount Attaches and mounts a physical or virtual disk in all WSL 2 distributions. Options: --vhd Specifies that refers to a virtual hard disk. --bare Attach the disk to WSL2, but don't mount it. --name Mount the disk using a custom name for the mountpoint. --type Filesystem to use when mounting a disk, if not specified defaults to ext4. --options Additional mount options. --partition Index of the partition to mount, if not specified defaults to the whole disk. --set-default-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 [Options] Exports the distribution to a tar file. The filename can be - for stdout. Options: --format Specifies the export format. Supported values: tar, tar.gz, tar.xz, vhd. --import [Options] Imports the specified tar file as a new distribution. The filename can be - for stdin. Options: --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 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 Sets the distribution as the default. --set-version Changes the version of the specified distribution. --terminate, -t Terminates the specified distribution. --unregister 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 Sets the distribution as the default. /t, /terminate Terminates the distribution. /u, /unregister 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 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() + L"\\TestSwap.vhdx"; const auto tokenUser = wil::get_token_information(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(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(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( [&]() { 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 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 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 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()); 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(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(); 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(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(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(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(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 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(distroId.value()); auto distributionProfileId = wsl::shared::string::GuidToString(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( 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(CreateV5Uuid(WslTerminalNamespace, std::as_bytes(std::span{distroIdString}))); VERIFY_ARE_EQUAL(launchProfile["guid"].get(), wsl::shared::string::WideToMultiByte(expectedId)); VERIFY_ARE_EQUAL(launchProfile["name"].get(), wsl::shared::string::WideToMultiByte(DistroName)); VERIFY_ARE_EQUAL(launchProfile["pathTranslationStyle"].get(), "wsl"); std::wstring systemDir; wil::GetSystemDirectoryW(systemDir); VERIFY_ARE_EQUAL( std::format("{}\\{} {} {}", systemDir, WSL_BINARY_NAME, WSL_DISTRIBUTION_ID_ARG, distroIdString), launchProfile["commandline"].get()); // Verify that startingDirectory is set to home directory VERIFY_ARE_EQUAL(launchProfile["startingDirectory"].get(), "~"); auto iconLocation = wsl::shared::string::MultiByteToWide(launchProfile["icon"].get()); 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(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 matches the running kernel. VERIFY_ARE_EQUAL( LxsstuLaunchWsl( LR"BASH(bash -ec ' d=$(mktemp -d) trap "rm -rf $d" EXIT cat > "$d/t.c" < #include #include #include 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( [&]() { 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(); 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(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(); 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(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(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(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( [&]() { 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(sizeBeforeWrite), static_cast(sizeAfterWrite), static_cast(sizeBeforeCompact), static_cast(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 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 getWslConfigFilePath(libWslDllPath.c_str(), "GetWslConfigFilePath"); LxssDynamicFunction createWslConfig(libWslDllPath.c_str(), "CreateWslConfig"); LxssDynamicFunction freeWslConfig(libWslDllPath.c_str(), "FreeWslConfig"); LxssDynamicFunction getWslConfigSetting(libWslDllPath.c_str(), "GetWslConfigSetting"); LxssDynamicFunction 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> 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>>> 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> timeoutValuesToTest{ {-132445, -132445}, {0, 0}, {1, 1}, {13456, 13456}, {100000000, 100000000}, }; // tuple: WslConfigSetting, expectedValue, actualValue std::vector>>> wslConfigSettingInt32TestPlan{ { {.ConfigEntry = WslConfigEntry::ProcessorCount}, { {-123443, systemInfo.dwNumberOfProcessors}, {-1, systemInfo.dwNumberOfProcessors}, {1, 1}, {2, std::min(2, static_cast(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> fileSizesBytesToTest{ {0, 0}, {1, 1}, {13456, 13456}, {100000000, 100000000}, {9223372036854775807, 9223372036854775807}}; // tuple: WslConfigSetting, expectedValue, actualValue std::vector>>> 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> booleansToTest{{false, false}, {true, true}}; // tuple: WslConfigSetting, expectedValue, actualValue std::vector>>> 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> 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>>> 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> memoryReclaimModesToTest{ {MemoryReclaimConfiguration::Disabled, MemoryReclaimConfiguration::Disabled}, {MemoryReclaimConfiguration::Gradual, MemoryReclaimConfiguration::Gradual}, {MemoryReclaimConfiguration::DropCache, MemoryReclaimConfiguration::DropCache}, }; // tuple: WslConfigSetting, expectedValue, actualValue std::vector>>> 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(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(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(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(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(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(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(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(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(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(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 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(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(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(testDistroId.value(), wsl::shared::string::GuidToStringFlags::None)), L"OK"); validateOutput( std::format( L"--distribution-id {} echo -n OK", wsl::shared::string::GuidToString(testDistroId.value(), wsl::shared::string::GuidToStringFlags::AddBraces)), L"OK"); validateOutput( std::format( L"--distribution-id {} echo -n OK", wsl::shared::string::GuidToString(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(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(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 runOOBE(lxssKey.get(), testDistroIdString.c_str(), L"RunOOBE", 1); const RegistryKeyChange 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 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::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(), "custom-value"); VERIFY_ARE_EQUAL(json["schemes"][0]["name"].get(), "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( 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(), 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(Content.size()), nullptr, nullptr)); RegistryKeyChange 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& 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& ExpectedOutput, char separator = '\0') { const auto result = wsl::shared::string::ParseMacAddressNoThrow(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& expected) { auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--list --online"); auto lines = wsl::shared::string::Split(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 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 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(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(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(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; // 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 pendingBytes = {}) { std::vector buffer; bool callbackInvoked = false; std::vector message; wsl::windows::common::io::MultiHandleWait io; io.AddHandle(std::make_unique( wsl::windows::common::io::HandleWrapper{std::move(server)}, buffer, pendingBytes, [&callbackInvoked, &message](const gsl::span& 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 payload(sizeof(MESSAGE_HEADER) + bodySize); auto* header = reinterpret_cast(payload.data()); header->MessageType = LxMiniInitMessageAny; header->MessageSize = gsl::narrow_cast(payload.size()); header->TransactionId = 42; header->TransactionStep = 2; for (size_t i = 0; i < bodySize; ++i) { payload[sizeof(MESSAGE_HEADER) + i] = static_cast(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(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 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(sizeof(header) + fullBodySize); header.TransactionId = 11; header.TransactionStep = 1; WriteSocket(client.get(), &header, sizeof(header)); std::array 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(&header); std::vector 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 payload(sizeof(MESSAGE_HEADER) + bodySize); auto* header = reinterpret_cast(payload.data()); header->MessageType = LxMiniInitMessageAny; header->MessageSize = gsl::narrow_cast(payload.size()); header->TransactionId = 81; header->TransactionStep = 3; for (size_t i = 0; i < bodySize; ++i) { payload[sizeof(MESSAGE_HEADER) + i] = static_cast(i ^ 0xA5); } std::vector 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 payload(sizeof(MESSAGE_HEADER) + bodySize); auto* header = reinterpret_cast(payload.data()); header->MessageType = LxMiniInitMessageAny; header->MessageSize = gsl::narrow_cast(payload.size()); header->TransactionId = 91; header->TransactionStep = 4; for (size_t i = 0; i < bodySize; ++i) { payload[sizeof(MESSAGE_HEADER) + i] = static_cast(0xC3); } std::vector 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 payload(sizeof(MESSAGE_HEADER) + bodySize); auto* header = reinterpret_cast(payload.data()); header->MessageType = LxMiniInitMessageAny; header->MessageSize = gsl::narrow_cast(payload.size()); header->TransactionId = 92; header->TransactionStep = 5; for (size_t i = 0; i < bodySize; ++i) { payload[sizeof(MESSAGE_HEADER) + i] = static_cast(i & 0xFF); } const size_t prebufferedBytes = sizeof(MESSAGE_HEADER) + prebufferedBodyBytes; std::vector 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(&header); std::vector pendingBytes{headerBytes, headerBytes + sizeof(header)}; std::vector buffer; bool callbackInvoked = false; const auto hr = wil::ResultFromException([&]() { wsl::windows::common::io::MultiHandleWait io; io.AddHandle(std::make_unique( wsl::windows::common::io::HandleWrapper{std::move(server)}, buffer, pendingBytes, [&callbackInvoked](const gsl::span&) { 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 events; events.reserve(handleCount); for (size_t i = 0; i < handleCount; ++i) { events.emplace_back(wil::EventOptions::ManualReset); } std::vector fired(handleCount, false); std::atomic 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::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 events; events.reserve(handleCount); for (size_t i = 0; i < handleCount; ++i) { events.emplace_back(wil::EventOptions::ManualReset); } std::vector fired(handleCount, false); std::atomic 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::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(destination); size_t total = 0; while (total < size) { const auto received = recv(socket, cursor + total, gsl::narrow_cast(size - total), 0); VERIFY_IS_TRUE(received > 0); total += static_cast(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 message{}; message.Header.MessageType = RESULT_MESSAGE::Type; message.Header.MessageSize = sizeof(message); message.Result = static_cast(0xCAFEBABE); channel.SendMessage(message); std::array received{}; recvAll(server.get(), received.data(), received.size()); const auto* header = reinterpret_cast(received.data()); VERIFY_ARE_EQUAL(header->MessageType, RESULT_MESSAGE::Type); VERIFY_ARE_EQUAL(header->MessageSize, gsl::narrow_cast(sizeof(message))); VERIFY_ARE_EQUAL(header->TransactionId, 1u); VERIFY_ARE_EQUAL(header->TransactionStep, static_cast(TRANSACTION_STEP::NONE)); const auto* payload = reinterpret_cast*>(received.data()); VERIFY_ARE_EQUAL(payload->Result, static_cast(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 message{}; message.Header.MessageType = RESULT_MESSAGE::Type; message.Header.MessageSize = sizeof(message); message.Result = 1234; sender.SendMessage(message); auto& received = receiver.ReceiveMessage>(); VERIFY_ARE_EQUAL(received.Header.MessageType, RESULT_MESSAGE::Type); VERIFY_ARE_EQUAL(received.Header.MessageSize, gsl::narrow_cast(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{exitEvent.get()}}; const auto hr = wil::ResultFromException([&]() { channel.ReceiveMessage>(); }); 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>(); VERIFY_IS_NULL(message); VERIFY_ARE_EQUAL(span.size(), static_cast(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 message{}; message.Header.MessageType = RESULT_MESSAGE::Type; message.Header.MessageSize = sizeof(message); message.Header.TransactionId = 99; message.Header.TransactionStep = static_cast(TRANSACTION_STEP::NONE); message.Result = 0; WriteSocket(client.get(), &message, sizeof(message)); const auto hr = wil::ResultFromException([&]() { channel.ReceiveMessage>(); }); 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 message{}; message.Header.MessageType = RESULT_MESSAGE::Type; message.Header.MessageSize = sizeof(message); message.Header.TransactionId = 1; message.Header.TransactionStep = static_cast(TRANSACTION_STEP::REQUEST); message.Result = 0; WriteSocket(client.get(), &message, sizeof(message)); const auto hr = wil::ResultFromException([&]() { channel.ReceiveMessage>(); }); 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::Type; header.MessageSize = sizeof(header); header.TransactionId = 1; header.TransactionStep = static_cast(TRANSACTION_STEP::NONE); WriteSocket(client.get(), &header, sizeof(header)); const auto hr = wil::ResultFromException([&]() { channel.ReceiveMessage>(); }); 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>(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 message; std::array unterminated{ static_cast('1'), static_cast('.'), static_cast('2'), static_cast('3')}; message.WriteSpan(gsl::make_span(unterminated)); sender.SendMessage(message.Span()); gsl::span span; receiver.ReceiveMessage(&span); const auto hr = wil::ResultFromException([&]() { wsl::shared::string::FromMessageBuffer(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 message; std::array unterminated{ static_cast('/'), static_cast('d'), static_cast('e'), static_cast('v')}; message.WriteSpan(gsl::make_span(unterminated)); sender.SendMessage(message.Span()); gsl::span span; receiver.ReceiveMessage(&span); const auto hr = wil::ResultFromException([&]() { wsl::shared::string::FromMessageBuffer(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( [&]() { 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(&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(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 systemdCleanup; std::optional systemdCleanup2; std::optional cgroupConfig; std::optional 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(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(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(SetConsoleOutputCP(intermediateCodePage))); console.SetOutputCodePageUtf8(); VERIFY_ARE_EQUAL( static_cast(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 storage{}; auto* message = reinterpret_cast(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 = \n" "FileContentsIndex = \n", message->Header.MessageSize); VERIFY_ARE_EQUAL(expected, output); } { alignas(LX_INIT_QUERY_ENVIRONMENT_VARIABLE) std::array storage{}; auto* message = reinterpret_cast(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 = \n", message->Header.MessageSize); VERIFY_ARE_EQUAL(expected, output); } { wsl::shared::MessageWriter 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(""); message->EntriesIndex = 0; verifyEntries(""); } } 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