Use unified kernel artifacts VHD layout (#41267)

Update the packaged kernel dependency from modules.vhd to artifacts.vhd and load kernel modules from the versioned <release>/modules directory. Preserve support for legacy flat module VHDs in the standard WSL boot path and teach WSLC sessions to mount modules from the nested layout. Update development paths and kernel module tests for the renamed VHD and new mount source. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Copilot-Session: a7cc3086-6311-4bc8-a591-49fd0e6b03fd

Ben Hillis committed Aug 20, 2026 at 10:57 UTC 9540481e9d276a3a732b22057c872f9bc6d41a0c
12 files changed +120 -39
CMakeLists.txt
+1 -1
@@ -518,7 +518,7 @@ endif()
518 if (DEFINED WSL_DEV_BINARY_PATH) # Development shortcut to make the package smaller
519 add_compile_definitions(WSL_SYSTEM_DISTRO_PATH="${WSL_DEV_BINARY_PATH}/system.vhd"
520 WSL_KERNEL_PATH="${WSL_DEV_BINARY_PATH}/kernel"
521 - WSL_KERNEL_MODULES_PATH="${WSL_DEV_BINARY_PATH}/modules.vhd"
521 + WSL_KERNEL_MODULES_PATH="${WSL_DEV_BINARY_PATH}/artifacts.vhd"
522 WSL_DEV_INSTALL_PATH="${WSL_DEV_BINARY_PATH}"
523 WSL_GPU_LIB_PATH="${WSL_DEV_BINARY_PATH}/lib")
524 endif()
UserConfig.cmake.sample
+2 -2
@@ -14,12 +14,12 @@ if(WSL_DEV_BINARY_PATH)
14 file(MAKE_DIRECTORY ${WSL_DEV_BINARY_PATH})
15 file(CREATE_LINK "${KERNEL_SOURCE_DIR}/bin/${TARGET_PLATFORM}/kernel" "${WSL_DEV_BINARY_PATH}/kernel" SYMBOLIC)
16 file(COPY_FILE "${WSLG_SOURCE_DIR}/${TARGET_PLATFORM}/system.vhd" "${WSL_DEV_BINARY_PATH}/system.vhd" ONLY_IF_DIFFERENT)
17 - file(COPY_FILE "${KERNEL_SOURCE_DIR}/bin/${TARGET_PLATFORM}/modules.vhd" "${WSL_DEV_BINARY_PATH}/modules.vhd" ONLY_IF_DIFFERENT)
17 + file(COPY_FILE "${KERNEL_SOURCE_DIR}/bin/${TARGET_PLATFORM}/artifacts.vhd" "${WSL_DEV_BINARY_PATH}/artifacts.vhd" ONLY_IF_DIFFERENT)
18
19 # read-only VHDs need to be world readable to mount successfully.
20 execute_process(
21 COMMAND icacls.exe "${WSL_DEV_BINARY_PATH}/system.vhd" "/grant:r" "Everyone:(R)" /Q
22 - COMMAND icacls.exe "${WSL_DEV_BINARY_PATH}/modules.vhd" "/grant:r" "Everyone:(R)" /Q
22 + COMMAND icacls.exe "${WSL_DEV_BINARY_PATH}/artifacts.vhd" "/grant:r" "Everyone:(R)" /Q
23 WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
24 COMMAND_ERROR_IS_FATAL ANY)
25
msipackage/package.wix.in
+1 -1
@@ -539,7 +539,7 @@
539
540 <?if "${WSL_DEV_BINARY_PATH}" = "" ?>
541 <File Id="kernel" Source="${KERNEL_SOURCE_DIR}/bin/${TARGET_PLATFORM}/kernel"/>
542 - <File Id="modules.vhd" Source="${KERNEL_SOURCE_DIR}/bin/${TARGET_PLATFORM}/modules.vhd"/>
542 + <File Id="artifacts.vhd" Source="${KERNEL_SOURCE_DIR}/bin/${TARGET_PLATFORM}/artifacts.vhd"/>
543 <?endif?>
544 </Component>
545 </DirectoryRef>
packages.config
+1 -1
@@ -20,7 +20,7 @@
20 <package id="Microsoft.WSL.Dependencies.amd64fre" version="10.0.27820.1000-250318-1700.rs-base2-hyp" targetFramework="native" />
21 <package id="Microsoft.WSL.Dependencies.arm64fre" version="10.0.27820.1000-250318-1700.rs-base2-hyp" targetFramework="native" />
22 <package id="Microsoft.WSL.DeviceHost" version="1.2.60-0" />
23 - <package id="Microsoft.WSL.Kernel" version="6.18.35.2-1" targetFramework="native" />
23 + <package id="Microsoft.WSL.Kernel" version="6.18.40.1-1" targetFramework="native" />
24 <package id="Microsoft.WSL.LinuxSdk" version="1.23.0" targetFramework="native" />
25 <package id="Microsoft.WSL.TestData" version="0.5.0" />
26 <package id="Microsoft.WSL.TestDistro" version="2.7.1-1" />
src/linux/init/WSLCInit.cpp
+44 -19
@@ -69,6 +69,8 @@ struct WSLCState
69
70 static WSLCState g_state;
71
72 +constexpr auto c_kernelModulesVhdMountPoint = "/kernel_modules_vhd";
73 +
74 void WriteWslcCdiSpec()
75 try
76 {
@@ -696,24 +698,7 @@ void HandleMountMessage(
698 }
699
700 const char* source = readField(Message.SourceIndex);
699 -
700 - const char* target{};
701 - if (WI_IsFlagSet(Message.Flags, WSLC_MOUNT::KernelModules))
702 - {
703 - assert(!g_state.ModulesMountPoint.has_value());
704 -
705 - // Modules need to be mounted to a specific path that depends on the kernel version.
706 -
707 - utsname UnameBuffer{};
708 - THROW_LAST_ERROR_IF(uname(&UnameBuffer) < 0);
709 -
710 - g_state.ModulesMountPoint = std::format("/lib/modules/{}", UnameBuffer.release);
711 - target = g_state.ModulesMountPoint->c_str();
712 - }
713 - else
714 - {
715 - target = readField(Message.DestinationIndex);
716 - }
701 + const char* target = readField(Message.DestinationIndex);
702
703 // Chroot without OverlayFs is not supported — the chroot logic depends on the overlay target path.
704 THROW_ERRNO_IF(EINVAL, WI_IsFlagSet(Message.Flags, WSLC_MOUNT::Chroot) && !WI_IsFlagSet(Message.Flags, WSLC_MOUNT::OverlayFs));
@@ -842,6 +827,46 @@ void HandleMessageImpl(
827 HandleMountMessage(Channel, Transaction, Message, Buffer);
828 }
829
830 +void HandleMessageImpl(
831 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_MOUNT_MODULES& Message, const gsl::span<gsl::byte>& Buffer)
832 +{
833 + WSLC_MOUNT_RESULT response{};
834 + response.Header.MessageType = WSLC_MOUNT_RESULT::Type;
835 + response.Header.MessageSize = sizeof(response);
836 +
837 + try
838 + {
839 + assert(!g_state.ModulesMountPoint.has_value());
840 +
841 + utsname unameBuffer{};
842 + THROW_LAST_ERROR_IF(uname(&unameBuffer) < 0);
843 +
844 + const char* source = wsl::shared::string::FromSpan(Buffer, Message.SourceIndex);
845 + THROW_LAST_ERROR_IF(UtilMount(source, c_kernelModulesVhdMountPoint, "ext4", MS_RDONLY, nullptr, c_defaultRetryTimeout) < 0);
846 +
847 + auto unmountVhd = wil::scope_exit([&]() {
848 + if (umount(c_kernelModulesVhdMountPoint) < 0)
849 + {
850 + LOG_ERROR("umount({}) failed {}", c_kernelModulesVhdMountPoint, errno);
851 + }
852 + });
853 +
854 + g_state.ModulesMountPoint = std::format("/lib/modules/{}", unameBuffer.release);
855 + const std::string modulesSource = std::format("{}/{}/modules", c_kernelModulesVhdMountPoint, unameBuffer.release);
856 + THROW_LAST_ERROR_IF(
857 + UtilMount(modulesSource.c_str(), g_state.ModulesMountPoint->c_str(), nullptr, (MS_BIND | MS_REC), nullptr, c_defaultRetryTimeout) < 0);
858 +
859 + response.Result = 0;
860 + }
861 + catch (...)
862 + {
863 + LOG_CAUGHT_EXCEPTION();
864 + response.Result = wil::ResultFromCaughtException();
865 + }
866 +
867 + Transaction.Send<WSLC_MOUNT_RESULT>(response);
868 +}
869 +
870 void HandleMessageImpl(
871 wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_EXEC& Message, const gsl::span<gsl::byte>& Buffer)
872 {
@@ -1079,7 +1104,7 @@ void ProcessMessage(wsl::shared::SocketChannel& Channel, wsl::shared::Transactio
1104 {
1105 try
1106 {
1082 - HandleMessage<WSLC_GET_DISK, WSLC_MOUNT, WSLC_MOUNT_VIRTIOFS, WSLC_EXEC, WSLC_FORK, WSLC_CONNECT, WSLC_SIGNAL, WSLC_TTY_RELAY, WSLC_PORT_RELAY, WSLC_UNMOUNT, WSLC_DETACH, WSLC_ACCEPT, WSLC_WATCH_PROCESSES, WSLC_UNIX_CONNECT, WSLC_GET_GUEST_CAPABILITIES, WSLC_LISTDIR, WSLC_WRITE_FILE>(
1107 + HandleMessage<WSLC_GET_DISK, WSLC_MOUNT, WSLC_MOUNT_VIRTIOFS, WSLC_MOUNT_MODULES, WSLC_EXEC, WSLC_FORK, WSLC_CONNECT, WSLC_SIGNAL, WSLC_TTY_RELAY, WSLC_PORT_RELAY, WSLC_UNMOUNT, WSLC_DETACH, WSLC_ACCEPT, WSLC_WATCH_PROCESSES, WSLC_UNIX_CONNECT, WSLC_GET_GUEST_CAPABILITIES, WSLC_LISTDIR, WSLC_WRITE_FILE>(
1108 Channel, Transaction, Type, Buffer);
1109 }
1110 catch (...)
src/linux/init/main.cpp
+30 -3
@@ -3223,7 +3223,10 @@ try
3223 // N.B. The VHD is mounted as read-only but with a writable overlayfs layer. The modules
3224 // directory must be writable for tools like depmod to work.
3225 //
3226 -
3226 + // N.B. The artifacts VHD nests the modules under <release>/modules.
3227 + // Older module-only VHDs place the modules tree at the filesystem root; fall back to that
3228 + // layout when the nested modules directory is not present.
3229 + //
3230 if (EarlyConfig->KernelModulesDeviceId != UINT_MAX)
3231 {
3232 THROW_LAST_ERROR_IF(
@@ -3232,9 +3235,33 @@ try
3235
3236 utsname UnameBuffer{};
3237 THROW_LAST_ERROR_IF(uname(&UnameBuffer) < 0);
3238 + const std::string Release{UnameBuffer.release};
3239 +
3240 + const std::string ArtifactsBase = std::format("{}/{}", KERNEL_MODULES_VHD_PATH, Release);
3241 + const std::string NestedModules = ArtifactsBase + "/modules";
3242 +
3243 + std::error_code Error{};
3244 + const bool NestedLayout = std::filesystem::is_directory(NestedModules, Error);
3245 + const std::string ModulesLower = NestedLayout ? NestedModules : std::string{KERNEL_MODULES_VHD_PATH};
3246 + const bool LegacyLayout = !NestedLayout && std::filesystem::is_regular_file(ModulesLower + "/modules.dep", Error);
3247 +
3248 + //
3249 + // A valid artifacts VHD nests the tree under <release>/modules; a legacy module-only VHD
3250 + // places it at the root.
3251 + //
3252 + if (LegacyLayout)
3253 + {
3254 + LOG_WARNING(
3255 + "kernel modules VHD uses the legacy flat layout; support for the legacy modules VHD format will be "
3256 + "removed in a future version");
3257 + }
3258 + else if (!NestedLayout)
3259 + {
3260 + LOG_WARNING("kernel modules VHD does not contain modules for {}", Release);
3261 + }
3262
3236 - std::string Target = std::format("{}/{}", KERNEL_MODULES_PATH, UnameBuffer.release);
3237 - THROW_LAST_ERROR_IF(UtilMountOverlayFs(Target.c_str(), KERNEL_MODULES_VHD_PATH, (MS_NOATIME | MS_NOSUID | MS_NODEV)) < 0);
3263 + std::string Target = std::format("{}/{}", KERNEL_MODULES_PATH, Release);
3264 + THROW_LAST_ERROR_IF(UtilMountOverlayFs(Target.c_str(), ModulesLower.c_str(), (MS_NOATIME | MS_NOSUID | MS_NODEV)) < 0);
3265
3266 const std::string KernelModulesList = wsl::shared::string::FromSpan(Buffer, EarlyConfig->KernelModulesListOffset);
3267 for (const auto& Module : wsl::shared::string::Split(KernelModulesList, ','))
src/shared/inc/lxinitshared.h
+17 -2
@@ -417,6 +417,7 @@ typedef enum _LX_MESSAGE_TYPE
417 LxMessageWSLCWriteFile,
418 LxMiniInitMessageTrimDistribution,
419 LxMiniInitMessageTrimDistributionResponse,
420 + LxMessageWSLCMountModules,
421 } LX_MESSAGE_TYPE,
422 *PLX_MESSAGE_TYPE;
423
@@ -535,6 +536,7 @@ inline auto ToString(LX_MESSAGE_TYPE messageType)
536 X(LxMessageWSLCWriteFile)
537 X(LxMiniInitMessageTrimDistribution)
538 X(LxMiniInitMessageTrimDistributionResponse)
539 + X(LxMessageWSLCMountModules)
540
541 default:
542 return "<unexpected LX_MESSAGE_TYPE>";
@@ -1676,8 +1678,7 @@ struct WSLC_MOUNT
1678 None,
1679 ReadOnly = 1,
1680 Chroot = 2,
1679 - OverlayFs = 4,
1680 - KernelModules = 8
1681 + OverlayFs = 4
1682 };
1683
1684 char Buffer[];
@@ -1704,6 +1705,20 @@ struct WSLC_MOUNT_VIRTIOFS
1705 PRETTY_PRINT(FIELD(Header), STRING_FIELD(SourceIndex), STRING_FIELD(DestinationIndex), STRING_FIELD(TypeIndex), STRING_FIELD(OptionsIndex), STRING_FIELD(ChildNameIndex));
1706 };
1707
1708 +struct WSLC_MOUNT_MODULES
1709 +{
1710 + static inline auto Type = LxMessageWSLCMountModules;
1711 + using TResponse = WSLC_MOUNT_RESULT;
1712 +
1713 + DECLARE_MESSAGE_CTOR(WSLC_MOUNT_MODULES);
1714 +
1715 + MESSAGE_HEADER Header{};
1716 + unsigned int SourceIndex{};
1717 + char Buffer[];
1718 +
1719 + PRETTY_PRINT(FIELD(Header), STRING_FIELD(SourceIndex));
1720 +};
1721 +
1722 struct WSLC_EXEC
1723 {
1724 static inline auto Type = LxMessageWSLCExec;
src/windows/service/exe/HcsVirtualMachine.cpp
+1 -1
@@ -238,7 +238,7 @@ HcsVirtualMachine::HcsVirtualMachine(_In_ const WSLCSessionSettings* Settings)
238 #ifdef WSL_KERNEL_MODULES_PATH
239 auto kernelModulesPath = std::filesystem::path(TEXT(WSL_KERNEL_MODULES_PATH));
240 #else
241 - auto kernelModulesPath = basePath / L"tools" / L"modules.vhd";
241 + auto kernelModulesPath = basePath / L"tools" / L"artifacts.vhd";
242 #endif
243
244 // Get root VHD path
src/windows/service/exe/WslCoreVm.cpp
+1 -1
@@ -236,7 +236,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken
236
237 #else
238
239 - m_vmConfig.KernelModulesPath = m_rootFsPath / L"modules.vhd";
239 + m_vmConfig.KernelModulesPath = m_rootFsPath / L"artifacts.vhd";
240
241 #endif
242 }
src/windows/wslcsession/WSLCVirtualMachine.cpp
+13 -1
@@ -364,7 +364,7 @@ void WSLCVirtualMachine::Initialize()
364 Mount(m_initChannel, rootDevice.c_str(), "/mnt", m_rootVhdType.c_str(), "ro", WSLC_MOUNT::Chroot | WSLC_MOUNT::OverlayFs);
365
366 const auto modulesDevice = GetVhdDevicePath(1);
367 - Mount(m_initChannel, modulesDevice.c_str(), "", "ext4", "ro", WSLC_MOUNT::KernelModules);
367 + MountModules(m_initChannel, modulesDevice.c_str());
368
369 // Discover the per-VM guest capabilities (currently the hv_pci swiotlb pool) and forward them
370 // to the service before virtiofs shares or Consomme networking devices are created.
@@ -984,6 +984,18 @@ void WSLCVirtualMachine::Mount(shared::SocketChannel& Channel, LPCSTR Source, LP
984 THROW_HR_IF(E_FAIL, response.Result != 0);
985 }
986
987 +void WSLCVirtualMachine::MountModules(shared::SocketChannel& Channel, LPCSTR Source)
988 +{
989 + wsl::shared::MessageWriter<WSLC_MOUNT_MODULES> message;
990 + message.WriteString(message->SourceIndex, Source);
991 +
992 + const auto& response = Channel.Transaction<WSLC_MOUNT_MODULES>(message.Span());
993 +
994 + WSL_LOG("WSLCMountModules", TraceLoggingValue(Source, "Source"), TraceLoggingValue(response.Result, "Result"));
995 +
996 + THROW_HR_IF(E_FAIL, response.Result != 0);
997 +}
998 +
999 void WSLCVirtualMachine::MountVirtioFsChild(shared::SocketChannel& Channel, LPCSTR Source, LPCSTR ChildName, LPCSTR Target, LPCSTR Options, ULONG Flags)
1000 {
1001 wsl::shared::MessageWriter<WSLC_MOUNT_VIRTIOFS> message;
src/windows/wslcsession/WSLCVirtualMachine.h
+1
@@ -237,6 +237,7 @@ private:
237 void ConfigureBuildKitPolicy();
238
239 static void Mount(wsl::shared::SocketChannel& Channel, LPCSTR Source, _In_ LPCSTR Target, _In_ LPCSTR Type, _In_ LPCSTR Options, _In_ ULONG Flags);
240 + static void MountModules(wsl::shared::SocketChannel& Channel, _In_ LPCSTR Source);
241 static void MountVirtioFsChild(
242 wsl::shared::SocketChannel& Channel, _In_ LPCSTR Source, _In_ LPCSTR ChildName, _In_ LPCSTR Target, _In_ LPCSTR Options, _In_ ULONG Flags);
243 void MountGpuLibraries(_In_ LPCSTR LibrariesMountPoint, _In_ LPCSTR DriversMountpoint);
test/windows/UnitTests.cpp
+8 -7
@@ -2923,10 +2923,11 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND
2923 // Ensure the kernel modules folder is mounted correctly.
2924 std::wstring command = std::format(
2925 L"mount | grep -iF 'none on /usr/lib/modules/{} type overlay "
2926 - L"(rw,nosuid,nodev,noatime,lowerdir=/modules,upperdir=/lib/modules/{}/rw/upper,workdir=/lib/modules/{}/rw/"
2926 + L"(rw,nosuid,nodev,noatime,lowerdir=/modules/{}/modules,upperdir=/lib/modules/{}/rw/upper,workdir=/lib/modules/{}/rw/"
2927 L"work,uuid=on)'",
2928 kernelVersion,
2929 kernelVersion,
2930 + kernelVersion,
2931 kernelVersion);
2932
2933 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(command.c_str(), nullptr, nullptr, nullptr, nullptr), 0u);
@@ -2953,7 +2954,7 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND
2954 #ifdef WSL_DEV_INSTALL_PATH
2955
2956 std::wstring kernelPath = WSL_DEV_INSTALL_PATH L"/kernel";
2956 - std::wstring kernelModulesPath = WSL_DEV_INSTALL_PATH L"/modules.vhd";
2957 + std::wstring kernelModulesPath = WSL_DEV_INSTALL_PATH L"/artifacts.vhd";
2958
2959 #else
2960
@@ -2963,7 +2964,7 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND
2964 std::filesystem::path wslInstallPath(installPath.value());
2965
2966 std::wstring kernelPath = wslInstallPath / "tools" / "kernel";
2966 - std::wstring kernelModulesPath = wslInstallPath / "tools" / "modules.vhd";
2967 + std::wstring kernelModulesPath = wslInstallPath / "tools" / "artifacts.vhd";
2968
2969 #endif
2970
@@ -6809,13 +6810,13 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n",
6810 // systemDistro VHDs live under the user profile and VMWP wasn't granted access.
6811 #ifdef WSL_DEV_INSTALL_PATH
6812
6812 - const auto modulesPath = std::format(L"{}\\modules.vhd", WSL_DEV_INSTALL_PATH);
6813 + const auto modulesPath = std::format(L"{}\\artifacts.vhd", WSL_DEV_INSTALL_PATH);
6814 const auto kernelPath = std::format(L"{}\\kernel", WSL_DEV_INSTALL_PATH);
6815 const auto systemDistroPath = std::format(L"{}\\system.vhd", WSL_DEV_INSTALL_PATH);
6816
6817 #else
6818 const auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath().value();
6818 - const auto modulesPath = std::format(L"{}\\tools\\modules.vhd", installPath);
6819 + const auto modulesPath = std::format(L"{}\\tools\\artifacts.vhd", installPath);
6820 const auto kernelPath = std::format(L"{}\\tools\\kernel", installPath);
6821 const auto systemDistroPath = std::format(L"{}\\system.vhd", installPath);
6822
@@ -6862,13 +6863,13 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n",
6863 // impersonated user lacks WRITE_DAC for HcsGrantVmAccess.
6864 #ifdef WSL_DEV_INSTALL_PATH
6865
6865 - const auto modulesPath = std::format(L"{}\\modules.vhd", WSL_DEV_INSTALL_PATH);
6866 + const auto modulesPath = std::format(L"{}\\artifacts.vhd", WSL_DEV_INSTALL_PATH);
6867 const auto kernelPath = std::format(L"{}\\kernel", WSL_DEV_INSTALL_PATH);
6868 const auto systemDistroPath = std::format(L"{}\\system.vhd", WSL_DEV_INSTALL_PATH);
6869
6870 #else
6871 const auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath().value();
6871 - const auto modulesPath = std::format(L"{}\\tools\\modules.vhd", installPath);
6872 + const auto modulesPath = std::format(L"{}\\tools\\artifacts.vhd", installPath);
6873 const auto kernelPath = std::format(L"{}\\tools\\kernel", installPath);
6874 const auto systemDistroPath = std::format(L"{}\\system.vhd", installPath);
6875