using System.Runtime.InteropServices; namespace WslcApiDump; /// /// D13's open question, reduced to one run (docs/WINDOWS_PORT.md §13.2). /// /// The plan routes container enumeration, session/container reattach and the Terminal panel's pty /// to the service-internal COM interface `IWSLCSessionManager` (IID 82A7ABC8-…). Reading /// `wslc.idl` turned up a problem the plan assumed away: **it declares interfaces and no /// activatable class.** There is no CLSID in it, so `CoCreateInstance` has nothing to name, and /// the only registered coclasses anywhere in WSL's IDLs belong to the *compat* surface and to the /// WSL service proper. /// /// The likely answer is that one of those coclasses also implements the internal interface — COM /// objects routinely expose several — and `wslc.exe` simply QIs for it. That is a yes/no question, /// and everything downstream depends on it: /// /// * **yes** → write the internal arm (`ListContainers`, `OpenSessionByName`, `OpenContainer`, /// `ResizeTty`), and a broker restart stops being fatal to a running sandbox. /// * **no** → D13 needs reopening. The realistic alternatives are a durable host-side roster /// plus `wsl --shutdown` on restart, or revisiting the rejected `wslc.exe` arm for enumeration. /// /// Two stages, because they carry very different risk: /// /// * --internal activates each candidate class and QIs for the internal IIDs. It never /// calls a method, so **it cannot crash on a vtable mismatch** — and QI alone answers the /// entry-point question. /// * --internal-call additionally calls through the interface (`GetVersion`, then /// `ListSessions`). This one CAN take the process down if the real vtable differs from the /// IDL — which is itself a finding, and the reason it is opt-in rather than default. /// internal static class InternalComProbe { // The only registered coclasses in WSL's IDLs. wslc.idl contributes none — that is the // problem — so every candidate here comes from WSLCCompat.idl or wslservice.idl. private static readonly (string Name, Guid Clsid, string Source)[] Candidates = [ ("WSLCCompatSessionManager", new Guid("a9b7a1b9-0671-405c-95f1-e0612cb4ce8f"), "WSLCCompat.idl — what the SDK itself activates, so the likeliest host"), ("WSLCCompatSessionManagerFactory", new Guid("9fcd2067-9fc6-4efa-9eb0-698169ebf7d3"), "WSLCCompat.idl"), ("LxssUserSession", new Guid("a9b7a1b9-0671-405c-95f1-e0612cb4ce7e"), "wslservice.idl — the WSL service proper; note it differs from the compat CLSID " + "only in the last byte (ce7e vs ce8f)"), ("LxssUserSessionInBox", new Guid("4f476546-b412-4579-b64c-123df331e3d6"), "wslservice.idl"), ]; private static readonly (string Name, Guid Iid, string Why)[] Interfaces = [ ("IWSLCSessionManager", new Guid("82A7ABC8-6B50-43FC-AB96-15FBBE7E8760"), "THE one that matters — OpenSessionByName/EnterSession/ListSessions, i.e. reattach"), ("IWSLCSession", new Guid("EF0661E4-6364-40EA-B433-E2FDF11F3519"), "ListContainers/OpenContainer — enumeration and the container half of reattach"), ("IWSLCVirtualMachine", new Guid("B5E2D8F1-9A3C-4E6B-8D1F-7C4A2E9B6D3A"), "GetId → the VM GUID an AF_HYPERV bind needs. Per the IDL only a factory the SYSTEM " + "service owns can produce one, so this is EXPECTED to fail — probed anyway, " + "because §13.2 retracted a decision on that reading and it deserves confirming"), ]; internal static void Run(bool callThrough) { Console.WriteLine(); Console.WriteLine("internal COM probe (docs/WINDOWS_PORT.md §13.2):"); Console.WriteLine(" wslc.idl declares no coclass, so the question is whether an EXISTING"); Console.WriteLine(" class answers a QI for the internal interfaces."); Console.WriteLine(); var initialized = CoInitializeEx(IntPtr.Zero, CoinitMultithreaded); // S_FALSE means already initialized on this thread; RPC_E_CHANGED_MODE means the runtime // picked the other apartment. Neither is fatal for an out-of-proc server. if (initialized < 0 && (uint)initialized != RpcEChangedMode) { Console.WriteLine($" CoInitializeEx failed: 0x{initialized:X8} — cannot probe"); return; } var bound = false; foreach (var (name, clsid, source) in Candidates) { Console.WriteLine($" {name} {{{clsid}}}"); Console.WriteLine($" ({source})"); var iunknown = typeof(object).GUID; // IID_IUnknown var hr = CoCreateInstance(in clsid, IntPtr.Zero, ClsctxAll, in iunknown, out var unknown); if (hr < 0) { Console.WriteLine($" activation failed: {Hresult(hr)}"); Console.WriteLine(); continue; } Console.WriteLine(" activated"); try { foreach (var (interfaceName, iid, why) in Interfaces) { var iidLocal = iid; var qi = Marshal.QueryInterface(unknown, in iidLocal, out var candidate); if (qi >= 0) { Console.WriteLine($" QI {interfaceName}: **YES**"); if (interfaceName == "IWSLCSessionManager") { bound = true; if (callThrough) CallThrough(candidate); } Marshal.Release(candidate); } else { Console.WriteLine($" QI {interfaceName}: no ({Hresult(qi)})"); foreach (var line in Wrap(why)) Console.WriteLine($" {line}"); } } } finally { Marshal.Release(unknown); } Console.WriteLine(); } Console.WriteLine(bound ? " RESULT: IWSLCSessionManager IS reachable. D13's internal arm has an entry point —\n" + " write it (ListContainers, OpenSessionByName, OpenContainer, ResizeTty)\n" + " and record the CLSID that answered in §13.2." : " RESULT: IWSLCSessionManager is NOT reachable from any known coclass. D13 needs\n" + " reopening — see §13.2 for the two alternatives. Re-run with\n" + " --internal-call only if a QI above succeeded; it adds nothing here."); if (!callThrough && bound) Console.WriteLine(" (pass --internal-call to also CALL through it — confirms the vtable " + "matches the IDL, and can crash if it does not)"); } /// /// Prove the vtable is really the IDL's, not just that the IID is recognised. A QI can succeed /// against an interface whose layout has since moved — `wslc.idl` says outright that breaking /// changes to it are fine — and the first thing that would tell us is a crash here rather /// than in the broker. /// private static void CallThrough(IntPtr manager) { Console.WriteLine(" calling through (vtable check):"); IWSLCSessionManager? proxy; try { proxy = (IWSLCSessionManager)Marshal.GetObjectForIUnknown(manager); } catch (Exception e) { Console.WriteLine($" could not build the RCW: {e.GetType().Name}: {e.Message}"); return; } try { var hr = proxy.GetVersion(out var version); Console.WriteLine(hr >= 0 ? $" GetVersion() = {version.Major}.{version.Minor}.{version.Revision} " + "— slot 3 matches the IDL" : $" GetVersion() failed: {Hresult(hr)}"); // Cross-check against the compat SDK's own GetVersion: the same service answering // both is what says these are two faces of one object rather than a coincidence. if (hr >= 0) Console.WriteLine(" compare with WslcService.GetVersion() above — they " + "should agree"); } catch (Exception e) { Console.WriteLine($" GetVersion() threw {e.GetType().Name}: {e.Message}"); return; } try { var hr = proxy.ListSessions(out var sessions, out var count); if (hr < 0) { Console.WriteLine($" ListSessions() failed: {Hresult(hr)}"); return; } Console.WriteLine($" ListSessions() = {count} session(s)"); for (var i = 0; i < count; i++) { var entry = Marshal.PtrToStructure( sessions + i * Marshal.SizeOf()); Console.WriteLine($" #{entry.SessionId} \"{entry.DisplayName}\" " + $"(creator pid {entry.CreatorPid})"); } // The callee allocated it; nobody else will free it. if (sessions != IntPtr.Zero) Marshal.FreeCoTaskMem(sessions); Console.WriteLine(" → enumeration and reattach are both reachable from here."); } catch (Exception e) { Console.WriteLine($" ListSessions() threw {e.GetType().Name}: {e.Message}"); } } private static IEnumerable Wrap(string text) { var words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries); var line = new System.Text.StringBuilder(); foreach (var word in words) { if (line.Length + word.Length + 1 > 88 && line.Length > 0) { yield return line.ToString(); line.Clear(); } if (line.Length > 0) line.Append(' '); line.Append(word); } if (line.Length > 0) yield return line.ToString(); } private static string Hresult(int code) => (uint)code switch { 0x80040154 => "REGDB_E_CLASSNOTREG — that class is not registered on this machine", 0x80004002 => "E_NOINTERFACE — the class does not implement it", 0x80070005 => "E_ACCESSDENIED — registered, but this token may not activate it", 0x800401F0 => "CO_E_NOTINITIALIZED", _ => $"0x{code:X8}", }; // MARK: - Interop private const uint ClsctxAll = 0x17; // INPROC_SERVER|HANDLER|LOCAL_SERVER|REMOTE_SERVER private const uint CoinitMultithreaded = 0; private const uint RpcEChangedMode = 0x80010106; [DllImport("ole32.dll")] private static extern int CoInitializeEx(IntPtr reserved, uint coInit); [DllImport("ole32.dll")] private static extern int CoCreateInstance( in Guid clsid, IntPtr outer, uint clsContext, in Guid iid, out IntPtr instance); [StructLayout(LayoutKind.Sequential)] private struct WslcVersion { public uint Major; public uint Minor; public uint Revision; } /// Matches `WSLCSessionListEntry` in wslc.idl: two 32-bit fields then two INLINE wide-char /// buffers (not pointers), which is why these are ByValTStr and why the sizes must be exact. [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct WslcSessionListEntry { public uint SessionId; public uint CreatorPid; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string DisplayName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 257)] public string Sid; } /// /// Declared only as far as ListSessions — but every preceding method must still be /// declared with the right parameter count, because a vtable is addressed by slot. The /// unused ones take IntPtr placeholders for their struct and interface pointers. /// PreserveSig throughout, so a failing HRESULT is a value to report rather than an /// exception to decode. /// [ComImport, Guid("82A7ABC8-6B50-43FC-AB96-15FBBE7E8760"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IWSLCSessionManager { [PreserveSig] int GetVersion(out WslcVersion version); [PreserveSig] int CreateSession(IntPtr settings, uint flags, IntPtr warningCallback, out IntPtr session); [PreserveSig] int EnterSession( [MarshalAs(UnmanagedType.LPWStr)] string displayName, [MarshalAs(UnmanagedType.LPWStr)] string storagePath, IntPtr warningCallback, out IntPtr session); [PreserveSig] int ListSessions(out IntPtr sessions, out uint count); [PreserveSig] int OpenSession(uint id, out IntPtr session); [PreserveSig] int OpenSessionByName( [MarshalAs(UnmanagedType.LPWStr)] string displayName, out IntPtr session); } }