Add job objects to terminate child processes on VM shutdown (#40607)

* Add job objects to terminate child processes on VM shutdown Add JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE job objects to ensure that child processes spawned by the service (wsldevicehost, wslhost.exe, wslrelay.exe) are terminated when the VM shuts down. This prevents lingering processes from holding DLLs locked during package upgrades. Changes: - DeviceHostProxy: Create a job object and assign the COM device host process to it in RegisterDeviceHost. - WslCoreVm: Create a per-VM job object and pass it to LaunchDebugConsole, LaunchKdRelay, LaunchPortRelay, and WslCoreInstance. - SubProcess: Add SetJobObject() which uses PROC_THREAD_ATTRIBUTE_JOB_LIST to assign the process to the job at creation time. - helpers: Add optional JobObject parameter to Launch* functions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: add job object for child processes spawned by wslcsession Follow-up to the per-VM job object work: WSLCVirtualMachine::LaunchPortRelay spawns wslrelay.exe directly via SubProcess. Without a per-VM job object, if wslcsession.exe crashes or is force-killed before signaling m_vmTerminatingEvent, the spawned wslrelay.exe is orphaned and keeps wslrelay.exe locked. Create a JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE job in WSLCVirtualMachine::Initialize and pass it to LaunchPortRelay via SubProcess::SetJobObject, matching the pattern used in WslCoreVm. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix formatting * wslc: reorder job object before port relay pipes Member destruction order is reverse of declaration order. Declare m_processJobObject before m_portRelayChannelRead/Write so the pipes are destroyed first and the job object (which kills wslrelay.exe) is destroyed last. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Factor out CreateKillOnCloseJob() helper Replace four near-identical CreateJobObject + SetInformationJobObject blocks with a single helpers::CreateKillOnCloseJob() call: - WslCoreVm - DeviceHostProxy - WSLCVirtualMachine - WSLCSessionManager Addresses code review feedback from JohnMcPMS. 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 May 21, 2026 at 16:29 UTC 35f65666c55b3e3622de3ff89d7b985e3369463a
13 files changed +133 -34
src/windows/common/DeviceHostProxy.cpp
+13
@@ -27,6 +27,10 @@ DeviceHostProxy::DeviceHostProxy(const std::wstring& VmId, const GUID& RuntimeId
27 {
28 m_devicesShutdown = false;
29 m_git = wil::CoCreateInstance<IGlobalInterfaceTable>(CLSID_StdGlobalInterfaceTable, CLSCTX_INPROC_SERVER);
30 +
31 + // Create a job object that will terminate device host processes when this proxy is destroyed
32 + // (i.e., when the VM shuts down).
33 + m_jobObject = wsl::windows::common::helpers::CreateKillOnCloseJob();
34 }
35
36 GUID DeviceHostProxy::AddNewDevice(const GUID& Type, const wil::com_ptr<IPlan9FileSystem>& Plan9Fs, const std::wstring& VirtIoTag)
@@ -152,6 +156,15 @@ try
156 const wil::com_ptr<IVmDeviceHost> remoteHost = DeviceHost;
157 const wil::com_ptr<IUnknown> unknown = remoteHost.query<IUnknown>();
158 THROW_IF_FAILED(proxyDeviceHost(m_system.get(), unknown.get(), ProcessId, IpcSectionHandle));
159 +
160 + // Add the device host process to the job object so it is terminated when the VM shuts down.
161 + wil::unique_handle process(OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, FALSE, ProcessId));
162 + LOG_LAST_ERROR_IF_MSG(!process, "Failed to open device host process %u for job assignment", ProcessId);
163 + if (process)
164 + {
165 + LOG_IF_WIN32_BOOL_FALSE(AssignProcessToJobObject(m_jobObject.get(), process.get()));
166 + }
167 +
168 return S_OK;
169 }
170 CATCH_RETURN()
src/windows/common/DeviceHostProxy.h
+2
@@ -114,6 +114,8 @@ private:
114 std::map<GUID, DeviceHostProxyEntry, wsl::windows::common::helpers::GuidLess> m_devices;
115 bool m_devicesShutdown;
116
117 + wil::unique_handle m_jobObject;
118 +
119 static constexpr LPCWSTR c_hdvModuleName = L"vmdevicehost.dll";
120 static constexpr LPCWSTR c_vmwpctrlModuleName = L"vmwpctrl.dll";
121 };
\ No newline at end of file
src/windows/common/SubProcess.cpp
+17
@@ -75,6 +75,11 @@ void SubProcess::SetShowWindow(WORD ShowWindow)
75 m_showWindow = ShowWindow;
76 }
77
78 +void SubProcess::SetJobObject(HANDLE JobObject)
79 +{
80 + m_jobObject = JobObject;
81 +}
82 +
83 wsl::windows::common::helpers::unique_proc_attribute_list SubProcess::BuildProcessAttributes()
84 {
85 DWORD attributes = 0;
@@ -93,6 +98,11 @@ wsl::windows::common::helpers::unique_proc_attribute_list SubProcess::BuildProce
98 attributes++;
99 }
100
101 + if (m_jobObject != nullptr)
102 + {
103 + attributes++;
104 + }
105 +
106 if (attributes == 0)
107 {
108 return {};
@@ -123,6 +133,13 @@ wsl::windows::common::helpers::unique_proc_attribute_list SubProcess::BuildProce
133 list.get(), 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, m_pseudoConsole, sizeof(m_pseudoConsole), nullptr, nullptr));
134 }
135
136 + // Job object
137 + if (m_jobObject != nullptr)
138 + {
139 + THROW_IF_WIN32_BOOL_FALSE(UpdateProcThreadAttribute(
140 + list.get(), 0, PROC_THREAD_ATTRIBUTE_JOB_LIST, &m_jobObject, sizeof(m_jobObject), nullptr, nullptr));
141 + }
142 +
143 return list;
144 }
145
src/windows/common/SubProcess.h
+2
@@ -40,6 +40,7 @@ public:
40 void SetToken(HANDLE Token);
41 void SetShowWindow(WORD Show);
42 void SetFlags(DWORD Flag);
43 + void SetJobObject(HANDLE JobObject);
44
45 wil::unique_handle Start();
46 DWORD Run(DWORD Timeout = INFINITE);
@@ -64,6 +65,7 @@ private:
65 HANDLE m_stdOut = nullptr;
66 HANDLE m_stdErr = nullptr;
67 HPCON m_pseudoConsole = nullptr;
68 + HANDLE m_jobObject = nullptr;
69 std::optional<DWORD> m_desktopAppPolicy;
70 std::optional<WORD> m_showWindow;
71 std::vector<HANDLE> m_inheritHandles;
src/windows/common/helpers.cpp
+44 -13
@@ -97,7 +97,8 @@ public:
97 }
98 };
99
100 - [[nodiscard]] wil::unique_handle Launch(_In_opt_ HANDLE UserToken, _In_ bool HideWindow, _In_ bool CreateNoWindow = false) const
100 + [[nodiscard]] wil::unique_handle Launch(
101 + _In_opt_ HANDLE UserToken, _In_ bool HideWindow, _In_ bool CreateNoWindow = false, _In_opt_ HANDLE JobObject = nullptr) const
102 {
103 // If a user token was provided, create an environment block from the token.
104 wsl::windows::common::helpers::unique_environment_block environmentBlock{nullptr};
@@ -125,6 +126,7 @@ public:
126
127 process.SetEnvironment(environmentBlock.get());
128 process.SetToken(UserToken);
129 + process.SetJobObject(JobObject);
130
131 // Launch the process.
132 return process.Start();
@@ -137,7 +139,13 @@ private:
139 };
140
141 [[nodiscard]] wil::unique_handle LaunchWslHost(
140 - _In_opt_ LPCGUID DistroId, _In_opt_ HANDLE InteropHandle, _In_opt_ HANDLE EventHandle, _In_opt_ HANDLE ParentHandle, _In_opt_ LPCGUID VmId, _In_opt_ HANDLE UserToken)
142 + _In_opt_ LPCGUID DistroId,
143 + _In_opt_ HANDLE InteropHandle,
144 + _In_opt_ HANDLE EventHandle,
145 + _In_opt_ HANDLE ParentHandle,
146 + _In_opt_ LPCGUID VmId,
147 + _In_opt_ HANDLE UserToken,
148 + _In_opt_ HANDLE JobObject = nullptr)
149 {
150 // Construct the command line.
151 //
@@ -151,7 +159,7 @@ private:
159 launcher.AddHandleOption(wslhost::handle_option, InteropHandle);
160 launcher.AddHandleOption(wslhost::event_option, EventHandle);
161 launcher.AddHandleOption(wslhost::parent_option, ParentHandle);
154 - return launcher.Launch(UserToken, true);
162 + return launcher.Launch(UserToken, true, false, JobObject);
163 }
164
165 [[nodiscard]] wil::unique_handle LaunchWslRelay(
@@ -162,7 +170,8 @@ private:
170 _In_opt_ std::optional<int> Port,
171 _In_opt_ HANDLE ExitEvent,
172 _In_opt_ HANDLE UserToken,
165 - _In_ LaunchWslRelayFlags Flags)
173 + _In_ LaunchWslRelayFlags Flags,
174 + _In_opt_ HANDLE JobObject = nullptr)
175 {
176 // Construct the command line.
177 //
@@ -191,7 +200,7 @@ private:
200 launcher.AddOption(wslrelay::connect_pipe_option);
201 }
202
194 - return launcher.Launch(UserToken, WI_IsFlagSet(Flags, LaunchWslRelayFlags::HideWindow));
203 + return launcher.Launch(UserToken, WI_IsFlagSet(Flags, LaunchWslRelayFlags::HideWindow), false, JobObject);
204 }
205 } // namespace
206
@@ -274,6 +283,18 @@ wsl::windows::common::helpers::unique_proc_attribute_list wsl::windows::common::
283 return List;
284 }
285
286 +wil::unique_handle wsl::windows::common::helpers::CreateKillOnCloseJob()
287 +{
288 + wil::unique_handle job{CreateJobObjectW(nullptr, nullptr)};
289 + THROW_LAST_ERROR_IF(!job);
290 +
291 + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo{};
292 + jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
293 + THROW_IF_WIN32_BOOL_FALSE(SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo)));
294 +
295 + return job;
296 +}
297 +
298 std::vector<gsl::byte> wsl::windows::common::helpers::GenerateConfigurationMessage(
299 _In_ const std::wstring& DistributionName,
300 _In_ ULONG FixedDrivesBitmap,
@@ -548,7 +569,7 @@ bool wsl::windows::common::helpers::IsWslSupportInterfacePresent()
569 }
570
571 void wsl::windows::common::helpers::LaunchDebugConsole(
551 - _In_ LPCWSTR PipeName, _In_ bool ConnectExistingPipe, _In_ HANDLE UserToken, _In_opt_ HANDLE LogFile, _In_ bool DisableTelemetry)
572 + _In_ LPCWSTR PipeName, _In_ bool ConnectExistingPipe, _In_ HANDLE UserToken, _In_opt_ HANDLE LogFile, _In_ bool DisableTelemetry, _In_opt_ HANDLE JobObject)
573 {
574 LaunchWslRelayFlags flags{};
575 wil::unique_hfile pipe;
@@ -576,16 +597,24 @@ void wsl::windows::common::helpers::LaunchDebugConsole(
597 THROW_LAST_ERROR_IF(!pipe);
598
599 WI_SetFlagIf(flags, LaunchWslRelayFlags::DisableTelemetry, DisableTelemetry);
579 - wil::unique_handle info{LaunchWslRelay(wslrelay::RelayMode::DebugConsole, LogFile, nullptr, pipe.get(), {}, nullptr, UserToken, flags)};
600 + wil::unique_handle info{
601 + LaunchWslRelay(wslrelay::RelayMode::DebugConsole, LogFile, nullptr, pipe.get(), {}, nullptr, UserToken, flags, JobObject)};
602 }
603
604 [[nodiscard]] wil::unique_handle wsl::windows::common::helpers::LaunchInteropServer(
583 - _In_opt_ LPCGUID DistroId, _In_ HANDLE InteropHandle, _In_opt_ HANDLE EventHandle, _In_opt_ HANDLE ParentHandle, _In_opt_ LPCGUID VmId, _In_opt_ HANDLE UserToken)
605 + _In_opt_ LPCGUID DistroId,
606 + _In_ HANDLE InteropHandle,
607 + _In_opt_ HANDLE EventHandle,
608 + _In_opt_ HANDLE ParentHandle,
609 + _In_opt_ LPCGUID VmId,
610 + _In_opt_ HANDLE UserToken,
611 + _In_opt_ HANDLE JobObject)
612 {
585 - return LaunchWslHost(DistroId, InteropHandle, EventHandle, ParentHandle, VmId, UserToken);
613 + return LaunchWslHost(DistroId, InteropHandle, EventHandle, ParentHandle, VmId, UserToken, JobObject);
614 }
615
588 -void wsl::windows::common::helpers::LaunchKdRelay(_In_ LPCWSTR PipeName, _In_ HANDLE UserToken, _In_ int Port, _In_ HANDLE ExitEvent, _In_ bool DisableTelemetry)
616 +void wsl::windows::common::helpers::LaunchKdRelay(
617 + _In_ LPCWSTR PipeName, _In_ HANDLE UserToken, _In_ int Port, _In_ HANDLE ExitEvent, _In_ bool DisableTelemetry, _In_opt_ HANDLE JobObject)
618 {
619 // Create a new pipe server. The pipe should be:
620 // Bi-directional: PIPE_ACCESS_DUPLEX
@@ -599,15 +628,17 @@ void wsl::windows::common::helpers::LaunchKdRelay(_In_ LPCWSTR PipeName, _In_ HA
628
629 LaunchWslRelayFlags flags = LaunchWslRelayFlags::ConnectPipe;
630 WI_SetFlagIf(flags, LaunchWslRelayFlags::DisableTelemetry, DisableTelemetry);
602 - wil::unique_handle info{LaunchWslRelay(wslrelay::RelayMode::KdRelay, nullptr, nullptr, pipe.get(), Port, ExitEvent, UserToken, flags)};
631 + wil::unique_handle info{
632 + LaunchWslRelay(wslrelay::RelayMode::KdRelay, nullptr, nullptr, pipe.get(), Port, ExitEvent, UserToken, flags, JobObject)};
633 }
634
605 -void wsl::windows::common::helpers::LaunchPortRelay(_In_ SOCKET Socket, _In_ const GUID& VmId, _In_ HANDLE UserToken, _In_ bool DisableTelemetry)
635 +void wsl::windows::common::helpers::LaunchPortRelay(
636 + _In_ SOCKET Socket, _In_ const GUID& VmId, _In_ HANDLE UserToken, _In_ bool DisableTelemetry, _In_opt_ HANDLE JobObject)
637 {
638 LaunchWslRelayFlags flags{};
639 WI_SetFlagIf(flags, LaunchWslRelayFlags::DisableTelemetry, DisableTelemetry);
640 wil::unique_handle info{LaunchWslRelay(
610 - wslrelay::RelayMode::PortRelay, reinterpret_cast<HANDLE>(Socket), &VmId, nullptr, {}, nullptr, UserToken, flags)};
641 + wslrelay::RelayMode::PortRelay, reinterpret_cast<HANDLE>(Socket), &VmId, nullptr, {}, nullptr, UserToken, flags, JobObject)};
642 }
643
644 void wsl::windows::common::helpers::LaunchWslSettingsOOBE(_In_ HANDLE UserToken)
src/windows/common/helpers.hpp
+8 -4
@@ -116,6 +116,8 @@ std::wstring_view ConsumeArgument(_In_ std::wstring_view CommandLine, _In_ std::
116
117 void CreateConsole(_In_ LPCWSTR ConsoleTitle = nullptr);
118
119 +[[nodiscard]] wil::unique_handle CreateKillOnCloseJob();
120 +
121 unique_proc_attribute_list CreateProcThreadAttributeList(_In_ DWORD AttributeCount);
122
123 std::vector<gsl::byte> GenerateConfigurationMessage(
@@ -165,7 +167,8 @@ bool IsWslOptionalComponentPresent();
167
168 bool IsWslSupportInterfacePresent();
169
168 -void LaunchDebugConsole(_In_ LPCWSTR PipeName, _In_ bool ConnectExistingPipe, _In_ HANDLE UserToken, _In_opt_ HANDLE LogFile, _In_ bool DisableTelemetry);
170 +void LaunchDebugConsole(
171 + _In_ LPCWSTR PipeName, _In_ bool ConnectExistingPipe, _In_ HANDLE UserToken, _In_opt_ HANDLE LogFile, _In_ bool DisableTelemetry, _In_opt_ HANDLE JobObject = nullptr);
172
173 [[nodiscard]] wil::unique_handle LaunchInteropServer(
174 _In_opt_ LPCGUID DistroId,
@@ -173,11 +176,12 @@ void LaunchDebugConsole(_In_ LPCWSTR PipeName, _In_ bool ConnectExistingPipe, _I
176 _In_opt_ HANDLE EventHandle,
177 _In_opt_ HANDLE ParentHandle,
178 _In_opt_ LPCGUID VmId,
176 - _In_opt_ HANDLE UserToken = nullptr);
179 + _In_opt_ HANDLE UserToken = nullptr,
180 + _In_opt_ HANDLE JobObject = nullptr);
181
178 -void LaunchKdRelay(_In_ LPCWSTR PipeName, _In_ HANDLE UserToken, _In_ int Port, _In_ HANDLE ExitEvent, _In_ bool DisableTelemetry);
182 +void LaunchKdRelay(_In_ LPCWSTR PipeName, _In_ HANDLE UserToken, _In_ int Port, _In_ HANDLE ExitEvent, _In_ bool DisableTelemetry, _In_opt_ HANDLE JobObject = nullptr);
183
180 -void LaunchPortRelay(_In_ SOCKET Socket, _In_ const GUID& VmId, _In_ HANDLE UserToken, _In_ bool DisableTelemetry);
184 +void LaunchPortRelay(_In_ SOCKET Socket, _In_ const GUID& VmId, _In_ HANDLE UserToken, _In_ bool DisableTelemetry, _In_opt_ HANDLE JobObject = nullptr);
185
186 void LaunchWslSettingsOOBE(_In_ HANDLE UserToken);
187
src/windows/service/exe/WSLCSessionManager.cpp
+2 -8
@@ -34,6 +34,7 @@ Abstract:
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
@@ -444,14 +445,7 @@ void WSLCSessionManagerImpl::EnsureJobObjectCreated()
445 // Create a job object that will automatically terminate all child processes
446 // when the job handle is closed (i.e., when wslservice exits or crashes).
447 std::call_once(m_jobObjectInitFlag, [this] {
447 - m_sessionJobObject.reset(CreateJobObjectW(nullptr, nullptr));
448 - THROW_LAST_ERROR_IF(!m_sessionJobObject);
449 -
450 - JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo{};
451 - jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
452 - THROW_IF_WIN32_BOOL_FALSE(
453 - SetInformationJobObject(m_sessionJobObject.get(), JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo)));
454 -
448 + m_sessionJobObject = wsl::windows::common::helpers::CreateKillOnCloseJob();
449 WSL_LOG("SessionManagerJobObjectCreated", TraceLoggingLevel(WINEVENT_LEVEL_INFO));
450 });
451 }
src/windows/service/exe/WslCoreInstance.cpp
+8 -4
@@ -28,7 +28,8 @@ WslCoreInstance::WslCoreInstance(
28 _In_ ULONG FeatureFlags,
29 _In_ DWORD SocketTimeout,
30 _In_ int IdleTimeout,
31 - _Out_opt_ ULONG* ConnectPort) :
31 + _Out_opt_ ULONG* ConnectPort,
32 + _In_opt_ HANDLE JobObject) :
33 LxssRunningInstance(IdleTimeout),
34 m_featureFlags(FeatureFlags),
35 m_instanceId(InstanceId),
@@ -38,7 +39,8 @@ WslCoreInstance::WslCoreInstance(
39 m_initializeDrvFs(DrvFsCallback),
40 m_ntClientLifetimeId(ClientLifetimeId),
41 m_redirectorConnectionTargets{m_configuration.Name},
41 - m_socketTimeout(SocketTimeout)
42 + m_socketTimeout(SocketTimeout),
43 + m_jobObject(JobObject)
44 {
45 // Establish a communication channel with the init daemon.
46 m_initChannel = std::make_shared<WslCorePort>(InitSocket.release(), m_runtimeId, m_socketTimeout);
@@ -125,7 +127,9 @@ WslCoreInstance::WslCoreInstance(
127 DrvFsCallback,
128 systemDistroFeatureFlags,
129 m_socketTimeout,
128 - IdleTimeout);
130 + IdleTimeout,
131 + nullptr,
132 + JobObject);
133 }
134 CATCH_LOG()
135 }
@@ -419,7 +423,7 @@ void WslCoreInstance::Initialize()
423 {
424 const wil::unique_socket socket{wsl::windows::common::hvsocket::Connect(m_runtimeId, response.InteropPort)};
425 wil::unique_handle info{wsl::windows::common::helpers::LaunchInteropServer(
422 - nullptr, reinterpret_cast<HANDLE>(socket.get()), nullptr, nullptr, &m_runtimeId, m_userToken.get())};
426 + nullptr, reinterpret_cast<HANDLE>(socket.get()), nullptr, nullptr, &m_runtimeId, m_userToken.get(), m_jobObject)};
427 }
428 CATCH_LOG()
429 }
src/windows/service/exe/WslCoreInstance.h
+3 -1
@@ -67,7 +67,8 @@ public:
67 _In_ ULONG FeatureFlags,
68 _In_ DWORD SocketTimeout,
69 _In_ int IdleTimeout,
70 - _Out_opt_ ULONG* ConnectPort = nullptr);
70 + _Out_opt_ ULONG* ConnectPort = nullptr,
71 + _In_opt_ HANDLE JobObject = nullptr);
72
73 virtual ~WslCoreInstance();
74
@@ -136,6 +137,7 @@ private:
137 std::shared_ptr<WslCoreInstance> m_systemDistro;
138 WSLDistributionInformation m_distributionInfo{};
139 DWORD m_socketTimeout{};
140 + HANDLE m_jobObject{};
141 std::thread m_oobeThread;
142 wil::unique_event m_destroyingEvent{wil::EventOptions::ManualReset};
143 wil::unique_event m_oobeCompleteEvent;
src/windows/service/exe/WslCoreVm.cpp
+19 -4
@@ -78,6 +78,9 @@ RequiredExtraMmioSpaceForPmemFileInMb(_In_ PCWSTR FilePath)
78 WslCoreVm::WslCoreVm(_In_ wsl::core::Config&& VmConfig) :
79 m_vmConfig(std::move(VmConfig)), m_traceClient(m_vmConfig.EnableTelemetry)
80 {
81 + // Create a job object that will terminate child processes (wslhost.exe, wslrelay.exe)
82 + // when the VM is destroyed.
83 + m_processJobObject = wsl::windows::common::helpers::CreateKillOnCloseJob();
84 }
85
86 std::unique_ptr<WslCoreVm> WslCoreVm::Create(_In_ const wil::shared_handle& UserToken, _In_ wsl::core::Config&& VmConfig, _In_ const GUID& VmId)
@@ -309,7 +312,12 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken
312 }
313
314 wsl::windows::common::helpers::LaunchDebugConsole(
312 - m_comPipe0.c_str(), !!m_dmesgCollector, m_restrictedToken.get(), logFile ? logFile.get() : nullptr, !m_vmConfig.EnableTelemetry);
315 + m_comPipe0.c_str(),
316 + !!m_dmesgCollector,
317 + m_restrictedToken.get(),
318 + logFile ? logFile.get() : nullptr,
319 + !m_vmConfig.EnableTelemetry,
320 + m_processJobObject.get());
321 }
322 CATCH_LOG()
323 }
@@ -1236,7 +1244,8 @@ std::shared_ptr<LxssRunningInstance> WslCoreVm::CreateInstanceInternal(
1244 featureFlags,
1245 m_vmConfig.DistributionStartTimeout,
1246 m_vmConfig.InstanceIdleTimeout,
1239 - ConnectPort);
1247 + ConnectPort,
1248 + m_processJobObject.get());
1249
1250 WI_ASSERT(!initSocket && !systemDistroSocket);
1251
@@ -1638,7 +1647,12 @@ std::wstring WslCoreVm::GenerateConfigJson()
1647
1648 m_comPipe1 = wsl::windows::common::helpers::GetUniquePipeName();
1649 wsl::windows::common::helpers::LaunchKdRelay(
1641 - m_comPipe1.c_str(), m_restrictedToken.get(), m_vmConfig.KernelDebugPort, m_terminatingEvent.get(), !m_vmConfig.EnableTelemetry);
1650 + m_comPipe1.c_str(),
1651 + m_restrictedToken.get(),
1652 + m_vmConfig.KernelDebugPort,
1653 + m_terminatingEvent.get(),
1654 + !m_vmConfig.EnableTelemetry,
1655 + m_processJobObject.get());
1656 }
1657 else
1658 {
@@ -1857,7 +1871,8 @@ void WslCoreVm::InitializeGuest()
1871 // N.B. The relay process is launched at medium integrity level, and its lifetime is tied to the lifetime of the utility VM.
1872 const auto result = wil::ResultFromException(WI_DIAGNOSTICS_INFO, [&]() {
1873 const auto socket = AcceptConnection(m_vmConfig.KernelBootTimeout);
1860 - wsl::windows::common::helpers::LaunchPortRelay(socket.get(), m_runtimeId, m_restrictedToken.get(), !m_vmConfig.EnableTelemetry);
1874 + wsl::windows::common::helpers::LaunchPortRelay(
1875 + socket.get(), m_runtimeId, m_restrictedToken.get(), !m_vmConfig.EnableTelemetry, m_processJobObject.get());
1876 });
1877
1878 if (FAILED(result))
src/windows/service/exe/WslCoreVm.h
+4
@@ -319,6 +319,10 @@ private:
319 _Guarded_by_(m_persistentMemoryLock) ULONG m_nextPersistentMemoryId = 0;
320
321 std::unique_ptr<wsl::core::INetworkingEngine> m_networkingEngine;
322 +
323 + // Job object that terminates child processes (wslhost.exe, wslrelay.exe)
324 + // when the VM shuts down.
325 + wil::unique_handle m_processJobObject;
326 };
327
328 DEFINE_ENUM_FLAG_OPERATORS(WslCoreVm::DiskStateFlags);
src/windows/wslcsession/WSLCVirtualMachine.cpp
+5
@@ -263,6 +263,10 @@ void WSLCVirtualMachine::Initialize()
263 {
264 THROW_IF_FAILED(m_vm->GetId(&m_vmId));
265
266 + // Create a job object that will terminate child processes (wslrelay.exe)
267 + // when the VM is destroyed.
268 + m_processJobObject = wsl::windows::common::helpers::CreateKillOnCloseJob();
269 +
270 // Start crash dump collection thread.
271 auto crashDumpSocket = hvsocket::Listen(m_vmId, LX_INIT_UTILITY_VM_CRASH_DUMP_PORT);
272 THROW_LAST_ERROR_IF(!crashDumpSocket);
@@ -870,6 +874,7 @@ void WSLCVirtualMachine::LaunchPortRelay()
874 wsl::windows::common::SubProcess process{nullptr, cmd.c_str()};
875 process.SetStdHandles(readPipe.get(), writePipe.get(), nullptr);
876 process.InheritHandle(m_vmTerminatingEvent.get());
877 + process.SetJobObject(m_processJobObject.get());
878 process.Start();
879
880 readPipe.release();
src/windows/wslcsession/WSLCVirtualMachine.h
+6
@@ -230,6 +230,12 @@ private:
230 wsl::shared::SocketChannel m_initChannel;
231 DWORD m_initChannelTimeout = 30 * 1000;
232
233 + // Job object that terminates child processes (wslrelay.exe) when the VM shuts down.
234 + // Declared before the port relay pipes so it is destroyed after them: any remaining
235 + // wslrelay.exe is given the chance to exit via the closed pipes / signaled terminating
236 + // event before the job-close kill kicks in.
237 + wil::unique_handle m_processJobObject;
238 +
239 wil::unique_handle m_portRelayChannelRead;
240 wil::unique_handle m_portRelayChannelWrite;
241