Skip to content

Fix Session 0 support for WSLC SDK callers (e.g. NETWORK SERVICE)#40367

Open
matthewGarton wants to merge 1 commit intomicrosoft:feature/wsl-for-appsfrom
matthewGarton:user/magarton/session0-fixes
Open

Fix Session 0 support for WSLC SDK callers (e.g. NETWORK SERVICE)#40367
matthewGarton wants to merge 1 commit intomicrosoft:feature/wsl-for-appsfrom
matthewGarton:user/magarton/session0-fixes

Conversation

@matthewGarton
Copy link
Copy Markdown

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

  • Closes: Link to issue #xxx
  • Communication: I've discussed this with core contributors already. If work hasn't been agreed, this work might be rejected
  • Tests: Added/updated if needed and all pass
  • Localization: All end user facing strings can be localized
  • Dev docs: Added/updated if needed
  • Documentation updated: If checked, please file a pull request on our docs repo and link it here: #xxx

Detailed Description of the Pull Request / Additional comments

Validation Steps Performed

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>
Copilot AI review requested due to automatic review settings April 30, 2026 17:28
@matthewGarton matthewGarton requested a review from a team as a code owner April 30, 2026 17:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ConfigureNetworking to pass socket handles as ULONG_PTR and 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 CoInitializeSecurity SD 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.

Comment on lines +421 to 425
// 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(
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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(

Copilot uses AI. Check for mistakes.

LOG_IF_FAILED_MSG(hrFallback, "CoInitializeSecurity fallback");

THROW_IF_FAILED(hrFallback);
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
THROW_IF_FAILED(hrFallback);
THROW_HR_IF(hrFallback, FAILED(hrFallback) && (hrFallback != RPC_E_TOO_LATE));

Copilot uses AI. Check for mistakes.
Comment on lines +333 to +365
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;
}
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +247 to +252
// 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));
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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));

Copilot uses AI. Check for mistakes.
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));
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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));

Copilot uses AI. Check for mistakes.
@benhillis benhillis changed the base branch from feature/wsl-for-apps to master April 30, 2026 21:03
@benhillis benhillis changed the base branch from master to feature/wsl-for-apps April 30, 2026 21:03
@benhillis
Copy link
Copy Markdown
Member

The feature/wsl-for-apps branch is now defunct. If your PR is still valid, please retarget the master branch. Reach out to @benhillis for help if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants