@samitouri / QOSAMI-WSL / commits / 9e9ef6f1

Fix edge cases around .vhd support (#13061)

* Fix edge cases around .vhd support * PR feedback Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * pr feedback * remove unneeded scope exit in unit test --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

Ben Hillis committed Oct 21, 2025 at 17:00 UTC 9e9ef6f145f996c880874da81f884e41a8b9a852
8 files changed +142 -55
localization/strings/en-US/Resources.resw
+16 -11
@@ -466,7 +466,7 @@ Arguments for managing Windows Subsystem for Linux:
466 Move the distribution to a new location.
467
468 --set-sparse, -s &lt;true|false&gt;
469 - Set the vhdx of distro to be sparse, allowing disk space to be automatically reclaimed.
469 + Set the VHD of distro to be sparse, allowing disk space to be automatically reclaimed.
470
471 --set-default-user &lt;Username&gt;
472 Set the default user of the distribution.
@@ -546,11 +546,11 @@ Arguments for managing distributions in Windows Subsystem for Linux:
546 Specifies the version to use for the new distribution.
547
548 --vhd
549 - Specifies that the provided file is a .vhdx file, not a tar file.
550 - This operation makes a copy of the .vhdx file at the specified install location.
549 + Specifies that the provided file is a .vhd or .vhdx file, not a tar file.
550 + This operation makes a copy of the VHD file at the specified install location.
551
552 --import-in-place &lt;Distro&gt; &lt;FileName&gt;
553 - Imports the specified .vhdx file as a new distribution.
553 + Imports the specified VHD file as a new distribution.
554 This virtual hard disk must be formatted with the ext4 filesystem type.
555
556 --list, -l [Options]
@@ -626,7 +626,7 @@ Build time: {}</value>
626 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
627 </data>
628 <data name="MessageCustomKernelModulesNotFound" xml:space="preserve">
629 - <value>The custom kernel modules vhd in {} was not found: '{}'.</value>
629 + <value>The custom kernel modules VHD in {} was not found: '{}'.</value>
630 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
631 </data>
632 <data name="MessageCustomSystemDistroError" xml:space="preserve">
@@ -704,8 +704,13 @@ The system may need to be restarted so the changes can take effect.</value>
704 <comment>{Locked="--install "}{Locked="--no-distribution
705 "}Command line arguments, file names and string inserts should not be translated</comment>
706 </data>
707 - <data name="MessageRequiresVhdxFileExtension" xml:space="preserve">
708 - <value>The specified file must have the .vhdx file extension.</value>
707 + <data name="MessageRequiresFileExtension" xml:space="preserve">
708 + <value>The specified file must have the {} file extension.</value>
709 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
710 + </data>
711 + <data name="MessageRequiresFileExtensions" xml:space="preserve">
712 + <value>The specified file must have the {} or {} file extension.</value>
713 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
714 </data>
715 <data name="MessageVmSwitchNotFound" xml:space="preserve">
716 <value>The VmSwitch '{}' was not found. Available switches: {}</value>
@@ -998,7 +1003,7 @@ Falling back to NAT networking.</value>
1003 <value>See Docs</value>
1004 </data>
1005 <data name="MessageVhdInUse" xml:space="preserve">
1001 - <value>The operation could not be completed because the vhdx is currently in use. To force WSL to stop use: wsl.exe --shutdown</value>
1006 + <value>The operation could not be completed because the VHD is currently in use. To force WSL to stop use: wsl.exe --shutdown</value>
1007 <comment>{Locked="--shutdown"}Command line arguments, file names and string inserts should not be translated</comment>
1008 </data>
1009 <data name="MessageInvalidBoolean" xml:space="preserve">
@@ -1006,7 +1011,7 @@ Falling back to NAT networking.</value>
1011 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1012 </data>
1013 <data name="MessageSparseVhdWsl2Only" xml:space="preserve">
1009 - <value>Sparse vhdx is supported on WSL2 only.</value>
1014 + <value>Sparse VHD is supported on WSL2 only.</value>
1015 </data>
1016 <data name="MessageLocalSystemNotSupported" xml:space="preserve">
1017 <value>Running WSL as local system is not supported.</value>
@@ -1055,7 +1060,7 @@ Falling back to NAT networking.</value>
1060 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
1061 </data>
1062 <data name="MessagePassVhdFlag" xml:space="preserve">
1058 - <value>This looks like a VHDX file. Use --vhd to import a VHDX instead of a tar.</value>
1063 + <value>This looks like a VHD file. Use --vhd to import a VHD instead of a tar.</value>
1064 <comment>{Locked="--vhd "}Command line arguments, file names and string inserts should not be translated</comment>
1065 </data>
1066 <data name="MessageDistroStoreInstallFailed" xml:space="preserve">
@@ -1107,7 +1112,7 @@ Error code: {}</value>
1112 </data>
1113 <data name="MessageSparseVhdDisabled" xml:space="preserve">
1114 <value>Sparse VHD support is currently disabled due to potential data corruption.
1110 -To force a distribution to use a sparse vhd, please run:
1115 +To force a distribution to use a sparse VHD, please run:
1116 wsl.exe --manage &lt;DistributionName&gt; --set-sparse true --allow-unsafe</value>
1117 <comment>{Locked="--manage "}{Locked="--set-sparse "}{Locked="--allow-unsafe"}Command line arguments, file names and string inserts should not be translated</comment>
1118 </data>
src/windows/common/WslClient.cpp
+3 -23
@@ -275,14 +275,6 @@ int ExportDistribution(_In_ std::wstring_view commandLine)
275 }
276 else
277 {
278 - // If exporting to a vhd, ensure the filename ends with the vhdx file extension.
279 - if (WI_IsFlagSet(flags, LXSS_EXPORT_DISTRO_FLAGS_VHD) &&
280 - !wsl::windows::common::string::IsPathComponentEqual(filePath.extension().native(), wsl::windows::common::wslutil::c_vhdxFileExtension))
281 - {
282 - wsl::windows::common::wslutil::PrintMessage(wsl::shared::Localization::MessageRequiresVhdxFileExtension());
283 - return -1;
284 - }
285 -
278 file.reset(CreateFileW(
279 filePath.c_str(), GENERIC_WRITE, (FILE_SHARE_READ | FILE_SHARE_DELETE), nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
280
@@ -371,22 +363,10 @@ int ImportDistribution(_In_ std::wstring_view commandLine)
363 }
364 else
365 {
374 - bool isVhd = wsl::windows::common::string::IsPathComponentEqual(
375 - filePath.extension().native(), wsl::windows::common::wslutil::c_vhdxFileExtension);
376 -
377 - if (WI_IsFlagSet(flags, LXSS_IMPORT_DISTRO_FLAGS_VHD))
378 - {
379 - // If importing from a vhd, ensure the filename ends with the vhdx file extension.
380 - if (!isVhd)
381 - {
382 - wsl::windows::common::wslutil::PrintMessage(wsl::shared::Localization::MessageRequiresVhdxFileExtension());
383 - return -1;
384 - }
385 - }
386 - else
366 + if (WI_IsFlagClear(flags, LXSS_IMPORT_DISTRO_FLAGS_VHD))
367 {
388 - // Fail if we expect a tar, but the file name has the .vhdx extension.
389 - if (isVhd)
368 + // Fail if expecting a tar, but the file name has the .vhd or .vhdx extension.
369 + if (wsl::windows::common::wslutil::IsVhdFile(filePath))
370 {
371 wsl::windows::common::wslutil::PrintMessage(wsl::shared::Localization::MessagePassVhdFlag());
372 return -1;
src/windows/common/wslutil.cpp
+7
@@ -1155,6 +1155,13 @@ bool wsl::windows::common::wslutil::IsRunningInMsix()
1155 return false;
1156 }
1157 }
1158 +
1159 +bool wsl::windows::common::wslutil::IsVhdFile(_In_ const std::filesystem::path& path)
1160 +{
1161 + return wsl::windows::common::string::IsPathComponentEqual(path.extension().native(), c_vhdFileExtension) ||
1162 + wsl::windows::common::string::IsPathComponentEqual(path.extension().native(), c_vhdxFileExtension);
1163 +}
1164 +
1165 std::vector<DWORD> wsl::windows::common::wslutil::ListRunningProcesses()
1166 {
1167 std::vector<DWORD> pids(1024);
src/windows/common/wslutil.h
+2
@@ -122,6 +122,8 @@ void InitializeWil();
122
123 bool IsRunningInMsix();
124
125 +bool IsVhdFile(_In_ const std::filesystem::path& path);
126 +
127 bool IsVirtualMachinePlatformInstalled();
128
129 std::vector<DWORD> ListRunningProcesses();
src/windows/service/exe/LxssUserSession.cpp
+39 -9
@@ -927,7 +927,7 @@ HRESULT LxssUserSessionImpl::MoveDistribution(_In_ LPCGUID DistroGuid, _In_ LPCW
927 std::filesystem::path newVhdPath = Location;
928 RETURN_HR_IF(E_INVALIDARG, newVhdPath.empty());
929
930 - newVhdPath /= LXSS_VM_MODE_VHD_NAME;
930 + newVhdPath /= distro.VhdFilePath.filename();
931
932 auto impersonate = wil::CoImpersonateClient();
933
@@ -952,7 +952,7 @@ HRESULT LxssUserSessionImpl::MoveDistribution(_In_ LPCGUID DistroGuid, _In_ LPCW
952
953 // Update the registry location
954 registration.Write(Property::BasePath, Location);
955 - registration.Write(Property::VhdFileName, LXSS_VM_MODE_VHD_NAME);
955 + registration.Write(Property::VhdFileName, newVhdPath.filename().c_str());
956
957 revert.release();
958
@@ -1079,6 +1079,22 @@ HRESULT LxssUserSessionImpl::ExportDistribution(_In_opt_ LPCGUID DistroGuid, _In
1079 {
1080 const wil::unique_handle userToken = wsl::windows::common::security::GetUserToken(TokenImpersonation);
1081 auto runAsUser = wil::impersonate_token(userToken.get());
1082 +
1083 + // Ensure the target file has the correct file extension.
1084 + if (GetFileType(FileHandle) == FILE_TYPE_DISK)
1085 + {
1086 + std::wstring exportPath;
1087 + THROW_IF_FAILED(wil::GetFinalPathNameByHandleW(FileHandle, exportPath));
1088 +
1089 + const auto sourceFileExtension = configuration.VhdFilePath.extension().native();
1090 + const auto targetFileExtension = std::filesystem::path(exportPath).extension().native();
1091 + if (!wsl::windows::common::string::IsPathComponentEqual(sourceFileExtension, targetFileExtension))
1092 + {
1093 + THROW_HR_WITH_USER_ERROR(
1094 + WSL_E_EXPORT_FAILED, wsl::shared::Localization::MessageRequiresFileExtension(sourceFileExtension.c_str()));
1095 + }
1096 + }
1097 +
1098 const wil::unique_hfile vhdFile(CreateFileW(
1099 configuration.VhdFilePath.c_str(), GENERIC_READ, (FILE_SHARE_READ | FILE_SHARE_DELETE), nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
1100
@@ -1258,13 +1274,9 @@ LxssUserSessionImpl::ImportDistributionInplace(_In_ LPCWSTR DistributionName, _I
1274
1275 s_ValidateDistroName(DistributionName);
1276
1261 - // Return an error if the path is not absolute or does not end in the .vhdx file extension.
1277 + // Return an error if the path is not absolute or does not have a valid VHD file extension.
1278 const std::filesystem::path path{VhdPath};
1263 - RETURN_HR_IF(
1264 - E_INVALIDARG,
1265 - !path.is_absolute() ||
1266 - (!wsl::windows::common::string::IsPathComponentEqual(path.extension().native(), wsl::windows::common::wslutil::c_vhdFileExtension) &&
1267 - !wsl::windows::common::string::IsPathComponentEqual(path.extension().c_str(), wsl::windows::common::wslutil::c_vhdxFileExtension)));
1279 + RETURN_HR_IF(E_INVALIDARG, !path.is_absolute() || !wsl::windows::common::wslutil::IsVhdFile(path));
1280
1281 const wil::unique_hkey lxssKey = s_OpenLxssUserKey();
1282 std::lock_guard lock(m_instanceLock);
@@ -1448,6 +1460,24 @@ HRESULT LxssUserSessionImpl::RegisterDistribution(
1460 wil::CreateDirectoryDeep(distributionPath.c_str());
1461 }
1462
1463 + // If importing a vhd, determine if it is a .vhd or .vhdx.
1464 + std::wstring vhdName{LXSS_VM_MODE_VHD_NAME};
1465 + if ((WI_IsFlagSet(Flags, LXSS_IMPORT_DISTRO_FLAGS_VHD)) && (GetFileType(FileHandle) == FILE_TYPE_DISK))
1466 + {
1467 + std::wstring pathBuffer;
1468 + THROW_IF_FAILED(wil::GetFinalPathNameByHandleW(FileHandle, pathBuffer));
1469 +
1470 + std::filesystem::path vhdPath{std::move(pathBuffer)};
1471 + if (!wsl::windows::common::wslutil::IsVhdFile(vhdPath))
1472 + {
1473 + using namespace wsl::windows::common::wslutil;
1474 + THROW_HR_WITH_USER_ERROR(
1475 + WSL_E_IMPORT_FAILED, wsl::shared::Localization::MessageRequiresFileExtensions(c_vhdFileExtension, c_vhdxFileExtension));
1476 + }
1477 +
1478 + vhdName = vhdPath.filename();
1479 + }
1480 +
1481 registration = DistributionRegistration::Create(
1482 lxssKey.get(),
1483 DistributionId,
@@ -1457,7 +1487,7 @@ HRESULT LxssUserSessionImpl::RegisterDistribution(
1487 flags,
1488 LX_UID_ROOT,
1489 PackageFamilyName,
1460 - LXSS_VM_MODE_VHD_NAME,
1490 + vhdName.c_str(),
1491 WI_IsFlagClear(Flags, LXSS_IMPORT_DISTRO_FLAGS_NO_OOBE));
1492
1493 configuration = s_GetDistributionConfiguration(registration, DistributionName == nullptr);
src/windows/service/exe/WslCoreFilesystem.cpp
+1 -3
@@ -71,9 +71,7 @@ void wsl::core::filesystem::CreateVhd(_In_ LPCWSTR target, _In_ ULONGLONG maximu
71
72 wil::unique_handle wsl::core::filesystem::OpenVhd(_In_ LPCWSTR Path, _In_ VIRTUAL_DISK_ACCESS_MASK Mask)
73 {
74 - WI_ASSERT(
75 - wsl::shared::string::IsEqual(std::filesystem::path{Path}.extension().c_str(), windows::common::wslutil::c_vhdFileExtension, true) ||
76 - wsl::shared::string::IsEqual(std::filesystem::path{Path}.extension().c_str(), windows::common::wslutil::c_vhdxFileExtension, true));
74 + WI_ASSERT(wsl::windows::common::wslutil::IsVhdFile(std::filesystem::path{Path}));
75
76 // N.B. Specifying unknown for device and vendor means the system will determine the type of VHD.
77 VIRTUAL_STORAGE_TYPE storageType{};
test/windows/SimpleTests.cpp
+2 -2
@@ -109,7 +109,7 @@ class SimpleTests
109 std::format(L"{} {} {} {}", WSL_IMPORT_ARG, tempDistro, vhdDir.wstring(), tar.wstring()).c_str(),
110 L"The operation completed successfully. \r\n",
111 L"wsl: Sparse VHD support is currently disabled due to potential data corruption.\r\n"
112 - L"To force a distribution to use a sparse vhd, please run:\r\n"
112 + L"To force a distribution to use a sparse VHD, please run:\r\n"
113 L"wsl.exe --manage <DistributionName> --set-sparse true --allow-unsafe\r\n",
114 0);
115
@@ -122,7 +122,7 @@ class SimpleTests
122 ValidateOutput(
123 std::format(L"{} {} {} {}", WSL_MANAGE_ARG, tempDistro, WSL_MANAGE_ARG_SET_SPARSE_OPTION_LONG, L"true").c_str(),
124 L"Sparse VHD support is currently disabled due to potential data corruption.\r\n"
125 - L"To force a distribution to use a sparse vhd, please run:\r\n"
125 + L"To force a distribution to use a sparse VHD, please run:\r\n"
126 L"wsl.exe --manage <DistributionName> --set-sparse true --allow-unsafe\r\nError code: Wsl/Service/E_INVALIDARG\r\n",
127 L"",
128 -1);
test/windows/UnitTests.cpp
+72 -7
@@ -1016,7 +1016,7 @@ class UnitTests
1016 L"Error code: Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS\r\n");
1017
1018 commandLine = std::format(L"--import dummy {} {} --version {}", LXSST_IMPORT_DISTRO_TEST_DIR, vhdFileName, version);
1019 - validateOutput(commandLine.c_str(), L"This looks like a VHDX file. Use --vhd to import a VHDX instead of a tar.\r\n");
1019 + validateOutput(commandLine.c_str(), L"This looks like a VHD file. Use --vhd to import a VHD instead of a tar.\r\n");
1020
1021 if (!LxsstuVmMode())
1022 {
@@ -1557,7 +1557,7 @@ Arguments for managing Windows Subsystem for Linux:
1557 Move the distribution to a new location.
1558
1559 --set-sparse, -s <true|false>
1560 - Set the vhdx of distro to be sparse, allowing disk space to be automatically reclaimed.
1560 + Set the VHD of distro to be sparse, allowing disk space to be automatically reclaimed.
1561
1562 --set-default-user <Username>
1563 Set the default user of the distribution.
@@ -1637,11 +1637,11 @@ Arguments for managing distributions in Windows Subsystem for Linux:
1637 Specifies the version to use for the new distribution.
1638
1639 --vhd
1640 - Specifies that the provided file is a .vhdx file, not a tar file.
1641 - This operation makes a copy of the .vhdx file at the specified install location.
1640 + Specifies that the provided file is a .vhd or .vhdx file, not a tar file.
1641 + This operation makes a copy of the VHD file at the specified install location.
1642
1643 --import-in-place <Distro> <FileName>
1644 - Imports the specified .vhdx file as a new distribution.
1644 + Imports the specified VHD file as a new distribution.
1645 This virtual hard disk must be formatted with the ext4 filesystem type.
1646
1647 --list, -l [Options]
@@ -2896,8 +2896,7 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND
2896 WslKeepAlive keepAlive;
2897 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--manage test_distro --resize 1500GB", -1);
2898 VERIFY_ARE_EQUAL(
2899 - L"The operation could not be completed because the vhdx is currently in use. To force WSL to stop use: "
2900 - L"wsl.exe "
2899 + L"The operation could not be completed because the VHD is currently in use. To force WSL to stop use: wsl.exe "
2900 L"--shutdown\r\nError code: Wsl/Service/WSL_E_DISTRO_NOT_STOPPED\r\n",
2901 out);
2902 }
@@ -6276,5 +6275,71 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n",
6275 VERIFY_ARE_EQUAL(out, L"755\n");
6276 }
6277
6278 + TEST_METHOD(ExportImportVhd)
6279 + {
6280 + WSL2_TEST_ONLY();
6281 +
6282 + WslShutdown();
6283 +
6284 + constexpr auto vhdPath = L"exported-test-distro.vhd";
6285 + constexpr auto vhdxPath = L"exported-test-distro.vhdx";
6286 + constexpr auto exportedVhdPath = L"exported-vhd.vhd";
6287 + constexpr auto newDistroName = L"imported-test-distro";
6288 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
6289 + LOG_IF_WIN32_BOOL_FALSE(DeleteFile(vhdPath));
6290 + LOG_IF_WIN32_BOOL_FALSE(DeleteFile(vhdxPath));
6291 + LOG_IF_WIN32_BOOL_FALSE(DeleteFile(exportedVhdPath));
6292 + LxsstuLaunchWsl(std::format(L"--unregister {}", newDistroName));
6293 + });
6294 +
6295 + // Attempt to export the distribution to a .vhd (should fail).
6296 + auto [out, err] =
6297 + LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", LXSS_DISTRO_NAME_TEST_L, vhdPath), -1);
6298 + VERIFY_ARE_EQUAL(
6299 + out, L"The specified file must have the .vhdx file extension.\r\nError code: Wsl/Service/WSL_E_EXPORT_FAILED\r\n");
6300 + VERIFY_ARE_EQUAL(err, L"");
6301 +
6302 + // Export the distribution to a .vhdx.
6303 + std::tie(out, err) =
6304 + LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", LXSS_DISTRO_NAME_TEST_L, vhdxPath));
6305 + VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
6306 + VERIFY_ARE_EQUAL(err, L"");
6307 +
6308 + // Convert the .vhdx to .vhd.
6309 + LxsstuLaunchPowershellAndCaptureOutput(std::format(L"Convert-VHD -Path '{}' -DestinationPath '{}'", vhdxPath, vhdPath));
6310 +
6311 + // Import a new distribution from the .vhd file.
6312 + std::tie(out, err) =
6313 + LxsstuLaunchWslAndCaptureOutput(std::format(L"--import {} {} {} --vhd", newDistroName, newDistroName, vhdPath));
6314 + VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
6315 + VERIFY_ARE_EQUAL(err, L"");
6316 +
6317 + // Export the newly imported distribution to another .vhd file.
6318 + std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", newDistroName, exportedVhdPath));
6319 + VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
6320 + VERIFY_ARE_EQUAL(err, L"");
6321 +
6322 + // Attempt to export to a .vhdx (should fail).
6323 + std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", newDistroName, vhdxPath), -1);
6324 + VERIFY_ARE_EQUAL(
6325 + out, L"The specified file must have the .vhd file extension.\r\nError code: Wsl/Service/WSL_E_EXPORT_FAILED\r\n");
6326 + VERIFY_ARE_EQUAL(err, L"");
6327 +
6328 + // Attempt to import to a non VHD file.
6329 + auto tempFile = wsl::windows::common::filesystem::TempFile(
6330 + GENERIC_ALL, 0, CREATE_ALWAYS, wsl::windows::common::filesystem::TempFileFlags::None, L"txt");
6331 +
6332 + tempFile.Handle.reset();
6333 +
6334 + constexpr auto negativeVariationDistro = L"negative-variation-distro";
6335 + std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(
6336 + std::format(L"--import {} {} {} --vhd", negativeVariationDistro, negativeVariationDistro, tempFile.Path), -1);
6337 + VERIFY_ARE_EQUAL(
6338 + out,
6339 + L"The specified file must have the .vhd or .vhdx file extension.\r\nError code: "
6340 + L"Wsl/Service/RegisterDistro/WSL_E_IMPORT_FAILED\r\n");
6341 + VERIFY_ARE_EQUAL(err, L"");
6342 + }
6343 +
6344 }; // namespace UnitTests
6345 } // namespace UnitTests