Service: Best-effort grant of VMWP access to user-supplied VHDs (#40485)

HCS fails with E_ACCESSDENIED when starting a VM whose user-supplied kernelModules or systemDistro VHDs live somewhere VMWP cannot read (e.g. under the user profile). Eagerly call HcsGrantVmAccess on those paths while impersonating the user, before the VM is started. The grant is best-effort: it requires WRITE_DAC on the file (typically via ownership), which the impersonated user may lack for VHDs they only have READ access to (e.g. SYSTEM-owned VHDs reachable via inherited folder ACLs). Failures are logged via CATCH_LOG; if VMWP truly cannot read the VHD, StartComputeSystem will still surface a clear E_ACCESSDENIED. Adds two regression tests: - CustomVhdsInUserProfile: VHDs under %TEMP%, exercises the grant path. - CustomVhdsAccessibleViaInheritedAcls: VHDs in the install dir launched as a non-elevated user, exercises the swallowed-grant-failure path. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot committed May 13, 2026 at 14:59 UTC 2afdc87fa1903abd59c91c12c0b62b7cb1b145eb
5 files changed +91 -21
src/windows/service/exe/WslCoreVm.cpp
+23 -4
@@ -221,6 +221,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken
221 }
222
223 // If the user did not specify custom modules, use the default modules only if using the default kernel.
224 + m_privateKernelModules = !m_vmConfig.KernelModulesPath.empty();
225 if (m_vmConfig.KernelModulesPath.empty())
226 {
227 if (m_defaultKernel)
@@ -228,6 +229,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken
229 #ifdef WSL_KERNEL_MODULES_PATH
230
231 m_vmConfig.KernelModulesPath = std::wstring(TEXT(WSL_KERNEL_MODULES_PATH));
232 + m_privateKernelModules = true;
233
234 #else
235
@@ -1690,7 +1692,11 @@ std::wstring WslCoreVm::GenerateConfigJson()
1692
1693 // Initialize SCSI devices.
1694 hcs::Scsi scsiController{};
1693 - auto attachDisk = [&](PCWSTR path) {
1695 +
1696 + // grantVmAccess should be true for user-supplied paths. Best-effort: failures (e.g. no
1697 + // WRITE_DAC on a SYSTEM-owned VHD) are swallowed since VMWP may already have access via
1698 + // inherited ACLs; otherwise StartComputeSystem will surface E_ACCESSDENIED.
1699 + auto attachDisk = [&](PCWSTR path, bool grantVmAccess) {
1700 auto lun = ReserveLun();
1701 hcs::Attachment disk{};
1702 disk.Type = hcs::AttachmentType::VirtualDisk;
@@ -1700,18 +1706,31 @@ std::wstring WslCoreVm::GenerateConfigJson()
1706 disk.AlwaysAllowSparseFiles = true;
1707 disk.SupportEncryptedFiles = true;
1708 scsiController.Attachments[std::to_string(lun)] = std::move(disk);
1703 - m_attachedDisks.emplace(AttachedDisk{DiskType::VHD, path, false}, DiskState{lun, {}, {}});
1709 +
1710 + DiskStateFlags diskFlags{};
1711 + if (grantVmAccess)
1712 + {
1713 + try
1714 + {
1715 + auto runAsUser = wil::impersonate_token(m_userToken.get());
1716 + wsl::windows::common::hcs::GrantVmAccess(m_machineId.c_str(), path);
1717 + WI_SetFlag(diskFlags, DiskStateFlags::AccessGranted);
1718 + }
1719 + CATCH_LOG()
1720 + }
1721 +
1722 + m_attachedDisks.emplace(AttachedDisk{DiskType::VHD, path, false}, DiskState{lun, {}, diskFlags});
1723 return lun;
1724 };
1725
1726 if (m_systemDistroDeviceType == LxMiniInitMountDeviceTypeLun)
1727 {
1709 - m_systemDistroDeviceId = attachDisk(m_vmConfig.SystemDistroPath.c_str());
1728 + m_systemDistroDeviceId = attachDisk(m_vmConfig.SystemDistroPath.c_str(), privateSystemDistro);
1729 }
1730
1731 if (!m_vmConfig.KernelModulesPath.empty())
1732 {
1714 - m_kernelModulesDeviceId = attachDisk(m_vmConfig.KernelModulesPath.c_str());
1733 + m_kernelModulesDeviceId = attachDisk(m_vmConfig.KernelModulesPath.c_str(), m_privateKernelModules);
1734 }
1735
1736 vmSettings.Devices.Scsi["0"] = std::move(scsiController);
src/windows/service/exe/WslCoreVm.h
+1
@@ -288,6 +288,7 @@ private:
288 bool m_tempDirectoryCreated;
289 bool m_enableInboxGpuLibs;
290 bool m_defaultKernel = true;
291 + bool m_privateKernelModules = false;
292 LX_MINI_INIT_MOUNT_DEVICE_TYPE m_systemDistroDeviceType = LxMiniInitMountDeviceTypeInvalid;
293 ULONG m_systemDistroDeviceId = ULONG_MAX;
294 ULONG m_kernelModulesDeviceId = ULONG_MAX;
test/windows/Common.cpp
+5
@@ -1528,6 +1528,11 @@ std::wstring LxssGenerateTestConfig(TestConfigDefaults Default)
1528 L"loadDefaultKernelModules=" + std::wstring(Default.loadDefaultKernelModules.value() ? L"true" : L"false") + L"\n";
1529 }
1530
1531 + if (Default.systemDistro.has_value())
1532 + {
1533 + newConfig += L"systemDistro=" + EscapePath(Default.systemDistro.value()) + L"\n";
1534 + }
1535 +
1536 switch (Default.networkingMode.value_or(wsl::core::NetworkingMode::Nat))
1537 {
1538 case wsl::core::NetworkingMode::Nat:
test/windows/Common.h
+1
@@ -535,6 +535,7 @@ struct TestConfigDefaults
535 std::optional<std::wstring> kernelModules;
536 std::optional<std::wstring> loadKernelModules;
537 std::optional<bool> loadDefaultKernelModules;
538 + std::optional<std::wstring> systemDistro;
539 std::optional<bool> sparse;
540 std::optional<bool> hostAddressLoopback;
541 int crashDumpCount = 100;
test/windows/UnitTests.cpp
+61 -17
@@ -6289,38 +6289,82 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n",
6289 }
6290 }
6291
6292 - WSL2_TEST_METHOD(CustomModulesVhd)
6292 + WSL2_TEST_METHOD(CustomVhdsInUserProfile)
6293 {
6294 + // Regression: HCS fails with E_ACCESSDENIED when user-supplied kernelModules or
6295 + // systemDistro VHDs live under the user profile and VMWP wasn't granted access.
6296 #ifdef WSL_DEV_INSTALL_PATH
6297
6296 - auto modulesPath = std::format(L"{}\\modules.vhd", WSL_DEV_INSTALL_PATH);
6297 - auto kernelPath = std::format(L"{}\\kernel", WSL_DEV_INSTALL_PATH);
6298 + const auto modulesPath = std::format(L"{}\\modules.vhd", WSL_DEV_INSTALL_PATH);
6299 + const auto kernelPath = std::format(L"{}\\kernel", WSL_DEV_INSTALL_PATH);
6300 + const auto systemDistroPath = std::format(L"{}\\system.vhd", WSL_DEV_INSTALL_PATH);
6301
6302 #else
6300 - auto modulesPath = std::format(L"{}\\tools\\modules.vhd", wsl::windows::common::wslutil::GetMsiPackagePath().value());
6301 - auto kernelPath = std::format(L"{}\\tools\\kernel", wsl::windows::common::wslutil::GetMsiPackagePath().value());
6303 + const auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath().value();
6304 + const auto modulesPath = std::format(L"{}\\tools\\modules.vhd", installPath);
6305 + const auto kernelPath = std::format(L"{}\\tools\\kernel", installPath);
6306 + const auto systemDistroPath = std::format(L"{}\\system.vhd", installPath);
6307
6308 #endif
6309
6305 - // Create a copy of the modules vhd
6306 - auto testModules = std::filesystem::current_path() / "test-modules.vhd";
6310 + // Unique folder under %TEMP% so parallel runs don't collide.
6311 + GUID runId;
6312 + THROW_IF_FAILED(CoCreateGuid(&runId));
6313 + const auto testFolder =
6314 + std::filesystem::temp_directory_path() /
6315 + std::format(L"wsl-test-vhd-grant-{}", wsl::shared::string::GuidToString<wchar_t>(runId, wsl::shared::string::GuidToStringFlags::None));
6316 + const auto testModules = testFolder / L"test-modules.vhd";
6317 + const auto testSystemDistro = testFolder / L"test-system.vhd";
6318 +
6319 + // Construct the cleanup scope before any filesystem mutations so a failed copy or
6320 + // VERIFY does not leak the directory across runs.
6321 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
6322 + std::error_code ignored;
6323 + std::filesystem::remove_all(testFolder, ignored);
6324 + });
6325 +
6326 + std::filesystem::create_directories(testFolder);
6327
6328 VERIFY_IS_TRUE(CopyFile(modulesPath.c_str(), testModules.c_str(), false));
6329 + VERIFY_IS_TRUE(CopyFile(systemDistroPath.c_str(), testSystemDistro.c_str(), false));
6330 +
6331 + for (const auto& path : {testModules, testSystemDistro})
6332 + {
6333 + auto cmd = std::format(L"icacls.exe \"{}\" /remove Everyone /Q", path.wstring());
6334 + LxsstuLaunchCommandAndCaptureOutput(cmd.data());
6335 + }
6336 +
6337 + WslConfigChange config{LxssGenerateTestConfig(
6338 + {.kernel = kernelPath, .kernelModules = testModules.wstring(), .systemDistro = testSystemDistro.wstring()})};
6339 +
6340 + auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"echo OK");
6341 + VERIFY_ARE_EQUAL(out, L"OK\n");
6342 + VERIFY_ARE_EQUAL(err, L"");
6343 + }
6344 +
6345 + WSL2_TEST_METHOD(CustomVhdsAccessibleViaInheritedAcls)
6346 + {
6347 + // Regression: VHDs reachable to VMWP via inherited ACLs must boot even when the
6348 + // impersonated user lacks WRITE_DAC for HcsGrantVmAccess.
6349 +#ifdef WSL_DEV_INSTALL_PATH
6350
6310 - auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove(testModules); });
6351 + const auto modulesPath = std::format(L"{}\\modules.vhd", WSL_DEV_INSTALL_PATH);
6352 + const auto kernelPath = std::format(L"{}\\kernel", WSL_DEV_INSTALL_PATH);
6353 + const auto systemDistroPath = std::format(L"{}\\system.vhd", WSL_DEV_INSTALL_PATH);
6354
6312 - auto cmd = std::format(
6313 - LR"($acl = Get-Acl '{}' ; $acl.RemoveAccessRuleAll((New-Object System.Security.AccessControl.FileSystemAccessRule(\"Everyone\", \"Read\", \"None\", \"None\", \"Allow\"))); Set-Acl -Path '{}' -AclObject $acl)",
6314 - testModules,
6315 - testModules);
6355 +#else
6356 + const auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath().value();
6357 + const auto modulesPath = std::format(L"{}\\tools\\modules.vhd", installPath);
6358 + const auto kernelPath = std::format(L"{}\\tools\\kernel", installPath);
6359 + const auto systemDistroPath = std::format(L"{}\\system.vhd", installPath);
6360
6317 - LxsstuLaunchPowershellAndCaptureOutput(cmd);
6361 +#endif
6362
6319 - // Update .wslconfig to point to the copied kernel
6320 - WslConfigChange config{LxssGenerateTestConfig({.kernel = kernelPath, .kernelModules = testModules.wstring()})};
6363 + WslConfigChange config{LxssGenerateTestConfig({.kernel = kernelPath, .kernelModules = modulesPath, .systemDistro = systemDistroPath})};
6364
6322 - // Validate that WSL starts correctly
6323 - auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"echo OK");
6365 + // Non-elevated launch so impersonation cannot WRITE_DAC the SYSTEM-owned VHD.
6366 + const auto nonElevatedToken = GetNonElevatedToken();
6367 + auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"echo OK", 0, nullptr, nonElevatedToken.get());
6368 VERIFY_ARE_EQUAL(out, L"OK\n");
6369 VERIFY_ARE_EQUAL(err, L"");
6370 }