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
}