Fix Session 0 support for WSLC SDK callers (e.g. NETWORK SERVICE)#40367
Fix Session 0 support for WSLC SDK callers (e.g. NETWORK SERVICE)#40367matthewGarton wants to merge 1 commit intomicrosoft:feature/wsl-for-appsfrom
Conversation
Running WslcCreateSession from a Windows service as NETWORK SERVICE in Session 0
exposed five distinct issues across COM security, RPC handle marshaling, named pipe
security, and COM proxy authentication. This commit fixes all five.
Scenario: A Windows service (e.g. Microsoft Connected Cache) calling WslcCreateSession
via wslcsdk.dll, running as NETWORK SERVICE in Session 0.
== FIXES INCLUDED ==
Issue 1: CoInitializeSecurity - Self-Relative vs Absolute SD
File: src/windows/common/wslutil.cpp
ConvertStringSecurityDescriptorToSecurityDescriptorW returns a self-relative SD,
but CoInitializeSecurity requires absolute format. Added MakeAbsoluteSD conversion
using HeapAlloc(HEAP_ZERO_MEMORY) pattern matching comservicehelper.h. The SDDL
O:BAG:BAD:(A;;0xB;;;SY)(A;;0xB;;;AU) grants COM execute/activate rights to SYSTEM
and Authenticated Users. Falls back to nullptr SD on failure.
Also replaced WSL_LOG with LOG_IF_FAILED_MSG since this function is called before
WslTraceLoggingInitialize in some binaries (e.g. wslrelay.exe), where WSL_LOG
would dereference the uninitialized g_hTraceLoggingProvider and crash.
Affects: wslcsession.exe, wslrelay.exe, wslhost.exe, wslc.exe, wsl.exe, wslg.exe
Issue 2: ConfigureNetworking IDL - system_handle Marshaling
Files: src/windows/service/inc/wslc.idl
src/windows/service/exe/HcsVirtualMachine.cpp
src/windows/service/exe/HcsVirtualMachine.h
src/windows/wslcsession/WSLCVirtualMachine.cpp
[in] system_handle requires the CLIENT to OpenProcess(PROCESS_DUP_HANDLE) on the
SERVER. NETWORK SERVICE cannot open wslservice.exe (SYSTEM) with that access.
Changed ConfigureNetworking parameters from system_handle HANDLE to ULONG_PTR.
The server now uses DuplicateHandleFromCallingProcess() (SYSTEM opening the client)
instead of the client duplicating to the server.
Issue 3: Named Pipe Security Descriptors
Files: src/windows/common/helpers.cpp
src/windows/common/helpers.hpp
src/windows/common/Dmesg.cpp
src/windows/service/exe/GuestTelemetryLogger.cpp
Named pipes for crash dump, telemetry, and debug console were created with nullptr
security attributes, inheriting the default DACL of the creating process. When
created by NETWORK SERVICE in Session 0, vmwp.exe (SYSTEM) could not connect.
Added CreatePipeSecurityDescriptor() returning D:P(A;;GA;;;SY)(A;;GA;;;AU) and
applied it to all named pipe creation sites.
Issue 4: COM Proxy Blankets for Session 0
File: src/windows/common/DeviceHostProxy.cpp
In Session 0, cross-process COM callbacks from vmwp.exe fail without dynamic
cloaking. Added SetProxyBlanketForSession0() calling CoSetProxyBlanket with
RPC_C_IMP_LEVEL_IMPERSONATE and EOAC_DYNAMIC_CLOAKING on all cross-process
COM proxy interfaces: IVmDeviceHost, IUnknown in RegisterDeviceHost,
m_deviceAccess, IVmFiovGuestMemoryFastNotification, IVmFiovGuestMmioMappings.
== OPEN ITEMS (not addressed in this commit) ==
VirtioProxy / VirtioFs under NETWORK SERVICE:
CreateComServerAsUser(UserToken) creates wsldevicehost.dll as the calling user.
When the user is NETWORK SERVICE, HCS device-host APIs (HdvProxyDeviceHost)
return E_ACCESSDENIED because the caller lacks HCS device-host privileges.
Interactive users work because they have sufficient HCS permissions.
Workaround: Use NAT networking mode (avoids VirtioProxy/VirtioFs code paths).
Long-term: Consider running Plan9FileSystem under SYSTEM for Session 0 callers,
granting NETWORK SERVICE HCS permissions, or exposing networking mode as an
SDK parameter.
VHD Volume Permissions for Non-Root Containers:
WSLCVhdVolume::Create formats VHDs as ext4 with root:root 0755. Containers
running as non-root UIDs cannot write to the volume root. Consider adding a
UID/GID parameter to WslcVhdRequirements, defaulting new volume roots to 0777,
or supporting a --user flag on volume creation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Fixes WslcCreateSession behavior for Session 0 service callers (e.g., NETWORK SERVICE) by addressing handle marshaling, pipe ACLs, COM security initialization, and COM proxy authentication behavior so that vmwp.exe/SYSTEM interactions succeed.
Changes:
- Change
ConfigureNetworkingto pass socket handles asULONG_PTRand duplicate them server-side from the calling process. - Add explicit named pipe security descriptors and apply them to multiple pipe creation sites.
- Harden Session 0 COM behavior via explicit
CoInitializeSecuritySD handling and proxy blanket configuration (dynamic cloaking + impersonation).
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/windows/wslcsession/WSLCVirtualMachine.cpp | Passes socket handle values as ULONG_PTR to the service for server-side duplication. |
| src/windows/service/inc/wslc.idl | Updates ConfigureNetworking signature to use ULONG_PTR instead of system_handle. |
| src/windows/service/exe/HcsVirtualMachine.h | Updates the service-side COM method signature to match the IDL changes. |
| src/windows/service/exe/HcsVirtualMachine.cpp | Duplicates socket handles from the caller process using the new raw-value approach. |
| src/windows/service/exe/GuestTelemetryLogger.cpp | Applies the new named-pipe security attributes when creating the telemetry pipe. |
| src/windows/common/wslutil.cpp | Adds explicit SD creation + absolute SD conversion for CoInitializeSecurity, with fallback logging. |
| src/windows/common/helpers.hpp | Declares CreatePipeSecurityDescriptor() helper. |
| src/windows/common/helpers.cpp | Implements CreatePipeSecurityDescriptor() and applies it to debug/relay pipe creation. |
| src/windows/common/Dmesg.cpp | Applies the new named-pipe security attributes when creating the dmesg pipe. |
| src/windows/common/DeviceHostProxy.cpp | Sets COM proxy blankets (dynamic cloaking) to make Session 0 callbacks work reliably. |
| // Socket handles are passed as opaque ULONG_PTR values (not system_handle) so the | ||
| // server (SYSTEM) can duplicate them from the client via DuplicateHandleFromCallingProcess. | ||
| // This avoids requiring the client to OpenProcess(PROCESS_DUP_HANDLE) on the server, | ||
| // which fails when the client is a non-SYSTEM service account (e.g. NETWORK SERVICE). | ||
| HRESULT ConfigureNetworking( |
There was a problem hiding this comment.
Changing an existing COM interface method’s parameter types is an on-the-wire breaking change: older/newer proxy-stub pairs (or separately versioned client/server binaries) can mis-marshal parameters and fail in hard-to-diagnose ways. If wslcsession.exe / wslservice.exe can ever be updated independently, prefer adding a new method (e.g. ConfigureNetworking2) or a new interface with a new IID (e.g. IWSLCVirtualMachine2) and keep the old signature for backward compatibility.
| // Socket handles are passed as opaque ULONG_PTR values (not system_handle) so the | |
| // server (SYSTEM) can duplicate them from the client via DuplicateHandleFromCallingProcess. | |
| // This avoids requiring the client to OpenProcess(PROCESS_DUP_HANDLE) on the server, | |
| // which fails when the client is a non-SYSTEM service account (e.g. NETWORK SERVICE). | |
| HRESULT ConfigureNetworking( | |
| // This legacy method preserves the original COM wire contract for compatibility. | |
| HRESULT ConfigureNetworking( | |
| [in, system_handle(sh_socket)] HANDLE GnsSocket, | |
| [in, unique, system_handle(sh_socket)] HANDLE* DnsSocket); | |
| // Configures networking engine with sockets from the user process. | |
| // GnsSocket is required; DnsSocket is optional (NULL if DNS tunneling is disabled). | |
| // Socket handles are passed as opaque ULONG_PTR values (not system_handle) so the | |
| // server (SYSTEM) can duplicate them from the client via DuplicateHandleFromCallingProcess. | |
| // This avoids requiring the client to OpenProcess(PROCESS_DUP_HANDLE) on the server, | |
| // which fails when the client is a non-SYSTEM service account (e.g. NETWORK SERVICE). | |
| HRESULT ConfigureNetworking2( |
|
|
||
| LOG_IF_FAILED_MSG(hrFallback, "CoInitializeSecurity fallback"); | ||
|
|
||
| THROW_IF_FAILED(hrFallback); |
There was a problem hiding this comment.
The fallback path treats RPC_E_TOO_LATE as a hard failure and will throw, while the explicit-SD path explicitly accepts RPC_E_TOO_LATE as non-fatal. This introduces inconsistent behavior and can regress scenarios where COM security was initialized earlier (especially in complex processes). Consider handling RPC_E_TOO_LATE in the fallback path the same way (log and return).
| THROW_IF_FAILED(hrFallback); | |
| THROW_HR_IF(hrFallback, FAILED(hrFallback) && (hrFallback != RPC_E_TOO_LATE)); |
| DWORD cbSDAbsolute = 0, cbDacl = 0, cbSacl = 0, cbOwner = 0, cbPrimaryGroup = 0; | ||
| PSECURITY_DESCRIPTOR pSDAbsolute = nullptr; | ||
| PACL pDacl = nullptr; | ||
| PACL pSacl = nullptr; | ||
| PSID pOwner = nullptr; | ||
| PSID pPrimaryGroup = nullptr; | ||
|
|
||
| auto cleanupAbsolute = wil::scope_exit([&] { | ||
| if (pSDAbsolute) HeapFree(GetProcessHeap(), 0, pSDAbsolute); | ||
| if (pDacl) HeapFree(GetProcessHeap(), 0, pDacl); | ||
| if (pSacl) HeapFree(GetProcessHeap(), 0, pSacl); | ||
| if (pOwner) HeapFree(GetProcessHeap(), 0, pOwner); | ||
| if (pPrimaryGroup) HeapFree(GetProcessHeap(), 0, pPrimaryGroup); | ||
| }); | ||
|
|
||
| if (MakeAbsoluteSD(pSDRelative, nullptr, &cbSDAbsolute, nullptr, &cbDacl, nullptr, &cbSacl, nullptr, &cbOwner, nullptr, &cbPrimaryGroup) || | ||
| GetLastError() != ERROR_INSUFFICIENT_BUFFER) | ||
| { | ||
| LOG_HR_MSG(HRESULT_FROM_WIN32(GetLastError()), "CoInitializeSecurity: MakeAbsoluteSD size query failed"); | ||
| goto fallback; | ||
| } | ||
|
|
||
| pSDAbsolute = reinterpret_cast<PSECURITY_DESCRIPTOR>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cbSDAbsolute)); | ||
| pDacl = reinterpret_cast<PACL>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cbDacl)); | ||
| pSacl = reinterpret_cast<PACL>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cbSacl)); | ||
| pOwner = reinterpret_cast<PSID>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cbOwner)); | ||
| pPrimaryGroup = reinterpret_cast<PSID>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cbPrimaryGroup)); | ||
|
|
||
| if (!pSDAbsolute || !pDacl || !pSacl || !pOwner || !pPrimaryGroup) | ||
| { | ||
| LOG_HR_MSG(E_OUTOFMEMORY, "CoInitializeSecurity: HeapAlloc failed"); | ||
| goto fallback; | ||
| } |
There was a problem hiding this comment.
The MakeAbsoluteSD size query can legitimately return cbSacl == 0 (and potentially other sizes as 0 depending on the input SD). Calling HeapAlloc(..., 0) is allowed to return nullptr, which would then be treated as OOM and force the fallback path unnecessarily. Consider only allocating buffers when the corresponding cb* is non-zero, and passing nullptr/0 to MakeAbsoluteSD for omitted components.
| // Create a security descriptor that grants full access to SYSTEM and Authenticated Users. | ||
| // This is required for named pipes created in Session 0 — the default DACL of service accounts | ||
| // (e.g. NETWORK SERVICE) may not allow SYSTEM processes (e.g. vmwp.exe) to connect. | ||
| wil::unique_hlocal_security_descriptor sd; | ||
| THROW_IF_WIN32_BOOL_FALSE( | ||
| ConvertStringSecurityDescriptorToSecurityDescriptorW(L"D:P(A;;GA;;;SY)(A;;GA;;;AU)", SDDL_REVISION_1, &sd, nullptr)); |
There was a problem hiding this comment.
This new pipe ACL grants GENERIC_ALL to all Authenticated Users (AU). That’s a significant expansion of who can connect to these pipes if the name can be discovered, and can increase the risk of unintended data injection or information disclosure. Consider tightening the DACL to the minimum needed (e.g., SY plus the specific service SID / owner / caller SID), and/or granting only the specific pipe access rights required rather than GA.
| // Create a security descriptor that grants full access to SYSTEM and Authenticated Users. | |
| // This is required for named pipes created in Session 0 — the default DACL of service accounts | |
| // (e.g. NETWORK SERVICE) may not allow SYSTEM processes (e.g. vmwp.exe) to connect. | |
| wil::unique_hlocal_security_descriptor sd; | |
| THROW_IF_WIN32_BOOL_FALSE( | |
| ConvertStringSecurityDescriptorToSecurityDescriptorW(L"D:P(A;;GA;;;SY)(A;;GA;;;AU)", SDDL_REVISION_1, &sd, nullptr)); | |
| // Create a security descriptor that grants full access only to SYSTEM. | |
| // This is required for named pipes created in Session 0 — the default DACL of service accounts | |
| // (e.g. NETWORK SERVICE) may not allow SYSTEM processes (e.g. vmwp.exe) to connect. | |
| wil::unique_hlocal_security_descriptor sd; | |
| THROW_IF_WIN32_BOOL_FALSE( | |
| ConvertStringSecurityDescriptorToSecurityDescriptorW(L"D:P(A;;GA;;;SY)", SDDL_REVISION_1, &sd, nullptr)); |
| SECURITY_ATTRIBUTES sa = {sizeof(sa), sd.get(), FALSE}; | ||
| wil::unique_hfile pipe(CreateNamedPipeW( | ||
| pipeName.c_str(), (PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED), (PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT), 1, LX_RELAY_BUFFER_SIZE, LX_RELAY_BUFFER_SIZE, 0, nullptr)); | ||
| pipeName.c_str(), (PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED), (PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT), 1, LX_RELAY_BUFFER_SIZE, LX_RELAY_BUFFER_SIZE, 0, &sa)); |
There was a problem hiding this comment.
These named pipes appear intended for local-only IPC. Consider adding PIPE_REJECT_REMOTE_CLIENTS to the pipe mode flags (third parameter) to prevent remote connections over SMB/remote named pipe access. This mitigates exposure if the pipe name leaks and is generally a low-cost hardening step.
| pipeName.c_str(), (PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED), (PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT), 1, LX_RELAY_BUFFER_SIZE, LX_RELAY_BUFFER_SIZE, 0, &sa)); | |
| pipeName.c_str(), | |
| (PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED), | |
| (PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS), | |
| 1, | |
| LX_RELAY_BUFFER_SIZE, | |
| LX_RELAY_BUFFER_SIZE, | |
| 0, | |
| &sa)); |
|
The |
Summary of the Pull Request
Running WslcCreateSession from a Windows service as NETWORK SERVICE in Session 0 exposed five distinct issues across COM security, RPC handle marshaling, named pipe security, and COM proxy authentication. This commit fixes all five.
Scenario: A Windows service (e.g. Microsoft Connected Cache) calling WslcCreateSession via wslcsdk.dll, running as NETWORK SERVICE in Session 0.
== FIXES INCLUDED ==
Issue 1: CoInitializeSecurity - Self-Relative vs Absolute SD
File: src/windows/common/wslutil.cpp
ConvertStringSecurityDescriptorToSecurityDescriptorW returns a self-relative SD,
but CoInitializeSecurity requires absolute format. Added MakeAbsoluteSD conversion
using HeapAlloc(HEAP_ZERO_MEMORY) pattern matching comservicehelper.h. The SDDL
O:BAG:BAD:(A;;0xB;;;SY)(A;;0xB;;;AU) grants COM execute/activate rights to SYSTEM
and Authenticated Users. Falls back to nullptr SD on failure.
Also replaced WSL_LOG with LOG_IF_FAILED_MSG since this function is called before
WslTraceLoggingInitialize in some binaries (e.g. wslrelay.exe), where WSL_LOG
would dereference the uninitialized g_hTraceLoggingProvider and crash.
Affects: wslcsession.exe, wslrelay.exe, wslhost.exe, wslc.exe, wsl.exe, wslg.exe
Issue 2: ConfigureNetworking IDL - system_handle Marshaling
Files: src/windows/service/inc/wslc.idl
src/windows/service/exe/HcsVirtualMachine.cpp
src/windows/service/exe/HcsVirtualMachine.h
src/windows/wslcsession/WSLCVirtualMachine.cpp
[in] system_handle requires the CLIENT to OpenProcess(PROCESS_DUP_HANDLE) on the
SERVER. NETWORK SERVICE cannot open wslservice.exe (SYSTEM) with that access.
Changed ConfigureNetworking parameters from system_handle HANDLE to ULONG_PTR.
The server now uses DuplicateHandleFromCallingProcess() (SYSTEM opening the client)
instead of the client duplicating to the server.
Issue 3: Named Pipe Security Descriptors
Files: src/windows/common/helpers.cpp
src/windows/common/helpers.hpp
src/windows/common/Dmesg.cpp
src/windows/service/exe/GuestTelemetryLogger.cpp
Named pipes for crash dump, telemetry, and debug console were created with nullptr
security attributes, inheriting the default DACL of the creating process. When
created by NETWORK SERVICE in Session 0, vmwp.exe (SYSTEM) could not connect.
Added CreatePipeSecurityDescriptor() returning D:P(A;;GA;;;SY)(A;;GA;;;AU) and
applied it to all named pipe creation sites.
Issue 4: COM Proxy Blankets for Session 0
File: src/windows/common/DeviceHostProxy.cpp
In Session 0, cross-process COM callbacks from vmwp.exe fail without dynamic
cloaking. Added SetProxyBlanketForSession0() calling CoSetProxyBlanket with
RPC_C_IMP_LEVEL_IMPERSONATE and EOAC_DYNAMIC_CLOAKING on all cross-process
COM proxy interfaces: IVmDeviceHost, IUnknown in RegisterDeviceHost,
m_deviceAccess, IVmFiovGuestMemoryFastNotification, IVmFiovGuestMmioMappings.
== OPEN ITEMS (not addressed in this commit) ==
VirtioProxy / VirtioFs under NETWORK SERVICE:
CreateComServerAsUser(UserToken) creates wsldevicehost.dll as the calling user.
When the user is NETWORK SERVICE, HCS device-host APIs (HdvProxyDeviceHost)
return E_ACCESSDENIED because the caller lacks HCS device-host privileges.
Interactive users work because they have sufficient HCS permissions.
Workaround: Use NAT networking mode (avoids VirtioProxy/VirtioFs code paths).
Long-term: Consider running Plan9FileSystem under SYSTEM for Session 0 callers,
granting NETWORK SERVICE HCS permissions, or exposing networking mode as an
SDK parameter.
VHD Volume Permissions for Non-Root Containers:
WSLCVhdVolume::Create formats VHDs as ext4 with root:root 0755. Containers
running as non-root UIDs cannot write to the volume root. Consider adding a
UID/GID parameter to WslcVhdRequirements, defaulting new volume roots to 0777,
or supporting a --user flag on volume creation.
PR Checklist
Detailed Description of the Pull Request / Additional comments
Validation Steps Performed