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. EXPECTED to fail here, and its failure is not a gap: a " + "session is RETURNED BY the manager (OpenSessionByName/EnterSession/CreateSession), " + "not QI'd off it. Probed only to confirm the manager is a manager and not a " + "do-everything object"), ("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, string? sessionName = null) { InternalComProbe.sessionName = sessionName; 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 hr = CoCreateInstance(in clsid, IntPtr.Zero, ClsctxAll, in IidIUnknown, out var unknown); if (hr < 0) { Console.WriteLine($" activation failed: {Hresult(hr)}"); if ((uint)hr == ENoInterface) // Every COM object implements IUnknown, so this combination is impossible // unless the IID being passed is not IID_IUnknown. Say so loudly: the first // version of this probe did exactly that and reported a confident false // negative that read like a real finding about WSL. Console.WriteLine(" !! E_NOINTERFACE on an IUnknown activation is IMPOSSIBLE — " + "every COM object implements IUnknown. This probe is passing a wrong IID; " + "treat the whole run as void and fix it, do NOT record a finding."); // A class factory is allowed to refuse IUnknown-first activation, so a failure // here is not yet an answer: ask for each internal interface directly before // concluding anything. foreach (var (interfaceName, iid, _) in Interfaces) { var direct = iid; var dhr = CoCreateInstance(in clsid, IntPtr.Zero, ClsctxAll, in direct, out var instance); Console.WriteLine($" direct activation as {interfaceName}: " + (dhr >= 0 ? "**YES**" : Hresult(dhr))); if (dhr < 0) continue; if (interfaceName == "IWSLCSessionManager") { bound = true; if (callThrough) CallThrough(instance); } Marshal.Release(instance); } 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); if (count == 0) // Worth saying, because "it returned S_OK" is weaker evidence than it looks: with // no entries, the WSLCSessionListEntry layout — two DWORDs then two INLINE // wchar_t buffers — was never actually unmarshalled. ListContainers uses the same // `size_is(, *Count)` double-pointer shape, so that layout is still unproven. Console.WriteLine(" (0 entries, so the ENTRY STRUCT layout is still " + "untested — run with a session up: --session --keep --internal-call)"); } catch (Exception e) { Console.WriteLine($" ListSessions() threw {e.GetType().Name}: {e.Message}"); return; } ProbeSessionHandoff(proxy); } /// /// The question that decides the SHAPE of D13's internal arm. /// /// `IWSLCSession` (internal, `EF0661E4-…`) and `IWSLCCompatSession` (SDK-facing, /// `DD7B2EF9-…`) are different interfaces with different IIDs. `WSLCCompatSessionManager` /// already proved one object can wear both faces — so if the session `OpenSessionByName` /// returns ALSO answers a QI for the compat interface, then a re-adopted session can be /// handed to `Session.FromAbi(ptr)` and driven by the **existing** facade code. /// /// * **YES** → internal COM is used only to *find* things. Reattach costs one QI, and /// nothing else in `WslcFacade` changes. /// * **NO** → every re-adopted handle is internal-only, and start/stop/delete/exec each need /// a second, hand-marshalled implementation. That is a much bigger arm, and it is the point /// at which the C++/WinRT shim §13.1 mused about earns its keep. /// private static void ProbeSessionHandoff(IWSLCSessionManager proxy) { var name = sessionName; if (string.IsNullOrEmpty(name)) { Console.WriteLine(" (no --session-name given; skipping the reattach handoff test)"); return; } Console.WriteLine(); Console.WriteLine($" reattach handoff test — OpenSessionByName(\"{name}\"):"); int hr; IntPtr session; try { hr = proxy.OpenSessionByName(name, out session); } catch (Exception e) { Console.WriteLine($" threw {e.GetType().Name}: {e.Message}"); return; } if (hr < 0) { Console.WriteLine($" failed: {Hresult(hr)}"); Console.WriteLine(" (expected if no session of that name is running — create " + "one first: --session --keep --internal-call)"); return; } Console.WriteLine(" opened — the session half of reattach WORKS"); try { var compat = IidCompatSession; var qi = Marshal.QueryInterface(session, in compat, out var projected); if (qi >= 0) { Console.WriteLine(" QI IWSLCCompatSession: **YES**"); Console.WriteLine(" → a re-adopted session can be handed to " + "Session.FromAbi() and driven by the EXISTING compat facade code."); Console.WriteLine(" → D13's internal arm shrinks to: find, then hand off."); Marshal.Release(projected); } else { Console.WriteLine($" QI IWSLCCompatSession: no ({Hresult(qi)})"); Console.WriteLine(" → a re-adopted session is internal-only, so every " + "operation on it needs a second hand-marshalled path."); Console.WriteLine(" → that is the case where a C++/WinRT shim is worth it."); } } finally { Marshal.Release(session); } } private static string? sessionName; 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 /// /// `IID_IUnknown`, spelled out. **Do not** reach for `typeof(object).GUID` here — that is the /// CLR's type GUID for `System.Object`, not `IID_IUnknown`, and activating with it asks every /// class factory for an interface nothing implements. The failure is maximally misleading: /// each registered class answers `E_NOINTERFACE` *at activation*, which reads exactly like /// "this class does not expose the interface you wanted" and is really "you asked for /// gibberish". That mistake produced a false negative on this very probe. /// private static readonly Guid IidIUnknown = new("00000000-0000-0000-C000-000000000046"); /// `IWSLCCompatSession` from WSLCCompat.idl — the SDK-facing session interface, and the one /// the C#/WinRT `Session` projection wraps. Distinct from the internal `IWSLCSession` /// (`EF0661E4-…`); whether one object answers both is the reattach-handoff question. private static readonly Guid IidCompatSession = new("DD7B2EF9-AA01-4F21-8A3A-29D394CBB579"); private const uint ClsctxAll = 0x17; // INPROC_SERVER|HANDLER|LOCAL_SERVER|REMOTE_SERVER private const uint CoinitMultithreaded = 0; private const uint RpcEChangedMode = 0x80010106; private const uint ENoInterface = 0x80004002; [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); } }