Skip to content

Commit 2dd296b

Browse files
committed
fix(daemon): answer the client when its binary image is rejected (DeusData#1383)
An image-verification rejection logged daemon.client_image_rejected and finished the worker WITHOUT sending a hello response - the only admission-failure path that never answered the peer. The client reported status "pending" indefinitely, indistinguishable from a slow cold start, with the reason visible only in the daemon log. The rejection now sends a CONNECT_REJECTED hello response carrying the reason (fingerprint_mismatch / image_unverifiable) and an actionable message, matching the version-conflict and capacity paths. The version-conflict path already responds to unverified peers, so this discloses nothing new to a same-uid local peer; admission stays rejected either way. Adds a CBM_ENABLE_TEST_SEAMS-only seam forcing peer-image verification to fail, since the in-process harness's peer pid is OS-authenticated socket credentials and always verifies against the service's own active image. Test: daemon_runtime_image_rejection_reaches_client_issue1383 - the client receives CONNECT_REJECTED with the reason in the message. RED without the response block (client times out), GREEN with it. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent ae5e9e4 commit 2dd296b

3 files changed

Lines changed: 77 additions & 9 deletions

File tree

‎src/daemon/runtime.c‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@
1818
#include <stdlib.h>
1919
#include <string.h>
2020

21+
#ifdef CBM_ENABLE_TEST_SEAMS
22+
/* #1383 test seam: force peer-image verification to fail so the rejection
23+
* -response path is reachable from the in-process harness — the peer pid is
24+
* OS-authenticated socket credentials, so a same-process peer always verifies
25+
* against the service's own active image. */
26+
static atomic_bool runtime_force_peer_image_unverified_seam;
27+
void cbm_daemon_runtime_force_peer_image_unverified_for_testing(bool force) {
28+
atomic_store(&runtime_force_peer_image_unverified_seam, force);
29+
}
30+
#endif
31+
2132
#ifdef _WIN32
2233
#ifndef WIN32_LEAN_AND_MEAN
2334
#define WIN32_LEAN_AND_MEAN
@@ -1728,9 +1739,29 @@ static void *runtime_connection_worker(void *opaque) {
17281739
strcmp(peer_fingerprint, requested_build) == 0 &&
17291740
strcmp(peer_fingerprint, service->identity.build_fingerprint) == 0;
17301741
}
1742+
#ifdef CBM_ENABLE_TEST_SEAMS
1743+
if (atomic_load(&runtime_force_peer_image_unverified_seam)) {
1744+
peer_image_verified = false;
1745+
peer_image_fingerprinted = false;
1746+
}
1747+
#endif
17311748
if (!peer_image_verified) {
1732-
cbm_log_error("daemon.client_image_rejected", "reason",
1733-
peer_image_fingerprinted ? "fingerprint_mismatch" : "image_unverifiable");
1749+
const char *reason =
1750+
peer_image_fingerprinted ? "fingerprint_mismatch" : "image_unverifiable";
1751+
cbm_log_error("daemon.client_image_rejected", "reason", reason);
1752+
/* #1383: answer the peer before closing. An unanswered rejection is
1753+
* indistinguishable from a slow cold start on the client side — the
1754+
* caller sat on "pending" indefinitely with the reason visible only in
1755+
* the daemon log. The version-conflict path above already responds to
1756+
* unverified peers, so this discloses nothing new to a same-uid local
1757+
* peer; admission stays rejected either way. */
1758+
runtime_result_rejected(&hello_result, "CBM daemon rejected this client's binary image");
1759+
(void)snprintf(hello_result.message, sizeof(hello_result.message),
1760+
"CBM daemon rejected this client: %s. The client binary must match the "
1761+
"running daemon's build; close CBM sessions (or run 'daemon stop') and "
1762+
"retry with one consistent install.",
1763+
reason);
1764+
(void)runtime_send_hello_response(worker->connection, &hello_result);
17341765
runtime_worker_finish(worker);
17351766
return NULL;
17361767
}

‎src/daemon/runtime.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,4 +411,10 @@ bool cbm_daemon_runtime_client_close_finish(cbm_daemon_runtime_client_t *client,
411411
* and join that worker before close_finish. */
412412
bool cbm_daemon_runtime_client_close(cbm_daemon_runtime_client_t *client, uint32_t timeout_ms);
413413

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

‎tests/test_daemon_runtime.c‎

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,6 +1846,39 @@ TEST(daemon_runtime_exact_hello_issues_connection_bound_identity) {
18461846
PASS();
18471847
}
18481848

1849+
/* Regression for #1383: an image-verification rejection must be ANSWERED, not
1850+
* silently dropped. The old path logged daemon.client_image_rejected and
1851+
* finished the worker without sending a hello response, so the client sat on
1852+
* "pending" indefinitely - indistinguishable from a slow cold start - with the
1853+
* reason visible only in the daemon log. */
1854+
TEST(daemon_runtime_image_rejection_reaches_client_issue1383) {
1855+
cbm_daemon_build_identity_t identity =
1856+
runtime_test_identity("2.4.0", runtime_test_self_build());
1857+
runtime_test_fixture_t fixture;
1858+
bool started = runtime_test_fixture_start(&fixture, "image-reject", &identity);
1859+
cbm_daemon_runtime_connect_result_t result = {0};
1860+
cbm_daemon_runtime_client_t *client = NULL;
1861+
1862+
cbm_daemon_runtime_force_peer_image_unverified_for_testing(true);
1863+
if (started) {
1864+
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
1865+
RUNTIME_TEST_TIMEOUT_MS, &result);
1866+
}
1867+
cbm_daemon_runtime_force_peer_image_unverified_for_testing(false);
1868+
1869+
bool rejected_with_reason = client == NULL &&
1870+
result.status == CBM_DAEMON_RUNTIME_CONNECT_REJECTED &&
1871+
strstr(result.message, "image_unverifiable") != NULL;
1872+
if (client) {
1873+
(void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
1874+
}
1875+
runtime_test_fixture_finish(&fixture);
1876+
1877+
ASSERT_TRUE(started);
1878+
ASSERT_TRUE(rejected_with_reason);
1879+
PASS();
1880+
}
1881+
18491882
TEST(daemon_runtime_unexpected_frame_payload_is_freed_once) {
18501883
static const uint8_t unexpected_payload[] = {0xde, 0xad, 0xbe, 0xef};
18511884
cbm_daemon_build_identity_t identity =
@@ -4387,7 +4420,6 @@ TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path) {
43874420
}
43884421
#endif
43894422

4390-
43914423
TEST(daemon_runtime_close_begin_releases_admission_with_inflight_request) {
43924424
static const uint8_t request[] = {'b', 'l', 'o', 'c', 'k'};
43934425
cbm_daemon_build_identity_t identity =
@@ -4436,9 +4468,8 @@ TEST(daemon_runtime_close_begin_releases_admission_with_inflight_request) {
44364468
/* The parity contract: after close_begin alone — before the handle
44374469
* closes — the daemon has released this client's admission. POSIX learns
44384470
* through shutdown()/EOF; Windows through the CLOSE_INTENT frame. */
4439-
admission_released_at_begin =
4440-
close_begun &&
4441-
cbm_daemon_runtime_service_wait_for_clients(fixture.service, 0, RUNTIME_TEST_TIMEOUT_MS);
4471+
admission_released_at_begin = close_begun && cbm_daemon_runtime_service_wait_for_clients(
4472+
fixture.service, 0, RUNTIME_TEST_TIMEOUT_MS);
44424473
if (request_thread_started) {
44434474
request_thread_joined = cbm_thread_join(&request_thread) == 0;
44444475
request_thread_started = false;
@@ -4449,9 +4480,8 @@ TEST(daemon_runtime_close_begin_releases_admission_with_inflight_request) {
44494480
call.status == CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED) &&
44504481
call.response == NULL && call.response_length == 0;
44514482
if (client) {
4452-
(void)(close_begun
4453-
? cbm_daemon_runtime_client_close_finish(client, RUNTIME_TEST_TIMEOUT_MS)
4454-
: cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS));
4483+
(void)(close_begun ? cbm_daemon_runtime_client_close_finish(client, RUNTIME_TEST_TIMEOUT_MS)
4484+
: cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS));
44554485
client = NULL;
44564486
}
44574487
if (started) {
@@ -4584,6 +4614,7 @@ SUITE(daemon_runtime) {
45844614
RUN_TEST(daemon_runtime_convenience_service_owns_participant_guard);
45854615
RUN_TEST(daemon_runtime_rendezvous_layout_is_frozen_and_detailed_abi_independent);
45864616
RUN_TEST(daemon_runtime_exact_hello_issues_connection_bound_identity);
4617+
RUN_TEST(daemon_runtime_image_rejection_reaches_client_issue1383);
45874618
RUN_TEST(daemon_runtime_unexpected_frame_payload_is_freed_once);
45884619
RUN_TEST(daemon_runtime_activation_rejects_forged_and_malformed_without_stop);
45894620
RUN_TEST(daemon_runtime_activation_ack_snapshots_then_interrupts_all_clients);

0 commit comments

Comments
 (0)