diff --git a/src/daemon/ipc.c b/src/daemon/ipc.c index 91147aa9b..a706a7ca5 100644 --- a/src/daemon/ipc.c +++ b/src/daemon/ipc.c @@ -1983,6 +1983,26 @@ static bool posix_directory_sync(int dir_fd) { return unsupported; } +static bool posix_socket_link_pair_unlink_if_matches(const cbm_daemon_ipc_endpoint_t *endpoint, + const posix_socket_identity_t *identity) { + posix_socket_identity_t stable = {0}; + posix_socket_identity_t anchor = {0}; + struct stat stable_status = {0}; + struct stat anchor_status = {0}; + return endpoint && identity && + posix_socket_path_identity_read(endpoint, endpoint->socket_name, &stable, + &stable_status) == 1 && + posix_socket_path_identity_read(endpoint, endpoint->socket_anchor_name, &anchor, + &anchor_status) == 1 && + stable_status.st_nlink == 2 && anchor_status.st_nlink == 2 && + posix_socket_identity_equal(&stable, identity) && + posix_socket_identity_equal(&anchor, identity) && + posix_socket_path_unlink_inode_if_matches(endpoint->dir_fd, endpoint->socket_name, + identity, 2) && + posix_socket_path_unlink_inode_if_matches(endpoint->dir_fd, endpoint->socket_anchor_name, + identity, 1); +} + static bool posix_socket_record_temp_name(const char *record_name, char temp_name[NAME_MAX + 1]) { if (!record_name || !temp_name) { return false; @@ -2366,23 +2386,7 @@ static int posix_stale_generation_cleanup_locked(const cbm_daemon_ipc_endpoint_t result = -1; goto cleanup_done; } - posix_socket_identity_t confirmed_stable = {0}; - posix_socket_identity_t confirmed_anchor = {0}; - struct stat confirmed_stable_status = {0}; - struct stat confirmed_anchor_status = {0}; - bool confirmed = - posix_socket_path_identity_read(endpoint, endpoint->socket_name, &confirmed_stable, - &confirmed_stable_status) == 1 && - posix_socket_path_identity_read(endpoint, endpoint->socket_anchor_name, - &confirmed_anchor, &confirmed_anchor_status) == 1 && - confirmed_stable_status.st_nlink == 2 && confirmed_anchor_status.st_nlink == 2 && - posix_socket_identity_equal(&confirmed_stable, &marker.identity) && - posix_socket_identity_equal(&confirmed_anchor, &marker.identity); - if (!confirmed || - !posix_socket_path_unlink_inode_if_matches(endpoint->dir_fd, endpoint->socket_name, - &marker.identity, 2) || - !posix_socket_path_unlink_inode_if_matches( - endpoint->dir_fd, endpoint->socket_anchor_name, &marker.identity, 1)) { + if (!posix_socket_link_pair_unlink_if_matches(endpoint, &marker.identity)) { result = -1; goto cleanup_done; } @@ -2441,10 +2445,33 @@ static int posix_stale_generation_cleanup_locked(const cbm_daemon_ipc_endpoint_t goto cleanup_done; } + /* A hard kill can land after the stable link is durable but before either + * publication record exists. With startup serialized and lifetime + * reserved above, the exact owner-private two-name/two-link shape is + * sufficient authority: no unrelated socket can acquire the deterministic + * anchor name without being the same inode. Re-read both names immediately + * before inode-matched unlinking so replacement or link-count races fail + * closed. */ + if (stable_state == 1 && anchor_state == 1 && stable_status.st_nlink == 2 && + anchor_status.st_nlink == 2 && + posix_socket_identity_equal(&stable_identity, &anchor_identity)) { + if (!posix_socket_link_pair_unlink_if_matches(endpoint, &stable_identity) || + !posix_directory_sync(endpoint->dir_fd)) { + result = -1; + goto cleanup_done; + } + result = 1; + goto cleanup_done; + } + /* The deterministic generation-local anchor lets us collect the sole * otherwise-untrackable crash boundary: bind/listen completed but the - * pending record was not yet durable. It never grants authority over the - * public stable path. */ + * pending record was not yet durable. It never grants authority over a + * differing stable socket, so preserve both names when one is present. */ + if (anchor_state == 1 && stable_state == 1) { + result = 0; + goto cleanup_done; + } if (anchor_state == 1) { if (anchor_status.st_nlink != 1 || !posix_socket_path_unlink_inode_if_matches( @@ -3141,11 +3168,11 @@ int cbm_daemon_ipc_generation_probe_under_startup_lock( if (!posix_startup_lock_matches_endpoint(endpoint, startup_lock) || startup_lock->prepared) { return -1; } - int lifetime = cbm_daemon_ipc_lifetime_reservation_probe(endpoint); - if (lifetime != 0) { - return lifetime; + int cleanup = cbm_daemon_ipc_stale_generation_cleanup(endpoint, startup_lock); + if (cleanup == 1) { + return 0; } - return cbm_daemon_ipc_endpoint_probe(endpoint, 0); + return cleanup == 0 ? 1 : -1; } bool cbm_daemon_ipc_startup_lock_prepare_handoff(cbm_daemon_ipc_startup_lock_t *lock) { diff --git a/src/daemon/ipc.h b/src/daemon/ipc.h index 0411995d4..2cdf792dd 100644 --- a/src/daemon/ipc.h +++ b/src/daemon/ipc.h @@ -131,9 +131,11 @@ int cbm_daemon_ipc_lifetime_reservation_probe(const cbm_daemon_ipc_endpoint_t *e /* Remove only a provably current-generation stale Unix socket identity. The * caller must first observe an absent lifetime reservation and retain the * matching startup lock for the complete call; the implementation rechecks - * both conditions. Stable deletion requires a committed marker whose named - * anchor and stable path are the same secure socket inode; pending alone may - * only complete that commit when both paths independently corroborate it. + * both conditions. Stable deletion normally requires a committed marker whose + * named anchor and stable path are the same secure socket inode; pending alone + * may only complete that commit when both paths independently corroborate it. + * With both records absent, the exact owner-private two-name/two-link stable + * socket and anchor shape is also recoverable after immediate revalidation. * A differing stable replacement is preserved while owned anchor/records are * collected. Returns 1 when the stable endpoint is absent and all owned * artifacts are absent or were removed, 0 when cleanup is refused for a live, @@ -193,8 +195,10 @@ int cbm_daemon_ipc_startup_lock_try_acquire(const cbm_daemon_ipc_endpoint_t *end cbm_daemon_ipc_startup_lock_t **lock_out); /* Activation-only no-spawn generation probe under the exact matching, * retained, unprepared startup lock. It ignores the caller's own startup-v2 - * and frozen-legacy startup claims, and reports only a live daemon lifetime, - * stable current transport, or deterministic legacy/current sentinel. + * and frozen-legacy startup claims, serially cleans a provable stale POSIX + * generation under a temporary lifetime reservation, and reports only a live + * daemon lifetime, remaining stable transport, or deterministic + * legacy/current sentinel. * Returns 1 when active, 0 when authoritatively absent, and -1 when the lock, * endpoint, transport, or ownership cannot be validated. */ int cbm_daemon_ipc_generation_probe_under_startup_lock( diff --git a/tests/test_cli.c b/tests/test_cli.c index f1507bd2a..f87881454 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -36,6 +36,8 @@ #include #include #ifndef _WIN32 +#include +#include #include #endif #ifdef __APPLE__ @@ -1341,6 +1343,122 @@ TEST(cli_activation_quiesce_does_not_wait_on_bootstrap_startup) { ASSERT_TRUE(event_order); PASS(); } + +TEST(cli_install_recovers_markerless_stale_rendezvous) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-markerless-rendezvous-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) { + FAIL("cbm_mkdtemp failed"); + } + char runtime_parent[512]; + char cache_dir[512]; + char install_dir[512]; + char target_path[640]; + char activation_log[640]; + snprintf(runtime_parent, sizeof(runtime_parent), "%s/runtime", tmpdir); + snprintf(cache_dir, sizeof(cache_dir), "%s/cache", tmpdir); + snprintf(install_dir, sizeof(install_dir), "%s/custom/bin", tmpdir); + snprintf(target_path, sizeof(target_path), "%s/codebase-memory-mcp", install_dir); + snprintf(activation_log, sizeof(activation_log), "%s/logs/activation-events.ndjson", cache_dir); + if (test_mkdirp(runtime_parent) != 0) { + test_rmdir_r(tmpdir); + FAIL("runtime parent setup failed"); + } + + char *old_home = NULL; + char *old_cache = NULL; + cli_activation_save_env(&old_home, &old_cache); + const char *shell = getenv("SHELL"); + char *old_shell = shell ? strdup(shell) : NULL; + cbm_setenv("HOME", tmpdir, 1); + cbm_setenv("SHELL", "/bin/zsh", 1); + cbm_setenv("CBM_CACHE_DIR", cache_dir, 1); + + cbm_daemon_ipc_endpoint_t *endpoint = cbm_daemon_bootstrap_endpoint_new(runtime_parent); + const char *socket_path = endpoint ? cbm_daemon_ipc_endpoint_address(endpoint) : NULL; + char anchor_path[1024] = {0}; + size_t socket_length = socket_path ? strlen(socket_path) : 0; + bool anchor_path_ok = + socket_length > 5 && strcmp(socket_path + socket_length - 5, ".sock") == 0; + if (anchor_path_ok) { + int written = snprintf(anchor_path, sizeof(anchor_path), "%.*s.anc", + (int)(socket_length - 5), socket_path); + anchor_path_ok = written > 0 && written < (int)sizeof(anchor_path); + } + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + size_t address_length = socket_path ? strlen(socket_path) : 0; + bool address_ok = address_length > 0 && address_length < sizeof(address.sun_path); + if (address_ok) { + memcpy(address.sun_path, socket_path, address_length + 1); + } +#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + address.sun_len = (uint8_t)(offsetof(struct sockaddr_un, sun_path) + address_length + 1); +#endif + socklen_t sockaddr_length = + (socklen_t)(offsetof(struct sockaddr_un, sun_path) + address_length + 1); + int raw_listener = address_ok && anchor_path_ok ? socket(AF_UNIX, SOCK_STREAM, 0) : -1; + struct stat socket_status = {0}; + struct stat anchor_status = {0}; + bool orphan_created = + raw_listener >= 0 && + bind(raw_listener, (const struct sockaddr *)&address, sockaddr_length) == 0 && + chmod(socket_path, 0600) == 0 && listen(raw_listener, 1) == 0 && + link(socket_path, anchor_path) == 0 && lstat(socket_path, &socket_status) == 0 && + lstat(anchor_path, &anchor_status) == 0 && S_ISSOCK(socket_status.st_mode) && + S_ISSOCK(anchor_status.st_mode) && socket_status.st_nlink == 2 && + anchor_status.st_nlink == 2 && socket_status.st_dev == anchor_status.st_dev && + socket_status.st_ino == anchor_status.st_ino; + if (raw_listener >= 0) { + (void)close(raw_listener); + } + + cbm_cli_set_activation_runtime_parent_for_test(runtime_parent); + char dir_arg[640]; + snprintf(dir_arg, sizeof(dir_arg), "--dir=%s", install_dir); + char *install_argv[] = {"--force", "--skip-config", "--yes", dir_arg}; + int install_rc = orphan_created ? cli_test_cmd_install(4, install_argv) : -1; + cbm_cli_set_activation_runtime_parent_for_test(NULL); + cbm_set_auto_answer_for_test(0); + + struct stat target_status = {0}; + struct stat log_status = {0}; + struct stat absent_status = {0}; + bool target_exists = stat(target_path, &target_status) == 0; + bool log_private = stat(activation_log, &log_status) == 0 && (log_status.st_mode & 0077) == 0; + const char *events = read_test_file(activation_log); + const char *requested = events ? strstr(events, "\"phase\":\"requested\"") : NULL; + const char *stopped = events ? strstr(events, "\"phase\":\"daemon_stopped\"") : NULL; + const char *completed = events ? strstr(events, "\"phase\":\"completed\"") : NULL; + bool event_order = + requested && stopped && completed && requested < stopped && stopped < completed; + errno = 0; + bool socket_removed = socket_path && lstat(socket_path, &absent_status) != 0 && errno == ENOENT; + errno = 0; + bool anchor_removed = lstat(anchor_path, &absent_status) != 0 && errno == ENOENT; + + if (old_shell) { + cbm_setenv("SHELL", old_shell, 1); + } else { + cbm_unsetenv("SHELL"); + } + free(old_shell); + cli_activation_restore_env(old_home, old_cache); + cbm_daemon_ipc_endpoint_free(endpoint); + test_rmdir_r(tmpdir); + + ASSERT_TRUE(anchor_path_ok); + ASSERT_TRUE(address_ok); + ASSERT_TRUE(orphan_created); + ASSERT_EQ(install_rc, 0); + ASSERT_TRUE(target_exists); + ASSERT_TRUE(log_private); + ASSERT_TRUE(event_order); + ASSERT_TRUE(socket_removed); + ASSERT_TRUE(anchor_removed); + PASS(); +} #endif TEST(cli_install_force_quiesces_active_cohort_before_replacing_binary) { @@ -14391,6 +14509,7 @@ SUITE(cli) { #ifndef _WIN32 RUN_TEST(cli_activation_cleanup_failure_fail_stops_before_lease_release); RUN_TEST(cli_activation_quiesce_does_not_wait_on_bootstrap_startup); + RUN_TEST(cli_install_recovers_markerless_stale_rendezvous); #endif RUN_TEST(cli_install_force_quiesces_active_cohort_before_replacing_binary); RUN_TEST(cli_install_dir_and_skip_config_stage_first_install_safely); diff --git a/tests/test_daemon_ipc.c b/tests/test_daemon_ipc.c index 977826751..9bb550948 100644 --- a/tests/test_daemon_ipc.c +++ b/tests/test_daemon_ipc.c @@ -3901,6 +3901,170 @@ TEST(daemon_ipc_posix_unknown_socket_without_identity_refuses_cleanup) { PASS(); } +TEST(daemon_ipc_posix_markerless_linked_socket_recovers_under_startup_lock) { + static const char key[] = "d1e2f30415263748"; + char parent[TEST_PATH_CAP] = {0}; + char runtime_dir[TEST_PATH_CAP] = {0}; + char socket_path[TEST_PATH_CAP] = {0}; + char anchor_path[TEST_PATH_CAP] = {0}; + cbm_daemon_ipc_endpoint_t *endpoint = NULL; + cbm_daemon_ipc_startup_lock_t *startup = NULL; + int raw_listener = -1; + struct sockaddr_un address; + socklen_t address_length = 0; + struct stat socket_status = {0}; + struct stat anchor_status = {0}; + struct stat absent_status = {0}; + bool paths_ok = false; + bool orphan_created = false; + int startup_result = -1; + int cleanup_result = -1; + int endpoint_after_cleanup = -1; + bool names_removed = false; + + if (ipc_test_parent_new(parent, "markerless-linked-socket")) { + endpoint = cbm_daemon_ipc_endpoint_new(key, parent); + } + if (endpoint) { + ipc_test_copy_path(runtime_dir, cbm_daemon_ipc_endpoint_runtime_dir(endpoint)); + ipc_test_copy_path(socket_path, cbm_daemon_ipc_endpoint_address(endpoint)); + paths_ok = ipc_test_socket_anchor_path(anchor_path, runtime_dir, key) && + ipc_test_unix_address_set(&address, socket_path, &address_length); + } + if (paths_ok) { + raw_listener = socket(AF_UNIX, SOCK_STREAM, 0); + } + if (raw_listener >= 0) { + orphan_created = + bind(raw_listener, (const struct sockaddr *)&address, address_length) == 0 && + chmod(socket_path, 0600) == 0 && listen(raw_listener, 1) == 0 && + link(socket_path, anchor_path) == 0 && lstat(socket_path, &socket_status) == 0 && + lstat(anchor_path, &anchor_status) == 0 && S_ISSOCK(socket_status.st_mode) && + S_ISSOCK(anchor_status.st_mode) && socket_status.st_nlink == 2 && + anchor_status.st_nlink == 2 && socket_status.st_dev == anchor_status.st_dev && + socket_status.st_ino == anchor_status.st_ino; + (void)close(raw_listener); + raw_listener = -1; + } + if (orphan_created) { + startup_result = cbm_daemon_ipc_startup_lock_try_acquire(endpoint, &startup); + } + if (startup) { + cleanup_result = cbm_daemon_ipc_stale_generation_cleanup(endpoint, startup); + endpoint_after_cleanup = cbm_daemon_ipc_endpoint_probe(endpoint, 0); + errno = 0; + bool socket_removed = lstat(socket_path, &absent_status) != 0 && errno == ENOENT; + errno = 0; + bool anchor_removed = lstat(anchor_path, &absent_status) != 0 && errno == ENOENT; + names_removed = socket_removed && anchor_removed; + } + + cbm_daemon_ipc_startup_lock_release(&startup); + if (raw_listener >= 0) { + (void)close(raw_listener); + } + (void)unlink(anchor_path); + (void)unlink(socket_path); + cbm_daemon_ipc_endpoint_free(endpoint); + ipc_test_remove_tree(runtime_dir, parent); + + ASSERT_TRUE(paths_ok); + ASSERT_TRUE(orphan_created); + ASSERT_EQ(startup_result, 1); + ASSERT_EQ(cleanup_result, 1); + ASSERT_EQ(endpoint_after_cleanup, 0); + ASSERT_TRUE(names_removed); + PASS(); +} + +TEST(daemon_ipc_posix_markerless_mismatched_socket_and_anchor_are_preserved) { + static const char key[] = "e1f2031425364758"; + char parent[TEST_PATH_CAP] = {0}; + char runtime_dir[TEST_PATH_CAP] = {0}; + char socket_path[TEST_PATH_CAP] = {0}; + char anchor_path[TEST_PATH_CAP] = {0}; + cbm_daemon_ipc_endpoint_t *endpoint = NULL; + cbm_daemon_ipc_startup_lock_t *startup = NULL; + int stable_listener = -1; + int anchor_listener = -1; + struct sockaddr_un stable_address; + struct sockaddr_un anchor_address; + socklen_t stable_address_length = 0; + socklen_t anchor_address_length = 0; + struct stat stable_before = {0}; + struct stat anchor_before = {0}; + struct stat stable_after = {0}; + struct stat anchor_after = {0}; + bool paths_ok = false; + bool mismatch_created = false; + int startup_result = -1; + int cleanup_result = -1; + bool mismatch_preserved = false; + + if (ipc_test_parent_new(parent, "markerless-mismatch")) { + endpoint = cbm_daemon_ipc_endpoint_new(key, parent); + } + if (endpoint) { + ipc_test_copy_path(runtime_dir, cbm_daemon_ipc_endpoint_runtime_dir(endpoint)); + ipc_test_copy_path(socket_path, cbm_daemon_ipc_endpoint_address(endpoint)); + paths_ok = + ipc_test_socket_anchor_path(anchor_path, runtime_dir, key) && + ipc_test_unix_address_set(&stable_address, socket_path, &stable_address_length) && + ipc_test_unix_address_set(&anchor_address, anchor_path, &anchor_address_length); + } + if (paths_ok) { + stable_listener = socket(AF_UNIX, SOCK_STREAM, 0); + anchor_listener = socket(AF_UNIX, SOCK_STREAM, 0); + } + if (stable_listener >= 0 && anchor_listener >= 0) { + mismatch_created = bind(stable_listener, (const struct sockaddr *)&stable_address, + stable_address_length) == 0 && + chmod(socket_path, 0600) == 0 && listen(stable_listener, 1) == 0 && + bind(anchor_listener, (const struct sockaddr *)&anchor_address, + anchor_address_length) == 0 && + chmod(anchor_path, 0600) == 0 && listen(anchor_listener, 1) == 0 && + lstat(socket_path, &stable_before) == 0 && + lstat(anchor_path, &anchor_before) == 0 && + S_ISSOCK(stable_before.st_mode) && S_ISSOCK(anchor_before.st_mode) && + stable_before.st_ino != anchor_before.st_ino; + (void)close(stable_listener); + stable_listener = -1; + (void)close(anchor_listener); + anchor_listener = -1; + } + if (mismatch_created) { + startup_result = cbm_daemon_ipc_startup_lock_try_acquire(endpoint, &startup); + } + if (startup) { + cleanup_result = cbm_daemon_ipc_stale_generation_cleanup(endpoint, startup); + mismatch_preserved = lstat(socket_path, &stable_after) == 0 && + lstat(anchor_path, &anchor_after) == 0 && + stable_after.st_dev == stable_before.st_dev && + stable_after.st_ino == stable_before.st_ino && + anchor_after.st_dev == anchor_before.st_dev && + anchor_after.st_ino == anchor_before.st_ino; + } + + cbm_daemon_ipc_startup_lock_release(&startup); + if (stable_listener >= 0) { + (void)close(stable_listener); + } + if (anchor_listener >= 0) { + (void)close(anchor_listener); + } + (void)unlink(anchor_path); + (void)unlink(socket_path); + cbm_daemon_ipc_endpoint_free(endpoint); + ipc_test_remove_tree(runtime_dir, parent); + + ASSERT_TRUE(paths_ok); + ASSERT_TRUE(mismatch_created); + ASSERT_EQ(startup_result, 1); + ASSERT_EQ(cleanup_result, 0); + ASSERT_TRUE(mismatch_preserved); + PASS(); +} + TEST(daemon_ipc_posix_active_listener_is_never_cleaned_under_queue_pressure) { static const char key[] = "c1d2e3f405162738"; enum { CLIENT_CAP = 64 }; @@ -4838,6 +5002,8 @@ SUITE(daemon_ipc) { RUN_TEST(daemon_ipc_posix_pending_without_anchor_never_deletes_stable); RUN_TEST(daemon_ipc_posix_current_generation_crash_cleanup_requires_startup_lock); RUN_TEST(daemon_ipc_posix_unknown_socket_without_identity_refuses_cleanup); + RUN_TEST(daemon_ipc_posix_markerless_linked_socket_recovers_under_startup_lock); + RUN_TEST(daemon_ipc_posix_markerless_mismatched_socket_and_anchor_are_preserved); RUN_TEST(daemon_ipc_posix_active_listener_is_never_cleaned_under_queue_pressure); RUN_TEST(daemon_ipc_posix_partial_frame_timeout_poisons_connection); #ifdef __APPLE__