mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(daemon): keep held runtime files fresh and exit when one is lost
macOS com.apple.tmp_cleaner deletes /tmp files whose atime, mtime and ctime are all older than three days. The daemon's long-held lock files are empty and never written, so under a daemon that lives past three days the cleaner unlinks them while they are held. The next participant then creates and locks a fresh inode: forked index workers see the cohort daemon marker as free, read the live daemon as uncoordinated and exit 1 on every reindex, and a lost IPC lifetime lock or cohort lifetime file stops protecting the generation at all. A lost socket identity marker additionally makes listener close leave the socket pair behind. The host lifetime loop now runs a heartbeat every 60 s over every file the generation holds (cohort daemon claim and lease, participant guard, IPC lifetime reservation, identity marker). Each file is refreshed with futimens through its held descriptor, only after proving the path still names that descriptor, so the cleaner never selects it. If any file is already lost, the daemon logs daemon.lifetime_end reason=coordination_file_lost and stops cleanly; the next client starts a coordinated generation. Re-claiming in place would race that client. Windows needs no refresh (no age-based cleaner; held lock handles deny deletion), so its entry points only validate the handles. Closes #2178 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
This commit is contained in:
1 parent
def38f3ebc
commit
9e793df87b
11 files changed
+314
-7
No files matched your search
+55
-2
@@ -52,6 +52,9 @@ enum {
|
||||
HOST_HTTP_RETRY_INITIAL_MS = 1000,
|
||||
HOST_HTTP_RETRY_MAX_MS = 30000,
|
||||
HOST_WATCH_INTERVAL_MS = 5000,
|
||||
/* Age-based temp cleaners need days of inactivity (macOS tmp_cleaner: 3);
|
||||
* a minute bounds the blind window after a long sleep (#2178). */
|
||||
HOST_COORDINATION_TOUCH_MS = 60000,
|
||||
HOST_CONFLICT_LOG_CAP = 1024 * 1024,
|
||||
HOST_OPERATION_LOG_CAP = 5 * 1024 * 1024,
|
||||
HOST_PATH_CAP = 4096,
|
||||
@@ -868,10 +871,42 @@ static bool host_background_start(host_state_t *host) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Coordination handles the host retains for the generation's whole lifetime. */
|
||||
typedef struct {
|
||||
const cbm_daemon_ipc_endpoint_t *endpoint;
|
||||
cbm_version_cohort_lease_t *cohort_lease;
|
||||
cbm_version_cohort_daemon_claim_t *daemon_claim;
|
||||
cbm_daemon_ipc_participant_guard_t *participant_guard;
|
||||
} host_coordination_t;
|
||||
|
||||
/* Refresh every long-held runtime file and name the first one found lost, or
|
||||
* return NULL. A held lock whose path now names another inode coordinates
|
||||
* nothing: forked index workers read the daemon as uncoordinated and a peer
|
||||
* can start a second generation (#2178). */
|
||||
static const char *host_coordination_touch(const host_coordination_t *coordination,
|
||||
cbm_daemon_runtime_service_t *service) {
|
||||
if (!cbm_version_cohort_daemon_claim_touch(coordination->daemon_claim)) {
|
||||
return "cohort_daemon_claim";
|
||||
}
|
||||
if (!cbm_version_cohort_lease_touch(coordination->cohort_lease)) {
|
||||
return "cohort_lease";
|
||||
}
|
||||
if (!cbm_daemon_ipc_participant_guard_touch(coordination->endpoint,
|
||||
coordination->participant_guard)) {
|
||||
return "participant_guard";
|
||||
}
|
||||
if (!cbm_daemon_runtime_service_touch_listener(service)) {
|
||||
return "listener";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool host_wait_for_lifetime(cbm_daemon_runtime_service_t *service,
|
||||
atomic_int *stop_requested, host_state_t *host, bool permanent) {
|
||||
atomic_int *stop_requested, host_state_t *host, bool permanent,
|
||||
const host_coordination_t *coordination) {
|
||||
uint64_t initial_deadline = cbm_now_ms() + HOST_INITIAL_CLIENT_TIMEOUT_MS;
|
||||
uint64_t stopping_deadline = 0;
|
||||
uint64_t next_touch = 0;
|
||||
for (;;) {
|
||||
cbm_daemon_runtime_service_state_t state = cbm_daemon_runtime_service_state(service);
|
||||
if (state == CBM_DAEMON_RUNTIME_SERVICE_EXITED) {
|
||||
@@ -903,6 +938,17 @@ static bool host_wait_for_lifetime(cbm_daemon_runtime_service_t *service,
|
||||
stop ? "stop_requested" : "initial_window_expired");
|
||||
return cbm_daemon_runtime_service_stop(service, HOST_RUNTIME_SHUTDOWN_MS);
|
||||
}
|
||||
/* Permanent generations too: exiting lets the next client start a
|
||||
* coordinated generation, while re-claiming would race that client. */
|
||||
if (cbm_now_ms() >= next_touch) {
|
||||
next_touch = cbm_now_ms() + HOST_COORDINATION_TOUCH_MS;
|
||||
const char *lost = host_coordination_touch(coordination, service);
|
||||
if (lost) {
|
||||
cbm_log_warn("daemon.lifetime_end", "reason", "coordination_file_lost", "file",
|
||||
lost);
|
||||
return cbm_daemon_runtime_service_stop(service, HOST_RUNTIME_SHUTDOWN_MS);
|
||||
}
|
||||
}
|
||||
host_http_reconcile_at(host, cbm_now_ms(), false);
|
||||
/* Retire an ephemeral generation that lingered for cold-storm cohort
|
||||
* participants once they drain, or once its bounded linger elapses.
|
||||
@@ -1123,7 +1169,14 @@ int cbm_daemon_host_run(const cbm_daemon_host_config_t *config) {
|
||||
: "unavailable",
|
||||
"memory_budget_bytes", memory_budget, "physical_job_limit", physical_job_limit,
|
||||
"worker_memory_budget_bytes", worker_memory_budget);
|
||||
if (!host_wait_for_lifetime(service, config->stop_requested, &host, config->permanent)) {
|
||||
host_coordination_t coordination = {
|
||||
.endpoint = config->endpoint,
|
||||
.cohort_lease = cohort_lease,
|
||||
.daemon_claim = daemon_claim,
|
||||
.participant_guard = participant_guard,
|
||||
};
|
||||
if (!host_wait_for_lifetime(service, config->stop_requested, &host, config->permanent,
|
||||
&coordination)) {
|
||||
host_force_terminate("runtime");
|
||||
}
|
||||
|
||||
|
||||
@@ -3220,6 +3220,46 @@ void cbm_daemon_ipc_listener_close(cbm_daemon_ipc_listener_t *listener) {
|
||||
free(listener);
|
||||
}
|
||||
|
||||
/* Touch through the held descriptor only after proving the path still names
|
||||
* it: refreshing by path would keep a replacement inode fresh and hide the
|
||||
* loss (#2178). */
|
||||
static bool posix_held_file_touch(int directory_fd, const char *base_name, int fd) {
|
||||
return private_regular_file_snapshot(directory_fd, base_name, fd, 1, NULL) &&
|
||||
futimens(fd, NULL) == 0;
|
||||
}
|
||||
|
||||
static bool posix_held_lock_touch(int directory_fd, const process_lock_entry_t *entry, int fd) {
|
||||
return entry && posix_held_file_touch(directory_fd, entry->lock_name, fd);
|
||||
}
|
||||
|
||||
bool cbm_daemon_ipc_listener_touch(cbm_daemon_ipc_listener_t *listener) {
|
||||
if (!listener || listener->dir_fd < 0 || listener->owner_pid != getpid() ||
|
||||
!listener->lifetime_reservation) {
|
||||
return false;
|
||||
}
|
||||
bool lifetime =
|
||||
posix_held_lock_touch(listener->dir_fd, listener->lifetime_reservation->process_entry,
|
||||
listener->lifetime_reservation->fd);
|
||||
const cbm_daemon_ipc_participant_guard_t *guard = listener->participant_guard;
|
||||
bool participant =
|
||||
!guard ||
|
||||
posix_held_lock_touch(listener->dir_fd, guard->legacy_process_entry, guard->legacy_fd);
|
||||
/* The marker is not held open; reopen it and require the published inode. */
|
||||
int marker_fd = openat(listener->dir_fd, listener->socket_identity_name,
|
||||
O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
|
||||
struct stat marker_status;
|
||||
bool marker = marker_fd >= 0 &&
|
||||
private_regular_file_snapshot(listener->dir_fd, listener->socket_identity_name,
|
||||
marker_fd, 1, &marker_status) &&
|
||||
marker_status.st_dev == listener->identity_device &&
|
||||
marker_status.st_ino == listener->identity_inode &&
|
||||
futimens(marker_fd, NULL) == 0;
|
||||
if (marker_fd >= 0) {
|
||||
(void)close(marker_fd);
|
||||
}
|
||||
return lifetime && participant && marker;
|
||||
}
|
||||
|
||||
int cbm_daemon_ipc_accept(cbm_daemon_ipc_listener_t *listener, uint32_t timeout_ms,
|
||||
cbm_daemon_ipc_connection_t **connection_out) {
|
||||
if (connection_out) {
|
||||
@@ -3603,6 +3643,12 @@ bool cbm_daemon_ipc_participant_guard_release(cbm_daemon_ipc_participant_guard_t
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cbm_daemon_ipc_participant_guard_touch(const cbm_daemon_ipc_endpoint_t *endpoint,
|
||||
cbm_daemon_ipc_participant_guard_t *guard) {
|
||||
return endpoint && guard && guard->owner_pid == getpid() &&
|
||||
posix_held_lock_touch(endpoint->dir_fd, guard->legacy_process_entry, guard->legacy_fd);
|
||||
}
|
||||
|
||||
int cbm_daemon_ipc_local_transition_try_acquire(
|
||||
const cbm_daemon_ipc_endpoint_t *endpoint, cbm_daemon_ipc_local_transition_t **transition_out) {
|
||||
if (transition_out) {
|
||||
@@ -6746,6 +6792,17 @@ bool cbm_daemon_ipc_participant_guard_release(cbm_daemon_ipc_participant_guard_t
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Windows has no age-based cleaner of the private runtime directory, and its
|
||||
* held handles deny deletion; the #2178 heartbeat has nothing to refresh. */
|
||||
bool cbm_daemon_ipc_listener_touch(cbm_daemon_ipc_listener_t *listener) {
|
||||
return listener != NULL;
|
||||
}
|
||||
|
||||
bool cbm_daemon_ipc_participant_guard_touch(const cbm_daemon_ipc_endpoint_t *endpoint,
|
||||
cbm_daemon_ipc_participant_guard_t *guard) {
|
||||
return endpoint && guard;
|
||||
}
|
||||
|
||||
int cbm_daemon_ipc_local_transition_try_acquire(
|
||||
const cbm_daemon_ipc_endpoint_t *endpoint, cbm_daemon_ipc_local_transition_t **transition_out) {
|
||||
if (transition_out) {
|
||||
|
||||
@@ -76,6 +76,15 @@ cbm_daemon_ipc_listener_t *cbm_daemon_ipc_listen_reserved(
|
||||
cbm_daemon_ipc_lifetime_reservation_t **reservation_io);
|
||||
void cbm_daemon_ipc_listener_close(cbm_daemon_ipc_listener_t *listener);
|
||||
|
||||
/* Heartbeat for the daemon's long-lived runtime artifacts: refresh the held
|
||||
* lifetime reservation, the listener's participant guard (if it owns one) and
|
||||
* the published identity marker so an age-based temp cleaner never deletes
|
||||
* them (#2178). Returns false once any of them no longer names the file this
|
||||
* listener owns: a lost lifetime file lets a peer start a second generation,
|
||||
* and a lost identity marker makes listener close leave the socket pair
|
||||
* behind. Only the owning thread may call it while the listener is open. */
|
||||
bool cbm_daemon_ipc_listener_touch(cbm_daemon_ipc_listener_t *listener);
|
||||
|
||||
/* Create or validate one private current-user directory at an already
|
||||
* canonical local path. Ancestors are handle-validated without mutation:
|
||||
* POSIX permits only root/current-user owners and no group/other write (apart
|
||||
@@ -270,6 +279,10 @@ bool cbm_daemon_ipc_startup_lock_release(cbm_daemon_ipc_startup_lock_t **lock_io
|
||||
int cbm_daemon_ipc_participant_guard_try_join(const cbm_daemon_ipc_endpoint_t *endpoint,
|
||||
cbm_daemon_ipc_participant_guard_t **guard_out);
|
||||
bool cbm_daemon_ipc_participant_guard_release(cbm_daemon_ipc_participant_guard_t **guard_io);
|
||||
/* Same heartbeat contract as cbm_daemon_ipc_listener_touch, for a guard the
|
||||
* caller retains itself. */
|
||||
bool cbm_daemon_ipc_participant_guard_touch(const cbm_daemon_ipc_endpoint_t *endpoint,
|
||||
cbm_daemon_ipc_participant_guard_t *guard);
|
||||
|
||||
/* Standalone CLI work joins the legacy-compatible current participant group
|
||||
* without becoming a daemon client. Acquisition retains startup-v2 only for
|
||||
|
||||
@@ -2578,6 +2578,10 @@ size_t cbm_daemon_runtime_service_active_connections(cbm_daemon_runtime_service_
|
||||
return count;
|
||||
}
|
||||
|
||||
bool cbm_daemon_runtime_service_touch_listener(cbm_daemon_runtime_service_t *service) {
|
||||
return service && service->listener && cbm_daemon_ipc_listener_touch(service->listener);
|
||||
}
|
||||
|
||||
void cbm_daemon_runtime_service_reconcile_lifetime(cbm_daemon_runtime_service_t *service) {
|
||||
if (!service) {
|
||||
return;
|
||||
|
||||
@@ -336,6 +336,10 @@ size_t cbm_daemon_runtime_service_active_connections(cbm_daemon_runtime_service_
|
||||
* an unbounded idle hang. The host lifetime loop calls this every tick; it is a
|
||||
* no-op unless a last-committed-client linger is armed. */
|
||||
void cbm_daemon_runtime_service_reconcile_lifetime(cbm_daemon_runtime_service_t *service);
|
||||
/* #2178 heartbeat over the listener's runtime artifacts; see
|
||||
* cbm_daemon_ipc_listener_touch. Call only from the thread that stops and
|
||||
* frees the service, which is the only thread that closes the listener. */
|
||||
bool cbm_daemon_runtime_service_touch_listener(cbm_daemon_runtime_service_t *service);
|
||||
size_t cbm_daemon_runtime_service_job_subscribers(cbm_daemon_runtime_service_t *service,
|
||||
const char *project_key);
|
||||
uint64_t cbm_daemon_runtime_service_client_process_id(cbm_daemon_runtime_service_t *service,
|
||||
|
||||
@@ -363,6 +363,25 @@ cbm_private_file_lock_status_t cbm_version_cohort_lease_release(
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool version_cohort_lock_touch(cbm_private_file_lock_t *lock) {
|
||||
return !lock || cbm_private_file_lock_touch(lock) == CBM_PRIVATE_FILE_LOCK_OK;
|
||||
}
|
||||
|
||||
bool cbm_version_cohort_lease_touch(cbm_version_cohort_lease_t *lease) {
|
||||
if (!lease) {
|
||||
return false;
|
||||
}
|
||||
/* Evaluate all three so one lost file does not leave the others to age. */
|
||||
bool lifetime = version_cohort_lock_touch(lease->lifetime);
|
||||
bool admission = version_cohort_lock_touch(lease->admission);
|
||||
bool maintenance = version_cohort_lock_touch(lease->maintenance);
|
||||
return lifetime && admission && maintenance;
|
||||
}
|
||||
|
||||
bool cbm_version_cohort_daemon_claim_touch(cbm_version_cohort_daemon_claim_t *claim) {
|
||||
return claim && claim->marker && version_cohort_lock_touch(claim->marker);
|
||||
}
|
||||
|
||||
static cbm_version_cohort_status_t version_cohort_failed(cbm_version_cohort_lease_t *lease,
|
||||
cbm_version_cohort_status_t status,
|
||||
cbm_version_cohort_lease_t **lease_out) {
|
||||
|
||||
@@ -145,6 +145,15 @@ cbm_version_cohort_daemon_presence_t cbm_version_cohort_daemon_presence_under_tr
|
||||
cbm_private_file_lock_status_t cbm_version_cohort_lease_release(
|
||||
cbm_version_cohort_lease_t **lease_io);
|
||||
|
||||
/* Heartbeat for a long-lived holder: refresh every lock file the lease or
|
||||
* claim holds and report false once any held file was unlinked or replaced.
|
||||
* An age-based temp cleaner deleting a held file silently ends coordination,
|
||||
* because the next participant creates and locks a fresh inode (#2178). The
|
||||
* only safe response to false is an orderly exit; re-claiming would race
|
||||
* that participant. */
|
||||
bool cbm_version_cohort_lease_touch(cbm_version_cohort_lease_t *lease);
|
||||
bool cbm_version_cohort_daemon_claim_touch(cbm_version_cohort_daemon_claim_t *claim);
|
||||
|
||||
/* Refuses teardown while a lease or retryable cleanup handle remains. */
|
||||
cbm_private_file_lock_status_t cbm_version_cohort_manager_free(
|
||||
cbm_version_cohort_manager_t **manager_io);
|
||||
|
||||
@@ -482,6 +482,24 @@ cbm_private_file_lock_status_t cbm_private_file_lock_payload_write(cbm_private_f
|
||||
return valid ? CBM_PRIVATE_FILE_LOCK_OK : CBM_PRIVATE_FILE_LOCK_IO;
|
||||
}
|
||||
|
||||
cbm_private_file_lock_status_t cbm_private_file_lock_touch(cbm_private_file_lock_t *lock) {
|
||||
if (!lock) {
|
||||
return CBM_PRIVATE_FILE_LOCK_UNSAFE;
|
||||
}
|
||||
if (!cbm_private_file_lock_fork_guard_enter()) {
|
||||
return CBM_PRIVATE_FILE_LOCK_IO;
|
||||
}
|
||||
/* An unlinked file keeps its inode alive for this handle with st_nlink 0,
|
||||
* which the payload validity check already rejects. */
|
||||
bool linked = private_payload_fd_valid(lock, NULL);
|
||||
bool touched = linked && futimens(lock->fd, NULL) == 0;
|
||||
cbm_private_file_lock_fork_guard_leave();
|
||||
if (!linked) {
|
||||
return CBM_PRIVATE_FILE_LOCK_UNSAFE;
|
||||
}
|
||||
return touched ? CBM_PRIVATE_FILE_LOCK_OK : CBM_PRIVATE_FILE_LOCK_IO;
|
||||
}
|
||||
|
||||
cbm_private_file_lock_status_t cbm_private_file_lock_release(cbm_private_file_lock_t **lock_io) {
|
||||
if (!lock_io || !*lock_io) {
|
||||
return CBM_PRIVATE_FILE_LOCK_IO;
|
||||
@@ -1450,6 +1468,20 @@ cbm_private_file_lock_status_t cbm_private_file_lock_payload_write(cbm_private_f
|
||||
return valid ? CBM_PRIVATE_FILE_LOCK_OK : CBM_PRIVATE_FILE_LOCK_IO;
|
||||
}
|
||||
|
||||
cbm_private_file_lock_status_t cbm_private_file_lock_touch(cbm_private_file_lock_t *lock) {
|
||||
/* Windows has no age-based cleaner of the private lock directory; only
|
||||
* confirm the held handle still names a linked file. */
|
||||
if (!lock) {
|
||||
return CBM_PRIVATE_FILE_LOCK_UNSAFE;
|
||||
}
|
||||
if (!cbm_private_file_lock_fork_guard_enter()) {
|
||||
return CBM_PRIVATE_FILE_LOCK_IO;
|
||||
}
|
||||
bool linked = private_win_payload_handle_valid(lock);
|
||||
cbm_private_file_lock_fork_guard_leave();
|
||||
return linked ? CBM_PRIVATE_FILE_LOCK_OK : CBM_PRIVATE_FILE_LOCK_UNSAFE;
|
||||
}
|
||||
|
||||
static bool private_win_release_unlock(cbm_private_file_lock_t *lock) {
|
||||
lock->test_unlock_attempts++;
|
||||
if (lock->test_fail_unlock_once) {
|
||||
|
||||
@@ -61,6 +61,13 @@ cbm_private_file_lock_status_t cbm_private_file_lock_payload_write(cbm_private_f
|
||||
const void *buffer,
|
||||
size_t length);
|
||||
|
||||
/* Long-lived holders call this periodically. It refreshes the held file's
|
||||
* timestamps through the validated handle so age-based temp cleaners (macOS
|
||||
* tmp_cleaner, systemd-tmpfiles) never select it, and returns UNSAFE once the
|
||||
* handle no longer names a linked file: a peer opening the path would then
|
||||
* lock a different inode and the lock coordinates nothing (#2178). */
|
||||
cbm_private_file_lock_status_t cbm_private_file_lock_touch(cbm_private_file_lock_t *lock);
|
||||
|
||||
/* Forces the next successfully acquired native lock down the post-lock
|
||||
* validation cleanup path and injects pre-call release failures there. */
|
||||
bool cbm_private_lock_directory_fail_post_acquire_cleanup_for_test(
|
||||
|
||||
+52
-4
@@ -1004,8 +1004,8 @@ TEST(daemon_ipc_windows_sid_trust_accepts_local_admin_rejects_foreign_500) {
|
||||
(void)CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, NULL, &builtin_needed);
|
||||
if (builtin_needed > 0) {
|
||||
builtin_admins = malloc(builtin_needed);
|
||||
if (builtin_admins &&
|
||||
!CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, builtin_admins, &builtin_needed)) {
|
||||
if (builtin_admins && !CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, builtin_admins,
|
||||
&builtin_needed)) {
|
||||
free(builtin_admins);
|
||||
builtin_admins = NULL;
|
||||
}
|
||||
@@ -2063,6 +2063,54 @@ TEST(daemon_ipc_lifetime_reservation_survives_saturated_second_listen) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2178: an age-based temp cleaner unlinked the live daemon's lifetime lock
|
||||
* and identity marker. The heartbeat must report either loss, so the daemon
|
||||
* exits instead of serving under a lifetime file that coordinates nothing or
|
||||
* leaving an unremovable socket pair behind at close. */
|
||||
TEST(daemon_ipc_listener_touch_detects_lost_runtime_files) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("unlink-while-held of runtime files is POSIX behavior");
|
||||
#else
|
||||
static const char key[] = "2178a2178a2178a0";
|
||||
static const char *const lost_files[] = {"lifetime.lock", "sock.identity"};
|
||||
char parent[TEST_PATH_CAP] = {0};
|
||||
char runtime_dir[TEST_PATH_CAP] = {0};
|
||||
cbm_daemon_ipc_endpoint_t *endpoint = NULL;
|
||||
bool started[2] = {false, false};
|
||||
bool touched_intact[2] = {false, false};
|
||||
bool unlinked[2] = {false, false};
|
||||
bool touched_after_loss[2] = {true, true};
|
||||
|
||||
if (ipc_test_parent_new(parent, "listener-touch")) {
|
||||
endpoint = cbm_daemon_ipc_endpoint_new(key, parent);
|
||||
}
|
||||
if (endpoint) {
|
||||
ipc_test_copy_path(runtime_dir, cbm_daemon_ipc_endpoint_runtime_dir(endpoint));
|
||||
}
|
||||
for (size_t i = 0; endpoint && i < 2; i++) {
|
||||
cbm_daemon_ipc_listener_t *listener = cbm_daemon_ipc_listen(endpoint);
|
||||
started[i] = listener != NULL;
|
||||
touched_intact[i] = started[i] && cbm_daemon_ipc_listener_touch(listener);
|
||||
char path[TEST_PATH_CAP];
|
||||
int written = snprintf(path, sizeof(path), "%s/cbm-%s.%s", runtime_dir, key, lost_files[i]);
|
||||
unlinked[i] =
|
||||
touched_intact[i] && written > 0 && written < (int)sizeof(path) && unlink(path) == 0;
|
||||
touched_after_loss[i] = unlinked[i] && cbm_daemon_ipc_listener_touch(listener);
|
||||
cbm_daemon_ipc_listener_close(listener);
|
||||
}
|
||||
cbm_daemon_ipc_endpoint_free(endpoint);
|
||||
ipc_test_remove_tree(runtime_dir, parent);
|
||||
|
||||
for (size_t i = 0; i < 2; i++) {
|
||||
ASSERT_TRUE(started[i]);
|
||||
ASSERT_TRUE(touched_intact[i]);
|
||||
ASSERT_TRUE(unlinked[i]);
|
||||
ASSERT_FALSE(touched_after_loss[i]);
|
||||
}
|
||||
PASS();
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(daemon_ipc_lifetime_reservation_transfers_without_unlock_window) {
|
||||
static const char key[] = "1029384756abcdef";
|
||||
char parent[TEST_PATH_CAP] = {0};
|
||||
@@ -5260,8 +5308,7 @@ TEST(daemon_ipc_posix_single_uid_userns_real_smoke_issue1830) {
|
||||
if (WEXITSTATUS(status) != 0 && WEXITSTATUS(status) != 1) {
|
||||
char unexpected[128];
|
||||
(void)snprintf(unexpected, sizeof(unexpected),
|
||||
"userns probe exited %d -- not a security verdict",
|
||||
WEXITSTATUS(status));
|
||||
"userns probe exited %d -- not a security verdict", WEXITSTATUS(status));
|
||||
FAIL(unexpected);
|
||||
}
|
||||
ASSERT_EQ(0, WEXITSTATUS(status));
|
||||
@@ -5376,6 +5423,7 @@ SUITE(daemon_ipc) {
|
||||
RUN_TEST(daemon_ipc_rejects_uppercase_instance_key);
|
||||
RUN_TEST(daemon_ipc_no_spawn_probe_distinguishes_absent_active_and_busy);
|
||||
RUN_TEST(daemon_ipc_lifetime_reservation_survives_saturated_second_listen);
|
||||
RUN_TEST(daemon_ipc_listener_touch_detects_lost_runtime_files);
|
||||
RUN_TEST(daemon_ipc_lifetime_reservation_transfers_without_unlock_window);
|
||||
RUN_TEST(daemon_ipc_local_frame_roundtrip);
|
||||
RUN_TEST(daemon_ipc_bounded_receive_rejects_oversize_before_payload);
|
||||
|
||||
@@ -166,7 +166,8 @@ static void private_lock_fixture_finish(private_lock_fixture_t *fixture) {
|
||||
"lock-attempt-cleanup.lock",
|
||||
"acl-directory.lock",
|
||||
"acl-file.lock",
|
||||
"fork.lock"};
|
||||
"fork.lock",
|
||||
"touch.lock"};
|
||||
if (fixture->root[0]) {
|
||||
for (size_t index = 0; index < sizeof(files) / sizeof(files[0]); index++) {
|
||||
if (!private_lock_path(path, fixture, files[index])) {
|
||||
@@ -480,6 +481,65 @@ TEST(private_file_lock_consumed_close_error_never_retries_recycled_fd) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/* #2178: macOS tmp_cleaner unlinks /tmp files whose atime, mtime and ctime are
|
||||
* all older than three days, including empty lock files a daemon holds for its
|
||||
* whole life. The next participant then creates and locks a fresh inode, so
|
||||
* the old lock coordinates nothing. Touch must keep a held file young and
|
||||
* report the loss once the path stops naming it. */
|
||||
TEST(private_file_lock_touch_refreshes_held_file_and_detects_unlink) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("age-based temp cleaning and unlink-while-held are POSIX behaviors");
|
||||
#else
|
||||
private_lock_fixture_t fixture;
|
||||
bool started = private_lock_fixture_start(&fixture);
|
||||
char path[PRIVATE_LOCK_TEST_PATH_CAP];
|
||||
bool path_ok = started && private_lock_path(path, &fixture, "touch.lock");
|
||||
cbm_private_file_lock_t *held = NULL;
|
||||
cbm_private_file_lock_t *successor = NULL;
|
||||
cbm_private_file_lock_status_t acquired =
|
||||
path_ok ? cbm_private_file_lock_try_acquire(fixture.directory, "touch.lock",
|
||||
CBM_PRIVATE_FILE_LOCK_EX, &held)
|
||||
: CBM_PRIVATE_FILE_LOCK_IO;
|
||||
const time_t stale = 1000000000;
|
||||
struct timespec stale_times[2] = {{.tv_sec = stale}, {.tv_sec = stale}};
|
||||
bool aged = acquired == CBM_PRIVATE_FILE_LOCK_OK &&
|
||||
futimens(cbm_private_file_lock_native_fd_for_test(held), stale_times) == 0;
|
||||
cbm_private_file_lock_status_t linked_touch =
|
||||
aged ? cbm_private_file_lock_touch(held) : CBM_PRIVATE_FILE_LOCK_IO;
|
||||
struct stat refreshed = {0};
|
||||
bool refreshed_ok = linked_touch == CBM_PRIVATE_FILE_LOCK_OK && stat(path, &refreshed) == 0;
|
||||
|
||||
bool unlinked = refreshed_ok && unlink(path) == 0;
|
||||
cbm_private_file_lock_status_t unlinked_touch =
|
||||
unlinked ? cbm_private_file_lock_touch(held) : CBM_PRIVATE_FILE_LOCK_IO;
|
||||
/* The hazard itself: with the path gone, a peer's EX succeeds while the
|
||||
* original holder still believes it is exclusive. */
|
||||
cbm_private_file_lock_status_t successor_status =
|
||||
unlinked ? cbm_private_file_lock_try_acquire(fixture.directory, "touch.lock",
|
||||
CBM_PRIVATE_FILE_LOCK_EX, &successor)
|
||||
: CBM_PRIVATE_FILE_LOCK_IO;
|
||||
if (successor) {
|
||||
(void)cbm_private_file_lock_release(&successor);
|
||||
}
|
||||
if (held) {
|
||||
(void)cbm_private_file_lock_release(&held);
|
||||
}
|
||||
private_lock_fixture_finish(&fixture);
|
||||
|
||||
ASSERT_TRUE(started);
|
||||
ASSERT_EQ(acquired, CBM_PRIVATE_FILE_LOCK_OK);
|
||||
ASSERT_TRUE(aged);
|
||||
ASSERT_EQ(linked_touch, CBM_PRIVATE_FILE_LOCK_OK);
|
||||
ASSERT_TRUE(refreshed_ok);
|
||||
ASSERT_TRUE(refreshed.st_atime > stale);
|
||||
ASSERT_TRUE(refreshed.st_mtime > stale);
|
||||
ASSERT_TRUE(unlinked);
|
||||
ASSERT_EQ(unlinked_touch, CBM_PRIVATE_FILE_LOCK_UNSAFE);
|
||||
ASSERT_EQ(successor_status, CBM_PRIVATE_FILE_LOCK_OK);
|
||||
PASS();
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(private_file_lock_post_acquire_failure_returns_cleanup_owner) {
|
||||
private_lock_fixture_t fixture;
|
||||
bool started = private_lock_fixture_start(&fixture);
|
||||
@@ -863,6 +923,7 @@ SUITE(private_file_lock) {
|
||||
RUN_TEST(private_file_lock_unlock_failure_retains_retryable_lock);
|
||||
RUN_TEST(private_file_lock_close_failure_retries_without_duplicate_unlock);
|
||||
RUN_TEST(private_file_lock_consumed_close_error_never_retries_recycled_fd);
|
||||
RUN_TEST(private_file_lock_touch_refreshes_held_file_and_detects_unlink);
|
||||
RUN_TEST(private_file_lock_post_acquire_failure_returns_cleanup_owner);
|
||||
RUN_TEST(private_file_lock_windows_lock_attempt_failure_returns_cleanup_owner);
|
||||
RUN_TEST(private_file_lock_rejects_unsafe_entries_and_replaced_root);
|
||||
|
||||
Reference in new issue
Block a user