diff --git a/spikes/README.md b/spikes/README.md index 65afd9d..3b7c1c2 100644 --- a/spikes/README.md +++ b/spikes/README.md @@ -180,6 +180,12 @@ for, and nothing above it should move. These three are different: mounted vs. in-VM). Deliberately held back until `WslcApiDump` has run: written now, against guessed names, it would not compile, and fixing it blind is the mistake this whole approach exists to avoid. +- **The internal arm itself** (`WslcInternal.cs` in the broker, not a spike) — now fully + de-risked by `--internal-call`: entry point, vtable, impersonation requirement and the + session→compat handoff are all confirmed on hardware. Shape is *find, then hand off*: + `OpenSessionByName` / `ListContainers` / `OpenContainer`, each result QI'd onto its compat + interface and passed to `FromAbi()`, after which the existing facade code drives it. The + container-level QI is the one step still unobserved — confirm it in `WslcSpike` below. - **`GatewaySpike`** — M1 (b), and the control-plane spike that actually matters now: can a `Bridged` wslc container reach the host at the `vEthernet (WSL)` address `WslcFacade` returns, under the **default** Windows firewall, and does `control-bridge.js` complete an MCP round trip diff --git a/spikes/WslcApiDump/InternalComProbe.cs b/spikes/WslcApiDump/InternalComProbe.cs index 5456131..ae4b8df 100644 --- a/spikes/WslcApiDump/InternalComProbe.cs +++ b/spikes/WslcApiDump/InternalComProbe.cs @@ -334,6 +334,7 @@ internal static class InternalComProbe 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."); + ProbeFromAbiOwnership(projected); Marshal.Release(projected); } else @@ -352,6 +353,67 @@ internal static class InternalComProbe private static string? sessionName; + /// + /// Does Session.FromAbi(ptr) take a reference, or borrow the caller's? + /// + /// This is not a detail — it is the difference between a leak and a use-after-free, and the + /// facade will call it on every reattach. `QueryInterface` already handed us one reference; + /// if `FromAbi` AddRefs as well we must `Release` ours, and if it merely wraps the pointer we + /// must NOT. Neither CsWinRT's docs nor the IDL say which. + /// + /// Measured rather than assumed, by the only reliable means COM offers: `AddRef`/`Release` + /// return the new count, so an AddRef/Release pair straddling the call reads the delta. + /// The absolute numbers are meaningless (proxies keep their own counts); the DIFFERENCE is + /// the answer. + /// + private static void ProbeFromAbiOwnership(IntPtr sessionPtr) + { + Console.WriteLine(); + Console.WriteLine(" FromAbi ownership (leak vs. use-after-free):"); + try + { + // Baseline: AddRef then Release, reading the count at the peak. + var before = Marshal.AddRef(sessionPtr); + Marshal.Release(sessionPtr); + + var projection = global::Microsoft.WSL.Containers.Session.FromAbi(sessionPtr); + if (projection is null) + { + Console.WriteLine(" FromAbi returned null — cannot judge"); + return; + } + + var after = Marshal.AddRef(sessionPtr); + Marshal.Release(sessionPtr); + + Console.WriteLine($" refcount {before} → {after} across FromAbi()"); + Console.WriteLine(after > before + ? " → FromAbi ADDS a reference. The facade must Release its QI reference " + + "after handing the pointer over, or every reattach leaks the session." + : " → FromAbi BORROWS the pointer. The facade must NOT Release its QI " + + "reference — the projection depends on it staying alive."); + + // Prove the projection is actually usable, not just constructed: GetImages() is the + // cheapest read on Session and mutates nothing. If this works, a re-adopted session + // really is a first-class compat Session. + try + { + var images = projection.GetImages(); + Console.WriteLine($" projection.GetImages() = {images.Count} image(s) " + + "— the re-adopted session is FULLY USABLE through the compat projection"); + } + catch (Exception e) + { + Console.WriteLine($" projection.GetImages() threw {e.GetType().Name}: " + + $"{e.Message} — it projects but does not work; investigate before relying on it"); + } + } + catch (Exception e) + { + Console.WriteLine($" FromAbi threw {e.GetType().Name}: {e.Message}"); + } + } + private static IEnumerable Wrap(string text) { var words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries);