@@ -36,6 +36,19 @@ static atomic_bool runtime_force_peer_image_mismatch_seam;
3636void cbm_daemon_runtime_force_peer_image_mismatch_for_testing (bool force ) {
3737 atomic_store (& runtime_force_peer_image_mismatch_seam , force );
3838}
39+ /* The abandoned-request containment path ends in process termination, which an
40+ * in-process harness cannot observe. The timeout override makes the ceiling
41+ * reachable in test time; the hook replaces termination with a recordable
42+ * callback. Both stay inert (zero/NULL) outside tests. */
43+ static _Atomic uint32_t runtime_abandoned_request_join_timeout_seam ;
44+ void cbm_daemon_runtime_set_abandoned_request_join_timeout_for_testing (uint32_t timeout_ms ) {
45+ atomic_store (& runtime_abandoned_request_join_timeout_seam , timeout_ms );
46+ }
47+ static _Atomic (cbm_daemon_runtime_containment_hook_t ) runtime_containment_hook_seam ;
48+ void cbm_daemon_runtime_set_containment_hook_for_testing (
49+ cbm_daemon_runtime_containment_hook_t hook ) {
50+ atomic_store (& runtime_containment_hook_seam , hook );
51+ }
3952#endif
4053
4154#ifdef _WIN32
@@ -72,6 +85,16 @@ enum {
7285 * a local cooperative peer is already blocked in read and consumes the
7386 * rejection within milliseconds. */
7487 RUNTIME_REJECT_DRAIN_TIMEOUT_MS = 250 ,
88+ /* Ceiling on how long a disconnecting worker waits for its in-flight
89+ * application request to observe cancellation. session_cancel has already
90+ * run by the time this wait starts, so a compliant handler returns within
91+ * milliseconds; the wait is sized for a handler that only polls its cancel
92+ * flag between long pipeline stages. A handler that ignores cancellation
93+ * past this ceiling turned the daemon into a permanent zombie once
94+ * (2026-08-29): the unbounded join blocked the disconnect path, admission
95+ * wedged behind it, and the dead generation held the endpoint pipes and
96+ * the UI port for nine hours with nothing logged. */
97+ RUNTIME_ABANDONED_REQUEST_JOIN_TIMEOUT_MS = 30000 ,
7598 RUNTIME_PATH_CAP = 4096 ,
7699
77100 RENDEZVOUS_REQUEST_ABI_OFFSET = 0 ,
@@ -1313,13 +1336,62 @@ static void *runtime_application_worker(void *opaque) {
13131336 return NULL ;
13141337}
13151338
1339+ static _Noreturn void runtime_cleanup_fail_stop (const char * component );
1340+
1341+ static void runtime_contain_unresponsive_application (cbm_daemon_runtime_worker_t * worker ) {
1342+ char peer_pid [32 ];
1343+ char token [32 ];
1344+ (void )snprintf (peer_pid , sizeof (peer_pid ), "%llu" , (unsigned long long )worker -> peer_process_id );
1345+ (void )snprintf (token , sizeof (token ), "%llu" ,
1346+ (unsigned long long )worker -> application_request_token );
1347+ cbm_log_error ("daemon.application_request_unresponsive" , "peer_pid" , peer_pid , "request_token" ,
1348+ token );
1349+ #ifdef CBM_ENABLE_TEST_SEAMS
1350+ cbm_daemon_runtime_containment_hook_t hook = atomic_load (& runtime_containment_hook_seam );
1351+ if (hook ) {
1352+ hook ("application_request_join" );
1353+ return ;
1354+ }
1355+ #endif
1356+ runtime_cleanup_fail_stop ("application_request_join" );
1357+ }
1358+
13161359static bool runtime_worker_reap_application (cbm_daemon_runtime_worker_t * worker , bool wait ) {
13171360 if (!worker -> application_thread_started ) {
13181361 return true;
13191362 }
13201363 if (!wait && !atomic_load_explicit (& worker -> application_thread_done , memory_order_acquire )) {
13211364 return false;
13221365 }
1366+ if (!atomic_load_explicit (& worker -> application_thread_done , memory_order_acquire )) {
1367+ /* The peer is gone and session_cancel already ran, so the handler owes
1368+ * a prompt return; only a handler that ignores cancellation reaches
1369+ * the ceiling. An unbounded join here once turned one wedged request
1370+ * into a whole-daemon zombie (2026-08-29): the disconnect path
1371+ * blocked, the worker slot never released, and the dead generation
1372+ * held the endpoint pipes and UI port for nine hours with nothing
1373+ * logged while every new client timed out bare. Containment
1374+ * terminates the process instead — the kernel releases every native
1375+ * claim and the next client starts a fresh generation. */
1376+ uint32_t timeout_ms = RUNTIME_ABANDONED_REQUEST_JOIN_TIMEOUT_MS ;
1377+ #ifdef CBM_ENABLE_TEST_SEAMS
1378+ uint32_t seam_timeout = atomic_load (& runtime_abandoned_request_join_timeout_seam );
1379+ if (seam_timeout != 0 ) {
1380+ timeout_ms = seam_timeout ;
1381+ }
1382+ #endif
1383+ uint64_t deadline = runtime_deadline_after (timeout_ms );
1384+ while (!atomic_load_explicit (& worker -> application_thread_done , memory_order_acquire )) {
1385+ if (cbm_now_ms () >= deadline ) {
1386+ /* Terminal in production. A test containment hook returns, and
1387+ * the join then waits for the harness to release the handler
1388+ * so teardown stays leak-free under the sanitizers. */
1389+ runtime_contain_unresponsive_application (worker );
1390+ deadline = UINT64_MAX ;
1391+ }
1392+ runtime_wait_tick (deadline );
1393+ }
1394+ }
13231395 if (cbm_thread_join (& worker -> application_thread ) != 0 ) {
13241396 return false;
13251397 }
@@ -2569,8 +2641,11 @@ static bool runtime_control_request_send(const cbm_daemon_ipc_endpoint_t *endpoi
25692641 const cbm_daemon_build_identity_t * identity ,
25702642 cbm_daemon_runtime_operation_t operation ,
25712643 uint32_t timeout_ms , uint32_t response_size ,
2572- uint8_t * * payload_out ) {
2644+ uint8_t * * payload_out , uint64_t * muted_holder_pid_out ) {
25732645 * payload_out = NULL ;
2646+ if (muted_holder_pid_out ) {
2647+ * muted_holder_pid_out = 0 ;
2648+ }
25742649 if (!endpoint || !identity || !identity -> build_fingerprint ||
25752650 timeout_ms == CBM_DAEMON_IPC_WAIT_FOREVER ) {
25762651 return false;
@@ -2590,6 +2665,15 @@ static bool runtime_control_request_send(const cbm_daemon_ipc_endpoint_t *endpoi
25902665 int received = sent ? cbm_daemon_ipc_receive_frame_bounded (connection , timeout_ms ,
25912666 response_size , & frame , & payload )
25922667 : 0 ;
2668+ if (muted_holder_pid_out && sent &&
2669+ (received != 1 || (frame .type == CBM_DAEMON_FRAME_RESPONSE && frame .flags != operation ))) {
2670+ /* Connected but not served: either total silence (dead runtime), or a
2671+ * wrong-operation reject frame (a wedged generation whose accept path
2672+ * still answers inline while every worker slot is stuck). Both are
2673+ * the zombie class — surface the holder's kernel-reported pid so
2674+ * `daemon status` can name it instead of reporting "not running". */
2675+ * muted_holder_pid_out = cbm_daemon_ipc_connection_peer_pid (connection );
2676+ }
25932677 bool valid = received == 1 && frame .type == CBM_DAEMON_FRAME_RESPONSE &&
25942678 frame .flags == operation && frame .length == response_size && payload &&
25952679 payload [0 ] == 1U ;
@@ -2617,7 +2701,8 @@ bool cbm_daemon_runtime_request_status(const cbm_daemon_ipc_endpoint_t *endpoint
26172701 memset (status_out , 0 , sizeof (* status_out ));
26182702 uint8_t * payload = NULL ;
26192703 if (!runtime_control_request_send (endpoint , identity , CBM_DAEMON_RUNTIME_OP_STATUS , timeout_ms ,
2620- CBM_DAEMON_STATUS_RESPONSE_SIZE , & payload )) {
2704+ CBM_DAEMON_STATUS_RESPONSE_SIZE , & payload ,
2705+ & status_out -> muted_endpoint_holder_pid )) {
26212706 return false;
26222707 }
26232708 status_out -> permanent = (payload [1 ] & 0x01U ) != 0U ;
@@ -2648,7 +2733,7 @@ bool cbm_daemon_runtime_request_stop(const cbm_daemon_ipc_endpoint_t *endpoint,
26482733 memset (result_out , 0 , sizeof (* result_out ));
26492734 uint8_t * payload = NULL ;
26502735 if (!runtime_control_request_send (endpoint , identity , CBM_DAEMON_RUNTIME_OP_STOP , timeout_ms ,
2651- CBM_DAEMON_STOP_RESPONSE_SIZE , & payload )) {
2736+ CBM_DAEMON_STOP_RESPONSE_SIZE , & payload , NULL )) {
26522737 return false;
26532738 }
26542739 result_out -> accepted = (payload [1 ] & 0x01U ) != 0U ;
@@ -2691,6 +2776,13 @@ cbm_daemon_runtime_client_t *cbm_daemon_runtime_client_connect(
26912776 frame .length == CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE &&
26922777 runtime_hello_response_decode (payload , result_out );
26932778 free (payload );
2779+ if (received != 1 ) {
2780+ /* The kernel completed the pipe/socket connect, so a server process
2781+ * exists, yet the HELLO went unanswered. Name that holder: a dead
2782+ * runtime behind a live endpoint is otherwise indistinguishable from
2783+ * absence, and the 2026-08-29 zombie hid behind exactly that gap. */
2784+ result_out -> muted_endpoint_holder_pid = cbm_daemon_ipc_connection_peer_pid (connection );
2785+ }
26942786 if (!valid || result_out -> status != CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED ) {
26952787 cbm_daemon_ipc_connection_close (connection );
26962788 return NULL ;
0 commit comments