2025-05-15 12:09:45 -07:00
/*++
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"
2026-07-02 10:08:57 -07:00
#include "WslCoreNetworkEndpointSettings.h"
2025-05-15 12:09:45 -07:00
#include <tlhelp32.h>
#include <werapi.h>
#include <Dbghelp.h>
2026-04-30 13:34:43 -07:00
#include <winsafer.h>
2025-05-15 12:09:45 -07:00
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 ;
2026-05-08 10:51:37 -07:00
2025-05-15 12:09:45 -07:00
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 ;
2026-08-06 17:53:15 -07:00
std :: wstring g_pipelineBuildId ;
2025-05-15 12:09:45 -07:00
std :: wstring g_testDistroPath ;
2026-04-30 13:34:43 -07:00
std :: wstring g_testDataPath ;
bool g_fastTestRun = false ; // True when test.bat was invoked with -f
static wil :: unique_mta_usage_cookie g_mtaCookie ;
2025-05-15 12:09:45 -07:00
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 ,
2026-03-24 19:22:36 -07:00
"Command \" %ls \" "
2025-05-15 12:09:45 -07:00
"returned unexpected exit code (%lu != %i). "
2026-03-24 19:22:36 -07:00
"Stdout: '%ls' "
2025-05-15 12:09:45 -07:00
"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 {
2026-04-30 13:34:43 -07:00
L "wsl.exe" ,
L "wslhost.exe" ,
L "wslrelay.exe" ,
L "wslservice.exe" ,
L "wslg.exe" ,
L "vmcompute.exe" ,
L "vmwp.exe" ,
L "wslcsession.exe" ,
L "wslc.exe" };
2025-05-15 12:09:45 -07:00
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 " \n Process: $pid (parent: $parent) "
echo - en "cmd: "
cat "/proc/$pid/cmdline" || true
echo - e " \n stat: "
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.
2025-07-23 18:19:00 -04:00
Delim - The delimiter to split the string on.
2025-05-15 12:09:45 -07:00
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 ;
}
2025-07-23 18:19:00 -04:00
// WslKeepAlive class definitions
2025-05-15 12:09:45 -07:00
2026-09-18 11:08:31 +08:00
WslKeepAlive :: WslKeepAlive ( HANDLE Token , const std :: wstring & DistroName ) : m_token ( Token ), m_distroName ( DistroName )
2025-05-15 12:09:45 -07:00
{
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" ;
2026-09-18 11:08:31 +08:00
const auto distroArgument = m_distroName . empty () ? L "" : std :: format ( L "-d {} " , m_distroName );
std :: wstring cmd = L "wsl.exe " + distroArgument + L "echo -n " + expectedOutput + L " && read -n 1 " ;
2025-05-15 12:09:45 -07:00
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 );
}
2026-09-18 00:39:19 +00:00
bool WaitForServiceState ( SC_HANDLE service , DWORD state , DWORD previousPid )
2025-05-15 12:09:45 -07:00
{
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 ());
}
2026-09-18 00:39:19 +00:00
return currentState == state ;
2025-05-15 12:09:45 -07:00
}
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 );
}
2026-09-18 00:39:19 +00:00
VERIFY_IS_TRUE ( WaitForServiceState ( service . get (), SERVICE_RUNNING , 0 ));
2025-05-15 12:09:45 -07:00
}
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 ());
}
2026-04-30 13:34:43 -07:00
wil :: unique_handle GetNonElevatedToken ( TOKEN_TYPE Type )
2025-05-15 12:09:45 -07:00
{
2026-04-30 13:34:43 -07:00
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 );
}
2025-05-15 12:09:45 -07:00
wil :: unique_handle nonElevatedToken ;
2026-04-30 13:34:43 -07:00
THROW_IF_WIN32_BOOL_FALSE ( DuplicateTokenEx ( token . get (), TOKEN_ALL_ACCESS , nullptr , SecurityImpersonation , Type , & nonElevatedToken ));
2025-05-15 12:09:45 -07:00
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 ());
}
}
2026-06-11 13:43:17 +08:00
HostFileChange :: HostFileChange ( const std :: filesystem :: path & Path , const std :: string & NewContent ) : m_path ( Path )
{
if ( std :: filesystem :: exists ( m_path ))
{
std :: ifstream file ( m_path , std :: ios :: binary );
THROW_HR_IF ( E_FAIL , ! file . is_open ());
std :: stringstream buffer ;
buffer << file . rdbuf ();
m_originalContent = buffer . str ();
}
Update ( NewContent );
}
HostFileChange ::~ HostFileChange ()
try
{
if ( m_originalContent . has_value ())
{
std :: filesystem :: create_directories ( m_path . parent_path ());
std :: ofstream file ( m_path , std :: ios :: binary | std :: ios :: trunc );
if ( file . is_open ())
{
file . write ( m_originalContent -> data (), static_cast < std :: streamsize > ( m_originalContent -> size ()));
}
}
else
{
std :: filesystem :: remove ( m_path );
}
}
CATCH_LOG ()
void HostFileChange :: Update ( const std :: string & NewContent ) const
{
std :: filesystem :: create_directories ( m_path . parent_path ());
std :: ofstream file ( m_path , std :: ios :: binary | std :: ios :: trunc );
THROW_HR_IF ( E_FAIL , ! file . is_open ());
file . write ( NewContent . data (), static_cast < std :: streamsize > ( NewContent . size ()));
THROW_HR_IF ( E_FAIL , ! file . good ());
}
2026-04-30 13:34:43 -07:00
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 ), {}};
}
2025-05-15 12:09:45 -07:00
// writes global WSL 2 config settings at %userprofile%/.wslconfig
std :: wstring LxssWriteWslConfig ( const std :: wstring & Content )
{
auto path = getenv ( "userprofile" ) + std :: string ( " \\ .wslconfig" );
2026-04-30 13:34:43 -07:00
auto previousContent = ReadFileContent ( path );
2025-05-15 12:09:45 -07:00
std :: wofstream config ( path );
VERIFY_IS_TRUE ( config . good ());
config << Content ;
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 " \n maxCrashDumpCount=" +
std :: to_wstring ( Default . crashDumpCount ) + L " \n vmIdleTimeout=" + std :: to_wstring ( Default . vmIdleTimeout . value_or ( 2000 )) +
L " \n "
L "mountDeviceTimeout=120000 \n "
L "kernelBootTimeout=120000 \n "
2026-09-14 16:04:02 +00:00
L "distributionStartTimeout=120000 \n "
2025-05-15 12:09:45 -07:00
L "debugConsoleLogFile=" +
2026-06-11 17:30:39 -07:00
EscapePath ( Default . debugConsoleLogFile . value_or ( kernelLogs )) +
2025-05-15 12:09:45 -07:00
L " \n "
L "telemetry=false \n " +
2026-03-30 11:13:10 -07:00
boolOptionToString ( L "safeMode" , Default . safeMode , false ) + boolOptionToString ( L "guiApplications" , Default . guiApplications , true ) +
2026-06-11 17:30:39 -07:00
boolOptionToString ( L "earlyBootLogging" , Default . earlyBootLogging , false ) +
networkingModeToString ( Default . networkingMode ) + drvFsModeToString ( Default . drvFsMode );
2025-05-15 12:09:45 -07:00
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 " ;
}
2026-05-13 14:59:35 -07:00
if ( Default . systemDistro . has_value ())
{
newConfig += L "systemDistro=" + EscapePath ( Default . systemDistro . value ()) + L " \n " ;
}
2025-05-15 12:09:45 -07:00
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] \n firewall=" ;
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 " \n macAddress=" + * Default . macAddress ;
}
newConfig += L " \n ipv6=" + 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] \n sparseVhd=" + 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 " ;
}
2026-07-22 15:06:32 +00:00
if ( Default . virtioFsAggregateShares . has_value ())
{
newConfig += L " \n [experimental] \n " ;
newConfig += boolOptionToString ( L "virtioFsAggregateShares" , Default . virtioFsAggregateShares , true );
newConfig += L "[wsl2] \n " ;
}
2025-05-15 12:09:45 -07:00
// TODO: Remove once SetVersion() truncated archive error is root caused.
newConfig += L " \n [experimental] \n SetVersionDebug=true \n [wsl2] \n " ;
2026-07-14 12:10:43 +08:00
if ( Default . isolateDistroCgroup . has_value ())
{
newConfig += boolOptionToString ( L "isolateDistroCgroup" , Default . isolateDistroCgroup , true );
}
2025-05-15 12:09:45 -07:00
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 ())
{
2026-01-29 09:16:54 -08:00
std :: wstring Command = std :: format ( L "/bin/cp /data/test/log/{} $(wslpath '{}')" , LogFileToken , LinuxLogPath );
2025-05-15 12:09:45 -07:00
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 ;
2025-12-01 21:17:28 -08:00
DWORD ExitCode = LxsstuLaunchWsl ( LaunchArguments . c_str ());
LogInfo ( "Test process exited with: %lu" , ExitCode );
2025-05-15 12:09:45 -07:00
//
// Parse the contents of the linux log(s) files and relog.
//
if ( ARGUMENT_PRESENT ( LogFileName ))
{
THROW_IF_NTSTATUS_FAILED ( LxsstuParseLinuxLogFiles ( LogFileName , & TestPassed ));
2025-12-01 21:17:28 -08:00
VERIFY_IS_TRUE ( TestPassed );
2025-05-15 12:09:45 -07:00
}
2025-12-01 21:17:28 -08:00
VERIFY_ARE_EQUAL ( 0 , ExitCode );
2025-05-15 12:09:45 -07:00
return ;
}
bool ModuleSetup ( VOID )
/*++
Routine Description:
Configures the machine to run tests
Arguments:
Return Value:
None.
--*/
{
2026-04-30 13:34:43 -07:00
wsl :: windows :: common :: wslutil :: InitializeWil ();
THROW_IF_FAILED ( CoIncrementMTAUsage ( & g_mtaCookie ));
2026-05-08 10:51:37 -07:00
// Assign a job object to the current process to ensure that we don't leak processes on failure.
2026-05-11 18:12:47 -07:00
// N.B. When the job object is closed, all processes associated with the job will be terminated.
// Because of that, we're purposefully leaking this job object so we don't kill the test process on cleanup.
auto job = CreateJobObjectW ( nullptr , nullptr );
THROW_LAST_ERROR_IF ( ! job );
2026-05-08 10:51:37 -07:00
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo {};
jobInfo . BasicLimitInformation . LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ;
2026-05-11 18:12:47 -07:00
THROW_IF_WIN32_BOOL_FALSE ( SetInformationJobObject ( job , JobObjectExtendedLimitInformation , & jobInfo , sizeof ( jobInfo )));
THROW_IF_WIN32_BOOL_FALSE ( AssignProcessToJobObject ( job , GetCurrentProcess ()));
2026-05-08 10:51:37 -07:00
2025-05-15 12:09:45 -07:00
// 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 * 10 i64 ); // 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" );
2026-04-30 13:34:43 -07:00
g_testDataPath = getTestParam ( L "TestDataPath" );
2025-05-15 12:09:45 -07:00
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 );
2026-04-30 13:34:43 -07:00
g_fastTestRun = true ;
2025-05-15 12:09:45 -07:00
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" );
2026-04-30 13:34:43 -07:00
auto cleanup = wil :: scope_exit_log ( WI_DIAGNOSTICS_INFO , [ & ] {
WslTraceLoggingUninitialize ();
g_mtaCookie . reset ();
});
2025-05-15 12:09:45 -07:00
//
// 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 ());
2025-09-10 19:36:25 -07:00
commandLine = std :: format ( L "Get-MpPreference > \" {} \\ Get-MpPreference.txt \" " , g_dumpFolder );
2025-05-15 12:09:45 -07:00
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.
2025-07-23 18:19:00 -04:00
// Validate the distro exists before restoring.
2025-05-15 12:09:45 -07:00
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 ());
}
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 ;
}
2026-10-03 18:50:56 +00:00
bool IsMirroredNetworkingSupported ()
{
// Windows Server can expose the Hyper-V firewall APIs without supporting mirrored networking.
return ! IsWindowsServer () && wsl :: windows :: common :: helpers :: IsWindows11OrAbove () &&
AreExperimentalNetworkingFeaturesSupported () && IsHyperVFirewallSupported ();
}
2025-05-15 12:09:45 -07:00
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 )));
}
2026-10-01 00:22:50 +00:00
void VerifyNoVmAccessToVhd ( LPCWSTR VhdPath )
{
PACL dacl {};
wil :: unique_hlocal descriptor ;
THROW_IF_WIN32_ERROR ( GetNamedSecurityInfoW ( VhdPath , SE_FILE_OBJECT , DACL_SECURITY_INFORMATION , nullptr , nullptr , & dacl , nullptr , & descriptor ));
VERIFY_IS_NOT_NULL ( dacl );
// VM-specific ACEs must be removed when the VHD is detached.
constexpr auto c_virtualMachineSidPrefix = L "S-1-5-83-1-" ;
for ( DWORD index = 0 ; index < dacl -> AceCount ; ++ index )
{
void * ace {};
THROW_IF_WIN32_BOOL_FALSE ( GetAce ( dacl , index , & ace ));
auto * allowed = static_cast < ACCESS_ALLOWED_ACE *> ( ace );
VERIFY_ARE_EQUAL ( ACCESS_ALLOWED_ACE_TYPE , allowed -> Header . AceType );
wil :: unique_hlocal_string sid ;
THROW_IF_WIN32_BOOL_FALSE ( ConvertSidToStringSidW ( & allowed -> SidStart , & sid ));
VERIFY_IS_FALSE ( std :: wstring_view ( sid . get ()). starts_with ( c_virtualMachineSidPrefix ));
}
}
2026-09-23 23:00:00 +00:00
std :: wstring GetBlockDeviceInWsl ( ULONGLONG SizeBytes )
{
// Wait for the disk to be attached.
const auto timeout = std :: chrono :: steady_clock :: now () + std :: chrono :: seconds ( 30 );
const auto expectedSize = std :: to_wstring ( SizeBytes );
bool done = false ;
while ( true )
{
for ( wchar_t name = 'a' ; name < 'z' ; name ++ )
{
std :: wstring cmd = L "-u root blockdev --getsize64 /dev/sd" ;
cmd += name ;
std :: wstring out ;
try
{
out = LxsstuLaunchWslAndCaptureOutput ( cmd . data ()). first ;
}
CATCH_LOG ()
Trim ( out );
if ( out == expectedSize )
{
return std :: wstring ( L "/dev/sd" ) + name ;
}
}
if ( done )
{
break ;
}
done = std :: chrono :: steady_clock :: now () > timeout ;
}
VERIFY_FAIL ( L "Failed to find the block device in WSL" );
// Unreachable.
return {};
}
2025-05-15 12:09:45 -07:00
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 ); });
}
2026-06-17 11:41:32 -07:00
static std :: optional < std :: wstring > CaptureEnvValue ( const std :: wstring & Name )
2025-05-15 12:09:45 -07:00
{
2026-06-17 11:41:32 -07:00
std :: wstring value ;
HRESULT hr = wil :: GetEnvironmentVariableW ( Name . c_str (), value );
if ( hr == HRESULT_FROM_WIN32 ( ERROR_ENVVAR_NOT_FOUND ))
{
return std :: nullopt ;
}
THROW_IF_FAILED ( hr );
return value ;
}
ScopedEnvVariable :: ScopedEnvVariable ( const std :: wstring & Name ) : m_name ( Name ), m_originalValue ( CaptureEnvValue ( Name ))
{
VERIFY_IS_TRUE ( SetEnvironmentVariableW ( Name . c_str (), nullptr ));
}
ScopedEnvVariable :: ScopedEnvVariable ( const std :: wstring & Name , const std :: wstring & Value ) :
m_name ( Name ), m_originalValue ( CaptureEnvValue ( Name ))
{
VERIFY_IS_TRUE ( SetEnvironmentVariableW ( Name . c_str (), Value . c_str ()));
2025-05-15 12:09:45 -07:00
}
ScopedEnvVariable ::~ ScopedEnvVariable ()
{
2026-06-17 11:41:32 -07:00
VERIFY_IS_TRUE ( SetEnvironmentVariableW ( m_name . c_str (), m_originalValue . has_value () ? m_originalValue -> c_str () : nullptr ));
}
void ScopedEnvVariable :: Set ( const std :: wstring & Value )
{
VERIFY_IS_TRUE ( SetEnvironmentVariableW ( m_name . c_str (), Value . c_str ()));
}
void ScopedEnvVariable :: Clear ()
{
VERIFY_IS_TRUE ( SetEnvironmentVariableW ( m_name . c_str (), nullptr ));
2025-05-15 12:09:45 -07:00
}
2026-07-28 09:12:15 -07:00
UniqueWebServer :: UniqueWebServer ( LPCWSTR Endpoint , LPCWSTR Content , UINT StatusCode )
2025-05-15 12:09:45 -07:00
{
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 ()
2026-07-28 09:12:15 -07:00
$ context . Response . StatusCode = {}
2025-05-15 12:09:45 -07:00
$ content = [ Text . Encoding ] :: UTF8 . GetBytes ( ' {} ' )
$ context . Response . OutputStream . Write ( $ content , 0 , $ content . length )
$ context . Response . close ()
}} ")" ,
Endpoint ,
2026-07-28 09:12:15 -07:00
StatusCode ,
2025-05-15 12:09:45 -07:00
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 ());
}
}
2026-10-06 16:53:24 +00:00
DistroFileChange :: DistroFileChange ( LPCWSTR Path , bool exists , LPCWSTR DistributionName ) :
m_path ( Path ), m_distributionName ( DistributionName )
2025-05-15 12:09:45 -07:00
{
if ( exists )
{
2026-10-06 16:53:24 +00:00
m_originalContent = LxsstuLaunchWslAndCaptureOutput ( std :: format ( L "-d {} -u root cat '{}'" , m_distributionName , m_path )). first ;
2025-05-15 12:09:45 -07:00
}
}
DistroFileChange ::~ DistroFileChange ()
{
if ( m_originalContent . has_value ())
{
SetContent ( m_originalContent -> c_str ());
}
else
{
Delete ();
}
}
void DistroFileChange :: SetContent ( LPCWSTR Content )
{
2026-10-06 16:53:24 +00:00
const auto cmd = LxssGenerateWslCommandLine ( std :: format ( L "-d {} -u root cat > '{}'" , m_distributionName , m_path ). c_str ());
2025-05-15 12:09:45 -07:00
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 ()
{
2026-10-06 16:53:24 +00:00
VERIFY_ARE_EQUAL ( LxsstuLaunchWsl ( std :: format ( L "-d {} -u root rm -f '{}'" , m_distributionName , m_path )), 0L );
2025-05-15 12:09:45 -07:00
}
2026-04-30 13:34:43 -07:00
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 ;
}
2026-05-11 20:32:12 -07:00
std :: pair < wil :: unique_socket , wil :: unique_socket > MakeSocketPair ()
{
wil :: unique_socket listenSocket { WSASocket ( AF_INET , SOCK_STREAM , IPPROTO_TCP , nullptr , 0 , WSA_FLAG_OVERLAPPED )};
THROW_LAST_ERROR_IF ( ! listenSocket );
sockaddr_in bindAddr {};
bindAddr . sin_family = AF_INET ;
bindAddr . sin_addr . s_addr = htonl ( INADDR_LOOPBACK );
bindAddr . sin_port = 0 ;
THROW_LAST_ERROR_IF ( bind ( listenSocket . get (), reinterpret_cast < sockaddr *> ( & bindAddr ), sizeof ( bindAddr )) == SOCKET_ERROR );
THROW_LAST_ERROR_IF ( listen ( listenSocket . get (), 1 ) == SOCKET_ERROR );
sockaddr_in boundAddr {};
int boundAddrLen = sizeof ( boundAddr );
THROW_LAST_ERROR_IF ( getsockname ( listenSocket . get (), reinterpret_cast < sockaddr *> ( & boundAddr ), & boundAddrLen ) == SOCKET_ERROR );
wil :: unique_socket clientSocket { WSASocket ( AF_INET , SOCK_STREAM , IPPROTO_TCP , nullptr , 0 , WSA_FLAG_OVERLAPPED )};
THROW_LAST_ERROR_IF ( ! clientSocket );
THROW_LAST_ERROR_IF ( connect ( clientSocket . get (), reinterpret_cast < sockaddr *> ( & boundAddr ), sizeof ( boundAddr )) == SOCKET_ERROR );
wil :: unique_socket serverSocket { accept ( listenSocket . get (), nullptr , nullptr )};
THROW_LAST_ERROR_IF ( ! serverSocket );
return { std :: move ( clientSocket ), std :: move ( serverSocket )};
}
2026-04-30 13:34:43 -07:00
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 ()
2026-08-26 18:41:05 +00:00
{
Stop ();
}
void PartialHandleRead :: Stop ()
2026-04-30 13:34:43 -07:00
{
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 ();
2026-05-14 12:46:55 -07:00
class ReadHandleWithTargetValue : public wsl :: windows :: common :: io :: ReadHandle
2026-04-30 13:34:43 -07:00
{
public :
NON_COPYABLE ( ReadHandleWithTargetValue );
NON_MOVABLE ( ReadHandleWithTargetValue );
2026-05-14 12:46:55 -07:00
ReadHandleWithTargetValue ( wsl :: windows :: common :: io :: HandleWrapper && MovedHandle , std :: string_view targetValue ) :
2026-04-30 13:34:43 -07:00
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 ()
{
2026-05-14 12:46:55 -07:00
using namespace wsl :: windows :: common :: io ;
2026-04-30 13:34:43 -07:00
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 ;
};
2026-08-26 18:41:05 +00:00
void WaitForOutput ( wsl :: windows :: common :: io :: HandleWrapper handle , std :: string_view targetValue , std :: chrono :: milliseconds timeout )
2026-04-30 13:34:43 -07:00
{
2026-05-14 12:46:55 -07:00
wsl :: windows :: common :: io :: MultiHandleWait io ;
2026-04-30 13:34:43 -07:00
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 if ( imageName == "wslc-registry:latest" )
{
result /= L "wslc-registry.tar" ;
}
2026-09-11 23:15:05 +00:00
else if ( imageName == "docker/dockerfile:1" )
{
result /= L "dockerfile-frontend.tar" ;
}
2026-04-30 13:34:43 -07:00
else
{
THROW_HR_MSG ( E_INVALIDARG , "Unknown test image: %hs" , imageName . data ());
}
return result ;
}
2026-05-15 17:08:41 -07:00
void LoadTestImage ( IWSLCSession & session , std :: string_view imageName )
{
std :: filesystem :: path imagePath = GetTestImagePath ( imageName );
wil :: unique_hfile imageFile {
CreateFileW ( imagePath . c_str (), GENERIC_READ , FILE_SHARE_READ , nullptr , OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , nullptr )};
THROW_LAST_ERROR_IF ( ! imageFile );
LARGE_INTEGER fileSize {};
THROW_LAST_ERROR_IF ( ! GetFileSizeEx ( imageFile . get (), & fileSize ));
2026-07-02 15:01:18 -07:00
THROW_IF_FAILED ( session . LoadImage ( wsl :: windows :: common :: wslutil :: ToCOMInputHandle ( imageFile . get ()), fileSize . QuadPart , nullptr , nullptr ));
2026-05-15 17:08:41 -07:00
}
2026-04-30 13:34:43 -07:00
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 ();
}
}
2026-07-02 10:08:57 -07:00
std :: optional < std :: wstring > GetHostAdapterIpv4 ()
2026-06-23 19:48:43 -07:00
{
2026-07-02 10:08:57 -07:00
auto endpoint = wsl :: core :: networking :: GetHostEndpointSettings ();
if ( ! endpoint || endpoint -> PreferredIpAddress . AddressString . empty ())
2026-06-23 19:48:43 -07:00
{
2026-07-02 10:08:57 -07:00
return {};
2026-06-23 19:48:43 -07:00
}
2026-07-02 10:08:57 -07:00
return endpoint -> PreferredIpAddress . AddressString ;
2026-06-23 19:48:43 -07:00
}
2026-04-30 13:34:43 -07:00
void SetPathAccess ( const std :: filesystem :: path & path , DWORD Permissions , ACCESS_MODE Mode )
{
auto [ everyoneSid , everyoneSidBuffer ] = wsl :: windows :: common :: security :: CreateSid ( SECURITY_WORLD_SID_AUTHORITY , SECURITY_WORLD_RID );
EXPLICIT_ACCESSW ea {};
ea . grfAccessPermissions = Permissions ;
ea . grfAccessMode = Mode ;
ea . grfInheritance = NO_INHERITANCE ;
ea . Trustee . TrusteeForm = TRUSTEE_IS_SID ;
ea . Trustee . ptstrName = static_cast < LPWSTR > ( everyoneSid );
PACL acl = nullptr ;
wil :: unique_hlocal descriptor ;
THROW_IF_WIN32_ERROR (
GetNamedSecurityInfoW ( path . c_str (), SE_FILE_OBJECT , DACL_SECURITY_INFORMATION , nullptr , nullptr , & acl , nullptr , & descriptor ));
wsl :: windows :: common :: security :: unique_acl newAcl ;
THROW_IF_WIN32_ERROR ( SetEntriesInAclW ( 1 , & ea , acl , & newAcl ));
THROW_IF_WIN32_ERROR ( SetNamedSecurityInfoW (
const_cast < LPWSTR > ( path . c_str ()), SE_FILE_OBJECT , DACL_SECURITY_INFORMATION , nullptr , nullptr , newAcl . get (), nullptr ));
}
2026-05-11 20:32:12 -07:00
void WriteSocket ( SOCKET Socket , const void * data , size_t size )
{
while ( size > 0 )
{
auto result = send ( Socket , static_cast < const char *> ( data ), gsl :: narrow_cast < int > ( size ), 0 );
VERIFY_IS_TRUE ( result > 0 );
size -= result ;
data = static_cast < const char *> ( data ) + result ;
}
}
2026-05-15 17:08:41 -07:00
void ValidateCOMErrorMessage ( const std :: optional < std :: wstring >& Expected , const std :: source_location & Source )
{
auto comError = wsl :: windows :: common :: wslutil :: GetCOMErrorInfo ();
if ( comError . has_value ())
{
if ( ! Expected . has_value ())
{
LogError ( "Unexpected COM error: '%ls'. Source: %hs" , comError -> Message . get (), std :: format ( "{}" , Source ). c_str ());
VERIFY_FAIL ();
}
VERIFY_ARE_EQUAL ( Expected . value (), comError -> Message . get ());
}
else
{
if ( Expected . has_value ())
{
LogError ( "Expected COM error: '%ls' but none was set. Source: %hs" , Expected -> c_str (), std :: format ( "{}" , Source ). c_str ());
VERIFY_FAIL ();
}
}
}
void ValidateCOMErrorMessageContains ( const std :: wstring & ExpectedSubstring )
{
auto comError = wsl :: windows :: common :: wslutil :: GetCOMErrorInfo ();
if ( comError . has_value ())
{
if ( ! comError -> Message )
{
LogError ( "Expected COM error containing: '%ls', but COM error message was null" , ExpectedSubstring . c_str ());
VERIFY_FAIL ();
}
if ( wcsstr ( comError -> Message . get (), ExpectedSubstring . c_str ()) == nullptr )
{
LogError ( "Expected COM error containing: '%ls', but got: '%ls'" , ExpectedSubstring . c_str (), comError -> Message . get ());
VERIFY_FAIL ();
}
}
else
{
LogError ( "Expected COM error containing: '%ls' but none was set" , ExpectedSubstring . c_str ());
VERIFY_FAIL ();
}
}
2026-08-26 11:05:10 +08:00
std :: wstring FormatErrorMessage ( std :: wstring_view message , std :: wstring_view errorCode )
{
return std :: format (
L "{} \r\n Error code: {} \r\n "
L "If this error was unexpected, please consider searching for existing issues or filing a new issue at "
L "https://github.com/microsoft/WSL/issues. \r\n " ,
message ,
errorCode );
}