Skip to content

internal/wsl: COM for the GUID-taking wslservice calls (Exec is out of scope) #356

Description

@zcsizmadia

Every WSL operation skrog performs is a wsl.exe spawn. internal/wsl exists precisely to keep that behind one interface, which makes a second implementation cheap to try: talk to wslservice over COM directly.

Why it is worth doing

  • Process spawn per call. The supervisor, status, doctor and the health check all pay it. wslkit's tray polling issue (tray: polling hawser status every 4s costs ~7% of a core, for a status dot #192, ~7% of a core for a status dot) is the same cost in the sibling project.
  • Console flash. cmd/skrogw exists largely to avoid a console window appearing. A COM client has no console to suppress.
  • Errors arrive as HRESULTs, not stderr. Today failure modes are recovered by string-matching wsl.exe output, which is localised, UTF-16, and reworded between releases. doctor would gain a real error code to map through the errors.json table wslkit already generates (Spike B: session-0 / no-login WSL2 from a Windows service (GO/NO-GO) #3).
  • It is readable now. WSL is open source, so the service's COM surface can be established from the source rather than guessed at. internal/winpath: Windows to WSL path translation #17 already worked out which CLSID belongs to which runtime flavour — that is the starting point, not a blank page.

Scope

Not the plugin API. #325 covers WSLCCreateProcess / WSLCProcessGetFd for the wslc root namespace, runs in-process inside wslservice as SYSTEM, and is blocked on Authenticode signing. This issue is the ordinary distro backend, as a COM client — no HKLM registration, no signing, no ability to crash the service.

To establish first

  1. Which operations have a COM equivalent: launch a distro, query running state, terminate, --shutdown, enumerate registrations. The health check and supervisor start/stop are the paths that matter; anything without an equivalent keeps the CLI implementation.
  2. Whether any of it needs elevation. If it does for a given call, that call stays on the CLI.
  3. How stable the interface is across WSL releases. wslc.idl explicitly disclaims stability (wslc backend: session lifecycle — one persistent named session, idle termination, 3.3 s cold boot, ~750 MB per VM #323); assume the same here until shown otherwise.

Risk, and the mitigation that has to ship with it

wsl --update can move this surface silently — the same failure mode already documented for the wslc backend's engine version. So:

  • Version-gate the COM implementation; fall back to the wsl.exe implementation when the running WSL version is outside the tested range.
  • A doctor check that detects the surface having shifted, rather than letting it fail as an unexplained supervisor error on someone else's machine after a Tuesday update.
  • The CLI implementation stays as the reference and keeps its tests. This is an optimisation with a fallback, not a replacement.

Deliverables

Found while discussing what the open-sourced WSL source makes newly possible; filed alongside #93.

Activity

  1. zcsizmadia commented on Sep 17, 2026

    @zcsizmadia
    CollaboratorAuthor

    Spike done and merged as #379 (spike/d/). Verdict: GO.

    Answering the three "to establish first" questions:

    1. Which operations have a COM equivalent

    All but one of the ones that matter. From wslservice.idl — ILxssUserSession, 24 methods after IUnknown:

    internal/wsl.WSL COM method vtable slot
    List EnumerateDistributions 15
    Terminate TerminateDistribution 7
    Unregister UnregisterDistribution 8
    Import RegisterDistribution 4
    Export ExportDistribution 18
    Exec / Start CreateLxProcess 16
    wsl --shutdown Shutdown 23
    Status none — stays on the CLI —

    2. Does it need elevation

    No. CoCreateInstance succeeds as a standard user. But it does need impersonation: the first call returns 0x80070542 (Win32 1346, 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.

    3. How stable is it

    Unknown, and the spike does not pretend otherwise — one host, one WSL version (2.9.11.0 Store). But one concrete instability already showed up: LxssUserSessionInBox returned E_NOINTERFACE on this machine. The two CLSIDs are not interchangeable, so capability detection has to pick, and in-box WSL is untested.

    So the version gate and the doctor drift check in this issue are not extras — they are the price of using this at all.

    The benchmark

    Three runs, first call discarded, Windows 10 Pro 19045:

    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

    ~85–90×.

    One finding that is not in the issue, and matters more than the speed

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

    Load-dependent, invisible in a quick test, and on a user's machine it would present as an unexplained supervisor error. With the lock: 6 of 6 clean. It only surfaced because the CLI benchmark ran in the same process as the COM calls.

    Left for the implementation

    • CreateLxProcess is untouched. It carries handles, pipes and a process lifecycle across the RPC boundary, and it is what Exec/Start need. Nothing in this spike should be taken as evidence about it — it is the hard half.
    • In-box WSL flavour.
    • The version gate, the doctor drift check, and keeping the CLI implementation as the reference with its tests.
  2. zcsizmadia commented on Sep 18, 2026

    @zcsizmadia
    CollaboratorAuthor

    #408 landed the tractable part and closed the question the issue was really asking.

    Exec is out of scope, with a reason. Reading CreateLxProcess in wslservice.idl: 24 parameters, four returned sockets (stdin, stdout, stderr, CommunicationChannel), a separate InteropSocket, a process handle and a server handle. Driving it means reimplementing the relay and channel protocol wsl.exe already implements, against an interface whose stability Microsoft disclaims, for calls that are not on a hot loop. Recorded in the Fast doc comment.

    What landed: GetDistributionId (0.54 ms) and TerminateDistribution. Terminate goes ~99 ms → 10-16 ms, but ~15 ms of that is the service genuinely stopping the distro, so the saving is ~89 ms of process spawn and nothing more. Marginal as latency — it is not a hot path.

    The durable value is GetDistributionId: every GUID-taking method needs it, so #381, #382 and #383 now have their lookup.

    A bug worth recording, caught only by the live test: a service-level error ("no such distro") was being read as "the COM surface has moved", which demoted COM for the whole process and re-ran the doomed call through wsl.exe. ServiceError now separates the two.

    Suggest narrowing or closing this. The Exec/Start ambition is settled as "no"; the remaining GUID-taking surface will be pulled in by #381/#382/#383 as each needs it, which is a better unit of work than a standing "port things to COM" ticket.

  3. changed the title [-]internal/wsl: talk to wslservice over COM instead of spawning wsl.exe for every call[/-] [+]internal/wsl: COM for the GUID-taking wslservice calls (Exec is out of scope)[/+] on Sep 18, 2026
  4. zcsizmadia commented on Sep 18, 2026

    @zcsizmadia
    CollaboratorAuthor

    Retitled to match what this actually is now. \Exec/\Start\ are settled as out of scope with a reason recorded in the \Fast\ doc comment (\CreateLxProcess: 24 parameters, four returned sockets, an undocumented \CommunicationChannel), so the title promising a general port was misleading.

    What remains under it is the rest of the GUID-taking surface, now that \GetDistributionId\ exists at 0.54 ms. Given #381 is parked on a design decision and #382/#383 are closed as non-viable, there is currently no caller waiting on more of it — so this is a fine thing to leave open and pull from when one appears, rather than work to schedule.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions