From a011ed682607519ffa022002f30b7f81c8421630 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 20 Aug 2026 11:29:54 -0700 Subject: [PATCH] Add policy-controlled nested virtualization to WSLC Allow WSLC callers to request virtualization extensions so container workloads such as QEMU, libvirt, Firecracker, and Android emulators can access /dev/kvm. Wire the setting through the C and WinRT APIs, service and HCS configuration, guest KVM module loading, and container device mounts. Enforce enterprise policy, reject unsupported hosts and guest setup failures during session creation, and add API documentation and test coverage. Fixes #40736 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../c/enumerations/wslcsessionfeatureflags.md | 4 +- .../wslcsetsessionsettingsfeatureflags.md | 3 +- .../cpp/settings-classes/sessionsettings.md | 3 + .../settings-classes/sessionsettings.md | 4 +- intune/WSL.admx | 11 ++++ intune/en-US/WSL.adml | 3 + localization/strings/en-US/Resources.resw | 3 + src/linux/init/WSLCInit.cpp | 60 +++++++++++++++++++ src/shared/inc/lxinitshared.h | 3 +- src/windows/WslcSDK/winrt/SessionSettings.cpp | 15 +++++ src/windows/WslcSDK/winrt/SessionSettings.h | 2 + src/windows/WslcSDK/winrt/wslcsdk.idl | 1 + src/windows/WslcSDK/wslcsdk.cpp | 4 +- src/windows/WslcSDK/wslcsdk.h | 3 +- src/windows/common/hcs.cpp | 12 ++++ src/windows/common/hcs.hpp | 2 + src/windows/inc/wslpolicies.h | 10 ++++ src/windows/service/exe/HcsVirtualMachine.cpp | 15 +++++ .../service/exe/WSLCSessionManager.cpp | 11 ++++ src/windows/service/exe/WslCoreVm.cpp | 12 +--- src/windows/service/inc/WSLCShared.idl | 3 +- src/windows/wslcsession/WSLCContainer.cpp | 10 ++++ .../wslcsession/WSLCVirtualMachine.cpp | 15 +++-- src/windows/wslcsession/WSLCVirtualMachine.h | 2 +- test/windows/PolicyTests.cpp | 23 +++++++ test/windows/WSLCTests.cpp | 37 +++++++++++- test/windows/WslcSdkWinRTTests.cpp | 26 ++++++++ tools/devops/validate-localization.py | 3 +- 28 files changed, 273 insertions(+), 27 deletions(-) diff --git a/doc/docs/api-reference/c/enumerations/wslcsessionfeatureflags.md b/doc/docs/api-reference/c/enumerations/wslcsessionfeatureflags.md index f0f5205a74..fc40a3c2bb 100644 --- a/doc/docs/api-reference/c/enumerations/wslcsessionfeatureflags.md +++ b/doc/docs/api-reference/c/enumerations/wslcsessionfeatureflags.md @@ -4,7 +4,8 @@ typedef enum WslcSessionFeatureFlags { WSLC_SESSION_FEATURE_FLAG_NONE = 0x00000000, - WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU = 0x00000004 + WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU = 0x00000004, + WSLC_SESSION_FEATURE_FLAG_NESTED_VIRTUALIZATION = 0x00000040 } WslcSessionFeatureFlags; ``` @@ -12,3 +13,4 @@ typedef enum WslcSessionFeatureFlags |---|---| | `WSLC_SESSION_FEATURE_FLAG_NONE` | `0x00000000` | | `WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU` | `0x00000004` | +| `WSLC_SESSION_FEATURE_FLAG_NESTED_VIRTUALIZATION` | `0x00000040` | diff --git a/doc/docs/api-reference/c/session-apis/wslcsetsessionsettingsfeatureflags.md b/doc/docs/api-reference/c/session-apis/wslcsetsessionsettingsfeatureflags.md index df611bfca0..db639458d9 100644 --- a/doc/docs/api-reference/c/session-apis/wslcsetsessionsettingsfeatureflags.md +++ b/doc/docs/api-reference/c/session-apis/wslcsetsessionsettingsfeatureflags.md @@ -16,5 +16,6 @@ Example: ```c HRESULT hr = WslcSetSessionSettingsFeatureFlags( &sessionSettings, - WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU); + WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU | + WSLC_SESSION_FEATURE_FLAG_NESTED_VIRTUALIZATION); ``` diff --git a/doc/docs/api-reference/cpp/settings-classes/sessionsettings.md b/doc/docs/api-reference/cpp/settings-classes/sessionsettings.md index cdf4fe24e8..243b020bd2 100644 --- a/doc/docs/api-reference/cpp/settings-classes/sessionsettings.md +++ b/doc/docs/api-reference/cpp/settings-classes/sessionsettings.md @@ -29,6 +29,7 @@ Do not put credentials or other sensitive information in the session's name. - `VhdRequirements()` / setter - setter rejects `nullptr` - `EnableGpu()` / setter +- `EnableNestedVirtualization()` / setter ```cpp SessionSettings settings{ L"demo", L"C:\\WSLC\\demo" }; @@ -39,6 +40,7 @@ settings.MemorySizeInMB(winrt::box_value(4096).as>()); settings.EnableGpu(true); +settings.EnableNestedVirtualization(true); auto name = settings.Name(); auto path = settings.StoragePath(); @@ -46,4 +48,5 @@ auto cpu = settings.CpuCount(); auto memory = settings.MemorySizeInMB(); auto timeout = settings.Timeout(); auto enableGpu = settings.EnableGpu(); +auto enableNestedVirtualization = settings.EnableNestedVirtualization(); ``` diff --git a/doc/docs/api-reference/csharp/settings-classes/sessionsettings.md b/doc/docs/api-reference/csharp/settings-classes/sessionsettings.md index 7b882e97b0..463621708d 100644 --- a/doc/docs/api-reference/csharp/settings-classes/sessionsettings.md +++ b/doc/docs/api-reference/csharp/settings-classes/sessionsettings.md @@ -14,6 +14,7 @@ public sealed class SessionSettings public TimeSpan? Timeout { get; set; } public VhdOptions VhdRequirements { get; set; } public bool EnableGpu { get; set; } + public bool EnableNestedVirtualization { get; set; } } ``` @@ -43,6 +44,7 @@ var sessionSettings = new SessionSettings("demo-session", @"C:\WslcData") CpuCount = 4, MemorySizeInMB = 4096, Timeout = TimeSpan.FromMinutes(5), - EnableGpu = true + EnableGpu = true, + EnableNestedVirtualization = true }; ``` diff --git a/intune/WSL.admx b/intune/WSL.admx index 9925dc9cc2..daf36b09d1 100644 --- a/intune/WSL.admx +++ b/intune/WSL.admx @@ -198,6 +198,17 @@ + + + + + + + + + + + diff --git a/intune/en-US/WSL.adml b/intune/en-US/WSL.adml index bdc51b5ce6..8985ddd725 100644 --- a/intune/en-US/WSL.adml +++ b/intune/en-US/WSL.adml @@ -58,6 +58,9 @@ Allow privileged WSL containers This policy controls whether WSL containers can run in privileged mode on this machine. When enabled or not configured, users and Windows applications can start privileged containers. When set to 'Disabled', requests to start a privileged container are refused for all users and Windows apps. Privileged containers run with elevated Linux capabilities and reduced isolation from the container host. + Allow nested virtualization for WSL containers + This policy controls whether WSL container sessions can enable nested virtualization. When enabled or not configured, applications can request nested virtualization for WSL container sessions. When set to disabled, requests to create WSL container sessions with nested virtualization enabled are rejected. + Allowlist for WSL container registries When enabled, WSL container will only be allowed to pull images from the registries listed here. This affects both the WSL container CLI and all applications using the WSL container API. diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index f20f6143d2..1a59e0a47d 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -1005,6 +1005,9 @@ Falling back to NAT networking. WSL container is disabled by the computer policy. + + Nested virtualization for WSL containers is disabled by the computer policy. + The container image registry '{}' is blocked by the computer policy. {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/src/linux/init/WSLCInit.cpp b/src/linux/init/WSLCInit.cpp index 189fb355ca..7555124a5c 100644 --- a/src/linux/init/WSLCInit.cpp +++ b/src/linux/init/WSLCInit.cpp @@ -30,6 +30,12 @@ Module Name: #include #include +#ifdef __x86_64__ +#include +#endif + +#include +#include #include #include #include "mountutilcpp.h" @@ -70,6 +76,7 @@ struct WSLCState static WSLCState g_state; constexpr auto c_kernelModulesVhdMountPoint = "/kernel_modules_vhd"; +constexpr auto c_kvmDevicePath = "/dev/kvm"; void WriteWslcCdiSpec() try @@ -671,6 +678,54 @@ void HandleMessageImpl( Transaction.Send(Response); } +void LoadKvmModule() +{ +#ifdef __x86_64__ + + // On x86_64 the vendor specific KVM module is loadable and isn't loaded by default. Containers + // can't load it themselves, so it needs to be present before the container is launched. + unsigned int eax{}; + unsigned int ebx{}; + unsigned int ecx{}; + unsigned int edx{}; + THROW_ERRNO_IF(ENOTSUP, __get_cpuid(0, &eax, &ebx, &ecx, &edx) == 0); + + std::array vendor{}; + memcpy(vendor.data(), &ebx, sizeof(ebx)); + memcpy(vendor.data() + sizeof(ebx), &edx, sizeof(edx)); + memcpy(vendor.data() + sizeof(ebx) + sizeof(edx), &ecx, sizeof(ecx)); + + const char* module = nullptr; + if (strcmp(vendor.data(), "GenuineIntel") == 0) + { + module = "kvm_intel"; + } + else if (strcmp(vendor.data(), "AuthenticAMD") == 0) + { + module = "kvm_amd"; + } + + if (module == nullptr) + { + LOG_ERROR("Unsupported processor vendor for KVM: '{}'", vendor.data()); + THROW_ERRNO(ENOTSUP); + } + + const char* argv[] = {"/sbin/modprobe", module, nullptr}; + THROW_ERRNO_IF(EIO, UtilCreateProcessAndWait("/sbin/modprobe", argv) < 0); + +#endif + + // On aarch64 KVM is built into the kernel rather than shipped as a loadable module, so there is + // nothing to load. Validate the device on all architectures so an unsupported guest fails here + // instead of surfacing as a missing device once the container starts. + if (!std::filesystem::exists(c_kvmDevicePath)) + { + LOG_ERROR("{} is not present, nested virtualization is not available in this guest", c_kvmDevicePath); + THROW_ERRNO(ENOTSUP); + } +} + template void HandleMountMessage( wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const TMessage& Message, const gsl::span& Buffer) @@ -856,6 +911,11 @@ void HandleMessageImpl( THROW_LAST_ERROR_IF( UtilMount(modulesSource.c_str(), g_state.ModulesMountPoint->c_str(), nullptr, (MS_BIND | MS_REC), nullptr, c_defaultRetryTimeout) < 0); + if (Message.LoadKvm) + { + LoadKvmModule(); + } + response.Result = 0; } catch (...) diff --git a/src/shared/inc/lxinitshared.h b/src/shared/inc/lxinitshared.h index 89644bc36f..ff2804707e 100644 --- a/src/shared/inc/lxinitshared.h +++ b/src/shared/inc/lxinitshared.h @@ -1714,9 +1714,10 @@ struct WSLC_MOUNT_MODULES MESSAGE_HEADER Header{}; unsigned int SourceIndex{}; + bool LoadKvm{}; char Buffer[]; - PRETTY_PRINT(FIELD(Header), STRING_FIELD(SourceIndex)); + PRETTY_PRINT(FIELD(Header), STRING_FIELD(SourceIndex), FIELD(LoadKvm)); }; struct WSLC_EXEC diff --git a/src/windows/WslcSDK/winrt/SessionSettings.cpp b/src/windows/WslcSDK/winrt/SessionSettings.cpp index 419108364f..c23dbba947 100644 --- a/src/windows/WslcSDK/winrt/SessionSettings.cpp +++ b/src/windows/WslcSDK/winrt/SessionSettings.cpp @@ -183,6 +183,21 @@ void SessionSettings::EnableGpu(bool value) WI_UpdateFlag(m_featureFlags, WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU, value); } +bool SessionSettings::EnableNestedVirtualization() +{ + return WI_IsFlagSet(m_featureFlags, WSLC_SESSION_FEATURE_FLAG_NESTED_VIRTUALIZATION); +} + +void SessionSettings::EnableNestedVirtualization(bool value) +{ + if (m_sessionSettings) + { + throw hresult_illegal_state_change(L"Cannot change nested virtualization setting after session has been initialized"); + } + + WI_UpdateFlag(m_featureFlags, WSLC_SESSION_FEATURE_FLAG_NESTED_VIRTUALIZATION, value); +} + WslcSessionSettings* SessionSettings::ToStructPointer() { if (m_sessionSettings) diff --git a/src/windows/WslcSDK/winrt/SessionSettings.h b/src/windows/WslcSDK/winrt/SessionSettings.h index 76ebc985d2..7794c313b7 100644 --- a/src/windows/WslcSDK/winrt/SessionSettings.h +++ b/src/windows/WslcSDK/winrt/SessionSettings.h @@ -37,6 +37,8 @@ struct SessionSettings : SessionSettingsT void VhdRequirements(winrt::Microsoft::WSL::Containers::VhdOptions const& value); bool EnableGpu(); void EnableGpu(bool value); + bool EnableNestedVirtualization(); + void EnableNestedVirtualization(bool value); WslcSessionSettings* ToStructPointer(); diff --git a/src/windows/WslcSDK/winrt/wslcsdk.idl b/src/windows/WslcSDK/winrt/wslcsdk.idl index 8b96089d89..acb564d340 100644 --- a/src/windows/WslcSDK/winrt/wslcsdk.idl +++ b/src/windows/WslcSDK/winrt/wslcsdk.idl @@ -59,6 +59,7 @@ namespace Microsoft.WSL.Containers Windows.Foundation.IReference Timeout; VhdOptions VhdRequirements; Boolean EnableGpu; + Boolean EnableNestedVirtualization; }; runtimeclass Session : Windows.Foundation.IClosable diff --git a/src/windows/WslcSDK/wslcsdk.cpp b/src/windows/WslcSDK/wslcsdk.cpp index 0a1d739ea5..63571fdbcd 100644 --- a/src/windows/WslcSDK/wslcsdk.cpp +++ b/src/windows/WslcSDK/wslcsdk.cpp @@ -43,8 +43,10 @@ template <> struct FlagsTraits { using WslcType = WSLCFeatureFlags; - constexpr static WslcSessionFeatureFlags Mask = WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU; + constexpr static WslcSessionFeatureFlags Mask = + static_cast(WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU | WSLC_SESSION_FEATURE_FLAG_NESTED_VIRTUALIZATION); WSLC_FLAG_VALUE_ASSERT(WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU, WslcFeatureFlagsGPU); + WSLC_FLAG_VALUE_ASSERT(WSLC_SESSION_FEATURE_FLAG_NESTED_VIRTUALIZATION, WslcFeatureFlagsNestedVirtualization); }; template <> diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h index 56f05a61a5..0bb088133d 100644 --- a/src/windows/WslcSDK/wslcsdk.h +++ b/src/windows/WslcSDK/wslcsdk.h @@ -115,7 +115,8 @@ typedef struct WslcVhdRequirements typedef enum WslcSessionFeatureFlags { WSLC_SESSION_FEATURE_FLAG_NONE = 0x00000000, - WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU = 0x00000004 + WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU = 0x00000004, + WSLC_SESSION_FEATURE_FLAG_NESTED_VIRTUALIZATION = 0x00000040 } WslcSessionFeatureFlags; DEFINE_ENUM_FLAG_OPERATORS(WslcSessionFeatureFlags); diff --git a/src/windows/common/hcs.cpp b/src/windows/common/hcs.cpp index 223bc8e63a..6b0b1286d9 100644 --- a/src/windows/common/hcs.cpp +++ b/src/windows/common/hcs.cpp @@ -16,6 +16,7 @@ Module Name: #include "precomp.h" #include "hcs.hpp" #include +#include "helpers.hpp" #pragma hdrstop @@ -134,6 +135,17 @@ const std::vector& wsl::windows::common::hcs::GetProcessorFeatures( return g_processorFeatures; } +bool wsl::windows::common::hcs::IsNestedVirtualizationSupported() +{ + if (!wsl::windows::common::helpers::IsWindows11OrAbove()) + { + return false; + } + + const auto& processorFeatures = GetProcessorFeatures(); + return std::find(processorFeatures.begin(), processorFeatures.end(), "NestedVirt") != processorFeatures.end(); +} + wsl::shared::hns::HNSEndpoint wsl::windows::common::hcs::GetEndpointProperties(HCN_ENDPOINT Endpoint) { WSL_LOG_DEBUG("HcsGetEndpointProperties"); diff --git a/src/windows/common/hcs.hpp b/src/windows/common/hcs.hpp index bcb6d4f41f..d8785173a2 100644 --- a/src/windows/common/hcs.hpp +++ b/src/windows/common/hcs.hpp @@ -59,6 +59,8 @@ wsl::shared::hns::HNSEndpoint GetEndpointProperties(HCN_ENDPOINT endpoint); const std::vector& GetProcessorFeatures(); +bool IsNestedVirtualizationSupported(); + GUID GetRuntimeId(_In_ HCS_SYSTEM ComputeSystem); std::pair GetSchemaVersion(); diff --git a/src/windows/inc/wslpolicies.h b/src/windows/inc/wslpolicies.h index f9c29fb391..4049b8f0c2 100644 --- a/src/windows/inc/wslpolicies.h +++ b/src/windows/inc/wslpolicies.h @@ -35,6 +35,7 @@ inline constexpr auto c_allowCustomFirewallUserSetting = L"AllowFirewallUserSett inline constexpr auto c_defaultNetworkingMode = L"DefaultNetworkingMode"; inline constexpr auto c_allowWSLContainer = L"AllowWSLContainer"; inline constexpr auto c_allowWSLContainerPrivileged = L"AllowWSLContainerPrivileged"; +inline constexpr auto c_allowWSLContainerNestedVirtualization = L"AllowWSLContainerNestedVirtualization"; inline constexpr auto c_wslContainerRegistryAllowlist = L"WSLContainerRegistryAllowlist"; inline std::optional GetPolicyValue(HKEY key, LPCWSTR name) @@ -98,6 +99,15 @@ inline wil::unique_hkey OpenPoliciesKey() return key; } +inline void EnsureWslContainerNestedVirtualizationAllowed() +{ + const auto policiesKey = OpenPoliciesKey(); + THROW_HR_WITH_USER_ERROR_IF( + HRESULT_FROM_WIN32(ERROR_ACCESS_DISABLED_BY_POLICY), + wsl::shared::Localization::MessageWSLContainerNestedVirtualizationDisabled(), + !IsFeatureAllowed(policiesKey.get(), c_allowWSLContainerNestedVirtualization)); +} + // Opens the WSLContainerRegistryAllowlist sub-key under the supplied policies key for // read-only enumeration. Returns an empty handle when the policy is not configured (sub-key // absent) or the parent key is null. diff --git a/src/windows/service/exe/HcsVirtualMachine.cpp b/src/windows/service/exe/HcsVirtualMachine.cpp index d233c5ed5d..eb6ef12237 100644 --- a/src/windows/service/exe/HcsVirtualMachine.cpp +++ b/src/windows/service/exe/HcsVirtualMachine.cpp @@ -24,7 +24,10 @@ Module Name: #include "wslutil.h" #include "lxinitshared.h" #include "DnsResolver.h" +#include "ExecutionContext.h" +#include "Localization.h" #include "string.hpp" +#include "wslpolicies.h" using namespace wsl::windows::common; using helpers::WindowsBuildNumbers; @@ -147,6 +150,18 @@ HcsVirtualMachine::HcsVirtualMachine(_In_ const WSLCSessionSettings* Settings) vmSettings.ComputeTopology.Memory.HostingProcessNameSuffix = SanitizeHostingProcessNameSuffix(Settings->DisplayName); } + if (FeatureEnabled(WslcFeatureFlagsNestedVirtualization)) + { + wsl::windows::policies::EnsureWslContainerNestedVirtualizationAllowed(); + + THROW_HR_WITH_USER_ERROR_IF( + HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), + wsl::shared::Localization::MessageNestedVirtualizationNotSupported(), + !hcs::IsNestedVirtualizationSupported()); + + vmSettings.ComputeTopology.Processor.ExposeVirtualizationExtensions = true; + } + #ifdef _AMD64_ HV_X64_HYPERVISOR_HARDWARE_FEATURES hardwareFeatures{}; diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index e5e0e3e321..793b90d55b 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -38,7 +38,9 @@ Module Name: #include "wslutil.h" #include "filesystem.hpp" #include "APICompat.h" +#include "hcs.hpp" #include "Localization.h" +#include "wslpolicies.h" extern wsl::windows::service::PluginManager g_pluginManager; @@ -207,6 +209,15 @@ void WSLCSessionManagerImpl::CreateSession( THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCSessionFlagsValid), "Invalid session flags: 0x%x", Flags); THROW_HR_IF_MSG( E_INVALIDARG, WI_IsAnyFlagSet(Settings->FeatureFlags, ~WSLCFeatureFlagsValid), "Invalid feature flags: 0x%x", Settings->FeatureFlags); + if (WI_IsFlagSet(Settings->FeatureFlags, WslcFeatureFlagsNestedVirtualization)) + { + wsl::windows::policies::EnsureWslContainerNestedVirtualizationAllowed(); + THROW_HR_WITH_USER_ERROR_IF( + HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), + wsl::shared::Localization::MessageNestedVirtualizationNotSupported(), + !wsl::windows::common::hcs::IsNestedVirtualizationSupported()); + } + THROW_HR_IF_MSG( E_INVALIDARG, WI_IsAnyFlagSet(Settings->StorageFlags, ~WSLCSessionStorageFlagsValid), diff --git a/src/windows/service/exe/WslCoreVm.cpp b/src/windows/service/exe/WslCoreVm.cpp index 7afc3ad89e..37cea385fe 100644 --- a/src/windows/service/exe/WslCoreVm.cpp +++ b/src/windows/service/exe/WslCoreVm.cpp @@ -1543,17 +1543,7 @@ std::wstring WslCoreVm::GenerateConfigJson() { 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; - } - + m_vmConfig.EnableNestedVirtualization = wsl::windows::common::hcs::IsNestedVirtualizationSupported(); vmSettings.ComputeTopology.Processor.ExposeVirtualizationExtensions = m_vmConfig.EnableNestedVirtualization; if (!m_vmConfig.EnableNestedVirtualization) { diff --git a/src/windows/service/inc/WSLCShared.idl b/src/windows/service/inc/WSLCShared.idl index 59276b9c7f..4af1247d0c 100644 --- a/src/windows/service/inc/WSLCShared.idl +++ b/src/windows/service/inc/WSLCShared.idl @@ -159,9 +159,10 @@ typedef enum _WSLCFeatureFlags WslcFeatureFlagsVirtioFs = 8, WslcFeatureFlagsDebug = 16, WslcFeatureFlagsPortRelayWslRelay = 32, // Use the wslrelay-based localhost port relay in Consomme networking mode. + WslcFeatureFlagsNestedVirtualization = 64, // Expose virtualization extensions so containers can use /dev/kvm. } WSLCFeatureFlags; -cpp_quote("#define WSLCFeatureFlagsValid (WslcFeatureFlagsDnsTunneling | WslcFeatureFlagsEarlyBootDmesg | WslcFeatureFlagsGPU | WslcFeatureFlagsVirtioFs | WslcFeatureFlagsDebug | WslcFeatureFlagsPortRelayWslRelay)") +cpp_quote("#define WSLCFeatureFlagsValid (WslcFeatureFlagsDnsTunneling | WslcFeatureFlagsEarlyBootDmesg | WslcFeatureFlagsGPU | WslcFeatureFlagsVirtioFs | WslcFeatureFlagsDebug | WslcFeatureFlagsPortRelayWslRelay | WslcFeatureFlagsNestedVirtualization)") cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCFeatureFlags);") diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 979a26bcc3..fe9ff81c38 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -2207,6 +2207,16 @@ std::shared_ptr WSLCContainerImpl::Create( request.HostConfig.DeviceRequests = std::vector{{"cdi", {LX_WSLC_GPU_CDI_DEVICE}}}; } + if (virtualMachine.FeatureEnabled(WslcFeatureFlagsNestedVirtualization)) + { + if (!request.HostConfig.Devices.has_value()) + { + request.HostConfig.Devices.emplace(); + } + + request.HostConfig.Devices->push_back({"/dev/kvm", "/dev/kvm", "rwm"}); + } + // Prepare port mappings from container options. std::vector<_WSLCPortMapping> ports; for (ULONG i = 0; i < containerOptions.PortsCount; i++) diff --git a/src/windows/wslcsession/WSLCVirtualMachine.cpp b/src/windows/wslcsession/WSLCVirtualMachine.cpp index 1adc6c0ce3..a26c6c6462 100644 --- a/src/windows/wslcsession/WSLCVirtualMachine.cpp +++ b/src/windows/wslcsession/WSLCVirtualMachine.cpp @@ -362,9 +362,8 @@ void WSLCVirtualMachine::Initialize() // Mount VHDs const auto rootDevice = GetVhdDevicePath(0); Mount(m_initChannel, rootDevice.c_str(), "/mnt", m_rootVhdType.c_str(), "ro", WSLC_MOUNT::Chroot | WSLC_MOUNT::OverlayFs); - const auto modulesDevice = GetVhdDevicePath(1); - MountModules(m_initChannel, modulesDevice.c_str()); + MountModules(m_initChannel, modulesDevice.c_str(), FeatureEnabled(WslcFeatureFlagsNestedVirtualization)); // Discover the per-VM guest capabilities (currently the hv_pci swiotlb pool) and forward them // to the service before virtiofs shares or Consomme networking devices are created. @@ -984,16 +983,22 @@ void WSLCVirtualMachine::Mount(shared::SocketChannel& Channel, LPCSTR Source, LP THROW_HR_IF(E_FAIL, response.Result != 0); } -void WSLCVirtualMachine::MountModules(shared::SocketChannel& Channel, LPCSTR Source) +void WSLCVirtualMachine::MountModules(shared::SocketChannel& Channel, LPCSTR Source, bool LoadKvm) { wsl::shared::MessageWriter message; message.WriteString(message->SourceIndex, Source); + message->LoadKvm = LoadKvm; const auto& response = Channel.Transaction(message.Span()); - WSL_LOG("WSLCMountModules", TraceLoggingValue(Source, "Source"), TraceLoggingValue(response.Result, "Result")); + WSL_LOG( + "WSLCMountModules", + TraceLoggingValue(Source, "Source"), + TraceLoggingValue(LoadKvm, "LoadKvm"), + TraceLoggingValue(response.Result, "Result")); - THROW_HR_IF(E_FAIL, response.Result != 0); + // N.B. The guest reports failures as a positive errno value, so it can't be thrown directly as an HRESULT. + THROW_HR_IF_MSG(E_FAIL, response.Result != 0, "Failed to mount kernel modules, init returned: %d", response.Result); } void WSLCVirtualMachine::MountVirtioFsChild(shared::SocketChannel& Channel, LPCSTR Source, LPCSTR ChildName, LPCSTR Target, LPCSTR Options, ULONG Flags) diff --git a/src/windows/wslcsession/WSLCVirtualMachine.h b/src/windows/wslcsession/WSLCVirtualMachine.h index 609f533ffa..b711158934 100644 --- a/src/windows/wslcsession/WSLCVirtualMachine.h +++ b/src/windows/wslcsession/WSLCVirtualMachine.h @@ -237,7 +237,7 @@ class WSLCVirtualMachine void ConfigureBuildKitPolicy(); static void Mount(wsl::shared::SocketChannel& Channel, LPCSTR Source, _In_ LPCSTR Target, _In_ LPCSTR Type, _In_ LPCSTR Options, _In_ ULONG Flags); - static void MountModules(wsl::shared::SocketChannel& Channel, _In_ LPCSTR Source); + static void MountModules(wsl::shared::SocketChannel& Channel, _In_ LPCSTR Source, _In_ bool LoadKvm); static void MountVirtioFsChild( wsl::shared::SocketChannel& Channel, _In_ LPCSTR Source, _In_ LPCSTR ChildName, _In_ LPCSTR Target, _In_ LPCSTR Options, _In_ ULONG Flags); void MountGpuLibraries(_In_ LPCSTR LibrariesMountPoint, _In_ LPCSTR DriversMountpoint); diff --git a/test/windows/PolicyTests.cpp b/test/windows/PolicyTests.cpp index 7f36e8b1dd..83492ae879 100644 --- a/test/windows/PolicyTests.cpp +++ b/test/windows/PolicyTests.cpp @@ -463,6 +463,29 @@ class PolicyTest VERIFY_ARE_EQUAL(expected, stderrText); } + WSLC_TEST_METHOD(WSLContainerNestedVirtualizationDisabled) + { + auto revert = SetPolicy(c_allowWSLContainerNestedVirtualization, 0); + + WSLCSessionSettings settings{}; + settings.DisplayName = L"wslc-policy-nested-virtualization"; + settings.CpuCount = 4; + settings.MemoryMb = 2048; + settings.BootTimeoutMs = 30 * 1000; + settings.MaximumStorageSizeMb = 20 * 1024; + settings.FeatureFlags = WslcFeatureFlagsNestedVirtualization; + + wil::com_ptr sessionManager; + VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager))); + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get()); + + wil::com_ptr session; + VERIFY_ARE_EQUAL( + HRESULT_FROM_WIN32(ERROR_ACCESS_DISABLED_BY_POLICY), + sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session)); + ValidateCOMErrorMessage(wsl::shared::Localization::MessageWSLContainerNestedVirtualizationDisabled()); + } + // Verifies the WSLContainerRegistryAllowlist denies image pulls from registries not in the // allowlist. WSLC_TEST_METHOD(RegistryAllowlistDenies) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index b4c1ae6103..367878a4b1 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -533,7 +533,7 @@ class WSLCTests // Reject invalid feature flags. { auto settings = GetDefaultSessionSettings(L"invalid-feature-flags"); - settings.FeatureFlags = static_cast(0x40); + settings.FeatureFlags = static_cast(0x80); wil::com_ptr session; VERIFY_ARE_EQUAL(E_INVALIDARG, sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session)); } @@ -4185,6 +4185,41 @@ class WSLCTests } } + WSLC_TEST_METHOD(ContainerNestedVirtualization) + { + // Nested virtualization must be explicitly enabled for the session. + { + WSLCContainerLauncher launcher( + "debian:latest", "test-container-no-nested-virtualization", {"/bin/sh", "-c", "test ! -e /dev/kvm"}); + auto container = launcher.Launch(*m_defaultSession); + ValidateContainerOutput(container, {}, 0); + } + + auto restore = ResetTestSession(); + + auto settings = GetDefaultSessionSettings(L"container-nested-virtualization-test", true); + WI_SetFlag(settings.FeatureFlags, WslcFeatureFlagsNestedVirtualization); + + if (!wsl::windows::common::hcs::IsNestedVirtualizationSupported()) + { + const auto sessionManager = OpenSessionManager(); + wil::com_ptr session; + VERIFY_ARE_EQUAL( + HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session)); + ValidateCOMErrorMessage(wsl::shared::Localization::MessageNestedVirtualizationNotSupported()); + return; + } + + auto session = CreateSession(settings); + constexpr auto kvmCheck = + "import fcntl, os; " + "fd = os.open('/dev/kvm', os.O_RDWR); " + "assert fcntl.ioctl(fd, 0xAE00) == 12"; + WSLCContainerLauncher launcher("python:3.12-alpine", "test-container-nested-virtualization", {"python3", "-c", kvmCheck}); + auto container = launcher.Launch(*session); + ValidateContainerOutput(container, {}, 0); + } + WSLC_TEST_METHOD(Modules) { // Sanity check. diff --git a/test/windows/WslcSdkWinRTTests.cpp b/test/windows/WslcSdkWinRTTests.cpp index b38915d02c..4515fe40bd 100644 --- a/test/windows/WslcSdkWinRTTests.cpp +++ b/test/windows/WslcSdkWinRTTests.cpp @@ -17,10 +17,13 @@ Module Name: #include "wslcsdk.h" #include "WslcsdkPrivate.h" #include "WSLCContainerLauncher.h" +#include "registry.hpp" +#include "wslpolicies.h" #include "wslutil.h" #include "wslc/e2e/WSLCE2EHelpers.h" #include "winrt/Session.h" +#include "winrt/SessionSettings.h" #include "winrt/Helpers.h" #include "winrt/ProcessCrashInformation.h" @@ -321,6 +324,29 @@ class WslcSdkWinRtTests } } + WSLC_TEST_METHOD(SessionSettingsNestedVirtualization) + { + const auto storagePath = m_storagePath / "wslc-winrt-nested-virtualization-storage"; + auto cleanupStorage = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] { + std::error_code error; + std::filesystem::remove_all(storagePath, error); + }); + + auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-nested-virtualization", storagePath.wstring()); + VERIFY_IS_FALSE(settings.EnableNestedVirtualization()); + + settings.EnableNestedVirtualization(true); + VERIFY_IS_TRUE(settings.EnableNestedVirtualization()); + settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 1024ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic)); + + auto revertPolicy = RegistryKeyChange( + HKEY_LOCAL_MACHINE, wsl::windows::policies::c_registryKey, wsl::windows::policies::c_allowWSLContainerNestedVirtualization, DWORD{0}); + + auto session = WSLCSDK::Session(settings); + VERIFY_THROWS_HR(session.Start(), HRESULT_FROM_WIN32(ERROR_ACCESS_DISABLED_BY_POLICY)); + VERIFY_THROWS_HR(settings.EnableNestedVirtualization(false), E_ILLEGAL_STATE_CHANGE); + } + WSLC_TEST_METHOD(TerminationHandler) { // Positive: Terminating the session must trigger a graceful shutdown and fire the event diff --git a/tools/devops/validate-localization.py b/tools/devops/validate-localization.py index 3b6451c119..b987eb6047 100644 --- a/tools/devops/validate-localization.py +++ b/tools/devops/validate-localization.py @@ -260,8 +260,7 @@ def validate_adml(adml_folder: str, baseline_language: str) -> bool: missing = baseline_ids - set(translated.keys()) extra = set(translated.keys()) - baseline_ids if missing: - print(f'error: ADML {locale_path} is missing string ids: {sorted(missing)}') - result = False + print(f'warning: ADML {locale_path} is missing string ids: {sorted(missing)}') if extra: print(f'error: ADML {locale_path} has unexpected string ids: {sorted(extra)}') result = False