diff --git a/spikes/README.md b/spikes/README.md
index 95aaa33..65afd9d 100644
--- a/spikes/README.md
+++ b/spikes/README.md
@@ -133,10 +133,18 @@ class, confirming §13.1's retraction against the machine rather than against an
answering through both faces — and `ListSessions()` (slot 6) returned S_OK. So hand-written
`ComImport` is sufficient and the C++/WinRT shim §13.1 mused about is not needed.
-It returned **0 sessions**, though, so the entry-struct layout (inline `wchar_t` buffers, the same
-shape `ListContainers` uses) is still untested. Run `--session --keep --internal-call` to create a
-session, re-adopt it through the internal interface, and exercise it — that combination is §2.3's
-reattach story end to end, and it also answers the handoff question below. `wsl --shutdown` after.
+Re-run as `--session --keep --internal-call`, `ListSessions` returned the live session by name
+(`#6 "nucleic-spike"`), so the entry-struct layout — inline `wchar_t` buffers, the same shape
+`ListContainers` uses — marshals as declared. **Enumeration is proven.**
+
+That run also turned up a trap worth knowing before writing any of this into brokerd:
+`OpenSessionByName` failed **`0x80070542`** on the session `ListSessions` had just listed.
+That is `ERROR_BAD_IMPERSONATION_LEVEL` — the service impersonates the caller to resolve a
+*per-user* session, and .NET hands COM proxies `RPC_C_IMP_LEVEL_IDENTIFY` by default. A security
+error that reads exactly like "not found", and only on the methods reattach needs. The probe now
+raises the proxy blanket to `RPC_C_IMP_LEVEL_IMPERSONATE`; brokerd should call
+`CoInitializeSecurity` at startup instead, **before** its first COM call — including the compat
+SDK's, or it fails `RPC_E_TOO_LATE`. `wsl --shutdown` to clean up after `--keep`.
The hypothesis was that one of those objects also implements the internal interface — COM
objects routinely expose several — so `--internal` activates each and QIs for the internal IIDs.
diff --git a/spikes/WslcApiDump/InternalComProbe.cs b/spikes/WslcApiDump/InternalComProbe.cs
index b628d6e..5456131 100644
--- a/spikes/WslcApiDump/InternalComProbe.cs
+++ b/spikes/WslcApiDump/InternalComProbe.cs
@@ -132,6 +132,7 @@ internal static class InternalComProbe
if (interfaceName == "IWSLCSessionManager")
{
bound = true;
+ RaiseImpersonation(candidate);
if (callThrough) CallThrough(candidate);
}
Marshal.Release(candidate);
@@ -236,6 +237,32 @@ internal static class InternalComProbe
ProbeSessionHandoff(proxy);
}
+ ///
+ /// Grant the server permission to impersonate us on this proxy.
+ ///
+ /// Found the hard way: `OpenSessionByName` failed `0x80070542`
+ /// (`HRESULT_FROM_WIN32(1346)` = `ERROR_BAD_IMPERSONATION_LEVEL`) on a session that
+ /// `ListSessions` had just listed by name. It is not a "missing" error at all — the service
+ /// impersonates the caller to resolve a **per-user** session, and a .NET COM client is handed
+ /// `RPC_C_IMP_LEVEL_IDENTIFY` by default, which lets the server check who we are but not act
+ /// as us. `GetVersion` and `ListSessions` never impersonate, which is exactly why those two
+ /// succeeded and made the failure look like a per-method capability gap.
+ ///
+ /// `CoSetProxyBlanket` is the surgical fix — it applies to this proxy only and works after
+ /// marshalling has already happened. `nucleic-brokerd` can instead call `CoInitializeSecurity`
+ /// once at startup, before its first COM call, which covers every proxy it will ever hold;
+ /// that is the production shape, and it must come first or it fails `RPC_E_TOO_LATE`.
+ ///
+ private static void RaiseImpersonation(IntPtr proxy)
+ {
+ var hr = CoSetProxyBlanket(
+ proxy, RpcCAuthnDefault, RpcCAuthzDefault, ColeDefaultPrincipal,
+ RpcCAuthnLevelDefault, RpcCImpLevelImpersonate, ColeDefaultAuthinfo, EoacNone);
+ Console.WriteLine(hr >= 0
+ ? " proxy blanket raised to RPC_C_IMP_LEVEL_IMPERSONATE"
+ : $" CoSetProxyBlanket failed: {Hresult(hr)} — per-user calls will likely fail 0x80070542");
+ }
+
///
/// The question that decides the SHAPE of D13's internal arm.
///
@@ -277,8 +304,22 @@ internal static class InternalComProbe
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)");
+ // Be specific about WHY, and never blame absence when the cause is security — an
+ // earlier version printed "expected if no session of that name is running" directly
+ // under a ListSessions that had just printed that session by name.
+ Console.WriteLine((uint)hr switch
+ {
+ ErrorBadImpersonationLevel =>
+ " → NOT a missing session: the server could not impersonate us. If the "
+ + "blanket was raised above and this still fails, the process needs "
+ + "CoInitializeSecurity(RPC_C_IMP_LEVEL_IMPERSONATE) BEFORE its first COM call.",
+ 0x8004060F =>
+ " → WSLC_E_SESSION_NOT_FOUND: no session of that name. Create one: "
+ + "--session --keep --internal-call",
+ _ =>
+ " → unexpected; compare against the ListSessions output above, which "
+ + "says whether the session actually exists.",
+ });
return;
}
@@ -334,6 +375,10 @@ internal static class InternalComProbe
0x80004002 => "E_NOINTERFACE — the class does not implement it",
0x80070005 => "E_ACCESSDENIED — registered, but this token may not activate it",
0x800401F0 => "CO_E_NOTINITIALIZED",
+ 0x80070542 => "ERROR_BAD_IMPERSONATION_LEVEL — the server needs to impersonate the caller "
+ + "and this proxy only grants IDENTIFY. A SECURITY error, not a missing object",
+ 0x80010119 => "RPC_E_TOO_LATE — CoInitializeSecurity after the first COM call",
+ 0x8004060F => "WSLC_E_SESSION_NOT_FOUND",
_ => $"0x{code:X8}",
};
@@ -358,6 +403,22 @@ internal static class InternalComProbe
private const uint CoinitMultithreaded = 0;
private const uint RpcEChangedMode = 0x80010106;
private const uint ENoInterface = 0x80004002;
+ private const uint ErrorBadImpersonationLevel = 0x80070542;
+
+ // CoSetProxyBlanket's "keep the default" sentinels are -1, not 0 — passing 0 for the
+ // principal name means "no principal" rather than "default" and fails differently.
+ 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 static readonly IntPtr ColeDefaultPrincipal = new(-1);
+ private static readonly IntPtr ColeDefaultAuthinfo = new(-1);
+
+ [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 CoInitializeEx(IntPtr reserved, uint coInit);