diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index d95f6a646..d18c65645 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
+
+ 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.
+
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/common/WSLCUserSettings.cpp b/src/windows/common/WSLCUserSettings.cpp
index f05610f79..b0c3e1e86 100644
--- a/src/windows/common/WSLCUserSettings.cpp
+++ b/src/windows/common/WSLCUserSettings.cpp
@@ -47,6 +47,12 @@ 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 session VHD is created at\n"
+ " # \\wslc\\sessions\\\\storage.vhdx. Must be an absolute path (e.g. D:\\data default: "
+ "%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"
" # used without an explicit address (default: 127.0.0.1)\n"
" # defaultBindingAddress: default\n"
@@ -147,6 +153,16 @@ namespace details {
return value;
}
+ WSLC_VALIDATE_SETTING(SessionStoragePath)
+ {
+ if (value.empty() || !std::filesystem::path(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..e5e0540e9 100644
--- a/src/windows/service/exe/WSLCSessionManager.cpp
+++ b/src/windows/service/exe/WSLCSessionManager.cpp
@@ -86,12 +86,20 @@ 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 bool customConfigured = !configuredStorageBase.empty();
+ const std::filesystem::path defaultBase = wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken);
+ const std::filesystem::path storageBase =
+ customConfigured ? std::filesystem::path(wsl::shared::string::MultiByteToWide(configuredStorageBase)) : defaultBase;
+
+ const auto storageDir = storageBase / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName;
+
+ // 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 std::unique_ptr(
- new SessionSettings(std::wstring(ResolvedName), std::move(storagePath), WSLCSessionStorageFlagsNone, 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/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 eefba3048..a1bdd4f6a 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;
@@ -504,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;
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);
}
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