| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | WSLCSessionManager.cpp |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | Implementation for WSLCSessionManager. |
| 12 | |
| 13 | Sessions run in a per-user COM server process for security isolation. |
| 14 | The SYSTEM service creates sessions via IWSLCSessionFactory which returns |
| 15 | both the session interface (for clients) and an IWSLCSessionReference |
| 16 | (for the service to track sessions via weak references). |
| 17 | |
| 18 | Session lifetime: |
| 19 | - Non-persistent sessions: tracked via IWSLCSessionReference which holds |
| 20 | weak references. Sessions are cleaned up when all client refs are released. |
| 21 | - Persistent sessions: the service holds an additional strong IWSLCSession |
| 22 | reference to keep them alive until explicitly terminated. |
| 23 | |
| 24 | A job object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ensures that all |
| 25 | per-user COM server processes are automatically terminated if wslservice |
| 26 | crashes or exits unexpectedly. |
| 27 | |
| 28 | --*/ |
| 29 | |
| 30 | #include "WSLCSessionManager.h" |
| 31 | #include "HcsVirtualMachine.h" |
| 32 | #include "WSLCUserSettings.h" |
| 33 | #include "WSLCSessionDefaults.h" |
| 34 | #include "WSLCPluginNotifier.h" |
| 35 | #include "PluginManager.h" |
| 36 | #include "ExecutionContext.h" |
| 37 | #include "helpers.hpp" |
| 38 | #include "wslutil.h" |
| 39 | #include "filesystem.hpp" |
| 40 | #include "APICompat.h" |
| 41 | #include "Localization.h" |
| 42 | |
| 43 | extern wsl::windows::service::PluginManager g_pluginManager; |
| 44 | |
| 45 | using wsl::windows::common::COMServiceExecutionContext; |
| 46 | using wsl::windows::service::wslc::CallingProcessTokenInfo; |
| 47 | using wsl::windows::service::wslc::HcsVirtualMachine; |
| 48 | using wsl::windows::service::wslc::WSLCPluginNotifier; |
| 49 | using wsl::windows::service::wslc::WSLCSessionManagerImpl; |
| 50 | using wsl::windows::service::wslc::WSLCVirtualMachineFactory; |
| 51 | namespace wslutil = wsl::windows::common::wslutil; |
| 52 | namespace apicompat = wsl::windows::common::apicompat; |
| 53 | namespace settings = wsl::windows::wslc::settings; |
| 54 | |
| 55 | namespace { |
| 56 | |
| 57 | std::atomic<wsl::windows::service::wslc::WSLCSessionManagerImpl*> g_managerInstance{nullptr}; |
| 58 | |
| 59 | // Session settings built server-side from the caller's settings.yaml. |
| 60 | struct SessionSettings |
| 61 | { |
| 62 | std::wstring DisplayName; |
| 63 | std::wstring StoragePath; |
| 64 | std::string HostLoopback; |
| 65 | WSLCSessionSettings Settings{}; |
| 66 | |
| 67 | NON_COPYABLE(SessionSettings); |
| 68 | NON_MOVABLE(SessionSettings); |
| 69 | |
| 70 | // Load user settings under impersonation. |
| 71 | static settings::UserSettings LoadUserSettings(HANDLE UserToken) |
| 72 | { |
| 73 | auto localAppData = wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken); |
| 74 | auto runAsUser = wil::impersonate_token(UserToken); |
| 75 | return settings::UserSettings(localAppData / L"wslc"); |
| 76 | } |
| 77 | |
| 78 | // Get default memory size. Half of available memory. |
| 79 | static uint32_t DefaultMemoryMb() |
| 80 | { |
| 81 | MEMORYSTATUSEX memInfo{sizeof(MEMORYSTATUSEX)}; |
| 82 | THROW_IF_WIN32_BOOL_FALSE(GlobalMemoryStatusEx(&memInfo)); |
| 83 | return static_cast<uint32_t>(memInfo.ullTotalPhys / (2 * _1MB)); |
| 84 | } |
| 85 | |
| 86 | // Default session: name and storage path determined from caller's token. |
| 87 | static std::unique_ptr<SessionSettings> Default(HANDLE UserToken, const std::wstring& ResolvedName) |
| 88 | { |
| 89 | auto userSettings = LoadUserSettings(UserToken); |
| 90 | |
| 91 | auto configuredStorageBase = userSettings.Get<settings::Setting::SessionStoragePath>(); |
| 92 | const bool customConfigured = !configuredStorageBase.empty(); |
| 93 | const std::filesystem::path defaultBase = wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken); |
| 94 | const std::filesystem::path storageBase = |
| 95 | customConfigured ? std::filesystem::path(wsl::shared::string::MultiByteToWide(configuredStorageBase)) : defaultBase; |
| 96 | |
| 97 | const auto storageDir = storageBase / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName; |
| 98 | |
| 99 | // wslcsession emits the custom-location warning when it actually creates the VHD, so the notice |
| 100 | // fires once at creation without a service-side callback that could stall CreateSession. |
| 101 | const auto storageFlags = customConfigured ? WSLCSessionStorageFlagsWarnCustomLocation : WSLCSessionStorageFlagsNone; |
| 102 | |
| 103 | return std::unique_ptr<SessionSettings>(new SessionSettings(std::wstring(ResolvedName), storageDir.wstring(), storageFlags, userSettings)); |
| 104 | } |
| 105 | |
| 106 | // Custom session: caller provides name and storage path. |
| 107 | static SessionSettings Custom(HANDLE UserToken, LPCWSTR Name, LPCWSTR Path, WSLCSessionStorageFlags StorageFlags = WSLCSessionStorageFlagsNone) |
| 108 | { |
| 109 | auto userSettings = LoadUserSettings(UserToken); |
| 110 | return SessionSettings(Name, Path, StorageFlags, userSettings); |
| 111 | } |
| 112 | |
| 113 | private: |
| 114 | SessionSettings(std::wstring name, std::wstring path, WSLCSessionStorageFlags storageFlags, const settings::UserSettings& userSettings) : |
| 115 | DisplayName(std::move(name)), StoragePath(std::move(path)), HostLoopback(userSettings.Get<settings::Setting::SessionHostLoopback>()) |
| 116 | { |
| 117 | Settings.DisplayName = DisplayName.c_str(); |
| 118 | Settings.StoragePath = StoragePath.c_str(); |
| 119 | Settings.HostLoopback = HostLoopback.empty() ? nullptr : HostLoopback.c_str(); |
| 120 | auto cpuCount = userSettings.Get<settings::Setting::SessionCpuCount>(); |
| 121 | Settings.CpuCount = cpuCount > 0 ? cpuCount : wsl::windows::common::wslutil::GetLogicalProcessorCount(); |
| 122 | auto memoryMb = userSettings.Get<settings::Setting::SessionMemoryMb>(); |
| 123 | Settings.MemoryMb = memoryMb > 0 ? memoryMb : SessionSettings::DefaultMemoryMb(); |
| 124 | Settings.MaximumStorageSizeMb = userSettings.Get<settings::Setting::SessionStorageSizeMb>(); |
| 125 | Settings.BootTimeoutMs = wsl::windows::wslc::DefaultBootTimeoutMs; |
| 126 | Settings.IdleTimeoutSec = userSettings.Get<settings::Setting::SessionIdleTimeout>(); |
| 127 | Settings.NetworkingMode = userSettings.Get<settings::Setting::SessionNetworkingMode>(); |
| 128 | |
| 129 | // TODO: Add a config setting to opt-out of GPU support. |
| 130 | Settings.FeatureFlags = WslcFeatureFlagsGPU; |
| 131 | WI_SetFlagIf(Settings.FeatureFlags, WslcFeatureFlagsDnsTunneling, userSettings.Get<settings::Setting::SessionDnsTunneling>()); |
| 132 | WI_SetFlagIf( |
| 133 | Settings.FeatureFlags, |
| 134 | WslcFeatureFlagsVirtioFs, |
| 135 | userSettings.Get<settings::Setting::SessionHostFileShareMode>() == settings::HostFileShareMode::VirtioFs); |
| 136 | WI_SetFlagIf( |
| 137 | Settings.FeatureFlags, |
| 138 | WslcFeatureFlagsPortRelayWslRelay, |
| 139 | userSettings.Get<settings::Setting::SessionPortRelay>() == settings::PortRelayType::WslRelay); |
| 140 | Settings.StorageFlags = storageFlags; |
| 141 | } |
| 142 | }; |
| 143 | |
| 144 | } // namespace |
| 145 | |
| 146 | WSLCSessionManagerImpl::WSLCSessionManagerImpl() |
| 147 | { |
| 148 | g_managerInstance.store(this); |
| 149 | } |
| 150 | |
| 151 | WSLCSessionManagerImpl::~WSLCSessionManagerImpl() |
| 152 | { |
| 153 | g_managerInstance.store(nullptr); |
| 154 | |
| 155 | // Terminate all sessions on shutdown. |
| 156 | // Call Terminate() directly rather than going through ForEachSession(), |
| 157 | // which would needlessly resolve weak references and call GetState(). |
| 158 | // Terminate() already handles the "session is gone" case gracefully. |
| 159 | std::lock_guard lock(m_wslcSessionsLock); |
| 160 | for (auto& entry : m_sessions) |
| 161 | { |
| 162 | NotifySessionStoppingLockHeld(entry); |
| 163 | LOG_IF_FAILED(entry.Ref->Terminate()); |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | void WSLCSessionManagerImpl::NotifySessionStoppingLockHeld(SessionEntry& entry) noexcept |
| 168 | try |
| 169 | { |
| 170 | if (entry.StoppingNotified) |
| 171 | { |
| 172 | return; |
| 173 | } |
| 174 | |
| 175 | entry.StoppingNotified = true; |
| 176 | WSLCSessionInformation info{}; |
| 177 | info.SessionId = static_cast<WSLCSessionId>(entry.SessionId); |
| 178 | info.DisplayName = entry.DisplayName.c_str(); |
| 179 | info.ApplicationPid = entry.CreatorPid; |
| 180 | info.UserToken = entry.UserToken.get(); |
| 181 | info.UserSid = entry.UserSid.data(); |
| 182 | g_pluginManager.OnWslcSessionStopping(&info); |
| 183 | } |
| 184 | CATCH_LOG() |
| 185 | |
| 186 | void WSLCSessionManagerImpl::CreateSession( |
| 187 | _In_ const WSLCSessionSettings* Settings, _In_ WSLCSessionFlags Flags, _In_opt_ IWarningCallback* WarningCallback, _Out_ IWSLCSession** WslcSession) |
| 188 | { |
| 189 | THROW_HR_IF_NULL(E_POINTER, WslcSession); |
| 190 | |
| 191 | auto tokenInfo = GetCallingProcessTokenInfo(); |
| 192 | const auto callerToken = wsl::windows::common::security::GetUserToken(TokenImpersonation); |
| 193 | |
| 194 | // Resolve display name upfront (for both default and custom sessions). |
| 195 | std::wstring resolvedDisplayName; |
| 196 | if (Settings == nullptr) |
| 197 | { |
| 198 | // Default session: name determined from token, qualified with username. |
| 199 | resolvedDisplayName = ResolveDefaultSessionName(tokenInfo); |
| 200 | Flags = WSLCSessionFlagsOpenExisting | WSLCSessionFlagsPersistent; |
| 201 | } |
| 202 | else |
| 203 | { |
| 204 | THROW_HR_IF(WSLC_E_INVALID_SESSION_NAME, Settings->DisplayName == nullptr || wcslen(Settings->DisplayName) == 0); |
| 205 | THROW_HR_IF(E_INVALIDARG, Settings->StoragePath != nullptr && wcslen(Settings->StoragePath) == 0); |
| 206 | THROW_HR_IF(WSLC_E_INVALID_SESSION_NAME, wcslen(Settings->DisplayName) >= std::size(WSLCSessionListEntry{}.DisplayName)); |
| 207 | THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCSessionFlagsValid), "Invalid session flags: 0x%x", Flags); |
| 208 | THROW_HR_IF_MSG( |
| 209 | E_INVALIDARG, WI_IsAnyFlagSet(Settings->FeatureFlags, ~WSLCFeatureFlagsValid), "Invalid feature flags: 0x%x", Settings->FeatureFlags); |
| 210 | THROW_HR_IF_MSG( |
| 211 | E_INVALIDARG, |
| 212 | WI_IsAnyFlagSet(Settings->StorageFlags, ~WSLCSessionStorageFlagsValid), |
| 213 | "Invalid storage flags: %i", |
| 214 | Settings->StorageFlags); |
| 215 | |
| 216 | // Reserved names can only be assigned server-side via null Settings. |
| 217 | THROW_HR_IF(WSLC_E_SESSION_RESERVED, IsReservedSessionName(Settings->DisplayName)); |
| 218 | |
| 219 | resolvedDisplayName = Settings->DisplayName; |
| 220 | } |
| 221 | |
| 222 | std::lock_guard lock(m_wslcSessionsLock); |
| 223 | |
| 224 | // Check for an existing session first. |
| 225 | auto result = ForEachSession<HRESULT>([&](auto& entry, const wil::com_ptr<IWSLCSession>& session) noexcept -> std::optional<HRESULT> { |
| 226 | if (!wsl::shared::string::IsEqual(entry.DisplayName.c_str(), resolvedDisplayName.c_str())) |
| 227 | { |
| 228 | return {}; |
| 229 | } |
| 230 | |
| 231 | RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), WI_IsFlagClear(Flags, WSLCSessionFlagsOpenExisting)); |
| 232 | |
| 233 | RETURN_IF_FAILED(CheckTokenAccess(entry, tokenInfo)); |
| 234 | |
| 235 | RETURN_IF_FAILED(wil::com_copy_to_nothrow(session, WslcSession)); |
| 236 | |
| 237 | return S_OK; |
| 238 | }); |
| 239 | |
| 240 | if (result.has_value()) |
| 241 | { |
| 242 | THROW_IF_FAILED(result.value()); |
| 243 | return; // Existing session was opened. |
| 244 | } |
| 245 | |
| 246 | wslutil::StopWatch stopWatch; |
| 247 | |
| 248 | // Initialize settings for the default session. |
| 249 | std::unique_ptr<SessionSettings> defaultSettings; |
| 250 | if (Settings == nullptr) |
| 251 | { |
| 252 | defaultSettings = SessionSettings::Default(callerToken.get(), resolvedDisplayName); |
| 253 | Settings = &defaultSettings->Settings; |
| 254 | } |
| 255 | |
| 256 | std::wstring callerFileName; |
| 257 | |
| 258 | HRESULT creationResult = wil::ResultFromException([&]() { |
| 259 | // Get caller info. |
| 260 | const auto callerProcess = wslutil::OpenCallingProcess(PROCESS_QUERY_LIMITED_INFORMATION); |
| 261 | const ULONG sessionId = m_nextSessionId++; |
| 262 | const DWORD creatorPid = GetProcessId(callerProcess.get()); |
| 263 | |
| 264 | // Query the full image path of the calling process and extract just the file name. |
| 265 | std::wstring callerFilePath; |
| 266 | if (SUCCEEDED_LOG(wil::QueryFullProcessImageNameW<std::wstring>(callerProcess.get(), 0, callerFilePath))) |
| 267 | { |
| 268 | callerFileName = std::filesystem::path(callerFilePath).filename().wstring(); |
| 269 | } |
| 270 | |
| 271 | const auto userToken = wsl::windows::common::security::GetUserToken(TokenImpersonation); |
| 272 | |
| 273 | // Capture a duplicated user token + raw SID so PluginManager can build |
| 274 | // WSLCSessionInformation later (e.g. on shutdown) without re-impersonating. |
| 275 | // The token is shared between the SessionEntry and the WSLCPluginNotifier. |
| 276 | wil::unique_handle dupToken; |
| 277 | THROW_IF_WIN32_BOOL_FALSE(DuplicateTokenEx( |
| 278 | userToken.get(), TOKEN_QUERY | TOKEN_DUPLICATE, nullptr, SecurityImpersonation, TokenImpersonation, &dupToken)); |
| 279 | wil::shared_handle sharedToken{dupToken.release()}; |
| 280 | |
| 281 | const DWORD sidLen = GetLengthSid(tokenInfo.TokenInfo->User.Sid); |
| 282 | std::vector<BYTE> storedSid(sidLen); |
| 283 | THROW_IF_WIN32_BOOL_FALSE(CopySid(sidLen, storedSid.data(), tokenInfo.TokenInfo->User.Sid)); |
| 284 | |
| 285 | // Build the plugin notifier service-side. Lifetime tracked via the SessionEntry. |
| 286 | Microsoft::WRL::ComPtr<IWSLCPluginNotifier> notifier; |
| 287 | notifier = wil::MakeOrThrow<WSLCPluginNotifier>( |
| 288 | g_pluginManager, sessionId, creatorPid, std::wstring(resolvedDisplayName), wil::shared_handle(sharedToken), std::vector<BYTE>(storedSid)); |
| 289 | |
| 290 | // Create the VM factory in the SYSTEM service (privileged). The per-user session |
| 291 | // uses it to create VMs on demand and recreate them after idle-termination. |
| 292 | auto vmFactory = Microsoft::WRL::Make<WSLCVirtualMachineFactory>(Settings); |
| 293 | |
| 294 | // Launch per-user COM server factory and add it to a fresh per-session job object for crash cleanup. |
| 295 | auto factory = wslutil::CreateComServerAsUser<IWSLCSessionFactory>(__uuidof(WSLCSessionFactory), userToken.get()); |
| 296 | wil::unique_handle sessionJob = CreateSessionProcessJob(factory.get()); |
| 297 | |
| 298 | const auto sessionSettings = CreateSessionSettings(sessionId, callerFileName.c_str(), Settings, resolvedDisplayName.c_str()); |
| 299 | wil::com_ptr<IWSLCSession> session; |
| 300 | wil::com_ptr<IWSLCSessionReference> serviceRef; |
| 301 | const auto factoryHr = |
| 302 | factory->CreateSession(&sessionSettings, vmFactory.Get(), notifier.Get(), WarningCallback, &session, &serviceRef); |
| 303 | if (FAILED(factoryHr)) |
| 304 | { |
| 305 | if (auto comError = wslutil::GetCOMErrorInfo(); comError && comError->Message) |
| 306 | { |
| 307 | THROW_HR_WITH_USER_ERROR(factoryHr, comError->Message.get()); |
| 308 | } |
| 309 | |
| 310 | THROW_HR(factoryHr); |
| 311 | } |
| 312 | |
| 313 | // Track the session via its service ref, along with metadata and security info. |
| 314 | m_sessions.push_back(SessionEntry{ |
| 315 | std::move(serviceRef), sessionId, creatorPid, resolvedDisplayName, std::move(tokenInfo), notifier, false, sharedToken, std::move(storedSid), std::move(sessionJob)}); |
| 316 | |
| 317 | // For persistent sessions, also hold a strong reference to keep them alive. |
| 318 | const bool persistent = WI_IsFlagSet(Flags, WSLCSessionFlagsPersistent); |
| 319 | if (persistent) |
| 320 | { |
| 321 | m_persistentSessions.emplace_back(sessionId, session); |
| 322 | } |
| 323 | |
| 324 | // Notify plugins that the session was created. A failure here aborts session creation. |
| 325 | try |
| 326 | { |
| 327 | auto& entry = m_sessions.back(); |
| 328 | WSLCSessionInformation info{}; |
| 329 | info.SessionId = static_cast<WSLCSessionId>(entry.SessionId); |
| 330 | info.DisplayName = entry.DisplayName.c_str(); |
| 331 | info.ApplicationPid = entry.CreatorPid; |
| 332 | info.UserToken = entry.UserToken.get(); |
| 333 | info.UserSid = entry.UserSid.data(); |
| 334 | g_pluginManager.OnWslcSessionCreated(&info); |
| 335 | } |
| 336 | catch (...) |
| 337 | { |
| 338 | const auto error = wil::ResultFromCaughtException(); |
| 339 | |
| 340 | // Plugin rejected the session: tear it down before propagating. |
| 341 | m_sessions.back().StoppingNotified = true; // Don't fire stopping for a session that never started successfully. |
| 342 | LOG_IF_FAILED(m_sessions.back().Ref->Terminate()); |
| 343 | m_sessions.pop_back(); |
| 344 | |
| 345 | auto remove = std::ranges::remove_if(m_persistentSessions, [&](const auto& e) { return e.first == sessionId; }); |
| 346 | m_persistentSessions.erase(remove.begin(), remove.end()); |
| 347 | |
| 348 | THROW_HR(error); |
| 349 | } |
| 350 | |
| 351 | *WslcSession = session.detach(); |
| 352 | }); |
| 353 | |
| 354 | // This telemetry event is used to keep track of session creation performance (via CreationTimeMs) and failure reasons (via Result). |
| 355 | WSL_LOG( |
| 356 | "WSLCCreateSession", |
| 357 | TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage), |
| 358 | TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA), |
| 359 | TraceLoggingValue(resolvedDisplayName.c_str(), "Name"), |
| 360 | TraceLoggingValue(WSL_PACKAGE_VERSION, "wslVersion"), |
| 361 | TraceLoggingValue(stopWatch.ElapsedMilliseconds(), "CreationTimeMs"), |
| 362 | TraceLoggingValue(creationResult, "Result"), |
| 363 | TraceLoggingValue(tokenInfo.Elevated, "Elevated"), |
| 364 | TraceLoggingValue(static_cast<uint32_t>(Flags), "Flags"), |
| 365 | TraceLoggingLevel(WINEVENT_LEVEL_INFO)); |
| 366 | |
| 367 | WSL_LOG( |
| 368 | "WSLCCreateSessionCaller", |
| 369 | TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage), |
| 370 | TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA), |
| 371 | TraceLoggingValue(callerFileName.c_str(), "CallerFileName"), |
| 372 | TraceLoggingLevel(WINEVENT_LEVEL_INFO)); |
| 373 | |
| 374 | THROW_IF_FAILED_MSG(creationResult, "Failed to create session: %ls", resolvedDisplayName.c_str()); |
| 375 | } |
| 376 | |
| 377 | void WSLCSessionManagerImpl::OpenSession(ULONG Id, IWSLCSession** Session) |
| 378 | { |
| 379 | THROW_HR_IF_NULL(E_POINTER, Session); |
| 380 | |
| 381 | auto tokenInfo = GetCallingProcessTokenInfo(); |
| 382 | auto result = ForEachSession<HRESULT>([&](auto& entry, const wil::com_ptr<IWSLCSession>& session) noexcept -> std::optional<HRESULT> { |
| 383 | if (entry.SessionId != Id) |
| 384 | { |
| 385 | return {}; |
| 386 | } |
| 387 | |
| 388 | RETURN_IF_FAILED(CheckTokenAccess(entry, tokenInfo)); |
| 389 | |
| 390 | RETURN_IF_FAILED(wil::com_copy_to_nothrow(session, Session)); |
| 391 | |
| 392 | return S_OK; |
| 393 | }); |
| 394 | |
| 395 | THROW_IF_FAILED_MSG(result.value_or(WSLC_E_SESSION_NOT_FOUND), "Session '%lu' not found", Id); |
| 396 | } |
| 397 | |
| 398 | void WSLCSessionManagerImpl::OpenSessionByName(LPCWSTR DisplayName, IWSLCSession** Session) |
| 399 | { |
| 400 | THROW_HR_IF_NULL(E_POINTER, Session); |
| 401 | |
| 402 | auto tokenInfo = GetCallingProcessTokenInfo(); |
| 403 | |
| 404 | // Null name = default session, resolved from caller's token + username. |
| 405 | std::wstring resolvedName; |
| 406 | if (DisplayName == nullptr) |
| 407 | { |
| 408 | resolvedName = ResolveDefaultSessionName(tokenInfo); |
| 409 | DisplayName = resolvedName.c_str(); |
| 410 | } |
| 411 | |
| 412 | auto result = ForEachSession<HRESULT>([&](auto& entry, const wil::com_ptr<IWSLCSession>& session) noexcept -> std::optional<HRESULT> { |
| 413 | if (!wsl::shared::string::IsEqual(entry.DisplayName.c_str(), DisplayName)) |
| 414 | { |
| 415 | return {}; |
| 416 | } |
| 417 | |
| 418 | RETURN_IF_FAILED(CheckTokenAccess(entry, tokenInfo)); |
| 419 | |
| 420 | RETURN_IF_FAILED(wil::com_copy_to_nothrow(session, Session)); |
| 421 | |
| 422 | return S_OK; |
| 423 | }); |
| 424 | |
| 425 | THROW_HR_WITH_USER_ERROR_IF( |
| 426 | WSLC_E_SESSION_NOT_FOUND, wsl::shared::Localization::MessageWslcSessionNotFound(DisplayName), !result.has_value()); |
| 427 | |
| 428 | THROW_IF_FAILED_MSG(result.value(), "Failed to open session '%ls'", DisplayName); |
| 429 | } |
| 430 | |
| 431 | void WSLCSessionManagerImpl::ListSessions(_Out_ WSLCSessionListEntry** Sessions, _Out_ ULONG* SessionsCount) |
| 432 | { |
| 433 | THROW_HR_IF_NULL(E_POINTER, Sessions); |
| 434 | THROW_HR_IF_NULL(E_POINTER, SessionsCount); |
| 435 | |
| 436 | std::vector<WSLCSessionListEntry> sessionInfo; |
| 437 | |
| 438 | ForEachSession<void>([&](auto& entry, const auto&) noexcept { |
| 439 | try |
| 440 | { |
| 441 | wil::unique_hlocal_string sidString; |
| 442 | THROW_IF_WIN32_BOOL_FALSE(ConvertSidToStringSidW(entry.Owner.TokenInfo->User.Sid, &sidString)); |
| 443 | |
| 444 | auto& it = sessionInfo.emplace_back(WSLCSessionListEntry{.SessionId = entry.SessionId, .CreatorPid = entry.CreatorPid}); |
| 445 | wcscpy_s(it.Sid, _countof(it.Sid), sidString.get()); |
| 446 | wcscpy_s(it.DisplayName, _countof(it.DisplayName), entry.DisplayName.c_str()); |
| 447 | } |
| 448 | CATCH_LOG() |
| 449 | }); |
| 450 | |
| 451 | auto output = wil::make_unique_cotaskmem<WSLCSessionListEntry[]>(sessionInfo.size()); |
| 452 | memcpy(output.get(), sessionInfo.data(), sessionInfo.size() * sizeof(WSLCSessionListEntry)); |
| 453 | |
| 454 | *Sessions = output.release(); |
| 455 | *SessionsCount = static_cast<ULONG>(sessionInfo.size()); |
| 456 | } |
| 457 | |
| 458 | void WSLCSessionManagerImpl::EnterSession( |
| 459 | _In_ LPCWSTR DisplayName, _In_ LPCWSTR StoragePath, _In_opt_ IWarningCallback* WarningCallback, _Out_ IWSLCSession** WslcSession) |
| 460 | { |
| 461 | THROW_HR_IF(E_POINTER, DisplayName == nullptr || StoragePath == nullptr); |
| 462 | THROW_HR_IF(E_INVALIDARG, DisplayName[0] == L'\0' || StoragePath[0] == L'\0'); |
| 463 | |
| 464 | const auto callerToken = wsl::windows::common::security::GetUserToken(TokenImpersonation); |
| 465 | auto sessionSettings = SessionSettings::Custom(callerToken.get(), DisplayName, StoragePath, WSLCSessionStorageFlagsNoCreate); |
| 466 | CreateSession(&sessionSettings.Settings, WSLCSessionFlagsNone, WarningCallback, WslcSession); |
| 467 | } |
| 468 | |
| 469 | WSLCSessionInitSettings WSLCSessionManagerImpl::CreateSessionSettings( |
| 470 | _In_ ULONG SessionId, _In_ LPCWSTR CreatorProcessName, _In_ const WSLCSessionSettings* Settings, _In_ LPCWSTR ResolvedDisplayName) |
| 471 | { |
| 472 | WSLCSessionInitSettings sessionSettings{}; |
| 473 | sessionSettings.SessionId = SessionId; |
| 474 | sessionSettings.CreatorProcessName = CreatorProcessName; |
| 475 | sessionSettings.DisplayName = ResolvedDisplayName; |
| 476 | sessionSettings.StoragePath = Settings->StoragePath; |
| 477 | sessionSettings.MaximumStorageSizeMb = Settings->MaximumStorageSizeMb; |
| 478 | sessionSettings.BootTimeoutMs = Settings->BootTimeoutMs; |
| 479 | sessionSettings.NetworkingMode = Settings->NetworkingMode; |
| 480 | sessionSettings.FeatureFlags = Settings->FeatureFlags; |
| 481 | sessionSettings.RootVhdTypeOverride = Settings->RootVhdTypeOverride; |
| 482 | sessionSettings.StorageFlags = Settings->StorageFlags; |
| 483 | sessionSettings.SwapSizeMb = Settings->MemoryMb; |
| 484 | sessionSettings.IdleTimeoutSec = Settings->IdleTimeoutSec; |
| 485 | return sessionSettings; |
| 486 | } |
| 487 | |
| 488 | wil::unique_handle WSLCSessionManagerImpl::CreateSessionProcessJob(_In_ IWSLCSessionFactory* Factory) |
| 489 | { |
| 490 | // Use a fresh job per session; reusing one fails intermittently with |
| 491 | // ERROR_ACCESS_DENIED once it's assigned to a process the system put in another job. |
| 492 | wil::unique_handle jobObject = wsl::windows::common::helpers::CreateKillOnCloseJob(); |
| 493 | |
| 494 | wil::unique_handle process; |
| 495 | THROW_IF_FAILED(Factory->GetProcessHandle(process.put())); |
| 496 | |
| 497 | THROW_IF_WIN32_BOOL_FALSE(AssignProcessToJobObject(jobObject.get(), process.get())); |
| 498 | |
| 499 | return jobObject; |
| 500 | } |
| 501 | |
| 502 | CallingProcessTokenInfo WSLCSessionManagerImpl::GetCallingProcessTokenInfo() |
| 503 | { |
| 504 | const wil::unique_handle userToken = wsl::windows::common::security::GetUserToken(TokenImpersonation); |
| 505 | |
| 506 | auto tokenInfo = wil::get_token_information<TOKEN_USER>(userToken.get()); |
| 507 | auto elevated = wil::test_token_membership(userToken.get(), SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); |
| 508 | |
| 509 | return {std::move(tokenInfo), elevated}; |
| 510 | } |
| 511 | |
| 512 | std::wstring WSLCSessionManagerImpl::ResolveDefaultSessionName(const CallingProcessTokenInfo& TokenInfo) |
| 513 | { |
| 514 | // Look up the username from the caller's SID so each user gets their own |
| 515 | // default session (e.g. "wslc-cli-alice", "wslc-cli-admin-bob"). |
| 516 | wchar_t username[256 + 1] = {}; |
| 517 | DWORD usernameLen = ARRAYSIZE(username); |
| 518 | wchar_t domain[MAX_PATH] = {}; |
| 519 | DWORD domainLen = ARRAYSIZE(domain); |
| 520 | SID_NAME_USE sidType; |
| 521 | THROW_IF_WIN32_BOOL_FALSE(LookupAccountSidW(nullptr, TokenInfo.TokenInfo->User.Sid, username, &usernameLen, domain, &domainLen, &sidType)); |
| 522 | |
| 523 | auto baseName = TokenInfo.Elevated ? wsl::windows::wslc::DefaultAdminSessionName : wsl::windows::wslc::DefaultSessionName; |
| 524 | return std::format(L"{}-{}", baseName, username); |
| 525 | } |
| 526 | |
| 527 | bool WSLCSessionManagerImpl::IsReservedSessionName(LPCWSTR Name) |
| 528 | { |
| 529 | // Block any name that is exactly "wslc-cli" or starts with "wslc-cli-", |
| 530 | // which covers the admin variant and all per-user resolved names. |
| 531 | constexpr std::wstring_view prefix{wsl::windows::wslc::DefaultSessionName}; |
| 532 | std::wstring_view name{Name}; |
| 533 | if (name.size() < prefix.size()) |
| 534 | { |
| 535 | return false; |
| 536 | } |
| 537 | |
| 538 | if (!wsl::shared::string::IsEqual(name.substr(0, prefix.size()), prefix, true)) |
| 539 | { |
| 540 | return false; |
| 541 | } |
| 542 | |
| 543 | return name.size() == prefix.size() || name[prefix.size()] == L'-'; |
| 544 | } |
| 545 | |
| 546 | HRESULT WSLCSessionManagerImpl::CheckTokenAccess(const SessionEntry& Entry, const CallingProcessTokenInfo& TokenInfo) |
| 547 | { |
| 548 | // Allow elevated tokens to access all sessions. |
| 549 | // Otherwise a token can only access sessions from the same SID and elevation status. |
| 550 | // TODO: Offer proper ACL checks. |
| 551 | |
| 552 | if (TokenInfo.Elevated) |
| 553 | { |
| 554 | return S_OK; // Token is elevated, allow access. |
| 555 | } |
| 556 | |
| 557 | RETURN_HR_IF(E_ACCESSDENIED, !EqualSid(Entry.Owner.TokenInfo->User.Sid, TokenInfo.TokenInfo->User.Sid)); // Different account, deny access. |
| 558 | |
| 559 | RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ELEVATION_REQUIRED), Entry.Owner.Elevated); // Non-elevated token trying to access elevated session, deny access. |
| 560 | |
| 561 | return S_OK; |
| 562 | } |
| 563 | |
| 564 | WSLCSessionManager::WSLCSessionManager(WSLCSessionManagerImpl* Impl) |
| 565 | { |
| 566 | Initialize(Impl); |
| 567 | } |
| 568 | |
| 569 | HRESULT WSLCSessionManager::GetVersion(_Out_ WSLCVersion* Version) |
| 570 | try |
| 571 | { |
| 572 | RETURN_HR_IF(E_POINTER, Version == nullptr); |
| 573 | |
| 574 | Version->Major = WSL_PACKAGE_VERSION_MAJOR; |
| 575 | Version->Minor = WSL_PACKAGE_VERSION_MINOR; |
| 576 | Version->Revision = WSL_PACKAGE_VERSION_REVISION; |
| 577 | |
| 578 | return S_OK; |
| 579 | } |
| 580 | CATCH_RETURN(); |
| 581 | |
| 582 | HRESULT WSLCSessionManager::IsClientVersionSupported(_In_ const WSLCCompatVersion* ClientVersion, _Out_ BOOL* IsSupported) |
| 583 | try |
| 584 | { |
| 585 | RETURN_HR_IF(E_POINTER, ClientVersion == nullptr || IsSupported == nullptr); |
| 586 | |
| 587 | WSL_LOG( |
| 588 | "ClientVersionCheck", |
| 589 | TraceLoggingValue(ClientVersion->Major, "Major"), |
| 590 | TraceLoggingValue(ClientVersion->Minor, "Minor"), |
| 591 | TraceLoggingValue(ClientVersion->Revision, "Revision")); |
| 592 | |
| 593 | // Moved to 2.9.5 in https://github.com/microsoft/WSL/pull/41199 to add OpenContainer to a compat interface. |
| 594 | // While we could have prevented moving this forward, as we are still in preview it should be acceptable to break. |
| 595 | // This also forces callers to experience the SDK support error early, hopefully leading to better support if a post-release support floor needs to be raised. |
| 596 | constexpr std::tuple<uint32_t, uint32_t, uint32_t> c_minClientVersion{2, 9, 5}; |
| 597 | |
| 598 | const std::tuple<uint32_t, uint32_t, uint32_t> clientVersion{ClientVersion->Major, ClientVersion->Minor, ClientVersion->Revision}; |
| 599 | |
| 600 | // Support anything at or above the minimum and when the client version exactly matches ours to cover dev builds before the break is released. |
| 601 | *IsSupported = (clientVersion >= c_minClientVersion || wsl::shared::PackageVersion == clientVersion); |
| 602 | |
| 603 | return S_OK; |
| 604 | } |
| 605 | CATCH_RETURN(); |
| 606 | |
| 607 | HRESULT WSLCSessionManager::CreateSession( |
| 608 | const WSLCSessionSettings* WslcSessionSettings, WSLCSessionFlags Flags, IWarningCallback* WarningCallback, IWSLCSession** WslcSession) |
| 609 | try |
| 610 | { |
| 611 | COMServiceExecutionContext context; |
| 612 | |
| 613 | return CallImpl(&WSLCSessionManagerImpl::CreateSession, WslcSessionSettings, Flags, WarningCallback, WslcSession); |
| 614 | } |
| 615 | CATCH_RETURN(); |
| 616 | |
| 617 | HRESULT WSLCSessionManager::EnterSession(_In_ LPCWSTR DisplayName, _In_ LPCWSTR StoragePath, IWarningCallback* WarningCallback, IWSLCSession** WslcSession) |
| 618 | { |
| 619 | COMServiceExecutionContext context; |
| 620 | |
| 621 | return CallImpl(&WSLCSessionManagerImpl::EnterSession, DisplayName, StoragePath, WarningCallback, WslcSession); |
| 622 | } |
| 623 | |
| 624 | HRESULT WSLCSessionManager::ListSessions(_Out_ WSLCSessionListEntry** Sessions, _Out_ ULONG* SessionsCount) |
| 625 | { |
| 626 | COMServiceExecutionContext context; |
| 627 | |
| 628 | return CallImpl(&WSLCSessionManagerImpl::ListSessions, Sessions, SessionsCount); |
| 629 | } |
| 630 | |
| 631 | HRESULT WSLCSessionManager::OpenSession(_In_ ULONG Id, _Out_ IWSLCSession** Session) |
| 632 | { |
| 633 | COMServiceExecutionContext context; |
| 634 | |
| 635 | return CallImpl(&WSLCSessionManagerImpl::OpenSession, Id, Session); |
| 636 | } |
| 637 | |
| 638 | HRESULT WSLCSessionManager::OpenSessionByName(_In_ LPCWSTR DisplayName, _Out_ IWSLCSession** Session) |
| 639 | { |
| 640 | COMServiceExecutionContext context; |
| 641 | |
| 642 | return CallImpl(&WSLCSessionManagerImpl::OpenSessionByName, DisplayName, Session); |
| 643 | } |
| 644 | |
| 645 | HRESULT WSLCSessionManager::InterfaceSupportsErrorInfo(_In_ REFIID riid) |
| 646 | { |
| 647 | return riid == __uuidof(IWSLCSessionManager) ? S_OK : S_FALSE; |
| 648 | } |
| 649 | |
| 650 | HRESULT WSLCSessionManager::GetVersion(_Out_ WSLCCompatVersion* Version) |
| 651 | try |
| 652 | { |
| 653 | RETURN_HR_IF_NULL(E_POINTER, Version); |
| 654 | |
| 655 | WSLCVersion version{}; |
| 656 | RETURN_IF_FAILED(GetVersion(&version)); |
| 657 | |
| 658 | *Version = apicompat::Convert(version); |
| 659 | return S_OK; |
| 660 | } |
| 661 | CATCH_RETURN(); |
| 662 | |
| 663 | HRESULT WSLCSessionManager::CreateSession( |
| 664 | const WSLCCompatSessionSettings* Settings, WSLCSessionFlags Flags, IWSLCCompatWarningCallback* WarningCallback, IWSLCCompatSession** Session) |
| 665 | try |
| 666 | { |
| 667 | RETURN_HR_IF_NULL(E_POINTER, Session); |
| 668 | *Session = nullptr; |
| 669 | |
| 670 | const auto warning = apicompat::Convert(WarningCallback); |
| 671 | |
| 672 | Microsoft::WRL::ComPtr<IWSLCSession> session; |
| 673 | if (Settings == nullptr) |
| 674 | { |
| 675 | RETURN_IF_FAILED(CreateSession(static_cast<const WSLCSessionSettings*>(nullptr), Flags, warning.Get(), &session)); |
| 676 | } |
| 677 | else |
| 678 | { |
| 679 | const auto settings = apicompat::Convert(*Settings); |
| 680 | RETURN_IF_FAILED(CreateSession(settings.Get(), Flags, warning.Get(), &session)); |
| 681 | } |
| 682 | |
| 683 | RETURN_HR_IF_NULL(E_UNEXPECTED, session); |
| 684 | |
| 685 | return session.CopyTo(Session); |
| 686 | } |
| 687 | CATCH_RETURN(); |
| 688 | |
| 689 | namespace wsl::windows::service::wslc { |
| 690 | |
| 691 | WSLCSessionManagerImpl* WSLCSessionManagerImpl::Instance() noexcept |
| 692 | { |
| 693 | return g_managerInstance.load(); |
| 694 | } |
| 695 | |
| 696 | wil::com_ptr<IWSLCSession> WSLCSessionManagerImpl::FindSession(ULONG Id) |
| 697 | { |
| 698 | wil::com_ptr<IWSLCSession> result; |
| 699 | |
| 700 | ForEachSession<HRESULT>( |
| 701 | [&](SessionEntry& entry, const wil::com_ptr<IWSLCSession>& session) noexcept -> std::optional<HRESULT> { |
| 702 | if (entry.SessionId != Id) |
| 703 | { |
| 704 | return std::nullopt; |
| 705 | } |
| 706 | |
| 707 | result = session; |
| 708 | return S_OK; |
| 709 | }, |
| 710 | PluginManager::IsInWslcNotification()); |
| 711 | |
| 712 | THROW_HR_IF_MSG(WSLC_E_SESSION_NOT_FOUND, !result, "WSLC session %lu not found", Id); |
| 713 | return result; |
| 714 | } |
| 715 | |
| 716 | } // namespace wsl::windows::service::wslc |