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
81 changes: 47 additions & 34 deletions src/daemon/ipc.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ enum {
CBM_DAEMON_IPC_RETRY_INTERVAL_MS = 10,
CBM_DAEMON_IPC_COORDINATION_CLEANUP_MS = 500,
CBM_DAEMON_IPC_WINDOWS_RENDEZVOUS_RETRY_MS = 250,
CBM_DAEMON_IPC_RELEASE_ERROR = -1,
CBM_DAEMON_IPC_RELEASE_COMPLETE = 1,
};

/* Last private-namespace validation refusal, set at the exact failing check.
Expand Down Expand Up @@ -3988,27 +3990,27 @@ bool cbm_daemon_ipc_local_transition_begin_work(cbm_daemon_ipc_local_transition_
return true;
}

bool cbm_daemon_ipc_local_transition_release(cbm_daemon_ipc_local_transition_t **transition_io) {
int cbm_daemon_ipc_local_transition_try_release(cbm_daemon_ipc_local_transition_t **transition_io) {
if (!transition_io) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
cbm_daemon_ipc_local_transition_t *transition = *transition_io;
if (!transition) {
return true;
return CBM_DAEMON_IPC_RELEASE_COMPLETE;
}
if (transition->legacy_fd < 0 || !transition->legacy_process_entry ||
transition->owner_pid != getpid() ||
(!transition->work_begun &&
(transition->startup_v2_fd < 0 || !transition->startup_v2_process_entry))) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
posix_named_lock_release(transition->legacy_fd, transition->legacy_process_entry);
if (!transition->work_begun) {
posix_named_lock_release(transition->startup_v2_fd, transition->startup_v2_process_entry);
}
free(transition);
*transition_io = NULL;
return true;
return CBM_DAEMON_IPC_RELEASE_COMPLETE;
}

#else /* _WIN32 */
Expand Down Expand Up @@ -6203,35 +6205,39 @@ static void win_participant_group_release_complete(cbm_private_file_lock_t **gro
}
}

