Merge nucleic/lucid-river-toad-6efj into dev

This commit is contained in:
2026-07-29 03:40:56 -07:00
parent 8c8add3c40
commit 7d222a1535
2 changed files with 75 additions and 6 deletions
+63 -2
View File
@@ -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);
}
/// <summary>
/// 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`.
/// </summary>
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");
}
/// <summary>
/// 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);