diff --git a/CMakeLists.txt b/CMakeLists.txt index e723b75d3e..fa247e8085 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -518,7 +518,7 @@ endif() if (DEFINED WSL_DEV_BINARY_PATH) # Development shortcut to make the package smaller add_compile_definitions(WSL_SYSTEM_DISTRO_PATH="${WSL_DEV_BINARY_PATH}/system.vhd" WSL_KERNEL_PATH="${WSL_DEV_BINARY_PATH}/kernel" - WSL_KERNEL_MODULES_PATH="${WSL_DEV_BINARY_PATH}/modules.vhd" + WSL_KERNEL_MODULES_PATH="${WSL_DEV_BINARY_PATH}/artifacts.vhd" WSL_DEV_INSTALL_PATH="${WSL_DEV_BINARY_PATH}" WSL_GPU_LIB_PATH="${WSL_DEV_BINARY_PATH}/lib") endif() diff --git a/UserConfig.cmake.sample b/UserConfig.cmake.sample index 82756940d2..12e5287574 100644 --- a/UserConfig.cmake.sample +++ b/UserConfig.cmake.sample @@ -14,12 +14,12 @@ if(WSL_DEV_BINARY_PATH) file(MAKE_DIRECTORY ${WSL_DEV_BINARY_PATH}) file(CREATE_LINK "${KERNEL_SOURCE_DIR}/bin/${TARGET_PLATFORM}/kernel" "${WSL_DEV_BINARY_PATH}/kernel" SYMBOLIC) file(COPY_FILE "${WSLG_SOURCE_DIR}/${TARGET_PLATFORM}/system.vhd" "${WSL_DEV_BINARY_PATH}/system.vhd" ONLY_IF_DIFFERENT) - file(COPY_FILE "${KERNEL_SOURCE_DIR}/bin/${TARGET_PLATFORM}/modules.vhd" "${WSL_DEV_BINARY_PATH}/modules.vhd" ONLY_IF_DIFFERENT) + file(COPY_FILE "${KERNEL_SOURCE_DIR}/bin/${TARGET_PLATFORM}/artifacts.vhd" "${WSL_DEV_BINARY_PATH}/artifacts.vhd" ONLY_IF_DIFFERENT) # read-only VHDs need to be world readable to mount successfully. execute_process( COMMAND icacls.exe "${WSL_DEV_BINARY_PATH}/system.vhd" "/grant:r" "Everyone:(R)" /Q - COMMAND icacls.exe "${WSL_DEV_BINARY_PATH}/modules.vhd" "/grant:r" "Everyone:(R)" /Q + COMMAND icacls.exe "${WSL_DEV_BINARY_PATH}/artifacts.vhd" "/grant:r" "Everyone:(R)" /Q WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} COMMAND_ERROR_IS_FATAL ANY) diff --git a/doc/docs/technical-documentation/boot-process.md b/doc/docs/technical-documentation/boot-process.md index fda30ccd51..e4a834206f 100644 --- a/doc/docs/technical-documentation/boot-process.md +++ b/doc/docs/technical-documentation/boot-process.md @@ -83,6 +83,7 @@ When started, the virtual machine will boot into the provided kernel, and then e - An entropy buffer, to seed the virtual machine's entropy - Information about the GPU drivers shares to mount, if any - Whether [wslg](https://github.com/microsoft/wslg) is enabled +- Whether to mount the bundled Linux kernel headers and perf tooling (shipped in the kernel artifacts VHD; headers are mounted at `/usr/src/linux-headers-$(uname -r)` with `/lib/modules/$(uname -r)/build` symlinked to them, and perf is mounted at `/usr/lib/linux-tools/$(uname -r)`, added to the default `$PATH` and, when the distribution ships its own `/usr/bin/perf`, bind mounted over it) After applying all the configuration requested by [wslservice.exe](wslservice.exe.md), the virtual machine is ready to start Linux distributions. diff --git a/msipackage/package.wix.in b/msipackage/package.wix.in index 1bf5a8dc18..38b01cd2d4 100644 --- a/msipackage/package.wix.in +++ b/msipackage/package.wix.in @@ -539,7 +539,7 @@ - + diff --git a/packages.config b/packages.config index 3546009112..00169e456a 100644 --- a/packages.config +++ b/packages.config @@ -20,7 +20,7 @@ - + diff --git a/src/linux/init/WSLCInit.cpp b/src/linux/init/WSLCInit.cpp index a671ad880d..189fb355ca 100644 --- a/src/linux/init/WSLCInit.cpp +++ b/src/linux/init/WSLCInit.cpp @@ -69,6 +69,8 @@ struct WSLCState static WSLCState g_state; +constexpr auto c_kernelModulesVhdMountPoint = "/kernel_modules_vhd"; + void WriteWslcCdiSpec() try { @@ -696,24 +698,7 @@ void HandleMountMessage( } const char* source = readField(Message.SourceIndex); - - const char* target{}; - if (WI_IsFlagSet(Message.Flags, WSLC_MOUNT::KernelModules)) - { - assert(!g_state.ModulesMountPoint.has_value()); - - // Modules need to be mounted to a specific path that depends on the kernel version. - - utsname UnameBuffer{}; - THROW_LAST_ERROR_IF(uname(&UnameBuffer) < 0); - - g_state.ModulesMountPoint = std::format("/lib/modules/{}", UnameBuffer.release); - target = g_state.ModulesMountPoint->c_str(); - } - else - { - target = readField(Message.DestinationIndex); - } + const char* target = readField(Message.DestinationIndex); // Chroot without OverlayFs is not supported — the chroot logic depends on the overlay target path. THROW_ERRNO_IF(EINVAL, WI_IsFlagSet(Message.Flags, WSLC_MOUNT::Chroot) && !WI_IsFlagSet(Message.Flags, WSLC_MOUNT::OverlayFs)); @@ -842,6 +827,46 @@ void HandleMessageImpl( HandleMountMessage(Channel, Transaction, Message, Buffer); } +void HandleMessageImpl( + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_MOUNT_MODULES& Message, const gsl::span& Buffer) +{ + WSLC_MOUNT_RESULT response{}; + response.Header.MessageType = WSLC_MOUNT_RESULT::Type; + response.Header.MessageSize = sizeof(response); + + try + { + assert(!g_state.ModulesMountPoint.has_value()); + + utsname unameBuffer{}; + THROW_LAST_ERROR_IF(uname(&unameBuffer) < 0); + + const char* source = wsl::shared::string::FromSpan(Buffer, Message.SourceIndex); + THROW_LAST_ERROR_IF(UtilMount(source, c_kernelModulesVhdMountPoint, "ext4", MS_RDONLY, nullptr, c_defaultRetryTimeout) < 0); + + auto unmountVhd = wil::scope_exit([&]() { + if (umount(c_kernelModulesVhdMountPoint) < 0) + { + LOG_ERROR("umount({}) failed {}", c_kernelModulesVhdMountPoint, errno); + } + }); + + g_state.ModulesMountPoint = std::format("/lib/modules/{}", unameBuffer.release); + const std::string modulesSource = std::format("{}/{}/modules", c_kernelModulesVhdMountPoint, unameBuffer.release); + THROW_LAST_ERROR_IF( + UtilMount(modulesSource.c_str(), g_state.ModulesMountPoint->c_str(), nullptr, (MS_BIND | MS_REC), nullptr, c_defaultRetryTimeout) < 0); + + response.Result = 0; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + response.Result = wil::ResultFromCaughtException(); + } + + Transaction.Send(response); +} + void HandleMessageImpl( wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_EXEC& Message, const gsl::span& Buffer) { @@ -1079,7 +1104,7 @@ void ProcessMessage(wsl::shared::SocketChannel& Channel, wsl::shared::Transactio { try { - HandleMessage( + HandleMessage( Channel, Transaction, Type, Buffer); } catch (...) diff --git a/src/linux/init/WslDistributionConfig.h b/src/linux/init/WslDistributionConfig.h index 71414a7201..120e1c8933 100644 --- a/src/linux/init/WslDistributionConfig.h +++ b/src/linux/init/WslDistributionConfig.h @@ -85,6 +85,7 @@ struct WslDistributionConfig bool GuiAppsEnabled = false; std::optional FeatureFlags; + std::optional KernelPerfPath; std::optional NetworkingMode; std::optional VmId; diff --git a/src/linux/init/config.cpp b/src/linux/init/config.cpp index a49cb5b306..7f193e443c 100644 --- a/src/linux/init/config.cpp +++ b/src/linux/init/config.cpp @@ -55,6 +55,8 @@ Module Name: #define LOCALE_FILE_PATH ETC_DEFAULT_FOLDER "locale" #define LOCALE_CONF_FILE_PATH ETC_FOLDER "locale.conf" #define PATH_ENV "PATH" +#define PERF_BINARY_PATH "/usr/bin/perf" +#define PERF_EXEC_PATH_ENV "PERF_EXEC_PATH" #define RESOLV_CONF_DIRECTORY_MODE 0755 #define RESOLV_CONF_FILE_MODE 0644 #define RESOLV_CONF_FILE_NAME "resolv.conf" @@ -170,6 +172,32 @@ class RemoveMountAndEnvironmentOnScopeExit const char* m_mountPath = nullptr; }; +// +// Moves a temporary mount created by mini_init into the distro namespace. The temporary mount point is +// passed via MountEnvironmentName and its final target via PathEnvironmentName. The callback is invoked +// with the target path once the mount has been moved. +// +template +static void MoveTemporaryMount(const char* MountEnvironmentName, const char* PathEnvironmentName, const TCallback& Callback) +try +{ + auto tempMount = RemoveMountAndEnvironmentOnScopeExit(MountEnvironmentName); + const char* target = tempMount ? getenv(PathEnvironmentName) : nullptr; + if (target == nullptr) + { + return; + } + + const std::string targetPath{target}; + unsetenv(PathEnvironmentName); + + if (tempMount.MoveMount(targetPath.c_str())) + { + Callback(targetPath); + } +} +CATCH_LOG() + constexpr auto HostsFileFormatString = LX_INIT_AUTO_GENERATED_FILE_HEADER "# [network]\n" "# generateHosts = false\n" @@ -1110,20 +1138,73 @@ Return Value: } CATCH_LOG() - try - { - auto tempMount = RemoveMountAndEnvironmentOnScopeExit(LX_WSL2_KERNEL_MODULES_MOUNT_ENV); - if (tempMount) + std::string kernelModulesPath; + MoveTemporaryMount(LX_WSL2_KERNEL_MODULES_MOUNT_ENV, LX_WSL2_KERNEL_MODULES_PATH_ENV, [&](const std::string& target) { + kernelModulesPath = target; + }); + + MoveTemporaryMount(LX_WSL2_KERNEL_HEADERS_MOUNT_ENV, LX_WSL2_KERNEL_HEADERS_PATH_ENV, [&](const std::string& target) { + if (kernelModulesPath.empty()) + { + return; + } + + // + // Point /lib/modules//build at the kernel headers, replacing any entry that the + // distro may have created so that it can't shadow the headers matching the running kernel. + // + // N.B. A directory can't be replaced with a symlink (and removing it would mean a recursive + // delete), so bind mount the headers over it instead. + // + + const std::string linkPath = kernelModulesPath + "/build"; + struct stat existing{}; + if ((lstat(linkPath.c_str(), &existing) == 0) && S_ISDIR(existing.st_mode)) { - auto target = getenv(LX_WSL2_KERNEL_MODULES_PATH_ENV); - if (target) + if (UtilMount(target.c_str(), linkPath.c_str(), nullptr, (MS_BIND | MS_REC), nullptr) < 0) { - unsetenv(LX_WSL2_KERNEL_MODULES_PATH_ENV); - tempMount.MoveMount(target); + LOG_ERROR("UtilMount({}, {}) failed {}", target, linkPath, errno); } + + return; } - } - CATCH_LOG() + + if ((unlink(linkPath.c_str()) < 0) && (errno != ENOENT)) + { + LOG_ERROR("unlink({}) failed {}", linkPath, errno); + } + + if (symlink(target.c_str(), linkPath.c_str()) < 0) + { + LOG_ERROR("symlink({}, {}) failed {}", target, linkPath, errno); + } + }); + + MoveTemporaryMount(LX_WSL2_KERNEL_PERF_MOUNT_ENV, LX_WSL2_KERNEL_PERF_PATH_ENV, [&](const std::string& target) { + // + // Expose the kernel-matched perf via the environment block (see ConfigCreateEnvironmentBlock). + // + + Config.KernelPerfPath = target; + + // + // If the distro ships its own perf, shadow it with a bind mount so that the binary matching the + // running kernel is used. + // + // N.B. The distro's file system is only modified if perf is already present as a regular file. + // Distros without perf pick it up via $PATH instead. + // + + struct stat existing{}; + if ((lstat(PERF_BINARY_PATH, &existing) == 0) && S_ISREG(existing.st_mode)) + { + const std::string perfBinary = target + "/bin/perf"; + if (UtilMountFile(perfBinary.c_str(), PERF_BINARY_PATH) < 0) + { + LOG_ERROR("UtilMountFile({}, {}) failed {}", perfBinary, PERF_BINARY_PATH, errno); + } + } + }); // // Change the permission of some devtmpfs devices to be more permissive. @@ -1664,6 +1745,25 @@ Return Value: { ConfigAppendToPath(Environment, LXSS_LIB_PATH); } + + // + // Add the kernel-matched perf tools to the $PATH variable and point perf at its helper + // scripts since it is built with a prefix that does not match where it is mounted. + // + + if (Config.KernelPerfPath.has_value()) + { + ConfigAppendToPath(Environment, std::format("{}/bin", *Config.KernelPerfPath)); + + // + // N.B. This is only set if the user has not provided a value via WSLENV. + // + + if (Environment.GetVariable(PERF_EXEC_PATH_ENV).empty()) + { + Environment.AddVariable(PERF_EXEC_PATH_ENV, std::format("{}/libexec/perf-core", *Config.KernelPerfPath)); + } + } } // diff --git a/src/linux/init/main.cpp b/src/linux/init/main.cpp index a07cc25850..d1c95134a4 100644 --- a/src/linux/init/main.cpp +++ b/src/linux/init/main.cpp @@ -85,6 +85,10 @@ Module Name: #define KERNEL_MODULES_PATH "/lib/modules" #define KERNEL_MODULES_VHD_PATH "/modules" #define KERNEL_MODULES_OVERLAY "/modules_overlay" +#define KERNEL_HEADERS_TEMP_PATH "/kernel_headers" +#define KERNEL_HEADERS_PATH_PREFIX "/usr/src/linux-headers-" +#define KERNEL_PERF_TEMP_PATH "/kernel_perf" +#define KERNEL_PERF_PATH_PREFIX "/usr/lib/linux-tools/" #define MODPROBE_PATH "/sbin/modprobe" #define PROCFS_PATH "/proc" #define RESOLV_CONF_FILE "resolv.conf" @@ -115,6 +119,8 @@ struct VmConfiguration bool EnableSystemDistro = false; bool EnableCrashDumpCollection = false; std::string KernelModulesPath; + std::string KernelHeadersTarget; + std::string KernelPerfTarget; LX_MINI_INIT_NETWORKING_MODE NetworkingMode = LxMiniInitNetworkingModeNone; }; @@ -1621,6 +1627,31 @@ try AddEnvironmentVariable(LX_WSL2_KERNEL_MODULES_PATH_ENV, Config.KernelModulesPath.c_str()); } + // + // If kernel headers were mounted, move them to a temporary location and pass the desired + // target path to the distro init via an environment variable. Distro init will move the + // mount to /usr/src/linux-headers-/include and create the + // /lib/modules//build symlink. + // + + if (!Config.KernelHeadersTarget.empty()) + { + AddTemporaryMount(LX_WSL2_KERNEL_HEADERS_MOUNT_ENV, KERNEL_HEADERS_TEMP_PATH, (MS_MOVE | MS_REC)); + AddEnvironmentVariable(LX_WSL2_KERNEL_HEADERS_PATH_ENV, Config.KernelHeadersTarget.c_str()); + } + + // + // If the perf tooling was mounted, move it to a temporary location and pass the desired target + // path to the distro init via an environment variable. Distro init will move the mount to + // /usr/lib/linux-tools/ and add it to the default $PATH. + // + + if (!Config.KernelPerfTarget.empty()) + { + AddTemporaryMount(LX_WSL2_KERNEL_PERF_MOUNT_ENV, KERNEL_PERF_TEMP_PATH, (MS_MOVE | MS_REC)); + AddEnvironmentVariable(LX_WSL2_KERNEL_PERF_PATH_ENV, Config.KernelPerfTarget.c_str()); + } + // // Bind mount the init daemon into the distro namespace. // @@ -3214,7 +3245,10 @@ try // N.B. The VHD is mounted as read-only but with a writable overlayfs layer. The modules // directory must be writable for tools like depmod to work. // - + // N.B. The artifacts VHD nests its payloads under /{modules,linux-headers,perf}. + // Older module-only VHDs place the modules tree at the filesystem root; fall back to that + // layout when the nested modules directory is not present. + // if (EarlyConfig->KernelModulesDeviceId != UINT_MAX) { THROW_LAST_ERROR_IF( @@ -3223,9 +3257,33 @@ try utsname UnameBuffer{}; THROW_LAST_ERROR_IF(uname(&UnameBuffer) < 0); + const std::string Release{UnameBuffer.release}; + + const std::string ArtifactsBase = std::format("{}/{}", KERNEL_MODULES_VHD_PATH, Release); + const std::string NestedModules = ArtifactsBase + "/modules"; + + std::error_code Error{}; + const bool NestedLayout = std::filesystem::is_directory(NestedModules, Error); + const std::string ModulesLower = NestedLayout ? NestedModules : std::string{KERNEL_MODULES_VHD_PATH}; + const bool LegacyLayout = !NestedLayout && std::filesystem::is_regular_file(ModulesLower + "/modules.dep", Error); + + // + // A valid artifacts VHD nests the tree under /modules; a legacy module-only VHD + // places it at the root. + // + if (LegacyLayout) + { + LOG_WARNING( + "kernel modules VHD uses the legacy flat layout; support for the legacy modules VHD format will be " + "removed in a future version; kernel headers and perf tooling are unavailable"); + } + else if (!NestedLayout) + { + LOG_WARNING("kernel modules VHD does not contain modules for {}", Release); + } - std::string Target = std::format("{}/{}", KERNEL_MODULES_PATH, UnameBuffer.release); - THROW_LAST_ERROR_IF(UtilMountOverlayFs(Target.c_str(), KERNEL_MODULES_VHD_PATH, (MS_NOATIME | MS_NOSUID | MS_NODEV)) < 0); + std::string Target = std::format("{}/{}", KERNEL_MODULES_PATH, Release); + THROW_LAST_ERROR_IF(UtilMountOverlayFs(Target.c_str(), ModulesLower.c_str(), (MS_NOATIME | MS_NOSUID | MS_NODEV)) < 0); const std::string KernelModulesList = wsl::shared::string::FromSpan(Buffer, EarlyConfig->KernelModulesListOffset); for (const auto& Module : wsl::shared::string::Split(KernelModulesList, ',')) @@ -3240,6 +3298,27 @@ try } Config.KernelModulesPath = std::move(Target); + + // + // When the nested artifacts layout is present, bind mount the kernel headers and perf + // tooling to temporary locations. Each distro init moves them into the distro namespace + // at /usr/src/linux-headers- and /usr/lib/linux-tools/ (see config.cpp). + // + + if (NestedLayout) + { + const std::string HeadersSource = ArtifactsBase + "/linux-headers"; + if (UtilMount(HeadersSource.c_str(), KERNEL_HEADERS_TEMP_PATH, nullptr, (MS_BIND | MS_REC), nullptr) == 0) + { + Config.KernelHeadersTarget = std::format("{}{}", KERNEL_HEADERS_PATH_PREFIX, Release); + } + + const std::string PerfSource = ArtifactsBase + "/perf"; + if (UtilMount(PerfSource.c_str(), KERNEL_PERF_TEMP_PATH, nullptr, (MS_BIND | MS_REC), nullptr) == 0) + { + Config.KernelPerfTarget = std::format("{}{}", KERNEL_PERF_PATH_PREFIX, Release); + } + } } // diff --git a/src/linux/init/util.cpp b/src/linux/init/util.cpp index cc11ca76c4..23781b4df8 100644 --- a/src/linux/init/util.cpp +++ b/src/linux/init/util.cpp @@ -1738,6 +1738,10 @@ Return Value: int UtilMountFile(const char* Source, const char* Destination) try { + struct stat sourceInfo{}; + THROW_LAST_ERROR_IF(stat(Source, &sourceInfo) < 0); + THROW_ERRNO_IF(EINVAL, !S_ISREG(sourceInfo.st_mode)); + // Is the file is a symlink, delete it since that would break the mount. if (std::filesystem::is_symlink(Destination)) { diff --git a/src/shared/inc/lxinitshared.h b/src/shared/inc/lxinitshared.h index e8da4e84f6..467d2c58dc 100644 --- a/src/shared/inc/lxinitshared.h +++ b/src/shared/inc/lxinitshared.h @@ -261,6 +261,10 @@ Module Name: #define LX_WSL2_GUI_APP_SUPPORT_ENV "WSL2_GUI_APPS_ENABLED" #define LX_WSL2_KERNEL_MODULES_MOUNT_ENV "WSL2_KERNEL_MODULES_MOUNT" #define LX_WSL2_KERNEL_MODULES_PATH_ENV "WSL2_KERNEL_MODULES_PATH" +#define LX_WSL2_KERNEL_HEADERS_MOUNT_ENV "WSL2_KERNEL_HEADERS_MOUNT" +#define LX_WSL2_KERNEL_HEADERS_PATH_ENV "WSL2_KERNEL_HEADERS_PATH" +#define LX_WSL2_KERNEL_PERF_MOUNT_ENV "WSL2_KERNEL_PERF_MOUNT" +#define LX_WSL2_KERNEL_PERF_PATH_ENV "WSL2_KERNEL_PERF_PATH" #define LX_WSL2_SYSTEM_DISTRO_SHARE_ENV "WSL2_SYSTEM_DISTRO_SHARE" #define LX_WSL2_GPU_SHARE_ENV "WSL2_GPU_SHARE_ENV_" #define LX_WSL2_SHARED_MEMORY_OB_DIRECTORY "WSL2_SHARED_MEMORY_OB_DIRECTORY" @@ -417,6 +421,7 @@ typedef enum _LX_MESSAGE_TYPE LxMessageWSLCWriteFile, LxMiniInitMessageTrimDistribution, LxMiniInitMessageTrimDistributionResponse, + LxMessageWSLCMountModules, } LX_MESSAGE_TYPE, *PLX_MESSAGE_TYPE; @@ -535,6 +540,7 @@ inline auto ToString(LX_MESSAGE_TYPE messageType) X(LxMessageWSLCWriteFile) X(LxMiniInitMessageTrimDistribution) X(LxMiniInitMessageTrimDistributionResponse) + X(LxMessageWSLCMountModules) default: return ""; @@ -1676,8 +1682,7 @@ struct WSLC_MOUNT None, ReadOnly = 1, Chroot = 2, - OverlayFs = 4, - KernelModules = 8 + OverlayFs = 4 }; char Buffer[]; @@ -1704,6 +1709,20 @@ struct WSLC_MOUNT_VIRTIOFS PRETTY_PRINT(FIELD(Header), STRING_FIELD(SourceIndex), STRING_FIELD(DestinationIndex), STRING_FIELD(TypeIndex), STRING_FIELD(OptionsIndex), STRING_FIELD(ChildNameIndex)); }; +struct WSLC_MOUNT_MODULES +{ + static inline auto Type = LxMessageWSLCMountModules; + using TResponse = WSLC_MOUNT_RESULT; + + DECLARE_MESSAGE_CTOR(WSLC_MOUNT_MODULES); + + MESSAGE_HEADER Header{}; + unsigned int SourceIndex{}; + char Buffer[]; + + PRETTY_PRINT(FIELD(Header), STRING_FIELD(SourceIndex)); +}; + struct WSLC_EXEC { static inline auto Type = LxMessageWSLCExec; diff --git a/src/windows/service/exe/HcsVirtualMachine.cpp b/src/windows/service/exe/HcsVirtualMachine.cpp index 5adc883f75..d233c5ed5d 100644 --- a/src/windows/service/exe/HcsVirtualMachine.cpp +++ b/src/windows/service/exe/HcsVirtualMachine.cpp @@ -238,7 +238,7 @@ HcsVirtualMachine::HcsVirtualMachine(_In_ const WSLCSessionSettings* Settings) #ifdef WSL_KERNEL_MODULES_PATH auto kernelModulesPath = std::filesystem::path(TEXT(WSL_KERNEL_MODULES_PATH)); #else - auto kernelModulesPath = basePath / L"tools" / L"modules.vhd"; + auto kernelModulesPath = basePath / L"tools" / L"artifacts.vhd"; #endif // Get root VHD path diff --git a/src/windows/service/exe/WslCoreVm.cpp b/src/windows/service/exe/WslCoreVm.cpp index 5a1a60b014..167ce11335 100644 --- a/src/windows/service/exe/WslCoreVm.cpp +++ b/src/windows/service/exe/WslCoreVm.cpp @@ -236,7 +236,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken #else - m_vmConfig.KernelModulesPath = m_rootFsPath / L"modules.vhd"; + m_vmConfig.KernelModulesPath = m_rootFsPath / L"artifacts.vhd"; #endif } diff --git a/src/windows/wslcsession/WSLCVirtualMachine.cpp b/src/windows/wslcsession/WSLCVirtualMachine.cpp index e86475b08b..1adc6c0ce3 100644 --- a/src/windows/wslcsession/WSLCVirtualMachine.cpp +++ b/src/windows/wslcsession/WSLCVirtualMachine.cpp @@ -364,7 +364,7 @@ void WSLCVirtualMachine::Initialize() Mount(m_initChannel, rootDevice.c_str(), "/mnt", m_rootVhdType.c_str(), "ro", WSLC_MOUNT::Chroot | WSLC_MOUNT::OverlayFs); const auto modulesDevice = GetVhdDevicePath(1); - Mount(m_initChannel, modulesDevice.c_str(), "", "ext4", "ro", WSLC_MOUNT::KernelModules); + MountModules(m_initChannel, modulesDevice.c_str()); // 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,6 +984,18 @@ 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) +{ + wsl::shared::MessageWriter message; + message.WriteString(message->SourceIndex, Source); + + const auto& response = Channel.Transaction(message.Span()); + + WSL_LOG("WSLCMountModules", TraceLoggingValue(Source, "Source"), TraceLoggingValue(response.Result, "Result")); + + THROW_HR_IF(E_FAIL, response.Result != 0); +} + void WSLCVirtualMachine::MountVirtioFsChild(shared::SocketChannel& Channel, LPCSTR Source, LPCSTR ChildName, LPCSTR Target, LPCSTR Options, ULONG Flags) { wsl::shared::MessageWriter message; diff --git a/src/windows/wslcsession/WSLCVirtualMachine.h b/src/windows/wslcsession/WSLCVirtualMachine.h index 842387cac7..609f533ffa 100644 --- a/src/windows/wslcsession/WSLCVirtualMachine.h +++ b/src/windows/wslcsession/WSLCVirtualMachine.h @@ -237,6 +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 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/UnitTests.cpp b/test/windows/UnitTests.cpp index 9f67fac2fb..4bb90baf91 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -2923,10 +2923,11 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND // Ensure the kernel modules folder is mounted correctly. std::wstring command = std::format( L"mount | grep -iF 'none on /usr/lib/modules/{} type overlay " - L"(rw,nosuid,nodev,noatime,lowerdir=/modules,upperdir=/lib/modules/{}/rw/upper,workdir=/lib/modules/{}/rw/" + L"(rw,nosuid,nodev,noatime,lowerdir=/modules/{}/modules,upperdir=/lib/modules/{}/rw/upper,workdir=/lib/modules/{}/rw/" L"work,uuid=on)'", kernelVersion, kernelVersion, + kernelVersion, kernelVersion); 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 #ifdef WSL_DEV_INSTALL_PATH std::wstring kernelPath = WSL_DEV_INSTALL_PATH L"/kernel"; - std::wstring kernelModulesPath = WSL_DEV_INSTALL_PATH L"/modules.vhd"; + std::wstring kernelModulesPath = WSL_DEV_INSTALL_PATH L"/artifacts.vhd"; #else @@ -2963,7 +2964,7 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND std::filesystem::path wslInstallPath(installPath.value()); std::wstring kernelPath = wslInstallPath / "tools" / "kernel"; - std::wstring kernelModulesPath = wslInstallPath / "tools" / "modules.vhd"; + std::wstring kernelModulesPath = wslInstallPath / "tools" / "artifacts.vhd"; #endif @@ -3002,6 +3003,93 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND ValidateOutput(L"dmesg | grep -iF \"failed to load module 'not-found'\" | wc -l", L"1\n", L"", 0); } + WSL2_TEST_METHOD(KernelArtifacts) + { + // The unified kernel artifacts VHD provides the kernel headers and the perf tooling + // alongside the kernel modules. Headers are mounted at /usr/src/linux-headers-$(uname -r) + // with /lib/modules/$(uname -r)/build symlinked to that directory; perf is mounted at + // /usr/lib/linux-tools/$(uname -r) and exposed via $PATH. + + // Headers: the build symlink and a representative uapi header are present. + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -L /lib/modules/$(uname -r)/build", nullptr, nullptr, nullptr, nullptr), 0u); + VERIFY_ARE_EQUAL( + LxsstuLaunchWsl(L"test -s /lib/modules/$(uname -r)/build/include/linux/version.h", nullptr, nullptr, nullptr, nullptr), 0u); + + // Headers are usable: compile and run a tiny program that includes recent uapi headers. The + // identifiers below fail to compile if the headers are missing or too old (BPF_PROG_TYPE_NETFILTER + // added in 6.4, IORING_OP_FUTEX_WAKE added in 6.7). Their numeric values are not a stable API + // contract, so the program only checks that matches the running kernel. + VERIFY_ARE_EQUAL( + LxsstuLaunchWsl( + LR"BASH(bash -ec ' + d=$(mktemp -d) + trap "rm -rf $d" EXIT + cat > "$d/t.c" < +#include +#include +#include +int main(void){ + (void)BPF_PROG_TYPE_NETFILTER; + (void)IORING_OP_FUTEX_WAKE; + printf("%u.%u.%u\n", + LINUX_VERSION_MAJOR, LINUX_VERSION_PATCHLEVEL, LINUX_VERSION_SUBLEVEL); + return 0; +} +EOF + cc -isystem /lib/modules/$(uname -r)/build/include -o "$d/t" "$d/t.c" + v=$("$d/t") + case "$(uname -r)" in "$v"*) exit 0 ;; *) exit 8 ;; esac + ')BASH", + nullptr, + nullptr, + nullptr, + nullptr), + 0u); + + // perf: leave no trace in the distro's file system and make the versioned binary reachable + // via $PATH, with PERF_EXEC_PATH pointing at its helper scripts. + // + // N.B. The test distro does not ship perf, so nothing should be created at /usr/bin/perf. + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -x /usr/lib/linux-tools/$(uname -r)/bin/perf", nullptr, nullptr, nullptr, nullptr), 0u); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test ! -e /usr/bin/perf", nullptr, nullptr, nullptr, nullptr), 0u); + VERIFY_ARE_EQUAL( + LxsstuLaunchWsl(L"test \"$(command -v perf)\" = \"/usr/lib/linux-tools/$(uname -r)/bin/perf\"", nullptr, nullptr, nullptr, nullptr), 0u); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"perf --version", nullptr, nullptr, nullptr, nullptr), 0u); + VERIFY_ARE_EQUAL( + LxsstuLaunchWsl(L"test \"$(perf --exec-path)\" = \"/usr/lib/linux-tools/$(uname -r)/libexec/perf-core\"", nullptr, nullptr, nullptr, nullptr), + 0u); + + // Stale distro-provided artifacts are replaced or hidden after the VM restarts. A distro + // provided perf is shadowed by the binary matching the running kernel. + // + // N.B. The cleanup is registered before the distro's file system is modified so that a + // failure can't leave a perf binary or a broken build symlink behind, which would + // break subsequent runs. + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + LxsstuLaunchWsl( + L"umount /usr/bin/perf 2>/dev/null; rm -f /usr/bin/perf; ln -snf /usr/src/linux-headers-$(uname -r) " + L"/lib/modules/$(uname -r)/build", + nullptr, + nullptr, + nullptr, + nullptr); + }); + + VERIFY_ARE_EQUAL( + LxsstuLaunchWsl(L"rm /lib/modules/$(uname -r)/build && ln -s /tmp /lib/modules/$(uname -r)/build && printf old-perf > /usr/bin/perf", nullptr, nullptr, nullptr, nullptr), + 0u); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--shutdown"), 0u); + VERIFY_ARE_EQUAL( + LxsstuLaunchWsl(L"test \"$(readlink /lib/modules/$(uname -r)/build)\" = \"/usr/src/linux-headers-$(uname -r)\"", nullptr, nullptr, nullptr, nullptr), + 0u); + VERIFY_ARE_EQUAL( + LxsstuLaunchWsl( + L"test \"$(stat -Lc %d:%i /usr/bin/perf)\" = \"$(stat -Lc %d:%i /usr/lib/linux-tools/$(uname -r)/bin/perf)\"", nullptr, nullptr, nullptr, nullptr), + 0u); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/usr/bin/perf --version", nullptr, nullptr, nullptr, nullptr), 0u); + } + WSL2_TEST_METHOD(CrashCollection) { const auto folder = std::filesystem::absolute(L"test-crash-dumps"); @@ -6812,13 +6900,13 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n", // systemDistro VHDs live under the user profile and VMWP wasn't granted access. #ifdef WSL_DEV_INSTALL_PATH - const auto modulesPath = std::format(L"{}\\modules.vhd", WSL_DEV_INSTALL_PATH); + const auto modulesPath = std::format(L"{}\\artifacts.vhd", WSL_DEV_INSTALL_PATH); const auto kernelPath = std::format(L"{}\\kernel", WSL_DEV_INSTALL_PATH); const auto systemDistroPath = std::format(L"{}\\system.vhd", WSL_DEV_INSTALL_PATH); #else const auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath().value(); - const auto modulesPath = std::format(L"{}\\tools\\modules.vhd", installPath); + const auto modulesPath = std::format(L"{}\\tools\\artifacts.vhd", installPath); const auto kernelPath = std::format(L"{}\\tools\\kernel", installPath); const auto systemDistroPath = std::format(L"{}\\system.vhd", installPath); @@ -6865,13 +6953,13 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n", // impersonated user lacks WRITE_DAC for HcsGrantVmAccess. #ifdef WSL_DEV_INSTALL_PATH - const auto modulesPath = std::format(L"{}\\modules.vhd", WSL_DEV_INSTALL_PATH); + const auto modulesPath = std::format(L"{}\\artifacts.vhd", WSL_DEV_INSTALL_PATH); const auto kernelPath = std::format(L"{}\\kernel", WSL_DEV_INSTALL_PATH); const auto systemDistroPath = std::format(L"{}\\system.vhd", WSL_DEV_INSTALL_PATH); #else const auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath().value(); - const auto modulesPath = std::format(L"{}\\tools\\modules.vhd", installPath); + const auto modulesPath = std::format(L"{}\\tools\\artifacts.vhd", installPath); const auto kernelPath = std::format(L"{}\\tools\\kernel", installPath); const auto systemDistroPath = std::format(L"{}\\system.vhd", installPath);