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);