mirror of
https://github.com/microsoft/WSL.git
synced 2026-02-04 02:06:49 -06:00
2847 lines
111 KiB
C++
2847 lines
111 KiB
C++
/*++
|
|
|
|
Copyright (c) Microsoft. All rights reserved.
|
|
|
|
Module Name:
|
|
|
|
WslCoreVm.cpp
|
|
|
|
Abstract:
|
|
|
|
This file contains utility VM function definitions.
|
|
|
|
--*/
|
|
|
|
#include "precomp.h"
|
|
#include "WslCoreVm.h"
|
|
#include "WslCoreNetworkingSupport.h"
|
|
#include <lxfsshares.h>
|
|
#include "disk.hpp"
|
|
#include "WslCoreInstance.h"
|
|
#include "NatNetworking.h"
|
|
#include "BridgedNetworking.h"
|
|
#include "MirroredNetworking.h"
|
|
#include "WslCoreFirewallSupport.h"
|
|
#include "DnsResolver.h"
|
|
#include "VirtioNetworking.h"
|
|
|
|
#include <TraceLoggingProvider.h>
|
|
|
|
using msl::utilities::SafeInt;
|
|
using wsl::windows::common::helpers::WindowsBuildNumbers;
|
|
using namespace wsl::windows::common::registry;
|
|
using namespace wsl::windows::common::string;
|
|
using namespace std::string_literals;
|
|
|
|
// The default high-gap MMIO space is 16GB
|
|
#define DEFAULT_HIGH_MMIO_GAP_IN_MB (16 * _1KB)
|
|
|
|
// Start of unaddressable memory if guest only supports the minimum 36-bit addressing.
|
|
#define MAX_36_BIT_PAGE_IN_MB (0x1000000000 / _1MB)
|
|
|
|
#define WSLG_SHARED_MEMORY_SIZE_MB 8192
|
|
#define PAGE_SIZE 0x1000
|
|
|
|
static constexpr size_t c_bootEntropy = 0x1000;
|
|
static constexpr auto c_localDevicesKey = L"SOFTWARE\\Microsoft\\Terminal Server Client\\LocalDevices";
|
|
|
|
#define LXSS_ENABLE_GUI_APPS() (m_vmConfig.EnableGuiApps && (m_systemDistroDeviceId != ULONG_MAX))
|
|
|
|
using namespace wsl::windows::common;
|
|
using wsl::core::NetworkingMode;
|
|
using wsl::core::networking::NetworkEndpoint;
|
|
using wsl::core::networking::NetworkSettings;
|
|
using wsl::shared::Localization;
|
|
using wsl::windows::common::Context;
|
|
using wsl::windows::common::ExecutionContext;
|
|
|
|
namespace {
|
|
INT64
|
|
RequiredExtraMmioSpaceForPmemFileInMb(_In_ PCWSTR FilePath)
|
|
{
|
|
// Open the file and retrieve the file's size.
|
|
const wil::unique_hfile fileHandle{CreateFile(FilePath, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)};
|
|
THROW_LAST_ERROR_IF(!fileHandle);
|
|
|
|
LARGE_INTEGER fileSizeBytes;
|
|
THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(fileHandle.get(), &fileSizeBytes));
|
|
|
|
// The file is mapped to the VM using PCI BARs, which can only be a power of two. Therefore,
|
|
// round the file size up to the nearest power of two.
|
|
fileSizeBytes.QuadPart = wsl::windows::common::helpers::RoundUpToNearestPowerOfTwo(fileSizeBytes.QuadPart);
|
|
|
|
// Convert from bytes to megabytes. Ensure that we don't truncate a 512kb file to 0mb.
|
|
return std::max(fileSizeBytes.QuadPart / static_cast<INT64>(_1MB), 1i64);
|
|
}
|
|
} // namespace
|
|
|
|
WslCoreVm::WslCoreVm(_In_ wsl::core::Config&& VmConfig) :
|
|
m_vmConfig(std::move(VmConfig)), m_traceClient(m_vmConfig.EnableTelemetry)
|
|
{
|
|
}
|
|
|
|
std::unique_ptr<WslCoreVm> WslCoreVm::Create(_In_ const wil::shared_handle& UserToken, _In_ wsl::core::Config&& VmConfig, _In_ const GUID& VmId)
|
|
{
|
|
auto newInstance = std::unique_ptr<WslCoreVm>{new WslCoreVm{std::move(VmConfig)}};
|
|
try
|
|
{
|
|
const auto startTimeMs = GetTickCount64();
|
|
auto privateKernel = !newInstance->m_vmConfig.KernelPath.empty();
|
|
// Log telemetry on how long it took to create the VM
|
|
WSL_LOG_TELEMETRY(
|
|
"CreateVmBegin", PDT_ProductAndServicePerformance, TraceLoggingValue(VmId, "vmId"), CONFIG_TELEMETRY(newInstance->m_vmConfig));
|
|
|
|
newInstance->Initialize(VmId, UserToken);
|
|
|
|
const auto timeToCreateVmMs = GetTickCount64() - startTimeMs;
|
|
WSL_LOG_TELEMETRY(
|
|
"CreateVmEnd",
|
|
PDT_ProductAndServicePerformance,
|
|
TraceLoggingValue(privateKernel, "privateKernel"),
|
|
TraceLoggingValue(newInstance->m_kernelVersionString.c_str(), "kernelVersion"),
|
|
TraceLoggingValue(newInstance->m_runtimeId, "vmId"),
|
|
TraceLoggingValue(timeToCreateVmMs, "timeToCreateVmMs"),
|
|
CONFIG_TELEMETRY(newInstance->m_vmConfig));
|
|
}
|
|
catch (...)
|
|
{
|
|
const auto hr = wil::ResultFromCaughtException();
|
|
|
|
// Log telemetry when the WSL VM fails to start including the error
|
|
WSL_LOG_TELEMETRY(
|
|
"FailedToStartVm",
|
|
PDT_ProductAndServicePerformance,
|
|
TraceLoggingValue(VmId, "vmId"),
|
|
TraceLoggingValue(hr, "error"),
|
|
CONFIG_TELEMETRY(newInstance->m_vmConfig));
|
|
|
|
if (hr == HRESULT_FROM_WIN32(WSAENOTCONN) || hr == HRESULT_FROM_WIN32(WSAECONNRESET) || hr == HRESULT_FROM_WIN32(WSAETIMEDOUT))
|
|
{
|
|
// A kernel panic can cause an hvsocket error. If we hit this, wait one second for an HCS notification to give a better error for the user.
|
|
if (newInstance->m_vmCrashEvent.wait(1000))
|
|
{
|
|
if (newInstance->m_vmCrashLogFile.has_value())
|
|
{
|
|
THROW_HR_WITH_USER_ERROR(
|
|
WSL_E_VM_CRASHED,
|
|
wsl::shared::Localization::MessageWSL2Crashed() + L"\r\n" +
|
|
Localization::MessageWSL2CrashedStackTrace(newInstance->m_vmCrashLogFile.value()));
|
|
}
|
|
else
|
|
{
|
|
THROW_HR_WITH_USER_ERROR(WSL_E_VM_CRASHED, wsl::shared::Localization::MessageWSL2Crashed());
|
|
}
|
|
}
|
|
}
|
|
|
|
throw;
|
|
}
|
|
|
|
return newInstance;
|
|
}
|
|
|
|
void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken)
|
|
{
|
|
auto signalEarlyTermination = wil::scope_exit([&] { m_terminatingEvent.SetEvent(); });
|
|
|
|
// create a restricted version of the token.
|
|
m_userToken = UserToken;
|
|
m_restrictedToken = wsl::windows::common::security::CreateRestrictedToken(m_userToken.get());
|
|
|
|
// Make a copy of the user sid.
|
|
auto tokenUser = wil::get_token_information<TOKEN_USER>(m_userToken.get());
|
|
THROW_IF_WIN32_BOOL_FALSE(::CopySid(sizeof(m_userSid), &m_userSid.Sid, tokenUser->User.Sid));
|
|
|
|
// Generate a machine ID string based on the VM ID. This is used for some HCS APIs.
|
|
m_machineId = wsl::shared::string::GuidToString<wchar_t>(VmId, wsl::shared::string::GuidToStringFlags::Uppercase);
|
|
|
|
// Set the install path of the package.
|
|
m_installPath = wsl::windows::common::wslutil::GetBasePath();
|
|
|
|
// Initialize the path to the tools folder.
|
|
m_rootFsPath = m_installPath / LXSS_TOOLS_DIRECTORY;
|
|
|
|
// Store the path of the user profile.
|
|
m_userProfile = wsl::windows::common::helpers::GetUserProfilePath(m_userToken.get());
|
|
|
|
// Query the Windows version.
|
|
m_windowsVersion = wsl::windows::common::helpers::GetWindowsVersion();
|
|
|
|
// Create a temporary folder for the VM.
|
|
try
|
|
{
|
|
const auto runAsUser = wil::impersonate_token(m_userToken.get());
|
|
m_tempPath = wsl::windows::common::filesystem::GetTempFolderPath(m_userToken.get()) / m_machineId;
|
|
|
|
wil::CreateDirectoryDeep(m_tempPath.c_str());
|
|
m_tempDirectoryCreated = true;
|
|
}
|
|
CATCH_LOG();
|
|
|
|
// If a private kernel was not specified, use the default.
|
|
m_defaultKernel = m_vmConfig.KernelPath.empty();
|
|
if (m_defaultKernel)
|
|
{
|
|
#ifdef WSL_KERNEL_PATH
|
|
|
|
m_vmConfig.KernelPath = TEXT(WSL_KERNEL_PATH);
|
|
|
|
#else
|
|
|
|
m_vmConfig.KernelPath = m_rootFsPath / LXSS_VM_MODE_KERNEL_NAME;
|
|
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
if (!wsl::windows::common::filesystem::FileExists(m_vmConfig.KernelPath.c_str()))
|
|
{
|
|
THROW_HR_WITH_USER_ERROR(
|
|
WSL_E_CUSTOM_KERNEL_NOT_FOUND,
|
|
Localization::MessageCustomKernelNotFound(
|
|
wsl::windows::common::helpers::GetWslConfigPath(m_userToken.get()), m_vmConfig.KernelPath.c_str()));
|
|
}
|
|
|
|
// Direct boot is not supported on ARM64. Modify the rootfs directory to be a temporary directory that contains
|
|
// copies of the initrd file and private kernel.
|
|
if constexpr (wsl::shared::Arm64)
|
|
{
|
|
auto impersonate = wil::impersonate_token(m_userToken.get());
|
|
|
|
m_rootFsPath = m_tempPath / LXSS_ROOTFS_DIRECTORY;
|
|
wil::CreateDirectoryDeep(m_rootFsPath.c_str());
|
|
auto initRdPath = m_installPath / LXSS_TOOLS_DIRECTORY / LXSS_VM_MODE_INITRD_NAME;
|
|
|
|
auto targetPath = m_rootFsPath / LXSS_VM_MODE_INITRD_NAME;
|
|
THROW_IF_WIN32_BOOL_FALSE(CopyFileW(initRdPath.c_str(), targetPath.c_str(), TRUE));
|
|
|
|
targetPath = m_rootFsPath / LXSS_VM_MODE_KERNEL_NAME;
|
|
THROW_IF_WIN32_BOOL_FALSE(CopyFileW(m_vmConfig.KernelPath.c_str(), targetPath.c_str(), TRUE));
|
|
}
|
|
}
|
|
|
|
// If the user did not specify custom modules, use the default modules only if using the default kernel.
|
|
if (m_vmConfig.KernelModulesPath.empty())
|
|
{
|
|
if (m_defaultKernel)
|
|
{
|
|
#ifdef WSL_KERNEL_MODULES_PATH
|
|
|
|
m_vmConfig.KernelModulesPath = std::wstring(TEXT(WSL_KERNEL_MODULES_PATH));
|
|
|
|
#else
|
|
|
|
m_vmConfig.KernelModulesPath = m_rootFsPath / L"modules.vhd";
|
|
|
|
#endif
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!wsl::windows::common::filesystem::FileExists(m_vmConfig.KernelModulesPath.c_str()))
|
|
{
|
|
THROW_HR_WITH_USER_ERROR(
|
|
WSL_E_CUSTOM_KERNEL_NOT_FOUND,
|
|
Localization::MessageCustomKernelModulesNotFound(
|
|
wsl::windows::common::helpers::GetWslConfigPath(m_userToken.get()), m_vmConfig.KernelModulesPath.c_str()));
|
|
}
|
|
|
|
if (m_defaultKernel)
|
|
{
|
|
THROW_HR_WITH_USER_ERROR(WSL_E_CUSTOM_KERNEL_NOT_FOUND, Localization::MessageMismatchedKernelModulesError());
|
|
}
|
|
}
|
|
|
|
// If debug console was requested, create a randomly-named pipe and spawn a wslhost process to read from the pipe.
|
|
//
|
|
// N.B. wslhost.exe is launched at medium integrity level and its lifetime
|
|
// is tied to the lifetime of the utility VM.
|
|
if (m_vmConfig.EnableDebugConsole || !m_vmConfig.DebugConsoleLogFile.empty())
|
|
{
|
|
try
|
|
{
|
|
m_vmConfig.EnableDebugConsole = true;
|
|
m_comPipe0 = wsl::windows::common::helpers::GetUniquePipeName();
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
// If the system supports virtio console serial ports, use dmesg capture for telemetry and/or debug output.
|
|
// Legacy serial is much slower, so this is not enabled without virtio console support.
|
|
auto enableVirtioSerial = m_vmConfig.EnableVirtio && helpers::IsVirtioSerialConsoleSupported();
|
|
m_vmConfig.EnableDebugShell &= enableVirtioSerial;
|
|
if (enableVirtioSerial)
|
|
{
|
|
try
|
|
{
|
|
bool enableTelemetry = TraceLoggingProviderEnabled(g_hTraceLoggingProvider, WINEVENT_LEVEL_INFO, 0);
|
|
m_dmesgCollector = DmesgCollector::Create(
|
|
VmId, m_vmExitEvent, enableTelemetry, m_vmConfig.EnableDebugConsole, m_comPipe0, m_vmConfig.EnableEarlyBootLogging, {});
|
|
|
|
WSL_LOG("DMESG collector created");
|
|
|
|
if (m_vmConfig.EnableDebugShell)
|
|
{
|
|
m_debugShellPipe = wsl::windows::common::wslutil::GetDebugShellPipeName(&m_userSid.Sid);
|
|
}
|
|
|
|
// Initialize the guest telemetry logger.
|
|
m_gnsTelemetryLogger = GuestTelemetryLogger::Create(VmId, m_vmExitEvent);
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
if (m_vmConfig.EnableDebugConsole)
|
|
{
|
|
try
|
|
{
|
|
// If specified, create a file to log the debug console output.
|
|
wil::unique_hfile logFile;
|
|
if (!m_vmConfig.DebugConsoleLogFile.empty())
|
|
{
|
|
auto impersonate = wil::impersonate_token(m_userToken.get());
|
|
logFile.reset(CreateFileW(
|
|
m_vmConfig.DebugConsoleLogFile.c_str(), FILE_APPEND_DATA, (FILE_SHARE_READ | FILE_SHARE_WRITE), nullptr, OPEN_ALWAYS, 0, nullptr));
|
|
|
|
LOG_LAST_ERROR_IF(!logFile);
|
|
}
|
|
|
|
wsl::windows::common::helpers::LaunchDebugConsole(
|
|
m_comPipe0.c_str(), !!m_dmesgCollector, m_restrictedToken.get(), logFile ? logFile.get() : nullptr, !m_vmConfig.EnableTelemetry);
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
// Create the utility VM and store the runtime ID.
|
|
std::wstring json = GenerateConfigJson();
|
|
m_system = wsl::windows::common::hcs::CreateComputeSystem(m_machineId.c_str(), json.c_str());
|
|
m_runtimeId = wsl::windows::common::hcs::GetRuntimeId(m_system.get());
|
|
WI_ASSERT(IsEqualGUID(VmId, m_runtimeId));
|
|
|
|
// Initialize the guest device manager.
|
|
m_guestDeviceManager = std::make_shared<GuestDeviceManager>(m_machineId, m_runtimeId);
|
|
|
|
// Create a socket listening for connections from mini_init.
|
|
m_listenSocket = wsl::windows::common::hvsocket::Listen(m_runtimeId, LX_INIT_UTILITY_VM_INIT_PORT);
|
|
|
|
if (m_vmConfig.MaxCrashDumpCount >= 0)
|
|
{
|
|
auto crashDumpSocket = wsl::windows::common::hvsocket::Listen(m_runtimeId, LX_INIT_UTILITY_VM_CRASH_DUMP_PORT);
|
|
THROW_LAST_ERROR_IF(!crashDumpSocket);
|
|
m_crashDumpCollectionThread =
|
|
std::thread{[this, socket = std::move(crashDumpSocket)]() mutable { CollectCrashDumps(std::move(socket)); }};
|
|
}
|
|
|
|
// Register a callback to detect if the utility VM exits unexpectedly.
|
|
wsl::windows::common::hcs::RegisterCallback(m_system.get(), s_OnExit, this);
|
|
signalEarlyTermination.release();
|
|
|
|
// Start the utility VM.
|
|
try
|
|
{
|
|
wsl::windows::common::hcs::StartComputeSystem(m_system.get(), json.c_str());
|
|
}
|
|
catch (...)
|
|
{
|
|
// Reset m_system so we don't try to wait for termination in the destructor, since the VM isn't even running.
|
|
m_system.reset();
|
|
throw;
|
|
}
|
|
|
|
// Add GPUs to the utility VM.
|
|
if (m_vmConfig.EnableGpuSupport)
|
|
{
|
|
ExecutionContext context(Context::ConfigureGpu);
|
|
|
|
hcs::ModifySettingRequest<hcs::GpuConfiguration> gpuRequest{};
|
|
gpuRequest.ResourcePath = L"VirtualMachine/ComputeTopology/Gpu";
|
|
gpuRequest.RequestType = hcs::ModifyRequestType::Update;
|
|
gpuRequest.Settings.AssignmentMode = hcs::GpuAssignmentMode::Mirror;
|
|
gpuRequest.Settings.AllowVendorExtension = true;
|
|
if (wsl::windows::common::helpers::IsDisableVgpuSettingsSupported())
|
|
{
|
|
gpuRequest.Settings.DisableGdiAcceleration = true;
|
|
gpuRequest.Settings.DisablePresentation = true;
|
|
}
|
|
|
|
wsl::windows::common::hcs::ModifyComputeSystem(m_system.get(), wsl::shared::ToJsonW(gpuRequest).c_str());
|
|
|
|
// Also add 9p shares for the library directories.
|
|
// N.B. These are not hosted by the out-of-proc drvfs 9p server because the GPU shares
|
|
// should work even if drvfs is disabled.
|
|
auto addShare = [&](PCWSTR name, PCWSTR path) {
|
|
constexpr auto flags = (hcs::Plan9ShareFlags::ReadOnly | hcs::Plan9ShareFlags::AllowOptions);
|
|
wsl::windows::common::hcs::AddPlan9Share(m_system.get(), name, name, path, LX_INIT_UTILITY_VM_PLAN9_PORT, flags);
|
|
};
|
|
|
|
std::wstring path;
|
|
THROW_IF_FAILED(wil::ExpandEnvironmentStringsW(L"%SystemRoot%\\System32\\DriverStore\\FileRepository", path));
|
|
addShare(TEXT(LXSS_GPU_DRIVERS_SHARE), path.c_str());
|
|
|
|
// N.B. There are inbox and packaged versions of the Direct 3D libraries. The packaged
|
|
// versions take presidence by using overlayfs in the guest.
|
|
THROW_IF_FAILED(wil::ExpandEnvironmentStringsW(L"%SystemRoot%\\System32\\lxss\\lib", path));
|
|
|
|
if (wsl::windows::common::filesystem::FileExists(path.c_str()))
|
|
{
|
|
try
|
|
{
|
|
addShare(TEXT(LXSS_GPU_INBOX_LIB_SHARE), path.c_str());
|
|
m_enableInboxGpuLibs = true;
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
#ifdef WSL_GPU_LIB_PATH
|
|
|
|
path = TEXT(WSL_GPU_LIB_PATH);
|
|
|
|
#else
|
|
|
|
path = m_installPath / L"lib";
|
|
|
|
#endif
|
|
|
|
addShare(TEXT(LXSS_GPU_PACKAGED_LIB_SHARE), path.c_str());
|
|
}
|
|
|
|
// Asynchronously add drvfs devices if supported.
|
|
if (m_vmConfig.EnableHostFileSystemAccess)
|
|
{
|
|
std::promise<bool> initialResult;
|
|
m_drvfsInitialResult = initialResult.get_future();
|
|
auto guestDeviceLock = m_guestDeviceLock.lock_exclusive();
|
|
std::thread([this, guestDeviceLock = std::move(guestDeviceLock), initialResult = std::move(initialResult)]() mutable {
|
|
try
|
|
{
|
|
wsl::windows::common::wslutil::SetThreadDescription(L"InitializeDrvfs");
|
|
initialResult.set_value(InitializeDrvFsLockHeld(m_userToken.get()));
|
|
}
|
|
catch (...)
|
|
{
|
|
try
|
|
{
|
|
initialResult.set_exception(std::current_exception());
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
}).detach();
|
|
}
|
|
|
|
// Accept a connection from mini_init with a receive timeout so the service does not get stuck waiting for a response from the VM.
|
|
m_miniInitChannel = wsl::shared::SocketChannel{AcceptConnection(m_vmConfig.KernelBootTimeout), "mini_init", m_terminatingEvent.get()};
|
|
|
|
// Accept the connection from the Linux guest for notifications.
|
|
m_notifyChannel = AcceptConnection(m_vmConfig.KernelBootTimeout);
|
|
|
|
// Receive and parse the guest kernel version
|
|
ReadGuestCapabilities();
|
|
|
|
// Mount the system distro.
|
|
// N.B. If using SCSI, the system distro is added during VM creation.
|
|
switch (m_systemDistroDeviceType)
|
|
{
|
|
case LxMiniInitMountDeviceTypePmem:
|
|
m_systemDistroDeviceId = MountFileAsPersistentMemory(m_vmConfig.SystemDistroPath.c_str(), true);
|
|
break;
|
|
}
|
|
|
|
// Attempt to create and mount the swap vhd.
|
|
//
|
|
// N.B. This can fail if the target directory is compressed, encrypted, or if
|
|
// the user does not have write access.
|
|
ULONG swapLun = ULONG_MAX;
|
|
if ((m_systemDistroDeviceId != ULONG_MAX) && (m_vmConfig.SwapSizeBytes > 0))
|
|
{
|
|
try
|
|
{
|
|
{
|
|
// If no user-specified swap vhd file path was specified, use a
|
|
// path in the temp directory.
|
|
auto runAsUser = wil::impersonate_token(m_userToken.get());
|
|
if (m_vmConfig.SwapFilePath.empty())
|
|
{
|
|
m_vmConfig.SwapFilePath = m_tempPath / L"swap";
|
|
}
|
|
|
|
// Ensure the swap vhd ends with the vhdx file extension.
|
|
if (!wsl::windows::common::string::IsPathComponentEqual(
|
|
m_vmConfig.SwapFilePath.extension().native(), wsl::windows::common::wslutil::c_vhdxFileExtension))
|
|
{
|
|
m_vmConfig.SwapFilePath += wsl::windows::common::wslutil::c_vhdxFileExtension;
|
|
}
|
|
|
|
// Create the VHD with an additional page for swap overhead.
|
|
m_vmConfig.SwapSizeBytes += PAGE_SIZE;
|
|
auto result = wil::ResultFromException([&]() {
|
|
wsl::core::filesystem::CreateVhd(m_vmConfig.SwapFilePath.c_str(), m_vmConfig.SwapSizeBytes, &m_userSid.Sid, false, false);
|
|
m_swapFileCreated = true;
|
|
});
|
|
|
|
if (result == HRESULT_FROM_WIN32(ERROR_FILE_EXISTS))
|
|
{
|
|
auto handle = wsl::core::filesystem::OpenVhd(
|
|
m_vmConfig.SwapFilePath.c_str(), VIRTUAL_DISK_ACCESS_CREATE | VIRTUAL_DISK_ACCESS_METAOPS | VIRTUAL_DISK_ACCESS_GET_INFO);
|
|
wsl::core::filesystem::ResizeExistingVhd(handle.get(), m_vmConfig.SwapSizeBytes, RESIZE_VIRTUAL_DISK_FLAG_ALLOW_UNSAFE_VIRTUAL_SIZE);
|
|
}
|
|
else if (FAILED(result))
|
|
{
|
|
EMIT_USER_WARNING(wsl::shared::Localization::MessagedFailedToCreateSwapVhd(
|
|
m_vmConfig.SwapFilePath.c_str(), wsl::windows::common::wslutil::GetSystemErrorString(result).c_str()));
|
|
|
|
THROW_HR(result);
|
|
}
|
|
}
|
|
|
|
swapLun = AttachDiskLockHeld(m_vmConfig.SwapFilePath.c_str(), DiskType::VHD, MountFlags::None, {}, false, m_userToken.get());
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
// Validate that the requesting network mode is supported.
|
|
//
|
|
// N.B. This must be done before sending the initial configuration message because some guest
|
|
// behavior is determined by the networking mode.
|
|
ValidateNetworkingMode();
|
|
|
|
// Send the early configuration message.
|
|
wsl::shared::MessageWriter<LX_MINI_INIT_EARLY_CONFIG_MESSAGE> message(LxMiniInitMessageEarlyConfig);
|
|
message->SwapLun = swapLun;
|
|
message->SystemDistroDeviceType = m_systemDistroDeviceType;
|
|
message->SystemDistroDeviceId = m_systemDistroDeviceId;
|
|
message->PageReportingOrder = m_coldDiscardShiftSize;
|
|
message->MemoryReclaimMode = static_cast<LX_MINI_INIT_MEMORY_RECLAIM_MODE>(m_vmConfig.MemoryReclaim);
|
|
message->EnableDebugShell = m_vmConfig.EnableDebugShell;
|
|
message->EnableSafeMode = m_vmConfig.EnableSafeMode;
|
|
message->EnableDnsTunneling = m_vmConfig.EnableDnsTunneling;
|
|
message->DefaultKernel = m_defaultKernel;
|
|
message->KernelModulesDeviceId = m_kernelModulesDeviceId;
|
|
message.WriteString(message->HostnameOffset, wsl::windows::common::filesystem::GetLinuxHostName());
|
|
message.WriteString(message->KernelModulesListOffset, m_vmConfig.KernelModulesList);
|
|
message->DnsTunnelingIpAddress = m_vmConfig.DnsTunnelingIpAddress.value_or(0);
|
|
|
|
m_miniInitChannel.SendMessage<LX_MINI_INIT_EARLY_CONFIG_MESSAGE>(message.Span());
|
|
|
|
{
|
|
ExecutionContext context(Context::ConfigureNetworking);
|
|
|
|
// Accept the connection from the guest network service and create the channel.
|
|
wsl::core::GnsChannel gnsChannel(AcceptConnection(m_vmConfig.KernelBootTimeout));
|
|
|
|
// Create hvsocket connection for DNS tunneling if enabled.
|
|
wil::unique_socket dnsTunnelingSocket;
|
|
if (m_vmConfig.EnableDnsTunneling)
|
|
{
|
|
dnsTunnelingSocket = AcceptConnection(m_vmConfig.KernelBootTimeout);
|
|
}
|
|
|
|
// Record the start time of the networking engine initialization so the duration can be logged.
|
|
const auto startTime = std::chrono::steady_clock::now();
|
|
|
|
// For NAT networking, ensure the network can be created. If creating the network fails, fall back to
|
|
// virtio proxy networking mode.
|
|
wsl::windows::common::hcs::unique_hcn_network natNetwork;
|
|
if (m_vmConfig.NetworkingMode == NetworkingMode::Nat)
|
|
{
|
|
natNetwork = wsl::core::NatNetworking::CreateNetwork(m_vmConfig);
|
|
if (!natNetwork)
|
|
{
|
|
EMIT_USER_WARNING(wsl::shared::Localization::MessageNetworkInitializationFailedFallback2(
|
|
ToString(m_vmConfig.NetworkingMode), ToString(NetworkingMode::VirtioProxy)));
|
|
|
|
m_vmConfig.NetworkingMode = NetworkingMode::VirtioProxy;
|
|
}
|
|
}
|
|
|
|
// Create and initialize the networking engine.
|
|
const auto result = wil::ResultFromException(WI_DIAGNOSTICS_INFO, [&] {
|
|
if (m_vmConfig.NetworkingMode == NetworkingMode::Mirrored)
|
|
{
|
|
m_networkingEngine = std::make_unique<wsl::core::MirroredNetworking>(
|
|
m_system.get(), std::move(gnsChannel), m_vmConfig, m_runtimeId, std::move(dnsTunnelingSocket));
|
|
}
|
|
else if (m_vmConfig.NetworkingMode == NetworkingMode::Nat)
|
|
{
|
|
WI_ASSERT(natNetwork);
|
|
|
|
m_networkingEngine = std::make_unique<wsl::core::NatNetworking>(
|
|
m_system.get(), std::move(natNetwork), std::move(gnsChannel), m_vmConfig, std::move(dnsTunnelingSocket));
|
|
}
|
|
else if (m_vmConfig.NetworkingMode == NetworkingMode::VirtioProxy)
|
|
{
|
|
m_networkingEngine = std::make_unique<wsl::core::VirtioNetworking>(
|
|
std::move(gnsChannel), m_vmConfig.EnableLocalhostRelay, LX_INIT_RESOLVCONF_FULL_HEADER, m_guestDeviceManager, m_userToken);
|
|
}
|
|
else if (m_vmConfig.NetworkingMode == NetworkingMode::Bridged)
|
|
{
|
|
m_networkingEngine = std::make_unique<wsl::core::BridgedNetworking>(m_system.get(), m_vmConfig);
|
|
}
|
|
else
|
|
{
|
|
WI_ASSERT(m_vmConfig.NetworkingMode == NetworkingMode::None);
|
|
}
|
|
|
|
if (m_networkingEngine)
|
|
{
|
|
m_networkingEngine->Initialize();
|
|
}
|
|
});
|
|
|
|
// Find the interface type of the host interface that is most likely to give Internet connectivity
|
|
const auto bestInterfaceIndex = wsl::core::networking::GetBestInterface();
|
|
MIB_IFROW row{};
|
|
row.dwIndex = bestInterfaceIndex;
|
|
IFTYPE bestInterfaceType{};
|
|
// Ignore failures
|
|
if (row.dwIndex != 0 && SUCCEEDED_WIN32(GetIfEntry(&row)))
|
|
{
|
|
bestInterfaceType = row.dwType;
|
|
}
|
|
|
|
const auto endTime = std::chrono::steady_clock::now();
|
|
|
|
// Log telemetry on the VM initialization including some of its key settings
|
|
WSL_LOG_TELEMETRY(
|
|
"WslCoreVmInitialize",
|
|
PDT_ProductAndServicePerformance,
|
|
TraceLoggingValue(m_runtimeId, "vmId"),
|
|
TraceLoggingValue(ToString(m_vmConfig.NetworkingMode), "networkingMode"),
|
|
TraceLoggingValue(m_vmConfig.FirewallConfig.Enabled(), "firewallEnabled"),
|
|
TraceLoggingValue(m_vmConfig.EnableDnsTunneling, "dnsTunnelingEnabled"),
|
|
TraceLoggingValue(
|
|
m_vmConfig.DnsTunnelingIpAddress.has_value()
|
|
? wsl::windows::common::string::IntegerIpv4ToWstring(m_vmConfig.DnsTunnelingIpAddress.value()).c_str()
|
|
: L"",
|
|
"dnsTunnelingIpAddress"),
|
|
TraceLoggingValue(bestInterfaceType, "bestInterfaceType"),
|
|
TraceLoggingValue(result, "result"),
|
|
TraceLoggingValue((std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime)).count(), "durationMs"));
|
|
|
|
if (FAILED(result))
|
|
{
|
|
const auto* context = ExecutionContext::Current();
|
|
if (context != nullptr)
|
|
{
|
|
// We already have a specialized error message, display it to the user.
|
|
const auto& currentError = context->ReportedError();
|
|
if (currentError.has_value())
|
|
{
|
|
auto strings = wsl::windows::common::wslutil::ErrorToString(currentError.value());
|
|
EMIT_USER_WARNING(Localization::MessageErrorCode(strings.Message, strings.Code));
|
|
}
|
|
}
|
|
|
|
// If something failed during initialization that indicates a dependent service is not running,
|
|
// inform the user to install the Virtual Machine Platform optional component.
|
|
if (wsl::core::networking::IsNetworkErrorForMissingServices(result) &&
|
|
!wsl::windows::common::wslutil::IsVirtualMachinePlatformInstalled())
|
|
{
|
|
wsl::windows::common::notifications::DisplayOptionalComponentsNotification();
|
|
EMIT_USER_WARNING(Localization::MessageVirtualMachinePlatformNotInstalled());
|
|
}
|
|
|
|
// Fall back to no networking.
|
|
EMIT_USER_WARNING(wsl::shared::Localization::MessageNetworkInitializationFailedFallback2(
|
|
ToString(m_vmConfig.NetworkingMode), ToString(NetworkingMode::None)));
|
|
|
|
m_vmConfig.NetworkingMode = NetworkingMode::None;
|
|
m_networkingEngine.reset();
|
|
}
|
|
}
|
|
|
|
// Perform additional initialization.
|
|
InitializeGuest();
|
|
}
|
|
|
|
WslCoreVm::~WslCoreVm() noexcept
|
|
{
|
|
TraceLoggingActivity<g_hTraceLoggingProvider, MICROSOFT_KEYWORD_MEASURES> activity;
|
|
TraceLoggingWriteStart(
|
|
activity,
|
|
"TerminateVmStart",
|
|
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance),
|
|
TraceLoggingValue(m_runtimeId, "vmId"));
|
|
|
|
m_networkingEngine.reset();
|
|
|
|
auto lock = m_lock.lock_exclusive();
|
|
|
|
if (m_drvfsInitialResult.valid())
|
|
{
|
|
try
|
|
{
|
|
m_drvfsInitialResult.get();
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
// Clear out the exit callback.
|
|
{
|
|
auto exitLock = m_exitCallbackLock.lock_exclusive();
|
|
m_onExit = nullptr;
|
|
|
|
// Signal that the vm is terminating
|
|
// N.B. This might have already been signaled if the VM exited abnormally.
|
|
m_terminatingEvent.SetEvent();
|
|
}
|
|
|
|
if (m_system)
|
|
{
|
|
bool unexpectedTerminate = m_vmExitEvent.is_signaled();
|
|
bool forcedTerminate = false;
|
|
|
|
// Close the socket to mini_init. This will cause mini_init to break out
|
|
// of its message processing loop and perform a clean shutdown.
|
|
m_miniInitChannel.Close();
|
|
|
|
if (!unexpectedTerminate)
|
|
{
|
|
// Wait to receive the notification that the VM has exited.
|
|
forcedTerminate = !m_vmExitEvent.wait(UTILITY_VM_SHUTDOWN_TIMEOUT);
|
|
|
|
// If the notification did not arrive within the timeout, the VM is
|
|
// forcefully terminated.
|
|
if (forcedTerminate)
|
|
{
|
|
try
|
|
{
|
|
wsl::windows::common::hcs::TerminateComputeSystem(m_system.get());
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
}
|
|
|
|
m_vmExitEvent.wait(UTILITY_VM_TERMINATE_TIMEOUT);
|
|
|
|
TraceLoggingWriteTagged(
|
|
activity,
|
|
"TerminateVm",
|
|
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
|
|
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance),
|
|
TraceLoggingValue(WSL_PACKAGE_VERSION, "wslVersion"),
|
|
TraceLoggingValue(m_runtimeId, "vmId"),
|
|
TraceLoggingValue(forcedTerminate, "forceTerminate"),
|
|
TraceLoggingValue(unexpectedTerminate, "unexpectedTerminate"),
|
|
TraceLoggingValue(m_vmExitEvent.is_signaled(), "terminationCallbackReceived"),
|
|
TraceLoggingValue(m_exitDetails.c_str(), "exitDetails"));
|
|
}
|
|
|
|
// Wait for the distro exit callback thread to exit.
|
|
// The thread might not have been started, in that case joinable() returns false.
|
|
if (m_distroExitThread.joinable())
|
|
{
|
|
m_distroExitThread.join();
|
|
}
|
|
|
|
if (m_virtioFsThread.joinable())
|
|
{
|
|
m_virtioFsThread.join();
|
|
}
|
|
|
|
if (m_crashDumpCollectionThread.joinable())
|
|
{
|
|
m_crashDumpCollectionThread.join();
|
|
}
|
|
|
|
// Close the handle to the VM. This will wait for any outstanding callbacks.
|
|
m_system.reset();
|
|
|
|
// This loops helps against a potential crash in build <= Windows 11 22H2.
|
|
for (const auto& e : m_plan9Servers)
|
|
{
|
|
LOG_IF_FAILED(e.second->Teardown());
|
|
}
|
|
|
|
// Shutdown virtio device hosts.
|
|
m_guestDeviceManager.reset();
|
|
|
|
// Call RevokeVmAccess on each VHD that was added to the utility VM. This
|
|
// ensures that the ACL on the VHD does not grow unbounded.
|
|
std::for_each(m_attachedDisks.begin(), m_attachedDisks.end(), [&](const auto& Entry) {
|
|
if ((Entry.first.Type == DiskType::PassThrough) && (WI_IsFlagSet(Entry.second.Flags, DiskStateFlags::Online)))
|
|
{
|
|
RestorePassthroughDiskState(Entry.first.Path.c_str());
|
|
}
|
|
|
|
if (WI_IsFlagSet(Entry.second.Flags, DiskStateFlags::AccessGranted))
|
|
{
|
|
try
|
|
{
|
|
wsl::windows::common::hcs::RevokeVmAccess(m_machineId.c_str(), Entry.first.Path.c_str());
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
});
|
|
|
|
// Delete the swap vhd if one was created.
|
|
if (m_swapFileCreated)
|
|
{
|
|
try
|
|
{
|
|
const auto runAsUser = wil::impersonate_token(m_userToken.get());
|
|
LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_vmConfig.SwapFilePath.c_str()));
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
// Delete the temp folder if it was created.
|
|
if (m_tempDirectoryCreated)
|
|
{
|
|
try
|
|
{
|
|
const auto runAsUser = wil::impersonate_token(m_userToken.get());
|
|
wil::RemoveDirectoryRecursive(m_tempPath.c_str());
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
// Delete the mstsc.exe local devices key if one was created.
|
|
if (m_localDevicesKeyCreated)
|
|
{
|
|
try
|
|
{
|
|
const auto runAsUser = wil::impersonate_token(m_userToken.get());
|
|
const auto userKey = wsl::windows::common::registry::OpenCurrentUser();
|
|
const auto key = wsl::windows::common::registry::CreateKey(userKey.get(), c_localDevicesKey, KEY_SET_VALUE);
|
|
THROW_IF_WIN32_ERROR(::RegDeleteKeyValueW(key.get(), nullptr, m_machineId.c_str()));
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
WSL_LOG("TerminateVmStop");
|
|
}
|
|
|
|
wil::unique_socket WslCoreVm::AcceptConnection(_In_ DWORD ReceiveTimeout, _In_ const std::source_location& Location) const
|
|
{
|
|
auto socket =
|
|
wsl::windows::common::hvsocket::Accept(m_listenSocket.get(), m_vmConfig.KernelBootTimeout, m_terminatingEvent.get(), Location);
|
|
if (ReceiveTimeout != 0)
|
|
{
|
|
THROW_LAST_ERROR_IF(setsockopt(socket.get(), SOL_SOCKET, SO_RCVTIMEO, (const char*)&ReceiveTimeout, sizeof(ReceiveTimeout)) == SOCKET_ERROR);
|
|
}
|
|
|
|
return socket;
|
|
}
|
|
|
|
_Requires_lock_held_(m_guestDeviceLock)
|
|
void WslCoreVm::AddDrvFsShare(_In_ bool Admin, _In_ HANDLE UserToken)
|
|
{
|
|
THROW_HR_IF(HCS_E_TERMINATED, !m_system);
|
|
|
|
// Allow the Plan 9 server to create NT symlinks.
|
|
//
|
|
// N.B. This may fail for unelevated users, however symlink creation will
|
|
// succeed even without this privilege if developer mode is enabled.
|
|
wsl::windows::common::security::EnableTokenPrivilege(UserToken, SE_CREATE_SYMBOLIC_LINK_NAME);
|
|
|
|
// Set the 9p port and virtio tag.
|
|
const UINT32 port = Admin ? LX_INIT_UTILITY_VM_PLAN9_DRVFS_ADMIN_PORT : LX_INIT_UTILITY_VM_PLAN9_DRVFS_PORT;
|
|
const PCWSTR tag = Admin ? TEXT(LX_INIT_DRVFS_ADMIN_VIRTIO_TAG) : TEXT(LX_INIT_DRVFS_VIRTIO_TAG);
|
|
AddPlan9Share(
|
|
TEXT(LX_INIT_UTILITY_VM_DRVFS_SHARE_NAME), L"\\\\?", port, (hcs::Plan9ShareFlags::AllowOptions | hcs::Plan9ShareFlags::AllowSubPaths), UserToken, tag);
|
|
|
|
const auto virtiofsInitialized = Admin ? m_adminDrvfsToken.is_valid() : m_drvfsToken.is_valid();
|
|
if (m_vmConfig.EnableVirtioFs && !virtiofsInitialized)
|
|
{
|
|
// Add virtiofs devices associating indices with paths from the fixed drive bitmap. These devices support
|
|
// multiple mounts in the guest, so this only needs to be done once.
|
|
auto fixedDrives = wsl::windows::common::filesystem::EnumerateFixedDrives(UserToken).first;
|
|
while (fixedDrives != 0)
|
|
{
|
|
ULONG index;
|
|
WI_VERIFY(_BitScanForward(&index, fixedDrives) != FALSE);
|
|
const wchar_t fixedDrivePath[] = {gsl::narrow_cast<wchar_t>(L'A' + index), L':', L'\\', L'\0'};
|
|
AddVirtioFsShare(Admin, fixedDrivePath, TEXT(LX_INIT_DEFAULT_PLAN9_MOUNT_OPTIONS), UserToken);
|
|
fixedDrives ^= (1 << index);
|
|
}
|
|
}
|
|
}
|
|
|
|
_Requires_lock_held_(m_guestDeviceLock)
|
|
void WslCoreVm::AddPlan9Share(
|
|
_In_ PCWSTR AccessName, _In_ PCWSTR Path, [[maybe_unused]] _In_ UINT32 Port, _In_ hcs::Plan9ShareFlags Flags, _In_ HANDLE UserToken, _In_opt_ PCWSTR VirtIoTag)
|
|
{
|
|
bool addNewDevice = false;
|
|
wil::com_ptr<IPlan9FileSystem> server;
|
|
|
|
{
|
|
auto revert = wil::impersonate_token(UserToken);
|
|
|
|
// This is called from AddDrvFsShare, which is called from InitializeDrvFs, so m_guestDeviceLock is
|
|
// already held.
|
|
|
|
if (m_vmConfig.EnableVirtio9p)
|
|
{
|
|
server = m_guestDeviceManager->GetRemoteFileSystem(__uuidof(p9fs::Plan9FileSystem), VirtIoTag);
|
|
}
|
|
else
|
|
{
|
|
const auto existingServer = m_plan9Servers.find(Port);
|
|
if (existingServer != m_plan9Servers.end())
|
|
{
|
|
server = existingServer->second;
|
|
}
|
|
}
|
|
|
|
if (!server)
|
|
{
|
|
server = wsl::windows::common::wslutil::CreateComServerAsUser<p9fs::Plan9FileSystem, IPlan9FileSystem>(UserToken);
|
|
if (m_vmConfig.EnableVirtio9p)
|
|
{
|
|
m_guestDeviceManager->AddRemoteFileSystem(__uuidof(p9fs::Plan9FileSystem), VirtIoTag, server);
|
|
|
|
// Start with one device to handle the first mount request. After
|
|
// each mount, the Plan9 file-system will request additional
|
|
// devices via the IPlan9FileSystemHost::NotifyAllDevicesInUse
|
|
// callback.
|
|
addNewDevice = true;
|
|
}
|
|
else
|
|
{
|
|
THROW_IF_FAILED(server->Init(&m_runtimeId, Port));
|
|
THROW_IF_FAILED(server->Resume());
|
|
m_plan9Servers.insert(std::make_pair(Port, server));
|
|
}
|
|
}
|
|
|
|
HRESULT result = server->AddSharePath(AccessName, Path, static_cast<UINT32>(Flags));
|
|
if (result == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS))
|
|
{
|
|
result = S_OK;
|
|
}
|
|
|
|
THROW_IF_FAILED(result);
|
|
}
|
|
|
|
if (addNewDevice)
|
|
{
|
|
// This requires more privileges than the user may have, so impersonation is disabled.
|
|
(void)m_guestDeviceManager->AddNewDevice(VIRTIO_PLAN9_DEVICE_ID, server, VirtIoTag);
|
|
}
|
|
}
|
|
|
|
ULONG WslCoreVm::AttachDisk(_In_ PCWSTR Disk, _In_ DiskType Type, _In_ std::optional<ULONG> Lun, _In_ bool IsUserDisk, _In_ HANDLE UserToken)
|
|
{
|
|
auto lock = m_lock.lock_exclusive();
|
|
return AttachDiskLockHeld(Disk, Type, MountFlags::None, Lun, IsUserDisk, UserToken);
|
|
}
|
|
|
|
ULONG WslCoreVm::AttachDiskLockHeld(
|
|
_In_ PCWSTR Disk, _In_ DiskType Type, _In_ MountFlags Flags, _In_ std::optional<ULONG> Lun, _In_ bool IsUserDisk, _In_opt_ HANDLE UserToken)
|
|
{
|
|
ExecutionContext context(Context::MountDisk);
|
|
|
|
Lun = ReserveLun(Lun);
|
|
|
|
// Set a scope exit variable to perform cleanup if attaching the disk fails.
|
|
DiskStateFlags diskFlags{};
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
|
|
FreeLun(Lun.value());
|
|
if (WI_IsFlagSet(diskFlags, DiskStateFlags::AccessGranted))
|
|
{
|
|
wsl::windows::common::hcs::RevokeVmAccess(m_machineId.c_str(), Disk);
|
|
}
|
|
|
|
if (WI_IsFlagSet(diskFlags, DiskStateFlags::Online))
|
|
{
|
|
const auto diskHandle = wsl::windows::common::disk::OpenDevice(Disk, GENERIC_READ | GENERIC_WRITE, m_vmConfig.MountDeviceTimeout);
|
|
wsl::windows::common::disk::SetOnline(diskHandle.get(), false, m_vmConfig.MountDeviceTimeout);
|
|
}
|
|
});
|
|
|
|
try
|
|
{
|
|
// Check if the disk is already attached.
|
|
const auto found = m_attachedDisks.find({Type, Disk});
|
|
|
|
if (Type == DiskType::PassThrough)
|
|
{
|
|
if (found != m_attachedDisks.end())
|
|
{
|
|
THROW_HR_WITH_USER_ERROR(WSL_E_DISK_ALREADY_ATTACHED, Localization::MessageDiskAlreadyAttached(Disk));
|
|
}
|
|
|
|
// Grant the VM access to the disk.
|
|
GrantVmWorkerProcessAccessToDisk(Disk, UserToken);
|
|
WI_SetFlag(diskFlags, DiskStateFlags::AccessGranted);
|
|
|
|
// Set the disk online if needed.
|
|
//
|
|
// N.B. The disk handle must be closed prior to adding the disk to the VM.
|
|
{
|
|
const auto diskHandle =
|
|
wsl::windows::common::disk::OpenDevice(Disk, GENERIC_READ | GENERIC_WRITE, m_vmConfig.MountDeviceTimeout);
|
|
if (wsl::windows::common::disk::IsDiskOnline(diskHandle.get()))
|
|
{
|
|
wsl::windows::common::disk::SetOnline(diskHandle.get(), false, m_vmConfig.MountDeviceTimeout);
|
|
WI_SetFlag(diskFlags, DiskStateFlags::Online);
|
|
}
|
|
}
|
|
|
|
// Add the disk to the VM.
|
|
wsl::shared::retry::RetryWithTimeout<void>(
|
|
std::bind(wsl::windows::common::hcs::AddPassThroughDisk, m_system.get(), Disk, Lun.value()),
|
|
wsl::windows::common::disk::c_diskOperationRetry,
|
|
std::chrono::milliseconds(m_vmConfig.MountDeviceTimeout),
|
|
[]() { return wil::ResultFromCaughtException() == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION); });
|
|
}
|
|
else
|
|
{
|
|
if (found != m_attachedDisks.end())
|
|
{
|
|
// Prevent user from launching a distro vhd after manually mounting it; otherwise, return the LUN of the mounted disk.
|
|
THROW_HR_IF(WSL_E_USER_VHD_ALREADY_ATTACHED, found->first.User);
|
|
|
|
return found->second.Lun;
|
|
}
|
|
|
|
auto grantDiskAccess = [&]() {
|
|
auto runAsUser = wil::impersonate_token(UserToken);
|
|
wsl::windows::common::hcs::GrantVmAccess(m_machineId.c_str(), Disk);
|
|
WI_SetFlag(diskFlags, DiskStateFlags::AccessGranted);
|
|
};
|
|
|
|
// Grant the VM access to the disk.
|
|
if (WI_IsFlagClear(Flags, MountFlags::ReadOnly))
|
|
{
|
|
grantDiskAccess();
|
|
}
|
|
|
|
auto result = wil::ResultFromException([&]() {
|
|
wsl::windows::common::hcs::AddVhd(m_system.get(), Disk, Lun.value(), WI_IsFlagSet(Flags, MountFlags::ReadOnly));
|
|
});
|
|
|
|
if (result == HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED) && WI_IsFlagClear(diskFlags, DiskStateFlags::AccessGranted))
|
|
{
|
|
grantDiskAccess();
|
|
wsl::windows::common::hcs::AddVhd(m_system.get(), Disk, Lun.value(), WI_IsFlagSet(Flags, MountFlags::ReadOnly));
|
|
}
|
|
else
|
|
{
|
|
THROW_IF_FAILED(result);
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
const auto result = wil::ResultFromCaughtException();
|
|
THROW_HR_WITH_USER_ERROR(
|
|
result, Localization::MessageFailedToAttachDisk(Disk, wsl::windows::common::wslutil::GetSystemErrorString(result)));
|
|
}
|
|
|
|
m_attachedDisks.emplace(AttachedDisk{Type, Disk, IsUserDisk}, DiskState{Lun.value(), {}, diskFlags});
|
|
cleanup.release();
|
|
|
|
return Lun.value();
|
|
}
|
|
|
|
void WslCoreVm::CollectCrashDumps(wil::unique_socket&& listenSocket) const
|
|
{
|
|
wsl::windows::common::wslutil::SetThreadDescription(L"CrashDumpCollection");
|
|
|
|
while (!m_terminatingEvent.is_signaled())
|
|
{
|
|
try
|
|
{
|
|
auto socket = wsl::windows::common::hvsocket::Accept(listenSocket.get(), INFINITE, m_terminatingEvent.get());
|
|
|
|
DWORD receiveTimeout = m_vmConfig.KernelBootTimeout;
|
|
THROW_LAST_ERROR_IF(
|
|
setsockopt(listenSocket.get(), SOL_SOCKET, SO_RCVTIMEO, (const char*)&receiveTimeout, sizeof(receiveTimeout)) == SOCKET_ERROR);
|
|
|
|
auto channel = wsl::shared::SocketChannel{std::move(socket), "crash_dump", m_terminatingEvent.get()};
|
|
|
|
const auto& message = channel.ReceiveMessage<LX_PROCESS_CRASH>();
|
|
const char* process = reinterpret_cast<const char*>(&message.Buffer);
|
|
|
|
constexpr auto dumpExtension = ".dmp";
|
|
constexpr auto dumpPrefix = "wsl-crash";
|
|
|
|
auto filename = std::format("{}-{}-{}-{}-{}{}", dumpPrefix, message.Timestamp, message.Pid, process, message.Signal, dumpExtension);
|
|
|
|
std::replace_if(filename.begin(), filename.end(), [](auto e) { return !std::isalnum(e) && e != '.' && e != '-'; }, '_');
|
|
|
|
auto fullPath = m_vmConfig.CrashDumpFolder / filename;
|
|
|
|
// Log telemetry when there is a crash within the WSL VM
|
|
WSL_LOG_TELEMETRY(
|
|
"LinuxCrash",
|
|
PDT_ProductAndServicePerformance,
|
|
TraceLoggingValue(fullPath.c_str(), "FullPath"),
|
|
TraceLoggingValue(message.Pid, "Pid"),
|
|
TraceLoggingValue(message.Signal, "Signal"),
|
|
TraceLoggingValue(process, "process"));
|
|
|
|
auto runAsUser = wil::impersonate_token(m_userToken.get());
|
|
|
|
std::error_code error;
|
|
std::filesystem::create_directories(m_vmConfig.CrashDumpFolder, error);
|
|
if (error.value())
|
|
{
|
|
THROW_WIN32_MSG(error.value(), "Failed to create folder: %ls", m_vmConfig.CrashDumpFolder.c_str());
|
|
}
|
|
|
|
// Only delete files that:
|
|
// - have the temporary flag set
|
|
// - start with 'wsl-crash'
|
|
// - end in .dmp
|
|
//
|
|
// This logic is here to prevent accidental user file deletion
|
|
|
|
auto pred = [&dumpExtension, &dumpPrefix](const auto& e) {
|
|
return WI_IsFlagSet(GetFileAttributes(e.path().c_str()), FILE_ATTRIBUTE_TEMPORARY) && e.path().has_extension() &&
|
|
e.path().extension() == dumpExtension && e.path().has_filename() &&
|
|
e.path().filename().string().find(dumpPrefix) == 0;
|
|
};
|
|
|
|
wsl::windows::common::wslutil::EnforceFileLimit(m_vmConfig.CrashDumpFolder.c_str(), m_vmConfig.MaxCrashDumpCount, pred);
|
|
|
|
wil::unique_hfile file{CreateFileW(fullPath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY, nullptr)};
|
|
THROW_LAST_ERROR_IF(!file);
|
|
|
|
channel.SendResultMessage<std::int32_t>(0);
|
|
|
|
wsl::windows::common::relay::InterruptableRelay(reinterpret_cast<HANDLE>(channel.Socket()), file.get(), nullptr);
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<LxssRunningInstance> WslCoreVm::CreateInstance(
|
|
_In_ const GUID& InstanceId,
|
|
_In_ const LXSS_DISTRO_CONFIGURATION& Configuration,
|
|
_In_ LX_MESSAGE_TYPE MessageType,
|
|
_In_ DWORD ReceiveTimeout,
|
|
_In_ ULONG DefaultUid,
|
|
_In_ ULONG64 ClientLifetimeId,
|
|
_In_ ULONG ExportFlags,
|
|
_Out_opt_ ULONG* ConnectPort)
|
|
{
|
|
// Add the VHD to the machine.
|
|
auto lock = m_lock.lock_exclusive();
|
|
const auto lun = AttachDiskLockHeld(Configuration.VhdFilePath.c_str(), DiskType::VHD, MountFlags::None, {}, false, m_userToken.get());
|
|
|
|
// Launch the init daemon and create the instance.
|
|
int flags = LxMiniInitMessageFlagNone;
|
|
std::wstring sharedMemoryRoot{};
|
|
|
|
#ifdef WSL_DEV_INSTALL_PATH
|
|
|
|
std::wstring installPath = TEXT(WSL_DEV_INSTALL_PATH);
|
|
|
|
#else
|
|
|
|
std::wstring installPath = m_installPath.wstring();
|
|
|
|
#endif
|
|
|
|
std::wstring userProfile{};
|
|
if (LXSS_ENABLE_GUI_APPS() && (MessageType == LxMiniInitMessageLaunchInit))
|
|
{
|
|
WI_SetFlag(flags, LxMiniInitMessageFlagLaunchSystemDistro);
|
|
sharedMemoryRoot = m_sharedMemoryRoot;
|
|
|
|
userProfile = m_userProfile;
|
|
}
|
|
|
|
WI_SetFlagIf(flags, LxMiniInitMessageFlagExportCompressGzip, WI_IsFlagSet(ExportFlags, LXSS_EXPORT_DISTRO_FLAGS_GZIP));
|
|
WI_SetFlagIf(flags, LxMiniInitMessageFlagExportCompressXzip, WI_IsFlagSet(ExportFlags, LXSS_EXPORT_DISTRO_FLAGS_XZIP));
|
|
WI_SetFlagIf(flags, LxMiniInitMessageFlagVerbose, WI_IsFlagSet(ExportFlags, LXSS_EXPORT_DISTRO_FLAGS_VERBOSE));
|
|
|
|
wsl::shared::MessageWriter<LX_MINI_INIT_MESSAGE> message(MessageType);
|
|
message->MountDeviceType = LxMiniInitMountDeviceTypeLun;
|
|
message->DeviceId = lun;
|
|
message->Flags = flags;
|
|
message.WriteString(message->FsTypeOffset, "ext4");
|
|
message.WriteString(message->MountOptionsOffset, "discard,errors=remount-ro,data=ordered");
|
|
message.WriteString(message->VmIdOffset, m_machineId);
|
|
message.WriteString(message->DistributionNameOffset, Configuration.Name);
|
|
message.WriteString(message->SharedMemoryRootOffset, sharedMemoryRoot);
|
|
message.WriteString(message->InstallPathOffset, installPath);
|
|
message.WriteString(message->UserProfileOffset, userProfile);
|
|
m_miniInitChannel.SendMessage<LX_MINI_INIT_MESSAGE>(message.Span());
|
|
|
|
return CreateInstanceInternal(
|
|
InstanceId, Configuration, ReceiveTimeout, DefaultUid, ClientLifetimeId, WI_IsFlagSet(flags, LxMiniInitMessageFlagLaunchSystemDistro), ConnectPort);
|
|
}
|
|
|
|
std::shared_ptr<LxssRunningInstance> WslCoreVm::CreateInstanceInternal(
|
|
_In_ const GUID& InstanceId,
|
|
_In_ const LXSS_DISTRO_CONFIGURATION& Configuration,
|
|
_In_ DWORD ReceiveTimeout,
|
|
_In_ ULONG DefaultUid,
|
|
_In_ ULONG64 ClientLifetimeId,
|
|
_In_ bool LaunchSystemDistro,
|
|
_Out_opt_ ULONG* ConnectPort)
|
|
{
|
|
// Clear the drive mounting flag if support is disabled at the VM level.
|
|
//
|
|
// N.B. If the system distro is enabled the share will still be created since
|
|
// GUI apps require access to the Windows file system in order to launch mstsc.
|
|
LXSS_DISTRO_CONFIGURATION localConfig = Configuration;
|
|
WI_ClearFlagIf(localConfig.Flags, LXSS_DISTRO_FLAGS_ENABLE_DRIVE_MOUNTING, !m_vmConfig.EnableHostFileSystemAccess);
|
|
|
|
// Establish a communication channel with the init daemon.
|
|
auto initSocket = AcceptConnection(ReceiveTimeout);
|
|
|
|
// If the system distro is enabled, establish a communication channel with its init daemon.
|
|
wil::unique_socket systemDistroSocket;
|
|
if (LaunchSystemDistro)
|
|
{
|
|
WI_ASSERT(m_vmConfig.EnableGuiApps);
|
|
systemDistroSocket = AcceptConnection(ReceiveTimeout);
|
|
}
|
|
|
|
// Set feature flags for the instance.
|
|
ULONG featureFlags{};
|
|
WI_SetFlagIf(featureFlags, LxInitFeatureVirtIo9p, m_vmConfig.EnableVirtio9p);
|
|
WI_SetFlagIf(featureFlags, LxInitFeatureVirtIoFs, m_vmConfig.EnableVirtioFs);
|
|
WI_SetFlagIf(featureFlags, LxInitFeatureDnsTunneling, m_vmConfig.EnableDnsTunneling);
|
|
|
|
// Create an instance, this takes ownership of the sockets.
|
|
auto instance = std::make_shared<WslCoreInstance>(
|
|
m_userToken.get(),
|
|
initSocket,
|
|
systemDistroSocket,
|
|
InstanceId,
|
|
m_runtimeId,
|
|
localConfig,
|
|
DefaultUid,
|
|
ClientLifetimeId,
|
|
std::bind(s_InitializeDrvFs, this, std::placeholders::_1),
|
|
featureFlags,
|
|
m_vmConfig.DistributionStartTimeout,
|
|
m_vmConfig.InstanceIdleTimeout,
|
|
ConnectPort);
|
|
|
|
WI_ASSERT(!initSocket && !systemDistroSocket);
|
|
|
|
return instance;
|
|
}
|
|
|
|
wil::unique_socket WslCoreVm::CreateListeningSocket() const
|
|
{
|
|
return wsl::windows::common::hvsocket::Listen(m_runtimeId, 0);
|
|
}
|
|
|
|
std::pair<int, LX_MINI_MOUNT_STEP> WslCoreVm::DetachDisk(_In_opt_ PCWSTR Disk)
|
|
{
|
|
bool deleted = !ARGUMENT_PRESENT(Disk);
|
|
std::vector<AttachedDisk> selectedDisks;
|
|
|
|
auto diskMatches = [TargetPath = Disk](const AttachedDisk& disk) {
|
|
if (!disk.User)
|
|
{
|
|
// Only user mounted disks can be detached
|
|
return false;
|
|
}
|
|
|
|
if (disk.Type == DiskType::VHD)
|
|
{
|
|
// N.B. std::filesystem::equivalent can throw if the path is malformed so use the noexcept variant.
|
|
std::error_code error{};
|
|
return TargetPath == nullptr || std::filesystem::equivalent(disk.Path, TargetPath, error);
|
|
}
|
|
else if (disk.Type == DiskType::PassThrough)
|
|
{
|
|
return TargetPath == nullptr || wsl::windows::common::string::IsPathComponentEqual(disk.Path, TargetPath);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
auto lock = m_lock.lock_exclusive();
|
|
for (auto it = m_attachedDisks.begin(); it != m_attachedDisks.end();)
|
|
{
|
|
if (diskMatches(it->first))
|
|
{
|
|
// Unmount any mounted volumes inside the utility VM.
|
|
const auto result = UnmountDisk(it->first, it->second);
|
|
if (result.first != 0)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
// Detach the disk from the VM.
|
|
wsl::windows::common::hcs::RemoveScsiDisk(m_system.get(), it->second.Lun);
|
|
if (WI_VERIFY(WI_IsFlagSet(it->second.Flags, DiskStateFlags::AccessGranted)))
|
|
{
|
|
wsl::windows::common::hcs::RevokeVmAccess(m_machineId.c_str(), it->first.Path.c_str());
|
|
}
|
|
|
|
FreeLun(it->second.Lun);
|
|
|
|
// If the disk was online before being attached, revert to that state.
|
|
if (WI_IsFlagSet(it->second.Flags, DiskStateFlags::Online))
|
|
{
|
|
RestorePassthroughDiskState(it->first.Path.c_str());
|
|
}
|
|
|
|
deleted = true;
|
|
it = m_attachedDisks.erase(it);
|
|
}
|
|
else
|
|
{
|
|
++it;
|
|
}
|
|
}
|
|
|
|
THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND), !deleted);
|
|
|
|
return std::make_pair(0, LxMiniInitMountStepNone);
|
|
}
|
|
|
|
void WslCoreVm::EjectVhd(_In_ PCWSTR VhdPath)
|
|
{
|
|
auto lock = m_lock.lock_exclusive();
|
|
return EjectVhdLockHeld(VhdPath);
|
|
}
|
|
|
|
_Requires_lock_held_(m_lock)
|
|
void WslCoreVm::EjectVhdLockHeld(_In_ PCWSTR VhdPath)
|
|
{
|
|
const auto search = m_attachedDisks.find({DiskType::VHD, VhdPath});
|
|
if (search != m_attachedDisks.end())
|
|
{
|
|
EJECT_VHD_MESSAGE message;
|
|
message.Header.MessageSize = sizeof(message);
|
|
message.Header.MessageType = LxMiniInitMessageEjectVhd;
|
|
message.Lun = search->second.Lun;
|
|
const auto& result = m_miniInitChannel.Transaction(message);
|
|
LOG_HR_IF_MSG(E_UNEXPECTED, result.Result != 0, "VHD eject failed: %u", result.Result);
|
|
|
|
// Impersonate the session manager and remove the vhd.
|
|
{
|
|
auto runAsSelf = wil::run_as_self();
|
|
wsl::windows::common::hcs::RemoveScsiDisk(m_system.get(), search->second.Lun);
|
|
if (WI_IsFlagSet(search->second.Flags, DiskStateFlags::AccessGranted))
|
|
{
|
|
wsl::windows::common::hcs::RevokeVmAccess(m_machineId.c_str(), VhdPath);
|
|
}
|
|
}
|
|
|
|
m_attachedDisks.erase(search);
|
|
FreeLun(message.Lun);
|
|
}
|
|
}
|
|
|
|
_Requires_lock_held_(m_guestDeviceLock)
|
|
std::optional<WslCoreVm::VirtioFsShare> WslCoreVm::FindVirtioFsShare(_In_ PCWSTR tag, _In_ std::optional<bool> Admin) const
|
|
{
|
|
for (const auto& share : m_virtioFsShares)
|
|
{
|
|
if ((share.second == tag) && (!Admin.has_value() || Admin.value() == share.first.Admin))
|
|
{
|
|
return share.first;
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void WslCoreVm::FreeLun(_In_ ULONG lun)
|
|
{
|
|
WI_ASSERT(m_lunBitmap[lun]);
|
|
m_lunBitmap.set(lun, false);
|
|
}
|
|
|
|
std::wstring WslCoreVm::GenerateConfigJson()
|
|
{
|
|
hcs::ComputeSystem systemSettings{};
|
|
systemSettings.Owner = wsl::windows::common::wslutil::c_vmOwner;
|
|
systemSettings.ShouldTerminateOnLastHandleClosed = true;
|
|
systemSettings.SchemaVersion.Major = 2;
|
|
systemSettings.SchemaVersion.Minor = 3;
|
|
hcs::VirtualMachine vmSettings{};
|
|
vmSettings.StopOnReset = true;
|
|
vmSettings.Chipset.UseUtc = true;
|
|
|
|
// Ensure the 2MB granularity enforced by HCS.
|
|
vmSettings.ComputeTopology.Memory.SizeInMB = ((m_vmConfig.MemorySizeBytes / _1MB) & ~0x1);
|
|
vmSettings.ComputeTopology.Memory.AllowOvercommit = true;
|
|
vmSettings.ComputeTopology.Memory.EnableDeferredCommit = true;
|
|
vmSettings.ComputeTopology.Memory.EnableColdDiscardHint = true;
|
|
|
|
// Configure backing page size, fault cluster shift size, and cold discard hint size to favor density (lower vmmem usage).
|
|
//
|
|
// N.B. Cold discard hint size should be a multiple of the fault cluster shift size.
|
|
//
|
|
// N.B. This is only done on builds that have the fix for the VID deadlock on partition teardown.
|
|
if ((m_windowsVersion.BuildNumber >= WindowsBuildNumbers::Germanium) ||
|
|
(m_windowsVersion.BuildNumber >= WindowsBuildNumbers::Cobalt && m_windowsVersion.UpdateBuildRevision >= 2360) ||
|
|
(m_windowsVersion.BuildNumber >= WindowsBuildNumbers::Iron && m_windowsVersion.UpdateBuildRevision >= 1970) ||
|
|
(m_windowsVersion.BuildNumber >= WindowsBuildNumbers::Vibranium_22H2 && m_windowsVersion.UpdateBuildRevision >= 3393))
|
|
{
|
|
vmSettings.ComputeTopology.Memory.BackingPageSize = hcs::MemoryBackingPageSize::Small;
|
|
vmSettings.ComputeTopology.Memory.FaultClusterSizeShift = 4; // 64k
|
|
vmSettings.ComputeTopology.Memory.DirectMapFaultClusterSizeShift = 4; // 64k
|
|
m_coldDiscardShiftSize = 5; // 128k
|
|
}
|
|
else
|
|
{
|
|
m_coldDiscardShiftSize = 9; // 2MB
|
|
}
|
|
|
|
// May need more MMIO than the default 16GB. WSL uses a vpci device per Plan9 share, WSLg adds a GPU device,
|
|
// and a pmem device, and each shared memory virtiofs device needs more than 8GB of MMIO.
|
|
SafeInt<INT64> highMmioGapInMB = DEFAULT_HIGH_MMIO_GAP_IN_MB;
|
|
|
|
// Add additional MMIO space for the system distro and WSLg.
|
|
bool privateSystemDistro = !m_vmConfig.SystemDistroPath.empty();
|
|
if (!privateSystemDistro)
|
|
{
|
|
#ifdef WSL_SYSTEM_DISTRO_PATH
|
|
|
|
m_vmConfig.SystemDistroPath = TEXT(WSL_SYSTEM_DISTRO_PATH);
|
|
privateSystemDistro = true;
|
|
|
|
#else
|
|
|
|
m_systemDistroDeviceType = LxMiniInitMountDeviceTypeLun;
|
|
m_vmConfig.SystemDistroPath = (m_installPath / L"system.vhd").wstring();
|
|
WI_ASSERT(wsl::windows::common::filesystem::FileExists(m_vmConfig.SystemDistroPath.c_str()));
|
|
|
|
#endif
|
|
}
|
|
|
|
// Ensure the system distro exists and ends with a img or vhd file extension.
|
|
if (privateSystemDistro)
|
|
{
|
|
if (wsl::windows::common::string::IsPathComponentEqual(m_vmConfig.SystemDistroPath.extension().native(), L".img"))
|
|
{
|
|
m_systemDistroDeviceType = LxMiniInitMountDeviceTypePmem;
|
|
}
|
|
else if (wsl::windows::common::string::IsPathComponentEqual(m_vmConfig.SystemDistroPath.extension().native(), L".vhd"))
|
|
{
|
|
m_systemDistroDeviceType = LxMiniInitMountDeviceTypeLun;
|
|
}
|
|
|
|
THROW_HR_IF(
|
|
WSL_E_CUSTOM_SYSTEM_DISTRO_ERROR,
|
|
(m_systemDistroDeviceType == LxMiniInitMountDeviceTypeInvalid) ||
|
|
(!wsl::windows::common::filesystem::FileExists(m_vmConfig.SystemDistroPath.c_str())));
|
|
}
|
|
|
|
// Add MMIO space for the WSLg virtio shared memory device.
|
|
if (m_vmConfig.EnableGuiApps && m_vmConfig.EnableVirtio)
|
|
{
|
|
highMmioGapInMB += WSLG_SHARED_MEMORY_SIZE_MB + EXTRA_MMIO_SIZE_PER_VIRTIOFS_DEVICE_IN_MB;
|
|
}
|
|
|
|
// If using pmem for the system distro, add MMIO space for the device.
|
|
if (m_systemDistroDeviceType == LxMiniInitMountDeviceTypePmem)
|
|
{
|
|
highMmioGapInMB += RequiredExtraMmioSpaceForPmemFileInMb(m_vmConfig.SystemDistroPath.c_str());
|
|
}
|
|
|
|
// Log telemetry to measure system distro usage.
|
|
WSL_LOG(
|
|
"InitializeSystemDistro",
|
|
TraceLoggingValue(static_cast<INT64>(highMmioGapInMB), "highMmioGapInMB"),
|
|
TraceLoggingValue(privateSystemDistro, "privateSystemDistro"),
|
|
TraceLoggingValue(static_cast<DWORD>(m_systemDistroDeviceType), "systemDistroDeviceType"),
|
|
TraceLoggingLevel(WINEVENT_LEVEL_INFO));
|
|
|
|
vmSettings.ComputeTopology.Memory.HighMmioGapInMB = highMmioGapInMB;
|
|
|
|
// The guest may only be able to access 36-bits of address space (minimum supported), so shift the high MMIO base
|
|
// down such that all addresses are accessible. The default starting point is 16G below the maximum 36-bit address,
|
|
// so for guests that support larger address spaces, the default base should suffice.
|
|
vmSettings.ComputeTopology.Memory.HighMmioBaseInMB = MAX_36_BIT_PAGE_IN_MB - highMmioGapInMB;
|
|
|
|
// Configure the number of processors.
|
|
vmSettings.ComputeTopology.Processor.Count = m_vmConfig.ProcessorCount;
|
|
|
|
// Set the vmmem suffix which will change the process name in task manager.
|
|
if (helpers::IsVmemmSuffixSupported())
|
|
{
|
|
vmSettings.ComputeTopology.Memory.HostingProcessNameSuffix = wsl::windows::common::wslutil::c_vmOwner;
|
|
}
|
|
|
|
// If nested virtualization was requested, ensure the platform supports it.
|
|
//
|
|
// N.B. This is done because arm64 and some older amd64 processors do not support nested virtualization.
|
|
// Nested virtualization not supported on Windows 10.
|
|
if (m_vmConfig.EnableNestedVirtualization)
|
|
{
|
|
try
|
|
{
|
|
if (wsl::windows::common::helpers::IsWindows11OrAbove())
|
|
{
|
|
const auto& processorFeatures = wsl::windows::common::hcs::GetProcessorFeatures();
|
|
auto feature = std::find(processorFeatures.begin(), processorFeatures.end(), "NestedVirt");
|
|
m_vmConfig.EnableNestedVirtualization = (feature != processorFeatures.end());
|
|
}
|
|
else
|
|
{
|
|
m_vmConfig.EnableNestedVirtualization = false;
|
|
}
|
|
|
|
vmSettings.ComputeTopology.Processor.ExposeVirtualizationExtensions = m_vmConfig.EnableNestedVirtualization;
|
|
if (!m_vmConfig.EnableNestedVirtualization)
|
|
{
|
|
EMIT_USER_WARNING(wsl::shared::Localization::MessageNestedVirtualizationNotSupported());
|
|
}
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
#ifdef _AMD64_
|
|
|
|
// Enable hardware performance counters if they are supported.
|
|
if (m_vmConfig.EnableHardwarePerformanceCounters)
|
|
{
|
|
HV_X64_HYPERVISOR_HARDWARE_FEATURES hardwareFeatures{};
|
|
__cpuid(reinterpret_cast<int*>(&hardwareFeatures), HvCpuIdFunctionMsHvHardwareFeatures);
|
|
vmSettings.ComputeTopology.Processor.EnablePerfmonPmu = hardwareFeatures.ChildPerfmonPmuSupported != 0;
|
|
vmSettings.ComputeTopology.Processor.EnablePerfmonLbr = hardwareFeatures.ChildPerfmonLbrSupported != 0;
|
|
}
|
|
|
|
#endif
|
|
|
|
// Initialize kernel command line.
|
|
std::wstring kernelCmdLine = L"initrd=\\" LXSS_VM_MODE_INITRD_NAME L" " TEXT(WSL_ROOT_INIT_ENV) L"=1 panic=-1";
|
|
|
|
// Set number of processors.
|
|
kernelCmdLine += std::format(L" nr_cpus={}", m_vmConfig.ProcessorCount);
|
|
|
|
// Enable timesync workaround to sync on resume from sleep in modern standby.
|
|
kernelCmdLine += L" hv_utils.timesync_implicit=1";
|
|
|
|
// If using virtio-9p, enable SWIOTLB as a perf optimization (will cause VM to consume 64MB more memory).
|
|
if (m_vmConfig.EnableVirtio9p)
|
|
{
|
|
kernelCmdLine += L" swiotlb=force";
|
|
}
|
|
|
|
if (m_vmConfig.EnableVirtio && helpers::IsVirtioSerialConsoleSupported())
|
|
{
|
|
vmSettings.Devices.VirtioSerial.emplace();
|
|
}
|
|
|
|
if (m_dmesgCollector)
|
|
{
|
|
if (m_vmConfig.EnableEarlyBootLogging)
|
|
{
|
|
// Capture using the very slow legacy serial port up until the point that the virtio device is started.
|
|
if constexpr (!wsl::shared::Arm64)
|
|
{
|
|
kernelCmdLine += L" earlycon=uart8250,io,0x3f8,115200";
|
|
}
|
|
else
|
|
{
|
|
kernelCmdLine += L" earlycon=pl011,0xeffec000,115200";
|
|
}
|
|
|
|
vmSettings.Devices.ComPorts["0"] = hcs::ComPort{m_dmesgCollector->EarlyConsoleName()};
|
|
}
|
|
|
|
// The primary "console" will be a virtio serial device.
|
|
kernelCmdLine += L" console=hvc0 debug";
|
|
hcs::VirtioSerialPort virtioPort{};
|
|
virtioPort.Name = L"hvc0";
|
|
virtioPort.NamedPipe = m_dmesgCollector->VirtioConsoleName();
|
|
virtioPort.ConsoleSupport = true;
|
|
vmSettings.Devices.VirtioSerial->Ports["0"] = std::move(virtioPort);
|
|
}
|
|
else if (m_vmConfig.EnableDebugConsole)
|
|
{
|
|
// If a debug console was requested, add required kernel command line options.
|
|
if constexpr (!wsl::shared::Arm64)
|
|
{
|
|
kernelCmdLine += L" console=ttyS0,115200 debug";
|
|
}
|
|
else
|
|
{
|
|
kernelCmdLine += L" console=ttyAMA0 debug";
|
|
}
|
|
}
|
|
|
|
//
|
|
// N.B. The ordering of these devices is important because it determines the order they show up as
|
|
// /dev/hvc devices in the guest.
|
|
//
|
|
|
|
if (m_gnsTelemetryLogger)
|
|
{
|
|
hcs::VirtioSerialPort virtioPort;
|
|
virtioPort.Name = TEXT(LX_INIT_HVC_TELEMETRY);
|
|
virtioPort.NamedPipe = m_gnsTelemetryLogger->GetPipeName();
|
|
virtioPort.ConsoleSupport = true;
|
|
vmSettings.Devices.VirtioSerial->Ports["1"] = std::move(virtioPort);
|
|
}
|
|
|
|
if (!m_debugShellPipe.empty())
|
|
{
|
|
hcs::VirtioSerialPort virtioPort;
|
|
virtioPort.Name = TEXT(LX_INIT_HVC_DEBUG_SHELL);
|
|
virtioPort.NamedPipe = m_debugShellPipe;
|
|
virtioPort.ConsoleSupport = true;
|
|
vmSettings.Devices.VirtioSerial->Ports["2"] = std::move(virtioPort);
|
|
}
|
|
|
|
// Ensure that virtio serial devices have unique names.
|
|
if constexpr (wsl::shared::Debug)
|
|
{
|
|
if (vmSettings.Devices.VirtioSerial)
|
|
{
|
|
std::set<std::wstring_view> uniqueNames;
|
|
for (const auto& device : vmSettings.Devices.VirtioSerial->Ports)
|
|
{
|
|
uniqueNames.emplace(device.second.Name);
|
|
}
|
|
|
|
WI_ASSERT_MSG(
|
|
uniqueNames.size() == vmSettings.Devices.VirtioSerial->Ports.size(), "Serial device names must be unique.");
|
|
}
|
|
}
|
|
|
|
// If a kernel debugger was requested, add required kernel command line options and
|
|
// generate the name of the pipe.
|
|
if (m_vmConfig.KernelDebugPort != 0)
|
|
{
|
|
PCWSTR debugDeviceName = nullptr;
|
|
if constexpr (wsl::shared::Arm64)
|
|
{
|
|
debugDeviceName = L"ttyAMA1";
|
|
}
|
|
else
|
|
{
|
|
debugDeviceName = L"ttyS1";
|
|
}
|
|
|
|
kernelCmdLine += std::format(L" pty.legacy_count=2 kgdboc={},115200", debugDeviceName);
|
|
|
|
m_comPipe1 = wsl::windows::common::helpers::GetUniquePipeName();
|
|
wsl::windows::common::helpers::LaunchKdRelay(
|
|
m_comPipe1.c_str(), m_restrictedToken.get(), m_vmConfig.KernelDebugPort, m_terminatingEvent.get(), !m_vmConfig.EnableTelemetry);
|
|
}
|
|
else
|
|
{
|
|
kernelCmdLine += L" pty.legacy_count=0";
|
|
}
|
|
|
|
if (!m_comPipe0.empty() && (!m_dmesgCollector || !m_vmConfig.EnableEarlyBootLogging))
|
|
{
|
|
vmSettings.Devices.ComPorts["0"] = hcs::ComPort{m_comPipe0};
|
|
}
|
|
|
|
if (!m_comPipe1.empty())
|
|
{
|
|
vmSettings.Devices.ComPorts["1"] = hcs::ComPort{m_comPipe1};
|
|
}
|
|
|
|
if (m_vmConfig.MaxCrashDumpCount >= 0)
|
|
{
|
|
kernelCmdLine += L" " WSL_ENABLE_CRASH_DUMP_ENV L"=1";
|
|
}
|
|
|
|
// Add user-specified kernel command line options at the end.
|
|
if (!m_vmConfig.KernelCommandLine.empty())
|
|
{
|
|
kernelCmdLine += L" ";
|
|
kernelCmdLine += m_vmConfig.KernelCommandLine;
|
|
}
|
|
|
|
// Set up boot params.
|
|
//
|
|
// N.B. Linux kernel direct boot is not yet supported on ARM64.
|
|
if constexpr (!wsl::shared::Arm64)
|
|
{
|
|
auto linuxKernelDirect = hcs::LinuxKernelDirect{};
|
|
linuxKernelDirect.KernelFilePath = m_vmConfig.KernelPath.c_str();
|
|
linuxKernelDirect.InitRdPath = (m_rootFsPath / LXSS_VM_MODE_INITRD_NAME).c_str();
|
|
linuxKernelDirect.KernelCmdLine = kernelCmdLine;
|
|
vmSettings.Chipset.LinuxKernelDirect = std::move(linuxKernelDirect);
|
|
}
|
|
else
|
|
{
|
|
auto bootThis = hcs::UefiBootEntry{};
|
|
bootThis.DeviceType = hcs::UefiBootDevice::VmbFs;
|
|
bootThis.VmbFsRootPath = m_rootFsPath.c_str();
|
|
bootThis.DevicePath = L"\\" LXSS_VM_MODE_KERNEL_NAME;
|
|
bootThis.OptionalData = kernelCmdLine;
|
|
hcs::Uefi uefiSettings{};
|
|
uefiSettings.BootThis = std::move(bootThis);
|
|
vmSettings.Chipset.Uefi = std::move(uefiSettings);
|
|
}
|
|
|
|
// Initialize SCSI devices.
|
|
hcs::Scsi scsiController{};
|
|
auto attachDisk = [&](PCWSTR path) {
|
|
auto lun = ReserveLun();
|
|
hcs::Attachment disk{};
|
|
disk.Type = hcs::AttachmentType::VirtualDisk;
|
|
disk.Path = path;
|
|
disk.ReadOnly = true;
|
|
disk.SupportCompressedVolumes = true;
|
|
disk.AlwaysAllowSparseFiles = true;
|
|
disk.SupportEncryptedFiles = true;
|
|
scsiController.Attachments[std::to_string(lun)] = std::move(disk);
|
|
m_attachedDisks.emplace(AttachedDisk{DiskType::VHD, path, false}, DiskState{lun, {}, {}});
|
|
return lun;
|
|
};
|
|
|
|
if (m_systemDistroDeviceType == LxMiniInitMountDeviceTypeLun)
|
|
{
|
|
m_systemDistroDeviceId = attachDisk(m_vmConfig.SystemDistroPath.c_str());
|
|
}
|
|
|
|
if (!m_vmConfig.KernelModulesPath.empty())
|
|
{
|
|
m_kernelModulesDeviceId = attachDisk(m_vmConfig.KernelModulesPath.c_str());
|
|
}
|
|
|
|
vmSettings.Devices.Scsi["0"] = std::move(scsiController);
|
|
|
|
// Construct a security descriptor that allows system and the current user.
|
|
wil::unique_hlocal_string userSidString;
|
|
THROW_LAST_ERROR_IF(!ConvertSidToStringSidW(&m_userSid.Sid, &userSidString));
|
|
|
|
std::wstring securityDescriptor{L"D:P(A;;FA;;;SY)(A;;FA;;;"};
|
|
securityDescriptor += userSidString.get();
|
|
securityDescriptor += L")";
|
|
hcs::HvSocket hvSocketConfig{};
|
|
hvSocketConfig.HvSocketConfig.DefaultBindSecurityDescriptor = securityDescriptor;
|
|
hvSocketConfig.HvSocketConfig.DefaultConnectSecurityDescriptor = securityDescriptor;
|
|
vmSettings.Devices.HvSocket = std::move(hvSocketConfig);
|
|
|
|
// N.B. Plan9 device is always added during serialization
|
|
|
|
systemSettings.VirtualMachine = std::move(vmSettings);
|
|
return wsl::shared::ToJsonW(systemSettings);
|
|
}
|
|
|
|
std::pair<int, LX_MINI_MOUNT_STEP> WslCoreVm::GetMountResult(_In_ wsl::shared::SocketChannel& Channel)
|
|
{
|
|
// Read the response from mini_init.
|
|
const auto& Message = Channel.ReceiveMessage<LX_MINI_INIT_MOUNT_RESULT_MESSAGE>();
|
|
return std::make_pair(Message.Result, Message.FailureStep);
|
|
}
|
|
|
|
const wsl::core::Config& WslCoreVm::GetConfig() const noexcept
|
|
{
|
|
return m_vmConfig;
|
|
}
|
|
|
|
GUID WslCoreVm::GetRuntimeId() const
|
|
{
|
|
return m_runtimeId;
|
|
}
|
|
|
|
int WslCoreVm::GetVmIdleTimeout() const
|
|
{
|
|
return m_vmConfig.VmIdleTimeout;
|
|
}
|
|
|
|
void WslCoreVm::GrantVmWorkerProcessAccessToDisk(_In_ PCWSTR Disk, _In_opt_ HANDLE UserToken) const
|
|
{
|
|
if (ARGUMENT_PRESENT(UserToken))
|
|
{
|
|
// Impersonating the user doesn't let us access a block device,
|
|
// check for an elevated token instead.
|
|
THROW_HR_IF(WSL_E_ELEVATION_NEEDED_TO_MOUNT_DISK, ((!wsl::windows::common::security::IsTokenElevated(UserToken))));
|
|
}
|
|
|
|
wsl::windows::common::hcs::GrantVmAccess(m_machineId.c_str(), Disk);
|
|
}
|
|
|
|
void WslCoreVm::InitializeGuest()
|
|
{
|
|
// If GUI apps are enabled, mount the shared memory device and write a registry key to suppress mstsc.exe security warnings.
|
|
if (LXSS_ENABLE_GUI_APPS())
|
|
{
|
|
if (m_vmConfig.EnableVirtio)
|
|
{
|
|
try
|
|
{
|
|
// Use the appropriate virtiofs class ID based on m_userToken elevation.
|
|
const bool admin = wsl::windows::common::security::IsTokenElevated(m_userToken.get());
|
|
const GUID classId = admin ? VIRTIO_FS_ADMIN_CLASS_ID : VIRTIO_FS_CLASS_ID;
|
|
m_guestDeviceManager->AddSharedMemoryDevice(classId, L"wslg", L"wslg", WSLG_SHARED_MEMORY_SIZE_MB, m_userToken.get());
|
|
m_sharedMemoryRoot = std::format(L"WSL\\{}\\wslg", m_machineId);
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
try
|
|
{
|
|
auto runAsUser = wil::impersonate_token(m_userToken.get());
|
|
const auto userKey = wsl::windows::common::registry::OpenCurrentUser();
|
|
const auto devicesKey = wsl::windows::common::registry::CreateKey(userKey.get(), c_localDevicesKey, KEY_SET_VALUE);
|
|
constexpr DWORD flags = 0xC4; // Allow clipboard, microphone, and printer access.
|
|
wsl::windows::common::registry::WriteDword(devicesKey.get(), nullptr, m_machineId.c_str(), flags);
|
|
m_localDevicesKeyCreated = true;
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
// Calculate the size of the configuration message.
|
|
wsl::shared::MessageWriter<LX_MINI_INIT_CONFIG_MESSAGE> message(LxMiniInitMessageInitialConfig);
|
|
message->EntropySize = c_bootEntropy;
|
|
message->EnableGuiApps = LXSS_ENABLE_GUI_APPS();
|
|
message->MountGpuShares = m_vmConfig.EnableGpuSupport;
|
|
message->EnableInboxGpuLibs = m_enableInboxGpuLibs;
|
|
if (m_networkingEngine)
|
|
{
|
|
m_networkingEngine->FillInitialConfiguration(message->NetworkingConfiguration);
|
|
}
|
|
|
|
WI_ASSERT(message->NetworkingConfiguration.NetworkingMode == static_cast<LX_MINI_INIT_NETWORKING_MODE>(m_vmConfig.NetworkingMode));
|
|
|
|
// Generate additional entropy to be injected.
|
|
if (message->EntropySize > 0)
|
|
{
|
|
THROW_IF_NTSTATUS_FAILED(BCryptGenRandom(
|
|
nullptr, (PUCHAR)message.InsertBuffer(message->EntropyOffset, message->EntropySize).data(), message->EntropySize, BCRYPT_USE_SYSTEM_PREFERRED_RNG));
|
|
}
|
|
|
|
// Send the message.
|
|
m_miniInitChannel.SendMessage<LX_MINI_INIT_CONFIG_MESSAGE>(message.Span());
|
|
|
|
// If port tracker or localhost relay are enabled, establish a connection with the guest and start processing messages.
|
|
switch (message->NetworkingConfiguration.PortTrackerType)
|
|
{
|
|
case LxMiniInitPortTrackerTypeMirrored:
|
|
{
|
|
auto socket = AcceptConnection(m_vmConfig.KernelBootTimeout);
|
|
m_networkingEngine->StartPortTracker(std::move(socket));
|
|
break;
|
|
}
|
|
case LxMiniInitPortTrackerTypeRelay:
|
|
{
|
|
// If localhost relay is enabled, create a relay process.
|
|
//
|
|
// N.B. The relay process is launched at medium integrity level, and its lifetime is tied to the lifetime of the utility VM.
|
|
const auto result = wil::ResultFromException(WI_DIAGNOSTICS_INFO, [&]() {
|
|
const auto socket = AcceptConnection(m_vmConfig.KernelBootTimeout);
|
|
wsl::windows::common::helpers::LaunchPortRelay(socket.get(), m_runtimeId, m_restrictedToken.get(), !m_vmConfig.EnableTelemetry);
|
|
});
|
|
|
|
if (FAILED(result))
|
|
{
|
|
const auto errorString = wsl::windows::common::wslutil::GetSystemErrorString(result);
|
|
EMIT_USER_WARNING(wsl::shared::Localization::MessageLocalhostRelayFailed(errorString));
|
|
}
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Returns true if the admin drvfs share should be used,
|
|
// false if the non-elevated share should be used
|
|
bool WslCoreVm::InitializeDrvFs(_In_ HANDLE UserToken)
|
|
{
|
|
auto guestDeviceLock = m_guestDeviceLock.lock_exclusive();
|
|
WI_ASSERT(m_vmConfig.EnableHostFileSystemAccess);
|
|
if (m_drvfsInitialResult.valid())
|
|
{
|
|
// The drvfs drives might have been initialized with a different token.
|
|
// Make sure the elevation status matches before returning the cached value.
|
|
const auto elevated = wsl::windows::common::security::IsTokenElevated(UserToken);
|
|
if (m_drvfsInitialResult.get() == elevated)
|
|
{
|
|
return elevated;
|
|
}
|
|
}
|
|
|
|
return InitializeDrvFsLockHeld(UserToken);
|
|
}
|
|
|
|
// Returns true if the admin drvfs share should be used,
|
|
// false if the non-elevated share should be used
|
|
_Requires_lock_held_(m_guestDeviceLock)
|
|
bool WslCoreVm::InitializeDrvFsLockHeld(_In_ HANDLE UserToken)
|
|
{
|
|
// Before checking whether DrvFs is already initialized, make sure any existing Plan 9 servers
|
|
// are usable.
|
|
VerifyPlan9Servers();
|
|
|
|
const auto elevated = wsl::windows::common::security::IsTokenElevated(UserToken);
|
|
if (elevated)
|
|
{
|
|
if (!m_adminDrvfsToken)
|
|
{
|
|
AddDrvFsShare(true, UserToken);
|
|
THROW_IF_WIN32_BOOL_FALSE(
|
|
::DuplicateTokenEx(UserToken, MAXIMUM_ALLOWED, nullptr, SecurityImpersonation, TokenImpersonation, &m_adminDrvfsToken));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!m_drvfsToken)
|
|
{
|
|
AddDrvFsShare(false, UserToken);
|
|
THROW_IF_WIN32_BOOL_FALSE(
|
|
::DuplicateTokenEx(UserToken, MAXIMUM_ALLOWED, nullptr, SecurityImpersonation, TokenImpersonation, &m_drvfsToken));
|
|
}
|
|
}
|
|
|
|
return elevated;
|
|
}
|
|
|
|
bool WslCoreVm::IsDnsTunnelingSupported() const
|
|
{
|
|
WI_ASSERT(m_vmConfig.NetworkingMode == NetworkingMode::Nat || m_vmConfig.NetworkingMode == NetworkingMode::Mirrored);
|
|
|
|
return SUCCEEDED_LOG(wsl::core::networking::DnsResolver::LoadDnsResolverMethods());
|
|
}
|
|
|
|
bool WslCoreVm::IsVhdAttached(_In_ PCWSTR VhdPath)
|
|
{
|
|
auto lock = m_lock.lock_exclusive();
|
|
return m_attachedDisks.contains({DiskType::VHD, VhdPath});
|
|
}
|
|
|
|
WslCoreVm::DiskMountResult WslCoreVm::MountDisk(
|
|
_In_ PCWSTR Disk, _In_ DiskType MountDiskType, _In_ ULONG PartitionIndex, _In_opt_ PCWSTR Name, _In_opt_ PCWSTR Type, _In_opt_ PCWSTR Options)
|
|
{
|
|
auto lock = m_lock.lock_exclusive();
|
|
return MountDiskLockHeld(Disk, MountDiskType, PartitionIndex, Name, Type, Options);
|
|
}
|
|
|
|
WslCoreVm::DiskMountResult WslCoreVm::MountDiskLockHeld(
|
|
_In_ PCWSTR Disk, _In_ DiskType MountDiskType, _In_ ULONG PartitionIndex, _In_opt_ PCWSTR Name, _In_opt_ PCWSTR Type, _In_opt_ PCWSTR Options)
|
|
{
|
|
const auto it = m_attachedDisks.find({MountDiskType, Disk});
|
|
THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND), (it == m_attachedDisks.end()));
|
|
THROW_HR_IF(WSL_E_DISK_ALREADY_MOUNTED, it->second.Mounts.find(PartitionIndex) != it->second.Mounts.end());
|
|
|
|
// Get the name for the mountpoint
|
|
auto targetName = s_GetMountTargetName(Disk, Name, PartitionIndex);
|
|
auto targetNameWide = wsl::shared::string::MultiByteToWide(targetName);
|
|
// For each attachedDisk pair
|
|
const auto nameCollision = std::any_of(m_attachedDisks.begin(), m_attachedDisks.end(), [&](const auto& diskEntry) {
|
|
// Check if the targetName matches the name of any Mount already present
|
|
return (std::any_of(diskEntry.second.Mounts.begin(), diskEntry.second.Mounts.end(), [&](const auto& mountEntry) {
|
|
return wsl::shared::string::IsEqual(mountEntry.second.Name, targetNameWide, false);
|
|
}));
|
|
});
|
|
|
|
// Throw error if the specified name was already used
|
|
THROW_HR_IF(WSL_E_VM_MODE_MOUNT_NAME_ALREADY_EXISTS, nameCollision);
|
|
|
|
wsl::shared::MessageWriter<LX_MINI_INIT_MOUNT_MESSAGE> message(LxMiniInitMessageMount);
|
|
message->PartitionIndex = PartitionIndex;
|
|
message->ScsiLun = it->second.Lun;
|
|
message.WriteString(message->TypeOffset, Type);
|
|
message.WriteString(message->TargetNameOffset, targetName);
|
|
message.WriteString(message->OptionsOffset, Options);
|
|
|
|
// Send the message.
|
|
m_miniInitChannel.SendMessage<LX_MINI_INIT_MOUNT_MESSAGE>(message.Span());
|
|
|
|
// Accept a connection from mini_init
|
|
wsl::shared::SocketChannel channel{AcceptConnection(m_vmConfig.KernelBootTimeout), "MountResult", m_terminatingEvent.get()};
|
|
|
|
// Get the mount result from mini_init
|
|
auto [mountResult, step] = GetMountResult(channel);
|
|
if (mountResult == 0)
|
|
{
|
|
Mount mount;
|
|
|
|
// Always set the Name attribute; use generated one as default
|
|
mount.Name = std::move(targetNameWide);
|
|
|
|
if (Type != nullptr)
|
|
{
|
|
mount.Type = Type;
|
|
}
|
|
|
|
if (Options != nullptr)
|
|
{
|
|
mount.Options = Options;
|
|
}
|
|
|
|
it->second.Mounts.emplace(PartitionIndex, std::move(mount));
|
|
}
|
|
|
|
return {std::move(targetName), mountResult, step};
|
|
}
|
|
|
|
wil::unique_socket WslCoreVm::CreateRootNamespaceProcess(_In_ LPCSTR Path, _In_ LPCSTR* Arguments)
|
|
{
|
|
auto lock = m_lock.lock_exclusive();
|
|
|
|
return LxssCreateProcess::CreateLinuxProcess(
|
|
Path, Arguments, m_runtimeId, m_miniInitChannel, m_terminatingEvent.get(), m_vmConfig.DistributionStartTimeout);
|
|
}
|
|
|
|
void WslCoreVm::MountRootNamespaceFolder(_In_ LPCWSTR HostPath, _In_ LPCWSTR GuestPath, _In_ bool ReadOnly, _In_ LPCWSTR Name)
|
|
{
|
|
auto lock = m_lock.lock_exclusive();
|
|
|
|
const auto flags = (ReadOnly ? hcs::Plan9ShareFlags::ReadOnly : hcs::Plan9ShareFlags::None) | hcs::Plan9ShareFlags::AllowOptions;
|
|
wsl::windows::common::hcs::AddPlan9Share(m_system.get(), Name, Name, HostPath, LX_INIT_UTILITY_VM_PLAN9_PORT, flags);
|
|
|
|
wsl::shared::MessageWriter<LX_MINI_INIT_MOUNT_FOLDER_MESSAGE> message(LxMiniInitMountFolder);
|
|
message.WriteString(message->PathIndex, GuestPath);
|
|
message.WriteString(message->NameIndex, Name);
|
|
message->ReadOnly = ReadOnly;
|
|
|
|
const auto& ResultMessage = m_miniInitChannel.Transaction<LX_MINI_INIT_MOUNT_FOLDER_MESSAGE>(message.Span());
|
|
|
|
THROW_HR_IF_MSG(
|
|
E_FAIL,
|
|
ResultMessage.Result != 0,
|
|
"Failed to mount folder. HostPath=%ls, GuestPath=%ls, Name=%ls, ReadOnly=%d, Result=%d",
|
|
HostPath,
|
|
GuestPath,
|
|
Name,
|
|
ReadOnly,
|
|
ResultMessage.Result);
|
|
}
|
|
|
|
ULONG
|
|
WslCoreVm::MountFileAsPersistentMemory(_In_ PCWSTR FilePath, _In_ bool ReadOnly)
|
|
{
|
|
hcs::Plan9ShareFlags flags{};
|
|
|
|
WI_SetFlagIf(flags, hcs::Plan9ShareFlags::ReadOnly, ReadOnly);
|
|
|
|
// Serialize calls to mount pmem devices to the VM. Some quick background on why we do this.
|
|
// The problem stems from the fact that our caller needs to know the dev path where the pmem
|
|
// device will be mounted (i.e. /dev/pmem0). We could dynamically discover the device path and
|
|
// return that to our caller. However, some callers statically declare the dev paths in their
|
|
// fstabs. Therefore, we must wait for each device to finish initializing before allowing the
|
|
// next to proceed, so that they appear in the expected predefined order.
|
|
//
|
|
// Ideally callers wouldn't rely on the dev path, and would setup their fstabs using names. If
|
|
// callers are ever updated, we could update this code to allow pmem devices to be added in
|
|
// parallel and dynamically discover their dev path (which we would then use if the caller
|
|
// asked us to mount the pmem device, instead of them doing it in their fstabs). To dynamically
|
|
// discover the dev path, we'd have to poll /sys/class/block. Eventually a path such as
|
|
// /sys/class/block/pmemX will appear. Once it appears, /sys/class/block/pmemX/device will be
|
|
// a symlink that points to a path like:
|
|
// /sys/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0004:00/VMBUS:00/<GUID>/pcicceb:00//cceb:00:00.0/virtio1/ndbus0/region0/namespace0.0/block/pmem0
|
|
// Notice the GUID in the middle of that path. That GUID is the instance ID, which is randomly
|
|
// generated by AddGuestDevice. So once we find a path with the instance ID, we know that
|
|
// eventually /dev/pmemX will appear in the guest.
|
|
auto persistentMemoryLock = m_persistentMemoryLock.lock_exclusive();
|
|
|
|
// Add the pmem device to the VM.
|
|
// N.B. If this succeeds, technically we'd need to remove the device if we later encounter any
|
|
// failures. Otherwise, we'd potentially leave the VM in a torn state. However, HCS
|
|
// doesn't currently support this. For now, we rely on the fact that all pmem devices are
|
|
// added as part of VM creation and therefore any failure will result in VM termination
|
|
// (in which case there's no need to remove the device).
|
|
{
|
|
(void)m_guestDeviceManager->AddGuestDevice(
|
|
VIRTIO_PMEM_DEVICE_ID, VIRTIO_PMEM_CLASS_ID, L"", nullptr, FilePath, static_cast<UINT32>(flags), m_userToken.get());
|
|
}
|
|
|
|
// Wait for the pmem device to appear in the VM at /dev/pmemX. Guess the value of X given the
|
|
// number of pmem devices that have been exposed to the VM. See above for more details why.
|
|
// N.B. If hot remove of pmem devices is ever added, this logic will need to be updated.
|
|
// Similarly, if nvdimm devices are ever passed through to the VM, this logic will need
|
|
// to be updated.
|
|
const ULONG persistentMemoryId = m_nextPersistentMemoryId;
|
|
WaitForPmemDeviceInVm(persistentMemoryId);
|
|
|
|
// The pmem device was successfully found in the VM. Increment the next expected pmem device ID.
|
|
m_nextPersistentMemoryId += 1;
|
|
|
|
return persistentMemoryId;
|
|
}
|
|
|
|
void WslCoreVm::WaitForPmemDeviceInVm(_In_ ULONG PmemId)
|
|
{
|
|
// Construct the mini_init message.
|
|
LX_MINI_INIT_WAIT_FOR_PMEM_DEVICE_MESSAGE message;
|
|
message.Header.MessageType = LxMiniInitMessageWaitForPmemDevice;
|
|
message.Header.MessageSize = sizeof(message);
|
|
message.PmemId = PmemId;
|
|
|
|
// Send the message to mini_init.
|
|
wsl::shared::SocketChannel channel;
|
|
{
|
|
auto lock = m_lock.lock_exclusive();
|
|
|
|
m_miniInitChannel.SendMessage(message);
|
|
channel = {
|
|
AcceptConnection(m_vmConfig.KernelBootTimeout),
|
|
"WaitForPmem",
|
|
m_terminatingEvent.get(),
|
|
};
|
|
}
|
|
|
|
// Wait for mini_init to respond.
|
|
|
|
const auto& resultMessage = channel.ReceiveMessage<LX_MINI_INIT_WAIT_FOR_PMEM_DEVICE_MESSAGE::TResponse>();
|
|
|
|
// Check if the device was found in the VM.
|
|
if (resultMessage.Result != 0)
|
|
{
|
|
THROW_WIN32_MSG(ERROR_NOT_FOUND, "Failed to find /dev/pmem%u with result %d", PmemId, resultMessage.Result);
|
|
}
|
|
}
|
|
|
|
_Requires_lock_held_(m_guestDeviceLock)
|
|
std::wstring WslCoreVm::AddVirtioFsShare(_In_ bool Admin, _In_ PCWSTR Path, _In_ PCWSTR Options, _In_opt_ HANDLE UserToken)
|
|
{
|
|
WI_ASSERT(m_vmConfig.EnableVirtioFs);
|
|
|
|
if (!ARGUMENT_PRESENT(UserToken))
|
|
{
|
|
UserToken = Admin ? m_adminDrvfsToken.get() : m_drvfsToken.get();
|
|
THROW_HR_IF_MSG(E_UNEXPECTED, !UserToken, "UserToken not set for supplied context (Admin = %d)", Admin);
|
|
}
|
|
|
|
WI_ASSERT(Admin == wsl::windows::common::security::IsTokenElevated(UserToken));
|
|
|
|
// Ensure that the path has a trailing path separator.
|
|
std::wstring sharePath(Path);
|
|
if (!sharePath.ends_with(L'\\') && !sharePath.ends_with(L'/'))
|
|
{
|
|
sharePath.push_back(L'\\');
|
|
}
|
|
|
|
sharePath = std::filesystem::weakly_canonical(sharePath).wstring();
|
|
|
|
// Check if a matching share already exists.
|
|
bool created = false;
|
|
std::wstring tag;
|
|
VirtioFsShare key(sharePath.c_str(), Options, Admin);
|
|
if (!m_virtioFsShares.contains(key))
|
|
{
|
|
// Generate a new unique tag for the share.
|
|
//
|
|
// N.B. The tag can be maximum 36 characters long so a GUID without braces fits perfectly.
|
|
GUID tagGuid{};
|
|
THROW_IF_FAILED(CoCreateGuid(&tagGuid));
|
|
|
|
tag = wsl::shared::string::GuidToString<wchar_t>(tagGuid, wsl::shared::string::None);
|
|
WI_ASSERT(!FindVirtioFsShare(tag.c_str(), Admin));
|
|
|
|
(void)m_guestDeviceManager->AddGuestDevice(
|
|
VIRTIO_FS_DEVICE_ID,
|
|
Admin ? VIRTIO_FS_ADMIN_CLASS_ID : VIRTIO_FS_CLASS_ID,
|
|
tag.c_str(),
|
|
key.OptionsString().c_str(),
|
|
sharePath.c_str(),
|
|
VIRTIO_FS_FLAGS_TYPE_FILES,
|
|
UserToken);
|
|
|
|
m_virtioFsShares.emplace(std::move(key), tag);
|
|
created = true;
|
|
}
|
|
else
|
|
{
|
|
tag = m_virtioFsShares[key];
|
|
}
|
|
|
|
WSL_LOG(
|
|
"WslCoreVmAddVirtioFsShare",
|
|
TraceLoggingValue(Admin, "admin"),
|
|
TraceLoggingValue(sharePath.c_str(), "path"),
|
|
TraceLoggingValue(Options, "options"),
|
|
TraceLoggingValue(tag.c_str(), "tag"),
|
|
TraceLoggingValue(created, "created"),
|
|
TraceLoggingValue(m_virtioFsShares.size(), "shareCount"));
|
|
|
|
return tag;
|
|
}
|
|
|
|
void WslCoreVm::OnCrash(_In_ LPCWSTR Details)
|
|
{
|
|
if (m_vmCrashEvent.is_signaled())
|
|
{
|
|
return; // Crash information has already been collected
|
|
}
|
|
|
|
WSL_LOG("GuestCrash", TraceLoggingValue(Details, "Data"));
|
|
const auto crashInformation = wsl::shared::FromJson<wsl::windows::common::hcs::CrashReport>(Details);
|
|
|
|
if (m_vmConfig.MaxCrashDumpCount >= 0)
|
|
{
|
|
constexpr auto c_extension = L".txt";
|
|
constexpr auto c_prefix = L"kernel-panic-";
|
|
const auto filename = std::format(L"{}{}-{}{}", c_prefix, std::time(nullptr), m_runtimeId, c_extension);
|
|
auto tracePath = m_vmConfig.CrashDumpFolder / filename;
|
|
|
|
auto runAsUser = wil::impersonate_token(m_userToken.get());
|
|
|
|
std::error_code error;
|
|
std::filesystem::create_directories(m_vmConfig.CrashDumpFolder, error);
|
|
if (error.value())
|
|
{
|
|
THROW_WIN32_MSG(error.value(), "Failed to create folder: %ls", m_vmConfig.CrashDumpFolder.c_str());
|
|
}
|
|
|
|
auto pred = [&c_extension, &c_prefix](const auto& e) {
|
|
return WI_IsFlagSet(GetFileAttributes(e.path().c_str()), FILE_ATTRIBUTE_TEMPORARY) && e.path().has_extension() &&
|
|
e.path().extension() == c_extension && e.path().has_filename() && e.path().filename().wstring().find(c_prefix) == 0;
|
|
};
|
|
|
|
wsl::windows::common::wslutil::EnforceFileLimit(m_vmConfig.CrashDumpFolder.c_str(), m_vmConfig.MaxCrashDumpCount, pred);
|
|
|
|
{
|
|
std::wofstream outputFile(tracePath.wstring());
|
|
THROW_HR_IF(E_UNEXPECTED, !outputFile || !(outputFile << crashInformation.CrashLog));
|
|
}
|
|
|
|
m_vmCrashLogFile = std::move(tracePath);
|
|
}
|
|
|
|
m_vmCrashEvent.SetEvent();
|
|
}
|
|
|
|
void WslCoreVm::OnExit(_In_opt_ PCWSTR ExitDetails)
|
|
{
|
|
// Indicate that the VM has exited, and wake any waiting threads. The instance may be in its destructor at this
|
|
// point but closing m_system will wait for any outstanding callbacks, so this function will complete before the
|
|
// destructor continues.
|
|
std::function<void(GUID)> terminationCallback{};
|
|
{
|
|
auto exitLock = m_exitCallbackLock.lock_exclusive();
|
|
if (ARGUMENT_PRESENT(ExitDetails))
|
|
{
|
|
m_exitDetails = ExitDetails;
|
|
}
|
|
|
|
m_vmExitEvent.SetEvent();
|
|
|
|
// If we reach this block and 'm_terminatingEvent' is not signaled, then this is abnormal shutdown.
|
|
// If that happens, set m_terminatingEvent so all pending socket operations can be properly cancelled.
|
|
if (!m_terminatingEvent.is_signaled())
|
|
{
|
|
WSL_LOG("AbnormalVmExit", TraceLoggingValue(ExitDetails, "Details"));
|
|
m_terminatingEvent.SetEvent();
|
|
}
|
|
|
|
terminationCallback = std::move(m_onExit);
|
|
}
|
|
|
|
if (terminationCallback)
|
|
{
|
|
terminationCallback(m_runtimeId);
|
|
}
|
|
}
|
|
|
|
void WslCoreVm::ReadGuestCapabilities()
|
|
{
|
|
const auto& info = m_miniInitChannel.ReceiveMessage<LX_INIT_GUEST_CAPABILITIES>();
|
|
|
|
m_kernelVersionString = wsl::shared::string::MultiByteToWide(info.Buffer);
|
|
|
|
// Parse the version string.
|
|
const std::regex pattern("(\\d+)\\.(\\d+)\\.(\\d+).*");
|
|
std::smatch match;
|
|
const std::string input = info.Buffer;
|
|
if (!std::regex_match(input, match, pattern) || match.size() != 4)
|
|
{
|
|
THROW_HR_MSG(E_UNEXPECTED, "Failed to parse kernel version: '%hs'", input.c_str());
|
|
}
|
|
|
|
auto get = [&](int position) { return std::stoul(match.str(position)); };
|
|
|
|
try
|
|
{
|
|
m_kernelVersion = std::make_tuple(get(1), get(2), get(3));
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
THROW_HR_MSG(E_UNEXPECTED, "Failed to parse kernel version: '%hs', %hs", info.Buffer, e.what());
|
|
}
|
|
|
|
m_seccompAvailable = info.SeccompAvailable;
|
|
WSL_LOG(
|
|
"GuestKernelInfo",
|
|
TraceLoggingValue(m_seccompAvailable, "SeccompAvailable"),
|
|
TraceLoggingValue(std::get<0>(m_kernelVersion), "Version"),
|
|
TraceLoggingValue(std::get<1>(m_kernelVersion), "Revision"),
|
|
TraceLoggingValue(std::get<2>(m_kernelVersion), "Minor"));
|
|
}
|
|
|
|
ULONG WslCoreVm::ReserveLun(_In_ std::optional<ULONG> Lun)
|
|
{
|
|
if (Lun.has_value() && !m_lunBitmap[Lun.value()])
|
|
{
|
|
m_lunBitmap[Lun.value()] = true;
|
|
return Lun.value();
|
|
}
|
|
|
|
for (ULONG index = 0; index < gsl::narrow_cast<ULONG>(m_lunBitmap.size()); index += 1)
|
|
{
|
|
if (!m_lunBitmap[index])
|
|
{
|
|
m_lunBitmap[index] = true;
|
|
return index;
|
|
}
|
|
}
|
|
|
|
THROW_HR(WSL_E_TOO_MANY_DISKS_ATTACHED);
|
|
}
|
|
|
|
void WslCoreVm::RestorePassthroughDiskState(_In_ LPCWSTR Disk) const
|
|
try
|
|
{
|
|
const auto diskHandle = wsl::windows::common::disk::OpenDevice(Disk, GENERIC_READ | GENERIC_WRITE, m_vmConfig.MountDeviceTimeout);
|
|
wsl::windows::common::disk::SetOnline(diskHandle.get(), true, m_vmConfig.MountDeviceTimeout);
|
|
return;
|
|
}
|
|
CATCH_LOG()
|
|
|
|
void WslCoreVm::RegisterCallbacks(_In_ const std::function<void(ULONG)>& DistroExitCallback, _In_ const std::function<void(GUID)>& TerminationCallback)
|
|
{
|
|
WSL_LOG(
|
|
"WslCoreVm::RegisterCallbacks",
|
|
TraceLoggingValue(static_cast<bool>(DistroExitCallback), "DistroExitCallback"),
|
|
TraceLoggingValue(static_cast<bool>(TerminationCallback), "TerminationCallback"));
|
|
|
|
if (DistroExitCallback)
|
|
{
|
|
auto lock = m_lock.lock_exclusive();
|
|
THROW_HR_IF(E_INVALIDARG, !m_notifyChannel);
|
|
m_distroExitThread = std::thread([exitCallback = std::move(DistroExitCallback),
|
|
notifyChannel = std::move(m_notifyChannel),
|
|
terminationEvent = m_terminatingEvent.get()]() {
|
|
try
|
|
{
|
|
wsl::windows::common::wslutil::SetThreadDescription(L"DistroExitCallback");
|
|
|
|
std::vector<gsl::byte> buffer;
|
|
for (;;)
|
|
{
|
|
// Read the message.
|
|
auto message = wsl::shared::socket::RecvMessage(notifyChannel.get(), buffer, terminationEvent);
|
|
if (message.empty())
|
|
{
|
|
break;
|
|
}
|
|
|
|
const auto* header = gslhelpers::get_struct<MESSAGE_HEADER>(message);
|
|
if (header->MessageType == LxMiniInitMessageChildExit)
|
|
{
|
|
const auto* exitMessage = gslhelpers::try_get_struct<LX_MINI_INIT_CHILD_EXIT_MESSAGE>(message);
|
|
WSL_LOG("ProcessExited", TraceLoggingValue(exitMessage->ChildPid, "pid"));
|
|
|
|
if (exitMessage)
|
|
{
|
|
exitCallback(exitMessage->ChildPid);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LOG_HR_MSG(E_UNEXPECTED, "Unexpected MessageType %d", header->MessageType);
|
|
}
|
|
}
|
|
}
|
|
CATCH_LOG()
|
|
});
|
|
}
|
|
|
|
if (TerminationCallback)
|
|
{
|
|
// Register the callback if the VM has not been terminated.
|
|
auto exitLock = m_exitCallbackLock.lock_exclusive();
|
|
THROW_HR_IF(E_INVALIDARG, m_onExit);
|
|
if (!m_terminatingEvent.is_signaled())
|
|
{
|
|
m_onExit = std::move(TerminationCallback);
|
|
}
|
|
else
|
|
{
|
|
// The VM has already been terminated, invoke the callback on a separate thread.
|
|
std::thread([terminationCallback = std::move(TerminationCallback), runtimeId = m_runtimeId]() {
|
|
wsl::windows::common::wslutil::SetThreadDescription(L"TerminationCallback");
|
|
terminationCallback(runtimeId);
|
|
}).detach();
|
|
}
|
|
}
|
|
|
|
if (m_vmConfig.EnableHostFileSystemAccess && m_vmConfig.EnableVirtioFs)
|
|
{
|
|
// Create a thread listening for handling virtiofs requests.
|
|
auto listenSocket = wsl::windows::common::hvsocket::Listen(m_runtimeId, LX_INIT_UTILITY_VM_VIRTIOFS_PORT);
|
|
m_virtioFsThread = std::thread(&WslCoreVm::VirtioFsWorker, this, std::move(listenSocket));
|
|
}
|
|
}
|
|
|
|
void WslCoreVm::ResizeDistribution(_In_ ULONG Lun, _In_ HANDLE OutputHandle, _In_ ULONG64 NewSize)
|
|
{
|
|
auto lock = m_lock.lock_exclusive();
|
|
|
|
LX_MINI_INIT_RESIZE_DISTRIBUTION_MESSAGE message;
|
|
message.Header.MessageSize = sizeof(message);
|
|
message.Header.MessageType = LxMiniInitMessageResizeDistribution;
|
|
message.ScsiLun = Lun;
|
|
message.NewSize = NewSize;
|
|
|
|
m_miniInitChannel.SendMessage(message);
|
|
|
|
wsl::shared::SocketChannel channel{AcceptConnection(m_vmConfig.KernelBootTimeout), "ResizeDistribution", m_terminatingEvent.get()};
|
|
auto outputChannel = AcceptConnection(m_vmConfig.KernelBootTimeout);
|
|
|
|
wsl::windows::common::relay::ScopedRelay outputRelay(std::move(outputChannel), OutputHandle);
|
|
|
|
const auto& resultMessage = channel.ReceiveMessage<LX_MINI_INIT_RESIZE_DISTRIBUTION_RESPONSE>();
|
|
if (resultMessage.ResponseCode != 0)
|
|
{
|
|
THROW_HR_WITH_USER_ERROR(E_FAIL, wsl::shared::Localization::MessageFailedToResizeDisk());
|
|
}
|
|
}
|
|
|
|
void WslCoreVm::SaveAttachedDisksState()
|
|
try
|
|
{
|
|
auto lock = m_lock.lock_exclusive();
|
|
const auto key = wsl::windows::common::registry::OpenOrCreateLxssDiskMountsKey(&m_userSid.Sid);
|
|
for (const auto& e : m_attachedDisks)
|
|
{
|
|
if (e.first.User)
|
|
{
|
|
SaveDiskState(key.get(), e.first, e.second, e.first.Type);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
CATCH_LOG()
|
|
|
|
void WslCoreVm::SaveDiskState(_In_ HKEY Key, _In_ const AttachedDisk& Disk, _In_ const DiskState& State, _In_ const DiskType& SaveDiskType)
|
|
{
|
|
const auto keyPath = std::to_wstring(State.Lun);
|
|
const auto diskKey = wsl::windows::common::registry::CreateKey(Key, keyPath.c_str(), KEY_ALL_ACCESS, nullptr, REG_OPTION_VOLATILE);
|
|
|
|
wsl::windows::common::registry::WriteString(diskKey.get(), nullptr, c_diskValueName, Disk.Path.c_str());
|
|
|
|
wsl::windows::common::registry::WriteDword(diskKey.get(), nullptr, c_disktypeValueName, static_cast<DWORD>(SaveDiskType));
|
|
|
|
for (const auto& e : State.Mounts)
|
|
{
|
|
auto partition = std::to_wstring(e.first);
|
|
auto mountKey = wsl::windows::common::registry::CreateKey(diskKey.get(), partition.c_str(), KEY_ALL_ACCESS, nullptr, REG_OPTION_VOLATILE);
|
|
|
|
wsl::windows::common::registry::WriteString(mountKey.get(), nullptr, c_mountNameValueName, e.second.Name.c_str());
|
|
|
|
if (e.second.Options.has_value())
|
|
{
|
|
wsl::windows::common::registry::WriteString(mountKey.get(), nullptr, c_optionsValueName, e.second.Options.value().c_str());
|
|
}
|
|
|
|
if (e.second.Type.has_value())
|
|
{
|
|
wsl::windows::common::registry::WriteString(mountKey.get(), nullptr, c_typeValueName, e.second.Type.value().c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
std::pair<int, LX_MINI_MOUNT_STEP> WslCoreVm::UnmountDisk(_In_ const AttachedDisk& Disk, _Inout_ DiskState& State)
|
|
{
|
|
// Iterate through the mountpoints to unmount and delete them
|
|
for (auto it = State.Mounts.begin(); it != State.Mounts.end(); it = State.Mounts.erase(it))
|
|
{
|
|
const auto result = UnmountVolume(Disk, it->first, it->second.Name.c_str());
|
|
if (result.first != 0)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Tell the guest to flush its IO caches and stop using the disk.
|
|
LX_MINI_INIT_DETACH_MESSAGE message;
|
|
message.Header.MessageType = LxMiniInitMessageDetach;
|
|
message.Header.MessageSize = sizeof(message);
|
|
message.ScsiLun = State.Lun;
|
|
|
|
m_miniInitChannel.SendMessage(message);
|
|
|
|
// Accept a connection from mini_init.
|
|
wsl::shared::SocketChannel channel{AcceptConnection(m_vmConfig.KernelBootTimeout), "MountResult", m_terminatingEvent.get()};
|
|
|
|
// Get the unmount result from mini_init
|
|
return GetMountResult(channel);
|
|
}
|
|
|
|
std::pair<int, LX_MINI_MOUNT_STEP> WslCoreVm::UnmountVolume(_In_ const AttachedDisk& Disk, _In_ ULONG PartitionIndex, _In_ PCWSTR Name)
|
|
{
|
|
wsl::shared::MessageWriter<LX_MINI_INIT_UNMOUNT_MESSAGE> message(LxMiniInitMessageUnmount);
|
|
message.WriteString(Name);
|
|
|
|
// Send the message.
|
|
m_miniInitChannel.SendMessage<LX_MINI_INIT_UNMOUNT_MESSAGE>(message.Span());
|
|
|
|
// Accept a connection from mini_init.
|
|
wsl::shared::SocketChannel channel{AcceptConnection(m_vmConfig.KernelBootTimeout), "MountResult", m_terminatingEvent.get()};
|
|
|
|
// Get the unmount result from mini_init.
|
|
return GetMountResult(channel);
|
|
}
|
|
|
|
_Requires_lock_held_(m_guestDeviceLock)
|
|
void WslCoreVm::VerifyPlan9Servers()
|
|
{
|
|
for (auto it = m_plan9Servers.begin(); it != m_plan9Servers.end();)
|
|
{
|
|
const HRESULT result = it->second->IsRunning();
|
|
|
|
// If the server process was terminated (which can happen e.g. if the user logged out and
|
|
// back in), attempting to make a COM call will return
|
|
// HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE). For this and other errors, remove the
|
|
// server from the list and mark DrvFs for that port uninitialized.
|
|
// N.B. The call will return S_FALSE if the server is not running. That should never
|
|
// happen since this service never calls Pause(), but in case it does that is also
|
|
// treated as an error.
|
|
if (result != S_OK)
|
|
{
|
|
if (it->first == LX_INIT_UTILITY_VM_PLAN9_DRVFS_ADMIN_PORT)
|
|
{
|
|
m_adminDrvfsToken.reset();
|
|
}
|
|
else
|
|
{
|
|
WI_ASSERT(it->first == LX_INIT_UTILITY_VM_PLAN9_DRVFS_PORT);
|
|
|
|
m_drvfsToken.reset();
|
|
}
|
|
|
|
it = m_plan9Servers.erase(it);
|
|
}
|
|
else
|
|
{
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WslCoreVm::VirtioFsWorker(_In_ const wil::unique_socket& listenSocket)
|
|
try
|
|
{
|
|
wsl::windows::common::wslutil::SetThreadDescription(L"VirtioFs - Worker");
|
|
|
|
for (;;)
|
|
{
|
|
// Create a worker thread to handle each request.
|
|
wsl::shared::SocketChannel channel{
|
|
wsl::windows::common::hvsocket::Accept(listenSocket.get(), INFINITE, m_terminatingEvent.get()),
|
|
"VirtioFs",
|
|
m_terminatingEvent.get()};
|
|
std::thread([this, channel = std::move(channel)]() mutable {
|
|
try
|
|
{
|
|
wsl::windows::common::wslutil::SetThreadDescription(L"VirtioFs - Request");
|
|
|
|
auto [message, span] = channel.ReceiveMessageOrClosed<MESSAGE_HEADER>();
|
|
if (message == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto respondWithTag = [&](const std::wstring& tag, HRESULT result) {
|
|
// Respond to the guest with the tag that should be used to mount the device.
|
|
|
|
wsl::shared::MessageWriter<LX_INIT_ADD_VIRTIOFS_SHARE_RESPONSE_MESSAGE> response(LxInitMessageAddVirtioFsDeviceResponse);
|
|
response->Result = SUCCEEDED(result) ? 0 : EINVAL; // TODO: Improved HRESULT -> errno mapping.
|
|
response.WriteString(response->TagOffset, tag);
|
|
|
|
channel.SendMessage<LX_INIT_ADD_VIRTIOFS_SHARE_RESPONSE_MESSAGE>(response.Span());
|
|
};
|
|
|
|
if (message->MessageType == LxInitMessageAddVirtioFsDevice)
|
|
{
|
|
std::wstring tag;
|
|
const auto result = wil::ResultFromException([this, span, &tag]() {
|
|
const auto* addShare = gslhelpers::try_get_struct<LX_INIT_ADD_VIRTIOFS_SHARE_MESSAGE>(span);
|
|
THROW_HR_IF(E_UNEXPECTED, !addShare);
|
|
|
|
const auto path = wsl::shared::string::FromSpan(span, addShare->PathOffset);
|
|
const auto pathWide = wsl::shared::string::MultiByteToWide(path);
|
|
const auto options = wsl::shared::string::FromSpan(span, addShare->OptionsOffset);
|
|
const auto optionsWide = wsl::shared::string::MultiByteToWide(options);
|
|
|
|
// Acquire the lock and attempt to add the device.
|
|
auto guestDeviceLock = m_guestDeviceLock.lock_exclusive();
|
|
tag = AddVirtioFsShare(addShare->Admin, pathWide.c_str(), optionsWide.c_str());
|
|
});
|
|
|
|
respondWithTag(tag, result);
|
|
}
|
|
else if (message->MessageType == LxInitMessageRemountVirtioFsDevice)
|
|
{
|
|
std::wstring newTag;
|
|
const auto result = wil::ResultFromException([this, span, &newTag]() {
|
|
const auto* remountShare = gslhelpers::try_get_struct<LX_INIT_REMOUNT_VIRTIOFS_SHARE_MESSAGE>(span);
|
|
THROW_HR_IF(E_UNEXPECTED, !remountShare);
|
|
|
|
const std::string tag = wsl::shared::string::FromSpan(span, remountShare->TagOffset);
|
|
const auto tagWide = wsl::shared::string::MultiByteToWide(tag);
|
|
auto guestDeviceLock = m_guestDeviceLock.lock_exclusive();
|
|
const auto foundShare = FindVirtioFsShare(tagWide.c_str(), !remountShare->Admin);
|
|
THROW_HR_IF_MSG(E_UNEXPECTED, !foundShare.has_value(), "Unknown tag %ls", tagWide.c_str());
|
|
|
|
newTag = AddVirtioFsShare(remountShare->Admin, foundShare->Path.c_str(), foundShare->OptionsString().c_str());
|
|
});
|
|
|
|
respondWithTag(newTag, result);
|
|
}
|
|
else if (message->MessageType == LxInitMessageQueryVirtioFsDevice)
|
|
{
|
|
std::wstring newTag;
|
|
const auto result = wil::ResultFromException([this, span, &newTag]() {
|
|
const auto* query = gslhelpers::try_get_struct<LX_INIT_QUERY_VIRTIOFS_SHARE_MESSAGE>(span);
|
|
THROW_HR_IF(E_UNEXPECTED, !query);
|
|
|
|
const std::string tag = wsl::shared::string::FromSpan(span, query->TagOffset);
|
|
const auto tagWide = wsl::shared::string::MultiByteToWide(tag);
|
|
auto guestDeviceLock = m_guestDeviceLock.lock_exclusive();
|
|
const auto foundShare = FindVirtioFsShare(tagWide.c_str());
|
|
THROW_HR_IF_MSG(E_UNEXPECTED, !foundShare.has_value(), "Unknown tag %ls", tagWide.c_str());
|
|
|
|
newTag = foundShare->Path;
|
|
});
|
|
|
|
respondWithTag(newTag, result);
|
|
}
|
|
else
|
|
{
|
|
THROW_HR_MSG(E_UNEXPECTED, "Unexpected MessageType %d", message->MessageType);
|
|
}
|
|
}
|
|
CATCH_LOG()
|
|
}).detach();
|
|
}
|
|
}
|
|
CATCH_LOG()
|
|
|
|
std::string WslCoreVm::s_GetMountTargetName(_In_ PCWSTR Disk, _In_opt_ PCWSTR Name, _In_ int PartitionIndex)
|
|
{
|
|
// Derive the mount target from the disk and partition names.
|
|
// The format is <Disk>p[partition]
|
|
// For Example: PhysicalDisk1p2
|
|
// If user has specified the name, ensure proper formatting and use it instead
|
|
if (ARGUMENT_PRESENT(Name))
|
|
{
|
|
auto mountName = wsl::shared::string::WideToMultiByte(Name);
|
|
// Throw if the name contains '/' since it is a linux path separator
|
|
THROW_HR_IF(WSL_E_VM_MODE_INVALID_MOUNT_NAME, mountName.find('/') != std::string::npos);
|
|
return mountName;
|
|
}
|
|
|
|
std::string target{};
|
|
auto mountName = wsl::shared::string::WideToMultiByte(Disk);
|
|
std::copy_if(mountName.begin(), mountName.end(), std::back_inserter(target), &isalnum);
|
|
if (PartitionIndex != 0)
|
|
{
|
|
target += std::format("p{}", PartitionIndex);
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
LX_INIT_DRVFS_MOUNT WslCoreVm::s_InitializeDrvFs(_Inout_ WslCoreVm* VmContext, _In_ HANDLE UserToken)
|
|
{
|
|
try
|
|
{
|
|
return VmContext->InitializeDrvFs(UserToken) ? LxInitDrvfsMountElevated : LxInitDrvfsMountNonElevated;
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_CAUGHT_EXCEPTION();
|
|
|
|
return LxInitDrvfsMountNone;
|
|
}
|
|
}
|
|
|
|
void CALLBACK WslCoreVm::s_OnExit(_In_ HCS_EVENT* Event, _In_opt_ void* Context)
|
|
try
|
|
{
|
|
const auto utilityVm = static_cast<WslCoreVm*>(Context);
|
|
if (Event->Type == HcsEventSystemCrashInitiated || Event->Type == HcsEventSystemCrashReport)
|
|
{
|
|
utilityVm->OnCrash(Event->EventData);
|
|
}
|
|
else if ((Event->Type == HcsEventSystemExited) || (Event->Type == HcsEventServiceDisconnect))
|
|
{
|
|
utilityVm->OnExit(Event->EventData);
|
|
}
|
|
}
|
|
CATCH_LOG();
|
|
|
|
bool WslCoreVm::AttachedDisk::operator<(const AttachedDisk& other) const
|
|
{
|
|
if (Type < other.Type)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (Type == other.Type)
|
|
{
|
|
return _wcsicmp(Path.c_str(), other.Path.c_str()) < 0;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool WslCoreVm::AttachedDisk::operator==(const AttachedDisk& other) const
|
|
{
|
|
return Type == other.Type && wsl::windows::common::string::IsPathComponentEqual(Path, other.Path);
|
|
}
|
|
|
|
WslCoreVm::VirtioFsShare::VirtioFsShare(PCWSTR Path, PCWSTR Options, bool Admin) : Path(Path), Admin(Admin)
|
|
{
|
|
// Parse the options string into a map representing mount options to ensure that shares with functionally
|
|
// identical options can share a single device.
|
|
// For example: "uid=1000;gid=1000" and "gid=1000;uid=1000"
|
|
auto optionsVector = wsl::shared::string::Split(std::wstring{Options}, L';');
|
|
for (const auto& option : optionsVector)
|
|
{
|
|
std::wstring key;
|
|
std::wstring value;
|
|
const auto pos = option.find_first_of(L'=');
|
|
if (pos == option.npos)
|
|
{
|
|
key = option;
|
|
}
|
|
else
|
|
{
|
|
key = option.substr(0, pos);
|
|
value = option.substr(pos + 1);
|
|
}
|
|
|
|
if (!key.empty())
|
|
{
|
|
this->Options.insert({std::move(key), std::move(value)});
|
|
}
|
|
}
|
|
|
|
if constexpr (wsl::shared::Debug)
|
|
{
|
|
const auto originalSet = std::set<std::wstring>(optionsVector.begin(), optionsVector.end());
|
|
auto newVector = wsl::shared::string::Split(OptionsString(), L';');
|
|
const auto newSet = std::set<std::wstring>(newVector.begin(), newVector.end());
|
|
WI_ASSERT_MSG(originalSet == newSet, "mount options do not match");
|
|
}
|
|
}
|
|
|
|
std::wstring WslCoreVm::VirtioFsShare::OptionsString() const
|
|
{
|
|
std::wstring optionsString;
|
|
for (const auto& option : Options)
|
|
{
|
|
if (!optionsString.empty())
|
|
{
|
|
optionsString += L';';
|
|
}
|
|
|
|
optionsString += option.first;
|
|
if (!option.second.empty())
|
|
{
|
|
optionsString += L'=';
|
|
optionsString += option.second;
|
|
}
|
|
}
|
|
|
|
return optionsString;
|
|
}
|
|
|
|
bool WslCoreVm::VirtioFsShare::operator<(const VirtioFsShare& other) const
|
|
{
|
|
return std::tie(Path, Options, Admin) < std::tie(other.Path, other.Options, other.Admin);
|
|
}
|
|
|
|
bool WslCoreVm::VirtioFsShare::operator==(const VirtioFsShare& other) const
|
|
{
|
|
return Path == other.Path && Options == other.Options && Admin == other.Admin;
|
|
}
|
|
|
|
void WslCoreVm::TraceLoggingRundown() const noexcept
|
|
try
|
|
{
|
|
WSL_LOG(
|
|
"WslCoreVm::Rundown",
|
|
TraceLoggingValue("Machine Config"),
|
|
TraceLoggingValue(m_machineId.c_str(), "machineId"),
|
|
TraceLoggingValue(ToString(m_vmConfig.NetworkingMode), "networkingMode"));
|
|
|
|
if (m_networkingEngine)
|
|
{
|
|
m_networkingEngine->TraceLoggingRundown();
|
|
}
|
|
}
|
|
CATCH_LOG()
|
|
|
|
void WslCoreVm::ValidateNetworkingMode()
|
|
{
|
|
using namespace wsl::core;
|
|
using namespace wsl::windows::common;
|
|
|
|
ExecutionContext context(Context::ConfigureNetworking);
|
|
|
|
// Cache requested networking features to be logged via telemetry.
|
|
const auto networkingModeRequested = m_vmConfig.NetworkingMode;
|
|
auto firewallRequested = m_vmConfig.FirewallConfig.Enabled();
|
|
auto dnsTunnelingRequested = m_vmConfig.EnableDnsTunneling;
|
|
|
|
// If Hyper-V firewall was requested, ensure it is supported by the OS.
|
|
if (m_vmConfig.FirewallConfig.Enabled())
|
|
{
|
|
if (m_vmConfig.NetworkingMode == NetworkingMode::Mirrored || m_vmConfig.NetworkingMode == NetworkingMode::Nat)
|
|
{
|
|
if (!wsl::core::MirroredNetworking::IsHyperVFirewallSupported(m_vmConfig))
|
|
{
|
|
// Since hyper-V firewall is enabled by default, only show the warning if the user explicitly asked for it.
|
|
if (m_vmConfig.FirewallConfigPresence == ConfigKeyPresence::Present)
|
|
{
|
|
EMIT_USER_WARNING(Localization::MessageHyperVFirewallNotSupported());
|
|
}
|
|
|
|
m_vmConfig.FirewallConfig.reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
// If mirrored networking was requested, ensure it is supported by the OS and guest kernel.
|
|
if (m_vmConfig.NetworkingMode == NetworkingMode::Mirrored)
|
|
{
|
|
if ((m_kernelVersion < std::make_tuple(5u, 10u, 0u)) || !m_seccompAvailable)
|
|
{
|
|
m_vmConfig.NetworkingMode = NetworkingMode::Nat;
|
|
EMIT_USER_WARNING(Localization::MessageMirroredNetworkingNotSupportedReason(
|
|
Localization::MessageMirroredNetworkingNotSupportedKernel()));
|
|
}
|
|
else if (!wsl::core::networking::IsFlowSteeringSupportedByHns() || !m_vmConfig.FirewallConfig.Enabled())
|
|
{
|
|
m_vmConfig.NetworkingMode = NetworkingMode::Nat;
|
|
EMIT_USER_WARNING(Localization::MessageMirroredNetworkingNotSupportedReason(Localization::MessageMirroredNetworkingNotSupportedWindowsVersion(
|
|
m_windowsVersion.BuildNumber, m_windowsVersion.UpdateBuildRevision)));
|
|
}
|
|
}
|
|
|
|
// Localhost relay is not supported in mirrored mode. Generate a warning if the user configures localhost relay
|
|
// together with mirrored mode.
|
|
// N.B. Mirrored mode already provides a way to communicate between Windows and Linux using localhost.
|
|
if (m_vmConfig.NetworkingMode == NetworkingMode::Mirrored && m_vmConfig.LocalhostRelayConfigPresence == ConfigKeyPresence::Present)
|
|
{
|
|
EMIT_USER_WARNING(Localization::MessageLocalhostForwardingNotSupportedMirroredMode());
|
|
}
|
|
|
|
// If DNS tunneling was requested, ensure it is supported by Windows.
|
|
if (m_vmConfig.EnableDnsTunneling && !IsDnsTunnelingSupported())
|
|
{
|
|
// Since DNS tunneling is enabled by default, only show the warning if the user explicitly asked for it.
|
|
if (m_vmConfig.DnsTunnelingConfigPresence == ConfigKeyPresence::Present)
|
|
{
|
|
EMIT_USER_WARNING(Localization::MessageDnsTunnelingNotSupported());
|
|
}
|
|
|
|
m_vmConfig.EnableDnsTunneling = false;
|
|
}
|
|
|
|
// Gives information about the requested networking settings and whether they were enabled or not
|
|
WSL_LOG_TELEMETRY(
|
|
"WslCoreVmValidateNetworkingMode",
|
|
PDT_ProductAndServicePerformance,
|
|
TraceLoggingValue(m_runtimeId, "vmId"),
|
|
TraceLoggingValue(ToString(networkingModeRequested), "networkingModeRequested"),
|
|
TraceLoggingValue(ToString(m_vmConfig.NetworkingMode), "networkingMode"),
|
|
TraceLoggingValue(m_vmConfig.NetworkingModePresence == ConfigKeyPresence::Present, "networkingModePresent"),
|
|
TraceLoggingValue(firewallRequested, "firewallRequested"),
|
|
TraceLoggingValue(m_vmConfig.FirewallConfig.Enabled(), "firewall"),
|
|
TraceLoggingValue(dnsTunnelingRequested, "dnsTunnelingRequested"),
|
|
TraceLoggingValue(m_vmConfig.DnsTunnelingConfigPresence == ConfigKeyPresence::Present, "dnsTunnelingConfigPresent"),
|
|
TraceLoggingValue(m_vmConfig.EnableDnsTunneling, "dnsTunneling"));
|
|
}
|