385 lines
18 KiB
C#
385 lines
18 KiB
C#
#if USE_WSLC
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace NucleicBroker.Wslc;
|
|
|
|
/// <summary>
|
|
/// 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. <see cref="InitializeSecurity"/>
|
|
/// 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.
|
|
/// </summary>
|
|
internal sealed class WslcInternal : IDisposable
|
|
{
|
|
private IWSLCSessionManager? manager;
|
|
private IntPtr managerPtr;
|
|
|
|
/// <summary>What a recovery found and did, for the host log and the `session.down` story.</summary>
|
|
internal sealed record Recovery(IReadOnlyList<string> Containers, bool Terminated);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Open the orphaned session named <paramref name="name"/>, 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The container roster of a session, by name. Enumeration the compat SDK has no call for at
|
|
/// all — <c>Session</c> exposes no listing and <c>Container</c> carries no <c>Name</c>.
|
|
/// </summary>
|
|
private static IReadOnlyList<string> 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<WslcListContainersOptions>());
|
|
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<WslcContainerEntry>();
|
|
var names = new List<string>((int)count);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var entry = Marshal.PtrToStructure<WslcContainerEntry>(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
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// `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).
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// `IWSLCSession`, declared only as far as <c>Terminate</c> (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.
|
|
/// </summary>
|
|
[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
|