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
+12 -4
View File
@@ -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 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. `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 Re-run as `--session --keep --internal-call`, `ListSessions` returned the live session by name
shape `ListContainers` uses) is still untested. Run `--session --keep --internal-call` to create a (`#6 "nucleic-spike"`), so the entry-struct layout — inline `wchar_t` buffers, the same shape
session, re-adopt it through the internal interface, and exercise it — that combination is §2.3's `ListContainers` uses — marshals as declared. **Enumeration is proven.**
reattach story end to end, and it also answers the handoff question below. `wsl --shutdown` after.
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 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. objects routinely expose several — so `--internal` activates each and QIs for the internal IIDs.
+63 -2
View File
@@ -132,6 +132,7 @@ internal static class InternalComProbe
if (interfaceName == "IWSLCSessionManager") if (interfaceName == "IWSLCSessionManager")
{ {
bound = true; bound = true;
RaiseImpersonation(candidate);
if (callThrough) CallThrough(candidate); if (callThrough) CallThrough(candidate);
} }
Marshal.Release(candidate); Marshal.Release(candidate);
@@ -236,6 +237,32 @@ internal static class InternalComProbe
ProbeSessionHandoff(proxy); 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> /// <summary>
/// The question that decides the SHAPE of D13's internal arm. /// The question that decides the SHAPE of D13's internal arm.
/// ///
@@ -277,8 +304,22 @@ internal static class InternalComProbe
if (hr < 0) if (hr < 0)
{ {
Console.WriteLine($" failed: {Hresult(hr)}"); Console.WriteLine($" failed: {Hresult(hr)}");
Console.WriteLine(" (expected if no session of that name is running — create " // Be specific about WHY, and never blame absence when the cause is security — an
+ "one first: --session --keep --internal-call)"); // 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; return;
} }
@@ -334,6 +375,10 @@ internal static class InternalComProbe
0x80004002 => "E_NOINTERFACE — the class does not implement it", 0x80004002 => "E_NOINTERFACE — the class does not implement it",
0x80070005 => "E_ACCESSDENIED — registered, but this token may not activate it", 0x80070005 => "E_ACCESSDENIED — registered, but this token may not activate it",
0x800401F0 => "CO_E_NOTINITIALIZED", 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}", _ => $"0x{code:X8}",
}; };
@@ -358,6 +403,22 @@ internal static class InternalComProbe
private const uint CoinitMultithreaded = 0; private const uint CoinitMultithreaded = 0;
private const uint RpcEChangedMode = 0x80010106; private const uint RpcEChangedMode = 0x80010106;
private const uint ENoInterface = 0x80004002; 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")] [DllImport("ole32.dll")]
private static extern int CoInitializeEx(IntPtr reserved, uint coInit); private static extern int CoInitializeEx(IntPtr reserved, uint coInit);