Files
WSL/test/windows/Common.cpp
Ben Hillis a8205a85ba merge master -> feature/wsl-for-apps (#14537)
* test: enable virtiofs tests and enable WSLG during testing (#14387)

* test: enable virtiofs tests and enable WSLG during testing

* test fix

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* chore(distributions): Almalinux auto-update - 20260311 14:52:02 (#14404)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Fix CVE-2026-26127: bump .NET runtime from 10.0.0 to 10.0.4 (#14421)

Addresses Dependabot alerts #10 and #11. The Microsoft.NETCore.App.Runtime
packages (win-x64 and win-arm64) at version 10.0.0 are vulnerable to a
denial of service via out-of-bounds read when decoding malformed Base64Url
input (CVSS 7.5 High). Bumped to 10.0.4 which includes the fix.

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Notice change from build: 141806547 (#14423)

Co-authored-by: WSL notice <noreply@microsoft.com>

* Ship initrd.img in MSI using build-time generation via powershell script (#14424)

* Ship initrd.img in MSI using build-time generation via tar.exe

Replace the install-time CreateInitrd/RemoveInitrd custom actions with a
build-time step that generates initrd.img using the Windows built-in
tar.exe (libarchive/bsdtar) and ships it directly in the MSI.

The install-time approach had a race condition: wsl.exe could launch
before the CreateInitrd custom action completed, causing
ERROR_FILE_NOT_FOUND for initrd.img.

Changes:
- Add CMake custom command to generate initrd.img via tar.exe --format=newc
- Add initrd.img as a regular file in the MSI tools component
- Remove CreateInitrd/RemoveInitrd custom actions from WiX, DllMain,
  and wslinstall.def
- Remove CreateCpioInitrd helper and its tests (no longer needed)
- Update pipeline build targets to build initramfs instead of init

* pr feedback

* more pr feedback

* switch to using a powershell script instead of tar.exe

* powershell script feedback

* hopefully final pr feedback

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* virtiofs: update logic so querying virtiofs mount source does not require a call to the service (#14380)

* virtiofs: update logic so querying virtiofs mount source does not require a call to the service

* more pr feedback

* use std::filesystem::read_symlink

* pr feedback and use canonical path in virtiofs symlink

* make sure canonical path is always used

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* virtio networking: add support for ipv6 (#14350)

* VirtioProxy: Add IPv6 address, gateway, and route support

- Add PreferredIpv6Address field and GetBestGatewayV6* methods to NetworkSettings
- Extend GetHostEndpointSettings() to discover IPv6 unicast address and gateway
- Add UpdateIpv6Address() using ModifyGuestEndpointSettingRequest<IPAddress>
- Push IPv6 default route to guest via UpdateDefaultRoute(AF_INET6)
- Remove AF_INET6 early return in ModifyOpenPorts, use INETADDR_PORT()
- Add EndpointRoute::DefaultRoute() static factory
- Pass client_ip_ipv6 in devicehost options (not yet parsed by devicehost)
- Remove gateway_ip from devicehost options (only needed for DHCP)
- Include IPv6 DNS servers in non-tunneling DNS settings
- Add ConfigurationV6 and DnsResolutionAAAA tests

* cleanup and add more ipv6 tests

* added test coverage and minor updates

* clang format

* pr feedback

* format source

* pr feedback

* test fixes

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Track `bind` syscall when port is 0 (#14333)

* Initial work

* .

* pr feedback and add unit test

* minor tweaks an fix use after free in logging statement

* implement PR feedback

* hopefully final pr feedback

* pr feedback in test function

* Address PR feedback: add try/catch to TrackPort and PortZeroBind queue push

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Add iptables to list of apps to install in WSL (#14459)

There were instructions already on how to install tcpdump in WSL, but
iptables are also needed for the log collection to be complete, so this
PR adds instructions on how to also install iptables.

Co-authored-by: Andre Muezerie <andremue@linux.microsoft.com>

* Update Microsoft.WSL.DeviceHost to version 1.1.39-0 (#14460)

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Moves all Ubuntu distros to the tar-based format (#14463)

* Move all supported Ubuntu images to the new format

We backported the build pipeline so all current LTSes come out in the new tar-based format

* Remove the appx based distros

All WSL users can run tar-based distros by now, right?
There is no benefit in maintaining both formats.

* Enable DNS tunneling for VirtioProxy networking mode (#14461)

- Allow VirtioProxy to keep EnableDnsTunneling=true in config, but clear
  socket-specific options (BestEffortDnsParsing, DnsTunnelingIpAddress)
- Suppress dedicated DNS tunneling hvsocket for VirtioProxy; tunneling
  is handled through the VirtioNetworking device host instead
- Set DnsTunneling flag on VirtioNetworkingFlags so the device host
  knows to tunnel DNS
- Expand SWIOTLB kernel cmdline to cover VirtioFs and VirtioProxy
- Bump DeviceHost package to 1.1.39-0
- Add VirtioProxy DNS test coverage for tunneling on/off
- Skip GuestPortIsReleasedV6 on Windows 10

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* test: disable LoopbackExplicit due to OS build 29555 regression (#14477)

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Refactor: trim unnecessary DLL deps from COMMON_LINK_LIBRARIES (#14426)

* Refactor: trim unnecessary DLL deps from COMMON_LINK_LIBRARIES

- Split MSI/Wintrust install functions from wslutil.cpp into install.cpp
- Remove MI.lib, wsldeps.lib, msi.lib, Wintrust.lib, computecore.lib,
  computenetwork.lib, Iphlpapi.lib from COMMON_LINK_LIBRARIES
- Add per-target MSI_LINK_LIBRARIES, HCS_LINK_LIBRARIES, SERVICE_LINK_LIBRARIES
- Delay-load msi.dll and WINTRUST.dll for wsl.exe and wslg.exe
- Result: wslhost, wslrelay, wslcsdk, testplugin lose msi/wintrust startup imports;
  wsl.exe and wslg.exe defer msi/wintrust loading until actually needed;
  wslservice is the only target that imports computecore/computenetwork/Iphlpapi

* minor fixes to install.cpp that were caught during PR

* move to wsl::windows::common::install namespace

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Fix wsl stuck when misconfigured cifs mount presents (#14466)

* detach terminal before running mount -a

* Potential fix for pull request finding

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

* use _exit on error before execv in child process to avoid unintentional resource release

* Add regression test

* Fix clang format issue

* fix all clang format issue

* Potential fix for pull request finding

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

* resolve ai comments

* move test to unit test

* Fix string literal

* Overwrite fstab to resolve pipeline missing file issue

---------

Co-authored-by: Feng Wang <wangfen@microsoft.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Update localization and notice scripts to target the branch that the pipeline is running on (#14492)

* test: Add arm64 test distro support (#14500)

* test: Add arm64 test distro support

* update unit test baseline

* more test baseline updates

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* test: remove duplicated DNS test coverage (#14522)

* test: remove duplicated DNS test coverage

* format source

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Fix: Fail and warn the user when --uninstall is given parameters (#14524)

Fail and warn the user when --uninstall is given parameters.

* Localization change from build: 142847827 (#14525)

Co-authored-by: WSL localization <noreply@microsoft.com>

* virito net: revert to previous DNS behavior while we debug an issue with DNS over TCP (#14532)

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* devicehost: update to latest devicehost nuget with tracing improvements (#14531)

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* fix merge issues

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>
Co-authored-by: AlmaLinux Autobot <107999298+almalinuxautobot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Blue <OneBlue@users.noreply.github.com>
Co-authored-by: WSL notice <noreply@microsoft.com>
Co-authored-by: Daman Mulye <daman_mulye@hotmail.com>
Co-authored-by: Andre Muezerie <108841174+andremueiot@users.noreply.github.com>
Co-authored-by: Andre Muezerie <andremue@linux.microsoft.com>
Co-authored-by: Carlos Nihelton <carlos.santanadeoliveira@canonical.com>
Co-authored-by: Feng Wang <wang6922@outlook.com>
Co-authored-by: Feng Wang <wangfen@microsoft.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-26 17:10:59 -07:00

2906 lines
80 KiB
C++

/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
Common.cpp
Abstract:
This contains common used definitions used for testing.
--*/
// includes
#include "precomp.h"
#include "Common.h"
#include "LxssDynamicFunction.h"
#include <tlhelp32.h>
#include <werapi.h>
#include <Dbghelp.h>
#include <winsafer.h>
using namespace WEX::Logging;
using namespace WEX::Common;
using namespace WEX::TestExecution;
MODULE_SETUP(ModuleSetup);
MODULE_CLEANUP(ModuleCleanup);
// Defines
#define LXSS_LOGS_DIRECTORY L"logs"
#define LXSS_TEST_DIRECTORY L"\\data\\test"
#define LXSS_TEST_LOG_SEPARATOR_CHAR L"&"
#define LXSS_DEFAULT_TIMEOUT (15 * 1000)
//
// The instance test timeout should roughly be the maximum time to start an
// instance.
//
#define LXSS_INSTANCE_TEST_TIMEOUT (3 * 1000)
//
// The watchdog timeout is set to 3 hours.
//
#define LXSS_WATCHDOG_TIMEOUT (3 * 60 * 60 * 1000)
#define LXSS_WATCHDOG_TIMEOUT_WINDOW 1000
//
// Global variables
//
static HANDLE g_OriginalStdout;
static HANDLE g_OriginalStderr;
static BOOL g_RelogEverything = TRUE;
static bool g_LogDmesgAfterEachTest = false;
static PTP_TIMER g_WatchdogTimer;
static BOOL g_VmMode;
static std::wstring g_originalConfig;
static std::wstring g_originalDefaultDistro;
std::wstring g_dumpFolder;
std::optional<std::wstring> g_dumpToolPath;
static bool g_enableWerReport = false;
static std::wstring g_pipelineBuildId;
std::wstring g_testDistroPath;
std::wstring g_testDataPath;
bool g_fastTestRun = false; // True when test.bat was invoked with -f
std::pair<wil::unique_handle, wil::unique_handle> CreateSubprocessPipe(bool inheritRead, bool inheritWrite, DWORD bufferSize, _In_opt_ SECURITY_ATTRIBUTES* sa)
{
wil::unique_handle read;
wil::unique_handle write;
THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&read, &write, sa, bufferSize));
if (inheritWrite)
{
THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(write.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
}
if (inheritRead)
{
THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(read.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
}
return {std::move(read), std::move(write)};
}
// LxsstuLaunchWsl
DWORD
LxsstuLaunchWsl(_In_opt_ LPCWSTR Arguments, _In_opt_ HANDLE StandardInput, _In_opt_ HANDLE StandardOutput, _In_opt_ HANDLE StandardError, _In_opt_ HANDLE Token, _In_ DWORD Flags)
{
// Launch wsl.exe to handle the operation.
auto CommandLine = LxssGenerateWslCommandLine(Arguments);
return LxsstuRunCommand(CommandLine.data(), StandardInput, StandardOutput, StandardError, Token, Flags);
}
DWORD
LxsstuLaunchWsl(_In_opt_ const std::wstring& Arguments, _In_opt_ HANDLE StandardInput, _In_opt_ HANDLE StandardOutput, _In_opt_ HANDLE StandardError, _In_opt_ HANDLE Token)
{
return LxsstuLaunchWsl(Arguments.data(), StandardInput, StandardOutput, StandardError, Token);
}
// LxsstuLaunchWslAndCaptureOutput
std::pair<std::wstring, std::wstring> LxsstuLaunchWslAndCaptureOutput(
_In_ LPCWSTR Cmd, _In_ int ExpectedExitCode, _In_opt_ HANDLE StandardInput, _In_opt_ HANDLE Token, _In_ DWORD Flags, _In_ LPCWSTR EntryPoint)
/*++
Routine Description:
Run a WSL command and capture its output.
Arguments:
Cmd - The command line to run.
ExpectedExitCode - The expected exit code from the child process.
StandardInput - Handle to the process's standard input
Return Value:
A pair of strings with stdout and stderr output.
--*/
{
auto CommandLine = LxssGenerateWslCommandLine(Cmd, EntryPoint);
return LxsstuLaunchCommandAndCaptureOutput(CommandLine.data(), ExpectedExitCode, StandardInput, Token, Flags);
}
// LxssGenerateWslCommandLine
std::wstring LxssGenerateWslCommandLine(_In_opt_ LPCWSTR Arguments, _In_ LPCWSTR EntryPoint)
{
std::wstring CommandLine;
THROW_IF_FAILED(wil::GetSystemDirectoryW(CommandLine));
CommandLine += L"\\";
CommandLine += EntryPoint;
if (ARGUMENT_PRESENT(Arguments))
{
CommandLine += L" ";
CommandLine += Arguments;
}
return CommandLine;
}
// LxsstuLaunchWslAndCaptureOutput
std::pair<std::wstring, std::wstring> LxsstuLaunchWslAndCaptureOutput(
_In_ const std::wstring& Cmd, _In_ int ExpectedExitCode, _In_opt_ HANDLE StandardInput, _In_opt_ HANDLE Token, _In_ DWORD Flags, _In_ LPCWSTR EntryPoint)
/*++
Routine Description:
Run a wsl command and return its output.
Arguments:
Cmd - Supplies the wsl command to run.
ExpectedExitCode - The expected exit code from the child process.
StandardInput - Handle to the process's standard input
Return Value:
The command's stdout and stderr output.
--*/
{
return LxsstuLaunchWslAndCaptureOutput(Cmd.data(), ExpectedExitCode, StandardInput, Token, Flags, EntryPoint);
}
std::pair<std::wstring, std::wstring> LxsstuLaunchCommandAndCaptureOutput(_In_ LPWSTR Cmd, _In_ LPCSTR StandardInput, _In_opt_ HANDLE Token, _In_ DWORD Flags)
{
const auto inputSize = static_cast<DWORD>(strlen(StandardInput));
auto [read, write] = CreateSubprocessPipe(true, false, inputSize);
THROW_IF_WIN32_BOOL_FALSE(WriteFile(write.get(), StandardInput, inputSize, nullptr, nullptr));
write.reset();
return LxsstuLaunchCommandAndCaptureOutput(Cmd, 0, read.get(), Token, Flags);
}
// LxsstuLaunchCommandAndCaptureOutputWithResult
std::tuple<std::wstring, std::wstring, int> LxsstuLaunchCommandAndCaptureOutputWithResult(
_In_ LPWSTR Cmd, _In_opt_ HANDLE StandardInput, _In_opt_ HANDLE Token, _In_ DWORD Flags)
/*++
Routine Description:
Run a command and capture its output.
Arguments:
Cmd - The command line to run.
Return Value:
A pair of strings with stdout and stderr output.
--*/
{
wsl::windows::common::SubProcess process(nullptr, Cmd);
process.SetStdHandles(StandardInput, nullptr, nullptr);
process.SetToken(Token);
process.SetFlags(Flags);
auto result = process.RunAndCaptureOutput();
return {result.Stdout, result.Stderr, result.ExitCode};
}
// LxsstuLaunchCommandAndCaptureOutput
std::pair<std::wstring, std::wstring> LxsstuLaunchCommandAndCaptureOutput(
_In_ LPWSTR Cmd, _In_ int ExpectedExitCode, _In_opt_ HANDLE StandardInput, _In_opt_ HANDLE Token, _In_ DWORD Flags)
/*++
Routine Description:
Run a command and capture its output.
Arguments:
Cmd - The command line to run.
Return Value:
A pair of strings with stdout and stderr output.
--*/
{
auto [Out, Err, ExitCode] = LxsstuLaunchCommandAndCaptureOutputWithResult(Cmd, StandardInput, Token, Flags);
if (ExitCode != ExpectedExitCode)
{
THROW_HR_MSG(
E_UNEXPECTED,
"Command \"%ls\" "
"returned unexpected exit code (%lu != %i). "
"Stdout: '%ls' "
"Stderr: '%ls'",
Cmd,
ExitCode,
ExpectedExitCode,
Out.c_str(),
Err.c_str());
}
return std::make_pair(Out, Err);
}
// LxsstuRunCommand
DWORD
LxsstuRunCommand(_In_ LPWSTR Command, _In_opt_ HANDLE StandardInput, _In_opt_ HANDLE StandardOutput, _In_opt_ HANDLE StandardError, _In_opt_ HANDLE Token, _In_ DWORD Flags)
{
const auto Process = LxsstuStartProcess(Command, StandardInput, StandardOutput, StandardError, Token, Flags);
return wsl::windows::common::SubProcess::GetExitCode(Process.get());
}
// LxsstuStartProcess
wil::unique_handle LxsstuStartProcess(
_In_ LPWSTR Command, _In_opt_ HANDLE StandardInput, _In_opt_ HANDLE StandardOutput, _In_opt_ HANDLE StandardError, _In_opt_ HANDLE Token, _In_ DWORD Flags)
{
wsl::windows::common::SubProcess process(nullptr, Command);
process.SetStdHandles(
ARGUMENT_PRESENT(StandardInput) ? StandardInput : GetStdHandle(STD_INPUT_HANDLE),
ARGUMENT_PRESENT(StandardOutput) ? StandardOutput : GetStdHandle(STD_OUTPUT_HANDLE),
ARGUMENT_PRESENT(StandardError) ? StandardError : GetStdHandle(STD_ERROR_HANDLE));
process.SetToken(Token);
process.SetFlags(Flags);
return process.Start();
}
// FileFromHandle
wil::unique_file FileFromHandle(_Inout_ wil::unique_handle& Handle, _In_ const char* Mode)
/*++
Routine Description:
Create a FILE from a handle.
Arguments:
Handle - The handle to create the FILE from.
Mode - The mode to create the FILE with.
Return Value:
The created FILE.
--*/
{
using UniqueFd = wil::unique_any<int, decltype(_close), _close, wil::details::pointer_access_all, int, int, -1>;
UniqueFd Fd(_open_osfhandle(reinterpret_cast<intptr_t>(Handle.get()), 0));
if (Fd.get() < 0)
{
THROW_LAST_ERROR_MSG("_open_osfhandle failed");
}
Handle.release();
wil::unique_file File(_fdopen(Fd.get(), Mode));
VERIFY_IS_NOT_NULL(File.get());
Fd.release();
return File;
}
// LxsstuInitialize
BOOL LxsstuInitialize(__in BOOLEAN RunInstanceTests)
{
wil::unique_hkey Key;
LRESULT Result;
BOOL Success;
DWORD Value;
Success = FALSE;
THROW_IF_FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED));
//
// Don't fail if CoInitializeSecurity has already been called.
//
const auto Hr = CoInitializeSecurity(
nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_STATIC_CLOAKING, 0);
THROW_HR_IF(Hr, FAILED(Hr) && Hr != RPC_E_TOO_LATE);
WSADATA Data;
THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &Data));
VERIFY_IS_TRUE(SetEnvironmentVariableW(L"WSL_UTF8", L"1"));
if (LxsstuVmMode() == FALSE)
{
Result = RegOpenKeyEx(HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, 0, KEY_ALL_ACCESS, &Key);
if (Result != ERROR_SUCCESS)
{
LogError("RegOpenKeyEx %s failed with %Id", LXSS_REGISTRY_PATH, Result);
goto InitializeEnd;
}
//
// Set the error level to critical so the driver will not break into kd
// while the test is running.
//
Value = LxErrorLevel_Critical;
Result = RegSetValueEx(Key.get(), LX_QUERY_REGISTRY_ERROR_LEVEL_SUBKEY, 0, REG_DWORD, (const PBYTE)&Value, sizeof(DWORD));
if (Result != ERROR_SUCCESS)
{
LogError("RegSetValueEx %s failed with %Id", LX_QUERY_REGISTRY_ERROR_LEVEL_SUBKEY, Result);
goto InitializeEnd;
}
//
// Disable breaking on syscall failures.
//
Value = FALSE;
Result = RegSetValueEx(Key.get(), LX_QUERY_REGISTRY_BREAK_ON_SYSCALL_FAILURE_SUBKEY, 0, REG_DWORD, (const PBYTE)&Value, sizeof(DWORD));
if (Result != ERROR_SUCCESS)
{
LogError("RegSetValueEx %s failed with %Id", LX_QUERY_REGISTRY_BREAK_ON_SYSCALL_FAILURE_SUBKEY, Result);
goto InitializeEnd;
}
//
// Enable lxbus root access.
//
Value = TRUE;
Result = RegSetValueEx(Key.get(), LX_QUERY_REGISTRY_ROOT_LXBUS_ACCESS, 0, REG_DWORD, (const PBYTE)&Value, sizeof(DWORD));
if (Result != ERROR_SUCCESS)
{
LogError("RegSetValueEx %s failed with %Id", LX_QUERY_REGISTRY_ROOT_LXBUS_ACCESS, Result);
goto InitializeEnd;
}
//
// Enable mounting DrvFs with case=force.
//
Value = TRUE;
Result = RegSetValueEx(Key.get(), LX_QUERY_REGISTRY_DRVFS_ALLOW_FORCE_CASE_SENSITIVITY, 0, REG_DWORD, (const PBYTE)&Value, sizeof(DWORD));
if (Result != ERROR_SUCCESS)
{
LogError("RegSetValueEx %s failed with %Id", LX_QUERY_REGISTRY_DRVFS_ALLOW_FORCE_CASE_SENSITIVITY, Result);
goto InitializeEnd;
}
}
else
{
const auto LogDirectory = LxsstuGetTestDirectory() + L"\\log";
wil::CreateDirectoryDeep(LogDirectory.c_str());
}
//
// Run the instance tests.
//
if (RunInstanceTests != FALSE)
{
VERIFY_NO_THROW(LxsstuInstanceTests());
}
Success = TRUE;
InitializeEnd:
return Success;
}
// LxxstuVmMode
BOOL LxsstuVmMode(VOID)
/*++
Routine Description:
Queries if the tests are being run in VM mode.
Arguments:
None.
Return Value:
TRUE if the tests are running in VM mode, FALSE otherwise.
--*/
{
return g_VmMode;
}
// LxsstuLaunchPowershellAndCaptureOutput
std::pair<std::wstring, std::wstring> LxsstuLaunchPowershellAndCaptureOutput(_In_ const std::wstring& Cmd, _In_ int ExpectedExitCode)
/*++
Routine Description:
Run a powershell command and return its output.
Arguments:
Cmd - Supplies the powershell command to run.
ExpectedExitCode - The expected exit code from the child process.
Return Value:
s
The command's stdout and stderr output.
--*/
{
auto CommandLine = L"Powershell -NoProfile -Command \"" + Cmd + L"\"";
LogInfo("Running the command: %ls\n", CommandLine.c_str());
return LxsstuLaunchCommandAndCaptureOutput(CommandLine.data(), ExpectedExitCode);
}
// LxsstuUninitialize
VOID LxsstuUninitialize(__in BOOLEAN RunInstanceTests)
{
wil::unique_hkey Key;
LRESULT Result;
//
// Run the instance tests again to make sure that the instance can be
// started and stopped (i.e. no leaked fs references).
//
if (RunInstanceTests != FALSE)
{
VERIFY_NO_THROW(LxsstuInstanceTests());
}
if (LxsstuVmMode() == FALSE)
{
//
// Delete registry subkeys that were set by the test framework.
//
Result = RegOpenKeyEx(HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, 0, KEY_ALL_ACCESS, &Key);
if (Result != ERROR_SUCCESS)
{
LogInfo("RegOpenKeyEx failed with %Id", Result);
}
else
{
auto DeleteKey = [&](LPCWSTR KeyName) {
Result = RegDeleteKeyValue(Key.get(), nullptr, KeyName);
if (Result != ERROR_SUCCESS)
{
LogInfo("RegDeleteKeyValue %s failed with %Id", KeyName, Result);
}
};
DeleteKey(LX_QUERY_REGISTRY_ERROR_LEVEL_SUBKEY);
DeleteKey(LX_QUERY_REGISTRY_BREAK_ON_SYSCALL_FAILURE_SUBKEY);
DeleteKey(LX_QUERY_REGISTRY_ROOT_LXBUS_ACCESS);
DeleteKey(LX_QUERY_REGISTRY_DRVFS_ALLOW_FORCE_CASE_SENSITIVITY);
}
}
VERIFY_IS_TRUE(SetEnvironmentVariableW(L"WSL_UTF8", nullptr));
WSACleanup();
//
// Clear the winrt cache in case LookupLiftedPackage() is called again after another CoInitialize().
//
winrt::clear_factory_cache();
CoUninitialize();
return;
}
// LxssLogKernelOutput
void LxssLogKernelOutput(VOID)
/*++
Routine Description:
Write the kernel output in the test logs.
Arguments:
None.
Return Value:
None.
--*/
{
if (!g_LogDmesgAfterEachTest)
{
return;
}
//
// dmesg -c isn't implemented on WSL1
//
const auto cmd = LxsstuVmMode() ? L"dmesg -c" : L"dmesg";
const auto Output = LxsstuLaunchWslAndCaptureOutput(cmd);
LogInfo("Kernel logs: '%ls'", Output.first.c_str());
}
// LxsstuGetTestDirectory
std::wstring LxsstuGetTestDirectory(VOID)
/*++
Description:
This routine gets the test directory.
Parameters:
None.
Return:
The test directory.
--*/
{
std::wstring TestDirectory = LxsstuGetLxssDirectory();
TestDirectory += L"\\" LXSS_ROOTFS_DIRECTORY LXSS_TEST_DIRECTORY;
return TestDirectory;
}
// LxsstuGetLxssDirectory
std::wstring LxsstuGetLxssDirectory(VOID)
/*++
Description:
This routine gets the lxss directory.
Parameters:
None.
Return:
The lxss directory.
--*/
{
const wil::unique_hkey LxssKey = wsl::windows::common::registry::OpenLxssUserKey();
const std::wstring Default = wsl::windows::common::registry::ReadString(LxssKey.get(), nullptr, L"DefaultDistribution", nullptr);
std::wstring BasePath = wsl::windows::common::registry::ReadString(LxssKey.get(), Default.c_str(), L"BasePath", nullptr);
return BasePath;
}
void CaptureLiveDump()
{
auto PrivilegeState = wsl::windows::common::security::AcquirePrivilege(SE_DEBUG_NAME);
const std::wstring targetFile = g_dumpFolder + L"\\livedump.dmp";
LogInfo("Writing livedump in: %ls", targetFile.c_str());
wsl::windows::common::SubProcess dumpProcess{nullptr, std::format(L"{} \"{}\"", g_dumpToolPath->c_str(), targetFile.c_str()).c_str()};
const auto exitCode = dumpProcess.Run();
if (exitCode != 0)
{
LogError("Failed to capture livedump. ExitCode=%lu", exitCode);
return;
}
LogInfo("Dump size: %llu", std::filesystem::file_size(targetFile));
// Try to compress the dump.
std::wstring command = L"Powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"Compress-Archive -Force -Path '" +
targetFile + L"' -DestinationPath '" + targetFile + L".zip'\"";
if (LxsstuRunCommand(command.data()) != 0)
{
// Note: powershell will fail to create the .zip if the dump is bigger than 2GB with:
// Exception calling "Write" with "3" argument(s): "Stream was too long."
LogError("Failed to compress live dump");
}
else
{
THROW_IF_WIN32_BOOL_FALSE(DeleteFile(targetFile.c_str()));
}
}
DEFINE_ENUM_FLAG_OPERATORS(MINIDUMP_TYPE);
DWORD FindThreadInProcess(DWORD Pid)
{
const wil::unique_handle Threads{CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)};
THREADENTRY32 ThreadInfo{};
ThreadInfo.dwSize = sizeof(ThreadInfo);
for (auto result = Thread32First(Threads.get(), &ThreadInfo); result; result = Thread32Next(Threads.get(), &ThreadInfo))
{
if (ThreadInfo.th32OwnerProcessID == Pid)
{
return ThreadInfo.th32ThreadID;
}
}
THROW_HR(HRESULT_FROM_WIN32(STATUS_NOT_FOUND));
}
PVOID GetModuleAddressInProcess(HANDLE Process, const std::wstring& Module)
{
// From: https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-enumprocessmodulesex
// Do not call CloseHandle on any of the handles returned by this function. The information comes from a snapshot, so there are no resources to be freed.
std::vector<HMODULE> Modules;
DWORD RequiredSize{};
bool Result{};
do
{
Modules.resize(RequiredSize / sizeof(HMODULE));
Result = EnumProcessModulesEx(Process, Modules.data(), static_cast<DWORD>(Modules.size() * sizeof(HMODULE)), &RequiredSize, LIST_MODULES_ALL);
} while (Result && RequiredSize / sizeof(HMODULE) > Modules.size());
for (const auto& e : Modules)
{
std::filesystem::path modulePath = wil::GetModuleFileNameExW<std::wstring>(Process, e);
if (wsl::windows::common::string::IsPathComponentEqual(modulePath.filename().native(), Module))
{
MODULEINFO Info{};
THROW_IF_WIN32_BOOL_FALSE(GetModuleInformation(Process, e, &Info, sizeof(Info)));
return Info.lpBaseOfDll;
}
}
THROW_HR(HRESULT_FROM_WIN32(STATUS_NOT_FOUND));
}
void CreateCrashReport(HANDLE Process, LPCWSTR ProcessName, DWORD Pid, std::wstring const& EventName)
{
using unique_hreport = wil::unique_any<HREPORT, decltype(WerReportCloseHandle), WerReportCloseHandle>;
auto setProperty = [](LPWSTR Target, const std::wstring& Value, size_t BufferSize) {
wcsncpy(Target, Value.c_str(), std::min(BufferSize - 1, Value.size()));
};
WER_REPORT_INFORMATION Info{};
Info.dwSize = sizeof(Info);
Info.hProcess = Process;
setProperty(Info.wzDescription, EventName, ARRAYSIZE(Info.wzDescription));
setProperty(Info.wzApplicationName, ProcessName, ARRAYSIZE(Info.wzApplicationName));
setProperty(Info.wzApplicationPath, wil::GetModuleFileNameExW<std::wstring>(Process, nullptr), ARRAYSIZE(Info.wzApplicationPath));
unique_hreport Report;
THROW_IF_FAILED(WerReportCreate(EventName.c_str(), WerReportApplicationCrash, &Info, &Report));
const std::wstring DumpPath = g_dumpFolder + L"\\" + ProcessName + L"." + std::to_wstring(Pid) + L".hdmp";
wil::unique_hfile DumpFile{CreateFileW(DumpPath.c_str(), GENERIC_ALL, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
THROW_LAST_ERROR_IF(!DumpFile);
std::optional<MINIDUMP_EXCEPTION_INFORMATION> ExceptionInfo;
EXCEPTION_RECORD Record{};
EXCEPTION_POINTERS Pointers{};
// To get access to the dumps in AzureWatson, the exception address needs to point to a module
// that we own. To do that, load the main module and point the exception to its entrypoint.
try
{
Record.ExceptionAddress = GetModuleAddressInProcess(Process, ProcessName);
Record.ExceptionCode = EXCEPTION_BREAKPOINT;
Pointers.ExceptionRecord = &Record;
ExceptionInfo.emplace();
ExceptionInfo->ExceptionPointers = &Pointers;
ExceptionInfo->ThreadId = FindThreadInProcess(Pid);
}
catch (...)
{
LogError("Failed to find module address / thread id for %ls, 0x%x", ProcessName, wil::ResultFromCaughtException());
}
THROW_IF_WIN32_BOOL_FALSE(MiniDumpWriteDump(
Process,
Pid,
DumpFile.get(),
MiniDumpWithDataSegs | MiniDumpWithProcessThreadData | MiniDumpWithHandleData | MiniDumpWithPrivateReadWriteMemory |
MiniDumpWithUnloadedModules | MiniDumpWithFullMemoryInfo | MiniDumpWithThreadInfo | MiniDumpWithTokenInformation |
MiniDumpWithPrivateWriteCopyMemory | MiniDumpWithCodeSegs,
ExceptionInfo.has_value() ? &ExceptionInfo.value() : nullptr,
nullptr,
nullptr));
DumpFile.reset();
THROW_IF_FAILED(WerReportAddFile(Report.get(), DumpPath.c_str(), WerFileTypeHeapdump, 0));
WER_SUBMIT_RESULT SubmitResult{};
const auto Result = WerReportSubmit(
Report.get(),
WerConsentApproved,
WER_SUBMIT_ADD_REGISTERED_DATA | WER_SUBMIT_NO_CLOSE_UI | WER_SUBMIT_BYPASS_DATA_THROTTLING | WER_SUBMIT_REPORT_MACHINE_ID | WER_SUBMIT_QUEUE,
&SubmitResult);
LogInfo("WerReportSubmit() returned 0x%x, SubmitResult = %i, EventName = %ls", Result, SubmitResult, EventName.c_str());
}
void CreateProcessCrashReport(DWORD Pid, LPCWSTR ImageName, LPCWSTR EventName)
{
try
{
LogInfo("Opening process %ls, Pid %lu", ImageName, Pid);
const wil::unique_handle Process(OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid));
THROW_LAST_ERROR_IF_NULL(Process);
CreateCrashReport(Process.get(), ImageName, Pid, EventName);
}
catch (...)
{
LogError("Failed to create crash report for process %ls (%lu), %lu", ImageName, Pid, wil::ResultFromCaughtException());
}
}
void CreateWerReports()
{
static const std::set<std::wstring, wsl::shared::string::CaseInsensitiveCompare> WslProcesses{
L"wsl.exe",
L"wslhost.exe",
L"wslrelay.exe",
L"wslservice.exe",
L"wslg.exe",
L"vmcompute.exe",
L"vmwp.exe",
L"wslasession.exe",
L"wslc.exe"};
auto PrivilegeState = wsl::windows::common::security::AcquirePrivilege(SE_DEBUG_NAME);
const std::wstring EventName = L"WslTestHang-" + g_pipelineBuildId;
LogInfo("Dumps here: https://azurewatson.microsoft.com/?EventType=%s", EventName.c_str());
// Start by capturing the test process, since collect dmesg changes the state of the UVM.
try
{
CreateProcessCrashReport(GetCurrentProcessId(), L"te.processhost.exe", EventName.c_str());
}
CATCH_LOG();
PROCESSENTRY32 PE32;
PE32.dwSize = sizeof(PE32);
const wil::unique_handle ProcessSnapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0));
THROW_LAST_ERROR_IF(ProcessSnapshot.get() == INVALID_HANDLE_VALUE);
try
{
if (Process32First(ProcessSnapshot.get(), &PE32))
{
do
{
if (WslProcesses.find(std::wstring(PE32.szExeFile)) == WslProcesses.end())
{
continue;
}
try
{
CreateProcessCrashReport(PE32.th32ProcessID, PE32.szExeFile, EventName.c_str());
}
CATCH_LOG();
} while (Process32Next(ProcessSnapshot.get(), &PE32));
}
THROW_LAST_ERROR_IF(GetLastError() != ERROR_NO_MORE_FILES);
}
CATCH_LOG();
// Also capture an HNS dump. Since the process name is svchost.exe, find its pid from its service.
const wil::unique_schandle manager{OpenSCManager(nullptr, nullptr, SC_MANAGER_CONNECT)};
THROW_LAST_ERROR_IF_NULL(manager);
const wil::unique_schandle service{OpenService(manager.get(), L"HNS", SERVICE_QUERY_STATUS)};
THROW_LAST_ERROR_IF_NULL(service);
auto [_, pid] = GetServiceState(service.get());
CreateProcessCrashReport(pid, L"svchost.exe", EventName.c_str());
}
void DumpGuestProcesses()
{
constexpr auto dumpScript =
R"(
set -ue
dmesg
# Try to install gdb
tdnf install -y gdb || true
declare -a pids_to_dump
for proc in /proc/[0-9]*; do
read -a stats < "$proc/stat" # Skip kernel threads to make the output easier to read
flags=${stats[8]}
if (( ("$flags" & 0x00200000) == 0x00200000 )); then
continue
fi
pid=$(basename "$proc")
pids_to_dump+=("$pid")
parent=$(ps -o ppid= -p "$pid")
echo -e "\nProcess: $pid (parent: $parent) "
echo -en "cmd: "
cat "/proc/$pid/cmdline" || true
echo -e "\nstat: "
cat "/proc/$pid/stat" || true
for tid in $(ls "/proc/$pid/task" || true); do
echo -n "tid: $tid - "
cat "/proc/$pid/task/$tid/comm" || true
cat "/proc/$pid/task/$tid/stack" || true
done
echo "fds: "
ls -la "/proc/$pid/fd" || true
done
for pid in "${pids_to_dump[@]}" ; do
name=$(ps -p "$pid" -o comm=)
if [[ "$name" =~ ^(bash|login)$ ]]; then
echo "Skipping dump for process: $name"
continue
fi
echo "Dumping process: $name ($pid) "
if gcore -a -o core "$pid" ; then
if ! /wsl-capture-crash 0 "$name" "$pid" 0 < "core.$pid" ; then
echo "Failed to dump process $pid"
fi
rm "core.$pid"
fi
done
echo "hvsockets: "
ss -lap --vsock
echo "meminfo: "
cat /proc/meminfo
poweroff -f
)";
const std::wstring filePath = g_dumpFolder + L"\\guest-state.txt";
LogInfo("Dumping guest processes in: %ls", filePath.c_str());
const wil::unique_hfile outputFile{CreateFileW(
filePath.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
THROW_LAST_ERROR_IF(!outputFile);
THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(outputFile.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
auto [readPipe, writePipe] = CreateSubprocessPipe(true, false);
auto cmd = LxssGenerateWslCommandLine(L"--debug-shell");
const auto process = LxsstuStartProcess(cmd.data(), readPipe.get(), outputFile.get());
THROW_IF_WIN32_BOOL_FALSE(WriteFile(writePipe.get(), dumpScript, static_cast<DWORD>(strlen(dumpScript)), nullptr, nullptr));
writePipe.reset();
// Wait up to 5 minutes for that process
const auto result = WaitForSingleObject(process.get(), 60 * 1000 * 5);
if (result != WAIT_TIMEOUT)
{
LogError("Unexpected status waiting for the debug shell, %lu", result);
}
}
// LxsstuWatchdogTimer
VOID __stdcall LxsstuWatchdogTimer(_Inout_ PTP_CALLBACK_INSTANCE Instance, _Inout_opt_ PVOID ThreadpoolTimerContext, _Inout_ PTP_TIMER Timer)
/*++
Routine Description:
Runs when the watch dog timer has fired to crash the process.
Arguments:
Instance - Not used.
ThreadpoolTimerContext - Not used.
Timer - Not used.
Return Value:
None.
--*/
{
UNREFERENCED_PARAMETER(Instance);
UNREFERENCED_PARAMETER(ThreadpoolTimerContext);
UNREFERENCED_PARAMETER(Timer);
try
{
if (g_enableWerReport)
{
CreateWerReports();
}
else
{
LogError("Wer reporting disabled, skipping");
}
}
catch (...)
{
LogError("Failed to create WER report, 0x%x", wil::ResultFromCaughtException());
}
if (LxsstuVmMode())
{
try
{
DumpGuestProcesses();
}
catch (...)
{
LogError("Failed to dump guest processes, 0x%x", wil::ResultFromCaughtException());
}
}
try
{
if (g_enableWerReport && g_dumpToolPath.has_value())
{
CaptureLiveDump();
}
}
catch (...)
{
LogError("Failed to capture livedump, 0x%x", wil::ResultFromCaughtException());
}
__fastfail(FAST_FAIL_FATAL_APP_EXIT);
return;
}
// LxsstuInstanceTests
VOID LxsstuInstanceTests(VOID)
/*++
Routine Description:
Runs the instance unit tests.
Arguments:
None.
Return Value:
None.
--*/
{
ULONG Iteration;
ULONG NumberOfIterations;
ULONG SleepDuration;
unsigned int Seed;
//
// Start and stop an instance multiple times, sleeping a random duration
// between the start and stop.
//
NumberOfIterations = 5;
Seed = GetTickCount();
srand(Seed);
LogInfo("Starting instance tests, Seed = %u", Seed);
for (Iteration = 0; Iteration < NumberOfIterations; Iteration++)
{
LogInfo("Create instance - Iteration %u of %u", (Iteration + 1), NumberOfIterations);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/bin/true"), 0u);
SleepDuration = rand() % LXSS_INSTANCE_TEST_TIMEOUT;
LogInfo("Sleeping %u milliseconds before destroying instance...", SleepDuration);
SleepEx(SleepDuration, FALSE);
TerminateDistribution();
}
LogPass("Instance tests passed");
return;
}
// LxxsSplitString
std::vector<std::wstring> LxssSplitString(_In_ const std::wstring& String, _In_ const std::wstring& Delim)
/*++
Routine Description:
Split a string by a delimiter.
Arguments:
String - Supplies the string to split.
Delim - The delimiter to split the string on.
Return Value:
A vector of split string.
--*/
{
std::vector<std::wstring> output;
std::wistringstream input(String);
std::wstring entry;
std::string::size_type index = 0;
std::string::size_type previous_index = 0;
while ((index = String.find(Delim, previous_index)) != std::string::npos)
{
output.emplace_back(String.substr(previous_index, index - previous_index));
previous_index = index + Delim.size();
}
auto remaining = String.substr(previous_index);
if (remaining != Delim && !remaining.empty())
{
output.emplace_back(std::move(remaining));
}
return output;
}
// WslKeepAlive class definitions
WslKeepAlive::WslKeepAlive(HANDLE Token) : m_token(Token)
{
Set();
}
WslKeepAlive::~WslKeepAlive()
{
Reset();
}
void WslKeepAlive::Set()
{
std::tie(m_read, m_write) = CreateSubprocessPipe(true, false);
m_running.emplace();
m_thread = std::thread(std::bind(&WslKeepAlive::Run, this));
m_running->get_future().wait();
}
void WslKeepAlive::Run()
{
try
{
// Create a pipe to read wsl's output
wil::unique_handle read;
wil::unique_handle write;
SECURITY_ATTRIBUTES attributes = {0};
attributes.nLength = sizeof(attributes);
attributes.bInheritHandle = true;
THROW_LAST_ERROR_IF(!CreatePipe(&read, &write, &attributes, sizeof(attributes)));
// Start a process that outputs 'running', then waits
const std::wstring expectedOutput = L"running";
std::wstring cmd = L"wsl.exe echo -n " + expectedOutput + L" && read -n 1 ";
const auto process = LxsstuStartProcess(cmd.data(), m_read.get(), write.get(), nullptr, m_token);
write.reset();
// Wait until we read 'running'
std::string buffer(expectedOutput.size(), '\0');
DWORD bytesRead = 0;
VERIFY_IS_TRUE(ReadFile(read.get(), buffer.data(), static_cast<DWORD>(expectedOutput.size()), &bytesRead, nullptr));
VERIFY_ARE_EQUAL(buffer, wsl::shared::string::WideToMultiByte(expectedOutput));
m_running->set_value();
WaitForSingleObject(process.get(), INFINITE);
}
catch (...)
{
LogError("Caught exception in WslKeepAlive::Run");
m_running->set_exception(std::current_exception());
}
}
void WslKeepAlive::Reset()
{
if (m_thread.joinable())
{
const char c = 'k';
THROW_LAST_ERROR_IF(!WriteFile(m_write.get(), &c, sizeof(c), nullptr, nullptr));
m_write.reset();
m_thread.join();
}
}
std::pair<DWORD, DWORD> GetServiceState(SC_HANDLE service)
{
DWORD dwBytesNeeded{};
SERVICE_STATUS_PROCESS status{};
if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO, (LPBYTE)&status, sizeof(status), &dwBytesNeeded))
{
LogError("QueryServiceStatusEx() failed, %lu", GetLastError());
VERIFY_FAIL();
}
return std::make_pair(status.dwCurrentState, status.dwProcessId);
}
void WaitForServiceState(SC_HANDLE service, DWORD state, DWORD previousPid)
{
DWORD currentState{};
DWORD pid{};
auto pred = [&]() {
std::tie(currentState, pid) = GetServiceState(service);
if (pid != previousPid && state == SERVICE_STOPPED)
{
return;
}
THROW_HR_IF(E_ABORT, currentState != state && currentState != SERVICE_STOPPED);
};
try
{
wsl::shared::retry::RetryWithTimeout<void>(pred, std::chrono::milliseconds(100), std::chrono::minutes(2), [&]() {
return wil::ResultFromCaughtException() == E_ABORT;
});
}
catch (...)
{
LogError("Timed waiting for service to reach state: %lu. Current state: %lu, error: 0x%x", state, currentState, wil::ResultFromCaughtException());
}
}
void StopService(SC_HANDLE service)
{
// Some services don't accept SERVICE_CONTROL_STOP when starting.
// Wait for them to be running before stopping them
auto [state, pid] = GetServiceState(service);
if (state == SERVICE_START_PENDING)
{
WaitForServiceState(service, SERVICE_RUNNING, pid);
}
SERVICE_STATUS status{};
if (!ControlService(service, SERVICE_CONTROL_STOP, &status))
{
const auto error = GetLastError();
if (error != ERROR_SERVICE_NOT_ACTIVE)
{
LogError("Unexpected error code: 0x%x", error);
VERIFY_FAIL();
}
return; // Service is not running
}
WaitForServiceState(service, SERVICE_STOPPED, pid);
}
void RestartWslService()
/*++
Routine Description:
Restart the WSL service.
Arguments:
None.
Return Value:
None.
--*/
{
LogInfo("Restarting WSLService");
const wil::unique_schandle manager{OpenSCManager(nullptr, nullptr, SC_MANAGER_CONNECT)};
VERIFY_IS_NOT_NULL(manager);
const wil::unique_schandle service{OpenService(manager.get(), L"wslservice", SERVICE_STOP | SERVICE_QUERY_STATUS | SERVICE_START)};
VERIFY_IS_NOT_NULL(service);
StopService(service.get());
if (!StartService(service.get(), 0, nullptr))
{
VERIFY_ARE_EQUAL(GetLastError(), ERROR_SERVICE_ALREADY_RUNNING);
}
}
void StopWslService()
{
LogInfo("Stopping WSLService");
const wil::unique_schandle manager{OpenSCManager(nullptr, nullptr, SC_MANAGER_CONNECT)};
VERIFY_IS_NOT_NULL(manager);
const wil::unique_schandle service{OpenService(manager.get(), L"wslservice", SERVICE_STOP | SERVICE_QUERY_STATUS)};
VERIFY_IS_NOT_NULL(service);
StopService(service.get());
}
wil::unique_handle GetNonElevatedToken(TOKEN_TYPE Type)
{
auto token = wil::open_current_access_token(TOKEN_ALL_ACCESS);
if (Type != TokenPrimary)
{
// N.B. Using the Safer API to create a non-elevated primary token break drvfs, so skipping this for primary tokens.
SAFER_LEVEL_HANDLE saferLevel = nullptr;
auto closeSaferLevel = wil::scope_exit([&]() { SaferCloseLevel(saferLevel); });
THROW_IF_WIN32_BOOL_FALSE(SaferCreateLevel(SAFER_SCOPEID_MACHINE, SAFER_LEVELID_NORMALUSER, SAFER_LEVEL_OPEN, &saferLevel, nullptr));
wil::unique_handle restrictedToken;
THROW_IF_WIN32_BOOL_FALSE(SaferComputeTokenFromLevel(saferLevel, token.get(), &restrictedToken, 0, nullptr));
token = std::move(restrictedToken);
}
wil::unique_handle nonElevatedToken;
THROW_IF_WIN32_BOOL_FALSE(DuplicateTokenEx(token.get(), TOKEN_ALL_ACCESS, nullptr, SecurityImpersonation, Type, &nonElevatedToken));
wil::unique_sid mediumIntegritySid;
THROW_LAST_ERROR_IF(!ConvertStringSidToSidA("S-1-16-8192", &mediumIntegritySid));
TOKEN_MANDATORY_LABEL label = {0};
label.Label.Attributes = SE_GROUP_INTEGRITY;
label.Label.Sid = mediumIntegritySid.get();
THROW_IF_WIN32_BOOL_FALSE(SetTokenInformation(nonElevatedToken.get(), TokenIntegrityLevel, &label, sizeof(label)));
return nonElevatedToken;
}
WslConfigChange::WslConfigChange(const std::wstring& Content)
{
m_originalContent = Update(Content);
}
WslConfigChange::WslConfigChange(WslConfigChange&& other) : m_originalContent(std::move(other.m_originalContent))
{
}
std::wstring WslConfigChange::Update(const std::wstring& Content)
{
auto previous = LxssWriteWslConfig(Content);
if (previous != Content)
{
RestartWslService();
}
return previous;
}
WslConfigChange::~WslConfigChange()
{
if (m_originalContent)
{
Update(m_originalContent.value());
}
}
std::wstring ReadFileContent(const std::string& Path)
{
std::ifstream configRead(Path);
return std::wstring{std::istreambuf_iterator<char>(configRead), {}};
}
std::wstring ReadFileContent(const std::wstring& Path)
{
std::wifstream configRead(Path);
return std::wstring{std::istreambuf_iterator<wchar_t>(configRead), {}};
}
// writes global WSL 2 config settings at %userprofile%/.wslconfig
std::wstring LxssWriteWslConfig(const std::wstring& Content)
{
auto path = getenv("userprofile") + std::string("\\.wslconfig");
auto previousContent = ReadFileContent(path);
std::wofstream config(path);
VERIFY_IS_TRUE(config.good());
config << Content;
return previousContent;
}
// writes distro specific settings /etc/wsl.conf
std::string LxssWriteWslDistroConfig(const std::string& Content)
{
std::string path = std::format("\\\\wsl.localhost\\{}\\etc\\wsl.conf", LXSS_DISTRO_NAME_TEST);
std::ifstream distroConfigRead(path);
auto previousContent = std::string{std::istreambuf_iterator<char>(distroConfigRead), {}};
distroConfigRead.close();
std::ofstream distroConfig(path, std::ios_base::binary);
VERIFY_IS_TRUE(distroConfig.good());
distroConfig.write(Content.c_str(), Content.size());
return previousContent;
}
// generates a sample global WSL config for the tests
std::wstring LxssGenerateTestConfig(TestConfigDefaults Default)
{
WEX::Common::String kernelLogsArg;
WEX::TestExecution::RuntimeParameters::TryGetValue(L"KernelLogs", kernelLogsArg);
std::wstring kernelLogs;
if (kernelLogsArg.IsEmpty())
{
kernelLogs = wil::GetCurrentDirectoryW().get() + std::wstring(L"\\kernelLogs.txt");
}
else
{
kernelLogs = kernelLogsArg;
}
auto boolOptionToString = [](LPCWSTR optionName, std::optional<bool> condition, bool defaultValue) {
std::wstring value{optionName};
value += L"=";
value += condition.value_or(defaultValue) ? L"true" : L"false";
value += L"\n";
return value;
};
auto networkingModeToString = [](std::optional<wsl::core::NetworkingMode> mode) {
if (mode.has_value())
{
std::wstring value = L"networkingMode=";
value += wsl::shared::string::MultiByteToWide(wsl::core::ToString(mode.value()));
value += L"\n";
return value;
}
return std::wstring{};
};
auto drvFsModeToString = [](std::optional<DrvFsMode> mode) {
std::wstring value;
switch (mode.value_or(DrvFsMode::Plan9))
{
case DrvFsMode::Plan9:
value = L"virtio9p=false";
break;
case DrvFsMode::Virtio9p:
value = L"virtio9p=true";
break;
case DrvFsMode::VirtioFs:
value = L"virtiofs=true";
break;
}
value += L"\n";
return value;
};
std::wstring newConfig =
L"[wsl2]\n"
L"crashDumpFolder=" +
EscapePath(Default.CrashDumpFolder.value_or(g_dumpFolder + L"\\linux-crashes")) + L"\nmaxCrashDumpCount=" +
std::to_wstring(Default.crashDumpCount) + L"\nvmIdleTimeout=" + std::to_wstring(Default.vmIdleTimeout.value_or(2000)) +
L"\n"
L"mountDeviceTimeout=120000\n"
L"kernelBootTimeout=120000\n"
L"debugConsoleLogFile=" +
EscapePath(kernelLogs) +
L"\n"
L"telemetry=false\n" +
boolOptionToString(L"safeMode", Default.safeMode, false) + boolOptionToString(L"guiApplications", Default.guiApplications, true) +
L"earlyBootLogging=false\n" + networkingModeToString(Default.networkingMode) + drvFsModeToString(Default.drvFsMode);
if (Default.kernel.has_value())
{
newConfig += L"kernel=" + EscapePath(Default.kernel.value()) + L"\n";
}
if (Default.kernelCommandLine.has_value())
{
newConfig += L"kernelCommandLine=" + Default.kernelCommandLine.value() + L"\n";
}
if (Default.kernelModules.has_value())
{
newConfig += L"kernelModules=" + EscapePath(Default.kernelModules.value()) + L"\n";
}
if (Default.loadKernelModules.has_value())
{
newConfig += L"loadKernelModules=" + Default.loadKernelModules.value() + L"\n";
}
if (Default.loadDefaultKernelModules.has_value())
{
newConfig +=
L"loadDefaultKernelModules=" + std::wstring(Default.loadDefaultKernelModules.value() ? L"true" : L"false") + L"\n";
}
switch (Default.networkingMode.value_or(wsl::core::NetworkingMode::Nat))
{
case wsl::core::NetworkingMode::Nat:
{
if (Default.dnsProxy.has_value())
{
newConfig += boolOptionToString(L"dnsProxy", Default.dnsProxy, false);
}
if (Default.firewall.has_value())
{
newConfig += L"[experimental]\nfirewall=";
newConfig += *Default.firewall ? L"true" : L"false";
newConfig += L"\n[wsl2]\n";
}
break;
}
case wsl::core::NetworkingMode::Bridged:
{
VERIFY_IS_TRUE(Default.vmSwitch.has_value());
newConfig += L"vmSwitch=" + *Default.vmSwitch;
if (Default.macAddress.has_value())
{
newConfig += L"\nmacAddress=" + *Default.macAddress;
}
newConfig += L"\nipv6=" + std::wstring(Default.ipv6 ? L"true" : L"false");
newConfig += L"\n";
break;
}
}
if (Default.dnsTunneling.has_value())
{
newConfig += L"\n[experimental]\n";
newConfig += boolOptionToString(L"dnsTunneling", Default.dnsTunneling, false);
newConfig += L"[wsl2]\n";
}
if (Default.dnsTunnelingIpAddress.has_value())
{
newConfig += L"\n[experimental]\n";
newConfig += L"dnsTunnelingIpAddress=" + Default.dnsTunnelingIpAddress.value() + L"\n";
newConfig += L"[wsl2]\n";
}
// always add this regardless if it has value, want to have it disabled by default for tests
newConfig += L"\n[experimental]\n";
newConfig += boolOptionToString(L"autoProxy", Default.autoProxy, false);
newConfig += L"[wsl2]\n";
if (Default.sparse.has_value())
{
std::wstring value = Default.sparse.value() ? L"true" : L"false";
newConfig += L"[experimental]\nsparseVhd=" + value + L"\n[wsl2]";
}
if (Default.hostAddressLoopback.has_value())
{
newConfig += L"\n[experimental]\n";
newConfig += boolOptionToString(L"hostAddressLoopback", Default.hostAddressLoopback, false);
newConfig += L"[wsl2]\n";
}
// TODO: Remove once SetVersion() truncated archive error is root caused.
newConfig += L"\n[experimental]\nSetVersionDebug=true\n[wsl2]\n";
return newConfig;
}
std::wstring EscapePath(std::wstring_view Path)
{
std::wstring escaped;
for (const auto e : Path)
{
escaped += e;
if (e == L'\\')
{
escaped += e;
}
}
return escaped;
}
NTSTATUS
LxsstuParseLinuxLogFiles(__in PCWSTR LogFileName, __out PBOOL TestPassed)
/*++
Routine Description:
Parses the output of the linux test and relogs the output.
Arguments:
LogFileName - Supplies a string containing the log files for the test
separated by LXSS_TEST_LOG_SEPARATOR_CHAR.
TestPassed - Supplies a buffer to receive a boolean value specifying if the
tests completed without errors.
Return Value:
NTSTATUS
--*/
{
HANDLE LinuxLogFile;
WCHAR LinuxLogPath[MAX_PATH];
WCHAR LocalLogFileBuffer[MAX_PATH];
PWCHAR LogFileToken;
DWORD PrintStatus;
NTSTATUS Status;
std::wstring TestDirectory;
LXSS_TEST_LAUNCHER_TEST TestRecord;
PWCHAR TokenState;
LinuxLogFile = INVALID_HANDLE_VALUE;
Status = STATUS_UNSUCCESSFUL;
*TestPassed = FALSE;
RtlZeroMemory(&TestRecord, sizeof(TestRecord));
//
// Make a copy of the log file name so wcstok can modify it.
//
PrintStatus = swprintf_s(LocalLogFileBuffer, RTL_NUMBER_OF(LocalLogFileBuffer), L"%s", LogFileName);
if (PrintStatus == -1)
{
Status = STATUS_UNSUCCESSFUL;
LogError("Increase LocalLogFileBuffer buffer");
goto ErrorExit;
}
//
// Get the test directory.
//
TestDirectory = LxsstuGetTestDirectory();
//
// Parse the logs for the test and determine how many passes / errors there
// were.
//
LogFileToken = wcstok(LocalLogFileBuffer, LXSS_TEST_LOG_SEPARATOR_CHAR, &TokenState);
while (LogFileToken != NULL)
{
LogInfo("LOGFILE: %s", LogFileToken);
PrintStatus = swprintf_s(LinuxLogPath, RTL_NUMBER_OF(LinuxLogPath), L"%s\\log\\%s", TestDirectory.c_str(), LogFileToken);
if (PrintStatus == -1)
{
Status = STATUS_UNSUCCESSFUL;
LogError("Increase LinuxLogPath buffer");
goto ErrorExit;
}
//
// For VM Mode, copy the output file out of the ext4 volume so it can
// be read.
//
if (LxsstuVmMode())
{
std::wstring Command = std::format(L"/bin/cp /data/test/log/{} $(wslpath '{}')", LogFileToken, LinuxLogPath);
VERIFY_NO_THROW(LxsstuRunTest(Command.c_str()));
}
LinuxLogFile =
CreateFileW(LinuxLogPath, GENERIC_READ, (FILE_SHARE_READ | FILE_SHARE_WRITE), NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (LinuxLogFile == INVALID_HANDLE_VALUE)
{
Status = STATUS_UNSUCCESSFUL;
LogError("Could not open {:%s:} after running test, LastError %#x", LinuxLogPath, GetLastError());
goto ErrorExit;
}
Status = LxsstuParseLogFile(LinuxLogFile, &TestRecord);
if (!NT_SUCCESS(Status))
{
goto ErrorExit;
}
if (TestRecord.NumberOfErrors > 0)
{
LogError("LOG FILE SUMMARY: %s - PASSED: %u ERRORS: %u", LogFileToken, TestRecord.NumberOfPasses, TestRecord.NumberOfErrors);
}
else if (TestRecord.NumberOfPasses > 0)
{
LogPass("LOG FILE SUMMARY: %s - PASSED: %u ERRORS: %u", LogFileToken, TestRecord.NumberOfPasses, TestRecord.NumberOfErrors);
}
else
{
LogError("LOG FILE SUMMARY: %s - log had no passes or errors, ensure test was actually run", LogFileToken);
}
CloseHandle(LinuxLogFile);
LinuxLogFile = INVALID_HANDLE_VALUE;
LogFileToken = wcstok(NULL, LXSS_TEST_LOG_SEPARATOR_CHAR, &TokenState);
}
Status = STATUS_SUCCESS;
ErrorExit:
if (LinuxLogFile != INVALID_HANDLE_VALUE)
{
CloseHandle(LinuxLogFile);
}
if ((TestRecord.NumberOfErrors == 0) && (TestRecord.NumberOfPasses > 0))
{
*TestPassed = TRUE;
}
return Status;
}
NTSTATUS
LxsstuParseLogFile(__in HANDLE FileHandle, __in PLXSS_TEST_LAUNCHER_TEST TestRecord)
/*++
Routine Description:
Parses a single log file.
Arguments:
TestName - Name of the test.
LogFileName - string containing the log files for the test separated by
LXSS_TEST_LOG_SEPARATOR_CHAR.
Return Value:
NTSTATUS
--*/
{
PBYTE Buffer;
DWORD BytesRead;
DWORD FileSize;
DWORD FileSizeHigh;
PCHAR Message;
LXSS_TEST_LAUNCHER_MESSAGE_TYPE MessageType;
NTSTATUS Status;
PCHAR Token;
Buffer = NULL;
Status = STATUS_UNSUCCESSFUL;
FileSize = GetFileSize(FileHandle, &FileSizeHigh);
Buffer = (PBYTE)ALLOC(FileSize + 1);
if (Buffer == NULL)
{
goto ErrorExit;
}
Buffer[FileSize] = '\0';
do
{
RtlZeroMemory(Buffer, FileSize);
if (ReadFile(FileHandle, Buffer, FileSize, &BytesRead, NULL) == FALSE)
{
Status = STATUS_UNSUCCESSFUL;
LogError("ReadFile failed, LastError %#x", GetLastError());
goto ErrorExit;
}
if (BytesRead == 0)
{
break;
}
//
// Parse the log line-by-line.
//
Token = strtok((PCHAR)Buffer, "\n");
while (Token != NULL)
{
//
// A well-formed message begins with a timestamp and then is either
// a start, info, error, or pass message. For example:
// [12:30:05.432] ERROR: Something went wrong!
//
// Anything that does not fit this format is re-logged an an "info"
// message.
//
MessageType = LogInfoMessage;
if (Token[0] == '[')
{
Message = strchr(Token, ' ');
if ((Message == NULL) || (strlen(Message) < 2))
{
break;
}
switch (Message[1])
{
case 'E':
case 'R':
MessageType = LogErrorMessage;
break;
case 'P':
MessageType = LogPassMessage;
break;
}
}
switch (MessageType)
{
case LogInfoMessage:
if (g_RelogEverything != FALSE)
{
LogInfo("%S", Token);
}
break;
case LogErrorMessage:
TestRecord->NumberOfErrors += 1;
if (g_RelogEverything != FALSE)
{
LogError("%S", Token);
}
break;
case LogPassMessage:
TestRecord->NumberOfPasses += 1;
if (g_RelogEverything != FALSE)
{
LogPass("%S", Token);
}
break;
DEFAULT_UNREACHABLE;
}
Token = strtok(NULL, "\n");
}
} while (BytesRead > 0);
Status = STATUS_SUCCESS;
ErrorExit:
if (Buffer != NULL)
{
FREE(Buffer);
}
return Status;
}
VOID LxsstuRunTest(_In_ PCWSTR CommandLine, _In_opt_ PCWSTR LogFileName, _In_opt_ PCWSTR Username) noexcept(false)
/*++
Routine Description:
Run an individual test.
Arguments:
CommandLine - Command line path and arguments to pass
LogFileName - Name of the linux log file
Username - User to run the test as, if one is not supplied the test
will be run as root
Return Value:
None.
--*/
{
BOOL TestPassed;
std::wstring LaunchArguments{};
if (ARGUMENT_PRESENT(Username))
{
LaunchArguments += WSL_USER_ARG L" ";
LaunchArguments += Username;
LaunchArguments += L" ";
}
LaunchArguments += CommandLine;
DWORD ExitCode = LxsstuLaunchWsl(LaunchArguments.c_str());
LogInfo("Test process exited with: %lu", ExitCode);
//
// Parse the contents of the linux log(s) files and relog.
//
if (ARGUMENT_PRESENT(LogFileName))
{
THROW_IF_NTSTATUS_FAILED(LxsstuParseLinuxLogFiles(LogFileName, &TestPassed));
VERIFY_IS_TRUE(TestPassed);
}
VERIFY_ARE_EQUAL(0, ExitCode);
return;
}
bool ModuleSetup(VOID)
/*++
Routine Description:
Configures the machine to run tests
Arguments:
Return Value:
None.
--*/
{
wsl::windows::common::wslutil::InitializeWil();
// Don't crash for unknown exceptions (makes debugging testpasses harder)
#ifndef _DEBUG
wil::g_fResultFailFastUnknownExceptions = false;
#endif
WslTraceLoggingInitialize(LxssTelemetryProvider, true);
wsl::windows::common::EnableContextualizedErrors(false);
auto getOptionalTestParam = [](LPCWSTR Name) -> std::optional<std::wstring> {
WEX::Common::String Value;
WEX::TestExecution::RuntimeParameters::TryGetValue(Name, Value);
return Value.IsEmpty() ? std::optional<std::wstring>() : static_cast<LPCWSTR>(Value);
};
auto getTestParam = [&](LPCWSTR Name) -> std::wstring {
auto value = getOptionalTestParam(Name);
if (!value.has_value())
{
const std::wstring error = L"Missing TE argument: " + std::wstring(Name);
VERIFY_FAIL(error.c_str());
}
return value.value();
};
try
{
const auto buildString = wsl::windows::common::registry::ReadString(
HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", L"BuildLabEx");
LogInfo("OS build string: %ls", buildString.c_str());
}
CATCH_LOG();
try
{
const auto userKey = wsl::windows::common::registry::OpenLxssUserKey();
g_originalDefaultDistro = wsl::windows::common::registry::ReadString(userKey.get(), nullptr, L"DefaultDistribution", L"");
}
CATCH_LOG();
g_originalConfig = LxssWriteWslConfig(LxssGenerateTestConfig());
const auto redirectStdout = getOptionalTestParam(L"RedirectStdout");
const auto redirectStderr = getOptionalTestParam(L"RedirectStderr");
if (redirectStdout.has_value())
{
g_OriginalStdout = LxssRedirectOutput(STD_OUTPUT_HANDLE, redirectStdout.value());
}
if (redirectStderr.has_value())
{
g_OriginalStderr = LxssRedirectOutput(STD_ERROR_HANDLE, redirectStderr.value());
}
g_dumpFolder = getOptionalTestParam(L"DumpFolder").value_or(L".");
g_dumpToolPath = getOptionalTestParam(L"DumpTool");
g_pipelineBuildId = getOptionalTestParam(L"PipelineBuildId").value_or(L"");
if (!g_pipelineBuildId.empty())
{
LogInfo("Pipeline build id: %ls", g_pipelineBuildId.c_str());
}
WEX::TestExecution::RuntimeParameters::TryGetValue(L"WerReport", g_enableWerReport);
WEX::TestExecution::RuntimeParameters::TryGetValue(L"LogDmesg", g_LogDmesgAfterEachTest);
g_WatchdogTimer = CreateThreadpoolTimer(LxsstuWatchdogTimer, nullptr, nullptr);
VERIFY_IS_NOT_NULL(g_WatchdogTimer);
ULARGE_INTEGER fileTimeConvert{};
fileTimeConvert.QuadPart = LXSS_WATCHDOG_TIMEOUT;
fileTimeConvert.QuadPart *= (-1 * 1000 * 10i64); // fileTime is unsigned- took out -1; check if this causes errors later
FILETIME DueTime{};
DueTime.dwLowDateTime = fileTimeConvert.LowPart;
DueTime.dwHighDateTime = fileTimeConvert.HighPart;
SetThreadpoolTimer(g_WatchdogTimer, &DueTime, 0, LXSS_WATCHDOG_TIMEOUT_WINDOW);
const auto version = getTestParam(L"Version");
if (version == L"1")
{
g_VmMode = false;
}
else if (version == L"2")
{
g_VmMode = true;
}
else
{
LogError("Unexpected version: %ls", version.c_str());
VERIFY_FAIL();
}
g_testDistroPath = getTestParam(L"DistroPath");
g_testDataPath = getTestParam(L"TestDataPath");
const auto setupScript = getOptionalTestParam(L"SetupScript");
if (!setupScript.has_value())
{
// If no setup script is present, mark test_distro as the default distro here for convenience.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--set-default " LXSS_DISTRO_NAME_TEST_L), 0L);
g_fastTestRun = true;
return true;
}
std::wstring Cmd =
L"Powershell \
-NoProfile \
-ExecutionPolicy Bypass \
-Command \"" +
setupScript.value() + L" -Version '" + getTestParam(L"Version") + L"'" + L" -DistroPath " + g_testDistroPath +
L" -DistroName " + LXSS_DISTRO_NAME_TEST_L + L" -Package '" + getTestParam(L"Package") + L"'" + L" -UnitTestsPath " +
getOptionalTestParam(L"UnitTestsPath").value_or(L"$null");
if (getOptionalTestParam(L"AllowUnsigned") == L"1")
{
Cmd += L" -AllowUnsigned";
}
Cmd += +L"\"";
LogInfo("Running test setup command: %ls", Cmd.c_str());
const auto ExitCode = LxsstuRunCommand(Cmd.data());
if (ExitCode != 0)
{
THROW_HR_MSG(E_FAIL, "Test setup returned non-zero exit code %lu", ExitCode);
}
return true;
}
bool ModuleCleanup(VOID)
/*++
Routine Description:
Called after the tests cases have been executed.
Reverts WSL version upgrades, if any.
Arguments:
None.
Return Value:
None.
--*/
{
LogInfo("Exiting UnitTests module");
//
// Release the watchdog timer.
//
if (g_WatchdogTimer != NULL)
{
SetThreadpoolTimer(g_WatchdogTimer, nullptr, 0, 0);
WaitForThreadpoolTimerCallbacks(g_WatchdogTimer, true);
CloseThreadpoolTimer(g_WatchdogTimer);
}
// Save the Appx & defender logs in the dump folder
if (!g_pipelineBuildId.empty())
{
auto commandLine = std::format(L"Get-AppPackageLog -All > \"{}\\appx-logs.txt\"", g_dumpFolder);
LxsstuLaunchPowershellAndCaptureOutput(commandLine.data());
commandLine = std::format(L"Get-MpThreatDetection > \"{}\\Get-MpThreatDetection.txt\"", g_dumpFolder);
LxsstuLaunchPowershellAndCaptureOutput(commandLine.data());
commandLine = std::format(L"Get-MpThreat > \"{}\\Get-MpThreat.txt\"", g_dumpFolder);
LxsstuLaunchPowershellAndCaptureOutput(commandLine.data());
commandLine = std::format(L"Get-MpPreference > \"{}\\Get-MpPreference.txt\"", g_dumpFolder);
LxsstuLaunchPowershellAndCaptureOutput(commandLine.data());
}
if (!g_originalConfig.empty())
{
LogInfo("Restoring .wslconfig");
LxssWriteWslConfig(g_originalConfig);
}
if (!g_originalDefaultDistro.empty())
{
// Edge case: If the previous default distro was the test distro, it might have been deleted during the testpass.
// Validate the distro exists before restoring.
const auto userKey = wsl::windows::common::registry::OpenLxssUserKey();
try
{
wsl::windows::common::registry::OpenKey(userKey.get(), g_originalDefaultDistro.c_str(), KEY_READ);
}
catch (...)
{
LogInfo("Previous default distro doesn't exist anymore: '%ls', skipping restore", g_originalDefaultDistro.c_str());
return true;
}
LogInfo("Restoring default distro: '%ls", g_originalDefaultDistro.c_str());
wsl::windows::common::registry::WriteString(userKey.get(), nullptr, L"DefaultDistribution", g_originalDefaultDistro.c_str());
}
WslTraceLoggingUninitialize();
return true;
}
HANDLE
LxssRedirectOutput(_In_ DWORD Stream, _In_ const std::wstring& File)
/*++
Routine Description:
Redirect a standard stream to a file
Arguments:
Stream - The stream to redirect
File - The file to redirect the output to
Return Value:
None.
--*/
{
const HANDLE OriginalHandle = GetStdHandle(Stream);
SECURITY_ATTRIBUTES Attributes = {0};
Attributes.nLength = sizeof(Attributes);
Attributes.bInheritHandle = true;
const auto Handle =
CreateFileW(File.c_str(), FILE_APPEND_DATA, FILE_SHARE_READ, &Attributes, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
VERIFY_IS_NOT_NULL(Handle);
VERIFY_IS_TRUE(SetStdHandle(Stream, Handle));
return OriginalHandle;
}
void CreateUser(_In_ const std::wstring& Username, _Out_ PULONG Uid, _Out_ PULONG Gid)
{
//
// Create the user account.
//
// N.B. The user may already exist if the test was run previously.
//
std::wstring CreateUser{L"/usr/sbin/adduser --quiet --force-badname --disabled-password --gecos \"\" "};
CreateUser += Username.c_str();
LxsstuLaunchWsl(CreateUser.c_str());
//
// Create an unnamed pipe to read the output of the launched commands.
//
wil::unique_handle ReadPipe;
wil::unique_handle WritePipe;
THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&ReadPipe, &WritePipe, NULL, 0));
//
// Mark the write end of the pipe as inheritable.
//
THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(WritePipe.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
//
// Query the UID.
//
std::wstring QueryUid{L"/usr/bin/id -u "};
QueryUid += Username.c_str();
THROW_HR_IF(E_UNEXPECTED, (LxsstuLaunchWsl(QueryUid.c_str(), nullptr, WritePipe.get()) != 0));
CHAR Buffer[64];
DWORD BytesRead;
THROW_IF_WIN32_BOOL_FALSE(ReadFile(ReadPipe.get(), Buffer, (sizeof(Buffer) - 1), &BytesRead, NULL));
Buffer[BytesRead] = ANSI_NULL;
const ULONG UidLocal = std::stoul(Buffer, nullptr, 10);
//
// Query the GID.
//
std::wstring QueryGid{L"/usr/bin/id -g "};
QueryGid += Username.c_str();
THROW_HR_IF(E_UNEXPECTED, (LxsstuLaunchWsl(QueryGid.c_str(), nullptr, WritePipe.get()) != 0));
THROW_IF_WIN32_BOOL_FALSE(ReadFile(ReadPipe.get(), Buffer, (sizeof(Buffer) - 1), &BytesRead, NULL));
Buffer[BytesRead] = ANSI_NULL;
const ULONG GidLocal = std::stoul(Buffer, nullptr, 10);
//
// Return the queried values to the user.
//
*Uid = UidLocal;
*Gid = GidLocal;
}
std::pair<HANDLE, HANDLE> UseOriginalStdHandles(VOID)
/*++
Routine Description:
Restores the original stdout & stderr handles, if any.
Arguments:
None.
Return Value:
A pair of the previous stdout & stderr handles.
--*/
{
HANDLE PreviousStdout = GetStdHandle(STD_OUTPUT_HANDLE);
HANDLE PreviousStderr = GetStdHandle(STD_ERROR_HANDLE);
if (g_OriginalStdout != nullptr)
{
VERIFY_IS_TRUE(SetStdHandle(STD_OUTPUT_HANDLE, g_OriginalStdout));
}
if (g_OriginalStderr != nullptr)
{
VERIFY_IS_TRUE(SetStdHandle(STD_ERROR_HANDLE, g_OriginalStderr));
}
return {PreviousStdout, PreviousStderr};
}
void RestoreTestStdHandles(_In_ const std::pair<HANDLE, HANDLE>& handles)
/*++
Routine Description:
Assign stdout & stderr handles.
Arguments:
None.
Return Value:
None.
--*/
{
VERIFY_IS_TRUE(SetStdHandle(STD_OUTPUT_HANDLE, handles.first));
VERIFY_IS_TRUE(SetStdHandle(STD_ERROR_HANDLE, handles.second));
}
bool TryLoadDnsResolverMethods() noexcept
{
constexpr auto c_dnsModuleName = L"dnsapi.dll";
const wil::shared_hmodule dnsModule{LoadLibraryEx(c_dnsModuleName, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)};
if (!dnsModule)
{
return false;
}
try
{
// attempt to find the functions for the DNS tunneling OS APIs.
static LxssDynamicFunction<decltype(DnsQueryRaw)> dnsQueryRaw{dnsModule, "DnsQueryRaw"};
static LxssDynamicFunction<decltype(DnsCancelQueryRaw)> dnsCancelQueryRaw{dnsModule, "DnsCancelQueryRaw"};
static LxssDynamicFunction<decltype(DnsQueryRawResultFree)> dnsQueryRawResultFree{dnsModule, "DnsQueryRawResultFree"};
// Make a dummy call to the DNS APIs to verify if they are working. The APIs are going to be present
// on older OS versions, where they can be turned on/off using a KIR. If the KIR is turned off, the APIs
// will be unusable and will return ERROR_CALL_NOT_IMPLEMENTED.
THROW_HR_IF(E_NOTIMPL, dnsQueryRaw(nullptr, nullptr) == ERROR_CALL_NOT_IMPLEMENTED);
}
catch (...)
{
return false;
}
return true;
}
bool AreExperimentalNetworkingFeaturesSupported()
{
constexpr auto NETWORKING_EXPERIMENTAL_FLOOR_BUILD = 25885;
constexpr auto GALLIUM_FLOOR_BUILD = 25846;
const auto build = wsl::windows::common::helpers::GetWindowsVersion();
return ((build.BuildNumber < GALLIUM_FLOOR_BUILD) || (build.BuildNumber >= GALLIUM_FLOOR_BUILD && build.BuildNumber >= NETWORKING_EXPERIMENTAL_FLOOR_BUILD));
}
bool IsHyperVFirewallSupported() noexcept
{
try
{
// Query for the Hyper-V Firewall profile object. If this object is successfully queried, then
// the OS has the necessary Hyper-V firewall support.
LxsstuLaunchPowershellAndCaptureOutput(L"Get-NetFirewallHyperVProfile");
}
catch (...)
{
return false;
}
return true;
}
std::optional<GUID> GetDistributionId(LPCWSTR Name)
{
// Get the GUID of the test distro
wsl::windows::common::SvcComm service;
for (const auto& e : service.EnumerateDistributions())
{
if (wsl::shared::string::IsEqual(e.DistroName, Name))
{
return e.DistroGuid;
}
}
return {};
}
wil::unique_hkey OpenDistributionKey(LPCWSTR Name)
{
const auto id = GetDistributionId(Name);
if (!id.has_value())
{
return {};
}
const auto idString = wsl::shared::string::GuidToString<wchar_t>(id.value());
const auto userKey = wsl::windows::common::registry::OpenLxssUserKey();
return wsl::windows::common::registry::OpenKey(userKey.get(), idString.c_str(), KEY_ALL_ACCESS);
}
bool WslShutdown()
{
return VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(WSL_SHUTDOWN_ARG));
}
void TerminateDistribution(LPCWSTR DistributionName)
{
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(std::format(L"{} {}", WSL_TERMINATE_ARG, DistributionName)));
}
void ValidateOutput(LPCWSTR CommandLine, const std::wstring& ExpectedOutput, const std::wstring& ExpectedWarnings, int ExitCode)
{
auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(CommandLine, ExitCode);
VERIFY_ARE_EQUAL(ExpectedOutput, output);
VERIFY_ARE_EQUAL(ExpectedWarnings, warnings);
}
// Trim helper method
void Trim(std::wstring& string)
{
// Remove any extra chars (lf, spaces, ...)
std::erase_if(string, [](auto c) { return !isalnum(c); });
}
ScopedEnvVariable::ScopedEnvVariable(const std::wstring& Name, const std::wstring& Value) : m_name(Name)
{
VERIFY_IS_TRUE(SetEnvironmentVariable(Name.c_str(), Value.c_str()));
}
ScopedEnvVariable::~ScopedEnvVariable()
{
VERIFY_IS_TRUE(SetEnvironmentVariable(m_name.c_str(), nullptr));
}
UniqueWebServer::UniqueWebServer(LPCWSTR Endpoint, LPCWSTR Content)
{
auto cmd = std::format(
LR"(Powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "
$ErrorActionPreference = 'Stop'
$server = New-Object System.Net.HttpListener
$server.Prefixes.Add('{}')
$server.Start()
while ($true)
{{
$context = $server.GetContext()
$context.Response.StatusCode
$content = [Text.Encoding]::UTF8.GetBytes('{}')
$context.Response.OutputStream.Write($content , 0, $content.length)
$context.Response.close()
}}")",
Endpoint,
Content);
m_process = LxsstuStartProcess(cmd.data());
}
UniqueWebServer::UniqueWebServer(LPCWSTR Endpoint, const std::filesystem::path& File)
{
auto cmd = std::format(
LR"(Powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "
$ErrorActionPreference = 'Stop'
$server = New-Object System.Net.HttpListener
$server.Prefixes.Add('{}')
$server.Start()
while ($true)
{{
$context = $server.GetContext()
$context.Response.StatusCode
$content = [System.IO.File]::ReadAllBytes('{}')
$context.Response.ContentLength64 = $content.length
$context.Response.ContentType = 'application/octet-stream'
$context.Response.OutputStream.Write($content, 0, $content.length)
$context.Response.close()
}}")",
Endpoint,
File.wstring());
m_process = LxsstuStartProcess(cmd.data());
}
UniqueWebServer::~UniqueWebServer()
{
if (!TerminateProcess(m_process.get(), 0))
{
LogError("TerminateProcess failed, %lu", GetLastError());
}
}
DistroFileChange::DistroFileChange(LPCWSTR Path, bool exists) : m_path(Path)
{
if (exists)
{
m_originalContent = LxsstuLaunchWslAndCaptureOutput(std::format(L"cat '{}'", m_path)).first;
}
}
DistroFileChange::~DistroFileChange()
{
if (m_originalContent.has_value())
{
SetContent(m_originalContent->c_str());
}
else
{
Delete();
}
}
void DistroFileChange::SetContent(LPCWSTR Content)
{
const auto cmd = LxssGenerateWslCommandLine(std::format(L" -u root cat > '{}'", m_path).c_str());
wsl::windows::common::SubProcess process(nullptr, cmd.c_str());
auto [read, write] = CreateSubprocessPipe(true, false);
process.SetStdHandles(read.get(), nullptr, nullptr);
const auto processHandle = process.Start();
const auto utf8content = wsl::shared::string::WideToMultiByte(Content);
auto index = 0;
while (index < utf8content.size())
{
DWORD written{};
VERIFY_IS_TRUE(WriteFile(write.get(), utf8content.data() + index, static_cast<DWORD>(utf8content.size() - index), &written, nullptr));
index += written;
}
write.reset();
VERIFY_ARE_EQUAL(wsl::windows::common::SubProcess::GetExitCode(processHandle.get()), 0L);
}
void DistroFileChange::Delete()
{
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-u root rm -f '{}'", m_path).c_str()), 0L);
}
std::string ReadToString(SOCKET Handle)
{
std::string output;
DWORD offset = 0;
while (true) // TODO: timeout
{
constexpr auto bufferSize = 512;
output.resize(output.size() + bufferSize);
int bytesRead = 0;
if ((bytesRead = recv(Handle, &output[offset], bufferSize, 0)) < 0)
{
LogError("recv failed with %lu", GetLastError());
VERIFY_FAIL();
}
if (bytesRead == 0)
{
output.resize(offset);
break;
}
output.resize(offset + bytesRead);
offset += bytesRead;
}
return output;
}
std::string ReadToString(HANDLE Handle)
{
std::string output;
DWORD offset = 0;
constexpr DWORD bufferSize = 4096;
while (true)
{
output.resize(offset + bufferSize);
DWORD bytesRead = 0;
if (!ReadFile(Handle, output.data() + offset, bufferSize, &bytesRead, nullptr))
{
VERIFY_ARE_EQUAL(GetLastError(), ERROR_BROKEN_PIPE);
}
offset += bytesRead;
output.resize(offset);
if (bytesRead == 0)
{
break;
}
}
return output;
}
void VerifyPatternMatch(const std::string& Content, const std::string& Pattern)
{
if (!PathMatchSpecA(Content.c_str(), Pattern.c_str()))
{
std::wstring message = std::format(L"Output: '{}' didn't match pattern: '{}'", Content, Pattern);
VERIFY_FAIL(message.c_str());
}
}
std::string EscapeString(const std::string& Input)
{
std::string Output;
for (const auto& e : Input)
{
if (e == '\n')
{
Output += "\\n";
}
else if (e == '\r')
{
Output += "\\r";
}
else if (e == '\0')
{
Output += "\\0";
}
else if (e == '\t')
{
Output += "\\t";
}
else if (e == '\x1b') // ESC character - start of VT sequence
{
Output += "\\x1b";
}
else
{
Output += e;
}
}
return Output;
}
PartialHandleRead::PartialHandleRead(HANDLE Handle) : m_handle(Handle)
{
m_thread = std::thread(std::bind(&PartialHandleRead::Run, this));
}
PartialHandleRead::~PartialHandleRead()
{
m_exitEvent.SetEvent();
if (m_thread.joinable())
{
m_thread.join();
}
}
std::string PartialHandleRead::ReadBytes(size_t Length)
{
wsl::shared::retry::RetryWithTimeout<void>(
[&]() {
std::lock_guard lock{m_mutex};
THROW_HR_IF(E_ABORT, m_data.size() < Length);
},
std::chrono::milliseconds(100),
std::chrono::seconds(60));
std::lock_guard lock{m_mutex};
return m_data.substr(0, Length);
}
std::string PartialHandleRead::ConsumeBytes(size_t Length)
{
wsl::shared::retry::RetryWithTimeout<void>(
[&]() {
std::lock_guard lock{m_mutex};
THROW_HR_IF(E_ABORT, m_data.size() < Length);
},
std::chrono::milliseconds(100),
std::chrono::seconds(60));
std::lock_guard lock{m_mutex};
std::string result = m_data.substr(0, Length);
m_data.erase(0, Length);
return result;
}
std::string PartialHandleRead::GetData() const
{
std::lock_guard lock{m_mutex};
return m_data;
}
void PartialHandleRead::Expect(const std::string& Expected)
{
auto content = ReadBytes(Expected.size());
VERIFY_ARE_EQUAL(content, Expected);
}
void PartialHandleRead::ExpectConsume(const std::string& Expected)
{
auto content = ConsumeBytes(Expected.size());
if (content != Expected)
{
VERIFY_FAIL(std::format(
L"Expected: '{}' but got: '{}'",
wsl::shared::string::MultiByteToWide(EscapeString(Expected)),
wsl::shared::string::MultiByteToWide(EscapeString(content)))
.c_str());
}
}
void PartialHandleRead::ExpectClosed(DWORD Timeout)
{
VERIFY_ARE_EQUAL(WaitForSingleObject(m_thread.native_handle(), Timeout), WAIT_OBJECT_0);
}
void PartialHandleRead::Run()
try
{
std::vector<gsl::byte> buffer(4096);
while (!m_exitEvent.is_signaled())
{
auto bytesRead = wsl::windows::common::relay::InterruptableRead(m_handle, gsl::make_span(buffer), {m_exitEvent.get()});
if (bytesRead == 0)
{
break;
}
std::lock_guard lock{m_mutex};
m_data.append(reinterpret_cast<char*>(buffer.data()), bytesRead);
}
}
CATCH_LOG();
class ReadHandleWithTargetValue : public wsl::windows::common::relay::ReadHandle
{
public:
NON_COPYABLE(ReadHandleWithTargetValue);
NON_MOVABLE(ReadHandleWithTargetValue);
ReadHandleWithTargetValue(wsl::windows::common::relay::HandleWrapper&& MovedHandle, std::string_view targetValue) :
ReadHandle(std::move(MovedHandle), [this](const auto& buffer) { m_readBuffer.append(buffer.data(), buffer.size()); }),
m_targetValue(targetValue)
{
}
void Schedule() override
{
ReadHandle::Schedule();
CheckIfTargetFound();
}
void Collect() override
{
ReadHandle::Collect();
CheckIfTargetFound();
}
private:
void CheckIfTargetFound()
{
using namespace wsl::windows::common::relay;
if (State == IOHandleStatus::Standby || State == IOHandleStatus::Completed)
{
bool targetFound = (m_readBuffer.find(m_targetValue) != std::string::npos);
if (State == IOHandleStatus::Standby)
{
if (targetFound)
{
State = IOHandleStatus::Completed;
}
}
else
{
THROW_WIN32_IF(ERROR_NOT_FOUND, !targetFound);
}
}
}
std::string m_readBuffer;
std::string m_targetValue;
};
void WaitForOutput(wil::unique_handle handle, std::string_view targetValue, std::chrono::milliseconds timeout)
{
wsl::windows::common::relay::MultiHandleWait io;
io.AddHandle(std::make_unique<ReadHandleWithTargetValue>(std::move(handle), targetValue));
io.Run(timeout);
}
std::filesystem::path GetTestImagePath(std::string_view imageName)
{
std::filesystem::path result = std::filesystem::path{g_testDataPath};
if (imageName == "debian:latest")
{
result /= L"debian-latest.tar";
}
else if (imageName == "python:3.12-alpine")
{
result /= L"python-3_12-alpine.tar";
}
else if (imageName == "alpine:latest")
{
result /= L"alpine-latest.tar";
}
else if (imageName == "hello-world:latest")
{
result /= L"HelloWorldSaved.tar";
}
else
{
THROW_HR_MSG(E_INVALIDARG, "Unknown test image: %hs", imageName.data());
}
return result;
}
void ExpectHttpResponse(LPCWSTR Url, std::optional<int> expectedCode, bool retry)
{
const winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter filter;
filter.CacheControl().WriteBehavior(winrt::Windows::Web::Http::Filters::HttpCacheWriteBehavior::NoCache);
const winrt::Windows::Web::Http::HttpClient client(filter);
const auto sendRequest = [&]() {
try
{
LogInfo("Sending request to: %ls", Url);
auto response = client.GetAsync(winrt::Windows::Foundation::Uri(Url)).get();
auto content = response.Content().ReadAsStringAsync().get();
if (expectedCode.has_value())
{
VERIFY_ARE_EQUAL(static_cast<int>(response.StatusCode()), expectedCode.value());
}
else
{
LogError("Unexpected reply for: %ls", Url);
VERIFY_FAIL();
}
}
catch (...)
{
auto result = wil::ResultFromCaughtException();
if (!expectedCode.has_value())
{
// We currently reset the connection if connect() fails inside
// the VM. Consider failing the Windows connect() instead.
VERIFY_ARE_EQUAL(result, HRESULT_FROM_WIN32(WININET_E_INVALID_SERVER_RESPONSE));
return;
}
// Throw so RetryWithTimeout can decide whether to retry.
THROW_HR(result);
}
};
if (retry)
{
wsl::shared::retry::RetryWithTimeout<void>(sendRequest, std::chrono::milliseconds(500), std::chrono::seconds(30), [&]() {
return wil::ResultFromCaughtException() == HRESULT_FROM_WIN32(WININET_E_INVALID_SERVER_RESPONSE);
});
}
else
{
sendRequest();
}
}