/*++ 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 #include #include 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 g_dumpToolPath; static bool g_enableWerReport = false; static std::wstring g_pipelineBuildId; std::wstring g_testDistroPath; std::pair 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 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 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 LxsstuLaunchCommandAndCaptureOutput(_In_ LPWSTR Cmd, _In_ LPCSTR StandardInput, _In_opt_ HANDLE Token, _In_ DWORD Flags) { const auto inputSize = static_cast(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 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 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; UniqueFd Fd(_open_osfhandle(reinterpret_cast(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 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 Modules; DWORD RequiredSize{}; bool Result{}; do { Modules.resize(RequiredSize / sizeof(HMODULE)); Result = EnumProcessModulesEx(Process, Modules.data(), static_cast(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(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; 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(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 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 WslProcesses{ L"wsl.exe", L"wslhost.exe", L"wslrelay.exe", L"wslservice.exe", L"wslg.exe", L"vmcompute.exe", L"vmwp.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(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 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 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(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 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(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() { const auto token = wil::open_current_access_token(TOKEN_ALL_ACCESS); wil::unique_handle nonElevatedToken; THROW_IF_WIN32_BOOL_FALSE(DuplicateTokenEx(token.get(), TOKEN_ALL_ACCESS, nullptr, SecurityImpersonation, TokenPrimary, &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()); } } // writes global WSL 2 config settings at %userprofile%/.wslconfig std::wstring LxssWriteWslConfig(const std::wstring& Content) { auto path = getenv("userprofile") + std::string("\\.wslconfig"); std::wifstream configRead(path); auto previousContent = std::wstring{std::istreambuf_iterator(configRead), {}}; configRead.close(); 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(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 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 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 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; }; // TODO: Reset guiApplications to true by default once the virtio hang is solved. 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, false) + 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 = L"/bin/cp /data/test/log/"; Command += LogFileToken; Command += L" $(wslpath '"; Command += LinuxLogPath; Command += L"')"; 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. --*/ { // 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 { WEX::Common::String Value; WEX::TestExecution::RuntimeParameters::TryGetValue(Name, Value); return Value.IsEmpty() ? std::optional() : static_cast(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"); 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); 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 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& 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 dnsQueryRaw{dnsModule, "DnsQueryRaw"}; static LxssDynamicFunction dnsCancelQueryRaw{dnsModule, "DnsCancelQueryRaw"}; static LxssDynamicFunction 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 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(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(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); }