fix(daemon): wait for the client's first frame before impersonating it

Root cause of the Windows client-rejection loop, named by the accept
classifier: ImpersonateNamedPipeClient fails with ERROR_CANNOT_IMPERSONATE
(1368) - "unable to impersonate using a named pipe until data has been
read from that pipe" - because the daemon authenticated the connecting
client the instant ConnectNamedPipe returned, before the client had put
any bytes on the wire. Every client was refused and the accept loop
spun (116 rejections in one smoke window).

The client always sends its HELLO request immediately after connecting
and only then waits for the response (cbm_daemon_runtime_client_connect:
ipc_connect -> send_frame -> receive), so the server can safely wait for
the client's first bytes before impersonating. win_pipe_wait_for_client_data
polls PeekNamedPipe - which inspects the buffer WITHOUT consuming it, so
the HELLO frame stays intact for the runtime's own read - and gates only
on data PRESENCE, the exact precondition the impersonation API requires.
Bounded by a finite deadline so a connect-but-never-send peer is refused,
not hung. The identity check itself is unchanged; it now simply runs
after the client is impersonatable.

Windows-only path; the unix matrix (green at 6742/0 on this tree) is
unaffected.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-07-18 22:22:09 +02:00
parent 1488579dab
commit 2e87810b4b
+38
View File
@@ -26,6 +26,13 @@ enum {
CBM_DAEMON_IPC_RETRY_INTERVAL_MS = 10,
CBM_DAEMON_IPC_COORDINATION_CLEANUP_MS = 500,
CBM_DAEMON_IPC_WINDOWS_RENDEZVOUS_RETRY_MS = 250,
/* After ConnectNamedPipe the server must see the client's first bytes
* before it can impersonate it (ImpersonateNamedPipeClient fails with
* ERROR_CANNOT_IMPERSONATE until data has been read from the pipe). The
* client always sends its HELLO request immediately after connecting, so
* this only bounds how long a connect-but-never-send peer is tolerated;
* generous for starved CI runners, still finite. */
CBM_DAEMON_IPC_FIRST_FRAME_TIMEOUT_MS = 15000,
};
/* Last private-namespace validation refusal, set at the exact failing check.
@@ -5128,6 +5135,26 @@ static int win_overlapped_wait(HANDLE handle, OVERLAPPED *overlapped, DWORD time
return result;
}
/* Wait until the connected client has placed its first bytes in the pipe.
* PeekNamedPipe inspects the buffer without consuming it, so the client's
* HELLO frame stays intact for the runtime's own read; we only gate on its
* PRESENCE, which is the precondition ImpersonateNamedPipeClient requires.
* Returns false on a broken pipe or if the deadline passes with no data. */
static bool win_pipe_wait_for_client_data(HANDLE pipe, uint64_t deadline_ms) {
for (;;) {
DWORD available = 0;
if (!PeekNamedPipe(pipe, NULL, 0, NULL, &available, NULL)) {
return false;
}
if (available > 0) {
return true;
}
if (!win_retry_pause(deadline_ms)) {
return false;
}
}
}
static bool win_pipe_client_is_current_user(HANDLE pipe) {
win_security_t security;
if (!win_security_init(&security)) {
@@ -5223,6 +5250,17 @@ int cbm_daemon_ipc_accept(cbm_daemon_ipc_listener_t *listener, uint32_t timeout_
listener->pipe = INVALID_HANDLE_VALUE;
return result;
}
/* The client sends its HELLO immediately after connecting; wait for those
* bytes before impersonating, or ImpersonateNamedPipeClient fails with
* ERROR_CANNOT_IMPERSONATE (1368) and every client is refused. */
if (!win_pipe_wait_for_client_data(listener->pipe,
ipc_deadline_after(CBM_DAEMON_IPC_FIRST_FRAME_TIMEOUT_MS))) {
cbm_log_warn("daemon.accept.client_rejected", "stage", "first_frame");
(void)DisconnectNamedPipe(listener->pipe);
(void)CloseHandle(listener->pipe);
listener->pipe = INVALID_HANDLE_VALUE;
return -1;
}
if (!win_pipe_client_is_current_user(listener->pipe)) {
/* The daemon-side twin of the client's server_identity log: without
* it a server-initiated disconnect is indistinguishable from a