From e8124ebf75a55e12f399e846411eacce7b0bb92e Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Wed, 15 Jul 2026 08:57:18 -0700 Subject: [PATCH 01/12] WSL2: ship kernel headers and perf in the modules artifacts VHD Rework the kernel headers feature so kernel modules, headers, and the perf binary all ship inside the existing kernel modules VHD (a single "artifacts VHD"), replacing the loose UAPI headers mounted over a 9p share and the separate kernelHeaders= config setting. Guest init detects the nested /{modules,linux-headers,perf} layout, overlays the modules tree so depmod/modprobe work, and bind-moves the headers and perf trees into each distro namespace, exposing /lib/modules//build and /usr/bin/perf. Adds dmesg warnings for malformed or mismatched artifacts VHDs (missing modules.dep, headers, perf dir, or perf binary). Copilot-Session: 43112c89-2bd9-412a-b971-8711b8581800 --- .../technical-documentation/boot-process.md | 1 + src/linux/init/config.cpp | 98 +++++++++++++++ src/linux/init/main.cpp | 118 +++++++++++++++++- src/shared/inc/lxinitshared.h | 4 + test/windows/UnitTests.cpp | 50 ++++++++ 5 files changed, 269 insertions(+), 2 deletions(-) diff --git a/doc/docs/technical-documentation/boot-process.md b/doc/docs/technical-documentation/boot-process.md index fda30ccd51..053c0228e6 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 modules 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)` with `/usr/bin/perf` symlinked to 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/src/linux/init/config.cpp b/src/linux/init/config.cpp index a49cb5b306..36f2d3672f 100644 --- a/src/linux/init/config.cpp +++ b/src/linux/init/config.cpp @@ -1125,6 +1125,104 @@ Return Value: } CATCH_LOG() + try + { + auto tempMount = RemoveMountAndEnvironmentOnScopeExit(LX_WSL2_KERNEL_HEADERS_MOUNT_ENV); + if (tempMount) + { + const char* target = getenv(LX_WSL2_KERNEL_HEADERS_PATH_ENV); + if (target) + { + std::string targetPath{target}; + unsetenv(LX_WSL2_KERNEL_HEADERS_PATH_ENV); + + // + // MS_MOVE requires the destination to already exist, so create the target directory + // tree before moving the mount into place. + // + + if (UtilMkdirPath(targetPath.c_str(), 0755) < 0) + { + LOG_ERROR("UtilMkdirPath({}) failed {}", targetPath, errno); + } + else if (tempMount.MoveMount(targetPath.c_str())) + { + constexpr std::string_view c_includeSuffix = "/include"; + if (targetPath.ends_with(c_includeSuffix)) + { + const std::string headersRoot = targetPath.substr(0, targetPath.size() - c_includeSuffix.size()); + + utsname unameBuffer{}; + THROW_LAST_ERROR_IF(uname(&unameBuffer) < 0); + + const std::string release{unameBuffer.release}; + const std::string modulesDir = std::format("/lib/modules/{}", release); + if (UtilMkdirPath(modulesDir.c_str(), 0755) == 0) + { + const std::string linkPath = modulesDir + "/build"; + if ((symlink(headersRoot.c_str(), linkPath.c_str()) < 0) && (errno != EEXIST)) + { + LOG_ERROR("symlink({}, {}) failed {}", headersRoot, linkPath, errno); + } + } + else + { + LOG_ERROR("UtilMkdirPath({}) failed {}", modulesDir, errno); + } + } + } + } + } + } + CATCH_LOG() + + try + { + auto tempMount = RemoveMountAndEnvironmentOnScopeExit(LX_WSL2_KERNEL_PERF_MOUNT_ENV); + if (tempMount) + { + const char* target = getenv(LX_WSL2_KERNEL_PERF_PATH_ENV); + if (target) + { + std::string targetPath{target}; + unsetenv(LX_WSL2_KERNEL_PERF_PATH_ENV); + + // + // MS_MOVE requires the destination to already exist, so create the target directory + // tree before moving the perf tooling into place. + // + + if (UtilMkdirPath(targetPath.c_str(), 0755) < 0) + { + LOG_ERROR("UtilMkdirPath({}) failed {}", targetPath, errno); + } + else if (tempMount.MoveMount(targetPath.c_str())) + { + // + // Expose perf on the default PATH via /usr/bin/perf, mirroring the Debian/Ubuntu + // linux-tools layout. + // + + const std::string perfBinary = targetPath + "/bin/perf"; + struct stat statBuffer{}; + if (stat(perfBinary.c_str(), &statBuffer) < 0) + { + LOG_WARNING("kernel modules VHD perf tooling has no perf binary at '{}'; /usr/bin/perf not created", perfBinary); + } + else if (UtilMkdirPath("/usr/bin", 0755) < 0) + { + LOG_ERROR("UtilMkdirPath(/usr/bin) failed {}", errno); + } + else if ((symlink(perfBinary.c_str(), "/usr/bin/perf") < 0) && (errno != EEXIST)) + { + LOG_ERROR("symlink({}, /usr/bin/perf) failed {}", perfBinary, errno); + } + } + } + } + } + CATCH_LOG() + // // Change the permission of some devtmpfs devices to be more permissive. // diff --git a/src/linux/init/main.cpp b/src/linux/init/main.cpp index a07cc25850..c9071d5b94 100644 --- a/src/linux/init/main.cpp +++ b/src/linux/init/main.cpp @@ -85,6 +85,11 @@ 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 KERNEL_PERF_BIN_SYMLINK "/usr/bin/perf" #define MODPROBE_PATH "/sbin/modprobe" #define PROCFS_PATH "/proc" #define RESOLV_CONF_FILE "resolv.conf" @@ -115,6 +120,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 +1628,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 create the /usr/bin/perf symlink. + // + + 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,6 +3246,14 @@ 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. + // + // TODO: Determine whether the legacy flat-layout fallback is still needed once the kernel package + // always ships the nested artifacts VHD. If no supported configuration produces a flat + // module-only VHD, this fallback can be removed. + // if (EarlyConfig->KernelModulesDeviceId != UINT_MAX) { @@ -3223,9 +3263,32 @@ 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::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); + struct stat StatBuffer{}; + const bool NestedLayout = (stat(NestedModules.c_str(), &StatBuffer) == 0) && S_ISDIR(StatBuffer.st_mode); + const std::string ModulesLower = NestedLayout ? NestedModules : std::string{KERNEL_MODULES_VHD_PATH}; + + // + // Warn if the selected modules tree does not contain a modules.dep for the running kernel. + // A valid artifacts VHD nests the tree under /modules; a legacy module-only VHD + // places it at the root. A missing modules.dep usually means the VHD is mismatched with the + // kernel, incomplete, or is not a kernel modules VHD at all. + // + if (stat((ModulesLower + "/modules.dep").c_str(), &StatBuffer) != 0) + { + LOG_WARNING( + "kernel modules VHD has no modules for kernel '{}' (no modules.dep in '{}'); modules, headers, and " + "perf may be unavailable", + Release, + ModulesLower); + } + + 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 +3303,57 @@ 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/include"; + if ((stat(HeadersSource.c_str(), &StatBuffer) == 0) && S_ISDIR(StatBuffer.st_mode)) + { + if ((UtilMkdir(KERNEL_HEADERS_TEMP_PATH, 0755) < 0) && (errno != EEXIST)) + { + LOG_ERROR("UtilMkdir({}) failed {}", KERNEL_HEADERS_TEMP_PATH, errno); + } + else if (UtilMount(HeadersSource.c_str(), KERNEL_HEADERS_TEMP_PATH, nullptr, (MS_BIND | MS_REC), nullptr) < 0) + { + LOG_ERROR("bind mount {} failed {}", HeadersSource, errno); + } + else + { + Config.KernelHeadersTarget = std::format("{}{}/include", KERNEL_HEADERS_PATH_PREFIX, Release); + } + } + else + { + LOG_WARNING("kernel modules VHD is missing kernel headers at '{}'", HeadersSource); + } + + const std::string PerfSource = ArtifactsBase + "/perf"; + if ((stat(PerfSource.c_str(), &StatBuffer) == 0) && S_ISDIR(StatBuffer.st_mode)) + { + if ((UtilMkdir(KERNEL_PERF_TEMP_PATH, 0755) < 0) && (errno != EEXIST)) + { + LOG_ERROR("UtilMkdir({}) failed {}", KERNEL_PERF_TEMP_PATH, errno); + } + else if (UtilMount(PerfSource.c_str(), KERNEL_PERF_TEMP_PATH, nullptr, (MS_BIND | MS_REC), nullptr) < 0) + { + LOG_ERROR("bind mount {} failed {}", PerfSource, errno); + } + else + { + Config.KernelPerfTarget = std::format("{}{}", KERNEL_PERF_PATH_PREFIX, Release); + } + } + else + { + LOG_WARNING("kernel modules VHD is missing perf tooling at '{}'", PerfSource); + } + } } // diff --git a/src/shared/inc/lxinitshared.h b/src/shared/inc/lxinitshared.h index e8da4e84f6..afe81f6057 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" diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index 9f67fac2fb..c746e8a70a 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -3002,6 +3002,56 @@ 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) with /usr/bin/perf symlinked to it. + + // 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: the versioned binary exists, /usr/bin/perf resolves to it, and it runs. + 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 -L /usr/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"); From af71d783483859ea52f9234a5c44b427a98104a9 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Wed, 5 Aug 2026 18:57:01 -0700 Subject: [PATCH 02/12] Use unified kernel artifacts VHD Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa7f28d5-85ed-4573-8f45-d056c46f5372 --- CMakeLists.txt | 2 +- UserConfig.cmake.sample | 4 +- .../technical-documentation/boot-process.md | 2 +- msipackage/package.wix.in | 2 +- packages.config | 2 +- src/linux/init/config.cpp | 43 ++----------- src/linux/init/main.cpp | 60 ++++--------------- src/windows/service/exe/HcsVirtualMachine.cpp | 2 +- src/windows/service/exe/WslCoreVm.cpp | 2 +- test/windows/UnitTests.cpp | 29 +++++---- 10 files changed, 44 insertions(+), 104 deletions(-) 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 053c0228e6..20c82f2bbf 100644 --- a/doc/docs/technical-documentation/boot-process.md +++ b/doc/docs/technical-documentation/boot-process.md @@ -83,7 +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 modules 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)` with `/usr/bin/perf` symlinked to it) +- 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)` with its binary bind mounted at `/usr/bin/perf`) 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/config.cpp b/src/linux/init/config.cpp index 36f2d3672f..6de0e3fd3a 100644 --- a/src/linux/init/config.cpp +++ b/src/linux/init/config.cpp @@ -1136,16 +1136,7 @@ Return Value: std::string targetPath{target}; unsetenv(LX_WSL2_KERNEL_HEADERS_PATH_ENV); - // - // MS_MOVE requires the destination to already exist, so create the target directory - // tree before moving the mount into place. - // - - if (UtilMkdirPath(targetPath.c_str(), 0755) < 0) - { - LOG_ERROR("UtilMkdirPath({}) failed {}", targetPath, errno); - } - else if (tempMount.MoveMount(targetPath.c_str())) + if (tempMount.MoveMount(targetPath.c_str())) { constexpr std::string_view c_includeSuffix = "/include"; if (targetPath.ends_with(c_includeSuffix)) @@ -1165,10 +1156,6 @@ Return Value: LOG_ERROR("symlink({}, {}) failed {}", headersRoot, linkPath, errno); } } - else - { - LOG_ERROR("UtilMkdirPath({}) failed {}", modulesDir, errno); - } } } } @@ -1187,35 +1174,17 @@ Return Value: std::string targetPath{target}; unsetenv(LX_WSL2_KERNEL_PERF_PATH_ENV); - // - // MS_MOVE requires the destination to already exist, so create the target directory - // tree before moving the perf tooling into place. - // - - if (UtilMkdirPath(targetPath.c_str(), 0755) < 0) - { - LOG_ERROR("UtilMkdirPath({}) failed {}", targetPath, errno); - } - else if (tempMount.MoveMount(targetPath.c_str())) + if (tempMount.MoveMount(targetPath.c_str())) { // - // Expose perf on the default PATH via /usr/bin/perf, mirroring the Debian/Ubuntu - // linux-tools layout. + // Expose the kernel-matched perf on the default PATH. A bind mount hides any + // distro-provided binary without overwriting a regular file. // const std::string perfBinary = targetPath + "/bin/perf"; - struct stat statBuffer{}; - if (stat(perfBinary.c_str(), &statBuffer) < 0) - { - LOG_WARNING("kernel modules VHD perf tooling has no perf binary at '{}'; /usr/bin/perf not created", perfBinary); - } - else if (UtilMkdirPath("/usr/bin", 0755) < 0) - { - LOG_ERROR("UtilMkdirPath(/usr/bin) failed {}", errno); - } - else if ((symlink(perfBinary.c_str(), "/usr/bin/perf") < 0) && (errno != EEXIST)) + if (UtilMountFile(perfBinary.c_str(), "/usr/bin/perf") < 0) { - LOG_ERROR("symlink({}, /usr/bin/perf) failed {}", perfBinary, errno); + LOG_ERROR("UtilMountFile({}, /usr/bin/perf) failed {}", perfBinary, errno); } } } diff --git a/src/linux/init/main.cpp b/src/linux/init/main.cpp index c9071d5b94..9ecd16ab59 100644 --- a/src/linux/init/main.cpp +++ b/src/linux/init/main.cpp @@ -89,7 +89,6 @@ Module Name: #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 KERNEL_PERF_BIN_SYMLINK "/usr/bin/perf" #define MODPROBE_PATH "/sbin/modprobe" #define PROCFS_PATH "/proc" #define RESOLV_CONF_FILE "resolv.conf" @@ -1644,7 +1643,7 @@ try // // 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 create the /usr/bin/perf symlink. + // /usr/lib/linux-tools/ and bind mount the binary at /usr/bin/perf. // if (!Config.KernelPerfTarget.empty()) @@ -3250,11 +3249,6 @@ try // 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. // - // TODO: Determine whether the legacy flat-layout fallback is still needed once the kernel package - // always ships the nested artifacts VHD. If no supported configuration produces a flat - // module-only VHD, this fallback can be removed. - // - if (EarlyConfig->KernelModulesDeviceId != UINT_MAX) { THROW_LAST_ERROR_IF( @@ -3271,20 +3265,18 @@ try struct stat StatBuffer{}; const bool NestedLayout = (stat(NestedModules.c_str(), &StatBuffer) == 0) && S_ISDIR(StatBuffer.st_mode); const std::string ModulesLower = NestedLayout ? NestedModules : std::string{KERNEL_MODULES_VHD_PATH}; + const bool LegacyLayout = + !NestedLayout && (stat((ModulesLower + "/modules.dep").c_str(), &StatBuffer) == 0) && S_ISREG(StatBuffer.st_mode); // - // Warn if the selected modules tree does not contain a modules.dep for the running kernel. // A valid artifacts VHD nests the tree under /modules; a legacy module-only VHD - // places it at the root. A missing modules.dep usually means the VHD is mismatched with the - // kernel, incomplete, or is not a kernel modules VHD at all. + // places it at the root. // - if (stat((ModulesLower + "/modules.dep").c_str(), &StatBuffer) != 0) + if (LegacyLayout) { LOG_WARNING( - "kernel modules VHD has no modules for kernel '{}' (no modules.dep in '{}'); modules, headers, and " - "perf may be unavailable", - Release, - ModulesLower); + "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"); } std::string Target = std::format("{}/{}", KERNEL_MODULES_PATH, Release); @@ -3313,45 +3305,15 @@ try if (NestedLayout) { const std::string HeadersSource = ArtifactsBase + "/linux-headers/include"; - if ((stat(HeadersSource.c_str(), &StatBuffer) == 0) && S_ISDIR(StatBuffer.st_mode)) - { - if ((UtilMkdir(KERNEL_HEADERS_TEMP_PATH, 0755) < 0) && (errno != EEXIST)) - { - LOG_ERROR("UtilMkdir({}) failed {}", KERNEL_HEADERS_TEMP_PATH, errno); - } - else if (UtilMount(HeadersSource.c_str(), KERNEL_HEADERS_TEMP_PATH, nullptr, (MS_BIND | MS_REC), nullptr) < 0) - { - LOG_ERROR("bind mount {} failed {}", HeadersSource, errno); - } - else - { - Config.KernelHeadersTarget = std::format("{}{}/include", KERNEL_HEADERS_PATH_PREFIX, Release); - } - } - else + if (UtilMount(HeadersSource.c_str(), KERNEL_HEADERS_TEMP_PATH, nullptr, (MS_BIND | MS_REC), nullptr) == 0) { - LOG_WARNING("kernel modules VHD is missing kernel headers at '{}'", HeadersSource); + Config.KernelHeadersTarget = std::format("{}{}/include", KERNEL_HEADERS_PATH_PREFIX, Release); } const std::string PerfSource = ArtifactsBase + "/perf"; - if ((stat(PerfSource.c_str(), &StatBuffer) == 0) && S_ISDIR(StatBuffer.st_mode)) - { - if ((UtilMkdir(KERNEL_PERF_TEMP_PATH, 0755) < 0) && (errno != EEXIST)) - { - LOG_ERROR("UtilMkdir({}) failed {}", KERNEL_PERF_TEMP_PATH, errno); - } - else if (UtilMount(PerfSource.c_str(), KERNEL_PERF_TEMP_PATH, nullptr, (MS_BIND | MS_REC), nullptr) < 0) - { - LOG_ERROR("bind mount {} failed {}", PerfSource, errno); - } - else - { - Config.KernelPerfTarget = std::format("{}{}", KERNEL_PERF_PATH_PREFIX, Release); - } - } - else + if (UtilMount(PerfSource.c_str(), KERNEL_PERF_TEMP_PATH, nullptr, (MS_BIND | MS_REC), nullptr) == 0) { - LOG_WARNING("kernel modules VHD is missing perf tooling at '{}'", PerfSource); + Config.KernelPerfTarget = std::format("{}{}", KERNEL_PERF_PATH_PREFIX, Release); } } } 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/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index c746e8a70a..98c5cb7ff2 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 @@ -3007,7 +3008,7 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND // 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) with /usr/bin/perf symlinked to it. + // /usr/lib/linux-tools/$(uname -r) with its binary bind mounted at /usr/bin/perf. // 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); @@ -3046,9 +3047,17 @@ EOF nullptr), 0u); - // perf: the versioned binary exists, /usr/bin/perf resolves to it, and it runs. + // perf: the versioned binary is bind mounted at /usr/bin/perf and runs. 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 -L /usr/bin/perf", 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); + + // A distro-provided regular file is hidden by the bind mount after the VM restarts. + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"umount /usr/bin/perf && printf old-perf > /usr/bin/perf", nullptr, nullptr, nullptr, nullptr), 0u); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--shutdown"), 0u); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/usr/bin/perf --version", nullptr, nullptr, nullptr, nullptr), 0u); } @@ -6862,13 +6871,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); @@ -6915,13 +6924,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); From 962befccd272453ef3f4590e5c0e29ed9856bce2 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Wed, 5 Aug 2026 20:13:23 -0700 Subject: [PATCH 03/12] Validate bind mount file sources Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa7f28d5-85ed-4573-8f45-d056c46f5372 --- src/linux/init/util.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/linux/init/util.cpp b/src/linux/init/util.cpp index cc11ca76c4..bacbde539d 100644 --- a/src/linux/init/util.cpp +++ b/src/linux/init/util.cpp @@ -1738,6 +1738,9 @@ Return Value: int UtilMountFile(const char* Source, const char* Destination) try { + struct stat sourceInfo{}; + THROW_LAST_ERROR_IF(stat(Source, &sourceInfo) < 0); + // Is the file is a symlink, delete it since that would break the mount. if (std::filesystem::is_symlink(Destination)) { From 71f1a7357fefe163e6ab7823e7a1ed4f2890f8e4 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Wed, 5 Aug 2026 20:30:01 -0700 Subject: [PATCH 04/12] Harden kernel artifact exposure Validate file bind mount sources and replace stale kernel header build links before exposing packaged artifacts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa7f28d5-85ed-4573-8f45-d056c46f5372 --- src/linux/init/config.cpp | 23 ++++++++++++++++++++++- src/linux/init/util.cpp | 1 + test/windows/UnitTests.cpp | 15 +++++++++++++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/linux/init/config.cpp b/src/linux/init/config.cpp index 6de0e3fd3a..e181ef8461 100644 --- a/src/linux/init/config.cpp +++ b/src/linux/init/config.cpp @@ -1151,7 +1151,28 @@ Return Value: if (UtilMkdirPath(modulesDir.c_str(), 0755) == 0) { const std::string linkPath = modulesDir + "/build"; - if ((symlink(headersRoot.c_str(), linkPath.c_str()) < 0) && (errno != EEXIST)) + bool createLink = true; + struct stat linkInfo{}; + if (lstat(linkPath.c_str(), &linkInfo) == 0) + { + if (S_ISDIR(linkInfo.st_mode)) + { + LOG_WARNING("cannot expose kernel headers because '{}' is a directory", linkPath); + createLink = false; + } + else if (unlink(linkPath.c_str()) < 0) + { + LOG_ERROR("unlink({}) failed {}", linkPath, errno); + createLink = false; + } + } + else if (errno != ENOENT) + { + LOG_ERROR("lstat({}) failed {}", linkPath, errno); + createLink = false; + } + + if (createLink && (symlink(headersRoot.c_str(), linkPath.c_str()) < 0)) { LOG_ERROR("symlink({}, {}) failed {}", headersRoot, linkPath, errno); } diff --git a/src/linux/init/util.cpp b/src/linux/init/util.cpp index bacbde539d..23781b4df8 100644 --- a/src/linux/init/util.cpp +++ b/src/linux/init/util.cpp @@ -1740,6 +1740,7 @@ 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/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index 98c5cb7ff2..829c892940 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -3055,9 +3055,20 @@ EOF 0u); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/usr/bin/perf --version", nullptr, nullptr, nullptr, nullptr), 0u); - // A distro-provided regular file is hidden by the bind mount after the VM restarts. - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"umount /usr/bin/perf && printf old-perf > /usr/bin/perf", nullptr, nullptr, nullptr, nullptr), 0u); + // Stale distro-provided artifacts are replaced or hidden after the VM restarts. + VERIFY_ARE_EQUAL( + LxsstuLaunchWsl( + L"rm /lib/modules/$(uname -r)/build && ln -s /tmp /lib/modules/$(uname -r)/build && " + L"umount /usr/bin/perf && 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"/usr/bin/perf --version", nullptr, nullptr, nullptr, nullptr), 0u); } From 762eae0a240cf6dc3e61d4d46cec5ccf77219c9b Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 6 Aug 2026 10:09:13 -0700 Subject: [PATCH 05/12] Mount WSLC modules from artifacts layout Mount the packaged artifacts VHD temporarily and bind the kernel-specific modules directory into the WSLC utility VM. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa7f28d5-85ed-4573-8f45-d056c46f5372 --- src/linux/init/WSLCInit.cpp | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/linux/init/WSLCInit.cpp b/src/linux/init/WSLCInit.cpp index a671ad880d..a053967459 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 { @@ -697,8 +699,10 @@ void HandleMountMessage( const char* source = readField(Message.SourceIndex); + const bool kernelModules = WI_IsFlagSet(Message.Flags, WSLC_MOUNT::KernelModules); + std::string kernelRelease; const char* target{}; - if (WI_IsFlagSet(Message.Flags, WSLC_MOUNT::KernelModules)) + if (kernelModules) { assert(!g_state.ModulesMountPoint.has_value()); @@ -707,7 +711,8 @@ void HandleMountMessage( utsname UnameBuffer{}; THROW_LAST_ERROR_IF(uname(&UnameBuffer) < 0); - g_state.ModulesMountPoint = std::format("/lib/modules/{}", UnameBuffer.release); + kernelRelease = UnameBuffer.release; + g_state.ModulesMountPoint = std::format("/lib/modules/{}", kernelRelease); target = g_state.ModulesMountPoint->c_str(); } else @@ -727,7 +732,27 @@ void HandleMountMessage( } else { - THROW_LAST_ERROR_IF(UtilMount(source, target, type, options.MountFlags, options.StringOptions.c_str(), c_defaultRetryTimeout) < 0); + if (kernelModules) + { + THROW_LAST_ERROR_IF( + UtilMount(source, c_kernelModulesVhdMountPoint, type, options.MountFlags, options.StringOptions.c_str(), c_defaultRetryTimeout) < + 0); + + auto unmountVhd = wil::scope_exit([&]() { + if (umount(c_kernelModulesVhdMountPoint) < 0) + { + LOG_ERROR("umount({}) failed {}", c_kernelModulesVhdMountPoint, errno); + } + }); + + const std::string modulesSource = std::format("{}/{}/modules", c_kernelModulesVhdMountPoint, kernelRelease); + THROW_LAST_ERROR_IF(UtilMount(modulesSource.c_str(), target, nullptr, (MS_BIND | MS_REC), nullptr, c_defaultRetryTimeout) < 0); + } + else + { + THROW_LAST_ERROR_IF( + UtilMount(source, target, type, options.MountFlags, options.StringOptions.c_str(), c_defaultRetryTimeout) < 0); + } } // Workaround for a Linux bug where virtiofs permissions aren't properly propagated when an overlay is mounted on top of a virtiofs share before the permissions have been fetched. From e512aba65e856efc2c1d11d15df8d261fc2b07ef Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 6 Aug 2026 11:02:22 -0700 Subject: [PATCH 06/12] Use dedicated WSLC modules mount message Move kernel artifacts mounting out of the generic mount flags and into a purpose-built WSLC protocol message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa7f28d5-85ed-4573-8f45-d056c46f5372 --- src/linux/init/WSLCInit.cpp | 86 +++++++++---------- src/shared/inc/lxinitshared.h | 19 +++- .../wslcsession/WSLCVirtualMachine.cpp | 14 ++- src/windows/wslcsession/WSLCVirtualMachine.h | 1 + 4 files changed, 74 insertions(+), 46 deletions(-) diff --git a/src/linux/init/WSLCInit.cpp b/src/linux/init/WSLCInit.cpp index a053967459..189fb355ca 100644 --- a/src/linux/init/WSLCInit.cpp +++ b/src/linux/init/WSLCInit.cpp @@ -698,27 +698,7 @@ void HandleMountMessage( } const char* source = readField(Message.SourceIndex); - - const bool kernelModules = WI_IsFlagSet(Message.Flags, WSLC_MOUNT::KernelModules); - std::string kernelRelease; - const char* target{}; - if (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); - - kernelRelease = UnameBuffer.release; - g_state.ModulesMountPoint = std::format("/lib/modules/{}", kernelRelease); - 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)); @@ -732,27 +712,7 @@ void HandleMountMessage( } else { - if (kernelModules) - { - THROW_LAST_ERROR_IF( - UtilMount(source, c_kernelModulesVhdMountPoint, type, options.MountFlags, options.StringOptions.c_str(), c_defaultRetryTimeout) < - 0); - - auto unmountVhd = wil::scope_exit([&]() { - if (umount(c_kernelModulesVhdMountPoint) < 0) - { - LOG_ERROR("umount({}) failed {}", c_kernelModulesVhdMountPoint, errno); - } - }); - - const std::string modulesSource = std::format("{}/{}/modules", c_kernelModulesVhdMountPoint, kernelRelease); - THROW_LAST_ERROR_IF(UtilMount(modulesSource.c_str(), target, nullptr, (MS_BIND | MS_REC), nullptr, c_defaultRetryTimeout) < 0); - } - else - { - THROW_LAST_ERROR_IF( - UtilMount(source, target, type, options.MountFlags, options.StringOptions.c_str(), c_defaultRetryTimeout) < 0); - } + THROW_LAST_ERROR_IF(UtilMount(source, target, type, options.MountFlags, options.StringOptions.c_str(), c_defaultRetryTimeout) < 0); } // Workaround for a Linux bug where virtiofs permissions aren't properly propagated when an overlay is mounted on top of a virtiofs share before the permissions have been fetched. @@ -867,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) { @@ -1104,7 +1104,7 @@ void ProcessMessage(wsl::shared::SocketChannel& Channel, wsl::shared::Transactio { try { - HandleMessage( + HandleMessage( Channel, Transaction, Type, Buffer); } catch (...) diff --git a/src/shared/inc/lxinitshared.h b/src/shared/inc/lxinitshared.h index afe81f6057..467d2c58dc 100644 --- a/src/shared/inc/lxinitshared.h +++ b/src/shared/inc/lxinitshared.h @@ -421,6 +421,7 @@ typedef enum _LX_MESSAGE_TYPE LxMessageWSLCWriteFile, LxMiniInitMessageTrimDistribution, LxMiniInitMessageTrimDistributionResponse, + LxMessageWSLCMountModules, } LX_MESSAGE_TYPE, *PLX_MESSAGE_TYPE; @@ -539,6 +540,7 @@ inline auto ToString(LX_MESSAGE_TYPE messageType) X(LxMessageWSLCWriteFile) X(LxMiniInitMessageTrimDistribution) X(LxMiniInitMessageTrimDistributionResponse) + X(LxMessageWSLCMountModules) default: return ""; @@ -1680,8 +1682,7 @@ struct WSLC_MOUNT None, ReadOnly = 1, Chroot = 2, - OverlayFs = 4, - KernelModules = 8 + OverlayFs = 4 }; char Buffer[]; @@ -1708,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/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); From a90bfd8c147e883350c8b5923d43f843f709468f Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Fri, 7 Aug 2026 08:05:01 -0700 Subject: [PATCH 07/12] Simplify kernel artifact mount handling Mount the kernel headers directory itself instead of its include subdirectory, share the temporary mount handling between the modules, headers and perf payloads, and reuse the modules mount point for the headers build symlink. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa7f28d5-85ed-4573-8f45-d056c46f5372 --- src/linux/init/config.cpp | 148 +++++++++++++++----------------------- src/linux/init/main.cpp | 4 +- 2 files changed, 58 insertions(+), 94 deletions(-) diff --git a/src/linux/init/config.cpp b/src/linux/init/config.cpp index e181ef8461..0a48d56350 100644 --- a/src/linux/init/config.cpp +++ b/src/linux/init/config.cpp @@ -170,6 +170,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,108 +1136,46 @@ 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()) { - auto target = getenv(LX_WSL2_KERNEL_MODULES_PATH_ENV); - if (target) - { - unsetenv(LX_WSL2_KERNEL_MODULES_PATH_ENV); - tempMount.MoveMount(target); - } + return; } - } - CATCH_LOG() - try - { - auto tempMount = RemoveMountAndEnvironmentOnScopeExit(LX_WSL2_KERNEL_HEADERS_MOUNT_ENV); - if (tempMount) - { - const char* target = getenv(LX_WSL2_KERNEL_HEADERS_PATH_ENV); - if (target) - { - std::string targetPath{target}; - unsetenv(LX_WSL2_KERNEL_HEADERS_PATH_ENV); + // + // 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. + // - if (tempMount.MoveMount(targetPath.c_str())) - { - constexpr std::string_view c_includeSuffix = "/include"; - if (targetPath.ends_with(c_includeSuffix)) - { - const std::string headersRoot = targetPath.substr(0, targetPath.size() - c_includeSuffix.size()); - - utsname unameBuffer{}; - THROW_LAST_ERROR_IF(uname(&unameBuffer) < 0); - - const std::string release{unameBuffer.release}; - const std::string modulesDir = std::format("/lib/modules/{}", release); - if (UtilMkdirPath(modulesDir.c_str(), 0755) == 0) - { - const std::string linkPath = modulesDir + "/build"; - bool createLink = true; - struct stat linkInfo{}; - if (lstat(linkPath.c_str(), &linkInfo) == 0) - { - if (S_ISDIR(linkInfo.st_mode)) - { - LOG_WARNING("cannot expose kernel headers because '{}' is a directory", linkPath); - createLink = false; - } - else if (unlink(linkPath.c_str()) < 0) - { - LOG_ERROR("unlink({}) failed {}", linkPath, errno); - createLink = false; - } - } - else if (errno != ENOENT) - { - LOG_ERROR("lstat({}) failed {}", linkPath, errno); - createLink = false; - } - - if (createLink && (symlink(headersRoot.c_str(), linkPath.c_str()) < 0)) - { - LOG_ERROR("symlink({}, {}) failed {}", headersRoot, linkPath, errno); - } - } - } - } - } + const std::string linkPath = kernelModulesPath + "/build"; + if ((unlink(linkPath.c_str()) < 0) && (errno != ENOENT)) + { + LOG_ERROR("unlink({}) failed {}", linkPath, errno); } - } - CATCH_LOG() - try - { - auto tempMount = RemoveMountAndEnvironmentOnScopeExit(LX_WSL2_KERNEL_PERF_MOUNT_ENV); - if (tempMount) + if (symlink(target.c_str(), linkPath.c_str()) < 0) { - const char* target = getenv(LX_WSL2_KERNEL_PERF_PATH_ENV); - if (target) - { - std::string targetPath{target}; - unsetenv(LX_WSL2_KERNEL_PERF_PATH_ENV); + LOG_ERROR("symlink({}, {}) failed {}", target, linkPath, errno); + } + }); - if (tempMount.MoveMount(targetPath.c_str())) - { - // - // Expose the kernel-matched perf on the default PATH. A bind mount hides any - // distro-provided binary without overwriting a regular file. - // + MoveTemporaryMount(LX_WSL2_KERNEL_PERF_MOUNT_ENV, LX_WSL2_KERNEL_PERF_PATH_ENV, [](const std::string& target) { + // + // Expose the kernel-matched perf on the default PATH. A bind mount hides any + // distro-provided binary without overwriting a regular file. + // - const std::string perfBinary = targetPath + "/bin/perf"; - if (UtilMountFile(perfBinary.c_str(), "/usr/bin/perf") < 0) - { - LOG_ERROR("UtilMountFile({}, /usr/bin/perf) failed {}", perfBinary, errno); - } - } - } + const std::string perfBinary = target + "/bin/perf"; + if (UtilMountFile(perfBinary.c_str(), "/usr/bin/perf") < 0) + { + LOG_ERROR("UtilMountFile({}, /usr/bin/perf) failed {}", perfBinary, errno); } - } - CATCH_LOG() + }); // // Change the permission of some devtmpfs devices to be more permissive. diff --git a/src/linux/init/main.cpp b/src/linux/init/main.cpp index 9ecd16ab59..d4cca3bf55 100644 --- a/src/linux/init/main.cpp +++ b/src/linux/init/main.cpp @@ -3304,10 +3304,10 @@ try if (NestedLayout) { - const std::string HeadersSource = ArtifactsBase + "/linux-headers/include"; + 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("{}{}/include", KERNEL_HEADERS_PATH_PREFIX, Release); + Config.KernelHeadersTarget = std::format("{}{}", KERNEL_HEADERS_PATH_PREFIX, Release); } const std::string PerfSource = ArtifactsBase + "/perf"; From 8d9fcde49570923fa3442213503316ac8f56394f Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Fri, 7 Aug 2026 08:55:26 -0700 Subject: [PATCH 08/12] Expose kernel perf without modifying the distro file system perf is now added to the default $PATH and only bind mounted over /usr/bin/perf when the distribution already ships one as a regular file, so distros without perf no longer get an empty stub created for them. PERF_EXEC_PATH is also set since perf is built with a prefix that does not match where it is mounted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa7f28d5-85ed-4573-8f45-d056c46f5372 --- .../technical-documentation/boot-process.md | 2 +- src/linux/init/WslDistributionConfig.h | 1 + src/linux/init/config.cpp | 38 ++++++++++++++++--- src/linux/init/main.cpp | 2 +- test/windows/UnitTests.cpp | 30 +++++++++------ 5 files changed, 53 insertions(+), 20 deletions(-) diff --git a/doc/docs/technical-documentation/boot-process.md b/doc/docs/technical-documentation/boot-process.md index 20c82f2bbf..e4a834206f 100644 --- a/doc/docs/technical-documentation/boot-process.md +++ b/doc/docs/technical-documentation/boot-process.md @@ -83,7 +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)` with its binary bind mounted at `/usr/bin/perf`) +- 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/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 0a48d56350..e93995664e 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" @@ -1164,16 +1166,29 @@ Return Value: } }); - MoveTemporaryMount(LX_WSL2_KERNEL_PERF_MOUNT_ENV, LX_WSL2_KERNEL_PERF_PATH_ENV, [](const std::string& target) { + MoveTemporaryMount(LX_WSL2_KERNEL_PERF_MOUNT_ENV, LX_WSL2_KERNEL_PERF_PATH_ENV, [&](const std::string& target) { // - // Expose the kernel-matched perf on the default PATH. A bind mount hides any - // distro-provided binary without overwriting a regular file. + // Expose the kernel-matched perf via the environment block (see ConfigCreateEnvironmentBlock). // - const std::string perfBinary = target + "/bin/perf"; - if (UtilMountFile(perfBinary.c_str(), "/usr/bin/perf") < 0) + 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)) { - LOG_ERROR("UtilMountFile({}, /usr/bin/perf) failed {}", perfBinary, errno); + 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); + } } }); @@ -1716,6 +1731,17 @@ 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)); + 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 d4cca3bf55..f78136cd14 100644 --- a/src/linux/init/main.cpp +++ b/src/linux/init/main.cpp @@ -1643,7 +1643,7 @@ try // // 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 bind mount the binary at /usr/bin/perf. + // /usr/lib/linux-tools/ and add it to the default $PATH. // if (!Config.KernelPerfTarget.empty()) diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index 829c892940..d4409534c9 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -3047,29 +3047,35 @@ EOF nullptr), 0u); - // perf: the versioned binary is bind mounted at /usr/bin/perf and runs. + // 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. + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"rm -f /usr/bin/perf", nullptr, nullptr, nullptr, nullptr), 0u); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--shutdown"), 0u); 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 \"$(stat -Lc %d:%i /usr/bin/perf)\" = \"$(stat -Lc %d:%i /usr/lib/linux-tools/$(uname -r)/bin/perf)\"", nullptr, nullptr, nullptr, nullptr), + 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); - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/usr/bin/perf --version", nullptr, nullptr, nullptr, nullptr), 0u); - // Stale distro-provided artifacts are replaced or hidden after the VM restarts. + // 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. VERIFY_ARE_EQUAL( - LxsstuLaunchWsl( - L"rm /lib/modules/$(uname -r)/build && ln -s /tmp /lib/modules/$(uname -r)/build && " - L"umount /usr/bin/perf && printf old-perf > /usr/bin/perf", - nullptr, - nullptr, - nullptr, - nullptr), + 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); + + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"umount /usr/bin/perf && rm -f /usr/bin/perf", nullptr, nullptr, nullptr, nullptr), 0u); } WSL2_TEST_METHOD(CrashCollection) From 1153cbc1efa6320d96a46865b7a5287d60502499 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Fri, 7 Aug 2026 09:42:59 -0700 Subject: [PATCH 09/12] Address code review feedback for kernel perf exposure Allow PERF_EXEC_PATH to be overridden via WSLENV, and make the perf portion of the KernelArtifacts test clean up on failure paths so that a leftover /usr/bin/perf can't break subsequent runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa7f28d5-85ed-4573-8f45-d056c46f5372 --- src/linux/init/config.cpp | 10 +++++++++- test/windows/UnitTests.cpp | 12 +++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/linux/init/config.cpp b/src/linux/init/config.cpp index e93995664e..0e6e150ca7 100644 --- a/src/linux/init/config.cpp +++ b/src/linux/init/config.cpp @@ -1740,7 +1740,15 @@ Return Value: if (Config.KernelPerfPath.has_value()) { ConfigAppendToPath(Environment, std::format("{}/bin", *Config.KernelPerfPath)); - Environment.AddVariable(PERF_EXEC_PATH_ENV, std::format("{}/libexec/perf-core", *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/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index d4409534c9..3944897208 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -3049,7 +3049,15 @@ EOF // 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. - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"rm -f /usr/bin/perf", nullptr, nullptr, nullptr, nullptr), 0u); + // + // N.B. The cleanup is registered before the distro's file system is modified so that a + // failure can't leave a perf binary behind, which would be shadowed by the bind mount + // on the next boot and break subsequent runs. + auto perfCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + LxsstuLaunchWsl(L"umount /usr/bin/perf 2>/dev/null; rm -f /usr/bin/perf", nullptr, nullptr, nullptr, nullptr); + }); + + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"umount /usr/bin/perf 2>/dev/null; rm -f /usr/bin/perf", nullptr, nullptr, nullptr, nullptr), 0u); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--shutdown"), 0u); 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); @@ -3074,8 +3082,6 @@ EOF 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); - - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"umount /usr/bin/perf && rm -f /usr/bin/perf", nullptr, nullptr, nullptr, nullptr), 0u); } WSL2_TEST_METHOD(CrashCollection) From 854b1ca45001ee3358521235d6909481970fd1bd Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 10 Aug 2026 14:11:32 -0700 Subject: [PATCH 10/12] Address review feedback on kernel artifact mounting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa7f28d5-85ed-4573-8f45-d056c46f5372 --- src/linux/init/config.cpp | 14 ++++++++++++++ src/linux/init/main.cpp | 7 +++---- src/linux/init/util.cpp | 5 +++++ test/windows/UnitTests.cpp | 20 ++++++++++---------- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/linux/init/config.cpp b/src/linux/init/config.cpp index 0e6e150ca7..7f193e443c 100644 --- a/src/linux/init/config.cpp +++ b/src/linux/init/config.cpp @@ -1153,8 +1153,22 @@ Return Value: // 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)) + { + if (UtilMount(target.c_str(), linkPath.c_str(), nullptr, (MS_BIND | MS_REC), nullptr) < 0) + { + LOG_ERROR("UtilMount({}, {}) failed {}", target, linkPath, errno); + } + + return; + } + if ((unlink(linkPath.c_str()) < 0) && (errno != ENOENT)) { LOG_ERROR("unlink({}) failed {}", linkPath, errno); diff --git a/src/linux/init/main.cpp b/src/linux/init/main.cpp index f78136cd14..23e21c4332 100644 --- a/src/linux/init/main.cpp +++ b/src/linux/init/main.cpp @@ -3262,11 +3262,10 @@ try const std::string ArtifactsBase = std::format("{}/{}", KERNEL_MODULES_VHD_PATH, Release); const std::string NestedModules = ArtifactsBase + "/modules"; - struct stat StatBuffer{}; - const bool NestedLayout = (stat(NestedModules.c_str(), &StatBuffer) == 0) && S_ISDIR(StatBuffer.st_mode); + 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 && (stat((ModulesLower + "/modules.dep").c_str(), &StatBuffer) == 0) && S_ISREG(StatBuffer.st_mode); + 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 diff --git a/src/linux/init/util.cpp b/src/linux/init/util.cpp index 23781b4df8..b4b7623b6f 100644 --- a/src/linux/init/util.cpp +++ b/src/linux/init/util.cpp @@ -1738,6 +1738,11 @@ Return Value: int UtilMountFile(const char* Source, const char* Destination) try { + // + // N.B. The source is validated up front because the destination is created below if it does not + // already exist; letting the mount fail instead would leave an empty file behind. + // + struct stat sourceInfo{}; THROW_LAST_ERROR_IF(stat(Source, &sourceInfo) < 0); THROW_ERRNO_IF(EINVAL, !S_ISREG(sourceInfo.st_mode)); diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index 3944897208..792baae184 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -3008,7 +3008,7 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND // 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) with its binary bind mounted at /usr/bin/perf. + // /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); @@ -3050,15 +3050,7 @@ EOF // 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 cleanup is registered before the distro's file system is modified so that a - // failure can't leave a perf binary behind, which would be shadowed by the bind mount - // on the next boot and break subsequent runs. - auto perfCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { - LxsstuLaunchWsl(L"umount /usr/bin/perf 2>/dev/null; rm -f /usr/bin/perf", nullptr, nullptr, nullptr, nullptr); - }); - - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"umount /usr/bin/perf 2>/dev/null; rm -f /usr/bin/perf", nullptr, nullptr, nullptr, nullptr), 0u); - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--shutdown"), 0u); + // 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( @@ -3070,6 +3062,14 @@ EOF // 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 behind, which would be shadowed by the bind mount + // on the next boot and break subsequent runs. + auto perfCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + LxsstuLaunchWsl(L"umount /usr/bin/perf 2>/dev/null; rm -f /usr/bin/perf", 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); From b04d3bbe783454735543a652c81b991cfe0d3dc2 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 10 Aug 2026 14:26:34 -0700 Subject: [PATCH 11/12] Remove redundant comment in UtilMountFile Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa7f28d5-85ed-4573-8f45-d056c46f5372 --- src/linux/init/util.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/linux/init/util.cpp b/src/linux/init/util.cpp index b4b7623b6f..23781b4df8 100644 --- a/src/linux/init/util.cpp +++ b/src/linux/init/util.cpp @@ -1738,11 +1738,6 @@ Return Value: int UtilMountFile(const char* Source, const char* Destination) try { - // - // N.B. The source is validated up front because the destination is created below if it does not - // already exist; letting the mount fail instead would leave an empty file behind. - // - struct stat sourceInfo{}; THROW_LAST_ERROR_IF(stat(Source, &sourceInfo) < 0); THROW_ERRNO_IF(EINVAL, !S_ISREG(sourceInfo.st_mode)); From 7d9b8f31dcc3c9f2088994f9d291b9b3c5adde80 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 10 Aug 2026 15:10:30 -0700 Subject: [PATCH 12/12] Improve kernel artifact diagnostics and test cleanup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa7f28d5-85ed-4573-8f45-d056c46f5372 --- src/linux/init/main.cpp | 4 ++++ test/windows/UnitTests.cpp | 14 ++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/linux/init/main.cpp b/src/linux/init/main.cpp index 23e21c4332..d1c95134a4 100644 --- a/src/linux/init/main.cpp +++ b/src/linux/init/main.cpp @@ -3277,6 +3277,10 @@ try "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, Release); THROW_LAST_ERROR_IF(UtilMountOverlayFs(Target.c_str(), ModulesLower.c_str(), (MS_NOATIME | MS_NOSUID | MS_NODEV)) < 0); diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index 792baae184..4bb90baf91 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -3064,10 +3064,16 @@ EOF // 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 behind, which would be shadowed by the bind mount - // on the next boot and break subsequent runs. - auto perfCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { - LxsstuLaunchWsl(L"umount /usr/bin/perf 2>/dev/null; rm -f /usr/bin/perf", nullptr, nullptr, nullptr, nullptr); + // 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(