Skip to content
Open
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
73 changes: 50 additions & 23 deletions src/daemon/ipc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 9 additions & 5 deletions src/daemon/ipc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
119 changes: 119 additions & 0 deletions tests/test_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
#include <sys/stat.h>
#include <unistd.h>
#ifndef _WIN32
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
#endif
#ifdef __APPLE__
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading