@samitouri / QOSAMI-WSL / commits / bcf84f6a

wslc: use a fresh job object per session to avoid intermittent ERROR_ACCESS_DENIED (#40763)

* wslc: use a fresh job object per session to avoid intermittent ERROR_ACCESS_DENIED The session manager assigned every per-session wslcsession.exe COM-server process to a single shared kill-on-close job object. Because the system places each newly launched process in its own job, once the shared job was assigned to one such process it could no longer accept a process belonging to an unrelated job, and subsequent AssignProcessToJobObject calls failed intermittently with ERROR_ACCESS_DENIED (most visible after hibernate/modern-standby resume). Create a fresh kill-on-close job per session and store the handle in the session's SessionEntry for the session's lifetime. Crash-cleanup behavior is unchanged: wslservice owns all the job handles, so if it exits or crashes every job closes and all session processes are terminated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: rename helper to CreateSessionProcessJob to reflect it creates/returns the job 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 Jun 10, 2026 at 11:13 UTC bcf84f6a2594ac242a34f44137ba7a1329343508
2 files changed +12 -22
src/windows/service/exe/WSLCSessionManager.cpp
+9 -15
@@ -271,9 +271,9 @@ void WSLCSessionManagerImpl::CreateSession(
271 // Create the VM in the SYSTEM service (privileged).
272 auto vm = Microsoft::WRL::Make<HcsVirtualMachine>(Settings);
273
274 - // Launch per-user COM server factory and add it to our job object for crash cleanup.
274 + // Launch per-user COM server factory and add it to a fresh per-session job object for crash cleanup.
275 auto factory = wslutil::CreateComServerAsUser<IWSLCSessionFactory>(__uuidof(WSLCSessionFactory), userToken.get());
276 - AddSessionProcessToJobObject(factory.get());
276 + wil::unique_handle sessionJob = CreateSessionProcessJob(factory.get());
277
278 const auto sessionSettings = CreateSessionSettings(sessionId, callerFileName.c_str(), Settings, resolvedDisplayName.c_str());
279 wil::com_ptr<IWSLCSession> session;
@@ -282,7 +282,7 @@ void WSLCSessionManagerImpl::CreateSession(
282
283 // Track the session via its service ref, along with metadata and security info.
284 m_sessions.push_back(SessionEntry{
285 - std::move(serviceRef), sessionId, creatorPid, resolvedDisplayName, std::move(tokenInfo), notifier, false, sharedToken, std::move(storedSid)});
285 + std::move(serviceRef), sessionId, creatorPid, resolvedDisplayName, std::move(tokenInfo), notifier, false, sharedToken, std::move(storedSid), std::move(sessionJob)});
286
287 // For persistent sessions, also hold a strong reference to keep them alive.
288 const bool persistent = WI_IsFlagSet(Flags, WSLCSessionFlagsPersistent);
@@ -445,24 +445,18 @@ WSLCSessionInitSettings WSLCSessionManagerImpl::CreateSessionSettings(
445 return sessionSettings;
446 }
447
448 -void WSLCSessionManagerImpl::AddSessionProcessToJobObject(_In_ IWSLCSessionFactory* Factory)
448 +wil::unique_handle WSLCSessionManagerImpl::CreateSessionProcessJob(_In_ IWSLCSessionFactory* Factory)
449 {
450 - EnsureJobObjectCreated();
450 + // Use a fresh job per session; reusing one fails intermittently with
451 + // ERROR_ACCESS_DENIED once it's assigned to a process the system put in another job.
452 + wil::unique_handle jobObject = wsl::windows::common::helpers::CreateKillOnCloseJob();
453
454 wil::unique_handle process;
455 THROW_IF_FAILED(Factory->GetProcessHandle(process.put()));
456
455 - THROW_IF_WIN32_BOOL_FALSE(AssignProcessToJobObject(m_sessionJobObject.get(), process.get()));
456 -}
457 + THROW_IF_WIN32_BOOL_FALSE(AssignProcessToJobObject(jobObject.get(), process.get()));
458
458 -void WSLCSessionManagerImpl::EnsureJobObjectCreated()
459 -{
460 - // Create a job object that will automatically terminate all child processes
461 - // when the job handle is closed (i.e., when wslservice exits or crashes).
462 - std::call_once(m_jobObjectInitFlag, [this] {
463 - m_sessionJobObject = wsl::windows::common::helpers::CreateKillOnCloseJob();
464 - WSL_LOG("SessionManagerJobObjectCreated", TraceLoggingLevel(WINEVENT_LEVEL_INFO));
465 - });
459 + return jobObject;
460 }
461
462 CallingProcessTokenInfo WSLCSessionManagerImpl::GetCallingProcessTokenInfo()
src/windows/service/exe/WSLCSessionManager.h
+3 -7
@@ -68,6 +68,8 @@ struct SessionEntry
68
69 wil::shared_handle UserToken;
70 std::vector<BYTE> UserSid;
71 +
72 + wil::unique_handle JobObject;
73 };
74
75 class WSLCSessionManagerImpl
@@ -163,10 +165,9 @@ private:
165 }
166 }
167
166 - void AddSessionProcessToJobObject(_In_ IWSLCSessionFactory* Factory);
168 + [[nodiscard]] wil::unique_handle CreateSessionProcessJob(_In_ IWSLCSessionFactory* Factory);
169 WSLCSessionInitSettings CreateSessionSettings(
170 _In_ ULONG SessionId, _In_ LPCWSTR CreatorProcessName, _In_ const WSLCSessionSettings* Settings, _In_ LPCWSTR ResolvedDisplayName);
169 - void EnsureJobObjectCreated();
171 static CallingProcessTokenInfo GetCallingProcessTokenInfo();
172 static HRESULT CheckTokenAccess(const SessionEntry& Entry, const CallingProcessTokenInfo& TokenInfo);
173
@@ -175,11 +176,6 @@ private:
176 std::atomic<ULONG> m_nextSessionId{1};
177 std::recursive_mutex m_wslcSessionsLock;
178
178 - // Job object that automatically terminates all child COM server processes
179 - // when this service exits or crashes (JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE).
180 - std::once_flag m_jobObjectInitFlag;
181 - wil::unique_handle m_sessionJobObject;
182 -
179 // All sessions tracked via SessionEntry (which holds weak refs and service-side security info).
180 // Sessions are automatically cleaned up when the underlying session is released.
181 std::vector<SessionEntry> m_sessions;