Skip to content

spike: drive wslservice over COM instead of spawning wsl.exe - #379

Merged
zcsizmadia merged 1 commit into
mainfrom
spike/wslservice-com
Sep 17, 2026
Merged

zcsizmadia merged 1 commit into
mainfrom
spike/wslservice-com

Conversation

@zcsizmadia

Copy link
Copy Markdown
Collaborator

Refs #356 — the spike that issue asks for before any implementation.

Verdict: GO, with one hard requirement and one open risk

Measured on Windows 10 Pro 19045, WSL 2.9.11.0 (Store), standard non-admin user, three runs, first call discarded:

wsl.exe -l -v EnumerateDistributions
run 1 66.9 ms 0.81 ms
run 2 62.6 ms 0.74 ms
run 3 66.0 ms 0.66 ms

Roughly 85–90×. That is the difference between a status poll costing a process spawn and costing a function call — and the supervisor, status, doctor and the health check all pay it today.

The interface is read, not guessed

From wslservice.idl in the open-sourced WSL:

CLSID_LxssUserSession       a9b7a1b9-0671-405c-95f1-e0612cb4ce7e
IID_ILxssUserSession        38541BDC-F54F-4CEB-85D0-37F0F3D2617E

24 methods after IUnknown; seven map onto internal/wsl.WSL. Status has no equivalent and stays on the CLI. Struct layouts confirmed against the running service (LXSS_ENUMERATE_INFO = 544 bytes, LXSS_ERROR_INFO = 40).

Two findings the implementation depends on

1. No elevation, but impersonation is required. CoCreateInstance succeeds as a standard user; the first call then fails:

EnumerateDistributions -> 0x80070542    // Win32 1346, ERROR_BAD_IMPERSONATION_LEVEL

The service impersonates the caller to act on that user's distros. CoInitializeSecurity(..., RPC_C_IMP_LEVEL_IMPERSONATE, ...) once per process fixes it.

2. runtime.LockOSThread is mandatory — and its absence is intermittent. Without it, CoCreateInstance failed in 2 of 4 runs with CO_E_NOTINITIALIZED, because a COM apartment belongs to an OS thread and Go moves goroutines between threads freely. The failures clustered immediately after the process-spawn benchmark loop, which is exactly when the scheduler has reason to migrate.

Load-dependent, invisible in a quick test, and it would reach a user as an unexplained supervisor error. With the lock: 6 of 6 clean. This is the single most important thing for the implementation to get right, and I would not have found it without benchmarking in the same process.

What is deliberately NOT established

  • Only EnumerateDistributions was actually called. CreateLxProcess is the hard one — handles, pipes and a process lifecycle across an RPC boundary — and it is what Exec/Start need. Nothing here should be assumed about it.
  • One host, one WSL version. So the version gate and the doctor drift check internal/wsl: COM for the GUID-taking wslservice calls (Exec is out of scope) #356 calls for are not extras; they are the price of using this at all.
  • In-box WSL untested. LxssUserSessionInBox returned E_NOINTERFACE on this Store-WSL machine, so the two CLSIDs are not interchangeable and that flavour is a real gap.

Scope

Spike only — a throwaway module under spike/d/, kept compiling by the existing CI step, with no change to internal/wsl. The second Backend implementation is the follow-on work, and #356 stays open for it.

The spike #356 asks for before any implementation: establish which
operations have a COM equivalent, whether elevation is needed, and how
stable the surface is.

Verdict GO. Measured on Windows 10 Pro 19045, WSL 2.9.11.0 Store, standard
non-admin user, three runs with the first call discarded:

  wsl.exe -l -v            mean 62-67 ms per call
  EnumerateDistributions   mean 0.66-0.81 ms per call

Roughly 85-90x. That is the difference between a status poll costing a
process spawn and costing a function call.

The interface is read from wslservice.idl in the open-sourced WSL rather
than guessed: CLSID_LxssUserSession, IID_ILxssUserSession, and 24 methods
after IUnknown, seven of which map onto internal/wsl.WSL. Struct layouts
confirmed against the running service.

Two findings the implementation depends on.

No elevation is needed, but IMPERSONATION is: the first call returns
ERROR_BAD_IMPERSONATION_LEVEL until the client calls CoInitializeSecurity
with RPC_C_IMP_LEVEL_IMPERSONATE. The service impersonates the caller to
act on that user's distros.

runtime.LockOSThread is MANDATORY. Without it CoCreateInstance failed in 2
of 4 runs with CO_E_NOTINITIALIZED, because a COM apartment belongs to an
OS thread and Go moves goroutines between threads freely. The failures
clustered right after the process-spawn loop, which is when the scheduler
has reason to migrate. Load-dependent, invisible in a quick test, and it
would reach a user as an unexplained supervisor error. With the lock, 6 of
6 clean.

Deliberately not established, and the README says so: only
EnumerateDistributions was actually called -- CreateLxProcess carries
handles and a process lifecycle across the RPC boundary and must not be
assumed from this; one host and one WSL version, so the version gate and
doctor drift check in #356 are the price of using this at all; and the
in-box CLSID returned E_NOINTERFACE on this Store-WSL machine, so that
flavour is untested.

Refs #356
@zcsizmadia
zcsizmadia merged commit d37663b into main Sep 17, 2026
2 checks passed
@zcsizmadia
zcsizmadia deleted the spike/wslservice-com branch September 17, 2026 00:26
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.

1 participant