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);