@samitouri / QOSAMI-WSL / commits / 33f38c03

Reject non-empty directories for new WSLC session storage (#40655)

beena352 committed Jun 22, 2026 at 13:34 UTC 33f38c0347389813c0c8a12c6afa5c41cd02c775
9 files changed +114 -12
localization/strings/en-US/Resources.resw
+8
@@ -2400,6 +2400,14 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2400 <value>No WSLC session found in '{}'</value>
2401 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2402 </data>
2403 + <data name="MessageWslcSessionStorageMustBeEmpty" xml:space="preserve">
2404 + <value>Cannot use '{}' as session storage because the directory is not empty</value>
2405 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2406 + </data>
2407 + <data name="MessageWslcSessionStorageMustBeDirectory" xml:space="preserve">
2408 + <value>Cannot use '{}' as session storage because it is not a directory</value>
2409 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2410 + </data>
2411 <data name="MessageWslcTagImageInvalidFormat" xml:space="preserve">
2412 <value>Invalid image tag format: '{}'. Expected format is 'name:tag'</value>
2413 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/service/exe/WSLCSessionManager.cpp
+16 -1
@@ -284,7 +284,17 @@ void WSLCSessionManagerImpl::CreateSession(
284 const auto sessionSettings = CreateSessionSettings(sessionId, callerFileName.c_str(), Settings, resolvedDisplayName.c_str());
285 wil::com_ptr<IWSLCSession> session;
286 wil::com_ptr<IWSLCSessionReference> serviceRef;
287 - THROW_IF_FAILED(factory->CreateSession(&sessionSettings, vmFactory.Get(), notifier.Get(), WarningCallback, &session, &serviceRef));
287 + const auto factoryHr =
288 + factory->CreateSession(&sessionSettings, vmFactory.Get(), notifier.Get(), WarningCallback, &session, &serviceRef);
289 + if (FAILED(factoryHr))
290 + {
291 + if (auto comError = wslutil::GetCOMErrorInfo(); comError && comError->Message)
292 + {
293 + THROW_HR_WITH_USER_ERROR(factoryHr, comError->Message.get());
294 + }
295 +
296 + THROW_HR(factoryHr);
297 + }
298
299 // Track the session via its service ref, along with metadata and security info.
300 m_sessions.push_back(SessionEntry{
@@ -607,6 +617,11 @@ HRESULT WSLCSessionManager::OpenSessionByName(_In_ LPCWSTR DisplayName, _Out_ IW
617 return CallImpl(&WSLCSessionManagerImpl::OpenSessionByName, DisplayName, Session);
618 }
619
620 +HRESULT WSLCSessionManager::InterfaceSupportsErrorInfo(_In_ REFIID riid)
621 +{
622 + return riid == __uuidof(IWSLCSessionManager) ? S_OK : S_FALSE;
623 +}
624 +
625 HRESULT WSLCSessionManager::GetVersion(_Out_ WSLCCompatVersion* Version)
626 try
627 {
src/windows/service/exe/WSLCSessionManager.h
+4 -1
@@ -188,7 +188,7 @@ private:
188 } // namespace wsl::windows::service::wslc
189
190 class DECLSPEC_UUID("a9b7a1b9-0671-405c-95f1-e0612cb4ce8f") WSLCSessionManager
191 - : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IWSLCSessionManager, IWSLCCompatSessionManager, IFastRundown>,
191 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IWSLCSessionManager, IWSLCCompatSessionManager, IFastRundown, ISupportErrorInfo>,
192 public wsl::windows::service::wslc::COMImplClass<wsl::windows::service::wslc::WSLCSessionManagerImpl>
193 {
194 public:
@@ -205,6 +205,9 @@ public:
205 IFACEMETHOD(OpenSession)(_In_ ULONG Id, _Out_ IWSLCSession** Session) override;
206 IFACEMETHOD(OpenSessionByName)(_In_ LPCWSTR DisplayName, _Out_ IWSLCSession** Session) override;
207
208 + // ISupportErrorInfo: enables IErrorInfo marshaling across COM boundaries.
209 + IFACEMETHOD(InterfaceSupportsErrorInfo)(_In_ REFIID riid) override;
210 +
211 // IWSLCCompatSessionManager.
212 IFACEMETHOD(GetVersion)(_Out_ WSLCCompatVersion* Version) override;
213 IFACEMETHOD(IsClientVersionSupported)(_In_ const WSLCCompatVersion* ClientVersion, _Out_ BOOL* IsSupported) override;
src/windows/wslcsession/WSLCSession.cpp
+31 -4
@@ -38,6 +38,7 @@ using wsl::windows::service::wslc::WSLCVirtualMachine;
38 constexpr auto c_containerdStorage = "/var/lib/docker";
39 constexpr auto c_containerdSocket = "/run/containerd/containerd.sock";
40 constexpr auto c_dockerdReadyLogLine = "API listen on /var/run/docker.sock";
41 +constexpr auto c_storageVhdFilename = L"storage.vhdx";
42 constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000;
43 constexpr DWORD c_processKillTimeoutMs = 10 * 1000;
44
@@ -439,13 +440,14 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID
440 {
441 // If no storage path is specified, use a tmpfs for convenience.
442 m_virtualMachine->Mount("", c_containerdStorage, "tmpfs", "", 0);
443 + m_storageMounted = true;
444 return;
445 }
446
447 std::filesystem::path storagePath{Settings.StoragePath};
448 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Settings.StoragePath), !storagePath.is_absolute());
449
448 - m_storageVhdPath = storagePath / "storage.vhdx";
450 + m_storageVhdPath = storagePath / c_storageVhdFilename;
451
452 std::string diskDevice;
453 std::optional<ULONG> diskLun{};
@@ -474,11 +476,31 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID
476 "Failed to attach vhd: %ls",
477 m_storageVhdPath.c_str());
478
479 + // No existing VHD — this is a new session. Reject if the caller forbade creation.
480 THROW_HR_WITH_USER_ERROR_IF(
481 HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND),
482 Localization::MessageWslcSessionStorageNotFound(Settings.StoragePath),
483 WI_IsFlagSet(Settings.StorageFlags, WSLCSessionStorageFlagsNoCreate));
484
485 + // Reject any non-empty existing path so we don't mix user files with session storage.
486 + // status's error_code distinguishes "doesn't exist yet" (OK, we'll create it) from other I/O errors.
487 + std::error_code ec;
488 + const auto status = std::filesystem::status(storagePath, ec);
489 + if (ec && ec.value() != ERROR_FILE_NOT_FOUND && ec.value() != ERROR_PATH_NOT_FOUND)
490 + {
491 + THROW_IF_WIN32_ERROR_MSG(ec.value(), "status failed for %ls", storagePath.c_str());
492 + }
493 +
494 + if (std::filesystem::exists(status))
495 + {
496 + THROW_HR_WITH_USER_ERROR_IF(
497 + E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeDirectory(storagePath.c_str()), !std::filesystem::is_directory(status));
498 +
499 + const bool empty = std::filesystem::is_empty(storagePath, ec);
500 + THROW_IF_WIN32_ERROR_MSG(ec.value(), "is_empty failed for %ls", storagePath.c_str());
501 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeEmpty(storagePath.c_str()), !empty);
502 + }
503 +
504 // If the VHD wasn't found, create it.
505 WSL_LOG("CreateStorageVhd", TraceLoggingValue(m_storageVhdPath.c_str(), "StorageVhdPath"));
506
@@ -495,6 +517,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID
517
518 // Mount the device to /root.
519 m_virtualMachine->Mount(diskDevice.c_str(), c_containerdStorage, "ext4", "", 0);
520 + m_storageMounted = true;
521
522 // Configure swap on a separate ephemeral VHD.
523 if (Settings.SwapSizeMb > 0)
@@ -2812,11 +2835,15 @@ try
2835 }
2836
2837 // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running.
2815 - try
2838 + if (m_storageMounted)
2839 {
2817 - m_virtualMachine->Unmount(c_containerdStorage);
2840 + try
2841 + {
2842 + m_virtualMachine->Unmount(c_containerdStorage);
2843 + m_storageMounted = false;
2844 + }
2845 + CATCH_LOG();
2846 }
2819 - CATCH_LOG();
2847 }
2848 }
2849
src/windows/wslcsession/WSLCSession.h
+1
@@ -302,6 +302,7 @@ private:
302 std::wstring m_creatorProcessName;
303 std::filesystem::path m_storageVhdPath;
304 std::filesystem::path m_swapVhdPath;
305 + bool m_storageMounted = false;
306
307 // N.B. m_lock must be acquired before acquiring m_containersLock or m_networksLock.
308 // These locks protect m_containers without requiring an exclusive m_lock.
src/windows/wslcsession/WSLCSessionFactory.cpp
+10 -4
@@ -47,10 +47,6 @@ try
47 // Create the session object.
48 auto session = Microsoft::WRL::Make<wslc::WSLCSession>();
49
50 - // Pass the destruction callback directly to the session.
51 - // One session per process, so when it's destroyed, exit.
52 - session->SetDestructionCallback(std::move(m_destructionCallback));
53 -
50 // Initialize the session with the VM factory (VMs are created on demand).
51 RETURN_IF_FAILED(session->Initialize(Settings, VmFactory, PluginNotifier, WarningCallback));
52
@@ -61,6 +57,11 @@ try
57 RETURN_IF_FAILED(session->QueryInterface(IID_PPV_ARGS(Session)));
58 *ServiceRef = serviceRef.Detach();
59
60 + // N.B. The destruction callback must be installed last, after all fallible operations.
61 + // If installed earlier, an unwinding session local would fire the exit callback and race
62 + // with the COM stub marshaling IErrorInfo back to the caller.
63 + session->SetDestructionCallback(std::move(m_destructionCallback));
64 +
65 WSL_LOG(
66 "WSLCSessionFactoryCreatedSession",
67 TraceLoggingLevel(WINEVENT_LEVEL_INFO),
@@ -71,6 +72,11 @@ try
72 }
73 CATCH_RETURN()
74
75 +HRESULT wslc::WSLCSessionFactory::InterfaceSupportsErrorInfo(_In_ REFIID riid)
76 +{
77 + return riid == __uuidof(IWSLCSessionFactory) ? S_OK : S_FALSE;
78 +}
79 +
80 HRESULT wslc::WSLCSessionFactory::GetProcessHandle(_Out_ HANDLE* ProcessHandle)
81 try
82 {
src/windows/wslcsession/WSLCSessionFactory.h
+4 -1
@@ -30,7 +30,7 @@ Abstract:
30 namespace wsl::windows::service::wslc {
31
32 class DECLSPEC_UUID("9FCD2067-9FC6-4EFA-9EB0-698169EBF7D3") WSLCSessionFactory
33 - : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IWSLCSessionFactory, IFastRundown>
33 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IWSLCSessionFactory, IFastRundown, ISupportErrorInfo>
34 {
35 public:
36 NON_COPYABLE(WSLCSessionFactory);
@@ -53,6 +53,9 @@ public:
53
54 IFACEMETHOD(GetProcessHandle)(_Out_ HANDLE* ProcessHandle) override;
55
56 + // ISupportErrorInfo: enables IErrorInfo marshaling across COM boundaries.
57 + IFACEMETHOD(InterfaceSupportsErrorInfo)(_In_ REFIID riid) override;
58 +
59 private:
60 std::function<void()> m_destructionCallback;
61 };
test/windows/WSLCTests.cpp
+38
@@ -461,6 +461,44 @@ class WSLCTests
461 VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session), E_INVALIDARG);
462 }
463
464 + // Reject non-empty storage directory that doesn't contain a session VHD.
465 + {
466 + const auto storagePath = std::filesystem::temp_directory_path() /
467 + std::format(L"wslc-test-storage-{}-{}", GetCurrentProcessId(), GetTickCount64());
468 + std::filesystem::create_directories(storagePath);
469 + auto cleanup = wil::scope_exit([&]() {
470 + std::error_code ignored;
471 + std::filesystem::remove_all(storagePath, ignored);
472 + });
473 +
474 + std::ofstream{storagePath / L"userfile.txt"} << "data";
475 +
476 + auto settings = GetDefaultSessionSettings(L"storage-not-empty");
477 + const auto storagePathString = storagePath.wstring();
478 + settings.StoragePath = storagePathString.c_str();
479 + wil::com_ptr<IWSLCSession> session;
480 + VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session), E_INVALIDARG);
481 + ValidateCOMErrorMessage(std::format(L"Cannot use '{}' as session storage because the directory is not empty", storagePathString));
482 + }
483 +
484 + // Reject storage path that exists but is not a directory.
485 + {
486 + const auto storagePath = std::filesystem::temp_directory_path() /
487 + std::format(L"wslc-test-storage-file-{}-{}", GetCurrentProcessId(), GetTickCount64());
488 + std::ofstream{storagePath} << "data";
489 + auto cleanup = wil::scope_exit([&]() {
490 + std::error_code ignored;
491 + std::filesystem::remove(storagePath, ignored);
492 + });
493 +
494 + auto settings = GetDefaultSessionSettings(L"storage-not-directory");
495 + const auto storagePathString = storagePath.wstring();
496 + settings.StoragePath = storagePathString.c_str();
497 + wil::com_ptr<IWSLCSession> session;
498 + VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session), E_INVALIDARG);
499 + ValidateCOMErrorMessage(std::format(L"Cannot use '{}' as session storage because it is not a directory", storagePathString));
500 + }
501 +
502 // Reject invalid session flags.
503 {
504 auto settings = GetDefaultSessionSettings(L"invalid-session-flags");
test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp
+2 -1
@@ -109,8 +109,9 @@ class WSLCE2ESessionEnterTests
109 WSLC_TEST_METHOD(WSLCE2E_SessionEnter_StoragePathNotFound)
110 {
111 auto result = RunWslc(L"system session enter does-not-exist");
112 + const auto expectedPath = std::filesystem::absolute(L"does-not-exist").wstring();
113 result.Verify({
113 - .Stderr = L"The system cannot find the path specified. \r\nError code: ERROR_PATH_NOT_FOUND\r\n",
114 + .Stderr = std::format(L"No WSLC session found in '{}'\r\nError code: ERROR_PATH_NOT_FOUND\r\n", expectedPath),
115 .ExitCode = 1,
116 });
117 }