Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions src/daemon/runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@
#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);
}
#endif

#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
Expand Down Expand Up @@ -1728,9 +1739,29 @@ static void *runtime_connection_worker(void *opaque) {
strcmp(peer_fingerprint, requested_build) == 0 &&
strcmp(peer_fingerprint, service->identity.build_fingerprint) == 0;
}
#ifdef CBM_ENABLE_TEST_SEAMS
if (atomic_load(&runtime_force_peer_image_unverified_seam)) {
peer_image_verified = false;
peer_image_fingerprinted = false;
}
#endif
if (!peer_image_verified) {
cbm_log_error("daemon.client_image_rejected", "reason",
peer_image_fingerprinted ? "fingerprint_mismatch" : "image_unverifiable");
const char *reason =
peer_image_fingerprinted ? "fingerprint_mismatch" : "image_unverifiable";
cbm_log_error("daemon.client_image_rejected", "reason", reason);
/* #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: %s. The client binary must match the "
"running daemon's build; close CBM sessions (or run 'daemon stop') and "
"retry with one consistent install.",
reason);
(void)runtime_send_hello_response(worker->connection, &hello_result);
runtime_worker_finish(worker);
return NULL;
}
Expand Down
6 changes: 6 additions & 0 deletions src/daemon/runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -411,4 +411,10 @@ bool cbm_daemon_runtime_client_close_finish(cbm_daemon_runtime_client_t *client,
* and join that worker before close_finish. */
bool cbm_daemon_runtime_client_close(cbm_daemon_runtime_client_t *client, uint32_t timeout_ms);

#ifdef CBM_ENABLE_TEST_SEAMS
/* #1383 test seam: force peer-image verification to fail so tests can exercise
* the rejection-response path from the in-process harness. */
void cbm_daemon_runtime_force_peer_image_unverified_for_testing(bool force);
#endif

#endif /* CBM_DAEMON_RUNTIME_H */
45 changes: 38 additions & 7 deletions tests/test_daemon_runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -1846,6 +1846,39 @@ TEST(daemon_runtime_exact_hello_issues_connection_bound_identity) {
PASS();
}

/* Regression for #1383: an image-verification rejection must be ANSWERED, not
* silently dropped. The old path logged daemon.client_image_rejected and
* finished the worker without sending a hello response, so the client sat on
* "pending" indefinitely - indistinguishable from a slow cold start - with the
* reason visible only in the daemon log. */
TEST(daemon_runtime_image_rejection_reaches_client_issue1383) {
cbm_daemon_build_identity_t identity =
runtime_test_identity("2.4.0", runtime_test_self_build());
runtime_test_fixture_t fixture;
bool started = runtime_test_fixture_start(&fixture, "image-reject", &identity);
cbm_daemon_runtime_connect_result_t result = {0};
cbm_daemon_runtime_client_t *client = NULL;

cbm_daemon_runtime_force_peer_image_unverified_for_testing(true);
if (started) {
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
RUNTIME_TEST_TIMEOUT_MS, &result);
}
cbm_daemon_runtime_force_peer_image_unverified_for_testing(false);

bool rejected_with_reason = client == NULL &&
result.status == CBM_DAEMON_RUNTIME_CONNECT_REJECTED &&
strstr(result.message, "image_unverifiable") != NULL;
if (client) {
(void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
}
runtime_test_fixture_finish(&fixture);

ASSERT_TRUE(started);
ASSERT_TRUE(rejected_with_reason);
PASS();
}

TEST(daemon_runtime_unexpected_frame_payload_is_freed_once) {
static const uint8_t unexpected_payload[] = {0xde, 0xad, 0xbe, 0xef};
cbm_daemon_build_identity_t identity =
Expand Down Expand Up @@ -4387,7 +4420,6 @@ TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path) {
}
#endif


TEST(daemon_runtime_close_begin_releases_admission_with_inflight_request) {
static const uint8_t request[] = {'b', 'l', 'o', 'c', 'k'};
cbm_daemon_build_identity_t identity =
Expand Down Expand Up @@ -4436,9 +4468,8 @@ TEST(daemon_runtime_close_begin_releases_admission_with_inflight_request) {
/* The parity contract: after close_begin alone — before the handle
* closes — the daemon has released this client's admission. POSIX learns
* through shutdown()/EOF; Windows through the CLOSE_INTENT frame. */
admission_released_at_begin =
close_begun &&
cbm_daemon_runtime_service_wait_for_clients(fixture.service, 0, RUNTIME_TEST_TIMEOUT_MS);
admission_released_at_begin = close_begun && cbm_daemon_runtime_service_wait_for_clients(
fixture.service, 0, RUNTIME_TEST_TIMEOUT_MS);
if (request_thread_started) {
request_thread_joined = cbm_thread_join(&request_thread) == 0;
request_thread_started = false;
Expand All @@ -4449,9 +4480,8 @@ TEST(daemon_runtime_close_begin_releases_admission_with_inflight_request) {
call.status == CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED) &&
call.response == NULL && call.response_length == 0;
if (client) {
(void)(close_begun
? cbm_daemon_runtime_client_close_finish(client, RUNTIME_TEST_TIMEOUT_MS)
: cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS));
(void)(close_begun ? cbm_daemon_runtime_client_close_finish(client, RUNTIME_TEST_TIMEOUT_MS)
: cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS));
client = NULL;
}
if (started) {
Expand Down Expand Up @@ -4584,6 +4614,7 @@ SUITE(daemon_runtime) {
RUN_TEST(daemon_runtime_convenience_service_owns_participant_guard);
RUN_TEST(daemon_runtime_rendezvous_layout_is_frozen_and_detailed_abi_independent);
RUN_TEST(daemon_runtime_exact_hello_issues_connection_bound_identity);
RUN_TEST(daemon_runtime_image_rejection_reaches_client_issue1383);
RUN_TEST(daemon_runtime_unexpected_frame_payload_is_freed_once);
RUN_TEST(daemon_runtime_activation_rejects_forged_and_malformed_without_stop);
RUN_TEST(daemon_runtime_activation_ack_snapshots_then_interrupts_all_clients);
Expand Down
Loading