static bool win_participant_state_release(const cbm_daemon_ipc_endpoint_t *endpoint,
cbm_private_lock_directory_t *directory,
cbm_private_file_lock_t **startup_v2_io,
win_legacy_mutex_guard_t **legacy_guard_io,
cbm_private_file_lock_t **group_io, HANDLE *sentinel_io) {
static int win_participant_state_release(const cbm_daemon_ipc_endpoint_t *endpoint,
cbm_private_lock_directory_t *directory,
cbm_private_file_lock_t **startup_v2_io,
win_legacy_mutex_guard_t **legacy_guard_io,
cbm_private_file_lock_t **group_io, HANDLE *sentinel_io) {
if (!endpoint || !directory || !startup_v2_io || !legacy_guard_io || !group_io ||
!sentinel_io) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
if (!*startup_v2_io) {
int startup = win_startup_v2_try_acquire(directory, startup_v2_io);
if (startup != 1) {
return false;
return startup;
}
}
if (!*legacy_guard_io) {
int legacy = win_legacy_mutex_guard_try_acquire(endpoint, legacy_guard_io);
if (legacy != 1) {
(void)cbm_private_file_lock_release(startup_v2_io);
return false;
if (cbm_private_file_lock_release(startup_v2_io) != CBM_PRIVATE_FILE_LOCK_OK) {
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
return legacy;
}
}
if (!win_participant_group_release_locked(group_io, sentinel_io)) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
if (!win_legacy_mutex_guard_release(legacy_guard_io)) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
return cbm_private_file_lock_release(startup_v2_io) == CBM_PRIVATE_FILE_LOCK_OK;
return cbm_private_file_lock_release(startup_v2_io) == CBM_PRIVATE_FILE_LOCK_OK
? CBM_DAEMON_IPC_RELEASE_COMPLETE
: CBM_DAEMON_IPC_RELEASE_ERROR;
}

cbm_daemon_ipc_listener_t *cbm_daemon_ipc_listen_reserved(
Expand Down Expand Up @@ -7007,9 +7013,9 @@ bool cbm_daemon_ipc_startup_lock_release(cbm_daemon_ipc_startup_lock_t **lock_io
if (!lock) {
return true;
}
if (!win_participant_state_release(lock->endpoint, lock->directory, &lock->startup_v2_lock,
&lock->legacy_guard, &lock->group_lock,
&lock->legacy_sentinel)) {
if (win_participant_state_release(lock->endpoint, lock->directory, &lock->startup_v2_lock,
&lock->legacy_guard, &lock->group_lock,
&lock->legacy_sentinel) != CBM_DAEMON_IPC_RELEASE_COMPLETE) {
return false;
}
cbm_private_lock_directory_close(lock->directory);
Expand Down Expand Up @@ -7072,9 +7078,10 @@ bool cbm_daemon_ipc_participant_guard_release(cbm_daemon_ipc_participant_guard_t
if (!guard) {
return true;
}
if (!win_participant_state_release(
guard->endpoint, guard->directory, &guard->teardown_startup_v2_lock,
&guard->teardown_legacy_guard, &guard->group_lock, &guard->legacy_sentinel)) {
if (win_participant_state_release(guard->endpoint, guard->directory,
&guard->teardown_startup_v2_lock,
&guard->teardown_legacy_guard, &guard->group_lock,
&guard->legacy_sentinel) != CBM_DAEMON_IPC_RELEASE_COMPLETE) {
return false;
}
cbm_private_lock_directory_close(guard->directory);
Expand Down Expand Up @@ -7169,39 +7176,45 @@ bool cbm_daemon_ipc_local_transition_begin_work(cbm_daemon_ipc_local_transition_
return true;
}

bool cbm_daemon_ipc_local_transition_release(cbm_daemon_ipc_local_transition_t **transition_io) {
int cbm_daemon_ipc_local_transition_try_release(cbm_daemon_ipc_local_transition_t **transition_io) {
if (!transition_io) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
cbm_daemon_ipc_local_transition_t *transition = *transition_io;
if (!transition) {
return true;
return CBM_DAEMON_IPC_RELEASE_COMPLETE;
}
if (!transition->endpoint || !transition->directory) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
if (transition->group_lock || transition->legacy_sentinel != INVALID_HANDLE_VALUE ||
transition->teardown_legacy_guard) {
if (!win_participant_state_release(transition->endpoint, transition->directory,
&transition->startup_v2_lock,
&transition->teardown_legacy_guard,
&transition->group_lock, &transition->legacy_sentinel)) {
return false;
int released = win_participant_state_release(
transition->endpoint, transition->directory, &transition->startup_v2_lock,
&transition->teardown_legacy_guard, &transition->group_lock,
&transition->legacy_sentinel);
if (released != CBM_DAEMON_IPC_RELEASE_COMPLETE) {
return released;
}
} else if (transition->startup_v2_lock &&
cbm_private_file_lock_release(&transition->startup_v2_lock) !=
CBM_PRIVATE_FILE_LOCK_OK) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
cbm_private_lock_directory_close(transition->directory);
transition->directory = NULL;
free(transition);
*transition_io = NULL;
return true;
return CBM_DAEMON_IPC_RELEASE_COMPLETE;
}

#endif /* _WIN32 */

bool cbm_daemon_ipc_local_transition_release(cbm_daemon_ipc_local_transition_t **transition_io) {
return cbm_daemon_ipc_local_transition_try_release(transition_io) ==
CBM_DAEMON_IPC_RELEASE_COMPLETE;
}

bool cbm_daemon_ipc_send_frame(cbm_daemon_ipc_connection_t *connection,
cbm_daemon_frame_type_t type, uint16_t flags, const void *payload,
uint32_t length) {
Expand Down
8 changes: 7 additions & 1 deletion src/daemon/ipc.h
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,13 @@ bool cbm_daemon_ipc_local_transition_begin_work(cbm_daemon_ipc_local_transition_
* Windows teardown reacquires startup-v2 and the legacy mutex, releases group
* SH, closes this participant's sentinel last, then releases mutex and v2.
* Success consumes and clears *transition_io; a retryable native cleanup
* failure returns false and retains it. */
* failure or startup-gate contention returns false and retains it. */
bool cbm_daemon_ipc_local_transition_release(cbm_daemon_ipc_local_transition_t **transition_io);
/* Nonblocking release: 1 consumes and clears *transition_io (also when already
* empty), 0 retains it while another startup owns a required gate, and -1
* retains it on an unsafe/native I/O failure. A NULL transition_io is invalid.
* Callers can give healthy contention its own wait policy without extending
* the deadline for repeated native cleanup failures. */
int cbm_daemon_ipc_local_transition_try_release(cbm_daemon_ipc_local_transition_t **transition_io);

#endif /* CBM_DAEMON_IPC_H */
24 changes: 14 additions & 10 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -1645,20 +1645,24 @@ static bool main_project_lock_manager_close(cbm_project_lock_manager_t **manager
}

static bool main_local_transition_close(cbm_daemon_ipc_local_transition_t **transition) {
uint64_t deadline = main_deadline_after(MAIN_COORDINATION_CLEANUP_MS);
uint64_t ceiling = main_deadline_after(MAIN_STARTUP_CONTENTION_CEILING_MS);
uint64_t error_deadline = main_deadline_after(MAIN_COORDINATION_CLEANUP_MS);
while (transition && *transition) {
/* A failed release always RETAINS the transition and is retriable by
* contract: the Windows release transition must briefly try-hold the
* shared startup-v2/legacy gates, so concurrent one-shot teardowns
* legitimately collide and succeed on retry. Success consumes the
* transition; only never-finishing cleanup is a failure, and the
* deadline escalation below owns that. */
(void)cbm_daemon_ipc_local_transition_release(transition);
/* Windows teardown reacquires the startup-v2/legacy gates before
* releasing its participant claim. A live startup owner can hold them
* beyond the native cleanup deadline. Retain protection and use the
* acquisition backstop for contention; repeated native errors still
* get only the short cleanup deadline. Success consumes the handle. */
int status = cbm_daemon_ipc_local_transition_try_release(transition);
if (*transition) {
if (cbm_now_ms() >= deadline) {
uint64_t now = cbm_now_ms();
if (now >= ceiling || (status < 0 && now >= error_deadline)) {
main_coordination_cleanup_fail_stop("local_transition_cleanup");
}
cbm_usleep(1000);
if (status == 0) {
error_deadline = main_deadline_after(MAIN_COORDINATION_CLEANUP_MS);
}
cbm_usleep(status == 0 ? 10000 : 1000);
}
}
return true;
Expand Down
90 changes: 88 additions & 2 deletions tests/test_daemon_ipc.c
Original file line number Diff line number Diff line change
Expand Up @@ -2892,6 +2892,91 @@ TEST(daemon_ipc_local_participants_overlap_and_allow_modern_startup) {
PASS();
}

TEST(daemon_ipc_windows_local_transition_release_reports_live_gate_contention) {
#ifndef _WIN32
PASS();
#else
static const char key[] = "6060717182829393";
char parent[TEST_PATH_CAP] = {0};
char canonical_parent[TEST_PATH_CAP] = {0};
char runtime_dir[TEST_PATH_CAP] = {0};
char legacy_pipe[CBM_DAEMON_IPC_WINDOWS_NAME_CAP] = {0};
char legacy_startup[CBM_DAEMON_IPC_WINDOWS_NAME_CAP] = {0};
cbm_daemon_ipc_endpoint_t *endpoint = NULL;
cbm_daemon_ipc_local_transition_t *transition = NULL;
cbm_daemon_ipc_startup_lock_t *startup = NULL;

bool parent_ok = ipc_test_parent_new(parent, "local-release-busy") &&
ipc_test_full_path(canonical_parent, parent);
bool names_ok = parent_ok && cbm_daemon_ipc_windows_legacy_names(canonical_parent, key,
legacy_pipe, legacy_startup);
if (names_ok) {
endpoint = cbm_daemon_ipc_endpoint_new(key, parent);
}
if (endpoint) {
ipc_test_copy_path(runtime_dir, cbm_daemon_ipc_endpoint_runtime_dir(endpoint));
}
int acquired =
endpoint ? cbm_daemon_ipc_local_transition_try_acquire(endpoint, &transition) : -1;
int sealed = acquired == 1 ? cbm_daemon_ipc_local_transition_seal_legacy(transition) : -1;
int lifetime =
sealed == 1 ? cbm_daemon_ipc_local_transition_lifetime_probe(endpoint, transition) : -1;
bool begun = lifetime == 0 && cbm_daemon_ipc_local_transition_begin_work(transition);
int startup_acquired = begun ? cbm_daemon_ipc_startup_lock_try_acquire(endpoint, &startup) : -1;
int blocked = cbm_daemon_ipc_local_transition_try_release(&transition);
bool retained_while_busy = transition != NULL;
bool boolean_blocked = cbm_daemon_ipc_local_transition_release(&transition);
/* Keep the already-validated named mutex alive after the startup owner
* exits. Holding just this mutex exercises the second contention path. */
wchar_t *legacy_name = names_ok ? cbm_utf8_to_wide(legacy_startup) : NULL;
HANDLE legacy_mutex = startup_acquired == 1 && legacy_name
? OpenMutexW(SYNCHRONIZE | MUTEX_MODIFY_STATE, FALSE, legacy_name)
: NULL;
free(legacy_name);
bool startup_released = cbm_daemon_ipc_startup_lock_release(&startup);
DWORD mutex_wait =
startup_released && legacy_mutex ? WaitForSingleObject(legacy_mutex, 0) : WAIT_FAILED;
bool mutex_owned = mutex_wait == WAIT_OBJECT_0 || mutex_wait == WAIT_ABANDONED;
int legacy_blocked =
mutex_owned ? cbm_daemon_ipc_local_transition_try_release(&transition) : -1;
bool retained_while_legacy_busy = transition != NULL;
bool mutex_released = mutex_owned && ReleaseMutex(legacy_mutex);
if (legacy_mutex) {
(void)CloseHandle(legacy_mutex);
}
int legacy_retained = endpoint ? cbm_daemon_ipc_legacy_generation_probe(endpoint) : -1;
int released = cbm_daemon_ipc_local_transition_try_release(&transition);
bool consumed = transition == NULL;
int legacy_after = endpoint ? cbm_daemon_ipc_legacy_generation_probe(endpoint) : -1;

(void)cbm_daemon_ipc_startup_lock_release(&startup);
(void)cbm_daemon_ipc_local_transition_release(&transition);
cbm_daemon_ipc_endpoint_free(endpoint);
ipc_test_remove_tree(runtime_dir, parent);

ASSERT_TRUE(parent_ok);
ASSERT_TRUE(names_ok);
ASSERT_EQ(acquired, 1);
ASSERT_EQ(sealed, 1);
ASSERT_EQ(lifetime, 0);
ASSERT_TRUE(begun);
ASSERT_EQ(startup_acquired, 1);
ASSERT_EQ(blocked, 0);
ASSERT_TRUE(retained_while_busy);
ASSERT_FALSE(boolean_blocked);
ASSERT_TRUE(startup_released);
ASSERT_EQ(mutex_wait, WAIT_OBJECT_0);
ASSERT_EQ(legacy_blocked, 0);
ASSERT_TRUE(retained_while_legacy_busy);
ASSERT_TRUE(mutex_released);
ASSERT_EQ(legacy_retained, 1);
ASSERT_EQ(released, 1);
ASSERT_TRUE(consumed);
ASSERT_EQ(legacy_after, 0);
PASS();
#endif
}

TEST(daemon_ipc_windows_local_transition_release_retries_retained_mutex) {
#ifndef _WIN32
PASS();
Expand Down Expand Up @@ -2919,7 +3004,7 @@ TEST(daemon_ipc_windows_local_transition_release_retries_retained_mutex) {
if (begun) {
cbm_daemon_ipc_windows_legacy_guard_release_failures_set_for_test(1);
}
bool first_release = cbm_daemon_ipc_local_transition_release(&transition);
int first_release = cbm_daemon_ipc_local_transition_try_release(&transition);
bool retained_after_failure = transition != NULL;
bool retry_release = cbm_daemon_ipc_local_transition_release(&transition);
cbm_daemon_ipc_windows_legacy_guard_release_failures_set_for_test(0);
Expand All @@ -2935,7 +3020,7 @@ TEST(daemon_ipc_windows_local_transition_release_retries_retained_mutex) {
ASSERT_EQ(sealed, 1);
ASSERT_EQ(lifetime, 0);
ASSERT_TRUE(begun);
ASSERT_FALSE(first_release);
ASSERT_EQ(first_release, -1);
ASSERT_TRUE(retained_after_failure);
ASSERT_TRUE(retry_release);
ASSERT_NULL(transition);
Expand Down Expand Up @@ -5792,6 +5877,7 @@ SUITE(daemon_ipc) {
RUN_TEST(daemon_ipc_activation_probe_ignores_matching_startup_claim_only);
RUN_TEST(daemon_ipc_local_transition_coexists_with_active_daemon_lifetime);
RUN_TEST(daemon_ipc_local_participants_overlap_and_allow_modern_startup);
RUN_TEST(daemon_ipc_windows_local_transition_release_reports_live_gate_contention);
RUN_TEST(daemon_ipc_windows_local_transition_release_retries_retained_mutex);
RUN_TEST(daemon_ipc_windows_startup_release_retains_retry_authority);
RUN_TEST(daemon_ipc_frame_timeout_poisons_connection);
Expand Down
Loading
Loading