#if USE_WSLC using System.Runtime.InteropServices; namespace NucleicBroker.Wslc; /// /// D13's internal-COM arm, **Tier 1: recover** (docs/WINDOWS_PORT.md §13.2). /// /// The compat SDK cannot re-adopt a running session — its `Session` constructor is lazy and /// `Start()` refuses an existing name with `ERROR_ALREADY_EXISTS`. So a broker that restarts /// while its session is up cannot reach the sandbox again, and today that costs the user a manual /// `wsl --shutdown`. This class fixes exactly that: it opens the orphaned session through the /// **service-internal** COM interface, reports what was running, terminates it, and lets the /// facade create a fresh one through the ordinary compat path. /// /// It deliberately does NOT try to keep those containers alive — that is Tier 2, and it is /// blocked (§13.2): `Session.FromAbi()` throws on a service-side pointer, because the WinRT layer /// the C# projection wraps lives client-side in `wslcsdk.dll`. Everything here is confirmed on /// hardware; nothing here depends on that unresolved question. /// /// **Four things that are not obvious and each cost a debugging round:** /// /// 1. **There is no CLSID for `IWSLCSessionManager`.** `wslc.idl` declares interfaces and no /// activatable class. The entry point is the *compat* coclass — `WSLCCompatSessionManager` /// also implements the internal interface. One object, two faces. /// 2. **The proxy must grant IMPERSONATE.** Per-user calls like `OpenSessionByName` fail /// `0x80070542` (`ERROR_BAD_IMPERSONATION_LEVEL`) under .NET's default `IDENTIFY` — a security /// error that reads exactly like "not found". `GetVersion`/`ListSessions` don't impersonate, /// so they succeed and make it look like a per-method gap. /// handles this process-wide; the per-proxy blanket here is belt and braces. /// 3. **Vtable slots are fixed by declaration ORDER, not signature.** Only the methods actually /// called need accurate signatures, which is what makes reaching `ListContainers` (method #19, /// behind four methods taking a by-value `WSLCHandle` union) tractable at all. /// 4. **The ABI is explicitly unstable.** `wslc.idl` says breaking changes are fine because /// Microsoft ships both ends. We are not both ends, so every entry point here is probed and /// every failure degrades to "no recovery" rather than propagating. /// internal sealed class WslcInternal : IDisposable { private IWSLCSessionManager? manager; private IntPtr managerPtr; /// What a recovery found and did, for the host log and the `session.down` story. internal sealed record Recovery(IReadOnlyList Containers, bool Terminated); /// /// Bind the internal interface, or return null. Called once at facade construction so the /// result can be reported in the `capabilities` hello (§2.3) rather than discovered when a /// user's broker restarts. /// internal static WslcInternal? TryBind() { var clsid = ClsidWslcCompatSessionManager; var iid = IidWslcSessionManager; // Ask for the internal interface directly. The compat coclass implements both, and going // straight for it means a machine where this arm is unavailable fails here rather than // half-way through a recovery. var hr = CoCreateInstance(in clsid, IntPtr.Zero, ClsctxAll, in iid, out var ptr); if (hr < 0) { // REGDB_E_CLASSNOTREG here means WSL simply isn't installed — the §8 onboarding // state, not a defect. Saying "recovery unavailable" without that distinction reads // as a broker fault on a machine that has not been set up yet. Console.Error.WriteLine((uint)hr == RegdbEClassNotReg ? "wslc: WSL is not installed — internal COM absent, as expected before onboarding" : $"wslc: internal COM did not bind (0x{hr:X8}) — a broker restart will not " + "auto-recover a running session; D13 Tier 1 is unavailable on this machine"); return null; } RaiseImpersonation(ptr); try { return new WslcInternal { managerPtr = ptr, manager = (IWSLCSessionManager)Marshal.GetObjectForIUnknown(ptr), }; } catch (Exception e) { Marshal.Release(ptr); Console.Error.WriteLine($"wslc: internal COM bound but unusable: {e.Message}"); return null; } } /// /// Open the orphaned session named , note what was running in it, and /// terminate it. Returns null when there is nothing to recover — which is the ordinary case /// and not an error. /// /// Terminating rather than adopting is the deliberate Tier 1 choice: the containers are lost, /// but they are lost today too, and `ContainerManager.reconcile` already copes with a sandbox /// that came back empty. What it buys is that the *next* `Start()` succeeds. /// internal Recovery? RecoverSession(string name) { if (manager is null) return null; int hr; IntPtr sessionPtr; try { hr = manager.OpenSessionByName(name, out sessionPtr); } catch (Exception e) { Console.Error.WriteLine($"wslc: OpenSessionByName('{name}') threw: {e.Message}"); return null; } if (hr < 0) { Console.Error.WriteLine($"wslc: no recoverable session '{name}' (0x{hr:X8})" + ((uint)hr == ErrorBadImpersonationLevel ? " — IMPERSONATE was not granted; CoInitializeSecurity must run before the " + "first COM call in the process" : "")); return null; } // The session proxy is a separate object from the manager, so it needs its own blanket. RaiseImpersonation(sessionPtr); try { var session = (IWSLCSession)Marshal.GetObjectForIUnknown(sessionPtr); var containers = ListContainers(session); var terminated = Terminate(session); Console.Error.WriteLine( $"wslc: recovered orphaned session '{name}' — {containers.Count} container(s) " + $"[{string.Join(", ", containers)}], terminated={terminated}"); return new Recovery(containers, terminated); } catch (Exception e) { Console.Error.WriteLine($"wslc: recovery of '{name}' failed: {e.Message}"); return null; } finally { Marshal.Release(sessionPtr); } } /// /// The container roster of a session, by name. Enumeration the compat SDK has no call for at /// all — Session exposes no listing and Container carries no Name. /// private static IReadOnlyList ListContainers(IWSLCSession session) { // Flags=All, or the listing is running-containers-only and a stopped container silently // vanishes from the recovery report. var options = new WslcListContainersOptions { Flags = WslcListContainersFlagsAll, Limit = 0, Filters = IntPtr.Zero, FiltersCount = 0, }; var optionsPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf()); var containers = IntPtr.Zero; var ports = IntPtr.Zero; try { Marshal.StructureToPtr(options, optionsPtr, fDeleteOld: false); var hr = session.ListContainers(optionsPtr, out containers, out var count, out ports, out _); if (hr < 0) { Console.Error.WriteLine($"wslc: ListContainers failed (0x{hr:X8})"); return []; } var size = Marshal.SizeOf(); var names = new List((int)count); for (var i = 0; i < count; i++) { var entry = Marshal.PtrToStructure(containers + i * size); names.Add(string.IsNullOrEmpty(entry.Name) ? entry.Id : entry.Name); } return names; } finally { Marshal.FreeCoTaskMem(optionsPtr); // Both out-arrays are callee-allocated; nobody else frees them. if (containers != IntPtr.Zero) Marshal.FreeCoTaskMem(containers); if (ports != IntPtr.Zero) Marshal.FreeCoTaskMem(ports); } } private static bool Terminate(IWSLCSession session) { try { var hr = session.Terminate(); if (hr >= 0) return true; Console.Error.WriteLine($"wslc: session Terminate failed (0x{hr:X8})"); return false; } catch (Exception e) { Console.Error.WriteLine($"wslc: session Terminate threw: {e.Message}"); return false; } } public void Dispose() { manager = null; if (managerPtr != IntPtr.Zero) { Marshal.Release(managerPtr); managerPtr = IntPtr.Zero; } } // MARK: - COM security /// /// Grant servers the right to impersonate this process, for **every** proxy it will hold. /// /// Must run before the first COM call in the process or it fails `RPC_E_TOO_LATE` — and the /// compat SDK makes COM calls of its own, so this has to precede any `WslcService`/`Session` /// use, not merely precede the internal arm. Failing is not fatal: only the per-user internal /// calls need it, so the sandbox still runs and recovery is what degrades. /// internal static void InitializeSecurity() { var hr = CoInitializeSecurity( IntPtr.Zero, -1, IntPtr.Zero, IntPtr.Zero, RpcCAuthnLevelDefault, RpcCImpLevelImpersonate, IntPtr.Zero, EoacNone, IntPtr.Zero); // RPC_E_TOO_LATE means something already initialised security — worth saying, because it // silently removes session recovery and nothing else will mention it. if (hr < 0) Console.Error.WriteLine( $"wslc: CoInitializeSecurity failed (0x{hr:X8})" + ((uint)hr == RpcETooLate ? " — RPC_E_TOO_LATE: a COM call ran first. Session recovery will fail " + "0x80070542." : "")); } private static void RaiseImpersonation(IntPtr proxy) => // Per-proxy, and harmless if CoInitializeSecurity already covered it. Kept because the // process-wide call is order-dependent and this one is not. CoSetProxyBlanket( proxy, RpcCAuthnDefault, RpcCAuthzDefault, ColeDefaultPrincipal, RpcCAuthnLevelDefault, RpcCImpLevelImpersonate, ColeDefaultAuthinfo, EoacNone); // MARK: - Interop /// `WSLCCompatSessionManager` from WSLCCompat.idl. Not a typo that this is the *compat* /// class: `wslc.idl` declares no coclass, and this one answers a QI for the internal /// interface (confirmed on hardware, §13.2). private static readonly Guid ClsidWslcCompatSessionManager = new("a9b7a1b9-0671-405c-95f1-e0612cb4ce8f"); private static readonly Guid IidWslcSessionManager = new("82A7ABC8-6B50-43FC-AB96-15FBBE7E8760"); private const uint ClsctxAll = 0x17; private const uint RpcCAuthnDefault = 0xFFFFFFFF; private const uint RpcCAuthzDefault = 0xFFFFFFFF; private const uint RpcCAuthnLevelDefault = 0; private const uint RpcCImpLevelImpersonate = 3; private const uint EoacNone = 0; private const uint RpcETooLate = 0x80010119; private const uint RegdbEClassNotReg = 0x80040154; private const uint ErrorBadImpersonationLevel = 0x80070542; private const uint WslcListContainersFlagsAll = 1; private static readonly IntPtr ColeDefaultPrincipal = new(-1); private static readonly IntPtr ColeDefaultAuthinfo = new(-1); [DllImport("ole32.dll")] private static extern int CoCreateInstance( in Guid clsid, IntPtr outer, uint clsContext, in Guid iid, out IntPtr instance); [DllImport("ole32.dll")] private static extern int CoSetProxyBlanket( IntPtr proxy, uint authnService, uint authzService, IntPtr serverPrincipalName, uint authnLevel, uint impersonationLevel, IntPtr authInfo, uint capabilities); [DllImport("ole32.dll")] private static extern int CoInitializeSecurity( IntPtr securityDescriptor, int authSvcCount, IntPtr authSvc, IntPtr reserved1, uint authnLevel, uint impersonationLevel, IntPtr authList, uint capabilities, IntPtr reserved3); [StructLayout(LayoutKind.Sequential)] private struct WslcListContainersOptions { public uint Flags; public int Limit; public IntPtr Filters; public uint FiltersCount; } /// /// `WSLCContainerEntry` from wslc.idl. The three char arrays are **inline fixed buffers**, /// not pointers — `ByValTStr`/`Ansi`, with the sizes straight from the IDL's `+ 1` constants /// (255+1, 255+1, 64+1). Getting a size wrong here does not fail loudly; it silently shifts /// every later field. The equivalent layout was validated on hardware via /// `ListSessions`, whose entry struct has the same shape (§13.2). /// [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] private struct WslcContainerEntry { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string Name; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string Image; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)] public string Id; public ulong StateChangedAt; public ulong CreatedAt; public uint State; } [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); } [StructLayout(LayoutKind.Sequential)] internal struct WslcVersion { public uint Major; public uint Minor; public uint Revision; } /// /// `IWSLCSession`, declared only as far as Terminate (method #23). /// /// **Every method ahead of the ones we call must still be declared**, because a COM vtable is /// addressed by slot — but only the called ones need accurate signatures, since a method that /// is never invoked is never marshalled. That is what makes this tractable: four of the /// placeholders (`LoadImage`, `ImportImage`, `SaveImage`, `SaveImages`) take `WSLCHandle` — a /// tagged union — **by value**, which would be genuinely awkward to marshal and does not have /// to be. Parameter *counts* are kept faithful to the IDL purely as documentation. /// /// Do not reorder. Do not delete an unused entry. Either silently shifts every slot below it. /// [ComImport, Guid("EF0661E4-6364-40EA-B433-E2FDF11F3519"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IWSLCSession { [PreserveSig] int GetId(out uint id); // 1 [PreserveSig] int GetDisplayName(out IntPtr displayName); // 2 [PreserveSig] int GetState(out uint state); // 3 [PreserveSig] int GetTerminationEvent(out IntPtr eventHandle); // 4 [PreserveSig] int GetTerminationReason(out uint reason, out IntPtr details); // 5 [PreserveSig] int PullImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d); // 6 [PreserveSig] int BuildImage(IntPtr a, IntPtr b, IntPtr c); // 7 [PreserveSig] int LoadImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d); // 8 (WSLCHandle by value) [PreserveSig] int ImportImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e); // 9 (WSLCHandle by value) [PreserveSig] int SaveImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d); // 10 (WSLCHandle by value) [PreserveSig] int SaveImages(IntPtr a, IntPtr b, IntPtr c, IntPtr d); // 11 (WSLCHandle by value) [PreserveSig] int ListImages(IntPtr a, IntPtr b, IntPtr c); // 12 [PreserveSig] int DeleteImage(IntPtr a, IntPtr b, IntPtr c); // 13 [PreserveSig] int TagImage(IntPtr a); // 14 [PreserveSig] int InspectImage(IntPtr a, IntPtr b); // 15 [PreserveSig] int PruneImages(IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e); // 16 [PreserveSig] int CreateContainer(IntPtr a, IntPtr b, IntPtr c); // 17 [PreserveSig] int OpenContainer(IntPtr a, IntPtr b); // 18 [PreserveSig] int ListContainers( // 19 IntPtr options, out IntPtr containers, out uint count, out IntPtr ports, out uint portsCount); [PreserveSig] int PruneContainers(IntPtr a, IntPtr b, IntPtr c); // 20 [PreserveSig] int CreateRootNamespaceProcess( IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e, IntPtr f); // 21 [PreserveSig] int FormatVirtualDisk(IntPtr a); // 22 [PreserveSig] int Terminate(); // 23 } } #endif