Files
codebase-memory-mcp/src/daemon/runtime.c
T
Martin Vogel d6dcb3218b Merge origin/main into fix/hook-augment-startup-deadline
Conflicts resolved:

- src/daemon/runtime.h: both sides add test seams at the end of the
  CBM_ENABLE_TEST_SEAMS block. Kept main's connection-cap and HELLO-order
  seams and placed this branch's fingerprint-cache seams after them; the
  branch's added lines are unchanged.
- src/cli/hook_augment.c: kept main's re-arm guard, which opens the
  timeouts log only once and keeps that descriptor. Dropped main's early
  return and breadcrumb formatting after the open: this branch formats
  the breadcrumb before the log path is resolved, so stderr gets it even
  when the log cannot be opened. That formatting runs on every arm, so
  main's re-arm comment now says the text above is refreshed, and the
  #858 header comment now says the breadcrumb also goes to stderr.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-11-10 23:08:14 +01:00

3996 lines
183 KiB
C

/*
* runtime.c — Mandatory per-account daemon service over authenticated IPC.
*/
#include "daemon/runtime.h"
#include "daemon/ipc_internal.h"
#include "daemon/service_internal.h"
#include "foundation/compat.h"
#include "foundation/compat_thread.h"
#include "foundation/log.h"
#include "foundation/mem.h"
#include "foundation/platform.h"
#include "foundation/win_utf8.h"
#include <limits.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef CBM_ENABLE_TEST_SEAMS
/* #1383 test seam: force peer-image verification to fail so the rejection
* -response path is reachable from the in-process harness — the peer pid is
* OS-authenticated socket credentials, so a same-process peer always verifies
* against the service's own active image. */
static atomic_bool runtime_force_peer_image_unverified_seam;
void cbm_daemon_runtime_force_peer_image_unverified_for_testing(bool force) {
atomic_store(&runtime_force_peer_image_unverified_seam, force);
}
/* The two failure modes are NOT interchangeable and must be testable apart:
* an image that cannot be examined at all is admitted (the peer already proved
* build compatibility in the HELLO), while one that CAN be examined and differs
* is rejected. One seam per mode keeps each contract honest. */
static atomic_bool runtime_force_peer_image_mismatch_seam;
void cbm_daemon_runtime_force_peer_image_mismatch_for_testing(bool force) {
atomic_store(&runtime_force_peer_image_mismatch_seam, force);
}
/* #1955 test seam: make the active-image comparison miss, exactly as when the
* peer runs a DIFFERENT file with identical bytes (a second install path, a
* package-manager cache copy). Everything after that comparison - the full
* fingerprint fallback and the verified-image cache - still runs for real. The
* counter records every full-image fingerprint the HELLO path computes, so a
* test asserts how often the O(image size) hash runs instead of timing it. */
static atomic_bool runtime_force_peer_image_distinct_copy_seam;
static atomic_uint_fast64_t runtime_peer_image_hash_count_seam;
void cbm_daemon_runtime_force_peer_image_distinct_copy_for_testing(bool force) {
atomic_store(&runtime_force_peer_image_distinct_copy_seam, force);
}
uint64_t cbm_daemon_runtime_peer_image_hashes_for_testing(void) {
return (uint64_t)atomic_load(&runtime_peer_image_hash_count_seam);
}
static void runtime_note_peer_image_hash(void) {
(void)atomic_fetch_add(&runtime_peer_image_hash_count_seam, 1);
}
/* The abandoned-request containment path ends in process termination, which an
* in-process harness cannot observe. The timeout override makes the ceiling
* reachable in test time; the hook replaces termination with a recordable
* callback. Both stay inert (zero/NULL) outside tests. */
static _Atomic uint32_t runtime_abandoned_request_join_timeout_seam;
void cbm_daemon_runtime_set_abandoned_request_join_timeout_for_testing(uint32_t timeout_ms) {
atomic_store(&runtime_abandoned_request_join_timeout_seam, timeout_ms);
}
static _Atomic(cbm_daemon_runtime_containment_hook_t) runtime_containment_hook_seam;
void cbm_daemon_runtime_set_containment_hook_for_testing(
cbm_daemon_runtime_containment_hook_t hook) {
atomic_store(&runtime_containment_hook_seam, hook);
}
/* Cold-storm ephemeral-linger seam (2026-09). Overrides the bounded linger
* window that ephemeral last-committed-client retirement grants while cohort
* participants are still mid-bootstrap, so a test can drive both the linger and
* its expiry backstop in test time. UINT32_MAX leaves the production constant;
* any other value (0 = expire immediately) overrides. */
static _Atomic uint32_t runtime_ephemeral_linger_timeout_seam = UINT32_MAX;
void cbm_daemon_runtime_service_set_ephemeral_linger_timeout_for_testing(uint32_t timeout_ms) {
atomic_store(&runtime_ephemeral_linger_timeout_seam, timeout_ms);
}
/* Connection-cap seams (flaky-ledger item 12): see runtime.h. */
static atomic_bool runtime_hold_peer_waits_seam;
void cbm_daemon_runtime_hold_peer_waits_for_testing(bool hold) {
atomic_store(&runtime_hold_peer_waits_seam, hold);
}
static _Atomic(cbm_daemon_runtime_hello_hook_t) runtime_hello_hook_seam;
void cbm_daemon_runtime_set_hello_hook_for_testing(cbm_daemon_runtime_hello_hook_t hook) {
atomic_store(&runtime_hello_hook_seam, hook);
}
static void runtime_hello_point(cbm_daemon_runtime_hello_point_t point) {
cbm_daemon_runtime_hello_hook_t hook = atomic_load(&runtime_hello_hook_seam);
if (hook) {
hook(point);
}
}
#define RUNTIME_HELLO_POINT(point) runtime_hello_point(CBM_DAEMON_RUNTIME_HELLO_POINT_##point)
#else
static void runtime_note_peer_image_hash(void) {}
#define RUNTIME_HELLO_POINT(point) ((void)0)
#endif
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#elif defined(__APPLE__)
#include <fcntl.h>
#include <libproc.h>
#include <mach/vm_prot.h>
#include <sys/proc_info.h>
#include <sys/stat.h>
#include <unistd.h>
#elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__)
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#if defined(__FreeBSD__) || defined(__NetBSD__)
#include <sys/types.h>
#include <sys/sysctl.h>
#endif
#endif
enum {
RUNTIME_MAX_CLIENTS_HARD = 64,
RUNTIME_ACCEPT_POLL_MS = 20,
RUNTIME_WAIT_POLL_NS = 1000000,
RUNTIME_WORKER_STACK_SIZE = 256 * 1024,
/* Distinct peer image files (install paths / identical copies) whose full
* fingerprint already matched this daemon. Each held entry pins one image
* inode, so the bound is small; a full table only means later copies are
* hashed per admission, exactly as before the table existed. */
RUNTIME_VERIFIED_PEER_IMAGE_CAPACITY = 8,
/* Bounds the drain-before-close wait for a peer consuming its final
* response; interrupted or already-poisoned connections skip it. */
RUNTIME_WORKER_DRAIN_TIMEOUT_MS = 2000,
/* Inline rejections run on the accept thread, so their drain stays short:
* a local cooperative peer is already blocked in read and consumes the
* rejection within milliseconds. */
RUNTIME_REJECT_DRAIN_TIMEOUT_MS = 250,
/* Ceiling on how long a disconnecting worker waits for its in-flight
* application request to observe cancellation. session_cancel has already
* run by the time this wait starts, so a compliant handler returns within
* milliseconds; the wait is sized for a handler that only polls its cancel
* flag between long pipeline stages. A handler that ignores cancellation
* past this ceiling turned the daemon into a permanent zombie once
* (2026-08-29): the unbounded join blocked the disconnect path, admission
* wedged behind it, and the dead generation held the endpoint pipes and
* the UI port for nine hours with nothing logged. */
RUNTIME_ABANDONED_REQUEST_JOIN_TIMEOUT_MS = 30000,
/* Cold-storm race (2026-09): when the final committed client of an
* ephemeral generation disconnects while cohort participants are still
* admitted but mid-bootstrap (racing connect()), the generation lingers
* this long for them to connect instead of retiring out from under them.
* Mirrors the host initial-client window; the linger is always bounded so
* a participant that never connects cannot wedge the generation (the
* 900s host_serving hang). */
RUNTIME_EPHEMERAL_LINGER_MS = 10000,
RUNTIME_PATH_CAP = 4096,
RENDEZVOUS_REQUEST_ABI_OFFSET = 0,
RENDEZVOUS_REQUEST_VERSION_OFFSET = 4,
RENDEZVOUS_REQUEST_BUILD_OFFSET =
RENDEZVOUS_REQUEST_VERSION_OFFSET + CBM_DAEMON_RENDEZVOUS_VERSION_TEXT_CAP,
RENDEZVOUS_RESPONSE_CONNECT_STATUS_OFFSET = 0,
RENDEZVOUS_RESPONSE_HELLO_STATUS_OFFSET = 4,
RENDEZVOUS_RESPONSE_CLIENT_ID_OFFSET = 8,
RENDEZVOUS_RESPONSE_PROCESS_ID_OFFSET = 16,
RENDEZVOUS_RESPONSE_CONFLICT_STATUS_OFFSET = 24,
RENDEZVOUS_RESPONSE_ACTIVE_VERSION_OFFSET = 28,
RENDEZVOUS_RESPONSE_ACTIVE_BUILD_OFFSET =
RENDEZVOUS_RESPONSE_ACTIVE_VERSION_OFFSET + CBM_DAEMON_RENDEZVOUS_VERSION_TEXT_CAP,
RENDEZVOUS_RESPONSE_REQUESTED_VERSION_OFFSET =
RENDEZVOUS_RESPONSE_ACTIVE_BUILD_OFFSET + CBM_DAEMON_RENDEZVOUS_BUILD_FINGERPRINT_CAP,
RENDEZVOUS_RESPONSE_REQUESTED_BUILD_OFFSET =
RENDEZVOUS_RESPONSE_REQUESTED_VERSION_OFFSET + CBM_DAEMON_RENDEZVOUS_VERSION_TEXT_CAP,
RENDEZVOUS_RESPONSE_MESSAGE_OFFSET =
RENDEZVOUS_RESPONSE_REQUESTED_BUILD_OFFSET + CBM_DAEMON_RENDEZVOUS_BUILD_FINGERPRINT_CAP,
ACTIVATION_REQUEST_ACTION_OFFSET = 0,
ACTIVATION_REQUEST_IDENTITY_OFFSET = 4,
ACTIVATION_RESPONSE_ABI_OFFSET = 0,
ACTIVATION_RESPONSE_ACCEPTED_OFFSET = 4,
ACTIVATION_RESPONSE_CLIENTS_OFFSET = 8,
ACTIVATION_RESPONSE_CONNECTIONS_OFFSET = 16,
STATUS_RESPONSE_SIZE = 4,
SUBSCRIBE_REQUEST_PREFIX_SIZE = 4,
SUBSCRIBE_RESPONSE_SIZE = 12,
UNSUBSCRIBE_REQUEST_SIZE = 8,
APPLICATION_CANCEL_REQUEST_SIZE = 8,
APPLICATION_REQUEST_PREFIX_SIZE = 12,
APPLICATION_RESPONSE_PREFIX_SIZE = 16,
};
_Static_assert(RENDEZVOUS_REQUEST_BUILD_OFFSET + CBM_DAEMON_RENDEZVOUS_BUILD_FINGERPRINT_CAP ==
CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE,
"rendezvous request layout changed");
_Static_assert(RENDEZVOUS_RESPONSE_MESSAGE_OFFSET + CBM_DAEMON_RENDEZVOUS_MESSAGE_CAP ==
CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE,
"rendezvous response layout changed");
_Static_assert(ACTIVATION_REQUEST_IDENTITY_OFFSET + CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE ==
CBM_DAEMON_ACTIVATION_SHUTDOWN_REQUEST_SIZE &&
ACTIVATION_RESPONSE_CONNECTIONS_OFFSET + 8 ==
CBM_DAEMON_ACTIVATION_SHUTDOWN_RESPONSE_SIZE,
"activation shutdown layout changed");
_Static_assert(CBM_DAEMON_RENDEZVOUS_VERSION_TEXT_CAP == CBM_DAEMON_VERSION_TEXT_SIZE,
"service version capacity requires a new rendezvous strategy");
_Static_assert(CBM_DAEMON_RENDEZVOUS_BUILD_FINGERPRINT_CAP == CBM_DAEMON_BUILD_FINGERPRINT_SIZE,
"service fingerprint capacity requires a new rendezvous strategy");
_Static_assert(CBM_DAEMON_RENDEZVOUS_MESSAGE_CAP == CBM_DAEMON_CONFLICT_MESSAGE_SIZE,
"service message capacity requires a new rendezvous strategy");
_Static_assert(CBM_DAEMON_RUNTIME_CONNECT_ERROR == 0 && CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED == 1 &&
CBM_DAEMON_RUNTIME_CONNECT_CONFLICT == 2 &&
CBM_DAEMON_RUNTIME_CONNECT_REJECTED == 3,
"rendezvous connect status values changed");
_Static_assert(CBM_DAEMON_HELLO_INVALID == 0 && CBM_DAEMON_HELLO_COMPATIBLE == 1 &&
CBM_DAEMON_HELLO_VERSION_CONFLICT == 2 && CBM_DAEMON_HELLO_BUILD_CONFLICT == 3 &&
CBM_DAEMON_HELLO_PROTOCOL_ABI_CONFLICT == 4 &&
CBM_DAEMON_HELLO_STORE_ABI_CONFLICT == 5 &&
CBM_DAEMON_HELLO_FEATURE_ABI_CONFLICT == 6,
"rendezvous hello status values changed");
_Static_assert(CBM_DAEMON_RENDEZVOUS_FRAME_VERSION == 1 && CBM_DAEMON_RENDEZVOUS_ABI == 1 &&
CBM_DAEMON_FRAME_REQUEST == 1 && CBM_DAEMON_FRAME_RESPONSE == 2 &&
CBM_DAEMON_RUNTIME_OP_HELLO == 1 &&
CBM_DAEMON_RUNTIME_OP_ACTIVATION_SHUTDOWN == 8 &&
CBM_DAEMON_RUNTIME_ACTIVATION_INSTALL == 1 &&
CBM_DAEMON_RUNTIME_ACTIVATION_UPDATE == 2 &&
CBM_DAEMON_RUNTIME_ACTIVATION_UNINSTALL == 3,
"rendezvous framing values changed");
typedef struct cbm_daemon_runtime_worker cbm_daemon_runtime_worker_t;
/* The service keeps the exact native image object whose bytes were hashed at
* startup. HELLO can then prove that a peer maps this same, still-stable file
* object without reading the executable again. A different file identity is
* not a rejection by itself: identical copies remain supported through the
* full fingerprint fallback. */
typedef struct {
bool held;
#ifdef _WIN32
HANDLE file;
BY_HANDLE_FILE_INFORMATION information;
LARGE_INTEGER size;
#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__)
int fd;
struct stat status;
#endif
} runtime_process_image_reference_t;
struct cbm_daemon_runtime_service {
cbm_mutex_t mutex;
cbm_daemon_ipc_listener_t *listener;
cbm_daemon_coordinator_t *coordinator;
cbm_daemon_runtime_worker_t *workers;
size_t worker_capacity;
size_t worker_mutexes_initialized;
size_t active_connections;
size_t committed_clients;
/* Monotonic count of every admission since service start. The host's
* nobody-ever-connected window latches on this instead of sampling the
* live client count: a short-lived first session can begin and end
* entirely between two host polls, and a sampled count then reads zero
* forever, stopping a daemon whose client is mid-conversation. */
uint64_t admitted_total;
/* Born via `daemon start`: last-client-disconnect does not begin stopping.
* The stop/drain ops and process kill remain the only exits. */
bool permanent;
cbm_thread_t accept_thread;
bool accept_thread_started;
bool accept_thread_joined;
atomic_bool accept_thread_done;
cbm_daemon_runtime_service_state_t state;
uint64_t stop_deadline_ms;
bool emergency_stop;
bool activation_shutdown_requested;
bool activation_response_inflight;
char semantic_version[CBM_DAEMON_VERSION_TEXT_SIZE];
char build_fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
cbm_daemon_build_identity_t identity;
runtime_process_image_reference_t active_image;
/* Guarded by mutex. Retained references keep each verified inode alive,
* so its stat identity cannot be reused by a different file. */
runtime_process_image_reference_t verified_peer_images[RUNTIME_VERIFIED_PEER_IMAGE_CAPACITY];
size_t verified_peer_image_count;
char *conflict_log_path;
size_t conflict_log_cap_bytes;
uint64_t lease_timeout_ms;
uint32_t request_timeout_ms;
uint32_t shutdown_timeout_ms;
cbm_daemon_runtime_application_callbacks_t application;
/* Owned only by the convenience start() path. start_reserved() callers
* retain their externally managed participant guard. */
cbm_daemon_ipc_participant_guard_t *owned_participant_guard;
/* Cold-storm ephemeral-retirement gate (2026-09). When the final committed
* client disconnects while cohort participants are still admitted but not
* yet committed, the generation lingers until this bounded deadline instead
* of retiring; reconcile_lifetime retires it once that window elapses with
* no new client committing. Zero means no linger is armed. */
uint64_t ephemeral_linger_deadline_ms;
};
struct cbm_daemon_runtime_worker {
cbm_daemon_runtime_service_t *service;
cbm_mutex_t send_mutex;
cbm_thread_t thread;
bool thread_started;
bool in_use;
bool joining;
atomic_bool done;
cbm_daemon_ipc_connection_t *connection;
cbm_daemon_client_id_t client_id;
uint64_t peer_process_id;
bool admitted;
bool admission_committed;
bool final_response_inflight;
cbm_daemon_runtime_application_session_t *application_session;
bool application_session_opened;
bool application_cancelled;
atomic_bool disconnecting;
cbm_thread_t application_thread;
bool application_thread_started;
atomic_bool application_thread_done;
cbm_daemon_runtime_application_token_t application_request_token;
cbm_daemon_runtime_application_token_t last_application_request_token;
uint8_t *application_request;
uint32_t application_request_length;
};
struct cbm_daemon_runtime_client {
cbm_mutex_t exchange_mutex;
cbm_mutex_t send_mutex;
cbm_mutex_t state_mutex;
cbm_daemon_ipc_connection_t *connection;
cbm_daemon_client_id_t client_id;
uint64_t authenticated_process_id;
bool usable;
bool exchange_active;
bool closing;
bool close_interrupted_exchange;
cbm_daemon_runtime_application_token_t next_application_token;
cbm_daemon_runtime_application_token_t last_started_application_token;
cbm_daemon_runtime_application_token_t active_application_token;
cbm_daemon_runtime_application_token_t pending_cancel_token;
bool application_request_sent;
bool application_cancel_sent;
};
static uint64_t runtime_deadline_after(uint32_t timeout_ms) {
uint64_t now_ms = cbm_now_ms();
if (UINT64_MAX - now_ms < (uint64_t)timeout_ms) {
return UINT64_MAX;
}
return now_ms + (uint64_t)timeout_ms;
}
static uint32_t runtime_ephemeral_linger_timeout_ms(void) {
#ifdef CBM_ENABLE_TEST_SEAMS
uint32_t seam = atomic_load(&runtime_ephemeral_linger_timeout_seam);
if (seam != UINT32_MAX) {
return seam;
}
#endif
return RUNTIME_EPHEMERAL_LINGER_MS;
}
static void runtime_wait_tick(uint64_t deadline_ms) {
uint64_t now_ms = cbm_now_ms();
if (now_ms >= deadline_ms) {
return;
}
uint64_t remaining_ms = deadline_ms - now_ms;
struct timespec pause = {
.tv_sec = 0,
.tv_nsec = remaining_ms > 1 ? RUNTIME_WAIT_POLL_NS : (long)(remaining_ms * 1000000ULL),
};
(void)cbm_nanosleep(&pause, NULL);
}
static void runtime_put_u32(uint8_t *out, uint32_t value) {
out[0] = (uint8_t)(value >> 24);
out[1] = (uint8_t)(value >> 16);
out[2] = (uint8_t)(value >> 8);
out[3] = (uint8_t)value;
}
static uint32_t runtime_get_u32(const uint8_t *in) {
return ((uint32_t)in[0] << 24) | ((uint32_t)in[1] << 16) | ((uint32_t)in[2] << 8) |
(uint32_t)in[3];
}
static void runtime_put_u64(uint8_t *out, uint64_t value) {
for (size_t i = 0; i < 8; i++) {
out[i] = (uint8_t)(value >> (56 - i * 8));
}
}
static uint64_t runtime_get_u64(const uint8_t *in) {
uint64_t value = 0;
for (size_t i = 0; i < 8; i++) {
value = (value << 8) | in[i];
}
return value;
}
static bool runtime_bounded_length(const char *value, size_t capacity, size_t *length_out) {
if (!value || capacity == 0) {
return false;
}
for (size_t i = 0; i < capacity; i++) {
if (value[i] == '\0') {
if (length_out) {
*length_out = i;
}
return true;
}
}
return false;
}
static bool runtime_wire_string_encode(uint8_t *out, size_t capacity, const char *value,
bool allow_empty) {
size_t length = 0;
if (!out || !runtime_bounded_length(value, capacity, &length) ||
(!allow_empty && length == 0)) {
return false;
}
memset(out, 0, capacity);
memcpy(out, value, length);
return true;
}
static bool runtime_wire_string_decode(const uint8_t *in, size_t capacity, char *out,
bool allow_empty) {
if (!in || !out || capacity == 0) {
return false;
}
size_t length = capacity;
for (size_t i = 0; i < capacity; i++) {
if (in[i] == 0) {
length = i;
break;
}
}
if (length == capacity || (!allow_empty && length == 0)) {
return false;
}
for (size_t i = length + 1; i < capacity; i++) {
if (in[i] != 0) {
return false;
}
}
memcpy(out, in, length);
out[length] = '\0';
return true;
}
/* Reuse the service layer's canonical printable-version and lowercase-SHA256
* validation without importing detailed ABI values into rendezvous. Fixed
* nonzero sentinels on both sides make only version/build ordering observable. */
static cbm_daemon_hello_status_t runtime_rendezvous_compare(const char *active_version,
const char *active_build,
const char *requested_version,
const char *requested_build,
cbm_daemon_conflict_t *conflict_out) {
cbm_daemon_build_identity_t active = {
.semantic_version = active_version,
.build_fingerprint = active_build,
.protocol_abi = 1,
.store_abi = 1,
.feature_abi = 1,
};
cbm_daemon_build_identity_t requested = {
.semantic_version = requested_version,
.build_fingerprint = requested_build,
.protocol_abi = 1,
.store_abi = 1,
.feature_abi = 1,
};
cbm_daemon_hello_status_t status = cbm_daemon_hello_compare(&active, &requested, conflict_out);
if (status != CBM_DAEMON_HELLO_INVALID && status != CBM_DAEMON_HELLO_COMPATIBLE &&
status != CBM_DAEMON_HELLO_VERSION_CONFLICT && status != CBM_DAEMON_HELLO_BUILD_CONFLICT) {
if (conflict_out) {
memset(conflict_out, 0, sizeof(*conflict_out));
conflict_out->status = CBM_DAEMON_HELLO_INVALID;
}
return CBM_DAEMON_HELLO_INVALID;
}
return status;
}
bool cbm_daemon_runtime_hello_request_encode(uint8_t out[CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE],
const cbm_daemon_build_identity_t *identity) {
if (!out || !identity) {
return false;
}
cbm_daemon_conflict_t validation;
if (runtime_rendezvous_compare(identity->semantic_version, identity->build_fingerprint,
identity->semantic_version, identity->build_fingerprint,
&validation) != CBM_DAEMON_HELLO_COMPATIBLE) {
return false;
}
memset(out, 0, CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE);
runtime_put_u32(out + RENDEZVOUS_REQUEST_ABI_OFFSET, CBM_DAEMON_RENDEZVOUS_ABI);
return runtime_wire_string_encode(out + RENDEZVOUS_REQUEST_VERSION_OFFSET,
CBM_DAEMON_RENDEZVOUS_VERSION_TEXT_CAP,
identity->semantic_version, false) &&
runtime_wire_string_encode(out + RENDEZVOUS_REQUEST_BUILD_OFFSET,
CBM_DAEMON_RENDEZVOUS_BUILD_FINGERPRINT_CAP,
identity->build_fingerprint, false);
}
static bool runtime_rendezvous_request_decode(
const uint8_t payload[CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE],
char version[CBM_DAEMON_VERSION_TEXT_SIZE], char build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
if (!payload || !version || !build ||
runtime_get_u32(payload + RENDEZVOUS_REQUEST_ABI_OFFSET) != CBM_DAEMON_RENDEZVOUS_ABI ||
!runtime_wire_string_decode(payload + RENDEZVOUS_REQUEST_VERSION_OFFSET,
CBM_DAEMON_RENDEZVOUS_VERSION_TEXT_CAP, version, false) ||
!runtime_wire_string_decode(payload + RENDEZVOUS_REQUEST_BUILD_OFFSET,
CBM_DAEMON_RENDEZVOUS_BUILD_FINGERPRINT_CAP, build, false)) {
return false;
}
cbm_daemon_conflict_t validation;
return runtime_rendezvous_compare(version, build, version, build, &validation) ==
CBM_DAEMON_HELLO_COMPATIBLE;
}
static bool runtime_activation_action_valid(cbm_daemon_runtime_activation_action_t action) {
return action == CBM_DAEMON_RUNTIME_ACTIVATION_INSTALL ||
action == CBM_DAEMON_RUNTIME_ACTIVATION_UPDATE ||
action == CBM_DAEMON_RUNTIME_ACTIVATION_UNINSTALL;
}
static const char *runtime_activation_action_text(cbm_daemon_runtime_activation_action_t action) {
switch (action) {
case CBM_DAEMON_RUNTIME_ACTIVATION_INSTALL:
return "install";
case CBM_DAEMON_RUNTIME_ACTIVATION_UPDATE:
return "update";
case CBM_DAEMON_RUNTIME_ACTIVATION_UNINSTALL:
return "uninstall";
default:
return "invalid";
}
}
static bool runtime_activation_request_encode(
uint8_t out[CBM_DAEMON_ACTIVATION_SHUTDOWN_REQUEST_SIZE],
cbm_daemon_runtime_activation_action_t action, const cbm_daemon_build_identity_t *identity) {
if (!out || !identity || !runtime_activation_action_valid(action)) {
return false;
}
memset(out, 0, CBM_DAEMON_ACTIVATION_SHUTDOWN_REQUEST_SIZE);
runtime_put_u32(out + ACTIVATION_REQUEST_ACTION_OFFSET, (uint32_t)action);
return cbm_daemon_runtime_hello_request_encode(out + ACTIVATION_REQUEST_IDENTITY_OFFSET,
identity);
}
static bool runtime_activation_request_decode(
const uint8_t payload[CBM_DAEMON_ACTIVATION_SHUTDOWN_REQUEST_SIZE],
cbm_daemon_runtime_activation_action_t *action_out, char version[CBM_DAEMON_VERSION_TEXT_SIZE],
char build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
if (!payload || !action_out || !version || !build) {
return false;
}
uint32_t raw_action = runtime_get_u32(payload + ACTIVATION_REQUEST_ACTION_OFFSET);
if (raw_action < (uint32_t)CBM_DAEMON_RUNTIME_ACTIVATION_INSTALL ||
raw_action > (uint32_t)CBM_DAEMON_RUNTIME_ACTIVATION_UNINSTALL ||
!runtime_rendezvous_request_decode(payload + ACTIVATION_REQUEST_IDENTITY_OFFSET, version,
build)) {
return false;
}
cbm_daemon_runtime_activation_action_t action =
(cbm_daemon_runtime_activation_action_t)raw_action;
*action_out = action;
return true;
}
static bool runtime_activation_response_encode(
uint8_t out[CBM_DAEMON_ACTIVATION_SHUTDOWN_RESPONSE_SIZE],
const cbm_daemon_runtime_activation_result_t *result) {
if (!out || !result) {
return false;
}
memset(out, 0, CBM_DAEMON_ACTIVATION_SHUTDOWN_RESPONSE_SIZE);
runtime_put_u32(out + ACTIVATION_RESPONSE_ABI_OFFSET, CBM_DAEMON_RENDEZVOUS_ABI);
runtime_put_u32(out + ACTIVATION_RESPONSE_ACCEPTED_OFFSET, result->accepted ? 1U : 0U);
runtime_put_u64(out + ACTIVATION_RESPONSE_CLIENTS_OFFSET, result->active_clients);
runtime_put_u64(out + ACTIVATION_RESPONSE_CONNECTIONS_OFFSET, result->active_connections);
return true;
}
static bool runtime_activation_response_decode(
const uint8_t payload[CBM_DAEMON_ACTIVATION_SHUTDOWN_RESPONSE_SIZE],
cbm_daemon_runtime_activation_result_t *result_out) {
if (!payload || !result_out ||
runtime_get_u32(payload + ACTIVATION_RESPONSE_ABI_OFFSET) != CBM_DAEMON_RENDEZVOUS_ABI) {
return false;
}
uint32_t accepted = runtime_get_u32(payload + ACTIVATION_RESPONSE_ACCEPTED_OFFSET);
if (accepted > 1U) {
return false;
}
result_out->accepted = accepted == 1U;
result_out->active_clients = runtime_get_u64(payload + ACTIVATION_RESPONSE_CLIENTS_OFFSET);
result_out->active_connections =
runtime_get_u64(payload + ACTIVATION_RESPONSE_CONNECTIONS_OFFSET);
return true;
}
static uint64_t runtime_current_process_id(void) {
#ifdef _WIN32
return (uint64_t)GetCurrentProcessId();
#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__)
return (uint64_t)getpid();
#else
return 0;
#endif
}
static void runtime_process_image_reference_init(runtime_process_image_reference_t *reference) {
if (!reference) {
return;
}
memset(reference, 0, sizeof(*reference));
#ifdef _WIN32
reference->file = INVALID_HANDLE_VALUE;
#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__)
reference->fd = -1;
#endif
}
static bool runtime_process_image_reference_release(runtime_process_image_reference_t *reference) {
if (!reference) {
return false;
}
bool ok = true;
#ifdef _WIN32
if (reference->file != INVALID_HANDLE_VALUE && !CloseHandle(reference->file)) {
ok = false;
}
#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__)
if (reference->fd >= 0 && close(reference->fd) != 0) {
ok = false;
}
#endif
runtime_process_image_reference_init(reference);
return ok;
}
#ifdef _WIN32
typedef struct {
FILETIME creation_time;
wchar_t path[32768];
} runtime_windows_process_image_snapshot_t;
static bool runtime_windows_process_image_snapshot(
HANDLE process, runtime_windows_process_image_snapshot_t *snapshot) {
if (!process || !snapshot) {
return false;
}
memset(snapshot, 0, sizeof(*snapshot));
FILETIME exit_time;
FILETIME kernel_time;
FILETIME user_time;
DWORD exit_code = 0;
DWORD path_length = (DWORD)(sizeof(snapshot->path) / sizeof(snapshot->path[0]));
bool ok = GetProcessTimes(process, &snapshot->creation_time, &exit_time, &kernel_time,
&user_time) != 0 &&
GetExitCodeProcess(process, &exit_code) != 0 && exit_code == STILL_ACTIVE &&
QueryFullProcessImageNameW(process, 0, snapshot->path, &path_length) != 0 &&
path_length > 0 &&
path_length < (DWORD)(sizeof(snapshot->path) / sizeof(snapshot->path[0]));
if (ok) {
snapshot->path[path_length] = L'\0';
}
return ok;
}
static bool runtime_windows_process_image_snapshot_same(
const runtime_windows_process_image_snapshot_t *first,
const runtime_windows_process_image_snapshot_t *second) {
return first && second && CompareFileTime(&first->creation_time, &second->creation_time) == 0 &&
CompareStringOrdinal(first->path, -1, second->path, -1, TRUE) == CSTR_EQUAL;
}
static bool runtime_windows_file_snapshot(HANDLE file, BY_HANDLE_FILE_INFORMATION *information,
LARGE_INTEGER *size) {
if (!information || !size) {
return false;
}
memset(information, 0, sizeof(*information));
size->QuadPart = -1;
return file != INVALID_HANDLE_VALUE && GetFileType(file) == FILE_TYPE_DISK &&
GetFileInformationByHandle(file, information) != 0 &&
(information->dwFileAttributes &
(FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) == 0 &&
GetFileSizeEx(file, size) != 0 && size->QuadPart >= 0;
}
static bool runtime_windows_file_snapshot_same(const BY_HANDLE_FILE_INFORMATION *first_information,
const LARGE_INTEGER *first_size,
const BY_HANDLE_FILE_INFORMATION *second_information,
const LARGE_INTEGER *second_size) {
return first_information && first_size && second_information && second_size &&
first_information->dwVolumeSerialNumber == second_information->dwVolumeSerialNumber &&
first_information->nFileIndexHigh == second_information->nFileIndexHigh &&
first_information->nFileIndexLow == second_information->nFileIndexLow &&
first_size->QuadPart == second_size->QuadPart &&
CompareFileTime(&first_information->ftLastWriteTime,
&second_information->ftLastWriteTime) == 0;
}
#elif defined(__APPLE__)
static bool runtime_mac_process_instance(int process_id, struct proc_bsdinfo *info) {
if (!info) {
return false;
}
memset(info, 0, sizeof(*info));
return proc_pidinfo(process_id, PROC_PIDTBSDINFO, 0, info, (int)sizeof(*info)) ==
(int)sizeof(*info) &&
info->pbi_pid == (uint32_t)process_id;
}
static bool runtime_mac_process_instance_same(const struct proc_bsdinfo *first,
const struct proc_bsdinfo *second) {
return first && second && first->pbi_pid == second->pbi_pid &&
first->pbi_start_tvsec == second->pbi_start_tvsec &&
first->pbi_start_tvusec == second->pbi_start_tvusec;
}
static bool runtime_mac_stat_matches_vnode(const struct stat *status,
const struct vinfo_stat *vnode) {
return status && vnode && S_ISREG(vnode->vst_mode) &&
vnode->vst_dev == (uint32_t)status->st_dev &&
vnode->vst_ino == (uint64_t)status->st_ino && vnode->vst_size == status->st_size &&
vnode->vst_mtime == status->st_mtimespec.tv_sec &&
vnode->vst_mtimensec == status->st_mtimespec.tv_nsec &&
vnode->vst_ctime == status->st_ctimespec.tv_sec &&
vnode->vst_ctimensec == status->st_ctimespec.tv_nsec;
}
static bool runtime_mac_stat_same(const struct stat *first, const struct stat *second) {
return first && second && first->st_dev == second->st_dev && first->st_ino == second->st_ino &&
first->st_size == second->st_size &&
first->st_mtimespec.tv_sec == second->st_mtimespec.tv_sec &&
first->st_mtimespec.tv_nsec == second->st_mtimespec.tv_nsec &&
first->st_ctimespec.tv_sec == second->st_ctimespec.tv_sec &&
first->st_ctimespec.tv_nsec == second->st_ctimespec.tv_nsec;
}
static bool runtime_mac_process_maps_file_executable(int process_id, const struct stat *status) {
uint64_t address = 0;
for (size_t region_count = 0; region_count < 65536; region_count++) {
struct proc_regionwithpathinfo region;
memset(&region, 0, sizeof(region));
int received =
proc_pidinfo(process_id, PROC_PIDREGIONPATHINFO, address, &region, (int)sizeof(region));
if (received != (int)sizeof(region)) {
return false;
}
if ((region.prp_prinfo.pri_protection & VM_PROT_EXECUTE) != 0 &&
runtime_mac_stat_matches_vnode(status, &region.prp_vip.vip_vi.vi_stat)) {
return true;
}
uint64_t region_address = region.prp_prinfo.pri_address;
uint64_t region_size = region.prp_prinfo.pri_size;
if (region_size == 0 || region_address > UINT64_MAX - region_size) {
return false;
}
uint64_t next = region_address + region_size;
if (next <= address) {
return false;
}
address = next;
}
return false;
}
#elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__)
static bool runtime_posix_stat_same_image(const struct stat *first, const struct stat *second) {
return first && second && S_ISREG(first->st_mode) && S_ISREG(second->st_mode) &&
first->st_dev == second->st_dev && first->st_ino == second->st_ino &&
first->st_size == second->st_size && first->st_mtim.tv_sec == second->st_mtim.tv_sec &&
first->st_mtim.tv_nsec == second->st_mtim.tv_nsec &&
first->st_ctim.tv_sec == second->st_ctim.tv_sec &&
first->st_ctim.tv_nsec == second->st_ctim.tv_nsec;
}
#endif
/* ── Executable-image fingerprint cache ──────────────────────────────────────
* The build-identity fingerprint is a SHA-256 over the entire executable
* (cbm_daemon_build_fingerprint_native_file), a full read of a ~300 MB image.
* A daemon process otherwise pays it twice for its own image at start (the
* supervisor's startup capture, then the runtime service's active-image check),
* so the digest of this process's OWN image is cached, keyed by the file
* identity the acquire path verifies as stable across the hash: device, inode,
* size, mtime and change time (on Windows FILE_BASIC_INFO.ChangeTime). A peer's
* image is never cached: it is hashed on every fingerprinting acquire
* (re-hashing of a verified peer copy is avoided separately, #1955).
*
* Limiting the cache to the own image is what makes that key sufficient. On
* POSIX the change time moves on every write and cannot be set back. A Windows
* change time can be set through SetFileInformationByHandle, but Windows keeps
* a running image locked against modification for as long as the process, and
* so this cache, exists. A rebuilt or replaced binary rolls the key and misses,
* so the cache never returns a stale digest and build-cohort admission keeps
* its exact meaning. One entry suffices: a process has one own image.
*
* The cache is process-local: a fresh process (a hook, a CLI call) still hashes
* its own image once. A hit does not weaken the identity proof: the acquire
* chain still brackets the held image with its before/after stat and
* process-maps checks, so the returned digest is bound to that same verified
* image; only the re-read of bytes whose identity is unchanged is skipped. A
* freshly hashed digest is committed only after that post-hash check has
* passed, under the identity observed after the hash, so an image that changed
* under the hash is never cached. */
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) || \
defined(__NetBSD__)
/* POSIX carries timespec fields; Windows stores FILETIME ticks in the seconds
* fields and leaves the nanoseconds fields zero. */
typedef struct {
bool valid;
uint64_t device;
uint64_t inode;
uint64_t size;
int64_t mtime_seconds;
int64_t mtime_nanoseconds;
int64_t ctime_seconds;
int64_t ctime_nanoseconds;
} runtime_fingerprint_key_t;
/* The single entry: this process's own image. */
static atomic_flag runtime_fingerprint_cache_lock = ATOMIC_FLAG_INIT;
static runtime_fingerprint_key_t runtime_fingerprint_cache_key;
static char runtime_fingerprint_cache_digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
#ifdef CBM_ENABLE_TEST_SEAMS
static atomic_int runtime_fingerprint_hash_call_count;
static atomic_bool runtime_fingerprint_hash_stub_active;
static char runtime_fingerprint_hash_stub_digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
#endif
static bool runtime_fingerprint_key_equal(const runtime_fingerprint_key_t *a,
const runtime_fingerprint_key_t *b) {
return a->valid && b->valid && a->device == b->device && a->inode == b->inode &&
a->size == b->size && a->mtime_seconds == b->mtime_seconds &&
a->mtime_nanoseconds == b->mtime_nanoseconds && a->ctime_seconds == b->ctime_seconds &&
a->ctime_nanoseconds == b->ctime_nanoseconds;
}
static void runtime_fingerprint_cache_enter(void) {
while (
atomic_flag_test_and_set_explicit(&runtime_fingerprint_cache_lock, memory_order_acquire)) {}
}
static void runtime_fingerprint_cache_leave(void) {
atomic_flag_clear_explicit(&runtime_fingerprint_cache_lock, memory_order_release);
}
static bool runtime_fingerprint_cache_lookup(const runtime_fingerprint_key_t *key,
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
if (!key->valid) {
return false;
}
runtime_fingerprint_cache_enter();
bool hit = runtime_fingerprint_key_equal(&runtime_fingerprint_cache_key, key);
if (hit) {
memcpy(out, runtime_fingerprint_cache_digest, CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
}
runtime_fingerprint_cache_leave();
return hit;
}
static void runtime_fingerprint_cache_store(
const runtime_fingerprint_key_t *key,
const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
if (!key->valid || fingerprint[0] == '\0') {
return;
}
runtime_fingerprint_cache_enter();
runtime_fingerprint_cache_key = *key;
memcpy(runtime_fingerprint_cache_digest, fingerprint, CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
runtime_fingerprint_cache_leave();
}
/* The single point where the expensive image hash is paid; a test seam counts
* calls and can substitute a stub digest so the cache's hit/miss behaviour is
* observable without a real full-image read. */
static bool runtime_fingerprint_hash_native_file(uintptr_t native_file,
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
#ifdef CBM_ENABLE_TEST_SEAMS
atomic_fetch_add_explicit(&runtime_fingerprint_hash_call_count, 1, memory_order_relaxed);
if (atomic_load_explicit(&runtime_fingerprint_hash_stub_active, memory_order_acquire)) {
memcpy(out, runtime_fingerprint_hash_stub_digest, CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
return true;
}
#endif
return cbm_daemon_build_fingerprint_native_file(native_file, out);
}
/* Resolve the digest for the image identity `key` of process `process_id`:
* from the cache when that process is this one and the cache already holds
* that identity, otherwise by hashing the held native image. `cached` reports
* which. Every acquire site resolves through here, so this is where the cache
* is limited to the own image: a peer's key is marked invalid, which is never
* looked up and never committed. A freshly hashed digest is deliberately not
* stored here: the acquire site commits it (runtime_fingerprint_cache_commit)
* only once its post-hash identity check has proven the hashed bytes belong to
* the identity they were keyed on. */
static bool runtime_build_fingerprint_resolve(uintptr_t native_file, uint64_t process_id,
runtime_fingerprint_key_t *key,
char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE],
bool *cached) {
if (process_id != runtime_current_process_id()) {
key->valid = false;
}
*cached = runtime_fingerprint_cache_lookup(key, fingerprint);
return *cached || runtime_fingerprint_hash_native_file(native_file, fingerprint);
}
/* Commit a freshly hashed digest after the acquire chain has passed. The
* identity observed after the hash must equal the one the digest was resolved
* under; otherwise the bytes read belong to no single identity and are not
* cached. A cache hit has nothing to commit, and neither has a peer, whose
* resolved key is invalid. */
static void runtime_fingerprint_cache_commit(
const runtime_fingerprint_key_t *before, const runtime_fingerprint_key_t *after, bool cached,
const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
if (!cached && runtime_fingerprint_key_equal(before, after)) {
runtime_fingerprint_cache_store(after, fingerprint);
}
}
#ifdef _WIN32
static int64_t runtime_fingerprint_filetime(FILETIME value) {
return (int64_t)(((uint64_t)value.dwHighDateTime << 32) | value.dwLowDateTime);
}
/* Key the held image by the snapshot identity the acquire path compares plus
* the kernel-maintained change time. An unreadable change time yields an
* invalid key, which never hits and is never stored: the image is simply
* hashed. */
static void runtime_fingerprint_key_windows(HANDLE file,
const BY_HANDLE_FILE_INFORMATION *information,
const LARGE_INTEGER *size,
runtime_fingerprint_key_t *key) {
memset(key, 0, sizeof(*key));
FILE_BASIC_INFO basic;
memset(&basic, 0, sizeof(basic));
key->valid = information != NULL && size != NULL && size->QuadPart >= 0 &&
file != INVALID_HANDLE_VALUE &&
GetFileInformationByHandleEx(file, FileBasicInfo, &basic, sizeof(basic)) != 0;
if (key->valid) {
key->device = information->dwVolumeSerialNumber;
key->inode = ((uint64_t)information->nFileIndexHigh << 32) | information->nFileIndexLow;
key->size = (uint64_t)size->QuadPart;
key->mtime_seconds = runtime_fingerprint_filetime(information->ftLastWriteTime);
key->ctime_seconds = basic.ChangeTime.QuadPart;
}
}
static bool runtime_build_fingerprint_resolve_windows(
HANDLE file, uint64_t process_id, const BY_HANDLE_FILE_INFORMATION *information,
const LARGE_INTEGER *size, char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE],
runtime_fingerprint_key_t *key, bool *cached) {
runtime_fingerprint_key_windows(file, information, size, key);
return runtime_build_fingerprint_resolve((uintptr_t)file, process_id, key, fingerprint, cached);
}
static void runtime_fingerprint_cache_commit_windows(
HANDLE file, const BY_HANDLE_FILE_INFORMATION *information, const LARGE_INTEGER *size,
const runtime_fingerprint_key_t *key, bool cached,
const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
runtime_fingerprint_key_t key_after;
runtime_fingerprint_key_windows(file, information, size, &key_after);
runtime_fingerprint_cache_commit(key, &key_after, cached, fingerprint);
}
#else
static void runtime_fingerprint_key_posix(const struct stat *status,
runtime_fingerprint_key_t *key) {
memset(key, 0, sizeof(*key));
key->valid = status != NULL && S_ISREG(status->st_mode);
if (key->valid) {
key->device = (uint64_t)status->st_dev;
key->inode = (uint64_t)status->st_ino;
key->size = (uint64_t)status->st_size;
#ifdef __APPLE__
key->mtime_seconds = status->st_mtimespec.tv_sec;
key->mtime_nanoseconds = status->st_mtimespec.tv_nsec;
key->ctime_seconds = status->st_ctimespec.tv_sec;
key->ctime_nanoseconds = status->st_ctimespec.tv_nsec;
#else
key->mtime_seconds = status->st_mtim.tv_sec;
key->mtime_nanoseconds = status->st_mtim.tv_nsec;
key->ctime_seconds = status->st_ctim.tv_sec;
key->ctime_nanoseconds = status->st_ctim.tv_nsec;
#endif
}
}
static bool runtime_build_fingerprint_resolve_posix(
uintptr_t native_file, uint64_t process_id, const struct stat *status,
char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE], runtime_fingerprint_key_t *key,
bool *cached) {
runtime_fingerprint_key_posix(status, key);
return runtime_build_fingerprint_resolve(native_file, process_id, key, fingerprint, cached);
}
static void runtime_fingerprint_cache_commit_posix(
const struct stat *status, const runtime_fingerprint_key_t *key, bool cached,
const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
runtime_fingerprint_key_t key_after;
runtime_fingerprint_key_posix(status, &key_after);
runtime_fingerprint_cache_commit(key, &key_after, cached, fingerprint);
}
#endif
#ifdef CBM_ENABLE_TEST_SEAMS
void cbm_daemon_runtime_fingerprint_cache_reset_for_testing(void) {
runtime_fingerprint_cache_enter();
memset(&runtime_fingerprint_cache_key, 0, sizeof(runtime_fingerprint_cache_key));
memset(runtime_fingerprint_cache_digest, 0, sizeof(runtime_fingerprint_cache_digest));
runtime_fingerprint_cache_leave();
atomic_store_explicit(&runtime_fingerprint_hash_call_count, 0, memory_order_relaxed);
atomic_store_explicit(&runtime_fingerprint_hash_stub_active, false, memory_order_release);
memset(runtime_fingerprint_hash_stub_digest, 0, sizeof(runtime_fingerprint_hash_stub_digest));
}
void cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(const char *digest) {
if (!digest) {
atomic_store_explicit(&runtime_fingerprint_hash_stub_active, false, memory_order_release);
return;
}
memset(runtime_fingerprint_hash_stub_digest, 0, sizeof(runtime_fingerprint_hash_stub_digest));
size_t length = strlen(digest);
if (length >= CBM_DAEMON_BUILD_FINGERPRINT_SIZE) {
length = CBM_DAEMON_BUILD_FINGERPRINT_SIZE - 1;
}
memcpy(runtime_fingerprint_hash_stub_digest, digest, length);
atomic_store_explicit(&runtime_fingerprint_hash_stub_active, true, memory_order_release);
}
int cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(void) {
return atomic_load_explicit(&runtime_fingerprint_hash_call_count, memory_order_relaxed);
}
static runtime_fingerprint_key_t runtime_fingerprint_key_for_testing(
const cbm_daemon_runtime_fingerprint_identity_for_testing_t *identity) {
runtime_fingerprint_key_t key = {.valid = true,
.device = identity->device,
.inode = identity->inode,
.size = identity->size,
.mtime_seconds = identity->mtime_seconds,
.mtime_nanoseconds = identity->mtime_nanoseconds,
.ctime_seconds = identity->ctime_seconds,
.ctime_nanoseconds = identity->ctime_nanoseconds};
return key;
}
bool cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(
const cbm_daemon_runtime_fingerprint_identity_for_testing_t *before,
const cbm_daemon_runtime_fingerprint_identity_for_testing_t *after,
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
/* There is no native image behind a synthetic identity: only the stubbed
* hash may ever be reached from here. The identity stands for this
* process's own image, the only one the cache serves. */
if (!before || !after || !out ||
!atomic_load_explicit(&runtime_fingerprint_hash_stub_active, memory_order_acquire)) {
return false;
}
runtime_fingerprint_key_t key_before = runtime_fingerprint_key_for_testing(before);
runtime_fingerprint_key_t key_after = runtime_fingerprint_key_for_testing(after);
bool cached = false;
if (!runtime_build_fingerprint_resolve((uintptr_t)0, runtime_current_process_id(), &key_before,
out, &cached)) {
return false;
}
runtime_fingerprint_cache_commit(&key_before, &key_after, cached, out);
return true;
}
#endif
#endif /* supported platform: executable-image fingerprint cache */
/* Acquire one process instance's mapped image as a native object. Supplying a
* fingerprint hashes that same held object; NULL performs metadata-only
* acquisition for the HELLO fast path. Every platform brackets the process
* identity/image lookup before returning the retained reference. */
static bool runtime_process_image_reference_acquire(
uint64_t process_id, runtime_process_image_reference_t *reference,
char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
if (!reference || process_id == 0) {
return false;
}
runtime_process_image_reference_init(reference);
if (fingerprint) {
fingerprint[0] = '\0';
}
#ifdef _WIN32
if (process_id > MAXDWORD) {
return false;
}
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD)process_id);
if (!process) {
return false;
}
runtime_windows_process_image_snapshot_t process_before;
runtime_windows_process_image_snapshot_t process_after;
BY_HANDLE_FILE_INFORMATION file_before;
BY_HANDLE_FILE_INFORMATION file_after;
LARGE_INTEGER size_before;
LARGE_INTEGER size_after;
runtime_fingerprint_key_t fingerprint_key = {0};
bool fingerprint_cached = false;
bool ok = runtime_windows_process_image_snapshot(process, &process_before);
/* QueryFullProcessImageNameW returns a stable identity spelling for the
* before/after comparison, but its Win32/DOS form may exceed MAX_PATH.
* Keep that snapshot byte-for-byte and use an owned extended spelling only
* at the file-API boundary. */
wchar_t *open_path = ok ? cbm_wide_path_to_extended(process_before.path) : NULL;
HANDLE file = open_path
? CreateFileW(open_path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_SEQUENTIAL_SCAN, NULL)
: INVALID_HANDLE_VALUE;
free(open_path);
ok = ok && runtime_windows_file_snapshot(file, &file_before, &size_before) &&
(!fingerprint || runtime_build_fingerprint_resolve_windows(
file, process_id, &file_before, &size_before, fingerprint,
&fingerprint_key, &fingerprint_cached)) &&
runtime_windows_file_snapshot(file, &file_after, &size_after) &&
runtime_windows_file_snapshot_same(&file_before, &size_before, &file_after, &size_after) &&
runtime_windows_process_image_snapshot(process, &process_after) &&
runtime_windows_process_image_snapshot_same(&process_before, &process_after);
if (!CloseHandle(process)) {
ok = false;
}
if (ok) {
if (fingerprint) {
runtime_fingerprint_cache_commit_windows(
file, &file_after, &size_after, &fingerprint_key, fingerprint_cached, fingerprint);
}
reference->held = true;
reference->file = file;
reference->information = file_after;
reference->size = size_after;
} else if (file != INVALID_HANDLE_VALUE) {
(void)CloseHandle(file);
}
#elif defined(__APPLE__)
if (process_id > INT_MAX) {
return false;
}
int pid = (int)process_id;
struct proc_bsdinfo process_before;
struct proc_bsdinfo process_after;
char path[PROC_PIDPATHINFO_MAXSIZE];
int path_length = 0;
bool ok = runtime_mac_process_instance(pid, &process_before) &&
(path_length = proc_pidpath(pid, path, sizeof(path))) > 0 &&
path_length < (int)sizeof(path);
if (ok) {
path[path_length] = '\0';
}
int fd = ok ? open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK) : -1;
struct stat file_before;
struct stat file_after;
runtime_fingerprint_key_t fingerprint_key = {0};
bool fingerprint_cached = false;
ok = ok && fd >= 0 && fstat(fd, &file_before) == 0 && S_ISREG(file_before.st_mode) &&
runtime_mac_process_maps_file_executable(pid, &file_before) &&
(!fingerprint || runtime_build_fingerprint_resolve_posix(
(uintptr_t)fd, process_id, &file_before, fingerprint,
&fingerprint_key, &fingerprint_cached)) &&
fstat(fd, &file_after) == 0 && runtime_mac_stat_same(&file_before, &file_after) &&
runtime_mac_process_maps_file_executable(pid, &file_after) &&
runtime_mac_process_instance(pid, &process_after) &&
runtime_mac_process_instance_same(&process_before, &process_after);
if (ok) {
if (fingerprint) {
runtime_fingerprint_cache_commit_posix(&file_after, &fingerprint_key,
fingerprint_cached, fingerprint);
}
reference->held = true;
reference->fd = fd;
reference->status = file_after;
} else if (fd >= 0) {
(void)close(fd);
}
#elif defined(__linux__)
char proc_path[64];
int written =
snprintf(proc_path, sizeof(proc_path), "/proc/%llu", (unsigned long long)process_id);
if (written <= 0 || written >= (int)sizeof(proc_path)) {
return false;
}
int process_fd = open(proc_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
int image_fd = process_fd >= 0 ? openat(process_fd, "exe", O_RDONLY | O_CLOEXEC) : -1;
struct stat image_before;
struct stat image_after;
runtime_fingerprint_key_t fingerprint_key = {0};
bool fingerprint_cached = false;
bool ok = image_fd >= 0 && fstat(image_fd, &image_before) == 0 &&
S_ISREG(image_before.st_mode) &&
(!fingerprint || runtime_build_fingerprint_resolve_posix(
(uintptr_t)image_fd, process_id, &image_before, fingerprint,
&fingerprint_key, &fingerprint_cached)) &&
fstat(image_fd, &image_after) == 0 &&
runtime_posix_stat_same_image(&image_before, &image_after);
int verify_fd = ok ? openat(process_fd, "exe", O_RDONLY | O_CLOEXEC) : -1;
struct stat verify_status;
ok = ok && verify_fd >= 0 && fstat(verify_fd, &verify_status) == 0 &&
runtime_posix_stat_same_image(&image_after, &verify_status);
if (verify_fd >= 0 && close(verify_fd) != 0) {
ok = false;
}
if (process_fd >= 0 && close(process_fd) != 0) {
ok = false;
}
if (ok) {
if (fingerprint) {
runtime_fingerprint_cache_commit_posix(&image_after, &fingerprint_key,
fingerprint_cached, fingerprint);
}
reference->held = true;
reference->fd = image_fd;
reference->status = image_after;
} else if (image_fd >= 0) {
(void)close(image_fd);
}
#elif defined(__FreeBSD__) || defined(__NetBSD__)
if (process_id > INT_MAX) {
return false;
}
int pid = (int)process_id;
#if defined(__NetBSD__)
int mib[4] = {CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_PATHNAME};
#else
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, pid};
#endif
char path[PATH_MAX];
size_t path_length = sizeof(path);
bool ok = sysctl(mib, 4, path, &path_length, NULL, 0) == 0 && path_length > 0;
if (ok) {
path[path_length < sizeof(path) ? path_length : sizeof(path) - 1] = '\0';
}
int image_fd = ok ? open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK) : -1;
struct stat image_before;
struct stat image_after;
runtime_fingerprint_key_t fingerprint_key = {0};
bool fingerprint_cached = false;
ok = image_fd >= 0 && fstat(image_fd, &image_before) == 0 && S_ISREG(image_before.st_mode) &&
(!fingerprint || runtime_build_fingerprint_resolve_posix(
(uintptr_t)image_fd, process_id, &image_before, fingerprint,
&fingerprint_key, &fingerprint_cached)) &&
fstat(image_fd, &image_after) == 0 &&
runtime_posix_stat_same_image(&image_before, &image_after);
if (ok) {
if (fingerprint) {
runtime_fingerprint_cache_commit_posix(&image_after, &fingerprint_key,
fingerprint_cached, fingerprint);
}
reference->held = true;
reference->fd = image_fd;
reference->status = image_after;
} else if (image_fd >= 0) {
(void)close(image_fd);
}
#else
(void)process_id;
bool ok = false;
#endif
if (!ok && fingerprint) {
fingerprint[0] = '\0';
}
return ok;
}
/* True when a retained reference is still the unchanged image it captured AND
* the freshly acquired peer maps that same image. Metadata only: no hashing. */
static bool runtime_process_image_reference_same(const runtime_process_image_reference_t *held,
const runtime_process_image_reference_t *peer) {
if (!held || !held->held || !peer || !peer->held) {
return false;
}
#ifdef _WIN32
BY_HANDLE_FILE_INFORMATION held_now;
LARGE_INTEGER held_size_now;
return runtime_windows_file_snapshot(held->file, &held_now, &held_size_now) &&
runtime_windows_file_snapshot_same(&held->information, &held->size, &held_now,
&held_size_now) &&
runtime_windows_file_snapshot_same(&held->information, &held->size, &peer->information,
&peer->size);
#elif defined(__APPLE__)
struct stat held_now;
return fstat(held->fd, &held_now) == 0 && runtime_mac_stat_same(&held->status, &held_now) &&
runtime_mac_stat_same(&held->status, &peer->status);
#elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__)
struct stat held_now;
return fstat(held->fd, &held_now) == 0 &&
runtime_posix_stat_same_image(&held->status, &held_now) &&
runtime_posix_stat_same_image(&held->status, &peer->status);
#else
return false;
#endif
}
/* A false result means only "not proven identical". The caller must use the
* full fingerprint fallback before rejecting the peer. */
static bool runtime_process_image_reference_matches_process(
const runtime_process_image_reference_t *active, uint64_t process_id) {
if (!active || !active->held || process_id == 0) {
return false;
}
runtime_process_image_reference_t peer;
runtime_process_image_reference_init(&peer);
bool same = runtime_process_image_reference_acquire(process_id, &peer, NULL) &&
runtime_process_image_reference_same(active, &peer);
bool released = runtime_process_image_reference_release(&peer);
return same && released;
}
bool cbm_daemon_runtime_process_build_fingerprint(uint64_t process_id,
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
if (!out) {
return false;
}
out[0] = '\0';
runtime_process_image_reference_t reference;
runtime_process_image_reference_init(&reference);
bool ok = runtime_process_image_reference_acquire(process_id, &reference, out);
if (!runtime_process_image_reference_release(&reference)) {
ok = false;
}
if (!ok) {
out[0] = '\0';
}
return ok;
}
typedef enum {
RUNTIME_PEER_IMAGE_VERIFIED,
RUNTIME_PEER_IMAGE_UNVERIFIABLE,
RUNTIME_PEER_IMAGE_MISMATCH,
} runtime_peer_image_verdict_t;
static bool runtime_verified_peer_image_known_locked(
const cbm_daemon_runtime_service_t *service, const runtime_process_image_reference_t *peer) {
for (size_t i = 0; i < service->verified_peer_image_count; i++) {
if (runtime_process_image_reference_same(&service->verified_peer_images[i], peer)) {
return true;
}
}
return false;
}
/* #1955: the HELLO must prove the peer runs this daemon's build. The cheap
* proof is "same image file as the daemon"; a peer running a DIFFERENT file
* with identical bytes (a second install path, a package-manager copy) needs
* the full fingerprint, i.e. hashing the whole (~300 MB) image. That hash is
* slower than the client's HELLO budget, so the client abandoned it and every
* re-probe started it again - admission never completed against a healthy
* daemon. A copy is now hashed once: its verified reference is retained and
* later admissions of the same unchanged file match it by metadata, exactly
* like the active image. The accepted set of images is unchanged. */
static runtime_peer_image_verdict_t runtime_peer_image_verify(cbm_daemon_runtime_service_t *service,
uint64_t process_id,
const char *requested_build) {
runtime_process_image_reference_t peer;
runtime_process_image_reference_init(&peer);
bool peer_held = runtime_process_image_reference_acquire(process_id, &peer, NULL);
bool known = false;
if (peer_held) {
known = runtime_process_image_reference_same(&service->active_image, &peer);
#ifdef CBM_ENABLE_TEST_SEAMS
if (atomic_load(&runtime_force_peer_image_distinct_copy_seam)) {
known = false;
}
#endif
if (!known) {
cbm_mutex_lock(&service->mutex);
known = runtime_verified_peer_image_known_locked(service, &peer);
cbm_mutex_unlock(&service->mutex);
}
}
bool released = runtime_process_image_reference_release(&peer);
if (known && released) {
return RUNTIME_PEER_IMAGE_VERIFIED;
}
char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
runtime_process_image_reference_t hashed;
runtime_process_image_reference_init(&hashed);
runtime_note_peer_image_hash();
bool fingerprinted = runtime_process_image_reference_acquire(process_id, &hashed, fingerprint);
bool verified = fingerprinted && strcmp(fingerprint, requested_build) == 0 &&
strcmp(fingerprint, service->identity.build_fingerprint) == 0;
if (verified) {
cbm_mutex_lock(&service->mutex);
if (service->verified_peer_image_count < RUNTIME_VERIFIED_PEER_IMAGE_CAPACITY &&
!runtime_verified_peer_image_known_locked(service, &hashed)) {
service->verified_peer_images[service->verified_peer_image_count++] = hashed;
runtime_process_image_reference_init(&hashed);
}
cbm_mutex_unlock(&service->mutex);
}
(void)runtime_process_image_reference_release(&hashed);
if (verified) {
return RUNTIME_PEER_IMAGE_VERIFIED;
}
return fingerprinted ? RUNTIME_PEER_IMAGE_MISMATCH : RUNTIME_PEER_IMAGE_UNVERIFIABLE;
}
static void runtime_verified_peer_images_release(cbm_daemon_runtime_service_t *service) {
for (size_t i = 0; i < service->verified_peer_image_count; i++) {
(void)runtime_process_image_reference_release(&service->verified_peer_images[i]);
}
service->verified_peer_image_count = 0;
}
static bool runtime_hello_response_encode(uint8_t out[CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE],
const cbm_daemon_runtime_connect_result_t *result) {
if (!out || !result) {
return false;
}
memset(out, 0, CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE);
runtime_put_u32(out + RENDEZVOUS_RESPONSE_CONNECT_STATUS_OFFSET, (uint32_t)result->status);
runtime_put_u32(out + RENDEZVOUS_RESPONSE_HELLO_STATUS_OFFSET, (uint32_t)result->hello_status);
runtime_put_u64(out + RENDEZVOUS_RESPONSE_CLIENT_ID_OFFSET, result->client_id);
runtime_put_u64(out + RENDEZVOUS_RESPONSE_PROCESS_ID_OFFSET, result->authenticated_process_id);
runtime_put_u32(out + RENDEZVOUS_RESPONSE_CONFLICT_STATUS_OFFSET,
(uint32_t)result->conflict.status);
if (!runtime_wire_string_encode(out + RENDEZVOUS_RESPONSE_ACTIVE_VERSION_OFFSET,
CBM_DAEMON_RENDEZVOUS_VERSION_TEXT_CAP,
result->conflict.active_version, true) ||
!runtime_wire_string_encode(out + RENDEZVOUS_RESPONSE_ACTIVE_BUILD_OFFSET,
CBM_DAEMON_RENDEZVOUS_BUILD_FINGERPRINT_CAP,
result->conflict.active_build_fingerprint, true) ||
!runtime_wire_string_encode(out + RENDEZVOUS_RESPONSE_REQUESTED_VERSION_OFFSET,
CBM_DAEMON_RENDEZVOUS_VERSION_TEXT_CAP,
result->conflict.requested_version, true) ||
!runtime_wire_string_encode(out + RENDEZVOUS_RESPONSE_REQUESTED_BUILD_OFFSET,
CBM_DAEMON_RENDEZVOUS_BUILD_FINGERPRINT_CAP,
result->conflict.requested_build_fingerprint, true) ||
!runtime_wire_string_encode(out + RENDEZVOUS_RESPONSE_MESSAGE_OFFSET,
CBM_DAEMON_RENDEZVOUS_MESSAGE_CAP, result->message, true)) {
return false;
}
return true;
}
static bool runtime_hello_response_decode(
const uint8_t payload[CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE],
cbm_daemon_runtime_connect_result_t *result) {
if (!payload || !result) {
return false;
}
memset(result, 0, sizeof(*result));
result->status = (cbm_daemon_runtime_connect_status_t)runtime_get_u32(
payload + RENDEZVOUS_RESPONSE_CONNECT_STATUS_OFFSET);
result->hello_status = (cbm_daemon_hello_status_t)runtime_get_u32(
payload + RENDEZVOUS_RESPONSE_HELLO_STATUS_OFFSET);
result->client_id = runtime_get_u64(payload + RENDEZVOUS_RESPONSE_CLIENT_ID_OFFSET);
result->authenticated_process_id =
runtime_get_u64(payload + RENDEZVOUS_RESPONSE_PROCESS_ID_OFFSET);
result->conflict.status = (cbm_daemon_hello_status_t)runtime_get_u32(
payload + RENDEZVOUS_RESPONSE_CONFLICT_STATUS_OFFSET);
if (!runtime_wire_string_decode(payload + RENDEZVOUS_RESPONSE_ACTIVE_VERSION_OFFSET,
CBM_DAEMON_RENDEZVOUS_VERSION_TEXT_CAP,
result->conflict.active_version, true) ||
!runtime_wire_string_decode(payload + RENDEZVOUS_RESPONSE_ACTIVE_BUILD_OFFSET,
CBM_DAEMON_RENDEZVOUS_BUILD_FINGERPRINT_CAP,
result->conflict.active_build_fingerprint, true) ||
!runtime_wire_string_decode(payload + RENDEZVOUS_RESPONSE_REQUESTED_VERSION_OFFSET,
CBM_DAEMON_RENDEZVOUS_VERSION_TEXT_CAP,
result->conflict.requested_version, true) ||
!runtime_wire_string_decode(payload + RENDEZVOUS_RESPONSE_REQUESTED_BUILD_OFFSET,
CBM_DAEMON_RENDEZVOUS_BUILD_FINGERPRINT_CAP,
result->conflict.requested_build_fingerprint, true) ||
!runtime_wire_string_decode(payload + RENDEZVOUS_RESPONSE_MESSAGE_OFFSET,
CBM_DAEMON_RENDEZVOUS_MESSAGE_CAP, result->message, true)) {
return false;
}
if (result->status == CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED) {
return result->hello_status == CBM_DAEMON_HELLO_COMPATIBLE &&
result->conflict.status == CBM_DAEMON_HELLO_COMPATIBLE &&
result->client_id != CBM_DAEMON_CLIENT_ID_INVALID &&
result->authenticated_process_id != 0;
}
if (result->status == CBM_DAEMON_RUNTIME_CONNECT_CONFLICT) {
char expected[CBM_DAEMON_RENDEZVOUS_MESSAGE_CAP];
return result->hello_status >= CBM_DAEMON_HELLO_VERSION_CONFLICT &&
result->hello_status <= CBM_DAEMON_HELLO_FEATURE_ABI_CONFLICT &&
result->conflict.status == result->hello_status &&
result->client_id == CBM_DAEMON_CLIENT_ID_INVALID &&
result->authenticated_process_id == 0 &&
cbm_daemon_conflict_format(&result->conflict, expected, sizeof(expected)) &&
strcmp(result->message, expected) == 0;
}
return result->status == CBM_DAEMON_RUNTIME_CONNECT_REJECTED &&
result->hello_status == CBM_DAEMON_HELLO_INVALID &&
result->conflict.status == CBM_DAEMON_HELLO_INVALID &&
result->client_id == CBM_DAEMON_CLIENT_ID_INVALID &&
result->authenticated_process_id == 0;
}
static bool runtime_send_hello_response(cbm_daemon_ipc_connection_t *connection,
const cbm_daemon_runtime_connect_result_t *result) {
uint8_t payload[CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE];
return runtime_hello_response_encode(payload, result) &&
cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_RESPONSE,
CBM_DAEMON_RUNTIME_OP_HELLO, payload,
(uint32_t)sizeof(payload));
}
static bool runtime_worker_send_frame(cbm_daemon_runtime_worker_t *worker,
cbm_daemon_frame_type_t type,
cbm_daemon_runtime_operation_t operation, const void *payload,
uint32_t length) {
if (!worker) {
return false;
}
cbm_mutex_lock(&worker->send_mutex);
bool sent =
worker->connection &&
cbm_daemon_ipc_send_frame(worker->connection, type, (uint16_t)operation, payload, length);
cbm_mutex_unlock(&worker->send_mutex);
return sent;
}
static bool runtime_worker_send_status(cbm_daemon_runtime_worker_t *worker,
cbm_daemon_runtime_operation_t operation, bool success) {
uint8_t payload[STATUS_RESPONSE_SIZE];
runtime_put_u32(payload, success ? 1U : 0U);
return runtime_worker_send_frame(worker, CBM_DAEMON_FRAME_RESPONSE, operation, payload,
(uint32_t)sizeof(payload));
}
static bool runtime_application_status_is_callback_result(
cbm_daemon_runtime_application_status_t status) {
return status == CBM_DAEMON_RUNTIME_APPLICATION_OK ||
status == CBM_DAEMON_RUNTIME_APPLICATION_REJECTED ||
status == CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR ||
status == CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED;
}
static bool runtime_worker_send_application_response(
cbm_daemon_runtime_worker_t *worker, cbm_daemon_runtime_application_token_t request_token,
cbm_daemon_runtime_application_status_t status, const uint8_t *response,
uint32_t response_length, bool suppress_when_disconnecting) {
if (!worker || request_token == CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID ||
status <= CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR ||
status > CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED ||
response_length > CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX ||
(response_length > 0 && !response)) {
return false;
}
uint64_t wire_length = APPLICATION_RESPONSE_PREFIX_SIZE + (uint64_t)response_length;
if (wire_length > CBM_DAEMON_MAX_FRAME_SIZE || wire_length > UINT32_MAX) {
return false;
}
uint8_t *wire = malloc((size_t)wire_length);
if (!wire) {
return false;
}
runtime_put_u64(wire, request_token);
runtime_put_u32(wire + 8, (uint32_t)status);
runtime_put_u32(wire + 12, response_length);
if (response_length > 0) {
memcpy(wire + APPLICATION_RESPONSE_PREFIX_SIZE, response, response_length);
}
cbm_mutex_lock(&worker->send_mutex);
bool disconnected = suppress_when_disconnecting &&
atomic_load_explicit(&worker->disconnecting, memory_order_acquire);
bool sent = !disconnected && worker->connection &&
cbm_daemon_ipc_send_frame(worker->connection, CBM_DAEMON_FRAME_RESPONSE,
CBM_DAEMON_RUNTIME_OP_APPLICATION_REQUEST, wire,
(uint32_t)wire_length);
cbm_mutex_unlock(&worker->send_mutex);
free(wire);
return sent;
}
static void runtime_result_rejected(cbm_daemon_runtime_connect_result_t *result,
const char *message) {
memset(result, 0, sizeof(*result));
result->status = CBM_DAEMON_RUNTIME_CONNECT_REJECTED;
result->hello_status = CBM_DAEMON_HELLO_INVALID;
result->conflict.status = CBM_DAEMON_HELLO_INVALID;
(void)snprintf(result->message, sizeof(result->message), "%s", message);
}
static void runtime_service_begin_stopping_locked(cbm_daemon_runtime_service_t *service,
uint64_t deadline, bool emergency,
const char *reason) {
if (service->state == CBM_DAEMON_RUNTIME_SERVICE_RUNNING) {
service->state = CBM_DAEMON_RUNTIME_SERVICE_STOPPING;
service->stop_deadline_ms = deadline;
/* The generation's fate is decided here by one of several owners
* (last-client exit, coordinator stop, activation drain, external
* stop). Sessions dropped by the losing side of a race are
* undiagnosable without naming which owner pulled the trigger. */
cbm_log_info("daemon.runtime_stopping", "reason", reason ? reason : "unspecified");
} else if (service->state == CBM_DAEMON_RUNTIME_SERVICE_STOPPING &&
(service->stop_deadline_ms == 0 || deadline < service->stop_deadline_ms)) {
service->stop_deadline_ms = deadline;
}
if (emergency) {
service->emergency_stop = true;
}
}
static void runtime_service_begin_stopping(cbm_daemon_runtime_service_t *service,
uint32_t timeout_ms, bool emergency,
const char *reason) {
uint64_t deadline = runtime_deadline_after(timeout_ms);
cbm_mutex_lock(&service->mutex);
runtime_service_begin_stopping_locked(service, deadline, emergency, reason);
cbm_mutex_unlock(&service->mutex);
}
static void runtime_service_interrupt_connections_except(cbm_daemon_runtime_service_t *service,
cbm_daemon_runtime_worker_t *except_worker,
bool activation_owner) {
uint64_t now_ms = cbm_now_ms();
cbm_mutex_lock(&service->mutex);
if (service->activation_response_inflight && !activation_owner) {
cbm_mutex_unlock(&service->mutex);
return;
}
bool force = service->emergency_stop ||
(service->stop_deadline_ms != 0 && now_ms >= service->stop_deadline_ms);
for (size_t i = 0; i < service->worker_capacity; i++) {
cbm_daemon_runtime_worker_t *worker = &service->workers[i];
if (worker != except_worker && worker->in_use && worker->connection &&
(activation_owner || force || !worker->final_response_inflight)) {
cbm_daemon_ipc_connection_interrupt(worker->connection);
}
}
cbm_mutex_unlock(&service->mutex);
}
static void runtime_service_interrupt_connections(cbm_daemon_runtime_service_t *service) {
runtime_service_interrupt_connections_except(service, NULL, false);
}
/* A cold-storm racer is a connection that has been ACCEPTED but has not yet
* passed HELLO admission (in_use with a live connection, not yet admitted, not
* tearing down) -- a one-shot client still mid-bootstrap. This deliberately
* does NOT count an already-admitted provisional session (HELLO done, app
* session opening): a provisional coordinator client must not keep a retiring
* generation alive (see runtime_worker_disconnect below and the
* daemon_runtime_final_disconnect_rejects_blocked_provisional_session guard).
* Caller holds service->mutex. `except` excludes the departing worker. */
static bool runtime_has_pending_hello_peer_locked(const cbm_daemon_runtime_service_t *service,
const cbm_daemon_runtime_worker_t *except) {
for (size_t i = 0; i < service->worker_capacity; i++) {
const cbm_daemon_runtime_worker_t *worker = &service->workers[i];
if (worker != except && worker->in_use && worker->connection && !worker->admitted &&
!atomic_load_explicit(&worker->disconnecting, memory_order_acquire)) {
return true;
}
}
return false;
}
static void runtime_worker_disconnect(cbm_daemon_runtime_worker_t *worker) {
cbm_daemon_runtime_service_t *service = worker->service;
cbm_daemon_client_id_t client_id = CBM_DAEMON_CLIENT_ID_INVALID;
uint64_t shutdown_deadline = runtime_deadline_after(service->shutdown_timeout_ms);
bool last_committed_left = false;
bool linger_armed = false;
atomic_store_explicit(&worker->disconnecting, true, memory_order_release);
cbm_mutex_lock(&service->mutex);
if (worker->admitted) {
client_id = worker->client_id;
worker->admitted = false;
}
if (worker->admission_committed) {
worker->admission_committed = false;
if (service->committed_clients > 0) {
service->committed_clients--;
}
if (service->committed_clients == 0 && !service->permanent) {
/* A HELLO whose application session is still opening is only a
* provisional coordinator client. It cannot keep the generation
* alive after the final fully committed frontend disconnects.
* A permanent generation (`daemon start`) deliberately survives
* this: only the stop/drain ops or a process kill end it. */
last_committed_left = true;
/* Cold-storm race (2026-09): if another connection is already
* accepted and still mid-HELLO (not yet admitted) when the last
* committed client leaves, it is a one-shot peer racing to commit —
* retiring now strands it on a STOPPING daemon and forces a full
* cold respawn ("cold-storm client failed (racing daemon spawn)").
* Linger a bounded window for it; the accept loop retires the
* generation once that racer drains without committing or the window
* elapses, and a racer that does commit clears the linger
* (runtime_worker_commit_admission). This deliberately lingers ONLY
* for a pre-HELLO racer, never for an already-admitted provisional
* session (HELLO done, app session opening): the contract that a
* provisional coordinator client cannot keep a retiring generation
* alive is preserved (the reject-blocked-provisional-session guard).
* With no such racer — the common single-client case — retire
* immediately, unchanged. (A cross-process cohort peer count is
* unavailable: advisory locks expose presence, not a holder count,
* and Windows LockFileEx has no non-owning probe.) */
if (runtime_has_pending_hello_peer_locked(service, worker)) {
service->ephemeral_linger_deadline_ms =
runtime_deadline_after(runtime_ephemeral_linger_timeout_ms());
linger_armed = true;
} else {
service->ephemeral_linger_deadline_ms = 0;
runtime_service_begin_stopping_locked(service, shutdown_deadline, false,
"last_committed_client_disconnected");
}
}
}
cbm_mutex_unlock(&service->mutex);
if (client_id == CBM_DAEMON_CLIENT_ID_INVALID) {
return;
}
/* Set the coordinator hold BEFORE releasing this client so its last-client
* self-transition to STOPPING is suppressed while a racing peer is still
* mid-HELLO; when no peer is present the hold stays clear so the release
* retires the coordinator normally. last_committed_left implies a committed (hence
* admitted) client, so client_id is always valid here. */
if (last_committed_left) {
cbm_daemon_coordinator_set_linger(service->coordinator, linger_armed);
}
(void)cbm_daemon_client_disconnected(service->coordinator, client_id, cbm_now_ms());
if (worker->application_session_opened && !worker->application_cancelled) {
worker->application_cancelled = true;
service->application.session_cancel(service->application.context,
worker->application_session);
}
if (cbm_daemon_coordinator_state(service->coordinator) == CBM_DAEMON_COORDINATOR_STOPPING) {
runtime_service_begin_stopping(service, service->shutdown_timeout_ms, false,
"coordinator_stopping");
}
}
static bool runtime_worker_service_running(cbm_daemon_runtime_worker_t *worker) {
cbm_daemon_runtime_service_t *service = worker->service;
cbm_mutex_lock(&service->mutex);
bool running = service->state == CBM_DAEMON_RUNTIME_SERVICE_RUNNING;
cbm_mutex_unlock(&service->mutex);
return running;
}
static bool runtime_worker_admit(cbm_daemon_runtime_worker_t *worker,
cbm_daemon_client_id_t *client_id_out) {
cbm_daemon_runtime_service_t *service = worker->service;
cbm_daemon_client_id_t client_id = CBM_DAEMON_CLIENT_ID_INVALID;
cbm_mutex_lock(&service->mutex);
if (service->state == CBM_DAEMON_RUNTIME_SERVICE_RUNNING) {
client_id = cbm_daemon_client_connected(service->coordinator, cbm_now_ms());
if (client_id != CBM_DAEMON_CLIENT_ID_INVALID) {
worker->client_id = client_id;
worker->admitted = true;
worker->admission_committed = false;
service->admitted_total++;
}
}
cbm_mutex_unlock(&service->mutex);
*client_id_out = client_id;
return client_id != CBM_DAEMON_CLIENT_ID_INVALID;
}
static bool runtime_worker_commit_admission(cbm_daemon_runtime_worker_t *worker) {
cbm_daemon_runtime_service_t *service = worker->service;
cbm_mutex_lock(&service->mutex);
bool committed = service->state == CBM_DAEMON_RUNTIME_SERVICE_RUNNING && worker->admitted &&
!worker->admission_committed;
if (committed) {
worker->admission_committed = true;
service->committed_clients++;
/* A freshly committed client ends any last-client linger window; the
* next drop to zero re-arms it against the participants outstanding
* then. */
service->ephemeral_linger_deadline_ms = 0;
}
cbm_mutex_unlock(&service->mutex);
if (committed) {
/* Mirror the cleared linger onto the coordinator. client_count is
* already nonzero (admission incremented it), so this only resets the
* hold for the next idle window — it never retires a live coordinator. */
cbm_daemon_coordinator_set_linger(service->coordinator, false);
}
return committed;
}
static bool runtime_worker_handle_subscribe(cbm_daemon_runtime_worker_t *worker,
const uint8_t *payload, uint32_t length) {
if (!runtime_worker_service_running(worker) || !payload ||
length < SUBSCRIBE_REQUEST_PREFIX_SIZE) {
return false;
}
uint32_t key_length = runtime_get_u32(payload);
if (key_length == 0 || key_length > CBM_DAEMON_RUNTIME_PROJECT_KEY_MAX ||
length != SUBSCRIBE_REQUEST_PREFIX_SIZE + key_length) {
return false;
}
char project_key[CBM_DAEMON_RUNTIME_PROJECT_KEY_MAX + 1];
memcpy(project_key, payload + SUBSCRIBE_REQUEST_PREFIX_SIZE, key_length);
project_key[key_length] = '\0';
if (memchr(project_key, '\0', key_length) != NULL) {
return false;
}
cbm_daemon_subscription_id_t subscription_id = CBM_DAEMON_SUBSCRIPTION_ID_INVALID;
cbm_daemon_subscription_result_t result = cbm_daemon_job_subscribe(
worker->service->coordinator, worker->client_id, project_key, &subscription_id);
uint8_t response[SUBSCRIBE_RESPONSE_SIZE];
runtime_put_u32(response, (uint32_t)result);
runtime_put_u64(response + 4, subscription_id);
return runtime_worker_send_frame(worker, CBM_DAEMON_FRAME_RESPONSE,
CBM_DAEMON_RUNTIME_OP_JOB_SUBSCRIBE, response,
(uint32_t)sizeof(response));
}
static bool runtime_worker_handle_unsubscribe(cbm_daemon_runtime_worker_t *worker,
const uint8_t *payload, uint32_t length) {
if (!runtime_worker_service_running(worker) || !payload || length != UNSUBSCRIBE_REQUEST_SIZE) {
return false;
}
cbm_daemon_subscription_id_t subscription_id = runtime_get_u64(payload);
bool removed = cbm_daemon_job_unsubscribe(worker->service->coordinator, worker->client_id,
subscription_id);
return runtime_worker_send_status(worker, CBM_DAEMON_RUNTIME_OP_JOB_UNSUBSCRIBE, removed);
}
static void *runtime_application_worker(void *opaque) {
cbm_daemon_runtime_worker_t *worker = opaque;
cbm_daemon_runtime_service_t *service = worker->service;
uint8_t *response = NULL;
uint32_t response_length = 0;
cbm_daemon_runtime_application_status_t status = service->application.request(
service->application.context, worker->application_session,
worker->application_request_token, worker->application_request,
worker->application_request_length, &response, &response_length);
bool valid_status = runtime_application_status_is_callback_result(status);
bool valid_response =
response_length <= CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX &&
(response_length == 0 || response != NULL) &&
(status == CBM_DAEMON_RUNTIME_APPLICATION_OK || (response == NULL && response_length == 0));
if (!valid_status || !valid_response) {
free(response);
response = NULL;
response_length = 0;
status = CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
}
/* Completion must be one atomic transition from the client's point of
* view: publish the done flag BEFORE the response bytes leave, so by the
* time any client can react to the response, admission already observes
* this slot as reusable. With the store after the send, a client that
* pipelines its next request the moment it sees a response raced this
* thread's final instructions and was rejected BUSY — on loaded Windows
* hosts roughly half of all back-to-back requests on one connection,
* surfacing as the wandering Phase 5 session drops. The admission side
* joins this thread after observing the flag, which safely absorbs the
* tail of the send. */
atomic_store_explicit(&worker->application_thread_done, true, memory_order_release);
bool sent = runtime_worker_send_application_response(worker, worker->application_request_token,
status, response, response_length, true);
free(response);
if (!sent && !atomic_load_explicit(&worker->disconnecting, memory_order_acquire)) {
cbm_daemon_ipc_connection_interrupt(worker->connection);
}
return NULL;
}
static _Noreturn void runtime_cleanup_fail_stop(const char *component);
static void runtime_contain_unresponsive_application(cbm_daemon_runtime_worker_t *worker) {
char peer_pid[32];
char token[32];
(void)snprintf(peer_pid, sizeof(peer_pid), "%llu", (unsigned long long)worker->peer_process_id);
(void)snprintf(token, sizeof(token), "%llu",
(unsigned long long)worker->application_request_token);
cbm_log_error("daemon.application_request_unresponsive", "peer_pid", peer_pid, "request_token",
token);
#ifdef CBM_ENABLE_TEST_SEAMS
cbm_daemon_runtime_containment_hook_t hook = atomic_load(&runtime_containment_hook_seam);
if (hook) {
hook("application_request_join");
return;
}
#endif
runtime_cleanup_fail_stop("application_request_join");
}
static bool runtime_worker_reap_application(cbm_daemon_runtime_worker_t *worker, bool wait) {
if (!worker->application_thread_started) {
return true;
}
if (!wait && !atomic_load_explicit(&worker->application_thread_done, memory_order_acquire)) {
return false;
}
if (!atomic_load_explicit(&worker->application_thread_done, memory_order_acquire)) {
/* The peer is gone and session_cancel already ran, so the handler owes
* a prompt return; only a handler that ignores cancellation reaches
* the ceiling. An unbounded join here once turned one wedged request
* into a whole-daemon zombie (2026-08-29): the disconnect path
* blocked, the worker slot never released, and the dead generation
* held the endpoint pipes and UI port for nine hours with nothing
* logged while every new client timed out bare. Containment
* terminates the process instead — the kernel releases every native
* claim and the next client starts a fresh generation. */
uint32_t timeout_ms = RUNTIME_ABANDONED_REQUEST_JOIN_TIMEOUT_MS;
#ifdef CBM_ENABLE_TEST_SEAMS
uint32_t seam_timeout = atomic_load(&runtime_abandoned_request_join_timeout_seam);
if (seam_timeout != 0) {
timeout_ms = seam_timeout;
}
#endif
uint64_t deadline = runtime_deadline_after(timeout_ms);
while (!atomic_load_explicit(&worker->application_thread_done, memory_order_acquire)) {
if (cbm_now_ms() >= deadline) {
/* Terminal in production. A test containment hook returns, and
* the join then waits for the harness to release the handler
* so teardown stays leak-free under the sanitizers. */
runtime_contain_unresponsive_application(worker);
deadline = UINT64_MAX;
}
runtime_wait_tick(deadline);
}
}
if (cbm_thread_join(&worker->application_thread) != 0) {
return false;
}
worker->application_thread_started = false;
free(worker->application_request);
worker->application_request = NULL;
worker->application_request_length = 0;
worker->application_request_token = CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
atomic_store_explicit(&worker->application_thread_done, false, memory_order_release);
return true;
}
static bool runtime_worker_handle_application(cbm_daemon_runtime_worker_t *worker,
const uint8_t *payload, uint32_t length) {
if (!payload || length < APPLICATION_REQUEST_PREFIX_SIZE) {
return false;
}
cbm_daemon_runtime_application_token_t request_token = runtime_get_u64(payload);
uint32_t request_length = runtime_get_u32(payload + 8);
if (request_length > CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX ||
length != APPLICATION_REQUEST_PREFIX_SIZE + request_length ||
request_token == CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID ||
request_token <= worker->last_application_request_token) {
return false;
}
/* A structurally valid token is consumed at admission, including BUSY,
* UNAVAILABLE, and local allocation/thread failures. Reusing a token after
* any terminal response would make late cancellation controls ambiguous. */
worker->last_application_request_token = request_token;
cbm_daemon_runtime_service_t *service = worker->service;
if (!service->application.request) {
return runtime_worker_send_application_response(
worker, request_token, CBM_DAEMON_RUNTIME_APPLICATION_UNAVAILABLE, NULL, 0, false);
}
(void)runtime_worker_reap_application(worker, false);
if (worker->application_thread_started) {
return runtime_worker_send_application_response(
worker, request_token, CBM_DAEMON_RUNTIME_APPLICATION_BUSY, NULL, 0, false);
}
uint8_t *request_copy = NULL;
if (request_length > 0) {
request_copy = malloc(request_length);
if (!request_copy) {
return runtime_worker_send_application_response(
worker, request_token, CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR, NULL, 0,
false);
}
memcpy(request_copy, payload + APPLICATION_REQUEST_PREFIX_SIZE, request_length);
}
worker->application_request_token = request_token;
worker->application_request = request_copy;
worker->application_request_length = request_length;
atomic_store_explicit(&worker->application_thread_done, false, memory_order_release);
if (cbm_thread_create(&worker->application_thread, RUNTIME_WORKER_STACK_SIZE,
runtime_application_worker, worker) != 0) {
free(worker->application_request);
worker->application_request = NULL;
worker->application_request_length = 0;
return runtime_worker_send_application_response(
worker, request_token, CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR, NULL, 0, false);
}
worker->application_thread_started = true;
return true;
}
static bool runtime_worker_handle_application_cancel(cbm_daemon_runtime_worker_t *worker,
const uint8_t *payload, uint32_t length) {
if (!worker || !payload || length != APPLICATION_CANCEL_REQUEST_SIZE) {
return false;
}
cbm_daemon_runtime_application_token_t request_token = runtime_get_u64(payload);
if (request_token == CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID) {
return false;
}
if (worker->application_thread_started && worker->application_request_token == request_token &&
!atomic_load_explicit(&worker->application_thread_done, memory_order_acquire)) {
worker->service->application.request_cancel(worker->service->application.context,
worker->application_session, request_token);
}
/* Cancellation is deliberately one-way. A stale exact-sized token is a
* harmless no-op and never changes stream response ordering. */
return true;
}
static bool runtime_worker_handle_close_intent(cbm_daemon_runtime_worker_t *worker,
uint32_t length) {
if (length != 0) {
return false;
}
/* Terminal for the session (admission, coordinator registration, session
* cancel), but not for the connection: the worker keeps reading so the
* in-flight response tail and the eventual EOF unwind normally. */
runtime_worker_disconnect(worker);
return true;
}
static bool runtime_worker_handle_disconnect(cbm_daemon_runtime_worker_t *worker, uint32_t length) {
if (length != 0) {
return false;
}
cbm_daemon_runtime_service_t *service = worker->service;
cbm_mutex_lock(&service->mutex);
worker->final_response_inflight = true;
cbm_mutex_unlock(&service->mutex);
runtime_worker_disconnect(worker);
bool sent = runtime_worker_send_status(worker, CBM_DAEMON_RUNTIME_OP_DISCONNECT, true);
cbm_mutex_lock(&service->mutex);
worker->final_response_inflight = false;
cbm_mutex_unlock(&service->mutex);
return sent;
}
static void runtime_worker_finish(cbm_daemon_runtime_worker_t *worker) {
cbm_daemon_runtime_service_t *service = worker->service;
runtime_worker_disconnect(worker);
if (!runtime_worker_reap_application(worker, true)) {
/* Fail closed on an impossible/invalid join rather than closing the
* session or freeing storage a callback could still reference. The
* service intentionally remains non-terminal for diagnosis. */
return;
}
if (worker->application_session_opened) {
service->application.session_close(service->application.context,
worker->application_session);
worker->application_session = NULL;
worker->application_session_opened = false;
}
if (worker->connection) {
/* Off the service mutex: a peer that just received its final response
* (hello rejection, disconnect acknowledgement) must be able to read
* it before the handle closes; see cbm_daemon_ipc_connection_drain. */
cbm_daemon_ipc_connection_drain(worker->connection, RUNTIME_WORKER_DRAIN_TIMEOUT_MS);
}
cbm_mutex_lock(&service->mutex);
if (worker->connection) {
cbm_daemon_ipc_connection_close(worker->connection);
worker->connection = NULL;
}
if (service->active_connections > 0) {
service->active_connections--;
}
atomic_store_explicit(&worker->done, true, memory_order_release);
cbm_mutex_unlock(&service->mutex);
}
static bool runtime_activation_peer_matches_claim(cbm_daemon_runtime_service_t *service,
uint64_t process_id, const char *claimed_build) {
if (!service || process_id == 0 || !claimed_build) {
return false;
}
bool active_image =
runtime_process_image_reference_matches_process(&service->active_image, process_id);
if (active_image) {
return strcmp(claimed_build, service->identity.build_fingerprint) == 0;
}
char peer_fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
return cbm_daemon_runtime_process_build_fingerprint(process_id, peer_fingerprint) &&
strcmp(peer_fingerprint, claimed_build) == 0;
}
static bool runtime_control_request_decode(const uint8_t *payload, uint32_t length,
char build_out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
if (!payload || length != CBM_DAEMON_CONTROL_REQUEST_SIZE ||
payload[CBM_DAEMON_CONTROL_REQUEST_SIZE - 1U] != 0U) {
return false;
}
memcpy(build_out, payload, CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
return strlen(build_out) == CBM_DAEMON_BUILD_FINGERPRINT_SIZE - 1U;
}
static void runtime_control_collect_clients_locked(cbm_daemon_runtime_service_t *service,
cbm_daemon_runtime_worker_t *except,
uint8_t *count_out,
uint32_t pids[CBM_DAEMON_CONTROL_CLIENT_CAP]) {
uint8_t count = 0U;
for (size_t index = 0; index < service->worker_capacity; index++) {
cbm_daemon_runtime_worker_t *candidate = &service->workers[index];
if (!candidate->in_use || !candidate->admission_committed || candidate == except) {
continue;
}
if (count < CBM_DAEMON_CONTROL_CLIENT_CAP) {
pids[count] = (uint32_t)candidate->peer_process_id;
}
count++;
}
*count_out =
count > CBM_DAEMON_CONTROL_CLIENT_CAP ? (uint8_t)CBM_DAEMON_CONTROL_CLIENT_CAP : count;
}
static void runtime_worker_handle_status(cbm_daemon_runtime_worker_t *worker,
const uint8_t *payload, uint32_t length) {
cbm_daemon_runtime_service_t *service = worker->service;
char requested_build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
bool peer_verified =
runtime_control_request_decode(payload, length, requested_build) &&
runtime_activation_peer_matches_claim(service, worker->peer_process_id, requested_build);
uint8_t response[CBM_DAEMON_STATUS_RESPONSE_SIZE];
memset(response, 0, sizeof(response));
if (peer_verified) {
uint8_t count = 0U;
uint32_t pids[CBM_DAEMON_CONTROL_CLIENT_CAP] = {0};
cbm_mutex_lock(&service->mutex);
response[0] = 1U;
response[1] =
(uint8_t)((service->permanent ? 0x01U : 0U) |
(service->state != CBM_DAEMON_RUNTIME_SERVICE_RUNNING ? 0x02U : 0U));
uint16_t committed = service->committed_clients > UINT16_MAX
? UINT16_MAX
: (uint16_t)service->committed_clients;
response[2] = (uint8_t)(committed >> 8);
response[3] = (uint8_t)(committed & 0xFFU);
uint32_t daemon_pid = (uint32_t)runtime_current_process_id();
response[4] = (uint8_t)(daemon_pid >> 24);
response[5] = (uint8_t)(daemon_pid >> 16);
response[6] = (uint8_t)(daemon_pid >> 8);
response[7] = (uint8_t)(daemon_pid & 0xFFU);
runtime_control_collect_clients_locked(service, worker, &count, pids);
response[8] = count;
for (size_t index = 0; index < CBM_DAEMON_CONTROL_CLIENT_CAP; index++) {
size_t offset = 12U + index * 4U;
response[offset] = (uint8_t)(pids[index] >> 24);
response[offset + 1U] = (uint8_t)(pids[index] >> 16);
response[offset + 2U] = (uint8_t)(pids[index] >> 8);
response[offset + 3U] = (uint8_t)(pids[index] & 0xFFU);
}
(void)snprintf((char *)response + 44U, CBM_DAEMON_BUILD_FINGERPRINT_SIZE, "%s",
service->identity.build_fingerprint);
(void)snprintf((char *)response + 109U, 12U, "%s",
service->identity.semantic_version ? service->identity.semantic_version
: "");
cbm_mutex_unlock(&service->mutex);
}
(void)runtime_worker_send_frame(worker, CBM_DAEMON_FRAME_RESPONSE, CBM_DAEMON_RUNTIME_OP_STATUS,
response, (uint32_t)sizeof(response));
runtime_worker_finish(worker);
}
static void runtime_worker_handle_stop(cbm_daemon_runtime_worker_t *worker, const uint8_t *payload,
uint32_t length) {
cbm_daemon_runtime_service_t *service = worker->service;
char requested_build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
bool peer_verified =
runtime_control_request_decode(payload, length, requested_build) &&
runtime_activation_peer_matches_claim(service, worker->peer_process_id, requested_build);
uint8_t response[CBM_DAEMON_STOP_RESPONSE_SIZE];
memset(response, 0, sizeof(response));
bool accepted = false;
if (peer_verified) {
uint8_t count = 0U;
uint32_t pids[CBM_DAEMON_CONTROL_CLIENT_CAP] = {0};
uint64_t deadline = runtime_deadline_after(service->shutdown_timeout_ms);
cbm_mutex_lock(&service->mutex);
response[0] = 1U;
uint16_t committed = service->committed_clients > UINT16_MAX
? UINT16_MAX
: (uint16_t)service->committed_clients;
runtime_control_collect_clients_locked(service, worker, &count, pids);
if (service->committed_clients > 0) {
/* Refuse-if-busy: the caller reports these pids to the user as
* the processes that must exit before a graceful stop. */
response[1] = 0x02U;
} else if (service->state == CBM_DAEMON_RUNTIME_SERVICE_RUNNING) {
/* Explicit stop is the sanctioned exit for PERMANENT generations
* as well — begin_stopping here overrides the permanence gate.
* Mirror the activation-shutdown ACK protocol: the stopping
* teardown must not consume this requester's response slot before
* the acceptance byte leaves. */
runtime_service_begin_stopping_locked(service, deadline, false,
"daemon_stop_requested");
/* Same exit criterion as an activation drain: a permanent
* coordinator never reports client-terminal, so resource
* emptiness must be allowed to complete this stop promptly. */
service->activation_shutdown_requested = true;
service->activation_response_inflight = true;
worker->final_response_inflight = true;
response[1] = 0x01U;
accepted = true;
}
response[2] = (uint8_t)(committed >> 8);
response[3] = (uint8_t)(committed & 0xFFU);
response[4] = count;
for (size_t index = 0; index < CBM_DAEMON_CONTROL_CLIENT_CAP; index++) {
size_t offset = 8U + index * 4U;
response[offset] = (uint8_t)(pids[index] >> 24);
response[offset + 1U] = (uint8_t)(pids[index] >> 16);
response[offset + 2U] = (uint8_t)(pids[index] >> 8);
response[offset + 3U] = (uint8_t)(pids[index] & 0xFFU);
}
cbm_mutex_unlock(&service->mutex);
}
if (accepted) {
char requester_pid[32];
(void)snprintf(requester_pid, sizeof(requester_pid), "%llu",
(unsigned long long)worker->peer_process_id);
cbm_log_info("daemon.stop_requested", "requester_pid", requester_pid, "requester_build",
requested_build);
runtime_service_interrupt_connections_except(worker->service, worker, true);
}
(void)runtime_worker_send_frame(worker, CBM_DAEMON_FRAME_RESPONSE, CBM_DAEMON_RUNTIME_OP_STOP,
response, (uint32_t)sizeof(response));
if (accepted) {
cbm_daemon_runtime_service_t *stop_service = worker->service;
runtime_worker_finish(worker);
cbm_mutex_lock(&stop_service->mutex);
worker->final_response_inflight = false;
stop_service->activation_response_inflight = false;
cbm_mutex_unlock(&stop_service->mutex);
runtime_service_interrupt_connections(stop_service);
return;
}
runtime_worker_finish(worker);
}
static void runtime_worker_handle_activation_shutdown(cbm_daemon_runtime_worker_t *worker,
const uint8_t *payload, uint32_t length) {
cbm_daemon_runtime_service_t *service = worker->service;
cbm_daemon_runtime_activation_result_t result = {0};
cbm_daemon_runtime_activation_action_t action = 0;
char requested_version[CBM_DAEMON_VERSION_TEXT_SIZE] = {0};
char requested_build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
bool request_valid =
length == CBM_DAEMON_ACTIVATION_SHUTDOWN_REQUEST_SIZE && payload &&
runtime_activation_request_decode(payload, &action, requested_version, requested_build);
bool peer_verified = request_valid && runtime_activation_peer_matches_claim(
service, worker->peer_process_id, requested_build);
if (peer_verified) {
uint64_t deadline = runtime_deadline_after(service->shutdown_timeout_ms);
cbm_mutex_lock(&service->mutex);
result.active_clients = (uint64_t)service->committed_clients;
/* Report only connections this request will drain. The authenticated
* one-shot requester is never a session and excludes itself. */
result.active_connections =
service->active_connections > 0 ? (uint64_t)(service->active_connections - 1U) : 0;
if (service->state == CBM_DAEMON_RUNTIME_SERVICE_RUNNING) {
runtime_service_begin_stopping_locked(service, deadline, false, "activation_shutdown");
service->activation_shutdown_requested = true;
service->activation_response_inflight = true;
worker->final_response_inflight = true;
result.accepted = true;
}
cbm_mutex_unlock(&service->mutex);
}
uint8_t response[CBM_DAEMON_ACTIVATION_SHUTDOWN_RESPONSE_SIZE];
bool encoded = runtime_activation_response_encode(response, &result);
if (result.accepted) {
char requester_pid[32];
char active_clients[32];
char active_connections[32];
(void)snprintf(requester_pid, sizeof(requester_pid), "%llu",
(unsigned long long)worker->peer_process_id);
(void)snprintf(active_clients, sizeof(active_clients), "%llu",
(unsigned long long)result.active_clients);
(void)snprintf(active_connections, sizeof(active_connections), "%llu",
(unsigned long long)result.active_connections);
cbm_log_info("daemon.activation_shutdown", "requester_pid", requester_pid,
"requester_build", requested_build, "action",
runtime_activation_action_text(action), "active_clients", active_clients,
"active_connections", active_connections);
/* Interrupt every other connection BEFORE the ACK leaves: the ACK is
* the requester's license to act on "snapshotted and draining", so
* every drain must already be initiated when it arrives. Acking first
* left a window where a session could still get one request serviced
* after the requester observed the ACK. The requester itself is
* excepted and its response path stays protected. */
runtime_service_interrupt_connections_except(service, worker, true);
}
bool response_sent =
encoded && runtime_worker_send_frame(worker, CBM_DAEMON_FRAME_RESPONSE,
CBM_DAEMON_RUNTIME_OP_ACTIVATION_SHUTDOWN, response,
(uint32_t)sizeof(response));
if (result.accepted && !response_sent) {
cbm_log_error("daemon.activation_shutdown_ack_failed", "shutdown", "accepted",
"requester_build", requested_build, "action",
runtime_activation_action_text(action));
}
if (result.accepted) {
/* Close this requester normally, then release the global ACK gate so
* the accept loop can finish any stragglers. */
runtime_worker_finish(worker);
cbm_mutex_lock(&service->mutex);
worker->final_response_inflight = false;
service->activation_response_inflight = false;
cbm_mutex_unlock(&service->mutex);
runtime_service_interrupt_connections(service);
return;
}
runtime_worker_finish(worker);
}
/* The first frame is due within the request deadline. Under the item-12 seam a
* connection that never sends one holds its slot until it closes; stop still
* interrupts the wait like any other worker receive. */
static uint32_t runtime_first_frame_timeout_ms(const cbm_daemon_runtime_service_t *service) {
#ifdef CBM_ENABLE_TEST_SEAMS
if (atomic_load(&runtime_hold_peer_waits_seam)) {
return CBM_DAEMON_IPC_WAIT_FOREVER;
}
#endif
return service->request_timeout_ms;
}
/* Under the same seam an inline rejection's drain ends only on the peer's
* close, never on the 250 ms bound (the drain only waits on Windows). */
static uint32_t runtime_reject_drain_timeout_ms(void) {
#ifdef CBM_ENABLE_TEST_SEAMS
if (atomic_load(&runtime_hold_peer_waits_seam)) {
return CBM_DAEMON_IPC_WAIT_FOREVER;
}
#endif
return RUNTIME_REJECT_DRAIN_TIMEOUT_MS;
}
static void *runtime_connection_worker(void *opaque) {
cbm_daemon_runtime_worker_t *worker = opaque;
cbm_daemon_runtime_service_t *service = worker->service;
cbm_daemon_frame_t frame = {0};
uint8_t *payload = NULL;
char requested_version[CBM_DAEMON_VERSION_TEXT_SIZE];
char requested_build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
int received = cbm_daemon_ipc_receive_frame_bounded(
worker->connection, runtime_first_frame_timeout_ms(service),
CBM_DAEMON_ACTIVATION_SHUTDOWN_REQUEST_SIZE, &frame, &payload);
if (received != 1 || frame.type != CBM_DAEMON_FRAME_REQUEST) {
free(payload);
runtime_worker_finish(worker);
return NULL;
}
if (frame.flags == CBM_DAEMON_RUNTIME_OP_ACTIVATION_SHUTDOWN) {
runtime_worker_handle_activation_shutdown(worker, payload, frame.length);
free(payload);
return NULL;
}
if (frame.flags == CBM_DAEMON_RUNTIME_OP_STATUS) {
runtime_worker_handle_status(worker, payload, frame.length);
free(payload);
return NULL;
}
if (frame.flags == CBM_DAEMON_RUNTIME_OP_STOP) {
runtime_worker_handle_stop(worker, payload, frame.length);
free(payload);
return NULL;
}
if (frame.flags != CBM_DAEMON_RUNTIME_OP_HELLO ||
frame.length != CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE ||
!runtime_rendezvous_request_decode(payload, requested_version, requested_build)) {
free(payload);
runtime_worker_finish(worker);
return NULL;
}
free(payload);
payload = NULL;
cbm_daemon_runtime_connect_result_t hello_result;
memset(&hello_result, 0, sizeof(hello_result));
cbm_daemon_hello_status_t hello_status = runtime_rendezvous_compare(
service->identity.semantic_version, service->identity.build_fingerprint, requested_version,
requested_build, &hello_result.conflict);
if (hello_status != CBM_DAEMON_HELLO_COMPATIBLE) {
if (hello_status == CBM_DAEMON_HELLO_INVALID) {
runtime_worker_finish(worker);
return NULL;
}
hello_result.status = CBM_DAEMON_RUNTIME_CONNECT_CONFLICT;
hello_result.hello_status = hello_status;
(void)cbm_daemon_conflict_format(&hello_result.conflict, hello_result.message,
sizeof(hello_result.message));
if (!cbm_daemon_conflict_log_append(service->conflict_log_path, &hello_result.conflict,
service->conflict_log_cap_bytes)) {
/* This goes through the daemon operation-log sink, never back
* through the failed conflict-log path. Do not silently lose the
* durable startup-conflict diagnostic. */
cbm_log_error("daemon.conflict_log_append_failed", "path", service->conflict_log_path);
}
(void)runtime_send_hello_response(worker->connection, &hello_result);
runtime_worker_finish(worker);
return NULL;
}
runtime_peer_image_verdict_t peer_image =
runtime_peer_image_verify(service, worker->peer_process_id, requested_build);
bool peer_image_verified = peer_image == RUNTIME_PEER_IMAGE_VERIFIED;
bool peer_image_fingerprinted = peer_image != RUNTIME_PEER_IMAGE_UNVERIFIABLE;
#ifdef CBM_ENABLE_TEST_SEAMS
if (atomic_load(&runtime_force_peer_image_unverified_seam)) {
peer_image_verified = false;
peer_image_fingerprinted = false;
}
if (atomic_load(&runtime_force_peer_image_mismatch_seam)) {
peer_image_verified = false;
peer_image_fingerprinted = true;
}
#endif
/* Two different failures wear the same "unverified" flag, and treating them
* alike broke every ephemeral-path client (#1539/#1383):
*
* fingerprint_mismatch — the peer's image WAS read and hashes differently
* than the running daemon. That is the tamper/skew case the gate exists
* for. Still rejected, hard.
* image_unverifiable — the peer's image could not be examined at all
* (ephemeral npx cache paths, ptrace_scope restrictions, sandboxed
* hosts). Nothing was contradicted; we simply could not look. The peer
* ALREADY proved semantic version, build fingerprint, protocol/store/
* feature ABI and cache root in the HELLO exchange above — rejecting on
* top of that traded a real compatibility proof for an unavailable one,
* and made `npx codebase-memory-mcp` unusable with the daemon. Admit,
* and say so out loud so the weaker check is never invisible. */
if (!peer_image_verified && !peer_image_fingerprinted) {
cbm_log_warn("daemon.client_image_unverifiable_admitted", "reason", "image_unverifiable",
"basis", "rendezvous_hello_verified");
peer_image_verified = true;
}
if (!peer_image_verified) {
cbm_log_error("daemon.client_image_rejected", "reason", "fingerprint_mismatch");
/* #1383: answer the peer before closing. An unanswered rejection is
* indistinguishable from a slow cold start on the client side — the
* caller sat on "pending" indefinitely with the reason visible only in
* the daemon log. The version-conflict path above already responds to
* unverified peers, so this discloses nothing new to a same-uid local
* peer; admission stays rejected either way. */
runtime_result_rejected(&hello_result, "CBM daemon rejected this client's binary image");
(void)snprintf(hello_result.message, sizeof(hello_result.message),
"CBM daemon rejected this client: fingerprint_mismatch. The client binary "
"must match the running daemon's build; close CBM sessions (or run "
"'daemon stop') and retry with one consistent install.");
(void)runtime_send_hello_response(worker->connection, &hello_result);
runtime_worker_finish(worker);
return NULL;
}
cbm_daemon_client_id_t client_id = CBM_DAEMON_CLIENT_ID_INVALID;
if (!runtime_worker_admit(worker, &client_id)) {
runtime_result_rejected(&hello_result, "CBM daemon is stopping");
(void)runtime_send_hello_response(worker->connection, &hello_result);
runtime_worker_finish(worker);
return NULL;
}
if (service->application.request) {
worker->application_session = service->application.session_open(
service->application.context, client_id, worker->peer_process_id);
if (!worker->application_session) {
runtime_result_rejected(&hello_result, "CBM daemon session initialization failed");
cbm_mutex_lock(&service->mutex);
worker->final_response_inflight = true;
cbm_mutex_unlock(&service->mutex);
runtime_worker_disconnect(worker);
(void)runtime_send_hello_response(worker->connection, &hello_result);
cbm_mutex_lock(&service->mutex);
worker->final_response_inflight = false;
cbm_mutex_unlock(&service->mutex);
runtime_worker_finish(worker);
return NULL;
}
worker->application_session_opened = true;
}
if (!runtime_worker_commit_admission(worker)) {
/* session_open() returns a provisional allocation. STOPPING and this
* commit linearize on service->mutex; a losing provisional session is
* cancelled and closed exactly like any other connection-owned state,
* but is never exposed to the frontend as admitted. */
runtime_result_rejected(&hello_result, "CBM daemon is stopping");
cbm_mutex_lock(&service->mutex);
worker->final_response_inflight = true;
cbm_mutex_unlock(&service->mutex);
runtime_worker_disconnect(worker);
(void)runtime_send_hello_response(worker->connection, &hello_result);
cbm_mutex_lock(&service->mutex);
worker->final_response_inflight = false;
cbm_mutex_unlock(&service->mutex);
runtime_worker_finish(worker);
return NULL;
}
hello_result.status = CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED;
hello_result.hello_status = CBM_DAEMON_HELLO_COMPATIBLE;
hello_result.conflict.status = CBM_DAEMON_HELLO_COMPATIBLE;
hello_result.client_id = client_id;
hello_result.authenticated_process_id = worker->peer_process_id;
if (!runtime_send_hello_response(worker->connection, &hello_result)) {
runtime_worker_finish(worker);
return NULL;
}
bool keep_running = true;
while (keep_running && runtime_worker_service_running(worker)) {
memset(&frame, 0, sizeof(frame));
payload = NULL;
received = cbm_daemon_ipc_receive_frame(worker->connection, CBM_DAEMON_IPC_WAIT_FOREVER,
&frame, &payload);
if (received != 1 || frame.type != CBM_DAEMON_FRAME_REQUEST) {
/* An admitted frontend disconnecting decides the daemon's fate
* (last committed client ends the generation), so the transport
* cause must be visible: 0 = orderly EOF, negative = transport
* error. A silent break here previously made session-drop races
* undiagnosable across three platforms. */
char received_text[16];
(void)snprintf(received_text, sizeof(received_text), "%d", received);
cbm_log_info("daemon.connection_end", "received", received_text, "frame_type",
received == 1 ? "non_request" : "none");
break;
}
switch (frame.flags) {
case CBM_DAEMON_RUNTIME_OP_HEARTBEAT: {
bool valid = frame.length == 0 &&
cbm_daemon_client_heartbeat(service->coordinator, client_id, cbm_now_ms());
keep_running =
valid && runtime_worker_send_status(worker, CBM_DAEMON_RUNTIME_OP_HEARTBEAT, true);
break;
}
case CBM_DAEMON_RUNTIME_OP_JOB_SUBSCRIBE:
keep_running = runtime_worker_handle_subscribe(worker, payload, frame.length);
break;
case CBM_DAEMON_RUNTIME_OP_JOB_UNSUBSCRIBE:
keep_running = runtime_worker_handle_unsubscribe(worker, payload, frame.length);
break;
case CBM_DAEMON_RUNTIME_OP_APPLICATION_REQUEST:
keep_running = runtime_worker_handle_application(worker, payload, frame.length);
break;
case CBM_DAEMON_RUNTIME_OP_APPLICATION_CANCEL:
keep_running = runtime_worker_handle_application_cancel(worker, payload, frame.length);
break;
case CBM_DAEMON_RUNTIME_OP_CLOSE_INTENT:
keep_running = runtime_worker_handle_close_intent(worker, frame.length);
break;
case CBM_DAEMON_RUNTIME_OP_DISCONNECT:
keep_running = false;
(void)runtime_worker_handle_disconnect(worker, frame.length);
break;
default:
keep_running = false;
break;
}
free(payload);
payload = NULL;
}
free(payload);
runtime_worker_finish(worker);
return NULL;
}
static void runtime_worker_reset_after_join(cbm_daemon_runtime_worker_t *worker) {
free(worker->application_request);
worker->thread_started = false;
worker->in_use = false;
worker->joining = false;
worker->connection = NULL;
worker->client_id = CBM_DAEMON_CLIENT_ID_INVALID;
worker->peer_process_id = 0;
worker->admitted = false;
worker->admission_committed = false;
worker->final_response_inflight = false;
worker->application_session = NULL;
worker->application_session_opened = false;
worker->application_cancelled = false;
worker->application_thread_started = false;
worker->application_request_token = CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
worker->last_application_request_token = CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
worker->application_request = NULL;
worker->application_request_length = 0;
atomic_store_explicit(&worker->done, false, memory_order_release);
atomic_store_explicit(&worker->disconnecting, false, memory_order_release);
atomic_store_explicit(&worker->application_thread_done, false, memory_order_release);
}
static void runtime_reap_completed_workers(cbm_daemon_runtime_service_t *service) {
for (size_t i = 0; i < service->worker_capacity; i++) {
cbm_daemon_runtime_worker_t *worker = &service->workers[i];
cbm_mutex_lock(&service->mutex);
bool reap = worker->in_use && worker->thread_started && !worker->joining &&
atomic_load_explicit(&worker->done, memory_order_acquire);
if (reap) {
worker->joining = true;
}
cbm_mutex_unlock(&service->mutex);
if (!reap) {
continue;
}
int joined = cbm_thread_join(&worker->thread);
cbm_mutex_lock(&service->mutex);
if (joined == 0) {
runtime_worker_reset_after_join(worker);
} else {
worker->joining = false;
}
cbm_mutex_unlock(&service->mutex);
}
}
static cbm_daemon_runtime_worker_t *runtime_find_free_worker_locked(
cbm_daemon_runtime_service_t *service) {
for (size_t i = 0; i < service->worker_capacity; i++) {
if (!service->workers[i].in_use) {
return &service->workers[i];
}
}
return NULL;
}
/* Answers WITHOUT reading the peer's HELLO, so on POSIX (where the drain is a
* no-op) the close can land before the peer has even sent it; the client reads
* this answer after its failed send (cbm_daemon_runtime_client_connect). */
static void runtime_reject_inline(cbm_daemon_ipc_connection_t *connection, const char *message) {
cbm_daemon_runtime_connect_result_t result;
runtime_result_rejected(&result, message);
RUNTIME_HELLO_POINT(DAEMON_REJECTING);
(void)runtime_send_hello_response(connection, &result);
/* Same Windows named-pipe discard hazard as runtime_worker_finish: the
* peer must get to read the rejection before the handle closes. */
cbm_daemon_ipc_connection_drain(connection, runtime_reject_drain_timeout_ms());
cbm_daemon_ipc_connection_close(connection);
RUNTIME_HELLO_POINT(DAEMON_REJECTED);
}
static void runtime_accept_connection(cbm_daemon_runtime_service_t *service,
cbm_daemon_ipc_connection_t *connection) {
uint64_t peer_pid = cbm_daemon_ipc_connection_peer_pid(connection);
if (peer_pid == 0) {
cbm_daemon_ipc_connection_close(connection);
return;
}
cbm_mutex_lock(&service->mutex);
bool running = service->state == CBM_DAEMON_RUNTIME_SERVICE_RUNNING;
bool capacity = service->active_connections >= service->worker_capacity;
cbm_daemon_runtime_worker_t *worker =
running && !capacity ? runtime_find_free_worker_locked(service) : NULL;
if (worker) {
worker->connection = connection;
worker->peer_process_id = peer_pid;
worker->client_id = CBM_DAEMON_CLIENT_ID_INVALID;
worker->admitted = false;
worker->admission_committed = false;
worker->final_response_inflight = false;
worker->application_session = NULL;
worker->application_session_opened = false;
worker->application_cancelled = false;
worker->application_thread_started = false;
free(worker->application_request);
worker->application_request = NULL;
worker->application_request_length = 0;
worker->joining = false;
worker->in_use = true;
atomic_store_explicit(&worker->done, false, memory_order_release);
atomic_store_explicit(&worker->disconnecting, false, memory_order_release);
atomic_store_explicit(&worker->application_thread_done, false, memory_order_release);
service->active_connections++;
int created = cbm_thread_create(&worker->thread, RUNTIME_WORKER_STACK_SIZE,
runtime_connection_worker, worker);
if (created == 0) {
worker->thread_started = true;
cbm_mutex_unlock(&service->mutex);
return;
}
service->active_connections--;
runtime_worker_reset_after_join(worker);
}
cbm_mutex_unlock(&service->mutex);
if (!running) {
runtime_reject_inline(connection, "CBM daemon is stopping");
} else {
runtime_reject_inline(connection, "CBM daemon connection capacity reached");
}
}
static bool runtime_service_can_exit(cbm_daemon_runtime_service_t *service, uint64_t now_ms) {
bool can_exit = false;
cbm_mutex_lock(&service->mutex);
if (service->state == CBM_DAEMON_RUNTIME_SERVICE_STOPPING && service->active_connections == 0) {
bool drained = cbm_daemon_should_exit(service->coordinator, now_ms);
if (!drained && service->activation_shutdown_requested) {
/* Activation can arrive while the daemon has no committed
* coordinator client, so no final disconnect exists to flip the
* coordinator's terminal bit. Resource emptiness is equivalent
* once the runtime has atomically stopped all new admission. */
drained = cbm_daemon_active_clients(service->coordinator) == 0 &&
cbm_daemon_active_jobs(service->coordinator) == 0 &&
cbm_daemon_active_watches(service->coordinator) == 0;
}
bool deadline_reached =
service->stop_deadline_ms != 0 && now_ms >= service->stop_deadline_ms;
can_exit = drained || deadline_reached || service->emergency_stop;
if (can_exit) {
service->state = CBM_DAEMON_RUNTIME_SERVICE_EXITED;
}
}
cbm_mutex_unlock(&service->mutex);
return can_exit;
}
static void *runtime_accept_loop(void *opaque) {
cbm_daemon_runtime_service_t *service = opaque;
for (;;) {
runtime_reap_completed_workers(service);
cbm_mutex_lock(&service->mutex);
cbm_daemon_runtime_service_state_t state = service->state;
cbm_mutex_unlock(&service->mutex);
if (state == CBM_DAEMON_RUNTIME_SERVICE_EXITED) {
break;
}
if (state == CBM_DAEMON_RUNTIME_SERVICE_STOPPING) {
runtime_service_interrupt_connections(service);
if (runtime_service_can_exit(service, cbm_now_ms())) {
break;
}
uint64_t tick_deadline = runtime_deadline_after(1);
runtime_wait_tick(tick_deadline);
continue;
}
/* Self-retire a generation lingering for a cold-storm peer once the peer
* drains or the bounded window elapses, without an external driver. A
* no-op unless a last-committed-client linger is armed. */
cbm_daemon_runtime_service_reconcile_lifetime(service);
cbm_daemon_ipc_connection_t *connection = NULL;
int accepted =
cbm_daemon_ipc_accept(service->listener, RUNTIME_ACCEPT_POLL_MS, &connection);
if (accepted == 1 && connection) {
runtime_accept_connection(service, connection);
} else if (accepted < 0) {
/* IPC intentionally combines invalid-peer and listener errors.
* Retry while RUNNING so one hostile peer cannot stop the daemon. */
uint64_t tick_deadline = runtime_deadline_after(1);
runtime_wait_tick(tick_deadline);
}
}
runtime_reap_completed_workers(service);
atomic_store_explicit(&service->accept_thread_done, true, memory_order_release);
return NULL;
}
static char *runtime_string_copy_bounded(const char *value, size_t capacity) {
size_t length = 0;
if (!runtime_bounded_length(value, capacity, &length)) {
return NULL;
}
char *copy = malloc(length + 1);
if (copy) {
memcpy(copy, value, length + 1);
}
return copy;
}
static bool runtime_application_callbacks_valid(
const cbm_daemon_runtime_application_callbacks_t *application) {
bool any = application->session_open || application->request || application->request_cancel ||
application->session_cancel || application->session_close;
bool all = application->session_open && application->request && application->request_cancel &&
application->session_cancel && application->session_close;
return !any || all;
}
static bool runtime_participant_guard_release_complete(
cbm_daemon_ipc_participant_guard_t **guard_io, uint32_t timeout_ms) {
uint64_t deadline = runtime_deadline_after(timeout_ms);
while (guard_io && *guard_io) {
(void)cbm_daemon_ipc_participant_guard_release(guard_io);
if (!*guard_io) {
return true;
}
if (cbm_now_ms() >= deadline) {
return false;
}
cbm_usleep(1000);
}
return guard_io != NULL;
}
static _Noreturn void runtime_cleanup_fail_stop(const char *component) {
/* Convenience start() has no outward cleanup handle on failure. Losing a
* live participant guard would permit later code to misreport teardown,
* so terminate the process and let the kernel release native ownership. */
cbm_log_error("daemon.forced_shutdown", "component", component);
(void)fflush(stdout);
(void)fflush(stderr);
#ifdef _WIN32
(void)TerminateProcess(GetCurrentProcess(), EXIT_FAILURE);
abort();
#else
_Exit(EXIT_FAILURE);
#endif
}
static void runtime_startup_lock_release_complete(cbm_daemon_ipc_startup_lock_t **lock_io,
uint32_t timeout_ms) {
uint64_t deadline = runtime_deadline_after(timeout_ms);
while (lock_io && *lock_io) {
(void)cbm_daemon_ipc_startup_lock_release(lock_io);
if (!*lock_io) {
return;
}
if (cbm_now_ms() >= deadline) {
runtime_cleanup_fail_stop("startup_lock_cleanup");
}
cbm_usleep(1000);
}
}
static void runtime_service_destroy_unstarted(cbm_daemon_runtime_service_t *service) {
if (!service) {
return;
}
cbm_daemon_ipc_listener_close(service->listener);
cbm_daemon_coordinator_free(service->coordinator);
(void)runtime_process_image_reference_release(&service->active_image);
runtime_verified_peer_images_release(service);
free(service->conflict_log_path);
for (size_t i = 0; i < service->worker_mutexes_initialized; i++) {
cbm_mutex_destroy(&service->workers[i].send_mutex);
}
free(service->workers);
cbm_mutex_destroy(&service->mutex);
free(service);
}
cbm_daemon_runtime_service_t *cbm_daemon_runtime_service_start_reserved(
const cbm_daemon_runtime_service_config_t *config,
cbm_daemon_ipc_lifetime_reservation_t **reservation_io) {
uint8_t validation[CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE];
char active_process_fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
runtime_process_image_reference_t active_image;
runtime_process_image_reference_init(&active_image);
if (!reservation_io || !*reservation_io || !config || !config->endpoint ||
!config->conflict_log_path || config->conflict_log_cap_bytes == 0 ||
config->max_clients == 0 || config->max_clients > RUNTIME_MAX_CLIENTS_HARD ||
config->lease_timeout_ms == 0 || config->request_timeout_ms == 0 ||
config->request_timeout_ms == CBM_DAEMON_IPC_WAIT_FOREVER ||
config->shutdown_timeout_ms == 0 ||
config->shutdown_timeout_ms == CBM_DAEMON_IPC_WAIT_FOREVER ||
!runtime_application_callbacks_valid(&config->application) ||
!cbm_daemon_runtime_hello_request_encode(validation, &config->identity)) {
cbm_log_error("daemon.runtime.start_failed", "stage", "config_validation");
return NULL;
}
/* WHY: cppcheck evaluates the unsupported-platform fail-closed branch
* because its invocation defines no host OS macro. Production compilers
* select one of the Windows/macOS/Linux native-image implementations. */
// cppcheck-suppress knownConditionTrueFalse
if (!runtime_process_image_reference_acquire(runtime_current_process_id(), &active_image,
active_process_fingerprint) ||
strcmp(active_process_fingerprint, config->identity.build_fingerprint) != 0) {
cbm_log_error("daemon.runtime.start_failed", "stage", "active_image_identity");
(void)runtime_process_image_reference_release(&active_image);
return NULL;
}
cbm_daemon_runtime_service_t *service = calloc(1, sizeof(*service));
if (!service) {
cbm_log_error("daemon.runtime.start_failed", "stage", "service_allocation");
(void)runtime_process_image_reference_release(&active_image);
return NULL;
}
service->active_image = active_image;
runtime_process_image_reference_init(&active_image);
cbm_mutex_init(&service->mutex);
atomic_init(&service->accept_thread_done, false);
service->worker_capacity = config->max_clients;
service->workers = calloc(service->worker_capacity, sizeof(*service->workers));
service->coordinator = cbm_daemon_coordinator_new(config->lease_timeout_ms);
if (service->coordinator && config->permanent) {
cbm_daemon_coordinator_set_permanent(service->coordinator, true);
}
service->conflict_log_path =
runtime_string_copy_bounded(config->conflict_log_path, RUNTIME_PATH_CAP);
size_t version_length = 0;
size_t build_length = 0;
bool copied_identity =
runtime_bounded_length(config->identity.semantic_version, sizeof(service->semantic_version),
&version_length) &&
runtime_bounded_length(config->identity.build_fingerprint,
sizeof(service->build_fingerprint), &build_length);
if (copied_identity) {
memcpy(service->semantic_version, config->identity.semantic_version, version_length + 1);
memcpy(service->build_fingerprint, config->identity.build_fingerprint, build_length + 1);
}
service->identity.semantic_version = service->semantic_version;
service->identity.build_fingerprint = service->build_fingerprint;
service->identity.protocol_abi = config->identity.protocol_abi;
service->identity.store_abi = config->identity.store_abi;
service->identity.feature_abi = config->identity.feature_abi;
service->conflict_log_cap_bytes = config->conflict_log_cap_bytes;
service->lease_timeout_ms = config->lease_timeout_ms;
service->request_timeout_ms = config->request_timeout_ms;
service->shutdown_timeout_ms = config->shutdown_timeout_ms;
service->application = config->application;
service->permanent = config->permanent;
service->state = CBM_DAEMON_RUNTIME_SERVICE_STARTING;
if (!service->workers || !service->coordinator || !service->conflict_log_path ||
!copied_identity) {
cbm_log_error("daemon.runtime.start_failed", "stage", "service_initialization");
runtime_service_destroy_unstarted(service);
return NULL;
}
for (size_t i = 0; i < service->worker_capacity; i++) {
service->workers[i].service = service;
cbm_mutex_init(&service->workers[i].send_mutex);
service->worker_mutexes_initialized++;
atomic_init(&service->workers[i].done, false);
atomic_init(&service->workers[i].disconnecting, false);
atomic_init(&service->workers[i].application_thread_done, false);
}
service->listener = cbm_daemon_ipc_listen_reserved(config->endpoint, reservation_io);
if (!service->listener) {
cbm_log_error("daemon.runtime.start_failed", "stage", "listener_handoff");
runtime_service_destroy_unstarted(service);
return NULL;
}
service->state = CBM_DAEMON_RUNTIME_SERVICE_RUNNING;
if (cbm_thread_create(&service->accept_thread, RUNTIME_WORKER_STACK_SIZE, runtime_accept_loop,
service) != 0) {
cbm_log_error("daemon.runtime.start_failed", "stage", "accept_thread");
service->state = CBM_DAEMON_RUNTIME_SERVICE_EXITED;
runtime_service_destroy_unstarted(service);
return NULL;
}
service->accept_thread_started = true;
return service;
}
cbm_daemon_runtime_service_t *cbm_daemon_runtime_service_start(
const cbm_daemon_runtime_service_config_t *config) {
if (!config || !config->endpoint) {
return NULL;
}
cbm_daemon_ipc_startup_lock_t *startup_lock = NULL;
if (cbm_daemon_ipc_startup_lock_try_acquire(config->endpoint, &startup_lock) != 1) {
return NULL;
}
int generation =
cbm_daemon_ipc_generation_probe_under_startup_lock(config->endpoint, startup_lock);
if (generation != 0 || !cbm_daemon_ipc_startup_lock_prepare_handoff(startup_lock)) {
runtime_startup_lock_release_complete(&startup_lock, config->shutdown_timeout_ms);
return NULL;
}
cbm_daemon_ipc_participant_guard_t *participant_guard = NULL;
if (cbm_daemon_ipc_participant_guard_try_join(config->endpoint, &participant_guard) != 1) {
runtime_startup_lock_release_complete(&startup_lock, config->shutdown_timeout_ms);
return NULL;
}
cbm_daemon_ipc_lifetime_reservation_t *reservation = NULL;
if (cbm_daemon_ipc_lifetime_reservation_try_acquire(config->endpoint, &reservation) != 1) {
if (!runtime_participant_guard_release_complete(&participant_guard,
config->shutdown_timeout_ms)) {
runtime_cleanup_fail_stop("participant_guard_cleanup");
}
runtime_startup_lock_release_complete(&startup_lock, config->shutdown_timeout_ms);
return NULL;
}
cbm_daemon_runtime_service_t *service =
cbm_daemon_runtime_service_start_reserved(config, &reservation);
cbm_daemon_ipc_lifetime_reservation_release(reservation);
if (!service) {
if (!runtime_participant_guard_release_complete(&participant_guard,
config->shutdown_timeout_ms)) {
runtime_cleanup_fail_stop("participant_guard_cleanup");
}
runtime_startup_lock_release_complete(&startup_lock, config->shutdown_timeout_ms);
return NULL;
}
service->owned_participant_guard = participant_guard;
runtime_startup_lock_release_complete(&startup_lock, config->shutdown_timeout_ms);
return service;
}
cbm_daemon_runtime_service_state_t cbm_daemon_runtime_service_state(
cbm_daemon_runtime_service_t *service) {
if (!service) {
return CBM_DAEMON_RUNTIME_SERVICE_EXITED;
}
cbm_mutex_lock(&service->mutex);
cbm_daemon_runtime_service_state_t state = service->state;
cbm_mutex_unlock(&service->mutex);
return state;
}
uint64_t cbm_daemon_runtime_service_clients_admitted_total(cbm_daemon_runtime_service_t *service) {
if (!service) {
return 0;
}
cbm_mutex_lock(&service->mutex);
uint64_t total = service->admitted_total;
cbm_mutex_unlock(&service->mutex);
return total;
}
size_t cbm_daemon_runtime_service_active_clients(cbm_daemon_runtime_service_t *service) {
if (!service) {
return 0;
}
cbm_mutex_lock(&service->mutex);
size_t count = service->committed_clients;
cbm_mutex_unlock(&service->mutex);
return count;
}
size_t cbm_daemon_runtime_service_active_connections(cbm_daemon_runtime_service_t *service) {
if (!service) {
return 0;
}
cbm_mutex_lock(&service->mutex);
size_t count = service->active_connections;
cbm_mutex_unlock(&service->mutex);
return count;
}
int cbm_daemon_runtime_service_touch_listener(cbm_daemon_runtime_service_t *service) {
return service && service->listener
? cbm_daemon_ipc_listener_touch(service->listener, service->owned_participant_guard)
: 0;
}
void cbm_daemon_runtime_service_reconcile_lifetime(cbm_daemon_runtime_service_t *service) {
if (!service) {
return;
}
bool retired = false;
cbm_mutex_lock(&service->mutex);
if (service->state == CBM_DAEMON_RUNTIME_SERVICE_RUNNING && !service->permanent &&
service->committed_clients == 0 && service->ephemeral_linger_deadline_ms != 0) {
bool peers_drained = !runtime_has_pending_hello_peer_locked(service, NULL);
bool window_elapsed = cbm_now_ms() >= service->ephemeral_linger_deadline_ms;
if (peers_drained || window_elapsed) {
/* The racing peer drained without committing (retire now), or never
* committed within the bounded window (the backstop that rules out
* an unbounded idle hang — the host_serving 900s mode). */
service->ephemeral_linger_deadline_ms = 0;
runtime_service_begin_stopping_locked(
service, runtime_deadline_after(service->shutdown_timeout_ms), false,
peers_drained ? "ephemeral_peer_drained" : "ephemeral_linger_expired");
retired = true;
}
}
cbm_mutex_unlock(&service->mutex);
if (retired) {
/* Release the coordinator hold now that the linger has resolved. With
* no client left this transitions the coordinator to STOPPING, so the
* service drains and exits cleanly rather than only on the deadline. */
cbm_daemon_coordinator_set_linger(service->coordinator, false);
}
}
size_t cbm_daemon_runtime_service_job_subscribers(cbm_daemon_runtime_service_t *service,
const char *project_key) {
return service ? cbm_daemon_job_subscribers(service->coordinator, project_key) : 0;
}
uint64_t cbm_daemon_runtime_service_client_process_id(cbm_daemon_runtime_service_t *service,
cbm_daemon_client_id_t client_id) {
if (!service || client_id == CBM_DAEMON_CLIENT_ID_INVALID) {
return 0;
}
uint64_t process_id = 0;
cbm_mutex_lock(&service->mutex);
for (size_t i = 0; i < service->worker_capacity; i++) {
cbm_daemon_runtime_worker_t *worker = &service->workers[i];
if (worker->in_use && worker->admitted && worker->admission_committed &&
worker->client_id == client_id) {
process_id = worker->peer_process_id;
break;
}
}
cbm_mutex_unlock(&service->mutex);
return process_id;
}
static bool runtime_wait_for_count(cbm_daemon_runtime_service_t *service, size_t expected,
uint32_t timeout_ms, bool connections) {
if (!service) {
return false;
}
uint64_t deadline = runtime_deadline_after(timeout_ms);
for (;;) {
size_t actual = connections ? cbm_daemon_runtime_service_active_connections(service)
: cbm_daemon_runtime_service_active_clients(service);
if (actual == expected) {
return true;
}
if (cbm_now_ms() >= deadline) {
return false;
}
runtime_wait_tick(deadline);
}
}
bool cbm_daemon_runtime_service_wait_for_clients(cbm_daemon_runtime_service_t *service,
size_t expected, uint32_t timeout_ms) {
return runtime_wait_for_count(service, expected, timeout_ms, false);
}
bool cbm_daemon_runtime_service_wait_for_connections(cbm_daemon_runtime_service_t *service,
size_t expected, uint32_t timeout_ms) {
return runtime_wait_for_count(service, expected, timeout_ms, true);
}
bool cbm_daemon_runtime_service_wait_exited(cbm_daemon_runtime_service_t *service,
uint32_t timeout_ms) {
if (!service) {
return false;
}
uint64_t deadline = runtime_deadline_after(timeout_ms);
for (;;) {
if (cbm_daemon_runtime_service_state(service) == CBM_DAEMON_RUNTIME_SERVICE_EXITED &&
atomic_load_explicit(&service->accept_thread_done, memory_order_acquire)) {
return true;
}
if (cbm_now_ms() >= deadline) {
return false;
}
runtime_wait_tick(deadline);
}
}
bool cbm_daemon_runtime_service_job_reaped(cbm_daemon_runtime_service_t *service,
const char *project_key) {
return service && cbm_daemon_job_reaped(service->coordinator, project_key, cbm_now_ms());
}
bool cbm_daemon_runtime_service_stop(cbm_daemon_runtime_service_t *service, uint32_t timeout_ms) {
if (!service) {
return false;
}
if (cbm_daemon_runtime_service_state(service) == CBM_DAEMON_RUNTIME_SERVICE_EXITED) {
return cbm_daemon_runtime_service_wait_exited(service, timeout_ms);
}
runtime_service_begin_stopping(service, timeout_ms, true, "service_stop");
runtime_service_interrupt_connections(service);
return cbm_daemon_runtime_service_wait_exited(service, timeout_ms);
}
bool cbm_daemon_runtime_service_free(cbm_daemon_runtime_service_t *service) {
if (!service ||
cbm_daemon_runtime_service_state(service) != CBM_DAEMON_RUNTIME_SERVICE_EXITED) {
return false;
}
if (service->accept_thread_started && !service->accept_thread_joined) {
if (cbm_thread_join(&service->accept_thread) != 0) {
return false;
}
service->accept_thread_joined = true;
}
for (size_t i = 0; i < service->worker_capacity; i++) {
cbm_daemon_runtime_worker_t *worker = &service->workers[i];
if (worker->thread_started) {
if (!atomic_load_explicit(&worker->done, memory_order_acquire) ||
cbm_thread_join(&worker->thread) != 0) {
return false;
}
runtime_worker_reset_after_join(worker);
}
}
cbm_daemon_ipc_listener_close(service->listener);
service->listener = NULL;
if (!runtime_participant_guard_release_complete(&service->owned_participant_guard,
service->shutdown_timeout_ms)) {
/* Preserve the service allocation so the caller can retry or force
* process exit without dropping the only live cleanup handle. */
return false;
}
cbm_daemon_coordinator_free(service->coordinator);
(void)runtime_process_image_reference_release(&service->active_image);
runtime_verified_peer_images_release(service);
free(service->conflict_log_path);
for (size_t i = 0; i < service->worker_mutexes_initialized; i++) {
cbm_mutex_destroy(&service->workers[i].send_mutex);
}
free(service->workers);
cbm_mutex_destroy(&service->mutex);
free(service);
return true;
}
bool cbm_daemon_runtime_request_activation_shutdown(
const cbm_daemon_ipc_endpoint_t *endpoint, const cbm_daemon_build_identity_t *identity,
cbm_daemon_runtime_activation_action_t action, uint32_t timeout_ms,
cbm_daemon_runtime_activation_result_t *result_out) {
if (result_out) {
memset(result_out, 0, sizeof(*result_out));
}
uint8_t request[CBM_DAEMON_ACTIVATION_SHUTDOWN_REQUEST_SIZE];
if (!endpoint || !identity || !result_out || timeout_ms == CBM_DAEMON_IPC_WAIT_FOREVER ||
!runtime_activation_request_encode(request, action, identity)) {
return false;
}
cbm_daemon_ipc_connection_t *connection = cbm_daemon_ipc_connect(endpoint, timeout_ms);
bool sent = connection && cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST,
CBM_DAEMON_RUNTIME_OP_ACTIVATION_SHUTDOWN,
request, (uint32_t)sizeof(request));
cbm_daemon_frame_t frame = {0};
uint8_t *payload = NULL;
int received = sent ? cbm_daemon_ipc_receive_frame_bounded(
connection, timeout_ms, CBM_DAEMON_ACTIVATION_SHUTDOWN_RESPONSE_SIZE,
&frame, &payload)
: 0;
bool valid = received == 1 && frame.type == CBM_DAEMON_FRAME_RESPONSE &&
frame.flags == CBM_DAEMON_RUNTIME_OP_ACTIVATION_SHUTDOWN &&
frame.length == CBM_DAEMON_ACTIVATION_SHUTDOWN_RESPONSE_SIZE &&
runtime_activation_response_decode(payload, result_out);
free(payload);
cbm_daemon_ipc_connection_close(connection);
if (!valid) {
memset(result_out, 0, sizeof(*result_out));
}
return valid;
}
static bool runtime_control_request_send(const cbm_daemon_ipc_endpoint_t *endpoint,
const cbm_daemon_build_identity_t *identity,
cbm_daemon_runtime_operation_t operation,
uint32_t timeout_ms, uint32_t response_size,
uint8_t **payload_out, uint64_t *muted_holder_pid_out) {
*payload_out = NULL;
if (muted_holder_pid_out) {
*muted_holder_pid_out = 0;
}
if (!endpoint || !identity || !identity->build_fingerprint ||
timeout_ms == CBM_DAEMON_IPC_WAIT_FOREVER) {
return false;
}
uint8_t request[CBM_DAEMON_CONTROL_REQUEST_SIZE];
memset(request, 0, sizeof(request));
int written = snprintf((char *)request, sizeof(request), "%s", identity->build_fingerprint);
if (written <= 0 || (size_t)written != CBM_DAEMON_BUILD_FINGERPRINT_SIZE - 1U) {
return false;
}
cbm_daemon_ipc_connection_t *connection = cbm_daemon_ipc_connect(endpoint, timeout_ms);
bool sent =
connection && cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST, operation,
request, (uint32_t)sizeof(request));
cbm_daemon_frame_t frame = {0};
uint8_t *payload = NULL;
int received = sent ? cbm_daemon_ipc_receive_frame_bounded(connection, timeout_ms,
response_size, &frame, &payload)
: 0;
if (muted_holder_pid_out && sent &&
(received != 1 || (frame.type == CBM_DAEMON_FRAME_RESPONSE && frame.flags != operation))) {
/* Connected but not served: either total silence (dead runtime), or a
* wrong-operation reject frame (a wedged generation whose accept path
* still answers inline while every worker slot is stuck). Both are
* the zombie class — surface the holder's kernel-reported pid so
* `daemon status` can name it instead of reporting "not running". */
*muted_holder_pid_out = cbm_daemon_ipc_connection_peer_pid(connection);
}
bool valid = received == 1 && frame.type == CBM_DAEMON_FRAME_RESPONSE &&
frame.flags == operation && frame.length == response_size && payload &&
payload[0] == 1U;
cbm_daemon_ipc_connection_close(connection);
if (!valid) {
free(payload);
return false;
}
*payload_out = payload;
return true;
}
static uint32_t runtime_control_read_u32(const uint8_t *bytes) {
return ((uint32_t)bytes[0] << 24) | ((uint32_t)bytes[1] << 16) | ((uint32_t)bytes[2] << 8) |
(uint32_t)bytes[3];
}
bool cbm_daemon_runtime_request_status(const cbm_daemon_ipc_endpoint_t *endpoint,
const cbm_daemon_build_identity_t *identity,
uint32_t timeout_ms,
cbm_daemon_runtime_status_t *status_out) {
if (!status_out) {
return false;
}
memset(status_out, 0, sizeof(*status_out));
uint8_t *payload = NULL;
if (!runtime_control_request_send(endpoint, identity, CBM_DAEMON_RUNTIME_OP_STATUS, timeout_ms,
CBM_DAEMON_STATUS_RESPONSE_SIZE, &payload,
&status_out->muted_endpoint_holder_pid)) {
return false;
}
status_out->permanent = (payload[1] & 0x01U) != 0U;
status_out->stopping = (payload[1] & 0x02U) != 0U;
status_out->committed_clients = (uint16_t)(((uint16_t)payload[2] << 8) | payload[3]);
status_out->daemon_pid = runtime_control_read_u32(payload + 4);
status_out->client_count = payload[8] > CBM_DAEMON_CONTROL_CLIENT_CAP
? (uint8_t)CBM_DAEMON_CONTROL_CLIENT_CAP
: payload[8];
for (size_t index = 0; index < CBM_DAEMON_CONTROL_CLIENT_CAP; index++) {
status_out->client_pids[index] = runtime_control_read_u32(payload + 12U + index * 4U);
}
memcpy(status_out->build_fingerprint, payload + 44U, CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
status_out->build_fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE - 1U] = '\0';
memcpy(status_out->semantic_version, payload + 109U, sizeof(status_out->semantic_version));
status_out->semantic_version[sizeof(status_out->semantic_version) - 1U] = '\0';
free(payload);
return true;
}
/* "; CBM sessions using it: pids 1, 2 and 3 more" — empty when none are known. */
static bool remedy_sessions_format(const cbm_daemon_runtime_status_t *active, char *out,
size_t out_size) {
out[0] = '\0';
size_t listed = active->client_count;
if (listed > CBM_DAEMON_CONTROL_CLIENT_CAP) {
listed = CBM_DAEMON_CONTROL_CLIENT_CAP;
}
size_t used = 0;
for (size_t i = 0; i < listed; i++) {
int written = snprintf(out + used, out_size - used, "%s%lu",
i == 0 ? "; CBM sessions using it: pids " : ", ",
(unsigned long)active->client_pids[i]);
if (written < 0 || (size_t)written >= out_size - used) {
return false;
}
used += (size_t)written;
}
if (listed > 0 && active->committed_clients > listed) {
int written = snprintf(out + used, out_size - used, " and %lu more",
(unsigned long)(active->committed_clients - listed));
if (written < 0 || (size_t)written >= out_size - used) {
return false;
}
}
return true;
}
bool cbm_daemon_conflict_remedy_format(const cbm_daemon_runtime_status_t *active, char *out,
size_t out_size) {
if (!out || out_size == 0) {
return false;
}
out[0] = '\0';
static const char install_hint[] =
"Usually another editor or agent session (for example Codex or Claude Code) "
"launched a different codebase-memory-mcp install: close it, or point every client "
"at one binary (compare `which -a codebase-memory-mcp` with each client's MCP "
"config).";
int written;
if (!active) {
written = snprintf(out, out_size,
"Run `codebase-memory-mcp daemon status` to see the active daemon and "
"the sessions using it. %s `codebase-memory-mcp daemon stop` retires "
"the daemon once no session uses it.",
install_hint);
} else {
char sessions[160];
if (!remedy_sessions_format(active, sessions, sizeof(sessions))) {
return false;
}
const char *lifetime =
active->permanent ? "permanent" : "session-managed, exits when its last session closes";
written = snprintf(out, out_size,
"Active daemon: pid %lu, version %s, %s%s%s. %s "
"`codebase-memory-mcp daemon stop` retires the daemon once no session "
"uses it.",
(unsigned long)active->daemon_pid, active->semantic_version, lifetime,
active->stopping ? ", already stopping" : "", sessions, install_hint);
}
if (written < 0 || (size_t)written >= out_size) {
out[0] = '\0';
return false;
}
return true;
}
bool cbm_daemon_runtime_request_stop(const cbm_daemon_ipc_endpoint_t *endpoint,
const cbm_daemon_build_identity_t *identity,
uint32_t timeout_ms,
cbm_daemon_runtime_stop_result_t *result_out) {
if (!result_out) {
return false;
}
memset(result_out, 0, sizeof(*result_out));
uint8_t *payload = NULL;
if (!runtime_control_request_send(endpoint, identity, CBM_DAEMON_RUNTIME_OP_STOP, timeout_ms,
CBM_DAEMON_STOP_RESPONSE_SIZE, &payload, NULL)) {
return false;
}
result_out->accepted = (payload[1] & 0x01U) != 0U;
result_out->busy = (payload[1] & 0x02U) != 0U;
result_out->committed_clients = (uint16_t)(((uint16_t)payload[2] << 8) | payload[3]);
result_out->client_count = payload[4] > CBM_DAEMON_CONTROL_CLIENT_CAP
? (uint8_t)CBM_DAEMON_CONTROL_CLIENT_CAP
: payload[4];
for (size_t index = 0; index < CBM_DAEMON_CONTROL_CLIENT_CAP; index++) {
result_out->client_pids[index] = runtime_control_read_u32(payload + 8U + index * 4U);
}
free(payload);
return true;
}
cbm_daemon_runtime_client_t *cbm_daemon_runtime_client_connect(
const cbm_daemon_ipc_endpoint_t *endpoint, const cbm_daemon_build_identity_t *identity,
uint32_t timeout_ms, cbm_daemon_runtime_connect_result_t *result_out) {
if (result_out) {
memset(result_out, 0, sizeof(*result_out));
}
uint8_t request[CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE];
if (!endpoint || !identity || !result_out || timeout_ms == CBM_DAEMON_IPC_WAIT_FOREVER ||
!cbm_daemon_runtime_hello_request_encode(request, identity)) {
return NULL;
}
cbm_daemon_ipc_connection_t *connection = cbm_daemon_ipc_connect(endpoint, timeout_ms);
if (!connection) {
return NULL;
}
RUNTIME_HELLO_POINT(CLIENT_CONNECTED);
bool hello_sent =
cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST, CBM_DAEMON_RUNTIME_OP_HELLO,
request, (uint32_t)sizeof(request));
RUNTIME_HELLO_POINT(CLIENT_SENT);
cbm_daemon_frame_t frame = {0};
uint8_t *payload = NULL;
/* A daemon at capacity or stopping answers from its accept loop without
* reading this HELLO and closes at once. When that close lands first, the
* send fails (EPIPE) with the whole answer already queued here: read it,
* or the caller gets a bare transport error and bootstrap classifies by
* lock state instead of by the daemon's own "capacity" / "stopping". */
int received =
hello_sent
? cbm_daemon_ipc_receive_frame_bounded(
connection, timeout_ms, CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE, &frame, &payload)
: cbm_daemon_ipc_receive_frame_after_failed_send(
connection, timeout_ms, CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE, &frame, &payload);
bool valid = received == 1 && frame.type == CBM_DAEMON_FRAME_RESPONSE &&
frame.flags == CBM_DAEMON_RUNTIME_OP_HELLO &&
frame.length == CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE &&
runtime_hello_response_decode(payload, result_out);
free(payload);
if (!hello_sent && (!valid || result_out->status != CBM_DAEMON_RUNTIME_CONNECT_REJECTED)) {
/* Only a rejection can answer a HELLO the daemon never received;
* anything else stays the plain send failure it always was. */
memset(result_out, 0, sizeof(*result_out));
cbm_daemon_ipc_connection_close(connection);
return NULL;
}
if (received != 1) {
/* The kernel completed the pipe/socket connect, so a server process
* exists, yet the HELLO went unanswered. Name that holder: a dead
* runtime behind a live endpoint is otherwise indistinguishable from
* absence, and the 2026-08-29 zombie hid behind exactly that gap. */
result_out->muted_endpoint_holder_pid = cbm_daemon_ipc_connection_peer_pid(connection);
}
if (!valid || result_out->status != CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED) {
cbm_daemon_ipc_connection_close(connection);
return NULL;
}
cbm_daemon_runtime_client_t *client = calloc(1, sizeof(*client));
if (!client) {
cbm_daemon_ipc_connection_close(connection);
memset(result_out, 0, sizeof(*result_out));
return NULL;
}
cbm_mutex_init(&client->exchange_mutex);
cbm_mutex_init(&client->send_mutex);
cbm_mutex_init(&client->state_mutex);
client->connection = connection;
client->client_id = result_out->client_id;
client->authenticated_process_id = result_out->authenticated_process_id;
client->usable = true;
return client;
}
cbm_daemon_client_id_t cbm_daemon_runtime_client_id(const cbm_daemon_runtime_client_t *client) {
return client ? client->client_id : CBM_DAEMON_CLIENT_ID_INVALID;
}
uint64_t cbm_daemon_runtime_client_process_id(const cbm_daemon_runtime_client_t *client) {
return client ? client->authenticated_process_id : 0;
}
static bool runtime_client_exchange(cbm_daemon_runtime_client_t *client,
cbm_daemon_runtime_operation_t operation, const void *request,
uint32_t request_length, uint32_t timeout_ms,
uint8_t **response_out, uint32_t expected_response_length) {
*response_out = NULL;
if (timeout_ms == CBM_DAEMON_IPC_WAIT_FOREVER) {
return false;
}
cbm_mutex_lock(&client->exchange_mutex);
cbm_mutex_lock(&client->state_mutex);
if (!client->usable || client->closing || !client->connection) {
cbm_mutex_unlock(&client->state_mutex);
cbm_mutex_unlock(&client->exchange_mutex);
return false;
}
cbm_daemon_ipc_connection_t *connection = client->connection;
client->exchange_active = true;
cbm_mutex_unlock(&client->state_mutex);
cbm_mutex_lock(&client->send_mutex);
bool sent = cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST, (uint16_t)operation,
request, request_length);
cbm_mutex_unlock(&client->send_mutex);
cbm_daemon_frame_t frame = {0};
uint8_t *payload = NULL;
int received =
sent ? cbm_daemon_ipc_receive_frame(connection, timeout_ms, &frame, &payload) : -1;
bool valid = sent && received == 1 && frame.type == CBM_DAEMON_FRAME_RESPONSE &&
frame.flags == (uint16_t)operation && frame.length == expected_response_length;
cbm_mutex_lock(&client->state_mutex);
client->exchange_active = false;
if (!valid) {
client->usable = false;
}
cbm_mutex_unlock(&client->state_mutex);
if (!valid) {
free(payload);
} else {
*response_out = payload;
}
cbm_mutex_unlock(&client->exchange_mutex);
return valid;
}
cbm_daemon_subscription_result_t cbm_daemon_runtime_client_job_subscribe(
cbm_daemon_runtime_client_t *client, const char *project_key,
cbm_daemon_subscription_id_t *subscription_id_out, uint32_t timeout_ms) {
if (subscription_id_out) {
*subscription_id_out = CBM_DAEMON_SUBSCRIPTION_ID_INVALID;
}
size_t key_length = 0;
if (!client || !subscription_id_out ||
!runtime_bounded_length(project_key, CBM_DAEMON_RUNTIME_PROJECT_KEY_MAX + 1, &key_length) ||
key_length == 0 || key_length > CBM_DAEMON_RUNTIME_PROJECT_KEY_MAX) {
return CBM_DAEMON_SUBSCRIPTION_REJECTED;
}
uint8_t request[SUBSCRIBE_REQUEST_PREFIX_SIZE + CBM_DAEMON_RUNTIME_PROJECT_KEY_MAX];
runtime_put_u32(request, (uint32_t)key_length);
memcpy(request + SUBSCRIBE_REQUEST_PREFIX_SIZE, project_key, key_length);
uint8_t *response = NULL;
if (!runtime_client_exchange(client, CBM_DAEMON_RUNTIME_OP_JOB_SUBSCRIBE, request,
(uint32_t)(SUBSCRIBE_REQUEST_PREFIX_SIZE + key_length), timeout_ms,
&response, SUBSCRIBE_RESPONSE_SIZE)) {
return CBM_DAEMON_SUBSCRIPTION_REJECTED;
}
cbm_daemon_subscription_result_t result =
(cbm_daemon_subscription_result_t)runtime_get_u32(response);
cbm_daemon_subscription_id_t subscription_id = runtime_get_u64(response + 4);
free(response);
if ((result != CBM_DAEMON_SUBSCRIPTION_STARTED && result != CBM_DAEMON_SUBSCRIPTION_JOINED) ||
subscription_id == CBM_DAEMON_SUBSCRIPTION_ID_INVALID) {
return CBM_DAEMON_SUBSCRIPTION_REJECTED;
}
*subscription_id_out = subscription_id;
return result;
}
bool cbm_daemon_runtime_client_job_unsubscribe(cbm_daemon_runtime_client_t *client,
cbm_daemon_subscription_id_t subscription_id,
uint32_t timeout_ms) {
if (!client || subscription_id == CBM_DAEMON_SUBSCRIPTION_ID_INVALID) {
return false;
}
uint8_t request[UNSUBSCRIBE_REQUEST_SIZE];
runtime_put_u64(request, subscription_id);
uint8_t *response = NULL;
if (!runtime_client_exchange(client, CBM_DAEMON_RUNTIME_OP_JOB_UNSUBSCRIBE, request,
(uint32_t)sizeof(request), timeout_ms, &response,
STATUS_RESPONSE_SIZE)) {
return false;
}
bool removed = runtime_get_u32(response) == 1;
free(response);
return removed;
}
bool cbm_daemon_runtime_client_heartbeat(cbm_daemon_runtime_client_t *client, uint32_t timeout_ms) {
if (!client) {
return false;
}
uint8_t *response = NULL;
if (!runtime_client_exchange(client, CBM_DAEMON_RUNTIME_OP_HEARTBEAT, NULL, 0, timeout_ms,
&response, STATUS_RESPONSE_SIZE)) {
return false;
}
bool valid = runtime_get_u32(response) == 1;
free(response);
return valid;
}
bool cbm_daemon_runtime_client_application_token_reserve(
cbm_daemon_runtime_client_t *client, cbm_daemon_runtime_application_token_t *token_out) {
if (token_out) {
*token_out = CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
}
if (!client || !token_out) {
return false;
}
cbm_mutex_lock(&client->state_mutex);
bool valid = client->usable && !client->closing && client->connection &&
client->active_application_token == CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID &&
client->next_application_token == client->last_started_application_token &&
client->next_application_token != UINT64_MAX;
if (valid) {
client->next_application_token++;
*token_out = client->next_application_token;
}
cbm_mutex_unlock(&client->state_mutex);
return valid;
}
cbm_daemon_runtime_cancel_result_t cbm_daemon_runtime_client_application_cancel(
cbm_daemon_runtime_client_t *client, cbm_daemon_runtime_application_token_t request_token) {
if (!client || request_token == CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID) {
return CBM_DAEMON_RUNTIME_CANCEL_ERROR;
}
bool dispatch = false;
cbm_mutex_lock(&client->state_mutex);
if (!client->usable || client->closing || !client->connection) {
cbm_mutex_unlock(&client->state_mutex);
return CBM_DAEMON_RUNTIME_CANCEL_ERROR;
}
if (client->active_application_token == request_token) {
client->pending_cancel_token = request_token;
dispatch = client->application_request_sent && !client->application_cancel_sent;
} else if (client->active_application_token == CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID &&
request_token == client->next_application_token &&
request_token > client->last_started_application_token) {
/* The frontend publishes its active identity immediately after token
* reservation. Preserve this one pre-send cancellation until the
* tagged exchange emits REQUEST then CANCEL under send_mutex. */
client->pending_cancel_token = request_token;
} else {
cbm_mutex_unlock(&client->state_mutex);
return CBM_DAEMON_RUNTIME_CANCEL_STALE;
}
cbm_mutex_unlock(&client->state_mutex);
if (!dispatch) {
return CBM_DAEMON_RUNTIME_CANCEL_ACCEPTED;
}
uint8_t wire[APPLICATION_CANCEL_REQUEST_SIZE];
runtime_put_u64(wire, request_token);
cbm_daemon_ipc_connection_t *connection = NULL;
bool already_sent = false;
bool unavailable = false;
cbm_mutex_lock(&client->send_mutex);
cbm_mutex_lock(&client->state_mutex);
dispatch = client->usable && !client->closing && client->connection &&
client->active_application_token == request_token &&
client->application_request_sent && !client->application_cancel_sent;
if (dispatch) {
client->application_cancel_sent = true;
connection = client->connection;
} else {
unavailable = !client->usable || client->closing || !client->connection;
already_sent = client->usable && !client->closing && client->connection &&
client->active_application_token == request_token &&
client->application_cancel_sent;
}
cbm_mutex_unlock(&client->state_mutex);
bool sent = !dispatch || cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST,
CBM_DAEMON_RUNTIME_OP_APPLICATION_CANCEL,
wire, (uint32_t)sizeof(wire));
cbm_mutex_unlock(&client->send_mutex);
if (!dispatch) {
/* The request path may have observed pending_cancel_token and emitted
* the control while this caller waited for send_mutex. That is still
* an accepted cancellation, not a stale target. */
return already_sent ? CBM_DAEMON_RUNTIME_CANCEL_ACCEPTED
: (unavailable ? CBM_DAEMON_RUNTIME_CANCEL_ERROR
: CBM_DAEMON_RUNTIME_CANCEL_STALE);
}
if (!sent) {
cbm_mutex_lock(&client->state_mutex);
client->usable = false;
cbm_mutex_unlock(&client->state_mutex);
cbm_daemon_ipc_connection_interrupt(connection);
return CBM_DAEMON_RUNTIME_CANCEL_ERROR;
}
return CBM_DAEMON_RUNTIME_CANCEL_ACCEPTED;
}
cbm_daemon_runtime_application_status_t cbm_daemon_runtime_client_application_request_tagged(
cbm_daemon_runtime_client_t *client, cbm_daemon_runtime_application_token_t request_token,
const void *request, uint32_t request_length, uint8_t **response_out,
uint32_t *response_length_out, bool *request_sent_out, uint32_t timeout_ms) {
if (response_out) {
*response_out = NULL;
}
if (response_length_out) {
*response_length_out = 0;
}
if (request_sent_out) {
*request_sent_out = false;
}
if (!client || !response_out || !response_length_out ||
request_token == CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID ||
timeout_ms == CBM_DAEMON_IPC_WAIT_FOREVER ||
request_length > CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX ||
(request_length > 0 && !request)) {
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
}
uint64_t wire_length = APPLICATION_REQUEST_PREFIX_SIZE + (uint64_t)request_length;
if (wire_length > CBM_DAEMON_MAX_FRAME_SIZE || wire_length > UINT32_MAX) {
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
}
uint8_t *wire = malloc((size_t)wire_length);
if (!wire) {
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
}
runtime_put_u64(wire, request_token);
runtime_put_u32(wire + 8, request_length);
if (request_length > 0) {
memcpy(wire + APPLICATION_REQUEST_PREFIX_SIZE, request, request_length);
}
cbm_mutex_lock(&client->exchange_mutex);
cbm_mutex_lock(&client->state_mutex);
if (!client->usable || client->closing || !client->connection ||
request_token != client->next_application_token ||
request_token <= client->last_started_application_token ||
client->active_application_token != CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID) {
cbm_mutex_unlock(&client->state_mutex);
cbm_mutex_unlock(&client->exchange_mutex);
free(wire);
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
}
cbm_daemon_ipc_connection_t *connection = client->connection;
client->exchange_active = true;
client->last_started_application_token = request_token;
client->active_application_token = request_token;
client->application_request_sent = false;
client->application_cancel_sent = false;
if (client->pending_cancel_token != request_token) {
client->pending_cancel_token = CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
}
cbm_mutex_unlock(&client->state_mutex);
uint8_t cancel_wire[APPLICATION_CANCEL_REQUEST_SIZE];
runtime_put_u64(cancel_wire, request_token);
cbm_mutex_lock(&client->send_mutex);
cbm_mutex_lock(&client->state_mutex);
bool request_can_send = client->usable && !client->closing &&
client->connection == connection &&
client->active_application_token == request_token;
cbm_mutex_unlock(&client->state_mutex);
bool sent =
request_can_send && cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST,
CBM_DAEMON_RUNTIME_OP_APPLICATION_REQUEST,
wire, (uint32_t)wire_length);
if (request_sent_out) {
*request_sent_out = sent;
}
bool send_cancel = false;
cbm_mutex_lock(&client->state_mutex);
client->application_request_sent = sent;
send_cancel = sent && !client->closing && client->pending_cancel_token == request_token &&
!client->application_cancel_sent;
if (send_cancel) {
client->application_cancel_sent = true;
}
cbm_mutex_unlock(&client->state_mutex);
if (send_cancel) {
sent = cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST,
CBM_DAEMON_RUNTIME_OP_APPLICATION_CANCEL, cancel_wire,
(uint32_t)sizeof(cancel_wire));
if (!sent) {
cbm_mutex_lock(&client->state_mutex);
client->usable = false;
cbm_mutex_unlock(&client->state_mutex);
}
}
cbm_mutex_unlock(&client->send_mutex);
free(wire);
cbm_daemon_frame_t frame = {0};
uint8_t *payload = NULL;
int received =
sent ? cbm_daemon_ipc_receive_frame(connection, timeout_ms, &frame, &payload) : -1;
bool protocol_valid = sent && received == 1 && frame.type == CBM_DAEMON_FRAME_RESPONSE &&
frame.flags == CBM_DAEMON_RUNTIME_OP_APPLICATION_REQUEST &&
frame.length >= APPLICATION_RESPONSE_PREFIX_SIZE && payload;
cbm_daemon_runtime_application_status_t status = CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
uint32_t response_length = 0;
if (protocol_valid) {
cbm_daemon_runtime_application_token_t response_token = runtime_get_u64(payload);
status = (cbm_daemon_runtime_application_status_t)runtime_get_u32(payload + 8);
response_length = runtime_get_u32(payload + 12);
protocol_valid = response_token == request_token &&
status >= CBM_DAEMON_RUNTIME_APPLICATION_OK &&
status <= CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED &&
response_length <= CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX &&
frame.length == APPLICATION_RESPONSE_PREFIX_SIZE + response_length &&
(status == CBM_DAEMON_RUNTIME_APPLICATION_OK || response_length == 0);
}
uint8_t *response_copy = NULL;
bool copied = true;
if (protocol_valid && response_length > 0) {
response_copy = malloc(response_length);
copied = response_copy != NULL;
if (copied) {
memcpy(response_copy, payload + APPLICATION_RESPONSE_PREFIX_SIZE, response_length);
}
}
free(payload);
cbm_mutex_lock(&client->state_mutex);
client->exchange_active = false;
if (client->active_application_token == request_token) {
client->active_application_token = CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
client->application_request_sent = false;
client->application_cancel_sent = false;
}
if (client->pending_cancel_token == request_token) {
client->pending_cancel_token = CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
}
if (!protocol_valid) {
client->usable = false;
}
cbm_mutex_unlock(&client->state_mutex);
cbm_mutex_unlock(&client->exchange_mutex);
if (!protocol_valid || !copied) {
free(response_copy);
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
}
*response_out = response_copy;
*response_length_out = response_length;
return status;
}
cbm_daemon_runtime_application_status_t cbm_daemon_runtime_client_application_request(
cbm_daemon_runtime_client_t *client, const void *request, uint32_t request_length,
uint8_t **response_out, uint32_t *response_length_out, uint32_t timeout_ms) {
if (response_out) {
*response_out = NULL;
}
if (response_length_out) {
*response_length_out = 0;
}
if (!client || !response_out || !response_length_out ||
timeout_ms == CBM_DAEMON_IPC_WAIT_FOREVER ||
request_length > CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX ||
(request_length > 0 && !request)) {
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
}
cbm_daemon_runtime_application_token_t request_token =
CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
if (!cbm_daemon_runtime_client_application_token_reserve(client, &request_token)) {
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
}
return cbm_daemon_runtime_client_application_request_tagged(
client, request_token, request, request_length, response_out, response_length_out, NULL,
timeout_ms);
}
bool cbm_daemon_runtime_client_close_begin(cbm_daemon_runtime_client_t *client) {
if (!client) {
return false;
}
/* Serialize with request publication. If the request won send_mutex, its
* exact token is visible here and cancellation is ordered after REQUEST;
* if close won, the request path's closing recheck refuses the late send. */
cbm_mutex_lock(&client->send_mutex);
cbm_mutex_lock(&client->state_mutex);
if (client->closing) {
cbm_mutex_unlock(&client->state_mutex);
cbm_mutex_unlock(&client->send_mutex);
return false;
}
client->closing = true;
client->close_interrupted_exchange = client->exchange_active;
cbm_daemon_ipc_connection_t *connection = client->connection;
cbm_daemon_runtime_application_token_t cancel_token = client->active_application_token;
bool send_cancel = client->close_interrupted_exchange && connection &&
client->application_request_sent && !client->application_cancel_sent &&
cancel_token != CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
if (send_cancel) {
client->application_cancel_sent = true;
}
cbm_mutex_unlock(&client->state_mutex);
if (send_cancel) {
uint8_t wire[APPLICATION_CANCEL_REQUEST_SIZE];
runtime_put_u64(wire, cancel_token);
(void)cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST,
CBM_DAEMON_RUNTIME_OP_APPLICATION_CANCEL, wire,
(uint32_t)sizeof(wire));
}
if (client->close_interrupted_exchange && connection) {
/* The interrupted-exchange close cannot run the DISCONNECT handshake,
* so announce the departure explicitly (ordered after the cancel);
* the server releases this client's admission on receipt instead of
* waiting for the handle to close. Mirrors POSIX shutdown() timing. */
(void)cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST,
CBM_DAEMON_RUNTIME_OP_CLOSE_INTENT, NULL, 0);
cbm_daemon_ipc_connection_interrupt(connection);
}
cbm_mutex_unlock(&client->send_mutex);
return true;
}
bool cbm_daemon_runtime_client_close_finish(cbm_daemon_runtime_client_t *client,
uint32_t timeout_ms) {
if (!client) {
return false;
}
cbm_mutex_lock(&client->state_mutex);
bool closing = client->closing;
cbm_mutex_unlock(&client->state_mutex);
if (!closing) {
return false;
}
cbm_mutex_lock(&client->exchange_mutex);
bool acknowledged = false;
cbm_mutex_lock(&client->state_mutex);
bool can_disconnect = timeout_ms != CBM_DAEMON_IPC_WAIT_FOREVER &&
!client->close_interrupted_exchange && client->usable &&
client->connection != NULL;
cbm_daemon_ipc_connection_t *connection = client->connection;
cbm_mutex_unlock(&client->state_mutex);
bool disconnect_sent = false;
if (can_disconnect) {
cbm_mutex_lock(&client->send_mutex);
disconnect_sent = cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST,
CBM_DAEMON_RUNTIME_OP_DISCONNECT, NULL, 0);
cbm_mutex_unlock(&client->send_mutex);
}
if (disconnect_sent) {
cbm_daemon_frame_t frame = {0};
uint8_t *response = NULL;
int received = cbm_daemon_ipc_receive_frame(connection, timeout_ms, &frame, &response);
acknowledged = received == 1 && frame.type == CBM_DAEMON_FRAME_RESPONSE &&
frame.flags == CBM_DAEMON_RUNTIME_OP_DISCONNECT &&
frame.length == STATUS_RESPONSE_SIZE && response &&
runtime_get_u32(response) == 1;
free(response);
}
cbm_mutex_lock(&client->state_mutex);
connection = client->connection;
client->connection = NULL;
client->usable = false;
client->exchange_active = false;
cbm_mutex_unlock(&client->state_mutex);
cbm_daemon_ipc_connection_close(connection);
cbm_mutex_unlock(&client->exchange_mutex);
cbm_mutex_destroy(&client->state_mutex);
cbm_mutex_destroy(&client->send_mutex);
cbm_mutex_destroy(&client->exchange_mutex);
free(client);
return acknowledged;
}
bool cbm_daemon_runtime_client_close(cbm_daemon_runtime_client_t *client, uint32_t timeout_ms) {
if (!cbm_daemon_runtime_client_close_begin(client)) {
return false;
}
return cbm_daemon_runtime_client_close_finish(client, timeout_ms);
}