From 90b47c8ca3cfdacbdbf893958103f020b4453521 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 30 Jun 2026 07:27:11 -0700 Subject: [PATCH 1/9] Add configurable storage path for the default wslc session The default wslc CLI session always created its storage VHD under %LOCALAPPDATA%\wslc\sessions, which is a problem when the system drive is small. Add a `session.storagePath` setting (settings.yaml) that lets users redirect the default session's storage to another location (e.g. a larger data drive). The value must be an absolute path; relative/empty values are rejected and fall back to %LOCALAPPDATA%. Named (custom) sessions are unaffected since they already accept an explicit path. Fixes #40953 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/common/WSLCUserSettings.cpp | 14 ++++ src/windows/common/WSLCUserSettings.h | 2 + .../service/exe/WSLCSessionManager.cpp | 8 ++- .../windows/wslc/WSLCCLISettingsUnitTests.cpp | 66 +++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/windows/common/WSLCUserSettings.cpp b/src/windows/common/WSLCUserSettings.cpp index f05610f79..73573b410 100644 --- a/src/windows/common/WSLCUserSettings.cpp +++ b/src/windows/common/WSLCUserSettings.cpp @@ -47,6 +47,10 @@ static constexpr std::string_view s_DefaultSettingsTemplate = " # Maximum disk image size (e.g. 500GB default: 1TB)\n" " # maxStorageSize: default\n" "\n" + " # Directory where the default session's storage VHD is created. Must be an absolute\n" + " # path (e.g. D:\\wslc default: %LOCALAPPDATA%\\wslc\\sessions)\n" + " # storagePath: default\n" + "\n" " # Default host address that published ports bind to when 'container run -p' is\n" " # used without an explicit address (default: 127.0.0.1)\n" " # defaultBindingAddress: default\n" @@ -147,6 +151,16 @@ namespace details { return value; } + WSLC_VALIDATE_SETTING(SessionStoragePath) + { + if (value.empty() || !std::filesystem::path(MultiByteToWide(value)).is_absolute()) + { + return std::nullopt; + } + + return value; + } + WSLC_VALIDATE_SETTING(CredentialStore) { if (value == "wincred") diff --git a/src/windows/common/WSLCUserSettings.h b/src/windows/common/WSLCUserSettings.h index 2f006b05b..30e3f3f9a 100644 --- a/src/windows/common/WSLCUserSettings.h +++ b/src/windows/common/WSLCUserSettings.h @@ -44,6 +44,7 @@ enum class Setting : size_t CredentialStore, SessionPortRelay, SessionDefaultBindingAddress, + SessionStoragePath, Max }; @@ -99,6 +100,7 @@ namespace details { DEFINE_SETTING_MAPPING(CredentialStore, std::string, CredentialStoreType, CredentialStoreType::WinCred, "credentialStore") DEFINE_SETTING_MAPPING(SessionPortRelay, std::string, PortRelayType, PortRelayType::VirtioNet, "experimental.portRelay") DEFINE_SETTING_MAPPING(SessionDefaultBindingAddress, std::string, std::string, std::string{}, "session.defaultBindingAddress") + DEFINE_SETTING_MAPPING(SessionStoragePath, std::string, std::string, std::string{}, "session.storagePath") #undef DEFINE_SETTING_MAPPING // clang-format on diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index f13ff8269..cbb09947e 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -86,9 +86,13 @@ struct SessionSettings static std::unique_ptr Default(HANDLE UserToken, const std::wstring& ResolvedName) { auto userSettings = LoadUserSettings(UserToken); - auto localAppData = wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken); - auto storagePath = (localAppData / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName).wstring(); + auto configuredStorageBase = userSettings.Get(); + const std::filesystem::path storageBase = + configuredStorageBase.empty() ? wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken) + : std::filesystem::path(wsl::shared::string::MultiByteToWide(configuredStorageBase)); + + auto storagePath = (storageBase / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName).wstring(); return std::unique_ptr( new SessionSettings(std::wstring(ResolvedName), std::move(storagePath), WSLCSessionStorageFlagsNone, userSettings)); diff --git a/test/windows/wslc/WSLCCLISettingsUnitTests.cpp b/test/windows/wslc/WSLCCLISettingsUnitTests.cpp index e316c0d33..cd7ae1d44 100644 --- a/test/windows/wslc/WSLCCLISettingsUnitTests.cpp +++ b/test/windows/wslc/WSLCCLISettingsUnitTests.cpp @@ -524,6 +524,7 @@ class WSLCCLISettingsUnitTests " networkingMode: nat\n" " hostFileShareMode: virtiofs\n" " dnsTunneling: true\n" + " storagePath: C:\\wslc-data\n" "experimental:\n" " portRelay: wslrelay\n" "credentialStore: wincred\n"); @@ -629,6 +630,71 @@ class WSLCCLISettingsUnitTests Loc::WSLCUserSettings_Warning_InvalidValue(L"session.defaultBindingAddress", s.SettingsFilePath().wstring(), 2), s.GetWarnings().front().Message); } + + // ----------------------------------------------------------------------- + // session.storagePath + // ----------------------------------------------------------------------- + + // When unset, the storage path falls back to the empty built-in default (caller uses %LOCALAPPDATA%). + TEST_METHOD(Validation_StoragePath_Absent_UsesEmptyDefault) + { + auto dir = UniqueTempDir(); + WriteFile(dir / L"settings.yaml", "session:\n cpuCount: 4\n"); + + UserSettingsTest s{dir}; + + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size()); + VERIFY_ARE_EQUAL(std::string{}, s.Get()); + } + + // A valid absolute path loads without warnings. + TEST_METHOD(Validation_StoragePath_AbsoluteValue) + { + auto dir = UniqueTempDir(); + WriteFile( + dir / L"settings.yaml", + "session:\n" + " storagePath: D:\\wslc\n"); + + UserSettingsTest s{dir}; + + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size()); + VERIFY_ARE_EQUAL(std::string("D:\\wslc"), s.Get()); + } + + // "default" magic string uses the built-in (empty) default with no warning. + TEST_METHOD(Validation_StoragePath_DefaultString) + { + auto dir = UniqueTempDir(); + WriteFile( + dir / L"settings.yaml", + "session:\n" + " storagePath: default\n"); + + UserSettingsTest s{dir}; + + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size()); + VERIFY_ARE_EQUAL(std::string{}, s.Get()); + } + + // A relative path is rejected: the default is used and an invalid-value warning emitted. + TEST_METHOD(Validation_StoragePath_RelativeValue_UsesDefaultAndWarns) + { + auto dir = UniqueTempDir(); + WriteFile( + dir / L"settings.yaml", + "session:\n" + " storagePath: wslc\\data\n"); + + UserSettingsTest s{dir}; + + VERIFY_ARE_EQUAL(std::string{}, s.Get()); + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size()); + VERIFY_ARE_EQUAL( + Loc::WSLCUserSettings_Warning_InvalidValue(L"session.storagePath", s.SettingsFilePath().wstring(), 2), + s.GetWarnings().front().Message); + VERIFY_ARE_EQUAL(std::wstring(L"session.storagePath"), s.GetWarnings().front().SettingPath); + } }; } // namespace WSLCCLISettingsUnitTests From 065521572eeb0c88fd4899d8b64e4dbfd798dde8 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 30 Jun 2026 07:33:59 -0700 Subject: [PATCH 2/9] Clarify storagePath is a base directory in settings template Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/common/WSLCUserSettings.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/windows/common/WSLCUserSettings.cpp b/src/windows/common/WSLCUserSettings.cpp index 73573b410..61c8db032 100644 --- a/src/windows/common/WSLCUserSettings.cpp +++ b/src/windows/common/WSLCUserSettings.cpp @@ -47,8 +47,8 @@ static constexpr std::string_view s_DefaultSettingsTemplate = " # Maximum disk image size (e.g. 500GB default: 1TB)\n" " # maxStorageSize: default\n" "\n" - " # Directory where the default session's storage VHD is created. Must be an absolute\n" - " # path (e.g. D:\\wslc default: %LOCALAPPDATA%\\wslc\\sessions)\n" + " # Base directory for the default session's storage; the VHD is created under\n" + " # \\wslc\\sessions. Must be an absolute path (e.g. D:\\data default: %LOCALAPPDATA%)\n" " # storagePath: default\n" "\n" " # Default host address that published ports bind to when 'container run -p' is\n" From 9a2b4d3810eb7cbb8273aa94ed222db08040eab1 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 30 Jun 2026 13:47:05 -0700 Subject: [PATCH 3/9] wslc: warn when configured storagePath would orphan existing default-session storage Surfaces a one-time warning on default-session creation when session.storagePath is set and an existing session exists at the default %LOCALAPPDATA% location but not yet at the configured one, so users are not surprised by their previous session/containers/images not being migrated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- localization/strings/en-US/Resources.resw | 4 +++ src/windows/common/WSLCSessionDefaults.h | 1 + .../service/exe/WSLCSessionManager.cpp | 36 ++++++++++++++++--- src/windows/wslcsession/WSLCSession.cpp | 3 +- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index d95f6a646..35ccb310d 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2417,6 +2417,10 @@ For privacy information about this product please visit https://aka.ms/privacy.< Cannot use '{}' as session storage because it is not a directory {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + Existing WSLC session storage was found at '{}'. Because session.storagePath is set, a new session will be created at '{}'; the existing session, containers, and images will not be migrated. To keep using the existing data, remove the session.storagePath setting or move the storage to the new location. + {FixedPlaceholder="{}"}{Locked="session.storagePath"}Command line arguments, file names and string inserts should not be translated + Invalid image tag format: '{}'. Expected format is 'name:tag' {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/src/windows/common/WSLCSessionDefaults.h b/src/windows/common/WSLCSessionDefaults.h index 5d502c6ad..e288d4179 100644 --- a/src/windows/common/WSLCSessionDefaults.h +++ b/src/windows/common/WSLCSessionDefaults.h @@ -20,6 +20,7 @@ namespace wsl::windows::wslc { inline constexpr const wchar_t DefaultSessionName[] = L"wslc-cli"; inline constexpr const wchar_t DefaultAdminSessionName[] = L"wslc-cli-admin"; inline constexpr const wchar_t DefaultStorageSubPath[] = L"wslc\\sessions"; +inline constexpr const wchar_t DefaultStorageVhdName[] = L"storage.vhdx"; inline constexpr uint32_t DefaultBootTimeoutMs = 30000; } // namespace wsl::windows::wslc diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index cbb09947e..4985363d1 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -63,6 +63,8 @@ struct SessionSettings std::wstring StoragePath; WSLCSessionSettings Settings{}; + std::optional OrphanedStoragePath; + NON_COPYABLE(SessionSettings); NON_MOVABLE(SessionSettings); @@ -88,14 +90,30 @@ struct SessionSettings auto userSettings = LoadUserSettings(UserToken); auto configuredStorageBase = userSettings.Get(); + const bool customConfigured = !configuredStorageBase.empty(); + const std::filesystem::path defaultBase = wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken); const std::filesystem::path storageBase = - configuredStorageBase.empty() ? wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken) - : std::filesystem::path(wsl::shared::string::MultiByteToWide(configuredStorageBase)); + customConfigured ? std::filesystem::path(wsl::shared::string::MultiByteToWide(configuredStorageBase)) : defaultBase; + + const auto storageDir = storageBase / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName; + + auto result = std::unique_ptr( + new SessionSettings(std::wstring(ResolvedName), storageDir.wstring(), WSLCSessionStorageFlagsNone, userSettings)); - auto storagePath = (storageBase / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName).wstring(); + if (customConfigured) + { + const auto defaultDir = defaultBase / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName; + auto runAsUser = wil::impersonate_token(UserToken); + std::error_code ec; + const bool defaultHasStorage = std::filesystem::exists(defaultDir / wsl::windows::wslc::DefaultStorageVhdName, ec); + const bool configuredHasStorage = std::filesystem::exists(storageDir / wsl::windows::wslc::DefaultStorageVhdName, ec); + if (defaultHasStorage && !configuredHasStorage) + { + result->OrphanedStoragePath = defaultDir.wstring(); + } + } - return std::unique_ptr( - new SessionSettings(std::wstring(ResolvedName), std::move(storagePath), WSLCSessionStorageFlagsNone, userSettings)); + return result; } // Custom session: caller provides name and storage path. @@ -244,6 +262,14 @@ void WSLCSessionManagerImpl::CreateSession( { defaultSettings = SessionSettings::Default(callerToken.get(), resolvedDisplayName); Settings = &defaultSettings->Settings; + + if (WarningCallback != nullptr && defaultSettings->OrphanedStoragePath) + { + LOG_IF_FAILED( + WarningCallback->OnWarning(wsl::shared::Localization::MessageWslcSessionStorageOrphaned( + defaultSettings->OrphanedStoragePath->c_str(), defaultSettings->StoragePath.c_str()) + .c_str())); + } } std::wstring callerFileName; diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index eefba3048..2a99d9d5e 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -21,6 +21,7 @@ Module Name: #include "ServiceProcessLauncher.h" #include "WindowsCertStore.h" #include "WslCoreFilesystem.h" +#include "WSLCSessionDefaults.h" #include "wslpolicies.h" #include "APICompat.h" @@ -38,7 +39,7 @@ using wsl::windows::service::wslc::WSLCVirtualMachine; constexpr auto c_containerdStorage = "/var/lib/docker"; constexpr auto c_containerdSocket = "/run/containerd/containerd.sock"; constexpr auto c_dockerdReadyLogLine = "API listen on /var/run/docker.sock"; -constexpr auto c_storageVhdFilename = L"storage.vhdx"; +constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName; constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000; constexpr DWORD c_processKillTimeoutMs = 10 * 1000; From 17103b9b7a459083ac2086806e140d6740c77c05 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 30 Jun 2026 14:26:11 -0700 Subject: [PATCH 4/9] wslc: address review nits on storagePath setting Drop unneeded MultiByteToWide in the SessionStoragePath validator (std::filesystem::path constructs from std::string and is_absolute only inspects the root) and clarify the settings template to show the full per-session VHD path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/common/WSLCUserSettings.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/windows/common/WSLCUserSettings.cpp b/src/windows/common/WSLCUserSettings.cpp index 61c8db032..eb37eb9d2 100644 --- a/src/windows/common/WSLCUserSettings.cpp +++ b/src/windows/common/WSLCUserSettings.cpp @@ -47,8 +47,9 @@ static constexpr std::string_view s_DefaultSettingsTemplate = " # Maximum disk image size (e.g. 500GB default: 1TB)\n" " # maxStorageSize: default\n" "\n" - " # Base directory for the default session's storage; the VHD is created under\n" - " # \\wslc\\sessions. Must be an absolute path (e.g. D:\\data default: %LOCALAPPDATA%)\n" + " # Base directory for the default session's storage; the session VHD is created at\n" + " # \\wslc\\sessions\\\\storage.vhdx. Must be an absolute path (e.g. D:\\data default: " + "%LOCALAPPDATA%)\n" " # storagePath: default\n" "\n" " # Default host address that published ports bind to when 'container run -p' is\n" @@ -153,7 +154,7 @@ namespace details { WSLC_VALIDATE_SETTING(SessionStoragePath) { - if (value.empty() || !std::filesystem::path(MultiByteToWide(value)).is_absolute()) + if (value.empty() || !std::filesystem::path(value).is_absolute()) { return std::nullopt; } From c9f8bdad238cd6ec33d770f6b16e6619594e0eb0 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 30 Jun 2026 15:18:21 -0700 Subject: [PATCH 5/9] wslc: fix localization comment for storage-orphaned string validate-localization.py enforces a canonical comment derived from auto-detected tokens; a manual {Locked=...} for a non-argument token (session.storagePath) diverges from the expected comment and fails CI. Use the canonical comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- localization/strings/en-US/Resources.resw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 35ccb310d..3a2242950 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2419,7 +2419,7 @@ For privacy information about this product please visit https://aka.ms/privacy.< Existing WSLC session storage was found at '{}'. Because session.storagePath is set, a new session will be created at '{}'; the existing session, containers, and images will not be migrated. To keep using the existing data, remove the session.storagePath setting or move the storage to the new location. - {FixedPlaceholder="{}"}{Locked="session.storagePath"}Command line arguments, file names and string inserts should not be translated + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated Invalid image tag format: '{}'. Expected format is 'name:tag' From 40f4d5ef739627b13ba64424237a02b50fbb4771 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 30 Jun 2026 15:34:49 -0700 Subject: [PATCH 6/9] wslc: lock session.storagePath key name in orphaned-storage warning Per PR review, the literal setting key should not be translated. Append a Locked token after the canonical comment so the validator canonical substring stays intact. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- localization/strings/en-US/Resources.resw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 3a2242950..0256afefa 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2419,7 +2419,7 @@ For privacy information about this product please visit https://aka.ms/privacy.< Existing WSLC session storage was found at '{}'. Because session.storagePath is set, a new session will be created at '{}'; the existing session, containers, and images will not be migrated. To keep using the existing data, remove the session.storagePath setting or move the storage to the new location. - {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated. {Locked="session.storagePath"}"session.storagePath" is a configuration setting name and should not be translated. Invalid image tag format: '{}'. Expected format is 'name:tag' From a6cf9490f37784f09ac18916d3d740e635df8aa4 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Wed, 1 Jul 2026 07:35:13 -0700 Subject: [PATCH 7/9] wslc: simplify storagePath warning to custom-location notice Per PR feedback (dkbennett, benhillis), drop the default-vs-configured orphan detection. Warn once when creating a session VHD at a non-default storagePath, and document the orphan-on-change behavior in the settings template comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- localization/strings/en-US/Resources.resw | 4 ++-- src/windows/common/WSLCUserSettings.cpp | 3 ++- src/windows/service/exe/WSLCSessionManager.cpp | 18 +++++------------- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 0256afefa..103c316b9 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2417,8 +2417,8 @@ For privacy information about this product please visit https://aka.ms/privacy.< Cannot use '{}' as session storage because it is not a directory {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated - - Existing WSLC session storage was found at '{}'. Because session.storagePath is set, a new session will be created at '{}'; the existing session, containers, and images will not be migrated. To keep using the existing data, remove the session.storagePath setting or move the storage to the new location. + + Creating WSLC session storage at '{}' as configured by session.storagePath. If you change or remove this setting, data stored at this location will no longer be used by the default session. {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated. {Locked="session.storagePath"}"session.storagePath" is a configuration setting name and should not be translated. diff --git a/src/windows/common/WSLCUserSettings.cpp b/src/windows/common/WSLCUserSettings.cpp index eb37eb9d2..b0c3e1e86 100644 --- a/src/windows/common/WSLCUserSettings.cpp +++ b/src/windows/common/WSLCUserSettings.cpp @@ -49,7 +49,8 @@ static constexpr std::string_view s_DefaultSettingsTemplate = "\n" " # Base directory for the default session's storage; the session VHD is created at\n" " # \\wslc\\sessions\\\\storage.vhdx. Must be an absolute path (e.g. D:\\data default: " - "%LOCALAPPDATA%)\n" + "%LOCALAPPDATA%). Changing this after a session already exists does not move existing storage, containers, or\n" + " # images; the previous location is left in place and a new empty session is created at the new path.\n" " # storagePath: default\n" "\n" " # Default host address that published ports bind to when 'container run -p' is\n" diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index 4985363d1..11e101071 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -63,7 +63,7 @@ struct SessionSettings std::wstring StoragePath; WSLCSessionSettings Settings{}; - std::optional OrphanedStoragePath; + bool WarnCustomStorageLocation = false; NON_COPYABLE(SessionSettings); NON_MOVABLE(SessionSettings); @@ -102,15 +102,9 @@ struct SessionSettings if (customConfigured) { - const auto defaultDir = defaultBase / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName; auto runAsUser = wil::impersonate_token(UserToken); std::error_code ec; - const bool defaultHasStorage = std::filesystem::exists(defaultDir / wsl::windows::wslc::DefaultStorageVhdName, ec); - const bool configuredHasStorage = std::filesystem::exists(storageDir / wsl::windows::wslc::DefaultStorageVhdName, ec); - if (defaultHasStorage && !configuredHasStorage) - { - result->OrphanedStoragePath = defaultDir.wstring(); - } + result->WarnCustomStorageLocation = !std::filesystem::exists(storageDir / wsl::windows::wslc::DefaultStorageVhdName, ec); } return result; @@ -263,12 +257,10 @@ void WSLCSessionManagerImpl::CreateSession( defaultSettings = SessionSettings::Default(callerToken.get(), resolvedDisplayName); Settings = &defaultSettings->Settings; - if (WarningCallback != nullptr && defaultSettings->OrphanedStoragePath) + if (WarningCallback != nullptr && defaultSettings->WarnCustomStorageLocation) { - LOG_IF_FAILED( - WarningCallback->OnWarning(wsl::shared::Localization::MessageWslcSessionStorageOrphaned( - defaultSettings->OrphanedStoragePath->c_str(), defaultSettings->StoragePath.c_str()) - .c_str())); + LOG_IF_FAILED(WarningCallback->OnWarning( + wsl::shared::Localization::MessageWslcSessionStorageCustomLocation(defaultSettings->StoragePath.c_str()).c_str())); } } From d8c3eed3fb1800bcf391e8fdae8562a9b358f230 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 6 Jul 2026 08:06:53 -0700 Subject: [PATCH 8/9] wslc: emit storagePath warning from wslcsession at VHD creation Move the custom storagePath notice out of the service's CreateSession, where it was raised via the IWarningCallback COM callback (which can stall the service if the client call hangs), into wslcsession where the VHD is actually created. A new WSLCSessionStorageFlagsWarnCustomLocation flag is set by the service when session.storagePath is configured, and wslcsession emits the warning via EMIT_USER_WARNING only when it creates a new VHD, so the notice fires once at creation. Also reword the message to note the data can be deleted to reclaim space. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- localization/strings/en-US/Resources.resw | 2 +- .../service/exe/WSLCSessionManager.cpp | 23 ++++--------------- src/windows/service/inc/WSLCShared.idl | 3 ++- src/windows/wslcsession/WSLCSession.cpp | 5 ++++ 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 103c316b9..d18c65645 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2418,7 +2418,7 @@ For privacy information about this product please visit https://aka.ms/privacy.< {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated - Creating WSLC session storage at '{}' as configured by session.storagePath. If you change or remove this setting, data stored at this location will no longer be used by the default session. + Creating session storage at '{}' as configured by session.storagePath. If you later change or remove this setting, data stored here will no longer be used by the default session and can be deleted to reclaim space. {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated. {Locked="session.storagePath"}"session.storagePath" is a configuration setting name and should not be translated. diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index 11e101071..b9331ed60 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -63,8 +63,6 @@ struct SessionSettings std::wstring StoragePath; WSLCSessionSettings Settings{}; - bool WarnCustomStorageLocation = false; - NON_COPYABLE(SessionSettings); NON_MOVABLE(SessionSettings); @@ -97,17 +95,12 @@ struct SessionSettings const auto storageDir = storageBase / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName; - auto result = std::unique_ptr( - new SessionSettings(std::wstring(ResolvedName), storageDir.wstring(), WSLCSessionStorageFlagsNone, userSettings)); - - if (customConfigured) - { - auto runAsUser = wil::impersonate_token(UserToken); - std::error_code ec; - result->WarnCustomStorageLocation = !std::filesystem::exists(storageDir / wsl::windows::wslc::DefaultStorageVhdName, ec); - } + // wslcsession emits the custom-location warning when it actually creates the VHD, so the notice + // fires once at creation without a service-side callback that could stall CreateSession. + const auto storageFlags = customConfigured ? WSLCSessionStorageFlagsWarnCustomLocation : WSLCSessionStorageFlagsNone; - return result; + return std::unique_ptr( + new SessionSettings(std::wstring(ResolvedName), storageDir.wstring(), storageFlags, userSettings)); } // Custom session: caller provides name and storage path. @@ -256,12 +249,6 @@ void WSLCSessionManagerImpl::CreateSession( { defaultSettings = SessionSettings::Default(callerToken.get(), resolvedDisplayName); Settings = &defaultSettings->Settings; - - if (WarningCallback != nullptr && defaultSettings->WarnCustomStorageLocation) - { - LOG_IF_FAILED(WarningCallback->OnWarning( - wsl::shared::Localization::MessageWslcSessionStorageCustomLocation(defaultSettings->StoragePath.c_str()).c_str())); - } } std::wstring callerFileName; diff --git a/src/windows/service/inc/WSLCShared.idl b/src/windows/service/inc/WSLCShared.idl index d02849aa0..b25fbc053 100644 --- a/src/windows/service/inc/WSLCShared.idl +++ b/src/windows/service/inc/WSLCShared.idl @@ -165,7 +165,8 @@ typedef enum _WSLCSessionStorageFlags { WSLCSessionStorageFlagsNone = 0, WSLCSessionStorageFlagsNoCreate = 1, // Open an existing storage path, but don't create a new one. - WSLCSessionStorageFlagsValid = WSLCSessionStorageFlagsNoCreate + WSLCSessionStorageFlagsWarnCustomLocation = 2, // Warn when creating a new VHD at a session.storagePath-configured location. + WSLCSessionStorageFlagsValid = WSLCSessionStorageFlagsNoCreate | WSLCSessionStorageFlagsWarnCustomLocation } WSLCSessionStorageFlags; cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCSessionStorageFlags);") diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 2a99d9d5e..a1bdd4f6a 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -505,6 +505,11 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID // If the VHD wasn't found, create it. WSL_LOG("CreateStorageVhd", TraceLoggingValue(m_storageVhdPath.c_str(), "StorageVhdPath")); + if (WI_IsFlagSet(Settings.StorageFlags, WSLCSessionStorageFlagsWarnCustomLocation)) + { + EMIT_USER_WARNING(Localization::MessageWslcSessionStorageCustomLocation(storagePath.c_str())); + } + std::filesystem::create_directories(storagePath); wsl::core::filesystem::CreateVhd(m_storageVhdPath.c_str(), Settings.MaximumStorageSizeMb * _1MB, UserSid, false, false); vhdCreated = true; From 6d67c2c1cc0a17b44f6aed5cddaf1c43667a28ca Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 6 Jul 2026 10:22:31 -0700 Subject: [PATCH 9/9] wslc: fix CI clang-format and storage-flags validation test Collapse the SessionSettings::Default return onto one line to satisfy clang-format (the constructor args shrank when the warning moved to a storage flag), and update CreateSessionValidation to use 0x4 as its invalid storage-flags value now that 0x2 (WSLCSessionStorageFlagsWarnCustomLocation) is a valid flag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/service/exe/WSLCSessionManager.cpp | 3 +-- test/windows/WSLCTests.cpp | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index b9331ed60..e5e0540e9 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -99,8 +99,7 @@ struct SessionSettings // fires once at creation without a service-side callback that could stall CreateSession. const auto storageFlags = customConfigured ? WSLCSessionStorageFlagsWarnCustomLocation : WSLCSessionStorageFlagsNone; - return std::unique_ptr( - new SessionSettings(std::wstring(ResolvedName), storageDir.wstring(), storageFlags, userSettings)); + return std::unique_ptr(new SessionSettings(std::wstring(ResolvedName), storageDir.wstring(), storageFlags, userSettings)); } // Custom session: caller provides name and storage path. diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 8baad20ab..63b293ee4 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -456,7 +456,7 @@ class WSLCTests // Reject invalid storage flags. { auto settings = GetDefaultSessionSettings(L"invalid-storage-flags"); - settings.StorageFlags = static_cast(0x2); + settings.StorageFlags = static_cast(0x4); wil::com_ptr session; VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session), E_INVALIDARG); }