Add swap support for WSLC virtual machines (separate ephemeral VHD) (#40375)

* Implement swap VHDX for WSLC virtual machines Create a swap VHD sized to the VM's memory during WSLCVirtualMachine::Initialize(). The VHD is created in the user's temp directory under wslc/, attached to the VM, formatted with mkswap, and activated with swapon. On failure, the VHD is detached and deleted via scope_exit. On success, the file is cleaned up in the destructor after the VM exits. Changes: - Add MemoryMb to WSLCSessionInitSettings IDL and plumb through WSLCSessionManager - Add swap VHD creation/attach/format/enable in WSLCVirtualMachine::Initialize() - Add swap VHD cleanup in WSLCVirtualMachine destructor - Consolidate temp folder under wslc/ (crashes subfolder) - Add SwapConfigured test to verify swap is active in the VM * Use dynamic VHD for swap and make test retry-tolerant - Use dynamic VHD instead of fixed for swap (reduces IO on boot) - Make SwapConfigured test use RetryWithTimeout to handle async swapon Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Ben Hillis committed May 1, 2026 at 16:32 UTC d9f6ff8e7a695297fddf6c76ab0ea573b0ec0246
5 files changed +42
src/windows/service/exe/WSLCSessionManager.cpp
+1
@@ -352,6 +352,7 @@ WSLCSessionInitSettings WSLCSessionManagerImpl::CreateSessionSettings(
352 sessionSettings.FeatureFlags = Settings->FeatureFlags;
353 sessionSettings.RootVhdTypeOverride = Settings->RootVhdTypeOverride;
354 sessionSettings.StorageFlags = Settings->StorageFlags;
355 + sessionSettings.SwapSizeMb = Settings->MemoryMb;
356 return sessionSettings;
357 }
358
src/windows/service/inc/wslc.idl
+1
@@ -674,6 +674,7 @@ typedef struct _WSLCSessionInitSettings
674 LPCWSTR StoragePath;
675 WSLCSessionStorageFlags StorageFlags;
676 ULONGLONG MaximumStorageSizeMb;
677 + ULONG SwapSizeMb;
678 ULONG BootTimeoutMs;
679 WSLCNetworkingMode NetworkingMode;
680 WSLCFeatureFlags FeatureFlags;
src/windows/wslcsession/WSLCSession.cpp
+26
@@ -357,6 +357,25 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID
357 // Mount the device to /root.
358 m_virtualMachine->Mount(diskDevice.c_str(), c_containerdStorage, "ext4", "", 0);
359
360 + // Configure swap on a separate ephemeral VHD.
361 + if (Settings.SwapSizeMb > 0)
362 + {
363 + try
364 + {
365 + m_swapVhdPath = storagePath / "swap.vhdx";
366 + DeleteFileW(m_swapVhdPath.c_str()); // Remove stale swap from prior run
367 + wsl::core::filesystem::CreateVhd(m_swapVhdPath.c_str(), static_cast<ULONGLONG>(Settings.SwapSizeMb) * _1MB, UserSid, false, false);
368 +
369 + auto [_, swapDevice] = m_virtualMachine->AttachDisk(m_swapVhdPath.c_str(), false);
370 +
371 + // Fire-and-forget: mkswap + swapon runs asynchronously since swap is best-effort.
372 + auto cmd = std::format("/usr/sbin/mkswap {0} && /usr/sbin/swapon {0}", swapDevice);
373 + ServiceProcessLauncher launcher("/bin/sh", {"/bin/sh", "-c", cmd});
374 + launcher.Launch(*m_virtualMachine);
375 + }
376 + CATCH_LOG()
377 + }
378 +
379 deleteVhdOnFailure.release();
380 }
381
@@ -2418,6 +2437,13 @@ try
2437 m_containerdProcess.reset();
2438 m_virtualMachine.reset();
2439
2440 + // Delete the ephemeral swap VHD now that the VM is gone.
2441 + if (!m_swapVhdPath.empty())
2442 + {
2443 + LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str()));
2444 + m_swapVhdPath.clear();
2445 + }
2446 +
2447 m_terminated = true;
2448 return S_OK;
2449 }
src/windows/wslcsession/WSLCSession.h
+1
@@ -197,6 +197,7 @@ private:
197 wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset};
198 std::wstring m_displayName;
199 std::filesystem::path m_storageVhdPath;
200 + std::filesystem::path m_swapVhdPath;
201
202 // N.B. m_lock must be acquired before acquiring m_volumesLock, m_containersLock, or m_networksLock.
203 // These locks protect m_volumes / m_containers without requiring an exclusive m_lock.
test/windows/WSLCTests.cpp
+13
@@ -7417,6 +7417,19 @@ class WSLCTests
7417 VERIFY_ARE_EQUAL(result.Output[1], std::format("{}\n", expectedOrder));
7418 }
7419
7420 + WSLC_TEST_METHOD(SwapConfigured)
7421 + {
7422 + // Swap is configured asynchronously (mkswap + swapon runs fire-and-forget), so retry until it's active.
7423 + wsl::shared::retry::RetryWithTimeout<void>(
7424 + [&]() {
7425 + auto result = ExpectCommandResult(m_defaultSession.get(), {"/usr/sbin/swapon", "--show=NAME,SIZE", "--noheadings"}, 0);
7426 +
7427 + THROW_WIN32_IF(ERROR_RETRY, result.Code != 0 || result.Output.size() < 2 || result.Output[1].find("/dev/") == std::string::npos);
7428 + },
7429 + std::chrono::milliseconds{500},
7430 + std::chrono::seconds{30});
7431 + }
7432 +
7433 WSLC_TEST_METHOD(ContainerAutoRemove)
7434 {
7435 // Test that a container with the Rm flag is automatically deleted on Stop().