@samitouri / QOSAMI-WSL / commits / f9655343

Add configurable storage path for the default wslc session (#40956)

* 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> * Clarify storagePath is a base directory in settings template Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * 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> * 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> * 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> * 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> * 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> * 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> * 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> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Ben Hillis committed Jul 7, 2026 at 08:34 UTC f9655343ad377a7778a8d22f7dd6ff219e74639e
9 files changed +111 -7
localization/strings/en-US/Resources.resw
+4
@@ -2421,6 +2421,10 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2421 <value>Cannot use '{}' as session storage because it is not a directory</value>
2422 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2423 </data>
2424 + <data name="MessageWslcSessionStorageCustomLocation" xml:space="preserve">
2425 + <value>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.</value>
2426 + <comment>{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.</comment>
2427 + </data>
2428 <data name="MessageWslcTagImageInvalidFormat" xml:space="preserve">
2429 <value>Invalid image tag format: '{}'. Expected format is 'name:tag'</value>
2430 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/common/WSLCSessionDefaults.h
+1
@@ -20,6 +20,7 @@ namespace wsl::windows::wslc {
20 inline constexpr const wchar_t DefaultSessionName[] = L"wslc-cli";
21 inline constexpr const wchar_t DefaultAdminSessionName[] = L"wslc-cli-admin";
22 inline constexpr const wchar_t DefaultStorageSubPath[] = L"wslc\\sessions";
23 +inline constexpr const wchar_t DefaultStorageVhdName[] = L"storage.vhdx";
24 inline constexpr uint32_t DefaultBootTimeoutMs = 30000;
25
26 } // namespace wsl::windows::wslc
src/windows/common/WSLCUserSettings.cpp
+16
@@ -47,6 +47,12 @@ static constexpr std::string_view s_DefaultSettingsTemplate =
47 " # Maximum disk image size (e.g. 500GB default: 1TB)\n"
48 " # maxStorageSize: default\n"
49 "\n"
50 + " # Base directory for the default session's storage; the session VHD is created at\n"
51 + " # <storagePath>\\wslc\\sessions\\<session>\\storage.vhdx. Must be an absolute path (e.g. D:\\data default: "
52 + "%LOCALAPPDATA%). Changing this after a session already exists does not move existing storage, containers, or\n"
53 + " # images; the previous location is left in place and a new empty session is created at the new path.\n"
54 + " # storagePath: default\n"
55 + "\n"
56 " # Default host address that published ports bind to when 'container run -p' is\n"
57 " # used without an explicit address (default: 127.0.0.1)\n"
58 " # defaultBindingAddress: default\n"
@@ -147,6 +153,16 @@ namespace details {
153 return value;
154 }
155
156 + WSLC_VALIDATE_SETTING(SessionStoragePath)
157 + {
158 + if (value.empty() || !std::filesystem::path(value).is_absolute())
159 + {
160 + return std::nullopt;
161 + }
162 +
163 + return value;
164 + }
165 +
166 WSLC_VALIDATE_SETTING(CredentialStore)
167 {
168 if (value == "wincred")
src/windows/common/WSLCUserSettings.h
+2
@@ -44,6 +44,7 @@ enum class Setting : size_t
44 CredentialStore,
45 SessionPortRelay,
46 SessionDefaultBindingAddress,
47 + SessionStoragePath,
48
49 Max
50 };
@@ -99,6 +100,7 @@ namespace details {
100 DEFINE_SETTING_MAPPING(CredentialStore, std::string, CredentialStoreType, CredentialStoreType::WinCred, "credentialStore")
101 DEFINE_SETTING_MAPPING(SessionPortRelay, std::string, PortRelayType, PortRelayType::VirtioNet, "experimental.portRelay")
102 DEFINE_SETTING_MAPPING(SessionDefaultBindingAddress, std::string, std::string, std::string{}, "session.defaultBindingAddress")
103 + DEFINE_SETTING_MAPPING(SessionStoragePath, std::string, std::string, std::string{}, "session.storagePath")
104
105 #undef DEFINE_SETTING_MAPPING
106 // clang-format on
src/windows/service/exe/WSLCSessionManager.cpp
+12 -4
@@ -86,12 +86,20 @@ struct SessionSettings
86 static std::unique_ptr<SessionSettings> Default(HANDLE UserToken, const std::wstring& ResolvedName)
87 {
88 auto userSettings = LoadUserSettings(UserToken);
89 - auto localAppData = wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken);
89
91 - auto storagePath = (localAppData / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName).wstring();
90 + auto configuredStorageBase = userSettings.Get<settings::Setting::SessionStoragePath>();
91 + const bool customConfigured = !configuredStorageBase.empty();
92 + const std::filesystem::path defaultBase = wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken);
93 + const std::filesystem::path storageBase =
94 + customConfigured ? std::filesystem::path(wsl::shared::string::MultiByteToWide(configuredStorageBase)) : defaultBase;
95 +
96 + const auto storageDir = storageBase / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName;
97 +
98 + // wslcsession emits the custom-location warning when it actually creates the VHD, so the notice
99 + // fires once at creation without a service-side callback that could stall CreateSession.
100 + const auto storageFlags = customConfigured ? WSLCSessionStorageFlagsWarnCustomLocation : WSLCSessionStorageFlagsNone;
101
93 - return std::unique_ptr<SessionSettings>(
94 - new SessionSettings(std::wstring(ResolvedName), std::move(storagePath), WSLCSessionStorageFlagsNone, userSettings));
102 + return std::unique_ptr<SessionSettings>(new SessionSettings(std::wstring(ResolvedName), storageDir.wstring(), storageFlags, userSettings));
103 }
104
105 // Custom session: caller provides name and storage path.
src/windows/service/inc/WSLCShared.idl
+2 -1
@@ -166,7 +166,8 @@ typedef enum _WSLCSessionStorageFlags
166 {
167 WSLCSessionStorageFlagsNone = 0,
168 WSLCSessionStorageFlagsNoCreate = 1, // Open an existing storage path, but don't create a new one.
169 - WSLCSessionStorageFlagsValid = WSLCSessionStorageFlagsNoCreate
169 + WSLCSessionStorageFlagsWarnCustomLocation = 2, // Warn when creating a new VHD at a session.storagePath-configured location.
170 + WSLCSessionStorageFlagsValid = WSLCSessionStorageFlagsNoCreate | WSLCSessionStorageFlagsWarnCustomLocation
171 } WSLCSessionStorageFlags;
172
173 cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCSessionStorageFlags);")
src/windows/wslcsession/WSLCSession.cpp
+7 -1
@@ -21,6 +21,7 @@ Abstract:
21 #include "ServiceProcessLauncher.h"
22 #include "WindowsCertStore.h"
23 #include "WslCoreFilesystem.h"
24 +#include "WSLCSessionDefaults.h"
25 #include "wslpolicies.h"
26 #include "APICompat.h"
27
@@ -38,7 +39,7 @@ using wsl::windows::service::wslc::WSLCVirtualMachine;
39 constexpr auto c_containerdStorage = "/var/lib/docker";
40 constexpr auto c_containerdSocket = "/run/containerd/containerd.sock";
41 constexpr auto c_dockerdReadyLogLine = "API listen on /var/run/docker.sock";
41 -constexpr auto c_storageVhdFilename = L"storage.vhdx";
42 +constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName;
43 constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000;
44 constexpr DWORD c_processKillTimeoutMs = 10 * 1000;
45
@@ -504,6 +505,11 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID
505 // If the VHD wasn't found, create it.
506 WSL_LOG("CreateStorageVhd", TraceLoggingValue(m_storageVhdPath.c_str(), "StorageVhdPath"));
507
508 + if (WI_IsFlagSet(Settings.StorageFlags, WSLCSessionStorageFlagsWarnCustomLocation))
509 + {
510 + EMIT_USER_WARNING(Localization::MessageWslcSessionStorageCustomLocation(storagePath.c_str()));
511 + }
512 +
513 std::filesystem::create_directories(storagePath);
514 wsl::core::filesystem::CreateVhd(m_storageVhdPath.c_str(), Settings.MaximumStorageSizeMb * _1MB, UserSid, false, false);
515 vhdCreated = true;
test/windows/WSLCTests.cpp
+1 -1
@@ -457,7 +457,7 @@ class WSLCTests
457 // Reject invalid storage flags.
458 {
459 auto settings = GetDefaultSessionSettings(L"invalid-storage-flags");
460 - settings.StorageFlags = static_cast<WSLCSessionStorageFlags>(0x2);
460 + settings.StorageFlags = static_cast<WSLCSessionStorageFlags>(0x4);
461 wil::com_ptr<IWSLCSession> session;
462 VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session), E_INVALIDARG);
463 }
test/windows/wslc/WSLCCLISettingsUnitTests.cpp
+66
@@ -524,6 +524,7 @@ class WSLCCLISettingsUnitTests
524 " networkingMode: nat\n"
525 " hostFileShareMode: virtiofs\n"
526 " dnsTunneling: true\n"
527 + " storagePath: C:\\wslc-data\n"
528 "experimental:\n"
529 " portRelay: wslrelay\n"
530 "credentialStore: wincred\n");
@@ -629,6 +630,71 @@ class WSLCCLISettingsUnitTests
630 Loc::WSLCUserSettings_Warning_InvalidValue(L"session.defaultBindingAddress", s.SettingsFilePath().wstring(), 2),
631 s.GetWarnings().front().Message);
632 }
633 +
634 + // -----------------------------------------------------------------------
635 + // session.storagePath
636 + // -----------------------------------------------------------------------
637 +
638 + // When unset, the storage path falls back to the empty built-in default (caller uses %LOCALAPPDATA%).
639 + TEST_METHOD(Validation_StoragePath_Absent_UsesEmptyDefault)
640 + {
641 + auto dir = UniqueTempDir();
642 + WriteFile(dir / L"settings.yaml", "session:\n cpuCount: 4\n");
643 +
644 + UserSettingsTest s{dir};
645 +
646 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
647 + VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionStoragePath>());
648 + }
649 +
650 + // A valid absolute path loads without warnings.
651 + TEST_METHOD(Validation_StoragePath_AbsoluteValue)
652 + {
653 + auto dir = UniqueTempDir();
654 + WriteFile(
655 + dir / L"settings.yaml",
656 + "session:\n"
657 + " storagePath: D:\\wslc\n");
658 +
659 + UserSettingsTest s{dir};
660 +
661 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
662 + VERIFY_ARE_EQUAL(std::string("D:\\wslc"), s.Get<Setting::SessionStoragePath>());
663 + }
664 +
665 + // "default" magic string uses the built-in (empty) default with no warning.
666 + TEST_METHOD(Validation_StoragePath_DefaultString)
667 + {
668 + auto dir = UniqueTempDir();
669 + WriteFile(
670 + dir / L"settings.yaml",
671 + "session:\n"
672 + " storagePath: default\n");
673 +
674 + UserSettingsTest s{dir};
675 +
676 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
677 + VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionStoragePath>());
678 + }
679 +
680 + // A relative path is rejected: the default is used and an invalid-value warning emitted.
681 + TEST_METHOD(Validation_StoragePath_RelativeValue_UsesDefaultAndWarns)
682 + {
683 + auto dir = UniqueTempDir();
684 + WriteFile(
685 + dir / L"settings.yaml",
686 + "session:\n"
687 + " storagePath: wslc\\data\n");
688 +
689 + UserSettingsTest s{dir};
690 +
691 + VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionStoragePath>());
692 + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
693 + VERIFY_ARE_EQUAL(
694 + Loc::WSLCUserSettings_Warning_InvalidValue(L"session.storagePath", s.SettingsFilePath().wstring(), 2),
695 + s.GetWarnings().front().Message);
696 + VERIFY_ARE_EQUAL(std::wstring(L"session.storagePath"), s.GetWarnings().front().SettingPath);
697 + }
698 };
699
700 } // namespace WSLCCLISettingsUnitTests