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