diff --git a/src/linux/init/main.cpp b/src/linux/init/main.cpp index 8cc79c1fd..ae5fa62d0 100644 --- a/src/linux/init/main.cpp +++ b/src/linux/init/main.cpp @@ -121,9 +121,9 @@ std::optional g_EnableSocketLogging; int Chroot(const char* Target); -void CreateSwap(unsigned int Lun); +std::optional FormatScratchDevice(unsigned int Lun); -int CreateTempDirectory(const char* ParentPath, std::string& Path); +void CreateSwap(unsigned int Lun); int DetachScsiDisk(unsigned int Lun); @@ -170,7 +170,8 @@ void LaunchSystemDistro( const char* SharedMemoryRoot, const char* InstallPath, const char* UserProfile, - pid_t DistroInitPid); + pid_t DistroInitPid, + unsigned int ScratchLun); std::map ListDiskPartitions(const std::string& DeviceName, std::optional WaitForIndex = {}); @@ -295,87 +296,96 @@ Return Value: return Fd; } -void CreateSwap(unsigned int Lun) +std::optional FormatScratchDevice(unsigned int Lun) /*++ Routine Description: - This routine sets up a swap area on the specified SCSI device. + This routine formats the scratch vhd used to back the system distro overlay's + read/write layer. + + N.B. This must run after the scratch device LUN is attached, but before the + overlay is created so the overlay can mount the device as its rw layer. Arguments: - Lun - Supplies the LUN number of the SCSI device. + Lun - Supplies the LUN of the scratch device, or UINT_MAX if none. Return Value: - None. + The scratch device path on success, std::nullopt on failure. --*/ +try { + if (Lun == UINT_MAX) + { + return std::nullopt; + } + + const std::string DevicePath = GetLunDevicePath(Lun); + WaitForBlockDevice(DevicePath.c_str()); + // - // Create the swap file asynchronously using the mkswap and swapon utilities in the system distro. + // Format the scratch device with ext4. // - // N.B. This is done because creating the swap file can take some time and - // the swap file does not need to be available immediately. + // N.B. This runs with the system distro as the root filesystem, so mkfs.ext4 + // and the scratch device node are directly reachable. + // + // N.B. The journal is omitted (the data is disposable) and the inode table is + // lazily initialized to keep formatting off the boot critical path. // - UtilCreateChildProcess("CreateSwap", [Lun]() { - std::string DevicePath = GetLunDevicePath(Lun); - - WaitForBlockDevice(DevicePath.c_str()); - - std::string CommandLine = std::format("/usr/sbin/mkswap '{}'", DevicePath); - THROW_LAST_ERROR_IF(UtilExecCommandLine(CommandLine.c_str(), nullptr) < 0); + const std::string CommandLine = std::format("/usr/sbin/mkfs.ext4 -q -m 0 -O ^has_journal -E lazy_itable_init=1 '{}'", DevicePath); + THROW_LAST_ERROR_IF(UtilExecCommandLine(CommandLine.c_str(), nullptr) < 0); - CommandLine = std::format("/usr/sbin/swapon '{}'", DevicePath); - UtilExecCommandLine(CommandLine.c_str(), nullptr); - }); + return DevicePath; +} +catch (...) +{ + LOG_CAUGHT_EXCEPTION(); + return std::nullopt; } -int CreateTempDirectory(const char* ParentPath, std::string& Path) +void CreateSwap(unsigned int Lun) /*++ Routine Description: - This routine creates a unique directory under the specified parent path. + This routine sets up a swap area on the specified SCSI device. Arguments: - ParentPath - Supplies the path of the parent directory. - - Path - Supplies a buffer to receive the path of the child directory that was - created. + Lun - Supplies the LUN number of the SCSI device. Return Value: - 0 on success, -1 on failure. + None. --*/ { - if (ParentPath) - { - Path = ParentPath; - } - // - // Generate a random name for the directory. + // Create the swap file asynchronously using the mkswap and swapon utilities in the system distro. // - // N.B. mkdtemp requires a template string that ends in "XXXXXX". + // N.B. This is done because creating the swap file can take some time and + // the swap file does not need to be available immediately. // - Path += "/wslXXXXXX"; + UtilCreateChildProcess("CreateSwap", [Lun]() { + std::string DevicePath = GetLunDevicePath(Lun); - if (mkdtemp(Path.data()) == NULL) - { - LOG_ERROR("mkdtemp({}) failed {}", Path.c_str(), errno); - return -1; - } + WaitForBlockDevice(DevicePath.c_str()); - return 0; + std::string CommandLine = std::format("/usr/sbin/mkswap '{}'", DevicePath); + THROW_LAST_ERROR_IF(UtilExecCommandLine(CommandLine.c_str(), nullptr) < 0); + + CommandLine = std::format("/usr/sbin/swapon '{}'", DevicePath); + UtilExecCommandLine(CommandLine.c_str(), nullptr); + }); } dev_t GetBlockDeviceNumber(const std::string& BlockDeviceName) @@ -1477,7 +1487,7 @@ try size_t TargetPathLength = strlen(Target); auto AddTemporaryMount = [&](const char* Name, const char* Source, unsigned long MountFlags) { std::string Path; - THROW_LAST_ERROR_IF(CreateTempDirectory(Target, Path) < 0); + THROW_LAST_ERROR_IF(UtilCreateTempDirectory(Target, Path) < 0); THROW_LAST_ERROR_IF(mount(Source, Path.c_str(), nullptr, MountFlags, nullptr) < 0); AddEnvironmentVariable(Name, Path.substr(TargetPathLength).data()); }; @@ -1665,7 +1675,8 @@ void LaunchSystemDistro( const char* SharedMemoryRoot, const char* InstallPath, const char* UserProfile, - pid_t DistroInitPid) + pid_t DistroInitPid, + unsigned int ScratchLun) /*++ @@ -1702,6 +1713,10 @@ Routine Description: DistroInitPid - Supplies the pid of the user distribution's init process. + ScratchLun - Supplies the SCSI LUN of the scratch device used to back the + overlay read/write layer, or UINT_MAX if none. When unavailable, the + overlay falls back to a tmpfs read/write layer. + Return Value: None. This method does not return. @@ -1711,10 +1726,26 @@ Return Value: try { // - // Create a writable layer on top of the read-only vhd. + // Format the scratch device (backed by a vhd) used to back the overlay + // read/write layer so it is disk-backed instead of consuming guest memory. + // On failure, the overlay transparently falls back to tmpfs. // - THROW_LAST_ERROR_IF(UtilMountOverlayFs(Target, SYSTEM_DISTRO_VHD_PATH) < 0); + const std::optional ScratchDevice = FormatScratchDevice(ScratchLun); + + // + // Create a writable layer on top of the read-only vhd. If the scratch-backed + // overlay fails to mount (e.g. the backing vhd is full or the rw layer setup + // fails), fall back to a tmpfs-backed overlay so the system distro can still + // launch. + // + + if (UtilMountOverlayFs(Target, SYSTEM_DISTRO_VHD_PATH, 0, {}, ScratchDevice) < 0) + { + THROW_LAST_ERROR_IF(!ScratchDevice.has_value()); + LOG_ERROR("scratch-backed overlay mount failed, falling back to tmpfs"); + THROW_LAST_ERROR_IF(UtilMountOverlayFs(Target, SYSTEM_DISTRO_VHD_PATH, 0, {}, std::nullopt) < 0); + } // // Launch the init daemon, this method does not return. @@ -1852,7 +1883,7 @@ try std::string MountPoint; if (Flags & LxMiniInitMessageFlagCreateOverlayFs) { - if (CreateTempDirectory(Target, MountPoint) < 0) + if (UtilCreateTempDirectory(Target, MountPoint) < 0) { return -1; } @@ -1940,7 +1971,7 @@ int MountSystemDistro(LX_MINI_INIT_MOUNT_DEVICE_TYPE DeviceType, unsigned int De Routine Description: This routine mounts the system distro as read-only, creates a writable - tmpfs layer using overlayfs, and chroots to the mount point. + overlayfs layer, and chroots to the mount point. Arguments: @@ -2301,7 +2332,8 @@ void ProcessLaunchInitMessage( wsl::shared::string::FromSpan(Buffer, Message->SharedMemoryRootOffset), wsl::shared::string::FromSpan(Buffer, Message->InstallPathOffset), wsl::shared::string::FromSpan(Buffer, Message->UserProfileOffset), - ChildPid); + ChildPid, + Message->ScratchLun); } } diff --git a/src/linux/init/util.cpp b/src/linux/init/util.cpp index 962e426a2..3d58ac75b 100644 --- a/src/linux/init/util.cpp +++ b/src/linux/init/util.cpp @@ -30,6 +30,7 @@ Module Name: #include #include #include +#include #include #include "common.h" #include "wslpath.h" @@ -1838,7 +1839,58 @@ Return Value: return 0; } -int UtilMountOverlayFs(const char* Target, const char* Lower, unsigned long MountFlags, std::optional TimeoutSeconds) +int UtilCreateTempDirectory(const char* ParentPath, std::string& Path) + +/*++ + +Routine Description: + + This routine creates a unique directory under the specified parent path. + +Arguments: + + ParentPath - Supplies the path of the parent directory. + + Path - Supplies a buffer to receive the path of the child directory that was + created. + +Return Value: + + 0 on success, -1 on failure. + +--*/ + +{ + // + // Set the parent path explicitly so the result does not depend on any prior contents of the + // caller's buffer when ParentPath is null. + // + + Path = ParentPath != nullptr ? ParentPath : ""; + + // + // Generate a random name for the directory. + // + // N.B. mkdtemp requires a template string that ends in "XXXXXX". + // + + Path += "/wslXXXXXX"; + + if (mkdtemp(Path.data()) == NULL) + { + LOG_ERROR("mkdtemp({}) failed {}", Path.c_str(), errno); + return -1; + } + + return 0; +} + +int UtilMountOverlayFs( + const char* Target, + const char* Lower, + unsigned long MountFlags, + std::optional TimeoutSeconds, + const std::optional& ScratchDevice) /*++ @@ -1857,6 +1909,9 @@ Routine Description: TimeoutSeconds - Supplies an optional timeout if the mount should be retried. + ScratchDevice - Supplies an optional ext4 scratch block device used to back the + read/write layer. When empty, the read/write layer is backed by a tmpfs. + Return Value: 0 on success, < 0 on failure. @@ -1869,7 +1924,7 @@ try // Set up the state required for overlayfs mount: // // - mount point for read/write overlayfs (this happens last) - // /rw - tmpfs mount for upper and work dirs + // /rw - read/write layer (scratch device or tmpfs) for upper and work dirs // /rw/upper - upper dir // /rw/work - work dir // @@ -1882,14 +1937,38 @@ try auto Path = std::format("{}/rw", Target); // - // Create a tmpfs mount for the read/write layer + // Mount the read/write layer at /rw, backed either by the scratch + // device (disk-backed, reclaimable) or, when no scratch device is available, + // by a tmpfs (pinned guest memory). It is unwound on a later failure so the + // caller is not left with a half-constructed overlay. // - if (UtilMount(nullptr, Path.c_str(), "tmpfs", 0, nullptr) < 0) + const std::string rwPath = Path; + bool rwMounted = false; + auto cleanupRwLayer = wil::scope_exit([&]() { + if (rwMounted) + { + umount2(rwPath.c_str(), MNT_DETACH); + } + }); + + if (ScratchDevice.has_value()) { - return -1; + if (UtilMount(ScratchDevice->c_str(), Path.c_str(), "ext4", MS_NOATIME, nullptr) < 0) + { + return -1; + } + } + else + { + if (UtilMount(nullptr, Path.c_str(), "tmpfs", 0, nullptr) < 0) + { + return -1; + } } + rwMounted = true; + // // Create upper and work directories. // @@ -1908,11 +1987,53 @@ try } MountOptions += std::format("workdir={}", Path); + + // + // The scratch-backed read/write layer is reformatted on every launch and discarded on + // teardown, so the overlay upper dir is disposable. Mount it "volatile" to skip syncs to the + // upper filesystem (supported since overlayfs 5.10), avoiding writeback stalls on a layer + // whose contents never need to survive a crash. overlayfs refuses to reuse a volatile upper + // dir after an unclean shutdown, but that is moot here because the upper dir is always freshly + // created above. A tmpfs read/write layer gains nothing from volatile, so it is only used for + // the scratch-backed layer. + // + // N.B. If the running kernel does not support the volatile option the mount fails with EINVAL; + // retry without it so a disk-backed overlay is still used rather than falling all the way + // back to a tmpfs layer. + // + + if (ScratchDevice.has_value()) + { + const auto VolatileOptions = MountOptions + ",volatile"; + const int VolatileResult = UtilMount(nullptr, Target, "overlay", MountFlags, VolatileOptions.c_str(), TimeoutSeconds); + if (VolatileResult >= 0) + { + cleanupRwLayer.release(); + return 0; + } + + // + // A kernel that does not understand the "volatile" option fails the mount with EINVAL; + // only that warrants retrying without it. Any other failure (e.g. ENOSPC, EBUSY) would + // recur, so surface it to the caller, which falls back to a tmpfs read/write layer. + // + // N.B. UtilMount returns the negated errno on failure. + // + + if (VolatileResult != -EINVAL) + { + return -1; + } + + LOG_ERROR("volatile overlay mount unsupported (errno {}), retrying without volatile", -VolatileResult); + } + if (UtilMount(nullptr, Target, "overlay", MountFlags, MountOptions.c_str(), TimeoutSeconds) < 0) { return -1; } + cleanupRwLayer.release(); return 0; } CATCH_RETURN_ERRNO() diff --git a/src/linux/init/util.h b/src/linux/init/util.h index 8c819ecf3..9260851da 100644 --- a/src/linux/init/util.h +++ b/src/linux/init/util.h @@ -251,11 +251,19 @@ int UtilMkdir(const char* Path, mode_t Mode); int UtilMkdirPath(const char* Path, mode_t Mode, bool SkipLast = false); +// Creates a uniquely-named directory under ParentPath, returning its path in Path. +int UtilCreateTempDirectory(const char* ParentPath, std::string& Path); + int UtilMountFile(const char* Source, const char* Destination); int UtilMount(const char* Source, const char* Target, const char* Type, unsigned long MountFlags, const char* Options, std::optional TimeoutSeconds = {}); -int UtilMountOverlayFs(const char* Target, const char* Lower, unsigned long MountFlags = 0, std::optional TimeoutSeconds = {}); +int UtilMountOverlayFs( + const char* Target, + const char* Lower, + unsigned long MountFlags = 0, + std::optional TimeoutSeconds = {}, + const std::optional& ScratchDevice = {}); int UtilOpenMountNamespace(void); diff --git a/src/shared/inc/lxinitshared.h b/src/shared/inc/lxinitshared.h index 9c9e3fde9..61d5ce1e5 100644 --- a/src/shared/inc/lxinitshared.h +++ b/src/shared/inc/lxinitshared.h @@ -1228,6 +1228,7 @@ typedef struct _LX_MINI_INIT_MESSAGE unsigned int UserProfileOffset; unsigned int Flags; unsigned int ConnectPort; + unsigned int ScratchLun; char Buffer[]; PRETTY_PRINT( @@ -1241,7 +1242,8 @@ typedef struct _LX_MINI_INIT_MESSAGE STRING_FIELD(InstallPathOffset), STRING_FIELD(UserProfileOffset), FIELD(Flags), - FIELD(ConnectPort)); + FIELD(ConnectPort), + FIELD(ScratchLun)); } LX_MINI_INIT_MESSAGE, *PLX_MINI_INIT_MESSAGE; diff --git a/src/windows/service/exe/LxssUserSession.cpp b/src/windows/service/exe/LxssUserSession.cpp index ba22e10e0..81d9b1f07 100644 --- a/src/windows/service/exe/LxssUserSession.cpp +++ b/src/windows/service/exe/LxssUserSession.cpp @@ -2591,6 +2591,16 @@ std::shared_ptr LxssUserSessionImpl::_CreateInstance(_In_op instanceId, configuration, LxMiniInitMessageLaunchInit, m_utilityVm->GetConfig().KernelBootTimeout, defaultUid, clientKey); } + // If instance startup fails after this point, ensure the per-instance overlay + // scratch vhd created in CreateInstance is ejected and deleted (the normal + // terminate path is not reached for an instance that never finishes starting). + auto scratchCleanupOnFailure = wil::scope_exit([&]() { + if (m_utilityVm) + { + m_utilityVm->CleanupInstanceScratch(instanceId); + } + }); + // Log telemetry to determine how long initialization takes. WSL_LOG( "InitializeInstanceBegin", @@ -2646,6 +2656,8 @@ std::shared_ptr LxssUserSessionImpl::_CreateInstance(_In_op cleanupOnFailure.release(); } + scratchCleanupOnFailure.release(); + result = S_OK; } catch (...) @@ -3603,9 +3615,11 @@ bool LxssUserSessionImpl::_TerminateInstanceInternal(_In_ LPCGUID DistroGuid, _I success = (success || force); if (success) { + std::optional scratchInstanceId; if (const auto* wslcoreInstance = dynamic_cast(instance->second.get()); wslcoreInstance != nullptr) { m_pluginManager.OnDistributionStopping(&m_session, wslcoreInstance->DistributionInformation()); + scratchInstanceId = wslcoreInstance->GetInstanceId(); } instance->second->Stop(); @@ -3620,6 +3634,12 @@ bool LxssUserSessionImpl::_TerminateInstanceInternal(_In_ LPCGUID DistroGuid, _I m_runningInstances.erase(instance); + // Eject and delete the per-instance overlay scratch vhd, if one was created. + if (scratchInstanceId.has_value() && m_utilityVm) + { + m_utilityVm->CleanupInstanceScratch(scratchInstanceId.value()); + } + // If the instance that was terminated was a WSL2 instance, // check if the VM is now idle. if (clientId != LXSS_CLIENT_ID_INVALID) diff --git a/src/windows/service/exe/WslCoreInstance.cpp b/src/windows/service/exe/WslCoreInstance.cpp index ba5c0a823..a3261e5f3 100644 --- a/src/windows/service/exe/WslCoreInstance.cpp +++ b/src/windows/service/exe/WslCoreInstance.cpp @@ -329,6 +329,11 @@ GUID WslCoreInstance::GetDistributionId() const return m_configuration.DistroId; } +GUID WslCoreInstance::GetInstanceId() const +{ + return m_instanceId; +} + std::shared_ptr WslCoreInstance::GetInitPort() { THROW_HR_IF(HCS_E_TERMINATED, !m_initChannel); diff --git a/src/windows/service/exe/WslCoreInstance.h b/src/windows/service/exe/WslCoreInstance.h index bf2adc2cc..370a7bb6a 100644 --- a/src/windows/service/exe/WslCoreInstance.h +++ b/src/windows/service/exe/WslCoreInstance.h @@ -92,6 +92,8 @@ class WslCoreInstance : public LxssRunningInstance GUID GetDistributionId() const override; + GUID GetInstanceId() const; + std::shared_ptr GetInitPort() override; std::shared_ptr GetSystemDistro(); diff --git a/src/windows/service/exe/WslCoreVm.cpp b/src/windows/service/exe/WslCoreVm.cpp index b658ce9ab..50bba0fb9 100644 --- a/src/windows/service/exe/WslCoreVm.cpp +++ b/src/windows/service/exe/WslCoreVm.cpp @@ -45,6 +45,10 @@ using namespace std::string_literals; static constexpr size_t c_bootEntropy = 0x1000; static constexpr auto c_localDevicesKey = L"SOFTWARE\\Microsoft\\Terminal Server Client\\LocalDevices"; +// Virtual size of the dynamically-expanding scratch vhd that backs overlay +// read/write layers. The vhd grows on demand, so this is only an upper bound. +static constexpr ULONGLONG c_scratchVhdSizeBytes = 64ull * _1GB; + #define LXSS_ENABLE_GUI_APPS() (m_vmConfig.EnableGuiApps && (m_systemDistroDeviceId != ULONG_MAX)) using namespace wsl::windows::common; @@ -1195,12 +1199,43 @@ std::shared_ptr WslCoreVm::CreateInstance( #endif std::wstring userProfile{}; + ULONG scratchLun = ULONG_MAX; + auto scratchCleanup = wil::scope_exit([&]() { CleanupInstanceScratchLockHeld(InstanceId); }); if (LXSS_ENABLE_GUI_APPS() && (MessageType == LxMiniInitMessageLaunchInit)) { WI_SetFlag(flags, LxMiniInitMessageFlagLaunchSystemDistro); sharedMemoryRoot = m_sharedMemoryRoot; userProfile = m_userProfile; + + // Create and attach a per-instance scratch vhd to back the system distro overlay's + // read/write layer, so heavy writes land on reclaimable disk instead of guest memory. + // + // N.B. The vhd path is derived from the instance id (GetInstanceScratchPath) and is not + // tracked: cleanup on failure here, on a later startup failure (scratchCleanup), and + // on terminate all recompute it. Creation can fail if the target directory is + // compressed, encrypted, or not writable by the user; on any failure the scratch is + // torn down and the guest falls back to a tmpfs overlay. + try + { + const auto scratchPath = GetInstanceScratchPath(InstanceId); + + { + const auto runAsUser = wil::impersonate_token(m_userToken.get()); + wsl::core::filesystem::CreateVhd(scratchPath.c_str(), c_scratchVhdSizeBytes, &m_userSid.Sid, false, false); + } + + scratchLun = AttachDiskLockHeld(scratchPath.c_str(), DiskType::VHD, MountFlags::None, {}, false, m_userToken.get()); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + + // Tear down any partially-created scratch and avoid advertising a torn-down lun; the + // guest falls back to a tmpfs overlay. + scratchLun = ULONG_MAX; + CleanupInstanceScratchLockHeld(InstanceId); + } } WI_SetFlagIf(flags, LxMiniInitMessageFlagExportCompressGzip, WI_IsFlagSet(ExportFlags, LXSS_EXPORT_DISTRO_FLAGS_GZIP)); @@ -1218,11 +1253,15 @@ std::shared_ptr WslCoreVm::CreateInstance( message.WriteString(message->SharedMemoryRootOffset, sharedMemoryRoot); message.WriteString(message->InstallPathOffset, installPath); message.WriteString(message->UserProfileOffset, userProfile); + message->ScratchLun = scratchLun; auto transaction = m_miniInitChannel.StartTransaction(); transaction.Send(message.Span()); - return CreateInstanceInternal( + auto instance = CreateInstanceInternal( InstanceId, Configuration, ReceiveTimeout, DefaultUid, ClientLifetimeId, WI_IsFlagSet(flags, LxMiniInitMessageFlagLaunchSystemDistro), ConnectPort); + + scratchCleanup.release(); + return instance; } std::shared_ptr WslCoreVm::CreateInstanceInternal( @@ -1388,6 +1427,48 @@ void WslCoreVm::EjectVhdLockHeld(_In_ PCWSTR VhdPath) } } +void WslCoreVm::CleanupInstanceScratch(_In_ const GUID& InstanceId) +{ + auto lock = m_lock.lock_exclusive(); + CleanupInstanceScratchLockHeld(InstanceId); +} + +std::filesystem::path WslCoreVm::GetInstanceScratchPath(_In_ const GUID& InstanceId) const +{ + return m_tempPath / std::format(L"scratch-{}.vhdx", wsl::shared::string::GuidToString(InstanceId)); +} + +_Requires_lock_held_(m_lock) +void WslCoreVm::CleanupInstanceScratchLockHeld(_In_ const GUID& InstanceId) +{ + // Eject and delete the per-instance overlay scratch vhd. The path is derived from the + // instance id, so this is idempotent and safe to call on any failure path and on terminate: + // ejecting a device that was never attached is a no-op, and deleting a file that was never + // created is ignored. + // + // N.B. The scratch device is only ever mounted inside the per-instance overlay mount + // namespace, which is destroyed when the instance terminates, so by this point no + // mount pins the device. Any leftover is also reclaimed when the per-VM temp directory + // is deleted on VM teardown. + const auto scratchPath = GetInstanceScratchPath(InstanceId); + + try + { + EjectVhdLockHeld(scratchPath.c_str()); + } + CATCH_LOG() + + try + { + const auto runAsUser = wil::impersonate_token(m_userToken.get()); + if (!DeleteFileW(scratchPath.c_str()) && (GetLastError() != ERROR_FILE_NOT_FOUND)) + { + LOG_LAST_ERROR(); + } + } + CATCH_LOG() +} + _Requires_lock_held_(m_guestDeviceLock) std::optional WslCoreVm::FindVirtioFsShare(_In_ PCWSTR tag, _In_ std::optional Admin) const { diff --git a/src/windows/service/exe/WslCoreVm.h b/src/windows/service/exe/WslCoreVm.h index b3bb810b6..1e2cba285 100644 --- a/src/windows/service/exe/WslCoreVm.h +++ b/src/windows/service/exe/WslCoreVm.h @@ -91,6 +91,10 @@ class WslCoreVm void EjectVhd(_In_ PCWSTR VhdPath); + // Ejects and deletes the per-instance overlay scratch vhd (if any) that was + // created for the instance with the specified id. + void CleanupInstanceScratch(_In_ const GUID& InstanceId); + const wsl::core::Config& GetConfig() const noexcept; GUID GetRuntimeId() const; @@ -202,6 +206,14 @@ class WslCoreVm _Requires_lock_held_(m_lock) void EjectVhdLockHeld(_In_ PCWSTR VhdPath); + _Requires_lock_held_(m_lock) + void CleanupInstanceScratchLockHeld(_In_ const GUID& InstanceId); + + // Returns the per-instance overlay scratch vhd path, derived from the instance id. The path + // is deterministic so the scratch vhd does not need to be tracked: every cleanup path + // recomputes it. + std::filesystem::path GetInstanceScratchPath(_In_ const GUID& InstanceId) const; + _Requires_lock_held_(m_guestDeviceLock) std::optional FindVirtioFsShare(_In_ PCWSTR tag, _In_ std::optional Admin = {}) const; diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index f5aa3b1a0..ab444610d 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -21,6 +21,7 @@ Module Name: #include #include #include +#include using namespace WEX::Logging; using namespace WEX::Common; @@ -2474,6 +2475,55 @@ void Trim(std::wstring& string) std::erase_if(string, [](auto c) { return !isalnum(c); }); } +std::wstring GetBlockDeviceInWsl() +{ + // Wait for the disk to be attached + const auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(30); + + bool done = false; + while (true) + { + for (wchar_t name = 'a'; name <= 'z'; name++) + { + std::wstring cmd = L"-u root blockdev --getsize64 /dev/sd"; + cmd += name; + + std::wstring out; + try + { + out = LxsstuLaunchWslAndCaptureOutput(cmd.data()).first; + } + CATCH_LOG() + + Trim(out); + + // Disk size is 20MB, so 20 * 1024 * 1024 bytes + if (out == L"20971520") + { + return std::wstring(L"/dev/sd") + name; + } + } + + if (done) + { + break; + } + + done = std::chrono::steady_clock::now() > timeout; + if (!done) + { + // Wait briefly before rescanning so the helper does not spin launching wsl.exe in a + // tight loop (burning CPU and spawning a burst of subprocesses) while the disk attaches. + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + } + } + + VERIFY_FAIL(L"Failed to find the block device in WSL"); + + // Unreachable. + return {}; +} + ScopedEnvVariable::ScopedEnvVariable(const std::wstring& Name, const std::wstring& Value) : m_name(Name) { VERIFY_IS_TRUE(SetEnvironmentVariable(Name.c_str(), Value.c_str())); diff --git a/test/windows/Common.h b/test/windows/Common.h index ad2706ebf..83234122d 100644 --- a/test/windows/Common.h +++ b/test/windows/Common.h @@ -577,6 +577,11 @@ void TerminateDistribution(LPCWSTR DistributionName = LXSS_DISTRO_NAME_TEST_L); void Trim(std::wstring& string); +// Returns the /dev/sd* node of the 20MB test block device attached to the WSL VM, waiting up +// to 30s for it to appear. The device letter is not stable across runs (it shifts with the +// number of VHDs attached to the VM), so callers must look it up rather than hard-coding it. +std::wstring GetBlockDeviceInWsl(); + inline auto EnableSystemd(const std::string& extraConfig = "") { // enable systemd on the test distro by editing /etc/wsl.conf diff --git a/test/windows/MountTests.cpp b/test/windows/MountTests.cpp index 2a75ac071..22c8c9292 100644 --- a/test/windows/MountTests.cpp +++ b/test/windows/MountTests.cpp @@ -943,49 +943,6 @@ class MountTests VERIFY_ARE_EQUAL(!offline, wsl::windows::common::disk::IsDiskOnline(disk.get())); } - static std::wstring GetBlockDeviceInWsl() - { - // Wait for the disk to be attached - const auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(30); - - bool done = false; - while (true) - { - for (wchar_t name = 'a'; name < 'z'; name++) - { - std::wstring cmd = L"-u root blockdev --getsize64 /dev/sd"; - cmd += name; - - std::wstring out; - try - { - out = LxsstuLaunchWslAndCaptureOutput(cmd.data()).first; - } - CATCH_LOG() - - Trim(out); - - // Disk size is 20MB, so 20 * 1024 * 1024 bytes - if (out == L"20971520") - { - return std::wstring(L"/dev/sd") + name; - } - } - - if (done) - { - break; - } - - done = std::chrono::steady_clock::now() > timeout; - } - - VERIFY_FAIL(L"Failed to find the block device in WSL"); - - // Unreachable. - return {}; - } - static bool IsBlockDevicePresent(const std::wstring& Device) { const auto Cmd = L"test -e " + Device; diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index c8aa0791d..348b75fec 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -6596,7 +6596,12 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n", LxsstuLaunchPowershellAndCaptureOutput(std::format(L"New-Vhd {} -SizeBytes 20MB", testVhd)); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--mount {} --vhd --bare", testVhd)), 0L); - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mkfs.ext4 /dev/sde"), 0L); + + // Locate the bare-mounted disk by its size rather than hard-coding a device node. + // The /dev/sd* letter depends on how many other VHDs are attached to the VM (e.g. + // the per-instance system distro overlay scratch vhd), which shifts the bare disk. + const auto bareDevice = GetBlockDeviceInWsl(); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl((L"-u root mkfs.ext4 " + bareDevice).c_str()), 0L); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unmount"), 0L); auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--import-in-place broken-test-distro {}", testVhd), -1);