8eabe191d2
Five field reports in the 24 hours after v0.10.0 all pointed at the same thing: gates that were right in principle refused real, ordinary setups, and then failed to say why. Per the consolidated strictness decision, each gate keeps the protection that matters and drops the part that was refusing legitimate users — and every refusal now names what it refused and how to proceed. **Daemon image gate: npx and every ephemeral install path (#1539, #1383).** The admission check treated "the peer's image hashes differently" and "the peer's image cannot be examined at all" as one failure. The second is what `npx codebase-memory-mcp` always produces (ephemeral cache path, unfingerprintable), so every npx-invoked client was rejected — and, because the client never reported it, agents saw a transport that closed mid-handshake with zero bytes on stdout. Reported by @wassolles with the admission path already read and the fix space mapped. An unverifiable image is now admitted: the rendezvous HELLO immediately above it has already proven semantic version, build fingerprint, and protocol/store/ feature ABI, and the image check was trading that real proof for an unavailable one. It logs daemon.client_image_unverifiable_admitted so the weaker check is never invisible. A fingerprint MISMATCH — the tamper case the gate exists for — still rejects hard. Separate test seams keep the two modes testable apart. **Client bootstrap failures are no longer silent (#1539).** An MCP client that cannot reach the daemon now emits a JSON-RPC error on stdout naming the reason, plus the same text on stderr. Previously the reason sat in bootstrap_result.message and the process exited having written nothing at all. **POSIX activation: group-writable ancestors (#1535, discussion #1526).** activation_directory_secure required no group or other write bit on the install directory AND every ancestor. WSL2 ships ~ and ~/.local at 0775, as do several distro skeletons and any site using a shared primary group, so install.sh failed for a large fraction of Linux users — reporting a policy refusal as "activation transaction I/O failed", which sent reporters after disk errors and filesystem types. Root-caused by @AmirF194 in a clean ubuntu container; @shochdoerfer and @iandol confirmed independently. World-writable ancestors are still refused (any local user could swap a path component mid-transaction). Group-writable ancestors are now warned about and admitted. The LEAF directory stays strictly owner-private — that is where the binary is published, and group write there would let another account replace the executable between validation and exec. Refusals now name the directory, its mode, and which rule refused. **The obsolete ui/standard chooser (#1538, from discussion #1526).** v0.10.0 consolidated to one archive per platform with the UI always embedded, but `update` still offered a variant choice: "ui" could only 404, and "standard" quietly WAS the UI build. Reported by @iandol upgrading 0.9.0 -> 0.10.0. The chooser, its --standard/--ui flags, and the ui- URL plumbing are removed, along with the CBM_VARIANT=ui remnant in the npm installer. Already-released 0.9.x binaries cannot be fixed retroactively, so the release workflow now publishes byte-identical ui-*-named alias assets — their updaters work again with no user action. The aliases are uploaded AFTER the VirusTotal gate: they are the same bytes as archives it already cleared, and uploading them earlier would duplicate every object in the scan set and the provenance manifest. **macOS install noise and attribution (#1537).** install.sh silenced the "No such xattr: com.apple.quarantine" line, which is what happens when a curl-downloaded archive carries no quarantine attribute — harmless, and it became the title of a bug report about an unrelated failure. The session-stop refusal now points at `daemon status` to list the client processes actually holding the daemon, instead of asserting sessions exist and leaving the reader to guess. Reported by @listepo. **Riders.** hatchling is pinned in pkg/pypi (an unpinned backend resolved fresh inside `python -m build` is what emitted Metadata-Version 2.5 and broke the v0.10.1 publish); SECURITY.md's supported-versions table moves to 0.10.x. Tests: separate seams for unverifiable vs mismatched peer images with a test per outcome; activation refusal must name directory + mode + rule; a group-writable ancestor must stage successfully. The update tests drop the flag that no longer exists. Verified against each reporter's environment shape. **Open security alerts (all three, OSSF Scorecard).** - HIGH, binary artifact: an 8.8 MB compiled Go ELF wrapper had been committed at pkg/go/codebase-memory-mcp by accident. Removed, and both it and its .exe sibling are gitignored so `go build` in that directory cannot repeat it. - HIGH, GHSA-2v37-7h3g-55p8: nanoid < 3.3.17 loops forever when a custom generator is called with size 0. It reaches us transitively (postcss -> vite), so it is pinned through the existing graph-ui overrides block rather than promoted to a direct dependency; the lockfile resolves 3.3.18. - MEDIUM, unpinned pip command: the publish step installed build/twine by version only, leaving the whole transitive graph resolved at run time. pkg/pypi/requirements-publish.txt now hash-pins the complete toolchain (316 hashes), generated on a linux/amd64 python:3.12 image so the wheels match what ubuntu-latest resolves, and the step runs pip with --require-hashes. Verified by installing from it in that same image. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
4788 lines
212 KiB
C
4788 lines
212 KiB
C
/*
|
|
* test_daemon_runtime.c — RED contract for the mandatory daemon runtime.
|
|
*
|
|
* These tests exercise a real in-process service over the authenticated IPC
|
|
* transport. A focused isolated host child also pins listener-before-background
|
|
* ordering; frontend bootstrap remains covered at its own layer.
|
|
*/
|
|
#include "test_framework.h"
|
|
#include "test_helpers.h"
|
|
#include "test_daemon_runtime_contract.h"
|
|
|
|
#include "daemon/application.h"
|
|
#include "daemon/host.h"
|
|
#include "daemon/host_internal.h"
|
|
#include "daemon/ipc.h"
|
|
#include "daemon/runtime.h"
|
|
#include "daemon/service.h"
|
|
#include "daemon/version_cohort.h"
|
|
#include "foundation/compat.h"
|
|
#include "foundation/compat_fs.h"
|
|
#include "foundation/compat_thread.h"
|
|
#include "foundation/log.h"
|
|
#include "foundation/platform.h"
|
|
#include "pipeline/pipeline.h"
|
|
#include "store/store.h"
|
|
|
|
#include <limits.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdatomic.h>
|
|
#include <string.h>
|
|
|
|
#ifdef _WIN32
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include "foundation/win_utf8.h"
|
|
#include <stddef.h>
|
|
#include <windows.h>
|
|
#else
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#ifdef __APPLE__
|
|
#include <libproc.h>
|
|
#endif
|
|
#include <signal.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <spawn.h>
|
|
#include <unistd.h>
|
|
|
|
extern char **environ;
|
|
#endif
|
|
|
|
enum {
|
|
RUNTIME_TEST_PATH_CAP = 1024,
|
|
RUNTIME_TEST_LOG_CAP = 4096,
|
|
RUNTIME_TEST_TIMEOUT_MS = 2000,
|
|
RUNTIME_TEST_CLEANUP_TIMEOUT_MS = 60000,
|
|
RUNTIME_TEST_CLEANUP_FREE_ATTEMPTS = 3,
|
|
/* Generation-zero rendezvous layout, deliberately repeated rather than
|
|
* derived from production macros so an accidental resize fails loudly. */
|
|
RUNTIME_TEST_RENDEZVOUS_ABI = 1,
|
|
RUNTIME_TEST_RENDEZVOUS_REQUEST_SIZE = 133,
|
|
RUNTIME_TEST_RENDEZVOUS_RESPONSE_SIZE = 798,
|
|
RUNTIME_TEST_RENDEZVOUS_VERSION_OFFSET = 4,
|
|
RUNTIME_TEST_RENDEZVOUS_BUILD_OFFSET = 68,
|
|
RUNTIME_TEST_RENDEZVOUS_ACTIVE_VERSION_OFFSET = 28,
|
|
RUNTIME_TEST_RENDEZVOUS_ACTIVE_BUILD_OFFSET = 92,
|
|
RUNTIME_TEST_RENDEZVOUS_REQUESTED_VERSION_OFFSET = 157,
|
|
RUNTIME_TEST_RENDEZVOUS_REQUESTED_BUILD_OFFSET = 221,
|
|
RUNTIME_TEST_RENDEZVOUS_MESSAGE_OFFSET = 286,
|
|
/* Activation shutdown is a separate frozen first-frame envelope:
|
|
* u32 action followed byte-for-byte by the 133-byte identity above.
|
|
* Response: u32 ABI, u32 accepted, u64 clients, u64 connections. */
|
|
RUNTIME_TEST_ACTIVATION_REQUEST_SIZE = 137,
|
|
RUNTIME_TEST_ACTIVATION_RESPONSE_SIZE = 24,
|
|
RUNTIME_TEST_ACTIVATION_IDENTITY_OFFSET = 4,
|
|
RUNTIME_TEST_ACTIVATION_RESPONSE_CLIENTS_OFFSET = 8,
|
|
RUNTIME_TEST_ACTIVATION_RESPONSE_CONNECTIONS_OFFSET = 16,
|
|
};
|
|
|
|
#define RUNTIME_TEST_BLOCKING_GIT_MARKER_ENV "CBM_TEST_RUNTIME_BLOCKING_GIT_PID_FILE"
|
|
|
|
_Static_assert(CBM_DAEMON_RUNTIME_OP_ACTIVATION_SHUTDOWN == 8 &&
|
|
CBM_DAEMON_ACTIVATION_SHUTDOWN_REQUEST_SIZE ==
|
|
RUNTIME_TEST_ACTIVATION_REQUEST_SIZE &&
|
|
CBM_DAEMON_ACTIVATION_SHUTDOWN_RESPONSE_SIZE ==
|
|
RUNTIME_TEST_ACTIVATION_RESPONSE_SIZE,
|
|
"activation shutdown frozen wire contract changed");
|
|
|
|
static const char RUNTIME_BUILD_B[] =
|
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
|
#ifndef _WIN32
|
|
static const char RUNTIME_CACHE_A[] =
|
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
#endif
|
|
static char runtime_self_build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
|
|
static atomic_bool runtime_conflict_log_fallback_seen;
|
|
static atomic_bool runtime_activation_shutdown_log_seen;
|
|
|
|
static void runtime_test_conflict_log_fallback_sink(const char *line) {
|
|
if (line && strstr(line, "daemon.conflict_log_append_failed")) {
|
|
atomic_store_explicit(&runtime_conflict_log_fallback_seen, true, memory_order_release);
|
|
}
|
|
}
|
|
|
|
static void runtime_test_activation_shutdown_sink(const char *line) {
|
|
if (line && strstr(line, "daemon.activation_shutdown") && strstr(line, "requester_pid") &&
|
|
strstr(line, "requester_build") && strstr(line, "action") &&
|
|
strstr(line, "active_clients") && strstr(line, "active_connections")) {
|
|
atomic_store_explicit(&runtime_activation_shutdown_log_seen, true, memory_order_release);
|
|
}
|
|
}
|
|
|
|
typedef struct {
|
|
char parent[RUNTIME_TEST_PATH_CAP];
|
|
char runtime_dir[RUNTIME_TEST_PATH_CAP];
|
|
char key[CBM_DAEMON_KEY_SIZE];
|
|
char log_path[RUNTIME_TEST_PATH_CAP];
|
|
char rotated_log_path[RUNTIME_TEST_PATH_CAP];
|
|
char lock_log_path[RUNTIME_TEST_PATH_CAP];
|
|
cbm_daemon_ipc_endpoint_t *endpoint;
|
|
cbm_daemon_runtime_service_t *service;
|
|
} runtime_test_fixture_t;
|
|
|
|
typedef struct {
|
|
atomic_int opened;
|
|
atomic_int requests;
|
|
atomic_int request_cancels;
|
|
atomic_int cancelled;
|
|
atomic_int closed;
|
|
atomic_bool block_first_request;
|
|
atomic_bool first_request_started;
|
|
atomic_bool ignore_first_request_cancel;
|
|
atomic_bool release_first_request;
|
|
atomic_bool block_second_open;
|
|
atomic_bool second_open_started;
|
|
atomic_bool release_second_open;
|
|
} runtime_application_context_t;
|
|
|
|
typedef struct {
|
|
runtime_application_context_t *context;
|
|
atomic_bool cancel_requested;
|
|
cbm_daemon_client_id_t client_id;
|
|
uint64_t authenticated_process_id;
|
|
} runtime_application_session_t;
|
|
|
|
typedef struct {
|
|
cbm_daemon_runtime_client_t *client;
|
|
const uint8_t *request;
|
|
uint32_t request_length;
|
|
cbm_daemon_runtime_application_token_t request_token;
|
|
bool tagged;
|
|
atomic_bool *completed;
|
|
cbm_daemon_runtime_application_status_t status;
|
|
uint8_t *response;
|
|
uint32_t response_length;
|
|
} runtime_application_client_call_t;
|
|
|
|
typedef struct {
|
|
cbm_daemon_runtime_client_t *client;
|
|
char arguments[RUNTIME_TEST_PATH_CAP + 64];
|
|
uint32_t timeout_ms;
|
|
atomic_bool completed;
|
|
cbm_daemon_runtime_application_status_t status;
|
|
uint8_t *response;
|
|
uint32_t response_length;
|
|
} runtime_real_application_call_t;
|
|
|
|
typedef struct {
|
|
const cbm_daemon_ipc_endpoint_t *endpoint;
|
|
cbm_daemon_build_identity_t identity;
|
|
cbm_daemon_runtime_connect_result_t result;
|
|
cbm_daemon_runtime_client_t *client;
|
|
atomic_bool completed;
|
|
} runtime_application_connect_call_t;
|
|
|
|
static cbm_daemon_build_identity_t runtime_test_identity(const char *version, const char *build) {
|
|
cbm_daemon_build_identity_t identity = {
|
|
.semantic_version = version,
|
|
.build_fingerprint = build,
|
|
.protocol_abi = 3,
|
|
.store_abi = 11,
|
|
.feature_abi = 7,
|
|
};
|
|
return identity;
|
|
}
|
|
|
|
static uint64_t runtime_test_process_id(void) {
|
|
#ifdef _WIN32
|
|
return (uint64_t)GetCurrentProcessId();
|
|
#else
|
|
return (uint64_t)getpid();
|
|
#endif
|
|
}
|
|
|
|
static const char *runtime_test_self_build(void) {
|
|
if (runtime_self_build[0] == '\0') {
|
|
(void)cbm_daemon_runtime_process_build_fingerprint(runtime_test_process_id(),
|
|
runtime_self_build);
|
|
}
|
|
return runtime_self_build;
|
|
}
|
|
|
|
static bool runtime_test_is_fingerprint(const char *value) {
|
|
if (!value || strlen(value) != CBM_DAEMON_BUILD_FINGERPRINT_SIZE - 1) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < CBM_DAEMON_BUILD_FINGERPRINT_SIZE - 1; i++) {
|
|
char ch = value[i];
|
|
if (!((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f'))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool runtime_test_copy_path(char out[RUNTIME_TEST_PATH_CAP], const char *path) {
|
|
if (!path) {
|
|
out[0] = '\0';
|
|
return false;
|
|
}
|
|
int written = snprintf(out, RUNTIME_TEST_PATH_CAP, "%s", path);
|
|
return written >= 0 && written < RUNTIME_TEST_PATH_CAP;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
|
|
enum {
|
|
RUNTIME_TEST_FILE_RENAME_INFO_EX = 22,
|
|
RUNTIME_TEST_FILE_RENAME_REPLACE_IF_EXISTS = 0x00000001,
|
|
RUNTIME_TEST_FILE_RENAME_POSIX_SEMANTICS = 0x00000002,
|
|
};
|
|
|
|
typedef struct {
|
|
DWORD flags;
|
|
HANDLE root_directory;
|
|
DWORD file_name_length;
|
|
wchar_t file_name[1];
|
|
} runtime_test_windows_rename_info_t;
|
|
|
|
static bool runtime_test_windows_copy_self(const char *destination) {
|
|
wchar_t source[32768];
|
|
DWORD source_length =
|
|
GetModuleFileNameW(NULL, source, (DWORD)(sizeof(source) / sizeof(source[0])));
|
|
wchar_t *destination_wide = cbm_path_to_wide(destination);
|
|
bool copied = source_length > 0 &&
|
|
source_length < (DWORD)(sizeof(source) / sizeof(source[0])) && destination_wide &&
|
|
CopyFileW(source, destination_wide, TRUE) != 0;
|
|
free(destination_wide);
|
|
return copied;
|
|
}
|
|
|
|
static bool runtime_test_windows_wait_image_probe(HANDLE process) {
|
|
if (!process) {
|
|
return false;
|
|
}
|
|
DWORD wait_status = WaitForSingleObject(process, TF_RUNTIME_IMAGE_WATCHDOG_MS);
|
|
if (wait_status == WAIT_OBJECT_0) {
|
|
return true;
|
|
}
|
|
(void)TerminateProcess(process, 30);
|
|
if (WaitForSingleObject(process, RUNTIME_TEST_TIMEOUT_MS) != WAIT_OBJECT_0) {
|
|
fprintf(stderr, "daemon_runtime copied-image child could not be reaped\n");
|
|
abort();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool runtime_test_windows_posix_replace(const char *source, const char *destination) {
|
|
wchar_t *source_wide = cbm_utf8_to_wide(source);
|
|
wchar_t *destination_wide = cbm_utf8_to_wide(destination);
|
|
size_t destination_length = destination_wide ? wcslen(destination_wide) : 0;
|
|
size_t destination_bytes = destination_length * sizeof(wchar_t);
|
|
size_t information_size = offsetof(runtime_test_windows_rename_info_t, file_name) +
|
|
destination_bytes + sizeof(wchar_t);
|
|
bool sizes_ok =
|
|
destination_length > 0 && destination_bytes <= MAXDWORD && information_size <= MAXDWORD;
|
|
runtime_test_windows_rename_info_t *information = sizes_ok ? calloc(1, information_size) : NULL;
|
|
if (information) {
|
|
information->flags =
|
|
RUNTIME_TEST_FILE_RENAME_REPLACE_IF_EXISTS | RUNTIME_TEST_FILE_RENAME_POSIX_SEMANTICS;
|
|
information->file_name_length = (DWORD)destination_bytes;
|
|
memcpy(information->file_name, destination_wide, destination_bytes + sizeof(wchar_t));
|
|
}
|
|
HANDLE source_file =
|
|
source_wide && information
|
|
? CreateFileW(source_wide, DELETE | SYNCHRONIZE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
|
|
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL)
|
|
: INVALID_HANDLE_VALUE;
|
|
bool replaced = source_file != INVALID_HANDLE_VALUE &&
|
|
SetFileInformationByHandle(
|
|
source_file, (FILE_INFO_BY_HANDLE_CLASS)RUNTIME_TEST_FILE_RENAME_INFO_EX,
|
|
information, (DWORD)information_size) != 0;
|
|
if (source_file != INVALID_HANDLE_VALUE && !CloseHandle(source_file)) {
|
|
replaced = false;
|
|
}
|
|
free(information);
|
|
free(destination_wide);
|
|
free(source_wide);
|
|
return replaced;
|
|
}
|
|
|
|
static bool runtime_test_windows_spawn_image_holder(const char *image_path, const char *ready_event,
|
|
PROCESS_INFORMATION *process_out) {
|
|
if (!image_path || !ready_event || !process_out) {
|
|
return false;
|
|
}
|
|
memset(process_out, 0, sizeof(*process_out));
|
|
char command_line[RUNTIME_TEST_PATH_CAP * 2];
|
|
int written = snprintf(command_line, sizeof(command_line),
|
|
"\"%s\" __cbm_runtime_image_holder \"%s\"", image_path, ready_event);
|
|
wchar_t *application = cbm_path_to_wide(image_path);
|
|
wchar_t *command =
|
|
written > 0 && written < (int)sizeof(command_line) ? cbm_utf8_to_wide(command_line) : NULL;
|
|
STARTUPINFOW startup;
|
|
memset(&startup, 0, sizeof(startup));
|
|
startup.cb = sizeof(startup);
|
|
bool created = application && command &&
|
|
CreateProcessW(application, command, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL,
|
|
NULL, &startup, process_out) != 0;
|
|
free(command);
|
|
free(application);
|
|
if (created) {
|
|
(void)CloseHandle(process_out->hThread);
|
|
process_out->hThread = NULL;
|
|
}
|
|
return created;
|
|
}
|
|
|
|
#endif
|
|
|
|
#if defined(__APPLE__) || defined(__linux__)
|
|
|
|
static bool runtime_test_copy_executable(const char *source, const char *destination) {
|
|
int source_fd = open(source, O_RDONLY | O_CLOEXEC);
|
|
int destination_fd =
|
|
source_fd >= 0 ? open(destination, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0700) : -1;
|
|
unsigned char buffer[64 * 1024];
|
|
bool ok = source_fd >= 0 && destination_fd >= 0;
|
|
while (ok) {
|
|
ssize_t count = read(source_fd, buffer, sizeof(buffer));
|
|
if (count == 0) {
|
|
break;
|
|
}
|
|
if (count < 0) {
|
|
if (errno == EINTR) {
|
|
continue;
|
|
}
|
|
ok = false;
|
|
break;
|
|
}
|
|
size_t written = 0;
|
|
while (written < (size_t)count) {
|
|
ssize_t chunk = write(destination_fd, buffer + written, (size_t)count - written);
|
|
if (chunk > 0) {
|
|
written += (size_t)chunk;
|
|
} else if (chunk < 0 && errno == EINTR) {
|
|
continue;
|
|
} else {
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (destination_fd >= 0 && fchmod(destination_fd, 0700) != 0) {
|
|
ok = false;
|
|
}
|
|
if (destination_fd >= 0 && close(destination_fd) != 0) {
|
|
ok = false;
|
|
}
|
|
if (source_fd >= 0 && close(source_fd) != 0) {
|
|
ok = false;
|
|
}
|
|
if (!ok) {
|
|
(void)unlink(destination);
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
static pid_t runtime_test_spawn_blocked_executable(const char *path, int *release_fd_out) {
|
|
int ready[2] = {-1, -1};
|
|
int input[2] = {-1, -1};
|
|
if (!path || !release_fd_out || pipe(ready) != 0 || pipe(input) != 0) {
|
|
if (ready[0] >= 0) {
|
|
(void)close(ready[0]);
|
|
(void)close(ready[1]);
|
|
}
|
|
if (input[0] >= 0) {
|
|
(void)close(input[0]);
|
|
(void)close(input[1]);
|
|
}
|
|
return -1;
|
|
}
|
|
int ready_flags = fcntl(ready[1], F_GETFD);
|
|
if (ready_flags < 0 || fcntl(ready[1], F_SETFD, ready_flags | FD_CLOEXEC) != 0) {
|
|
(void)close(ready[0]);
|
|
(void)close(ready[1]);
|
|
(void)close(input[0]);
|
|
(void)close(input[1]);
|
|
return -1;
|
|
}
|
|
/* posix_spawn rather than fork+exec: this process can carry a sanitizer's
|
|
* very large shadow mapping, and fork() duplicates the parent address
|
|
* space. On macOS that duplicate trips the per-process memory limit and
|
|
* jetsam SIGKILLs the child before exec ever replaces the image, so the
|
|
* fixture fails under TSan for reasons unrelated to the code under test.
|
|
* posix_spawn never copies the parent's address space. Exec failure is
|
|
* reported by posix_spawn itself, so the child no longer needs to signal
|
|
* it over the ready pipe; the pipe's FD_CLOEXEC close still marks a
|
|
* successful exec with EOF exactly as before. */
|
|
posix_spawn_file_actions_t actions;
|
|
if (posix_spawn_file_actions_init(&actions) != 0) {
|
|
(void)close(ready[0]);
|
|
(void)close(ready[1]);
|
|
(void)close(input[0]);
|
|
(void)close(input[1]);
|
|
return -1;
|
|
}
|
|
(void)posix_spawn_file_actions_addclose(&actions, ready[0]);
|
|
(void)posix_spawn_file_actions_addclose(&actions, input[1]);
|
|
(void)posix_spawn_file_actions_adddup2(&actions, input[0], STDIN_FILENO);
|
|
if (input[0] != STDIN_FILENO) {
|
|
(void)posix_spawn_file_actions_addclose(&actions, input[0]);
|
|
}
|
|
char *const child_argv[] = {(char *)path, NULL};
|
|
pid_t child = -1;
|
|
if (posix_spawn(&child, path, &actions, NULL, child_argv, environ) != 0) {
|
|
child = -1;
|
|
}
|
|
(void)posix_spawn_file_actions_destroy(&actions);
|
|
(void)close(ready[1]);
|
|
(void)close(input[0]);
|
|
char unexpected = '\0';
|
|
ssize_t ready_count;
|
|
do {
|
|
ready_count = read(ready[0], &unexpected, 1);
|
|
} while (ready_count < 0 && errno == EINTR);
|
|
(void)close(ready[0]);
|
|
int status = 0;
|
|
bool running = child > 0 && ready_count == 0 && waitpid(child, &status, WNOHANG) == 0;
|
|
if (!running) {
|
|
(void)close(input[1]);
|
|
if (child > 0) {
|
|
(void)kill(child, SIGKILL);
|
|
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {}
|
|
}
|
|
return -1;
|
|
}
|
|
*release_fd_out = input[1];
|
|
return child;
|
|
}
|
|
|
|
static void runtime_test_stop_blocked_executable(pid_t child, int release_fd) {
|
|
if (release_fd >= 0) {
|
|
(void)close(release_fd);
|
|
}
|
|
if (child <= 0) {
|
|
return;
|
|
}
|
|
(void)kill(child, SIGTERM);
|
|
int status = 0;
|
|
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {}
|
|
}
|
|
|
|
#endif
|
|
|
|
#if defined(__APPLE__) || defined(__linux__)
|
|
static bool runtime_test_self_image_path(char source[RUNTIME_TEST_PATH_CAP]) {
|
|
#ifdef __APPLE__
|
|
int length = proc_pidpath(getpid(), source, RUNTIME_TEST_PATH_CAP);
|
|
bool resolved = length > 0 && length < RUNTIME_TEST_PATH_CAP;
|
|
if (resolved) {
|
|
source[length] = '\0';
|
|
}
|
|
#else
|
|
ssize_t length = readlink("/proc/self/exe", source, RUNTIME_TEST_PATH_CAP - 1);
|
|
bool resolved = length > 0 && length < (ssize_t)RUNTIME_TEST_PATH_CAP - 1;
|
|
if (resolved) {
|
|
source[length] = '\0';
|
|
}
|
|
#endif
|
|
return resolved;
|
|
}
|
|
#endif
|
|
|
|
static bool runtime_test_copy_self_image(const char *destination) {
|
|
#ifdef _WIN32
|
|
return runtime_test_windows_copy_self(destination);
|
|
#elif defined(__APPLE__) || defined(__linux__)
|
|
char source[RUNTIME_TEST_PATH_CAP];
|
|
return runtime_test_self_image_path(source) &&
|
|
runtime_test_copy_executable(source, destination);
|
|
#else
|
|
(void)destination;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
static bool runtime_test_paths_refer_to_same_file(const char *left, const char *right) {
|
|
enum { CANONICAL_CAP = 4096 };
|
|
char left_canonical[CANONICAL_CAP];
|
|
char right_canonical[CANONICAL_CAP];
|
|
if (!left || !right || !cbm_canonical_path(left, left_canonical, sizeof(left_canonical)) ||
|
|
!cbm_canonical_path(right, right_canonical, sizeof(right_canonical))) {
|
|
return false;
|
|
}
|
|
#ifdef _WIN32
|
|
return _stricmp(left_canonical, right_canonical) == 0;
|
|
#else
|
|
return strcmp(left_canonical, right_canonical) == 0;
|
|
#endif
|
|
}
|
|
|
|
static bool runtime_test_process_image_matches(uint64_t process_id, const char *expected_image) {
|
|
if (process_id <= 1 || process_id == runtime_test_process_id() || !expected_image) {
|
|
return false;
|
|
}
|
|
char observed[4096] = {0};
|
|
#ifdef _WIN32
|
|
if (process_id > UINT32_MAX) {
|
|
return false;
|
|
}
|
|
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD)process_id);
|
|
wchar_t wide[32768];
|
|
DWORD wide_length = (DWORD)(sizeof(wide) / sizeof(wide[0]));
|
|
bool queried = process && QueryFullProcessImageNameW(process, 0, wide, &wide_length) != 0;
|
|
char *utf8 = queried ? cbm_wide_to_utf8(wide) : NULL;
|
|
if (utf8) {
|
|
(void)snprintf(observed, sizeof(observed), "%s", utf8);
|
|
}
|
|
free(utf8);
|
|
if (process) {
|
|
(void)CloseHandle(process);
|
|
}
|
|
#elif defined(__APPLE__)
|
|
int length = process_id <= INT_MAX
|
|
? proc_pidpath((int)process_id, observed, (uint32_t)sizeof(observed))
|
|
: 0;
|
|
if (length <= 0 || length >= (int)sizeof(observed)) {
|
|
observed[0] = '\0';
|
|
}
|
|
#elif defined(__linux__)
|
|
char proc_path[64];
|
|
int written =
|
|
snprintf(proc_path, sizeof(proc_path), "/proc/%llu/exe", (unsigned long long)process_id);
|
|
ssize_t length = written > 0 && written < (int)sizeof(proc_path)
|
|
? readlink(proc_path, observed, sizeof(observed) - 1)
|
|
: -1;
|
|
if (length > 0 && length < (ssize_t)sizeof(observed)) {
|
|
observed[length] = '\0';
|
|
} else {
|
|
observed[0] = '\0';
|
|
}
|
|
#else
|
|
(void)process_id;
|
|
#endif
|
|
return observed[0] && runtime_test_paths_refer_to_same_file(observed, expected_image);
|
|
}
|
|
|
|
static bool runtime_test_wait_pid_marker(const char *path, uint32_t timeout_ms,
|
|
uint64_t *process_id_out) {
|
|
if (process_id_out) {
|
|
*process_id_out = 0;
|
|
}
|
|
uint64_t deadline = cbm_now_ms() + timeout_ms;
|
|
do {
|
|
FILE *marker = cbm_fopen(path, "rb");
|
|
unsigned long long parsed = 0;
|
|
bool valid = marker && fscanf(marker, "%llu", &parsed) == 1 && parsed > 1 &&
|
|
parsed != runtime_test_process_id();
|
|
if (marker) {
|
|
(void)fclose(marker);
|
|
}
|
|
if (valid) {
|
|
if (process_id_out) {
|
|
*process_id_out = (uint64_t)parsed;
|
|
}
|
|
return true;
|
|
}
|
|
cbm_usleep(1000);
|
|
} while (cbm_now_ms() < deadline);
|
|
return false;
|
|
}
|
|
|
|
static bool runtime_test_wait_process_image_gone(uint64_t process_id, const char *expected_image,
|
|
uint32_t timeout_ms) {
|
|
uint64_t deadline = cbm_now_ms() + timeout_ms;
|
|
do {
|
|
if (!runtime_test_process_image_matches(process_id, expected_image)) {
|
|
return true;
|
|
}
|
|
cbm_usleep(1000);
|
|
} while (cbm_now_ms() < deadline);
|
|
return !runtime_test_process_image_matches(process_id, expected_image);
|
|
}
|
|
|
|
static bool runtime_test_force_terminate_verified(uint64_t process_id, const char *expected_image) {
|
|
if (!runtime_test_process_image_matches(process_id, expected_image)) {
|
|
return true;
|
|
}
|
|
#ifdef _WIN32
|
|
HANDLE process =
|
|
process_id <= UINT32_MAX
|
|
? OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE | SYNCHRONIZE,
|
|
FALSE, (DWORD)process_id)
|
|
: NULL;
|
|
if (!process) {
|
|
return false;
|
|
}
|
|
wchar_t wide[32768];
|
|
DWORD wide_length = (DWORD)(sizeof(wide) / sizeof(wide[0]));
|
|
bool queried = QueryFullProcessImageNameW(process, 0, wide, &wide_length) != 0;
|
|
char *utf8 = queried ? cbm_wide_to_utf8(wide) : NULL;
|
|
bool exact = utf8 && runtime_test_paths_refer_to_same_file(utf8, expected_image);
|
|
free(utf8);
|
|
bool terminated = exact && TerminateProcess(process, 99) != 0;
|
|
if (terminated) {
|
|
terminated = WaitForSingleObject(process, RUNTIME_TEST_TIMEOUT_MS) == WAIT_OBJECT_0;
|
|
}
|
|
(void)CloseHandle(process);
|
|
return terminated;
|
|
#elif defined(__APPLE__) || defined(__linux__)
|
|
/* The marker lives in a private test directory and was written by this
|
|
* exact copied image. Revalidate immediately before signaling so the
|
|
* cleanup backstop never targets an unrelated or PID-reused process. */
|
|
return runtime_test_process_image_matches(process_id, expected_image) &&
|
|
kill((pid_t)process_id, SIGKILL) == 0 &&
|
|
runtime_test_wait_process_image_gone(process_id, expected_image,
|
|
RUNTIME_TEST_TIMEOUT_MS);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
#if defined(_WIN32) || defined(__linux__)
|
|
static bool runtime_test_append_image_marker(const char *path) {
|
|
FILE *file = cbm_fopen(path, "ab");
|
|
bool written = file && fputc('\n', file) != EOF;
|
|
if (file) {
|
|
written = fclose(file) == 0 && written;
|
|
}
|
|
return written;
|
|
}
|
|
#endif
|
|
|
|
#ifdef __APPLE__
|
|
static bool runtime_test_mac_ad_hoc_sign(const char *path) {
|
|
if (!path) {
|
|
return false;
|
|
}
|
|
pid_t child = fork();
|
|
if (child == 0) {
|
|
execl("/usr/bin/codesign", "codesign", "--force", "--sign", "-", "--timestamp=none",
|
|
"--identifier", "org.deusdata.cbm.foreign-test", path, (char *)NULL);
|
|
_exit(127);
|
|
}
|
|
int status = 0;
|
|
pid_t waited;
|
|
do {
|
|
waited = child > 0 ? waitpid(child, &status, 0) : -1;
|
|
} while (waited < 0 && errno == EINTR);
|
|
return waited == child && WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
}
|
|
#endif
|
|
|
|
static bool runtime_test_run_hello_image(const char *image_path,
|
|
const runtime_test_fixture_t *fixture,
|
|
const cbm_daemon_build_identity_t *identity,
|
|
int *exit_code_out) {
|
|
if (!image_path || !fixture || !identity || !identity->semantic_version ||
|
|
!identity->build_fingerprint || !exit_code_out) {
|
|
return false;
|
|
}
|
|
*exit_code_out = -1;
|
|
#ifdef _WIN32
|
|
char command_line[RUNTIME_TEST_PATH_CAP * 3];
|
|
int written =
|
|
snprintf(command_line, sizeof(command_line),
|
|
"\"%s\" __cbm_runtime_hello_client \"%s\" %s %s %s", image_path, fixture->parent,
|
|
fixture->key, identity->semantic_version, identity->build_fingerprint);
|
|
wchar_t *application = cbm_utf8_to_wide(image_path);
|
|
wchar_t *command =
|
|
written > 0 && written < (int)sizeof(command_line) ? cbm_utf8_to_wide(command_line) : NULL;
|
|
STARTUPINFOW startup;
|
|
PROCESS_INFORMATION process;
|
|
memset(&startup, 0, sizeof(startup));
|
|
memset(&process, 0, sizeof(process));
|
|
startup.cb = sizeof(startup);
|
|
bool started = application && command &&
|
|
CreateProcessW(application, command, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL,
|
|
NULL, &startup, &process) != 0;
|
|
free(command);
|
|
free(application);
|
|
bool waited = started && runtime_test_windows_wait_image_probe(process.hProcess);
|
|
DWORD exit_code = 0;
|
|
bool read = waited && GetExitCodeProcess(process.hProcess, &exit_code) != 0;
|
|
if (started) {
|
|
(void)CloseHandle(process.hThread);
|
|
(void)CloseHandle(process.hProcess);
|
|
}
|
|
if (read && exit_code <= INT_MAX) {
|
|
*exit_code_out = (int)exit_code;
|
|
}
|
|
return read && exit_code <= INT_MAX;
|
|
#elif defined(__APPLE__) || defined(__linux__)
|
|
pid_t child = fork();
|
|
if (child == 0) {
|
|
(void)alarm(TF_RUNTIME_IMAGE_WATCHDOG_SECONDS);
|
|
execl(image_path, image_path, "__cbm_runtime_hello_client", fixture->parent, fixture->key,
|
|
identity->semantic_version, identity->build_fingerprint, (char *)NULL);
|
|
_exit(127);
|
|
}
|
|
int status = 0;
|
|
pid_t waited;
|
|
do {
|
|
waited = child > 0 ? waitpid(child, &status, 0) : -1;
|
|
} while (waited < 0 && errno == EINTR);
|
|
if (waited != child || !WIFEXITED(status)) {
|
|
return false;
|
|
}
|
|
*exit_code_out = WEXITSTATUS(status);
|
|
return true;
|
|
#else
|
|
(void)image_path;
|
|
(void)fixture;
|
|
(void)identity;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
static bool runtime_test_run_activation_image(const char *image_path,
|
|
const runtime_test_fixture_t *fixture,
|
|
const cbm_daemon_build_identity_t *identity,
|
|
cbm_daemon_runtime_activation_action_t action,
|
|
int *exit_code_out) {
|
|
if (!image_path || !fixture || !identity || !identity->semantic_version ||
|
|
!identity->build_fingerprint || !exit_code_out) {
|
|
return false;
|
|
}
|
|
*exit_code_out = -1;
|
|
#ifdef _WIN32
|
|
char command_line[RUNTIME_TEST_PATH_CAP * 3];
|
|
int written = snprintf(command_line, sizeof(command_line),
|
|
"\"%s\" __cbm_runtime_activation_client \"%s\" %s %s %s %u", image_path,
|
|
fixture->parent, fixture->key, identity->semantic_version,
|
|
identity->build_fingerprint, (unsigned int)action);
|
|
wchar_t *application = cbm_utf8_to_wide(image_path);
|
|
wchar_t *command =
|
|
written > 0 && written < (int)sizeof(command_line) ? cbm_utf8_to_wide(command_line) : NULL;
|
|
STARTUPINFOW startup;
|
|
PROCESS_INFORMATION process;
|
|
memset(&startup, 0, sizeof(startup));
|
|
memset(&process, 0, sizeof(process));
|
|
startup.cb = sizeof(startup);
|
|
bool started = application && command &&
|
|
CreateProcessW(application, command, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL,
|
|
NULL, &startup, &process) != 0;
|
|
free(command);
|
|
free(application);
|
|
bool waited = started && runtime_test_windows_wait_image_probe(process.hProcess);
|
|
DWORD exit_code = 0;
|
|
bool read = waited && GetExitCodeProcess(process.hProcess, &exit_code) != 0;
|
|
if (started) {
|
|
(void)CloseHandle(process.hThread);
|
|
(void)CloseHandle(process.hProcess);
|
|
}
|
|
if (read && exit_code <= INT_MAX) {
|
|
*exit_code_out = (int)exit_code;
|
|
}
|
|
return read && exit_code <= INT_MAX;
|
|
#elif defined(__APPLE__) || defined(__linux__)
|
|
char action_text[16];
|
|
int action_written = snprintf(action_text, sizeof(action_text), "%u", (unsigned int)action);
|
|
pid_t child = action_written > 0 && action_written < (int)sizeof(action_text) ? fork() : -1;
|
|
if (child == 0) {
|
|
(void)alarm(TF_RUNTIME_IMAGE_WATCHDOG_SECONDS);
|
|
execl(image_path, image_path, "__cbm_runtime_activation_client", fixture->parent,
|
|
fixture->key, identity->semantic_version, identity->build_fingerprint, action_text,
|
|
(char *)NULL);
|
|
_exit(127);
|
|
}
|
|
int status = 0;
|
|
pid_t waited;
|
|
do {
|
|
waited = child > 0 ? waitpid(child, &status, 0) : -1;
|
|
} while (waited < 0 && errno == EINTR);
|
|
if (waited != child || !WIFEXITED(status)) {
|
|
return false;
|
|
}
|
|
*exit_code_out = WEXITSTATUS(status);
|
|
return true;
|
|
#else
|
|
(void)image_path;
|
|
(void)fixture;
|
|
(void)identity;
|
|
(void)action;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
#ifdef __APPLE__
|
|
static bool runtime_test_run_mapped_hello_image(const char *image_path,
|
|
const char *mapped_image_path,
|
|
const runtime_test_fixture_t *fixture,
|
|
const cbm_daemon_build_identity_t *identity,
|
|
int *exit_code_out) {
|
|
if (!image_path || !mapped_image_path || !fixture || !identity || !identity->semantic_version ||
|
|
!identity->build_fingerprint || !exit_code_out) {
|
|
return false;
|
|
}
|
|
*exit_code_out = -1;
|
|
pid_t child = fork();
|
|
if (child == 0) {
|
|
execl(image_path, image_path, "__cbm_runtime_mapped_hello_client", mapped_image_path,
|
|
fixture->parent, fixture->key, identity->semantic_version,
|
|
identity->build_fingerprint, (char *)NULL);
|
|
_exit(127);
|
|
}
|
|
int status = 0;
|
|
pid_t waited;
|
|
do {
|
|
waited = child > 0 ? waitpid(child, &status, 0) : -1;
|
|
} while (waited < 0 && errno == EINTR);
|
|
if (waited != child || !WIFEXITED(status)) {
|
|
return false;
|
|
}
|
|
*exit_code_out = WEXITSTATUS(status);
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
static cbm_daemon_runtime_application_session_t *runtime_application_session_open(
|
|
void *opaque, cbm_daemon_client_id_t client_id, uint64_t authenticated_process_id) {
|
|
runtime_application_context_t *context = opaque;
|
|
runtime_application_session_t *session = calloc(1, sizeof(*session));
|
|
if (!context || !session || client_id == CBM_DAEMON_CLIENT_ID_INVALID ||
|
|
authenticated_process_id == 0) {
|
|
free(session);
|
|
return NULL;
|
|
}
|
|
session->context = context;
|
|
session->client_id = client_id;
|
|
session->authenticated_process_id = authenticated_process_id;
|
|
atomic_init(&session->cancel_requested, false);
|
|
int open_index = atomic_fetch_add_explicit(&context->opened, 1, memory_order_relaxed);
|
|
if (open_index == 1 &&
|
|
atomic_load_explicit(&context->block_second_open, memory_order_acquire)) {
|
|
atomic_store_explicit(&context->second_open_started, true, memory_order_release);
|
|
while (!atomic_load_explicit(&context->release_second_open, memory_order_acquire)) {
|
|
struct timespec pause = {.tv_sec = 0, .tv_nsec = 1000000};
|
|
(void)cbm_nanosleep(&pause, NULL);
|
|
}
|
|
}
|
|
return (cbm_daemon_runtime_application_session_t *)session;
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t runtime_application_request(
|
|
void *opaque, cbm_daemon_runtime_application_session_t *opaque_session,
|
|
cbm_daemon_runtime_application_token_t request_token, const uint8_t *request,
|
|
uint32_t request_length, uint8_t **response_out, uint32_t *response_length_out) {
|
|
(void)request_token;
|
|
runtime_application_context_t *context = opaque;
|
|
runtime_application_session_t *session = (runtime_application_session_t *)opaque_session;
|
|
if (!context || !session || session->context != context || !response_out ||
|
|
!response_length_out || (request_length > 0 && !request)) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
*response_out = NULL;
|
|
*response_length_out = 0;
|
|
int request_index = atomic_fetch_add_explicit(&context->requests, 1, memory_order_relaxed);
|
|
if (request_index == 0 &&
|
|
atomic_load_explicit(&context->block_first_request, memory_order_acquire)) {
|
|
atomic_store_explicit(&context->first_request_started, true, memory_order_release);
|
|
bool ignore_cancel =
|
|
atomic_load_explicit(&context->ignore_first_request_cancel, memory_order_acquire);
|
|
while (!(ignore_cancel
|
|
? atomic_load_explicit(&context->release_first_request, memory_order_acquire)
|
|
: atomic_load_explicit(&session->cancel_requested, memory_order_acquire))) {
|
|
struct timespec pause = {.tv_sec = 0, .tv_nsec = 1000000};
|
|
(void)cbm_nanosleep(&pause, NULL);
|
|
}
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED;
|
|
}
|
|
if (request_length == 0) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_OK;
|
|
}
|
|
uint8_t *response = malloc(request_length);
|
|
if (!response) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
memcpy(response, request, request_length);
|
|
*response_out = response;
|
|
*response_length_out = request_length;
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_OK;
|
|
}
|
|
|
|
static void *runtime_application_client_request_thread(void *opaque) {
|
|
runtime_application_client_call_t *call = opaque;
|
|
call->status = call->tagged
|
|
? cbm_daemon_runtime_client_application_request_tagged(
|
|
call->client, call->request_token, call->request, call->request_length,
|
|
&call->response, &call->response_length, RUNTIME_TEST_TIMEOUT_MS)
|
|
: cbm_daemon_runtime_client_application_request(
|
|
call->client, call->request, call->request_length, &call->response,
|
|
&call->response_length, RUNTIME_TEST_TIMEOUT_MS);
|
|
if (call->completed) {
|
|
/* This must remain the helper's final access to call/client state. A
|
|
* failed OS join can then fall back to this lifetime sentinel without
|
|
* racing stack teardown or client cleanup. */
|
|
atomic_store_explicit(call->completed, true, memory_order_release);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void *runtime_real_application_detect_changes_thread(void *opaque) {
|
|
runtime_real_application_call_t *call = opaque;
|
|
uint32_t timeout_ms = call->timeout_ms ? call->timeout_ms : RUNTIME_TEST_TIMEOUT_MS;
|
|
call->status =
|
|
cbm_daemon_application_client_tool(call->client, "detect_changes", call->arguments,
|
|
&call->response, &call->response_length, timeout_ms);
|
|
atomic_store_explicit(&call->completed, true, memory_order_release);
|
|
return NULL;
|
|
}
|
|
|
|
static bool runtime_real_application_ingest_probe(cbm_daemon_runtime_client_t *client) {
|
|
uint8_t *response = NULL;
|
|
uint32_t response_length = 0;
|
|
cbm_daemon_runtime_application_status_t status =
|
|
cbm_daemon_application_client_tool(client, "ingest_traces", "{\"traces\":[]}", &response,
|
|
&response_length, RUNTIME_TEST_TIMEOUT_MS);
|
|
bool usable = status == CBM_DAEMON_RUNTIME_APPLICATION_OK && response && response_length > 0 &&
|
|
strstr((const char *)response, "traces_received");
|
|
free(response);
|
|
return usable;
|
|
}
|
|
|
|
static void runtime_test_restore_environment(const char *name, const char *saved_value,
|
|
bool was_set) {
|
|
if (was_set) {
|
|
(void)cbm_setenv(name, saved_value, 1);
|
|
} else {
|
|
(void)cbm_unsetenv(name);
|
|
}
|
|
}
|
|
|
|
static void *runtime_application_client_connect_thread(void *opaque) {
|
|
runtime_application_connect_call_t *call = opaque;
|
|
call->client = cbm_daemon_runtime_client_connect(call->endpoint, &call->identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &call->result);
|
|
atomic_store_explicit(&call->completed, true, memory_order_release);
|
|
return NULL;
|
|
}
|
|
|
|
static void runtime_application_request_cancel(
|
|
void *opaque, cbm_daemon_runtime_application_session_t *opaque_session,
|
|
cbm_daemon_runtime_application_token_t request_token) {
|
|
(void)request_token;
|
|
runtime_application_context_t *context = opaque;
|
|
runtime_application_session_t *session = (runtime_application_session_t *)opaque_session;
|
|
if (!context || !session || session->context != context) {
|
|
return;
|
|
}
|
|
atomic_store_explicit(&session->cancel_requested, true, memory_order_release);
|
|
(void)atomic_fetch_add_explicit(&context->request_cancels, 1, memory_order_relaxed);
|
|
}
|
|
|
|
static void runtime_application_session_cancel(
|
|
void *opaque, cbm_daemon_runtime_application_session_t *opaque_session) {
|
|
runtime_application_context_t *context = opaque;
|
|
runtime_application_session_t *session = (runtime_application_session_t *)opaque_session;
|
|
if (!context || !session || session->context != context) {
|
|
return;
|
|
}
|
|
atomic_store_explicit(&session->cancel_requested, true, memory_order_release);
|
|
(void)atomic_fetch_add_explicit(&context->cancelled, 1, memory_order_relaxed);
|
|
}
|
|
|
|
static void runtime_application_session_close(
|
|
void *opaque, cbm_daemon_runtime_application_session_t *opaque_session) {
|
|
runtime_application_context_t *context = opaque;
|
|
runtime_application_session_t *session = (runtime_application_session_t *)opaque_session;
|
|
if (!context || !session || session->context != context) {
|
|
return;
|
|
}
|
|
(void)atomic_fetch_add_explicit(&context->closed, 1, memory_order_relaxed);
|
|
free(session);
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_callbacks_t runtime_application_callbacks(
|
|
runtime_application_context_t *context) {
|
|
cbm_daemon_runtime_application_callbacks_t callbacks = {
|
|
.context = context,
|
|
.session_open = runtime_application_session_open,
|
|
.request = runtime_application_request,
|
|
.request_cancel = runtime_application_request_cancel,
|
|
.session_cancel = runtime_application_session_cancel,
|
|
.session_close = runtime_application_session_close,
|
|
};
|
|
return callbacks;
|
|
}
|
|
|
|
static void runtime_application_context_init(runtime_application_context_t *context,
|
|
bool block_first_request) {
|
|
memset(context, 0, sizeof(*context));
|
|
atomic_init(&context->opened, 0);
|
|
atomic_init(&context->requests, 0);
|
|
atomic_init(&context->request_cancels, 0);
|
|
atomic_init(&context->cancelled, 0);
|
|
atomic_init(&context->closed, 0);
|
|
atomic_init(&context->block_first_request, block_first_request);
|
|
atomic_init(&context->first_request_started, false);
|
|
atomic_init(&context->ignore_first_request_cancel, false);
|
|
atomic_init(&context->release_first_request, false);
|
|
atomic_init(&context->block_second_open, false);
|
|
atomic_init(&context->second_open_started, false);
|
|
atomic_init(&context->release_second_open, false);
|
|
}
|
|
|
|
static bool runtime_test_wait_atomic_bool(atomic_bool *value, uint32_t timeout_ms) {
|
|
uint64_t deadline = cbm_now_ms() + (uint64_t)timeout_ms;
|
|
while (!atomic_load_explicit(value, memory_order_acquire)) {
|
|
if (cbm_now_ms() >= deadline) {
|
|
return false;
|
|
}
|
|
struct timespec pause = {.tv_sec = 0, .tv_nsec = 1000000};
|
|
(void)cbm_nanosleep(&pause, NULL);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool runtime_test_wait_atomic_int(atomic_int *value, int expected, uint32_t timeout_ms) {
|
|
uint64_t deadline = cbm_now_ms() + (uint64_t)timeout_ms;
|
|
while (atomic_load_explicit(value, memory_order_acquire) != expected) {
|
|
if (cbm_now_ms() >= deadline) {
|
|
return false;
|
|
}
|
|
struct timespec pause = {.tv_sec = 0, .tv_nsec = 1000000};
|
|
(void)cbm_nanosleep(&pause, NULL);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void runtime_test_put_u32(uint8_t out[4], uint32_t value) {
|
|
out[0] = (uint8_t)(value >> 24);
|
|
out[1] = (uint8_t)(value >> 16);
|
|
out[2] = (uint8_t)(value >> 8);
|
|
out[3] = (uint8_t)value;
|
|
}
|
|
|
|
static uint32_t runtime_test_get_u32(const uint8_t in[4]) {
|
|
return ((uint32_t)in[0] << 24) | ((uint32_t)in[1] << 16) | ((uint32_t)in[2] << 8) |
|
|
(uint32_t)in[3];
|
|
}
|
|
|
|
static void runtime_test_put_u64(uint8_t out[8], uint64_t value) {
|
|
for (size_t index = 0; index < 8; index++) {
|
|
out[index] = (uint8_t)(value >> (56U - index * 8U));
|
|
}
|
|
}
|
|
|
|
static uint64_t runtime_test_get_u64(const uint8_t in[8]) {
|
|
uint64_t value = 0;
|
|
for (size_t index = 0; index < 8; index++) {
|
|
value = (value << 8U) | in[index];
|
|
}
|
|
return value;
|
|
}
|
|
|
|
static bool runtime_test_raw_activation_exchange(
|
|
const cbm_daemon_ipc_endpoint_t *endpoint, const uint8_t *request, uint32_t request_length,
|
|
cbm_daemon_runtime_activation_result_t *result_out) {
|
|
if (result_out) {
|
|
memset(result_out, 0, sizeof(*result_out));
|
|
}
|
|
if (!endpoint || !request || !result_out) {
|
|
return false;
|
|
}
|
|
cbm_daemon_ipc_connection_t *connection =
|
|
cbm_daemon_ipc_connect(endpoint, RUNTIME_TEST_TIMEOUT_MS);
|
|
bool sent = connection && cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST,
|
|
CBM_DAEMON_RUNTIME_OP_ACTIVATION_SHUTDOWN,
|
|
request, request_length);
|
|
cbm_daemon_frame_t frame = {0};
|
|
uint8_t *response = NULL;
|
|
int received =
|
|
sent ? cbm_daemon_ipc_receive_frame(connection, RUNTIME_TEST_TIMEOUT_MS, &frame, &response)
|
|
: 0;
|
|
uint32_t status = received == 1 && response ? runtime_test_get_u32(response + 4) : UINT32_MAX;
|
|
bool valid = received == 1 && frame.type == CBM_DAEMON_FRAME_RESPONSE &&
|
|
frame.flags == CBM_DAEMON_RUNTIME_OP_ACTIVATION_SHUTDOWN &&
|
|
frame.length == RUNTIME_TEST_ACTIVATION_RESPONSE_SIZE && response &&
|
|
runtime_test_get_u32(response) == RUNTIME_TEST_RENDEZVOUS_ABI && status <= 1U;
|
|
if (valid) {
|
|
result_out->accepted = status == 1U;
|
|
result_out->active_clients =
|
|
runtime_test_get_u64(response + RUNTIME_TEST_ACTIVATION_RESPONSE_CLIENTS_OFFSET);
|
|
result_out->active_connections =
|
|
runtime_test_get_u64(response + RUNTIME_TEST_ACTIVATION_RESPONSE_CONNECTIONS_OFFSET);
|
|
}
|
|
free(response);
|
|
cbm_daemon_ipc_connection_close(connection);
|
|
return valid;
|
|
}
|
|
|
|
static bool runtime_test_activation_request_encode(
|
|
uint8_t out[RUNTIME_TEST_ACTIVATION_REQUEST_SIZE],
|
|
cbm_daemon_runtime_activation_action_t action, const cbm_daemon_build_identity_t *identity) {
|
|
if (!out || !identity) {
|
|
return false;
|
|
}
|
|
memset(out, 0, RUNTIME_TEST_ACTIVATION_REQUEST_SIZE);
|
|
runtime_test_put_u32(out, (uint32_t)action);
|
|
return cbm_daemon_runtime_hello_request_encode(out + RUNTIME_TEST_ACTIVATION_IDENTITY_OFFSET,
|
|
identity);
|
|
}
|
|
|
|
static bool runtime_test_fixed_string_equals(const uint8_t *wire, size_t capacity,
|
|
const char *expected) {
|
|
if (!wire || !expected) {
|
|
return false;
|
|
}
|
|
size_t length = strlen(expected);
|
|
if (length >= capacity || memcmp(wire, expected, length) != 0 || wire[length] != 0) {
|
|
return false;
|
|
}
|
|
for (size_t i = length + 1; i < capacity; i++) {
|
|
if (wire[i] != 0) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static cbm_daemon_ipc_connection_t *runtime_test_raw_client_connect(
|
|
const cbm_daemon_ipc_endpoint_t *endpoint, const cbm_daemon_build_identity_t *identity) {
|
|
uint8_t hello[CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE];
|
|
if (!cbm_daemon_runtime_hello_request_encode(hello, identity)) {
|
|
return NULL;
|
|
}
|
|
cbm_daemon_ipc_connection_t *connection =
|
|
cbm_daemon_ipc_connect(endpoint, RUNTIME_TEST_TIMEOUT_MS);
|
|
if (!connection ||
|
|
!cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST,
|
|
CBM_DAEMON_RUNTIME_OP_HELLO, hello, (uint32_t)sizeof(hello))) {
|
|
cbm_daemon_ipc_connection_close(connection);
|
|
return NULL;
|
|
}
|
|
cbm_daemon_frame_t frame = {0};
|
|
uint8_t *payload = NULL;
|
|
int received =
|
|
cbm_daemon_ipc_receive_frame(connection, RUNTIME_TEST_TIMEOUT_MS, &frame, &payload);
|
|
bool accepted = received == 1 && frame.type == CBM_DAEMON_FRAME_RESPONSE &&
|
|
frame.flags == CBM_DAEMON_RUNTIME_OP_HELLO && frame.length > 0;
|
|
free(payload);
|
|
if (!accepted) {
|
|
cbm_daemon_ipc_connection_close(connection);
|
|
return NULL;
|
|
}
|
|
return connection;
|
|
}
|
|
|
|
static bool runtime_test_raw_application_send_token(cbm_daemon_ipc_connection_t *connection,
|
|
uint64_t request_token, const void *payload,
|
|
uint32_t payload_length,
|
|
uint32_t declared_length) {
|
|
uint64_t wire_length = 12ULL + payload_length;
|
|
if (!connection || request_token == 0 || wire_length > UINT32_MAX ||
|
|
(payload_length > 0 && !payload)) {
|
|
return false;
|
|
}
|
|
uint8_t *wire = malloc((size_t)wire_length);
|
|
if (!wire) {
|
|
return false;
|
|
}
|
|
runtime_test_put_u64(wire, request_token);
|
|
runtime_test_put_u32(wire + 8, declared_length);
|
|
if (payload_length > 0) {
|
|
memcpy(wire + 12, payload, payload_length);
|
|
}
|
|
bool sent = cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST,
|
|
CBM_DAEMON_RUNTIME_OP_APPLICATION_REQUEST, wire,
|
|
(uint32_t)wire_length);
|
|
free(wire);
|
|
return sent;
|
|
}
|
|
|
|
static bool runtime_test_raw_application_send(cbm_daemon_ipc_connection_t *connection,
|
|
const void *payload, uint32_t payload_length,
|
|
uint32_t declared_length) {
|
|
static atomic_uint_fast64_t next_token = ATOMIC_VAR_INIT(1);
|
|
uint64_t request_token = atomic_fetch_add_explicit(&next_token, 1, memory_order_relaxed);
|
|
return runtime_test_raw_application_send_token(connection, request_token, payload,
|
|
payload_length, declared_length);
|
|
}
|
|
|
|
static bool runtime_test_raw_application_cancel(cbm_daemon_ipc_connection_t *connection,
|
|
uint64_t request_token) {
|
|
uint8_t wire[8];
|
|
if (!connection || request_token == 0) {
|
|
return false;
|
|
}
|
|
runtime_test_put_u64(wire, request_token);
|
|
return cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST,
|
|
CBM_DAEMON_RUNTIME_OP_APPLICATION_CANCEL, wire,
|
|
(uint32_t)sizeof(wire));
|
|
}
|
|
|
|
static bool runtime_test_raw_application_receive_status_token(
|
|
cbm_daemon_ipc_connection_t *connection, uint64_t expected_token,
|
|
cbm_daemon_runtime_application_status_t expected_status) {
|
|
cbm_daemon_frame_t frame = {0};
|
|
uint8_t *payload = NULL;
|
|
int received =
|
|
cbm_daemon_ipc_receive_frame(connection, RUNTIME_TEST_TIMEOUT_MS, &frame, &payload);
|
|
bool valid = received == 1 && frame.type == CBM_DAEMON_FRAME_RESPONSE &&
|
|
frame.flags == CBM_DAEMON_RUNTIME_OP_APPLICATION_REQUEST && frame.length >= 16 &&
|
|
payload && runtime_test_get_u64(payload) == expected_token &&
|
|
runtime_test_get_u32(payload + 8) == (uint32_t)expected_status &&
|
|
runtime_test_get_u32(payload + 12) == frame.length - 16;
|
|
free(payload);
|
|
return valid;
|
|
}
|
|
|
|
static bool runtime_test_raw_application_receive_status(
|
|
cbm_daemon_ipc_connection_t *connection,
|
|
cbm_daemon_runtime_application_status_t expected_status) {
|
|
cbm_daemon_frame_t frame = {0};
|
|
uint8_t *payload = NULL;
|
|
int received =
|
|
cbm_daemon_ipc_receive_frame(connection, RUNTIME_TEST_TIMEOUT_MS, &frame, &payload);
|
|
bool valid = received == 1 && frame.type == CBM_DAEMON_FRAME_RESPONSE &&
|
|
frame.flags == CBM_DAEMON_RUNTIME_OP_APPLICATION_REQUEST && frame.length >= 16 &&
|
|
payload && runtime_test_get_u64(payload) != 0 &&
|
|
runtime_test_get_u32(payload + 8) == (uint32_t)expected_status &&
|
|
runtime_test_get_u32(payload + 12) == frame.length - 16;
|
|
free(payload);
|
|
return valid;
|
|
}
|
|
|
|
static long long runtime_test_last_os_error(void) {
|
|
#ifdef _WIN32
|
|
return (long long)GetLastError();
|
|
#else
|
|
return (long long)errno;
|
|
#endif
|
|
}
|
|
|
|
static bool runtime_test_fixture_start_failed(const char *tag, const char *stage,
|
|
long long detail) {
|
|
printf(" runtime fixture startup failed: tag=%s stage=%s detail=%lld\n", tag ? tag : "(null)",
|
|
stage ? stage : "(null)", detail);
|
|
return false;
|
|
}
|
|
|
|
static bool runtime_test_fixture_permanent = false;
|
|
|
|
static bool runtime_test_fixture_start_configured(
|
|
runtime_test_fixture_t *fixture, const char *tag, const cbm_daemon_build_identity_t *identity,
|
|
uint32_t max_clients, uint64_t lease_timeout_ms,
|
|
const cbm_daemon_runtime_application_callbacks_t *application) {
|
|
memset(fixture, 0, sizeof(*fixture));
|
|
if (!th_secure_runtime_parent_new(fixture->parent, sizeof(fixture->parent), tag)) {
|
|
return runtime_test_fixture_start_failed(tag, "temporary-directory",
|
|
runtime_test_last_os_error());
|
|
}
|
|
|
|
if (!cbm_daemon_rendezvous_key(fixture->key)) {
|
|
return runtime_test_fixture_start_failed(tag, "rendezvous-key",
|
|
runtime_test_last_os_error());
|
|
}
|
|
fixture->endpoint = cbm_daemon_ipc_endpoint_new(fixture->key, fixture->parent);
|
|
if (!fixture->endpoint) {
|
|
return runtime_test_fixture_start_failed(tag, "endpoint", runtime_test_last_os_error());
|
|
}
|
|
if (!runtime_test_copy_path(fixture->runtime_dir,
|
|
cbm_daemon_ipc_endpoint_runtime_dir(fixture->endpoint))) {
|
|
return runtime_test_fixture_start_failed(tag, "runtime-path", 0);
|
|
}
|
|
|
|
int log_written = snprintf(fixture->log_path, sizeof(fixture->log_path), "%s/conflicts.ndjson",
|
|
fixture->parent);
|
|
int rotated_written = snprintf(fixture->rotated_log_path, sizeof(fixture->rotated_log_path),
|
|
"%s.1", fixture->log_path);
|
|
int lock_written = snprintf(fixture->lock_log_path, sizeof(fixture->lock_log_path), "%s.lock",
|
|
fixture->log_path);
|
|
if (log_written <= 0 || log_written >= (int)sizeof(fixture->log_path)) {
|
|
return runtime_test_fixture_start_failed(tag, "conflict-log-path", log_written);
|
|
}
|
|
if (rotated_written <= 0 || rotated_written >= (int)sizeof(fixture->rotated_log_path)) {
|
|
return runtime_test_fixture_start_failed(tag, "rotated-log-path", rotated_written);
|
|
}
|
|
if (lock_written <= 0 || lock_written >= (int)sizeof(fixture->lock_log_path)) {
|
|
return runtime_test_fixture_start_failed(tag, "lock-log-path", lock_written);
|
|
}
|
|
|
|
cbm_daemon_runtime_service_config_t config = {
|
|
.endpoint = fixture->endpoint,
|
|
.identity = *identity,
|
|
.conflict_log_path = fixture->log_path,
|
|
.conflict_log_cap_bytes = 64U * 1024U,
|
|
.max_clients = max_clients,
|
|
.lease_timeout_ms = lease_timeout_ms,
|
|
.request_timeout_ms = RUNTIME_TEST_TIMEOUT_MS,
|
|
.shutdown_timeout_ms = RUNTIME_TEST_TIMEOUT_MS,
|
|
/* Default false: every teardown-latency fixture depends on prompt
|
|
* last-client-exit. Only the permanent-lifecycle tests flip this. */
|
|
.permanent = runtime_test_fixture_permanent,
|
|
};
|
|
if (application) {
|
|
config.application = *application;
|
|
}
|
|
fixture->service = cbm_daemon_runtime_service_start(&config);
|
|
if (!fixture->service) {
|
|
return runtime_test_fixture_start_failed(tag, "service-start",
|
|
runtime_test_last_os_error());
|
|
}
|
|
cbm_daemon_runtime_service_state_t state = cbm_daemon_runtime_service_state(fixture->service);
|
|
if (state != CBM_DAEMON_RUNTIME_SERVICE_RUNNING) {
|
|
return runtime_test_fixture_start_failed(tag, "service-state", (long long)state);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool runtime_test_fixture_start_limited(runtime_test_fixture_t *fixture, const char *tag,
|
|
const cbm_daemon_build_identity_t *identity,
|
|
uint32_t max_clients) {
|
|
return runtime_test_fixture_start_configured(fixture, tag, identity, max_clients, 5000, NULL);
|
|
}
|
|
|
|
static bool runtime_test_fixture_start(runtime_test_fixture_t *fixture, const char *tag,
|
|
const cbm_daemon_build_identity_t *identity) {
|
|
return runtime_test_fixture_start_limited(fixture, tag, identity, 8);
|
|
}
|
|
|
|
static void runtime_test_fixture_finish(runtime_test_fixture_t *fixture);
|
|
|
|
/* The public convenience constructor participates in the migration guard for
|
|
* the service's complete lifetime. free must refuse a running service without
|
|
* consuming retry authority, then prove participant release after stop. */
|
|
TEST(daemon_runtime_convenience_service_owns_participant_guard) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "participant", &identity);
|
|
int active = started ? cbm_daemon_ipc_legacy_generation_probe(fixture.endpoint) : -1;
|
|
bool running_free_refused = started && !cbm_daemon_runtime_service_free(fixture.service);
|
|
bool stopped =
|
|
started && cbm_daemon_runtime_service_stop(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
bool freed = stopped && cbm_daemon_runtime_service_free(fixture.service);
|
|
if (freed) {
|
|
fixture.service = NULL;
|
|
}
|
|
int released = freed ? cbm_daemon_ipc_legacy_generation_probe(fixture.endpoint) : -1;
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_EQ(active, 1);
|
|
ASSERT_TRUE(running_free_refused);
|
|
ASSERT_TRUE(stopped);
|
|
ASSERT_TRUE(freed);
|
|
ASSERT_EQ(released, 0);
|
|
PASS();
|
|
}
|
|
|
|
static bool runtime_test_fixture_start_application(runtime_test_fixture_t *fixture, const char *tag,
|
|
const cbm_daemon_build_identity_t *identity,
|
|
runtime_application_context_t *context) {
|
|
cbm_daemon_runtime_application_callbacks_t application = runtime_application_callbacks(context);
|
|
return runtime_test_fixture_start_configured(fixture, tag, identity, 8, 5000, &application);
|
|
}
|
|
|
|
static void runtime_test_fixture_finish(runtime_test_fixture_t *fixture) {
|
|
if (fixture->service) {
|
|
cbm_daemon_runtime_service_state_t state =
|
|
cbm_daemon_runtime_service_state(fixture->service);
|
|
bool stopped =
|
|
state == CBM_DAEMON_RUNTIME_SERVICE_EXITED ||
|
|
cbm_daemon_runtime_service_stop(fixture->service, RUNTIME_TEST_CLEANUP_TIMEOUT_MS);
|
|
bool freed = false;
|
|
for (size_t attempt = 0; stopped && !freed && attempt < RUNTIME_TEST_CLEANUP_FREE_ATTEMPTS;
|
|
attempt++) {
|
|
freed = cbm_daemon_runtime_service_free(fixture->service);
|
|
if (!freed) {
|
|
cbm_usleep(1000);
|
|
}
|
|
}
|
|
if (!freed) {
|
|
fprintf(stderr, "daemon_runtime fixture teardown failed\n");
|
|
abort();
|
|
}
|
|
fixture->service = NULL;
|
|
}
|
|
cbm_daemon_ipc_endpoint_free(fixture->endpoint);
|
|
(void)cbm_unlink(fixture->rotated_log_path);
|
|
(void)cbm_unlink(fixture->log_path);
|
|
(void)cbm_unlink(fixture->lock_log_path);
|
|
(void)cbm_rmdir(fixture->runtime_dir);
|
|
(void)cbm_rmdir(fixture->parent);
|
|
memset(fixture, 0, sizeof(*fixture));
|
|
}
|
|
|
|
static bool runtime_test_read_log(const char *path, char out[RUNTIME_TEST_LOG_CAP]) {
|
|
FILE *file = cbm_fopen(path, "rb");
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
size_t used = fread(out, 1, RUNTIME_TEST_LOG_CAP - 1, file);
|
|
bool complete = !ferror(file) && feof(file);
|
|
out[used] = '\0';
|
|
(void)fclose(file);
|
|
return complete;
|
|
}
|
|
|
|
/* A detached daemon loses its inherited stderr by design. Failures before the
|
|
* runtime listener exists must therefore reach the owner-private operation log
|
|
* or users and smoke tests see only a generic bootstrap timeout. An existing
|
|
* daemon claim deterministically fails before runtime startup on POSIX. The
|
|
* Windows same-process participant guard rejects the nested host one stage
|
|
* earlier, and that platform-correct refusal must be equally durable. */
|
|
TEST(daemon_host_early_coordination_failure_is_durable) {
|
|
const char *old_cache = getenv("CBM_CACHE_DIR");
|
|
bool had_cache = old_cache != NULL;
|
|
char *saved_cache = old_cache ? cbm_strdup(old_cache) : NULL;
|
|
bool snapshot_ok = !had_cache || saved_cache;
|
|
|
|
char parent[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char cache[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char log_path[RUNTIME_TEST_PATH_CAP] = {0};
|
|
bool parent_created =
|
|
snapshot_ok && th_secure_runtime_parent_new(parent, sizeof(parent), "host-early-log");
|
|
int cache_written = parent_created ? snprintf(cache, sizeof(cache), "%s/cache", parent) : -1;
|
|
int log_written =
|
|
parent_created ? snprintf(log_path, sizeof(log_path), "%s/logs/cbm-daemon.log", cache) : -1;
|
|
bool environment_ready = cache_written > 0 && cache_written < (int)sizeof(cache) &&
|
|
log_written > 0 && log_written < (int)sizeof(log_path) &&
|
|
cbm_mkdir_p(cache, 0700) && cbm_setenv("CBM_CACHE_DIR", cache, 1) == 0;
|
|
cbm_daemon_ipc_endpoint_t *endpoint =
|
|
environment_ready ? cbm_daemon_ipc_endpoint_new("0123456789abcdef", parent) : NULL;
|
|
cbm_version_cohort_manager_t *active_manager =
|
|
endpoint ? cbm_version_cohort_manager_new(endpoint) : NULL;
|
|
cbm_version_cohort_lease_t *active_lease = NULL;
|
|
cbm_version_cohort_daemon_claim_t *active_claim = NULL;
|
|
cbm_daemon_conflict_t active_conflict;
|
|
cbm_daemon_build_identity_t active_identity =
|
|
runtime_test_identity("active-host", runtime_test_self_build());
|
|
active_identity.cache_fingerprint = RUNTIME_BUILD_B;
|
|
cbm_version_cohort_status_t active_status =
|
|
active_manager ? cbm_version_cohort_acquire(active_manager, &active_identity, UINT64_MAX,
|
|
&active_lease, &active_conflict)
|
|
: CBM_VERSION_COHORT_IO;
|
|
cbm_version_cohort_status_t claim_status =
|
|
active_status == CBM_VERSION_COHORT_OK
|
|
? cbm_version_cohort_daemon_claim_acquire(active_manager, &active_claim)
|
|
: CBM_VERSION_COHORT_IO;
|
|
atomic_int stop_requested = ATOMIC_VAR_INIT(0);
|
|
cbm_daemon_host_config_t config = {
|
|
.endpoint = endpoint,
|
|
.identity = active_identity,
|
|
.executable_path = "/host-early-log-test",
|
|
.stop_requested = &stop_requested,
|
|
};
|
|
int run_result = claim_status == CBM_VERSION_COHORT_OK ? cbm_daemon_host_run(&config) : 0;
|
|
|
|
char log[RUNTIME_TEST_LOG_CAP] = {0};
|
|
bool log_read = runtime_test_read_log(log_path, log);
|
|
bool durable_component = false;
|
|
#ifdef _WIN32
|
|
durable_component = strstr(log, "participant") != NULL;
|
|
#else
|
|
durable_component = strstr(log, "claim") != NULL;
|
|
#endif
|
|
bool durable = log_read && strstr(log, "daemon.start_failed") != NULL && durable_component;
|
|
bool endpoint_created = endpoint != NULL;
|
|
while (active_claim &&
|
|
cbm_version_cohort_daemon_claim_release(&active_claim) != CBM_PRIVATE_FILE_LOCK_OK) {
|
|
cbm_usleep(1000);
|
|
}
|
|
while (active_lease &&
|
|
cbm_version_cohort_lease_release(&active_lease) != CBM_PRIVATE_FILE_LOCK_OK) {
|
|
cbm_usleep(1000);
|
|
}
|
|
while (active_manager &&
|
|
cbm_version_cohort_manager_free(&active_manager) != CBM_PRIVATE_FILE_LOCK_OK) {
|
|
cbm_usleep(1000);
|
|
}
|
|
cbm_daemon_ipc_endpoint_free(endpoint);
|
|
runtime_test_restore_environment("CBM_CACHE_DIR", saved_cache, had_cache);
|
|
free(saved_cache);
|
|
bool cleaned = !parent_created || th_rmtree(parent) == 0;
|
|
|
|
ASSERT_TRUE(snapshot_ok);
|
|
ASSERT_TRUE(parent_created);
|
|
ASSERT_TRUE(environment_ready);
|
|
ASSERT_TRUE(endpoint_created);
|
|
ASSERT_EQ(active_status, CBM_VERSION_COHORT_OK);
|
|
ASSERT_EQ(claim_status, CBM_VERSION_COHORT_OK);
|
|
ASSERT_EQ(run_result, -1);
|
|
ASSERT_TRUE(durable);
|
|
ASSERT_TRUE(cleaned);
|
|
PASS();
|
|
}
|
|
|
|
/* RED on the former host preparation contract: a failed _config.db open was
|
|
* silently converted into default settings, so startup continued and could
|
|
* enable background behavior the user had explicitly disabled. */
|
|
TEST(daemon_host_refuses_unopenable_runtime_config_database) {
|
|
const char *old_cache = getenv("CBM_CACHE_DIR");
|
|
bool had_cache = old_cache != NULL;
|
|
char *saved_cache = old_cache ? cbm_strdup(old_cache) : NULL;
|
|
bool snapshot_ok = !had_cache || saved_cache;
|
|
|
|
char parent[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char cache[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char config_blocker[RUNTIME_TEST_PATH_CAP] = {0};
|
|
bool parent_created =
|
|
snapshot_ok && th_secure_runtime_parent_new(parent, sizeof(parent), "host-config");
|
|
int cache_written = parent_created ? snprintf(cache, sizeof(cache), "%s/cache", parent) : -1;
|
|
int blocker_written =
|
|
parent_created ? snprintf(config_blocker, sizeof(config_blocker), "%s/_config.db", cache)
|
|
: -1;
|
|
bool blocker_created = cache_written > 0 && cache_written < (int)sizeof(cache) &&
|
|
blocker_written > 0 && blocker_written < (int)sizeof(config_blocker) &&
|
|
cbm_mkdir_p(cache, 0700) && cbm_mkdir_p(config_blocker, 0700);
|
|
bool environment_ready = blocker_created && cbm_setenv("CBM_CACHE_DIR", cache, 1) == 0;
|
|
cbm_daemon_ipc_endpoint_t *endpoint =
|
|
environment_ready ? cbm_daemon_ipc_endpoint_new("0123456789abcdef", parent) : NULL;
|
|
bool endpoint_created = endpoint != NULL;
|
|
bool prepared = endpoint && cbm_daemon_host_state_prepare_for_test(endpoint);
|
|
|
|
cbm_daemon_ipc_endpoint_free(endpoint);
|
|
runtime_test_restore_environment("CBM_CACHE_DIR", saved_cache, had_cache);
|
|
free(saved_cache);
|
|
bool cleaned = !parent_created || th_rmtree(parent) == 0;
|
|
|
|
ASSERT_TRUE(snapshot_ok);
|
|
ASSERT_TRUE(parent_created);
|
|
ASSERT_TRUE(blocker_created);
|
|
ASSERT_TRUE(environment_ready);
|
|
ASSERT_TRUE(endpoint_created);
|
|
ASSERT_FALSE(prepared);
|
|
ASSERT_TRUE(cleaned);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_host_http_reconcile_rate_limits_and_retries_transient_failures) {
|
|
const uint64_t timestamps[] = {
|
|
0, 100, 999, 1000, 1500, 2000, 2999, 3000,
|
|
};
|
|
cbm_daemon_host_http_reconcile_test_result_t result = {0};
|
|
bool driven = cbm_daemon_host_http_reconcile_sequence_for_test(
|
|
timestamps, sizeof(timestamps) / sizeof(timestamps[0]), 1, 1, &result);
|
|
|
|
ASSERT_TRUE(driven);
|
|
ASSERT_EQ(result.config_loads, 4);
|
|
ASSERT_EQ(result.server_create_attempts, 3);
|
|
ASSERT_EQ(result.thread_start_attempts, 2);
|
|
ASSERT_TRUE(result.active_after_sequence);
|
|
ASSERT_EQ(result.largest_scheduled_retry_ms, 2000);
|
|
ASSERT_EQ(result.next_retry_ms, 0);
|
|
ASSERT_EQ(result.server_stops, 1);
|
|
ASSERT_EQ(result.server_frees, 2);
|
|
ASSERT_EQ(result.thread_joins, 1);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_host_http_retry_backoff_is_bounded) {
|
|
const uint64_t timestamps[] = {
|
|
0, 1000, 3000, 7000, 15000, 31000, 61000, 91000,
|
|
};
|
|
cbm_daemon_host_http_reconcile_test_result_t result = {0};
|
|
bool driven = cbm_daemon_host_http_reconcile_sequence_for_test(
|
|
timestamps, sizeof(timestamps) / sizeof(timestamps[0]),
|
|
sizeof(timestamps) / sizeof(timestamps[0]), 0, &result);
|
|
|
|
ASSERT_TRUE(driven);
|
|
ASSERT_EQ(result.config_loads, sizeof(timestamps) / sizeof(timestamps[0]));
|
|
ASSERT_EQ(result.server_create_attempts, sizeof(timestamps) / sizeof(timestamps[0]));
|
|
ASSERT_EQ(result.thread_start_attempts, 0);
|
|
ASSERT_FALSE(result.active_after_sequence);
|
|
ASSERT_EQ(result.largest_scheduled_retry_ms, 30000);
|
|
ASSERT_EQ(result.next_retry_ms, 121000);
|
|
ASSERT_EQ(result.server_stops, 0);
|
|
ASSERT_EQ(result.server_frees, 0);
|
|
ASSERT_EQ(result.thread_joins, 0);
|
|
PASS();
|
|
}
|
|
|
|
/* A server can refuse destruction while an index callback still owns its host
|
|
* context. Reconfiguration must retain that server and retry its retirement
|
|
* even if the desired port reverts to the cached value before the next poll;
|
|
* only successful retirement permits a replacement. */
|
|
TEST(daemon_host_http_reconcile_retains_busy_server_until_free_succeeds) {
|
|
cbm_daemon_host_http_free_refusal_test_result_t result = {0};
|
|
bool driven = cbm_daemon_host_http_reconcile_free_refusal_for_test(&result);
|
|
|
|
ASSERT_TRUE(driven);
|
|
ASSERT_TRUE(result.retained_after_refusal);
|
|
ASSERT_EQ(result.server_create_attempts_after_refusal, 1);
|
|
ASSERT_TRUE(result.replacement_active_after_retry);
|
|
ASSERT_EQ(result.server_create_attempts, 2);
|
|
ASSERT_EQ(result.thread_start_attempts, 2);
|
|
ASSERT_EQ(result.server_stops, 1);
|
|
ASSERT_EQ(result.server_free_attempts, 2);
|
|
ASSERT_EQ(result.thread_joins, 1);
|
|
PASS();
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
static int runtime_test_failed_host_child(const char *parent, const char *key) {
|
|
char cache[RUNTIME_TEST_PATH_CAP];
|
|
char log_path[RUNTIME_TEST_PATH_CAP];
|
|
int cache_written = snprintf(cache, sizeof(cache), "%s/cache", parent);
|
|
int log_written = snprintf(log_path, sizeof(log_path), "%s/logs/cbm-daemon.log", cache);
|
|
if (cache_written <= 0 || cache_written >= (int)sizeof(cache) || log_written <= 0 ||
|
|
log_written >= (int)sizeof(log_path) || cbm_setenv("CBM_CACHE_DIR", cache, 1) != 0) {
|
|
return 40;
|
|
}
|
|
|
|
cbm_daemon_ipc_endpoint_t *endpoint = cbm_daemon_ipc_endpoint_new(key, parent);
|
|
atomic_int stop_requested = ATOMIC_VAR_INIT(0);
|
|
cbm_daemon_build_identity_t mismatched =
|
|
runtime_test_identity("host-order-test", RUNTIME_BUILD_B);
|
|
mismatched.cache_fingerprint = RUNTIME_CACHE_A;
|
|
cbm_daemon_host_config_t config = {
|
|
.endpoint = endpoint,
|
|
.identity = mismatched,
|
|
.executable_path = "/host-order-test",
|
|
.stop_requested = &stop_requested,
|
|
};
|
|
int run_result = endpoint ? cbm_daemon_host_run(&config) : 0;
|
|
|
|
char log[RUNTIME_TEST_LOG_CAP] = {0};
|
|
bool read = runtime_test_read_log(log_path, log);
|
|
bool failed_at_runtime =
|
|
read && strstr(log, "daemon.start_failed") != NULL && strstr(log, "runtime") != NULL;
|
|
bool watcher_started = read && strstr(log, "watcher.start") != NULL;
|
|
cbm_daemon_ipc_endpoint_free(endpoint);
|
|
if (run_result != -1 || !failed_at_runtime) {
|
|
return 41;
|
|
}
|
|
return watcher_started ? 42 : 0;
|
|
}
|
|
|
|
/* RED on the former host order: host_state_start launched the watcher before
|
|
* runtime fingerprint validation/listener reservation, so the isolated log
|
|
* always contained watcher.start even though no daemon could serve a client. */
|
|
TEST(daemon_host_failed_listener_reservation_starts_no_background_work) {
|
|
char parent[RUNTIME_TEST_PATH_CAP];
|
|
int written = snprintf(parent, sizeof(parent), "%s/cbm-host-order-XXXXXX", cbm_tmpdir());
|
|
bool parent_created =
|
|
written > 0 && written < (int)sizeof(parent) && cbm_mkdtemp(parent) != NULL;
|
|
char key[CBM_DAEMON_KEY_SIZE] = {0};
|
|
cbm_daemon_ipc_endpoint_t *endpoint = parent_created && cbm_daemon_rendezvous_key(key)
|
|
? cbm_daemon_ipc_endpoint_new(key, parent)
|
|
: NULL;
|
|
cbm_daemon_ipc_startup_lock_t *startup = NULL;
|
|
bool setup = endpoint && cbm_daemon_ipc_startup_lock_try_acquire(endpoint, &startup) == 1 &&
|
|
cbm_daemon_ipc_startup_lock_prepare_handoff(startup);
|
|
pid_t child = setup ? fork() : -1;
|
|
if (child == 0) {
|
|
_exit(runtime_test_failed_host_child(parent, key));
|
|
}
|
|
|
|
int status = 0;
|
|
bool waited = child > 0 && waitpid(child, &status, 0) == child;
|
|
bool startup_released = cbm_daemon_ipc_startup_lock_release(&startup);
|
|
cbm_daemon_ipc_endpoint_free(endpoint);
|
|
th_cleanup(parent_created ? parent : NULL);
|
|
|
|
ASSERT_TRUE(setup);
|
|
ASSERT_TRUE(child > 0);
|
|
ASSERT_TRUE(waited);
|
|
ASSERT_TRUE(startup_released);
|
|
ASSERT_TRUE(WIFEXITED(status));
|
|
ASSERT_EQ(WEXITSTATUS(status), 0);
|
|
PASS();
|
|
}
|
|
|
|
static _Noreturn void runtime_test_forced_host_shutdown_child(const char *parent) {
|
|
char cache[RUNTIME_TEST_PATH_CAP];
|
|
int written = snprintf(cache, sizeof(cache), "%s/cache", parent);
|
|
if (written <= 0 || written >= (int)sizeof(cache) ||
|
|
cbm_setenv("CBM_CACHE_DIR", cache, 1) != 0) {
|
|
_exit(80);
|
|
}
|
|
(void)alarm(2);
|
|
cbm_daemon_host_force_terminate_for_test("noncooperative_callback");
|
|
}
|
|
|
|
static bool runtime_test_cleanup_release_never_succeeds(void *context) {
|
|
(void)context;
|
|
return false;
|
|
}
|
|
|
|
static _Noreturn void runtime_test_persistent_cleanup_failure_child(const char *parent) {
|
|
char cache[RUNTIME_TEST_PATH_CAP];
|
|
int written = snprintf(cache, sizeof(cache), "%s/cache", parent);
|
|
if (written <= 0 || written >= (int)sizeof(cache) ||
|
|
cbm_setenv("CBM_CACHE_DIR", cache, 1) != 0) {
|
|
_exit(81);
|
|
}
|
|
cbm_daemon_host_cleanup_release_until_complete_for_test(
|
|
runtime_test_cleanup_release_never_succeeds, NULL);
|
|
/* Continuing after an unreleased native coordination claim would let the
|
|
* daemon report a clean stop that never actually completed. */
|
|
_exit(82);
|
|
}
|
|
|
|
/* Native unlock/close failures are retryable, but a persistent OS error must
|
|
* not turn daemon shutdown into an immortal process. The parent polls with a
|
|
* hard deadline and always kills/reaps a regressed child so this test itself
|
|
* can fail without hanging the rest of the suite. */
|
|
TEST(daemon_host_persistent_cleanup_release_failure_is_process_bounded) {
|
|
char parent[RUNTIME_TEST_PATH_CAP];
|
|
char log_path[RUNTIME_TEST_PATH_CAP];
|
|
int parent_written =
|
|
snprintf(parent, sizeof(parent), "%s/cbm-host-cleanup-XXXXXX", cbm_tmpdir());
|
|
bool parent_created =
|
|
parent_written > 0 && parent_written < (int)sizeof(parent) && cbm_mkdtemp(parent) != NULL;
|
|
int log_written = parent_created ? snprintf(log_path, sizeof(log_path),
|
|
"%s/cache/logs/cbm-daemon.log", parent)
|
|
: -1;
|
|
bool path_ok = log_written > 0 && log_written < (int)sizeof(log_path);
|
|
pid_t child = path_ok ? fork() : -1;
|
|
if (child == 0) {
|
|
runtime_test_persistent_cleanup_failure_child(parent);
|
|
}
|
|
|
|
int status = 0;
|
|
bool completed = false;
|
|
uint64_t deadline = cbm_now_ms() + RUNTIME_TEST_TIMEOUT_MS;
|
|
while (child > 0 && cbm_now_ms() < deadline) {
|
|
pid_t waited = waitpid(child, &status, WNOHANG);
|
|
if (waited == child) {
|
|
completed = true;
|
|
break;
|
|
}
|
|
if (waited < 0 && errno != EINTR) {
|
|
break;
|
|
}
|
|
cbm_usleep(1000);
|
|
}
|
|
if (child > 0 && !completed) {
|
|
(void)kill(child, SIGKILL);
|
|
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {}
|
|
}
|
|
|
|
char log[RUNTIME_TEST_LOG_CAP] = {0};
|
|
bool durable = completed && runtime_test_read_log(log_path, log) &&
|
|
strstr(log, "daemon.forced_shutdown") != NULL &&
|
|
strstr(log, "coordination_cleanup") != NULL;
|
|
th_cleanup(parent_created ? parent : NULL);
|
|
|
|
ASSERT_TRUE(parent_created);
|
|
ASSERT_TRUE(path_ok);
|
|
ASSERT_TRUE(child > 0);
|
|
ASSERT_TRUE(completed);
|
|
ASSERT_TRUE(WIFEXITED(status));
|
|
ASSERT_EQ(WEXITSTATUS(status), EXIT_FAILURE);
|
|
ASSERT_TRUE(durable);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_host_forced_shutdown_is_logged_flushed_and_process_bounded) {
|
|
char parent[RUNTIME_TEST_PATH_CAP];
|
|
char log_path[RUNTIME_TEST_PATH_CAP];
|
|
int parent_written = snprintf(parent, sizeof(parent), "%s/cbm-host-force-XXXXXX", cbm_tmpdir());
|
|
bool parent_created =
|
|
parent_written > 0 && parent_written < (int)sizeof(parent) && cbm_mkdtemp(parent) != NULL;
|
|
int log_written = parent_created ? snprintf(log_path, sizeof(log_path),
|
|
"%s/cache/logs/cbm-daemon.log", parent)
|
|
: -1;
|
|
bool path_ok = log_written > 0 && log_written < (int)sizeof(log_path);
|
|
pid_t child = path_ok ? fork() : -1;
|
|
if (child == 0) {
|
|
runtime_test_forced_host_shutdown_child(parent);
|
|
}
|
|
|
|
int status = 0;
|
|
bool waited = child > 0 && waitpid(child, &status, 0) == child;
|
|
char log[RUNTIME_TEST_LOG_CAP] = {0};
|
|
bool durable = waited && runtime_test_read_log(log_path, log) &&
|
|
strstr(log, "daemon.forced_shutdown") != NULL &&
|
|
strstr(log, "noncooperative_callback") != NULL;
|
|
th_cleanup(parent_created ? parent : NULL);
|
|
|
|
ASSERT_TRUE(parent_created);
|
|
ASSERT_TRUE(path_ok);
|
|
ASSERT_TRUE(child > 0);
|
|
ASSERT_TRUE(waited);
|
|
ASSERT_TRUE(WIFEXITED(status));
|
|
ASSERT_EQ(WEXITSTATUS(status), EXIT_FAILURE);
|
|
ASSERT_TRUE(durable);
|
|
PASS();
|
|
}
|
|
#endif
|
|
|
|
TEST(daemon_runtime_exact_hello_issues_connection_bound_identity) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "exact-hello", &identity);
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
bool accepted = false;
|
|
bool identity_anchored = false;
|
|
bool closed = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
/* Service and client are the same process image here, exercising the
|
|
* native-identity HELLO path rather than copied-image fallback. */
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
if (client) {
|
|
uint64_t expected_pid = runtime_test_process_id();
|
|
accepted = result.status == CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED &&
|
|
result.hello_status == CBM_DAEMON_HELLO_COMPATIBLE &&
|
|
result.client_id != CBM_DAEMON_CLIENT_ID_INVALID &&
|
|
result.client_id == cbm_daemon_runtime_client_id(client);
|
|
identity_anchored = result.authenticated_process_id == expected_pid &&
|
|
cbm_daemon_runtime_client_process_id(client) == expected_pid &&
|
|
cbm_daemon_runtime_service_client_process_id(
|
|
fixture.service, result.client_id) == expected_pid;
|
|
closed = cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
client = NULL;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
if (client) {
|
|
(void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(accepted);
|
|
ASSERT_TRUE(identity_anchored);
|
|
ASSERT_TRUE(closed);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
/* Regression for #1383: an image-verification rejection must be ANSWERED, not
|
|
* silently dropped. The old path logged daemon.client_image_rejected and
|
|
* finished the worker without sending a hello response, so the client sat on
|
|
* "pending" indefinitely - indistinguishable from a slow cold start - with the
|
|
* reason visible only in the daemon log.
|
|
*
|
|
* The rejection is now scoped to fingerprint_mismatch (see #1539 below), so
|
|
* this drives the seam that keeps a peer image readable but DIFFERENT. */
|
|
TEST(daemon_runtime_image_rejection_reaches_client_issue1383) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "image-reject", &identity);
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
|
|
cbm_daemon_runtime_force_peer_image_mismatch_for_testing(true);
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
cbm_daemon_runtime_force_peer_image_mismatch_for_testing(false);
|
|
|
|
bool rejected_with_reason = client == NULL &&
|
|
result.status == CBM_DAEMON_RUNTIME_CONNECT_REJECTED &&
|
|
strstr(result.message, "fingerprint_mismatch") != NULL;
|
|
if (client) {
|
|
(void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(rejected_with_reason);
|
|
PASS();
|
|
}
|
|
|
|
/* #1539: a peer whose image cannot be EXAMINED is not a peer that failed a
|
|
* check — it is a peer we could not look at. Every `npx codebase-memory-mcp`
|
|
* invocation lands here (ephemeral cache path, unfingerprintable), and the old
|
|
* gate rejected all of them: the MCP client saw a 30 s wait and zero bytes.
|
|
* The HELLO exchange that already succeeded proves version, build fingerprint
|
|
* and ABI compatibility, so admission is the honest outcome. */
|
|
TEST(daemon_runtime_unverifiable_image_is_admitted_issue1539) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "image-unverifiable", &identity);
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
|
|
cbm_daemon_runtime_force_peer_image_unverified_for_testing(true);
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
cbm_daemon_runtime_force_peer_image_unverified_for_testing(false);
|
|
|
|
bool admitted = client != NULL && result.status == CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED;
|
|
if (client) {
|
|
admitted = cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS) && admitted;
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(admitted);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_unexpected_frame_payload_is_freed_once) {
|
|
static const uint8_t unexpected_payload[] = {0xde, 0xad, 0xbe, 0xef};
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "unexpected-frame", &identity);
|
|
cbm_daemon_runtime_connect_result_t owner_result = {0};
|
|
cbm_daemon_runtime_client_t *owner = NULL;
|
|
cbm_daemon_ipc_connection_t *raw = NULL;
|
|
bool raw_connected = false;
|
|
bool unexpected_sent = false;
|
|
bool bad_peer_released = false;
|
|
bool bad_peer_closed = false;
|
|
bool owner_survived = false;
|
|
bool owner_closed = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
owner = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &owner_result);
|
|
}
|
|
if (owner) {
|
|
raw = runtime_test_raw_client_connect(fixture.endpoint, &identity);
|
|
raw_connected = raw != NULL;
|
|
}
|
|
if (raw) {
|
|
unexpected_sent = cbm_daemon_ipc_send_frame(
|
|
raw, CBM_DAEMON_FRAME_RESPONSE, CBM_DAEMON_RUNTIME_OP_HEARTBEAT, unexpected_payload,
|
|
(uint32_t)sizeof(unexpected_payload));
|
|
}
|
|
if (unexpected_sent) {
|
|
bad_peer_released = cbm_daemon_runtime_service_wait_for_clients(fixture.service, 1,
|
|
RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (bad_peer_released) {
|
|
cbm_daemon_frame_t frame = {0};
|
|
uint8_t *payload = NULL;
|
|
int received = cbm_daemon_ipc_receive_frame(raw, RUNTIME_TEST_TIMEOUT_MS, &frame, &payload);
|
|
bad_peer_closed = received != 1;
|
|
free(payload);
|
|
owner_survived = cbm_daemon_runtime_client_heartbeat(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
cbm_daemon_ipc_connection_close(raw);
|
|
raw = NULL;
|
|
if (owner) {
|
|
owner_closed = cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
owner = NULL;
|
|
}
|
|
if (started) {
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (owner) {
|
|
(void)cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
cbm_daemon_ipc_connection_close(raw);
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(raw_connected);
|
|
ASSERT_TRUE(unexpected_sent);
|
|
ASSERT_TRUE(bad_peer_released);
|
|
ASSERT_TRUE(bad_peer_closed);
|
|
ASSERT_TRUE(owner_survived);
|
|
ASSERT_TRUE(owner_closed);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_activation_rejects_forged_and_malformed_without_stop) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
cbm_daemon_build_identity_t forged = runtime_test_identity("9.9.9", RUNTIME_BUILD_B);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "activation-reject", &identity);
|
|
cbm_daemon_runtime_connect_result_t owner_result = {0};
|
|
cbm_daemon_runtime_client_t *owner = NULL;
|
|
uint8_t request[RUNTIME_TEST_ACTIVATION_REQUEST_SIZE];
|
|
bool encoded = runtime_test_activation_request_encode(
|
|
request, CBM_DAEMON_RUNTIME_ACTIVATION_UPDATE, &identity);
|
|
cbm_daemon_runtime_activation_result_t short_result = {0};
|
|
cbm_daemon_runtime_activation_result_t action_result = {0};
|
|
cbm_daemon_runtime_activation_result_t abi_result = {0};
|
|
cbm_daemon_runtime_activation_result_t forged_result = {0};
|
|
bool short_rejected = false;
|
|
bool action_rejected = false;
|
|
bool abi_rejected = false;
|
|
bool forged_rejected = false;
|
|
bool unchanged = false;
|
|
bool heartbeat = false;
|
|
bool closed = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
owner = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &owner_result);
|
|
}
|
|
if (owner && encoded) {
|
|
short_rejected = runtime_test_raw_activation_exchange(
|
|
fixture.endpoint, request, RUNTIME_TEST_ACTIVATION_REQUEST_SIZE - 1U,
|
|
&short_result) &&
|
|
!short_result.accepted;
|
|
runtime_test_put_u32(request, UINT32_C(99));
|
|
action_rejected = runtime_test_raw_activation_exchange(fixture.endpoint, request,
|
|
RUNTIME_TEST_ACTIVATION_REQUEST_SIZE,
|
|
&action_result) &&
|
|
!action_result.accepted;
|
|
runtime_test_put_u32(request, (uint32_t)CBM_DAEMON_RUNTIME_ACTIVATION_UPDATE);
|
|
runtime_test_put_u32(request + RUNTIME_TEST_ACTIVATION_IDENTITY_OFFSET,
|
|
RUNTIME_TEST_RENDEZVOUS_ABI + 1U);
|
|
abi_rejected = runtime_test_raw_activation_exchange(fixture.endpoint, request,
|
|
RUNTIME_TEST_ACTIVATION_REQUEST_SIZE,
|
|
&abi_result) &&
|
|
!abi_result.accepted;
|
|
forged_rejected = cbm_daemon_runtime_request_activation_shutdown(
|
|
fixture.endpoint, &forged, CBM_DAEMON_RUNTIME_ACTIVATION_UNINSTALL,
|
|
RUNTIME_TEST_TIMEOUT_MS, &forged_result) &&
|
|
!forged_result.accepted;
|
|
unchanged = cbm_daemon_runtime_service_state(fixture.service) ==
|
|
CBM_DAEMON_RUNTIME_SERVICE_RUNNING &&
|
|
cbm_daemon_runtime_service_active_clients(fixture.service) == 1 &&
|
|
cbm_daemon_runtime_service_wait_for_connections(fixture.service, 1,
|
|
RUNTIME_TEST_TIMEOUT_MS);
|
|
heartbeat = cbm_daemon_runtime_client_heartbeat(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (owner) {
|
|
closed = cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
owner = NULL;
|
|
}
|
|
if (started) {
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_EQ(owner_result.status, CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED);
|
|
ASSERT_TRUE(encoded);
|
|
ASSERT_TRUE(short_rejected);
|
|
ASSERT_TRUE(action_rejected);
|
|
ASSERT_TRUE(abi_rejected);
|
|
ASSERT_TRUE(forged_rejected);
|
|
ASSERT_TRUE(unchanged);
|
|
ASSERT_TRUE(heartbeat);
|
|
ASSERT_TRUE(closed);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_activation_ack_snapshots_then_interrupts_all_clients) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "activation-drain", &identity);
|
|
cbm_daemon_runtime_connect_result_t first_result = {0};
|
|
cbm_daemon_runtime_connect_result_t second_result = {0};
|
|
cbm_daemon_runtime_client_t *first = NULL;
|
|
cbm_daemon_runtime_client_t *second = NULL;
|
|
cbm_daemon_runtime_activation_result_t activation = {0};
|
|
bool requested = false;
|
|
bool first_interrupted = false;
|
|
bool second_interrupted = false;
|
|
bool exited = false;
|
|
atomic_store_explicit(&runtime_activation_shutdown_log_seen, false, memory_order_release);
|
|
cbm_log_set_sink(runtime_test_activation_shutdown_sink);
|
|
|
|
if (started) {
|
|
first = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &first_result);
|
|
second = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &second_result);
|
|
}
|
|
if (first && second) {
|
|
requested = cbm_daemon_runtime_request_activation_shutdown(
|
|
fixture.endpoint, &identity, CBM_DAEMON_RUNTIME_ACTIVATION_UPDATE,
|
|
RUNTIME_TEST_TIMEOUT_MS, &activation);
|
|
first_interrupted = !cbm_daemon_runtime_client_heartbeat(first, RUNTIME_TEST_TIMEOUT_MS);
|
|
second_interrupted = !cbm_daemon_runtime_client_heartbeat(second, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
cbm_log_set_sink(NULL);
|
|
if (first) {
|
|
(void)cbm_daemon_runtime_client_close(first, RUNTIME_TEST_TIMEOUT_MS);
|
|
first = NULL;
|
|
}
|
|
if (second) {
|
|
(void)cbm_daemon_runtime_client_close(second, RUNTIME_TEST_TIMEOUT_MS);
|
|
second = NULL;
|
|
}
|
|
if (started) {
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_EQ(first_result.status, CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED);
|
|
ASSERT_EQ(second_result.status, CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED);
|
|
ASSERT_TRUE(requested);
|
|
ASSERT_TRUE(activation.accepted);
|
|
ASSERT_EQ(activation.active_clients, 2);
|
|
/* The one-shot activation requester is not part of the drain snapshot. */
|
|
ASSERT_EQ(activation.active_connections, 2);
|
|
ASSERT_TRUE(atomic_load_explicit(&runtime_activation_shutdown_log_seen, memory_order_acquire));
|
|
ASSERT_TRUE(first_interrupted);
|
|
ASSERT_TRUE(second_interrupted);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
|
|
TEST(daemon_runtime_activation_accepts_authenticated_different_build) {
|
|
cbm_daemon_build_identity_t active_identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
char directory[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char foreign_image[RUNTIME_TEST_PATH_CAP] = {0};
|
|
int directory_written =
|
|
snprintf(directory, sizeof(directory), "%s/cbm-runtime-activation-XXXXXX", cbm_tmpdir());
|
|
bool directory_created = directory_written > 0 && directory_written < (int)sizeof(directory) &&
|
|
cbm_mkdtemp(directory) != NULL;
|
|
int image_written = directory_created ? snprintf(foreign_image, sizeof(foreign_image),
|
|
"%s/foreign-activation", directory)
|
|
: -1;
|
|
bool copied = image_written > 0 && image_written < (int)sizeof(foreign_image) &&
|
|
runtime_test_copy_self_image(foreign_image);
|
|
#ifdef __APPLE__
|
|
/* Appending an overlay invalidates Mach-O strict validation. A distinct
|
|
* signing identifier changes the executable bytes while keeping the copy
|
|
* runnable, exactly like the mapped-main-image adversarial fixture. */
|
|
bool changed = copied;
|
|
bool runnable = changed && runtime_test_mac_ad_hoc_sign(foreign_image);
|
|
#else
|
|
bool changed = copied && runtime_test_append_image_marker(foreign_image);
|
|
bool runnable = changed;
|
|
#endif
|
|
char foreign_build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
bool fingerprinted =
|
|
runnable && cbm_daemon_build_fingerprint_file(foreign_image, foreign_build);
|
|
bool differs = fingerprinted && strcmp(foreign_build, active_identity.build_fingerprint) != 0;
|
|
cbm_daemon_build_identity_t foreign_identity = runtime_test_identity("9.9.9", foreign_build);
|
|
|
|
/* Finalize the foreign executable before the runtime starts any worker
|
|
* threads. In particular, macOS code signing is an external process; the
|
|
* test must not fork that helper from the live multithreaded daemon. The
|
|
* immutable signed copy remains the actual peer image authenticated by
|
|
* the activation request below. */
|
|
runtime_test_fixture_t fixture;
|
|
memset(&fixture, 0, sizeof(fixture));
|
|
bool started =
|
|
differs && runtime_test_fixture_start(&fixture, "activation-foreign", &active_identity);
|
|
cbm_daemon_runtime_connect_result_t owner_result = {0};
|
|
cbm_daemon_runtime_client_t *owner = NULL;
|
|
if (started) {
|
|
owner = cbm_daemon_runtime_client_connect(fixture.endpoint, &active_identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &owner_result);
|
|
}
|
|
int foreign_exit = -1;
|
|
bool foreign_ran =
|
|
owner && differs &&
|
|
runtime_test_run_activation_image(foreign_image, &fixture, &foreign_identity,
|
|
CBM_DAEMON_RUNTIME_ACTIVATION_INSTALL, &foreign_exit);
|
|
bool owner_interrupted = owner && foreign_ran &&
|
|
!cbm_daemon_runtime_client_heartbeat(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
if (owner) {
|
|
(void)cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
owner = NULL;
|
|
}
|
|
bool exited =
|
|
started && cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
runtime_test_fixture_finish(&fixture);
|
|
if (image_written > 0 && image_written < (int)sizeof(foreign_image)) {
|
|
(void)cbm_unlink(foreign_image);
|
|
}
|
|
if (directory_created) {
|
|
(void)cbm_rmdir(directory);
|
|
}
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_EQ(owner_result.status, CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED);
|
|
ASSERT_TRUE(directory_created);
|
|
ASSERT_TRUE(copied);
|
|
ASSERT_TRUE(changed);
|
|
ASSERT_TRUE(runnable);
|
|
ASSERT_TRUE(fingerprinted);
|
|
ASSERT_TRUE(differs);
|
|
ASSERT_TRUE(foreign_ran);
|
|
ASSERT_EQ(foreign_exit, 0);
|
|
ASSERT_TRUE(owner_interrupted);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
#endif
|
|
|
|
TEST(daemon_runtime_rendezvous_layout_is_frozen_and_detailed_abi_independent) {
|
|
cbm_daemon_build_identity_t first = runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
cbm_daemon_build_identity_t different_detail = first;
|
|
different_detail.protocol_abi = 0;
|
|
different_detail.store_abi = 0;
|
|
different_detail.feature_abi = 0;
|
|
|
|
uint8_t first_wire[CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE];
|
|
uint8_t second_wire[CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE];
|
|
bool first_encoded = cbm_daemon_runtime_hello_request_encode(first_wire, &first);
|
|
bool second_encoded = cbm_daemon_runtime_hello_request_encode(second_wire, &different_detail);
|
|
|
|
ASSERT_TRUE(first_encoded);
|
|
ASSERT_TRUE(second_encoded);
|
|
ASSERT_EQ(CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE, RUNTIME_TEST_RENDEZVOUS_REQUEST_SIZE);
|
|
ASSERT_EQ(CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE, RUNTIME_TEST_RENDEZVOUS_RESPONSE_SIZE);
|
|
ASSERT_EQ(CBM_DAEMON_RENDEZVOUS_VERSION_TEXT_CAP, CBM_DAEMON_VERSION_TEXT_SIZE);
|
|
ASSERT_EQ(CBM_DAEMON_RENDEZVOUS_BUILD_FINGERPRINT_CAP, CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
|
|
ASSERT_EQ(CBM_DAEMON_RENDEZVOUS_MESSAGE_CAP, CBM_DAEMON_CONFLICT_MESSAGE_SIZE);
|
|
ASSERT_EQ(runtime_test_get_u32(first_wire), RUNTIME_TEST_RENDEZVOUS_ABI);
|
|
ASSERT_TRUE(
|
|
runtime_test_fixed_string_equals(first_wire + RUNTIME_TEST_RENDEZVOUS_VERSION_OFFSET,
|
|
CBM_DAEMON_VERSION_TEXT_SIZE, first.semantic_version));
|
|
ASSERT_TRUE(runtime_test_fixed_string_equals(first_wire + RUNTIME_TEST_RENDEZVOUS_BUILD_OFFSET,
|
|
CBM_DAEMON_BUILD_FINGERPRINT_SIZE,
|
|
first.build_fingerprint));
|
|
ASSERT_EQ(memcmp(first_wire, second_wire, sizeof(first_wire)), 0);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_future_generation_gets_stable_explicit_conflict) {
|
|
const char *active_build = runtime_test_self_build();
|
|
cbm_daemon_build_identity_t active = runtime_test_identity("2.4.0", active_build);
|
|
cbm_daemon_build_identity_t future =
|
|
runtime_test_identity("9.0.0-future-wire-v2", RUNTIME_BUILD_B);
|
|
future.protocol_abi = active.protocol_abi + 1000;
|
|
future.store_abi = active.store_abi + 2000;
|
|
future.feature_abi = active.feature_abi + 3000;
|
|
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "future-generation", &active);
|
|
cbm_daemon_runtime_connect_result_t owner_result = {0};
|
|
cbm_daemon_runtime_client_t *owner = NULL;
|
|
cbm_daemon_ipc_connection_t *future_connection = NULL;
|
|
uint8_t future_wire[CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE];
|
|
bool encoded = false;
|
|
bool sent = false;
|
|
bool explicit_conflict = false;
|
|
bool logged = false;
|
|
bool exited = false;
|
|
cbm_daemon_frame_t response_frame = {0};
|
|
uint8_t *response_payload = NULL;
|
|
char log[RUNTIME_TEST_LOG_CAP] = {0};
|
|
char expected_message[CBM_DAEMON_CONFLICT_MESSAGE_SIZE] = {0};
|
|
int expected_length = snprintf(
|
|
expected_message, sizeof(expected_message),
|
|
"CBM could not start because a conflicting CBM process is active "
|
|
"(version; active version %s, build %s; requested version %s, build %s). "
|
|
"Close all CBM sessions and commands, then retry.",
|
|
active.semantic_version, active_build, future.semantic_version, future.build_fingerprint);
|
|
bool expected_message_valid =
|
|
expected_length > 0 && (size_t)expected_length < sizeof(expected_message);
|
|
|
|
if (started) {
|
|
owner = cbm_daemon_runtime_client_connect(fixture.endpoint, &active,
|
|
RUNTIME_TEST_TIMEOUT_MS, &owner_result);
|
|
}
|
|
if (owner) {
|
|
encoded = cbm_daemon_runtime_hello_request_encode(future_wire, &future);
|
|
future_connection = cbm_daemon_ipc_connect(fixture.endpoint, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (future_connection && encoded) {
|
|
/* A future generation sends only the permanent identity envelope here;
|
|
* its detailed runtime layout is negotiated by exact executable build,
|
|
* never added to this stable endpoint message. */
|
|
sent = cbm_daemon_ipc_send_frame(future_connection, CBM_DAEMON_FRAME_REQUEST,
|
|
CBM_DAEMON_RUNTIME_OP_HELLO, future_wire,
|
|
RUNTIME_TEST_RENDEZVOUS_REQUEST_SIZE);
|
|
int received = cbm_daemon_ipc_receive_frame(future_connection, RUNTIME_TEST_TIMEOUT_MS,
|
|
&response_frame, &response_payload);
|
|
explicit_conflict =
|
|
expected_message_valid && received == 1 && response_payload &&
|
|
response_frame.type == CBM_DAEMON_FRAME_RESPONSE &&
|
|
response_frame.flags == CBM_DAEMON_RUNTIME_OP_HELLO &&
|
|
response_frame.length == RUNTIME_TEST_RENDEZVOUS_RESPONSE_SIZE &&
|
|
runtime_test_get_u32(response_payload) == CBM_DAEMON_RUNTIME_CONNECT_CONFLICT &&
|
|
runtime_test_get_u32(response_payload + 4) == CBM_DAEMON_HELLO_VERSION_CONFLICT &&
|
|
runtime_test_fixed_string_equals(
|
|
response_payload + RUNTIME_TEST_RENDEZVOUS_ACTIVE_VERSION_OFFSET,
|
|
CBM_DAEMON_VERSION_TEXT_SIZE, active.semantic_version) &&
|
|
runtime_test_fixed_string_equals(response_payload +
|
|
RUNTIME_TEST_RENDEZVOUS_ACTIVE_BUILD_OFFSET,
|
|
CBM_DAEMON_BUILD_FINGERPRINT_SIZE, active_build) &&
|
|
runtime_test_fixed_string_equals(
|
|
response_payload + RUNTIME_TEST_RENDEZVOUS_REQUESTED_VERSION_OFFSET,
|
|
CBM_DAEMON_VERSION_TEXT_SIZE, future.semantic_version) &&
|
|
runtime_test_fixed_string_equals(
|
|
response_payload + RUNTIME_TEST_RENDEZVOUS_REQUESTED_BUILD_OFFSET,
|
|
CBM_DAEMON_BUILD_FINGERPRINT_SIZE, future.build_fingerprint) &&
|
|
runtime_test_fixed_string_equals(response_payload +
|
|
RUNTIME_TEST_RENDEZVOUS_MESSAGE_OFFSET,
|
|
CBM_DAEMON_CONFLICT_MESSAGE_SIZE, expected_message);
|
|
}
|
|
cbm_daemon_ipc_connection_close(future_connection);
|
|
future_connection = NULL;
|
|
free(response_payload);
|
|
response_payload = NULL;
|
|
|
|
if (owner) {
|
|
logged = runtime_test_read_log(fixture.log_path, log) &&
|
|
strstr(log, "\"reason\":\"version\"") != NULL &&
|
|
strstr(log, active_build) != NULL && strstr(log, RUNTIME_BUILD_B) != NULL;
|
|
(void)cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
owner = NULL;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
cbm_daemon_ipc_connection_close(future_connection);
|
|
free(response_payload);
|
|
if (owner) {
|
|
(void)cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(encoded);
|
|
ASSERT_TRUE(sent);
|
|
ASSERT_TRUE(explicit_conflict);
|
|
ASSERT_TRUE(logged);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_matching_clients_share_one_service_endpoint) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "shared-endpoint", &identity);
|
|
cbm_daemon_ipc_endpoint_t *same_endpoint = NULL;
|
|
cbm_daemon_runtime_client_t *first = NULL;
|
|
cbm_daemon_runtime_client_t *second = NULL;
|
|
cbm_daemon_runtime_connect_result_t first_result = {0};
|
|
cbm_daemon_runtime_connect_result_t second_result = {0};
|
|
bool same_address = false;
|
|
bool both_registered = false;
|
|
bool one_remains = false;
|
|
bool exited = false;
|
|
|
|
char key[CBM_DAEMON_KEY_SIZE];
|
|
if (started && cbm_daemon_rendezvous_key(key)) {
|
|
same_endpoint = cbm_daemon_ipc_endpoint_new(key, fixture.parent);
|
|
}
|
|
if (same_endpoint) {
|
|
same_address = strcmp(cbm_daemon_ipc_endpoint_address(fixture.endpoint),
|
|
cbm_daemon_ipc_endpoint_address(same_endpoint)) == 0;
|
|
first = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &first_result);
|
|
second = cbm_daemon_runtime_client_connect(same_endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &second_result);
|
|
}
|
|
if (first && second) {
|
|
both_registered = first_result.client_id != second_result.client_id &&
|
|
cbm_daemon_runtime_service_active_clients(fixture.service) == 2;
|
|
(void)cbm_daemon_runtime_client_close(first, RUNTIME_TEST_TIMEOUT_MS);
|
|
first = NULL;
|
|
one_remains = cbm_daemon_runtime_service_wait_for_clients(fixture.service, 1,
|
|
RUNTIME_TEST_TIMEOUT_MS);
|
|
(void)cbm_daemon_runtime_client_close(second, RUNTIME_TEST_TIMEOUT_MS);
|
|
second = NULL;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
if (second) {
|
|
(void)cbm_daemon_runtime_client_close(second, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (first) {
|
|
(void)cbm_daemon_runtime_client_close(first, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
cbm_daemon_ipc_endpoint_free(same_endpoint);
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(same_address);
|
|
ASSERT_TRUE(both_registered);
|
|
ASSERT_TRUE(one_remains);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_same_version_different_build_is_visible_and_logged) {
|
|
const char *active_build = runtime_test_self_build();
|
|
cbm_daemon_build_identity_t active = runtime_test_identity("2.4.0", active_build);
|
|
cbm_daemon_build_identity_t rebuilt = runtime_test_identity("2.4.0", RUNTIME_BUILD_B);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "build-conflict", &active);
|
|
cbm_daemon_runtime_connect_result_t owner_result = {0};
|
|
cbm_daemon_runtime_connect_result_t conflict_result = {0};
|
|
cbm_daemon_runtime_client_t *owner = NULL;
|
|
cbm_daemon_runtime_client_t *rejected = NULL;
|
|
bool explicit_conflict = false;
|
|
bool logged = false;
|
|
bool owner_unchanged = false;
|
|
bool exited = false;
|
|
char log[RUNTIME_TEST_LOG_CAP] = {0};
|
|
|
|
if (started) {
|
|
owner = cbm_daemon_runtime_client_connect(fixture.endpoint, &active,
|
|
RUNTIME_TEST_TIMEOUT_MS, &owner_result);
|
|
}
|
|
if (owner) {
|
|
rejected = cbm_daemon_runtime_client_connect(fixture.endpoint, &rebuilt,
|
|
RUNTIME_TEST_TIMEOUT_MS, &conflict_result);
|
|
explicit_conflict = rejected == NULL &&
|
|
conflict_result.status == CBM_DAEMON_RUNTIME_CONNECT_CONFLICT &&
|
|
conflict_result.hello_status == CBM_DAEMON_HELLO_BUILD_CONFLICT &&
|
|
strstr(conflict_result.message, "could not start") != NULL &&
|
|
strstr(conflict_result.message, active_build) != NULL &&
|
|
strstr(conflict_result.message, RUNTIME_BUILD_B) != NULL;
|
|
owner_unchanged = cbm_daemon_runtime_service_active_clients(fixture.service) == 1;
|
|
logged = runtime_test_read_log(fixture.log_path, log) &&
|
|
strstr(log, "daemon.version_conflict") != NULL &&
|
|
strstr(log, "\"reason\":\"build\"") != NULL && strstr(log, active_build) != NULL &&
|
|
strstr(log, RUNTIME_BUILD_B) != NULL;
|
|
(void)cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
owner = NULL;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
if (rejected) {
|
|
(void)cbm_daemon_runtime_client_close(rejected, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (owner) {
|
|
(void)cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(explicit_conflict);
|
|
ASSERT_TRUE(owner_unchanged);
|
|
ASSERT_TRUE(logged);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_conflict_log_failure_uses_operation_log_fallback) {
|
|
const char *active_build = runtime_test_self_build();
|
|
cbm_daemon_build_identity_t active = runtime_test_identity("2.4.0", active_build);
|
|
cbm_daemon_build_identity_t rebuilt = runtime_test_identity("2.4.0", RUNTIME_BUILD_B);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "conflict-log-fallback", &active);
|
|
bool obstruction_created = started && cbm_mkdir_p(fixture.log_path, 0700);
|
|
cbm_daemon_runtime_connect_result_t owner_result = {0};
|
|
cbm_daemon_runtime_connect_result_t conflict_result = {0};
|
|
cbm_daemon_runtime_client_t *owner = NULL;
|
|
cbm_daemon_runtime_client_t *rejected = NULL;
|
|
CBMLogLevel prior_level = cbm_log_get_level();
|
|
bool explicit_conflict = false;
|
|
bool exited = false;
|
|
|
|
atomic_store_explicit(&runtime_conflict_log_fallback_seen, false, memory_order_release);
|
|
if (obstruction_created) {
|
|
cbm_log_set_level(CBM_LOG_ERROR);
|
|
cbm_log_set_sink(runtime_test_conflict_log_fallback_sink);
|
|
owner = cbm_daemon_runtime_client_connect(fixture.endpoint, &active,
|
|
RUNTIME_TEST_TIMEOUT_MS, &owner_result);
|
|
}
|
|
if (owner) {
|
|
rejected = cbm_daemon_runtime_client_connect(fixture.endpoint, &rebuilt,
|
|
RUNTIME_TEST_TIMEOUT_MS, &conflict_result);
|
|
explicit_conflict = rejected == NULL &&
|
|
conflict_result.status == CBM_DAEMON_RUNTIME_CONNECT_CONFLICT &&
|
|
conflict_result.hello_status == CBM_DAEMON_HELLO_BUILD_CONFLICT;
|
|
if (!explicit_conflict) {
|
|
printf(" conflict fallback diagnostic: owner=%d rejected=%d owner_status=%d "
|
|
"conflict_status=%d hello_status=%d message=%s\n",
|
|
owner != NULL ? 1 : 0, rejected != NULL ? 1 : 0, (int)owner_result.status,
|
|
(int)conflict_result.status, (int)conflict_result.hello_status,
|
|
conflict_result.message);
|
|
}
|
|
(void)cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
owner = NULL;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
cbm_log_set_sink(NULL);
|
|
cbm_log_set_level(prior_level);
|
|
|
|
if (rejected) {
|
|
(void)cbm_daemon_runtime_client_close(rejected, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (owner) {
|
|
(void)cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
bool fallback_seen =
|
|
atomic_load_explicit(&runtime_conflict_log_fallback_seen, memory_order_acquire);
|
|
if (obstruction_created) {
|
|
(void)cbm_rmdir(fixture.log_path);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(obstruction_created);
|
|
ASSERT_TRUE(explicit_conflict);
|
|
ASSERT_TRUE(fallback_seen);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_disconnect_releases_only_connection_subscriptions) {
|
|
static const char project[] = "runtime-shared-project";
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "owned-subscriptions", &identity);
|
|
cbm_daemon_runtime_client_t *first = NULL;
|
|
cbm_daemon_runtime_client_t *second = NULL;
|
|
cbm_daemon_runtime_connect_result_t first_result = {0};
|
|
cbm_daemon_runtime_connect_result_t second_result = {0};
|
|
cbm_daemon_subscription_id_t first_subscription = CBM_DAEMON_SUBSCRIPTION_ID_INVALID;
|
|
cbm_daemon_subscription_id_t second_subscription = CBM_DAEMON_SUBSCRIPTION_ID_INVALID;
|
|
bool subscribed = false;
|
|
bool second_survived = false;
|
|
bool reaped = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
first = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &first_result);
|
|
second = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &second_result);
|
|
}
|
|
if (first && second) {
|
|
cbm_daemon_subscription_result_t first_status = cbm_daemon_runtime_client_job_subscribe(
|
|
first, project, &first_subscription, RUNTIME_TEST_TIMEOUT_MS);
|
|
cbm_daemon_subscription_result_t second_status = cbm_daemon_runtime_client_job_subscribe(
|
|
second, project, &second_subscription, RUNTIME_TEST_TIMEOUT_MS);
|
|
subscribed = first_status == CBM_DAEMON_SUBSCRIPTION_STARTED &&
|
|
second_status == CBM_DAEMON_SUBSCRIPTION_JOINED &&
|
|
first_subscription != CBM_DAEMON_SUBSCRIPTION_ID_INVALID &&
|
|
second_subscription != CBM_DAEMON_SUBSCRIPTION_ID_INVALID &&
|
|
first_subscription != second_subscription &&
|
|
cbm_daemon_runtime_service_job_subscribers(fixture.service, project) == 2;
|
|
|
|
(void)cbm_daemon_runtime_client_close(first, RUNTIME_TEST_TIMEOUT_MS);
|
|
first = NULL;
|
|
second_survived =
|
|
cbm_daemon_runtime_service_wait_for_clients(fixture.service, 1,
|
|
RUNTIME_TEST_TIMEOUT_MS) &&
|
|
cbm_daemon_runtime_service_job_subscribers(fixture.service, project) == 1 &&
|
|
cbm_daemon_runtime_client_job_unsubscribe(second, second_subscription,
|
|
RUNTIME_TEST_TIMEOUT_MS) &&
|
|
cbm_daemon_runtime_service_job_subscribers(fixture.service, project) == 0;
|
|
reaped = cbm_daemon_runtime_service_job_reaped(fixture.service, project);
|
|
(void)cbm_daemon_runtime_client_close(second, RUNTIME_TEST_TIMEOUT_MS);
|
|
second = NULL;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
if (second) {
|
|
(void)cbm_daemon_runtime_client_close(second, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (first) {
|
|
(void)cbm_daemon_runtime_client_close(first, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(subscribed);
|
|
ASSERT_TRUE(second_survived);
|
|
ASSERT_TRUE(reaped);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_final_disconnect_automatically_exits_within_bound) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "bounded-exit", &identity);
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
bool terminal_transition = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
if (client) {
|
|
(void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
client = NULL;
|
|
cbm_daemon_runtime_service_state_t after_close =
|
|
cbm_daemon_runtime_service_state(fixture.service);
|
|
terminal_transition = after_close == CBM_DAEMON_RUNTIME_SERVICE_STOPPING ||
|
|
after_close == CBM_DAEMON_RUNTIME_SERVICE_EXITED;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
if (client) {
|
|
(void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(terminal_transition);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_authenticated_idle_connection_outlives_lease_interval) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
bool started =
|
|
runtime_test_fixture_start_configured(&fixture, "idle-connection", &identity, 8, 20, NULL);
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
bool remained_connected = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
if (client) {
|
|
struct timespec beyond_lease = {.tv_sec = 0, .tv_nsec = 60000000};
|
|
(void)cbm_nanosleep(&beyond_lease, NULL);
|
|
remained_connected = cbm_daemon_runtime_service_active_clients(fixture.service) == 1 &&
|
|
cbm_daemon_runtime_client_heartbeat(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
(void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
client = NULL;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
if (client) {
|
|
(void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(remained_connected);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_connection_cap_covers_slow_hello_and_stopping_is_terminal) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start_limited(&fixture, "connection-cap", &identity, 2);
|
|
cbm_daemon_ipc_connection_t *slow_hello = NULL;
|
|
cbm_daemon_runtime_client_t *accepted = NULL;
|
|
cbm_daemon_runtime_client_t *overflow = NULL;
|
|
cbm_daemon_runtime_client_t *resurrection = NULL;
|
|
cbm_daemon_runtime_connect_result_t accepted_result = {0};
|
|
cbm_daemon_runtime_connect_result_t overflow_result = {0};
|
|
cbm_daemon_runtime_connect_result_t resurrection_result = {0};
|
|
bool slow_slot_counted = false;
|
|
bool capacity_rejected = false;
|
|
bool slow_slot_released = false;
|
|
bool exited = false;
|
|
bool no_resurrection = false;
|
|
|
|
if (started) {
|
|
slow_hello = cbm_daemon_ipc_connect(fixture.endpoint, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (slow_hello) {
|
|
slow_slot_counted = cbm_daemon_runtime_service_wait_for_connections(
|
|
fixture.service, 1, RUNTIME_TEST_TIMEOUT_MS);
|
|
accepted = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &accepted_result);
|
|
}
|
|
if (accepted) {
|
|
overflow = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &overflow_result);
|
|
capacity_rejected = overflow == NULL &&
|
|
overflow_result.status == CBM_DAEMON_RUNTIME_CONNECT_REJECTED &&
|
|
strstr(overflow_result.message, "capacity") != NULL &&
|
|
cbm_daemon_runtime_service_active_connections(fixture.service) == 2 &&
|
|
cbm_daemon_runtime_service_active_clients(fixture.service) == 1;
|
|
|
|
cbm_daemon_ipc_connection_close(slow_hello);
|
|
slow_hello = NULL;
|
|
slow_slot_released = cbm_daemon_runtime_service_wait_for_connections(
|
|
fixture.service, 1, RUNTIME_TEST_TIMEOUT_MS);
|
|
(void)cbm_daemon_runtime_client_close(accepted, RUNTIME_TEST_TIMEOUT_MS);
|
|
accepted = NULL;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
|
|
resurrection = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity, 100,
|
|
&resurrection_result);
|
|
cbm_daemon_runtime_service_state_t terminal_state =
|
|
cbm_daemon_runtime_service_state(fixture.service);
|
|
no_resurrection = resurrection == NULL &&
|
|
(resurrection_result.status == CBM_DAEMON_RUNTIME_CONNECT_ERROR ||
|
|
resurrection_result.status == CBM_DAEMON_RUNTIME_CONNECT_REJECTED) &&
|
|
cbm_daemon_runtime_service_active_clients(fixture.service) == 0 &&
|
|
(terminal_state == CBM_DAEMON_RUNTIME_SERVICE_STOPPING ||
|
|
terminal_state == CBM_DAEMON_RUNTIME_SERVICE_EXITED);
|
|
}
|
|
|
|
if (resurrection) {
|
|
(void)cbm_daemon_runtime_client_close(resurrection, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (overflow) {
|
|
(void)cbm_daemon_runtime_client_close(overflow, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (accepted) {
|
|
(void)cbm_daemon_runtime_client_close(accepted, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
cbm_daemon_ipc_connection_close(slow_hello);
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(slow_slot_counted);
|
|
ASSERT_TRUE(capacity_rejected);
|
|
ASSERT_TRUE(slow_slot_released);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_TRUE(no_resurrection);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_rejects_forged_identity_extension) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start(&fixture, "forged-identity", &identity);
|
|
cbm_daemon_ipc_connection_t *raw = NULL;
|
|
uint8_t forged[CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE + sizeof(uint64_t) * 2] = {0};
|
|
uint64_t forged_client_id = UINT64_MAX - 1;
|
|
uint64_t forged_process_id = UINT64_MAX;
|
|
bool encoded = false;
|
|
bool connected = false;
|
|
bool sent = false;
|
|
bool rejected = false;
|
|
cbm_daemon_frame_t response_frame = {0};
|
|
uint8_t *response_payload = NULL;
|
|
|
|
if (started) {
|
|
encoded = cbm_daemon_runtime_hello_request_encode(forged, &identity);
|
|
memcpy(forged + CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE, &forged_client_id,
|
|
sizeof(forged_client_id));
|
|
memcpy(forged + CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE + sizeof(forged_client_id),
|
|
&forged_process_id, sizeof(forged_process_id));
|
|
raw = cbm_daemon_ipc_connect(fixture.endpoint, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
connected = raw != NULL;
|
|
if (raw && encoded) {
|
|
/* The forged HELLO is 149 bytes against the 137-byte first-frame
|
|
* envelope cap, so the worker rejects it from the HEADER and closes
|
|
* without ever reading the payload -- deliberately, so that no
|
|
* attacker-controlled bytes are read (cbm_daemon_ipc_receive_frame_bounded).
|
|
* send_frame writes the header and the payload as two separate writes,
|
|
* so whether the payload write lands before that close is pure
|
|
* scheduling: it wins on an idle host and loses on a loaded one. Both
|
|
* outcomes ARE the rejection, so the transmit result is recorded and
|
|
* NOT asserted -- asserting it made the verdict a coin flip. */
|
|
sent = cbm_daemon_ipc_send_frame(raw, CBM_DAEMON_FRAME_REQUEST, CBM_DAEMON_RUNTIME_OP_HELLO,
|
|
forged, (uint32_t)sizeof(forged));
|
|
int received = sent ? cbm_daemon_ipc_receive_frame(raw, RUNTIME_TEST_TIMEOUT_MS,
|
|
&response_frame, &response_payload)
|
|
: 0;
|
|
/* Wait for the state actually asserted instead of sampling it once: the
|
|
* forged peer is the only client, so the count is monotonic here and the
|
|
* bound is a liveness backstop, never the verdict. */
|
|
rejected = received != 1 && cbm_daemon_runtime_service_wait_for_clients(
|
|
fixture.service, 0, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
free(response_payload);
|
|
cbm_daemon_ipc_connection_close(raw);
|
|
|
|
/* A malformed peer must not poison the stable service for valid clients. */
|
|
cbm_daemon_runtime_connect_result_t valid_result = {0};
|
|
cbm_daemon_runtime_client_t *valid = NULL;
|
|
bool valid_after_rejection = false;
|
|
bool exited = false;
|
|
if (started) {
|
|
valid = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &valid_result);
|
|
}
|
|
if (valid) {
|
|
valid_after_rejection =
|
|
valid_result.status == CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED &&
|
|
valid_result.authenticated_process_id == runtime_test_process_id() &&
|
|
valid_result.client_id != forged_client_id;
|
|
(void)cbm_daemon_runtime_client_close(valid, RUNTIME_TEST_TIMEOUT_MS);
|
|
valid = NULL;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
if (valid) {
|
|
(void)cbm_daemon_runtime_client_close(valid, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(encoded);
|
|
ASSERT_TRUE(connected);
|
|
ASSERT_TRUE(rejected);
|
|
ASSERT_TRUE(valid_after_rejection);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_application_response_roundtrip_is_byte_exact) {
|
|
static const uint8_t request[] = {
|
|
0x00, 0x7f, 0x80, 0xff, 'c', 'b', 'm', 0x00, 0x13,
|
|
};
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_application_context_t context;
|
|
runtime_application_context_init(&context, false);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start_application(&fixture, "application-roundtrip",
|
|
&identity, &context);
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
uint8_t *response = NULL;
|
|
uint32_t response_length = 0;
|
|
cbm_daemon_runtime_application_status_t status = CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
|
|
bool exact = false;
|
|
bool closed = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
if (client) {
|
|
status = cbm_daemon_runtime_client_application_request(
|
|
client, request, (uint32_t)sizeof(request), &response, &response_length,
|
|
RUNTIME_TEST_TIMEOUT_MS);
|
|
exact = status == CBM_DAEMON_RUNTIME_APPLICATION_OK && response_length == sizeof(request) &&
|
|
response && memcmp(response, request, sizeof(request)) == 0;
|
|
free(response);
|
|
response = NULL;
|
|
closed = cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
client = NULL;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
free(response);
|
|
if (client) {
|
|
(void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(exact);
|
|
ASSERT_TRUE(closed);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_EQ(atomic_load(&context.opened), 1);
|
|
ASSERT_EQ(atomic_load(&context.requests), 1);
|
|
/* A close after a completed exchange has nothing to cancel: the client
|
|
* only sends APPLICATION_CANCEL for an interrupted in-flight token, and
|
|
* the server treats stale tokens as deliberate one-way no-ops. */
|
|
ASSERT_EQ(atomic_load(&context.request_cancels), 0);
|
|
ASSERT_EQ(atomic_load(&context.cancelled), 1);
|
|
ASSERT_EQ(atomic_load(&context.closed), 1);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_final_disconnect_rejects_blocked_provisional_session) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_application_context_t context;
|
|
runtime_application_context_init(&context, false);
|
|
atomic_store_explicit(&context.block_second_open, true, memory_order_release);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start_application(&fixture, "application-open-race",
|
|
&identity, &context);
|
|
cbm_daemon_runtime_connect_result_t owner_result = {0};
|
|
cbm_daemon_runtime_client_t *owner = NULL;
|
|
runtime_application_connect_call_t contender = {
|
|
.identity = identity,
|
|
};
|
|
atomic_init(&contender.completed, false);
|
|
cbm_thread_t connect_thread;
|
|
int connect_thread_create_rc = -1;
|
|
int connect_thread_join_rc = -1;
|
|
bool connect_thread_started = false;
|
|
bool provisional_started = false;
|
|
bool owner_closed = false;
|
|
bool shutdown_won = false;
|
|
bool contender_accepted = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
owner = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &owner_result);
|
|
}
|
|
if (owner) {
|
|
contender.endpoint = fixture.endpoint;
|
|
connect_thread_create_rc = cbm_thread_create(
|
|
&connect_thread, 128U * 1024U, runtime_application_client_connect_thread, &contender);
|
|
connect_thread_started = connect_thread_create_rc == 0;
|
|
provisional_started =
|
|
connect_thread_started &&
|
|
runtime_test_wait_atomic_bool(&context.second_open_started, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (provisional_started) {
|
|
owner_closed = cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
owner = NULL;
|
|
shutdown_won = cbm_daemon_runtime_service_state(fixture.service) ==
|
|
CBM_DAEMON_RUNTIME_SERVICE_STOPPING;
|
|
}
|
|
|
|
/* Release on every setup outcome so neither the server worker nor the
|
|
* helper can retain stack-owned test state during cleanup. */
|
|
atomic_store_explicit(&context.release_second_open, true, memory_order_release);
|
|
if (connect_thread_started) {
|
|
connect_thread_join_rc = cbm_thread_join(&connect_thread);
|
|
if (connect_thread_join_rc == 0) {
|
|
connect_thread_started = false;
|
|
}
|
|
}
|
|
if (connect_thread_started) {
|
|
while (!atomic_load_explicit(&contender.completed, memory_order_acquire)) {
|
|
struct timespec pause = {.tv_sec = 0, .tv_nsec = 1000000};
|
|
(void)cbm_nanosleep(&pause, NULL);
|
|
}
|
|
}
|
|
contender_accepted = contender.client != NULL;
|
|
if (owner) {
|
|
(void)cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
owner = NULL;
|
|
}
|
|
if (contender.client) {
|
|
(void)cbm_daemon_runtime_client_close(contender.client, RUNTIME_TEST_TIMEOUT_MS);
|
|
contender.client = NULL;
|
|
}
|
|
if (started) {
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_EQ(owner_result.status, CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED);
|
|
ASSERT_EQ(connect_thread_create_rc, 0);
|
|
ASSERT_TRUE(provisional_started);
|
|
ASSERT_TRUE(owner_closed);
|
|
ASSERT_TRUE(shutdown_won);
|
|
ASSERT_EQ(connect_thread_join_rc, 0);
|
|
ASSERT_TRUE(atomic_load_explicit(&contender.completed, memory_order_acquire));
|
|
ASSERT_FALSE(contender_accepted);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_EQ(atomic_load(&context.opened), 2);
|
|
ASSERT_EQ(atomic_load(&context.cancelled), 2);
|
|
ASSERT_EQ(atomic_load(&context.closed), 2);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_request_cancel_is_exact_and_session_remains_usable) {
|
|
static const uint8_t blocking_request[] = {'b', 'l', 'o', 'c', 'k'};
|
|
static const uint8_t next_request[] = {0xde, 0xad, 0x00, 0xbe, 0xef};
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_application_context_t context;
|
|
runtime_application_context_init(&context, true);
|
|
atomic_bool request_thread_completed;
|
|
atomic_init(&request_thread_completed, false);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start_application(&fixture, "application-request-cancel",
|
|
&identity, &context);
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
cbm_daemon_runtime_application_token_t request_token =
|
|
CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
|
|
runtime_application_client_call_t call = {
|
|
.request = blocking_request,
|
|
.request_length = (uint32_t)sizeof(blocking_request),
|
|
.tagged = true,
|
|
.completed = &request_thread_completed,
|
|
.status = CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR,
|
|
};
|
|
cbm_thread_t request_thread;
|
|
int request_thread_create_rc = -1;
|
|
int request_thread_join_rc = -1;
|
|
bool request_thread_join_attempted = false;
|
|
bool token_reserved = false;
|
|
bool request_thread_started = false;
|
|
bool callback_started = false;
|
|
cbm_daemon_runtime_cancel_result_t wrong_cancel = CBM_DAEMON_RUNTIME_CANCEL_ERROR;
|
|
int cancels_after_wrong = -1;
|
|
cbm_daemon_runtime_cancel_result_t exact_cancel = CBM_DAEMON_RUNTIME_CANCEL_ERROR;
|
|
bool cancel_delivered = false;
|
|
cbm_daemon_runtime_cancel_result_t late_duplicate_cancel = CBM_DAEMON_RUNTIME_CANCEL_ERROR;
|
|
int cancels_after_duplicate = -1;
|
|
bool close_begun = false;
|
|
bool request_thread_joined = false;
|
|
uint8_t *next_response = NULL;
|
|
uint32_t next_response_length = 0;
|
|
cbm_daemon_runtime_application_status_t next_status =
|
|
CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
|
|
bool next_exact = false;
|
|
bool heartbeat = false;
|
|
bool closed = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
if (client) {
|
|
token_reserved =
|
|
cbm_daemon_runtime_client_application_token_reserve(client, &request_token);
|
|
}
|
|
if (token_reserved) {
|
|
call.client = client;
|
|
call.request_token = request_token;
|
|
request_thread_create_rc = cbm_thread_create(
|
|
&request_thread, 128U * 1024U, runtime_application_client_request_thread, &call);
|
|
request_thread_started = request_thread_create_rc == 0;
|
|
if (!request_thread_started) {
|
|
printf(" runtime helper thread create failed: rc=%d\n", request_thread_create_rc);
|
|
}
|
|
callback_started =
|
|
request_thread_started &&
|
|
runtime_test_wait_atomic_bool(&context.first_request_started, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (callback_started) {
|
|
wrong_cancel = cbm_daemon_runtime_client_application_cancel(client, request_token + 1U);
|
|
cancels_after_wrong = atomic_load_explicit(&context.request_cancels, memory_order_acquire);
|
|
exact_cancel = cbm_daemon_runtime_client_application_cancel(client, request_token);
|
|
cancel_delivered =
|
|
exact_cancel == CBM_DAEMON_RUNTIME_CANCEL_ACCEPTED &&
|
|
runtime_test_wait_atomic_int(&context.request_cancels, 1, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (request_thread_started && !cancel_delivered && client) {
|
|
close_begun = cbm_daemon_runtime_client_close_begin(client);
|
|
}
|
|
if (request_thread_started && (cancel_delivered || close_begun)) {
|
|
request_thread_join_attempted = true;
|
|
request_thread_join_rc = cbm_thread_join(&request_thread);
|
|
request_thread_joined = request_thread_join_rc == 0;
|
|
if (request_thread_joined) {
|
|
request_thread_started = false;
|
|
} else {
|
|
printf(" runtime helper thread join failed: rc=%d\n", request_thread_join_rc);
|
|
}
|
|
}
|
|
if (request_thread_joined && cancel_delivered && !close_begun) {
|
|
late_duplicate_cancel = cbm_daemon_runtime_client_application_cancel(client, request_token);
|
|
cancels_after_duplicate =
|
|
atomic_load_explicit(&context.request_cancels, memory_order_acquire);
|
|
next_status = cbm_daemon_runtime_client_application_request(
|
|
client, next_request, (uint32_t)sizeof(next_request), &next_response,
|
|
&next_response_length, RUNTIME_TEST_TIMEOUT_MS);
|
|
next_exact = next_status == CBM_DAEMON_RUNTIME_APPLICATION_OK &&
|
|
next_response_length == sizeof(next_request) && next_response &&
|
|
memcmp(next_response, next_request, sizeof(next_request)) == 0;
|
|
heartbeat = cbm_daemon_runtime_client_heartbeat(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
free(next_response);
|
|
if (request_thread_started) {
|
|
/* Keep call/client storage alive until the helper's final release
|
|
* store even when an exceptional OS join failure prevents proving
|
|
* termination through the thread API. */
|
|
while (!atomic_load_explicit(&request_thread_completed, memory_order_acquire)) {
|
|
struct timespec pause = {.tv_sec = 0, .tv_nsec = 1000000};
|
|
(void)cbm_nanosleep(&pause, NULL);
|
|
}
|
|
if (!request_thread_join_attempted) {
|
|
request_thread_join_attempted = true;
|
|
request_thread_join_rc = cbm_thread_join(&request_thread);
|
|
request_thread_joined = request_thread_join_rc == 0;
|
|
if (request_thread_joined) {
|
|
request_thread_started = false;
|
|
} else {
|
|
printf(" runtime helper thread join failed: rc=%d\n", request_thread_join_rc);
|
|
}
|
|
}
|
|
}
|
|
if (client) {
|
|
closed = close_begun
|
|
? cbm_daemon_runtime_client_close_finish(client, RUNTIME_TEST_TIMEOUT_MS)
|
|
: cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
client = NULL;
|
|
}
|
|
if (started) {
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
bool cancelled_response = call.status == CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED &&
|
|
call.response == NULL && call.response_length == 0;
|
|
free(call.response);
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(token_reserved);
|
|
ASSERT_EQ(request_thread_create_rc, 0);
|
|
ASSERT_TRUE(request_thread_join_attempted);
|
|
ASSERT_EQ(request_thread_join_rc, 0);
|
|
ASSERT_TRUE(callback_started);
|
|
ASSERT_EQ(wrong_cancel, CBM_DAEMON_RUNTIME_CANCEL_STALE);
|
|
ASSERT_EQ(cancels_after_wrong, 0);
|
|
ASSERT_EQ(exact_cancel, CBM_DAEMON_RUNTIME_CANCEL_ACCEPTED);
|
|
ASSERT_TRUE(cancel_delivered);
|
|
ASSERT_TRUE(request_thread_joined);
|
|
ASSERT_TRUE(cancelled_response);
|
|
ASSERT_EQ(late_duplicate_cancel, CBM_DAEMON_RUNTIME_CANCEL_STALE);
|
|
ASSERT_EQ(cancels_after_duplicate, 1);
|
|
ASSERT_TRUE(next_exact);
|
|
ASSERT_TRUE(heartbeat);
|
|
ASSERT_TRUE(closed);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_EQ(atomic_load(&context.opened), 1);
|
|
ASSERT_EQ(atomic_load(&context.requests), 2);
|
|
ASSERT_EQ(atomic_load(&context.request_cancels), 1);
|
|
ASSERT_EQ(atomic_load(&context.cancelled), 1);
|
|
ASSERT_EQ(atomic_load(&context.closed), 1);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_presend_request_cancel_is_sticky_and_nonterminal) {
|
|
static const uint8_t blocking_request[] = {'p', 'r', 'e'};
|
|
static const uint8_t next_request[] = {'n', 'e', 'x', 't'};
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_application_context_t context;
|
|
runtime_application_context_init(&context, true);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start_application(&fixture, "application-presend-cancel",
|
|
&identity, &context);
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
cbm_daemon_runtime_application_token_t request_token =
|
|
CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
|
|
bool token_reserved = false;
|
|
cbm_daemon_runtime_cancel_result_t cancel = CBM_DAEMON_RUNTIME_CANCEL_ERROR;
|
|
uint8_t *cancelled_response = NULL;
|
|
uint32_t cancelled_response_length = 0;
|
|
cbm_daemon_runtime_application_status_t cancelled_status =
|
|
CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
|
|
uint8_t *next_response = NULL;
|
|
uint32_t next_response_length = 0;
|
|
cbm_daemon_runtime_application_status_t next_status =
|
|
CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
|
|
bool next_exact = false;
|
|
bool closed = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
if (client) {
|
|
token_reserved =
|
|
cbm_daemon_runtime_client_application_token_reserve(client, &request_token);
|
|
}
|
|
if (token_reserved) {
|
|
cancel = cbm_daemon_runtime_client_application_cancel(client, request_token);
|
|
cancelled_status = cbm_daemon_runtime_client_application_request_tagged(
|
|
client, request_token, blocking_request, (uint32_t)sizeof(blocking_request),
|
|
&cancelled_response, &cancelled_response_length, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (cancelled_status == CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED) {
|
|
next_status = cbm_daemon_runtime_client_application_request(
|
|
client, next_request, (uint32_t)sizeof(next_request), &next_response,
|
|
&next_response_length, RUNTIME_TEST_TIMEOUT_MS);
|
|
next_exact = next_status == CBM_DAEMON_RUNTIME_APPLICATION_OK &&
|
|
next_response_length == sizeof(next_request) && next_response &&
|
|
memcmp(next_response, next_request, sizeof(next_request)) == 0;
|
|
}
|
|
free(cancelled_response);
|
|
free(next_response);
|
|
if (client) {
|
|
closed = cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
client = NULL;
|
|
}
|
|
if (started) {
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(token_reserved);
|
|
ASSERT_EQ(cancel, CBM_DAEMON_RUNTIME_CANCEL_ACCEPTED);
|
|
ASSERT_EQ(cancelled_status, CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED);
|
|
ASSERT_EQ(cancelled_response_length, 0);
|
|
ASSERT_TRUE(next_exact);
|
|
ASSERT_TRUE(closed);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_EQ(atomic_load(&context.opened), 1);
|
|
ASSERT_EQ(atomic_load(&context.requests), 2);
|
|
ASSERT_EQ(atomic_load(&context.request_cancels), 1);
|
|
ASSERT_EQ(atomic_load(&context.cancelled), 1);
|
|
ASSERT_EQ(atomic_load(&context.closed), 1);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_allows_only_one_unstarted_application_token) {
|
|
static const uint8_t first_request[] = {'f', 'i', 'r', 's', 't'};
|
|
static const uint8_t second_request[] = {'s', 'e', 'c', 'o', 'n', 'd'};
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_application_context_t context;
|
|
runtime_application_context_init(&context, false);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start_application(&fixture, "application-token-reservation",
|
|
&identity, &context);
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
cbm_daemon_runtime_application_token_t first_token =
|
|
CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
|
|
cbm_daemon_runtime_application_token_t rejected_token = UINT64_MAX;
|
|
cbm_daemon_runtime_application_token_t second_token =
|
|
CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
|
|
bool first_reserved = false;
|
|
bool duplicate_reservation_rejected = false;
|
|
bool first_exact = false;
|
|
bool second_reserved = false;
|
|
bool second_exact = false;
|
|
bool closed = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
if (client) {
|
|
first_reserved = cbm_daemon_runtime_client_application_token_reserve(client, &first_token);
|
|
duplicate_reservation_rejected =
|
|
!cbm_daemon_runtime_client_application_token_reserve(client, &rejected_token) &&
|
|
rejected_token == CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
|
|
}
|
|
if (first_reserved && duplicate_reservation_rejected) {
|
|
uint8_t *response = NULL;
|
|
uint32_t response_length = 0;
|
|
cbm_daemon_runtime_application_status_t status =
|
|
cbm_daemon_runtime_client_application_request_tagged(
|
|
client, first_token, first_request, (uint32_t)sizeof(first_request), &response,
|
|
&response_length, RUNTIME_TEST_TIMEOUT_MS);
|
|
first_exact = status == CBM_DAEMON_RUNTIME_APPLICATION_OK &&
|
|
response_length == sizeof(first_request) && response &&
|
|
memcmp(response, first_request, sizeof(first_request)) == 0;
|
|
free(response);
|
|
}
|
|
if (first_exact) {
|
|
second_reserved =
|
|
cbm_daemon_runtime_client_application_token_reserve(client, &second_token);
|
|
}
|
|
if (second_reserved) {
|
|
uint8_t *response = NULL;
|
|
uint32_t response_length = 0;
|
|
cbm_daemon_runtime_application_status_t status =
|
|
cbm_daemon_runtime_client_application_request_tagged(
|
|
client, second_token, second_request, (uint32_t)sizeof(second_request), &response,
|
|
&response_length, RUNTIME_TEST_TIMEOUT_MS);
|
|
second_exact = status == CBM_DAEMON_RUNTIME_APPLICATION_OK &&
|
|
response_length == sizeof(second_request) && response &&
|
|
memcmp(response, second_request, sizeof(second_request)) == 0;
|
|
free(response);
|
|
}
|
|
if (client) {
|
|
closed = cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
client = NULL;
|
|
}
|
|
if (started) {
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(first_reserved);
|
|
ASSERT_TRUE(duplicate_reservation_rejected);
|
|
ASSERT_TRUE(first_exact);
|
|
ASSERT_TRUE(second_reserved);
|
|
ASSERT_EQ(second_token, first_token + 1U);
|
|
ASSERT_TRUE(second_exact);
|
|
ASSERT_TRUE(closed);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_EQ(atomic_load(&context.requests), 2);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_consumes_busy_application_token_before_response) {
|
|
static const uint8_t blocking_request[] = {'b', 'l', 'o', 'c', 'k'};
|
|
static const uint8_t busy_request[] = {'b', 'u', 's', 'y'};
|
|
enum { FIRST_TOKEN = 41, BUSY_TOKEN = 42 };
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_application_context_t context;
|
|
runtime_application_context_init(&context, true);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start_application(&fixture, "application-token-replay",
|
|
&identity, &context);
|
|
cbm_daemon_ipc_connection_t *raw = NULL;
|
|
bool first_sent = false;
|
|
bool first_started = false;
|
|
bool busy_sent = false;
|
|
bool busy_received = false;
|
|
bool cancel_sent = false;
|
|
bool cancellation_received = false;
|
|
bool replay_sent = false;
|
|
bool replay_rejected = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
raw = runtime_test_raw_client_connect(fixture.endpoint, &identity);
|
|
}
|
|
if (raw) {
|
|
first_sent = runtime_test_raw_application_send_token(raw, FIRST_TOKEN, blocking_request,
|
|
(uint32_t)sizeof(blocking_request),
|
|
(uint32_t)sizeof(blocking_request));
|
|
first_started = first_sent && runtime_test_wait_atomic_bool(&context.first_request_started,
|
|
RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (first_started) {
|
|
busy_sent = runtime_test_raw_application_send_token(raw, BUSY_TOKEN, busy_request,
|
|
(uint32_t)sizeof(busy_request),
|
|
(uint32_t)sizeof(busy_request));
|
|
busy_received = busy_sent && runtime_test_raw_application_receive_status_token(
|
|
raw, BUSY_TOKEN, CBM_DAEMON_RUNTIME_APPLICATION_BUSY);
|
|
}
|
|
if (busy_received) {
|
|
cancel_sent = runtime_test_raw_application_cancel(raw, FIRST_TOKEN);
|
|
cancellation_received =
|
|
cancel_sent && runtime_test_raw_application_receive_status_token(
|
|
raw, FIRST_TOKEN, CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED);
|
|
}
|
|
if (cancellation_received) {
|
|
replay_sent = runtime_test_raw_application_send_token(raw, BUSY_TOKEN, busy_request,
|
|
(uint32_t)sizeof(busy_request),
|
|
(uint32_t)sizeof(busy_request));
|
|
}
|
|
if (replay_sent) {
|
|
cbm_daemon_frame_t frame = {0};
|
|
uint8_t *payload = NULL;
|
|
int received = cbm_daemon_ipc_receive_frame(raw, RUNTIME_TEST_TIMEOUT_MS, &frame, &payload);
|
|
replay_rejected = received != 1;
|
|
free(payload);
|
|
}
|
|
cbm_daemon_ipc_connection_close(raw);
|
|
raw = NULL;
|
|
if (started) {
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(first_sent);
|
|
ASSERT_TRUE(first_started);
|
|
ASSERT_TRUE(busy_sent);
|
|
ASSERT_TRUE(busy_received);
|
|
ASSERT_TRUE(cancel_sent);
|
|
ASSERT_TRUE(cancellation_received);
|
|
ASSERT_TRUE(replay_sent);
|
|
ASSERT_TRUE(replay_rejected);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_EQ(atomic_load(&context.requests), 1);
|
|
ASSERT_EQ(atomic_load(&context.request_cancels), 1);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_close_begin_retains_storage_and_rejects_late_exchange) {
|
|
static const uint8_t request[] = {'t', 'o', 'o', '-', 'l', 'a', 't', 'e'};
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_application_context_t context;
|
|
runtime_application_context_init(&context, false);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start_application(&fixture, "application-close-gap",
|
|
&identity, &context);
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
uint8_t *response = NULL;
|
|
uint32_t response_length = 0;
|
|
cbm_daemon_runtime_application_status_t status = CBM_DAEMON_RUNTIME_APPLICATION_OK;
|
|
bool close_begun = false;
|
|
bool duplicate_begin_rejected = false;
|
|
bool close_acknowledged = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
if (client) {
|
|
close_begun = cbm_daemon_runtime_client_close_begin(client);
|
|
duplicate_begin_rejected = close_begun && !cbm_daemon_runtime_client_close_begin(client);
|
|
}
|
|
if (close_begun) {
|
|
/* Deterministically models the frontend boundary where close begins
|
|
* after a worker claims an item but before it enters the runtime API.
|
|
* The retained allocation must reject the call without touching IPC. */
|
|
status = cbm_daemon_runtime_client_application_request(
|
|
client, request, (uint32_t)sizeof(request), &response, &response_length,
|
|
RUNTIME_TEST_TIMEOUT_MS);
|
|
close_acknowledged =
|
|
cbm_daemon_runtime_client_close_finish(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
client = NULL;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
free(response);
|
|
if (client) {
|
|
if (close_begun) {
|
|
(void)cbm_daemon_runtime_client_close_finish(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
} else {
|
|
(void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(close_begun);
|
|
ASSERT_TRUE(duplicate_begin_rejected);
|
|
ASSERT_EQ(status, CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR);
|
|
ASSERT_NULL(response);
|
|
ASSERT_EQ(response_length, 0);
|
|
ASSERT_TRUE(close_acknowledged);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_EQ(atomic_load(&context.opened), 1);
|
|
ASSERT_EQ(atomic_load(&context.requests), 0);
|
|
ASSERT_EQ(atomic_load(&context.cancelled), 1);
|
|
ASSERT_EQ(atomic_load(&context.closed), 1);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_disconnect_cancels_blocked_application_before_exit) {
|
|
static const uint8_t request[] = {'b', 'l', 'o', 'c', 'k'};
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_application_context_t context;
|
|
runtime_application_context_init(&context, true);
|
|
atomic_bool request_thread_completed;
|
|
atomic_init(&request_thread_completed, false);
|
|
runtime_test_fixture_t fixture;
|
|
bool started =
|
|
runtime_test_fixture_start_application(&fixture, "application-cancel", &identity, &context);
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
runtime_application_client_call_t call = {
|
|
.request = request,
|
|
.request_length = (uint32_t)sizeof(request),
|
|
.completed = &request_thread_completed,
|
|
.status = CBM_DAEMON_RUNTIME_APPLICATION_OK,
|
|
};
|
|
cbm_thread_t request_thread;
|
|
int request_thread_create_rc = -1;
|
|
int request_thread_join_rc = -1;
|
|
bool request_thread_join_attempted = false;
|
|
bool request_thread_started = false;
|
|
bool callback_started = false;
|
|
bool close_begun = false;
|
|
bool close_acknowledged = false;
|
|
bool request_thread_joined = false;
|
|
bool request_interrupted = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
if (client) {
|
|
call.client = client;
|
|
request_thread_create_rc = cbm_thread_create(
|
|
&request_thread, 128U * 1024U, runtime_application_client_request_thread, &call);
|
|
request_thread_started = request_thread_create_rc == 0;
|
|
if (!request_thread_started) {
|
|
printf(" runtime helper thread create failed: rc=%d\n", request_thread_create_rc);
|
|
}
|
|
callback_started =
|
|
request_thread_started &&
|
|
runtime_test_wait_atomic_bool(&context.first_request_started, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (callback_started) {
|
|
close_begun = cbm_daemon_runtime_client_close_begin(client);
|
|
}
|
|
if (client && request_thread_started && !close_begun) {
|
|
close_begun = cbm_daemon_runtime_client_close_begin(client);
|
|
}
|
|
if (request_thread_started && close_begun) {
|
|
request_thread_join_attempted = true;
|
|
request_thread_join_rc = cbm_thread_join(&request_thread);
|
|
request_thread_joined = request_thread_join_rc == 0;
|
|
if (request_thread_joined) {
|
|
request_thread_started = false;
|
|
} else {
|
|
printf(" runtime helper thread join failed: rc=%d\n", request_thread_join_rc);
|
|
}
|
|
}
|
|
|
|
if (request_thread_started) {
|
|
/* A join error does not prove the helper stopped. Wait for its final
|
|
* release-store before touching call/client/fixture state. This is
|
|
* intentionally unbounded, matching the successful join path's
|
|
* semantics while keeping an exceptional cleanup path free of UAF. */
|
|
while (!atomic_load_explicit(&request_thread_completed, memory_order_acquire)) {
|
|
struct timespec pause = {.tv_sec = 0, .tv_nsec = 1000000};
|
|
(void)cbm_nanosleep(&pause, NULL);
|
|
}
|
|
if (!request_thread_join_attempted) {
|
|
request_thread_join_attempted = true;
|
|
request_thread_join_rc = cbm_thread_join(&request_thread);
|
|
request_thread_joined = request_thread_join_rc == 0;
|
|
if (request_thread_joined) {
|
|
request_thread_started = false;
|
|
} else {
|
|
printf(" runtime helper thread join failed: rc=%d\n", request_thread_join_rc);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (client) {
|
|
close_acknowledged =
|
|
close_begun ? cbm_daemon_runtime_client_close_finish(client, RUNTIME_TEST_TIMEOUT_MS)
|
|
: cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
client = NULL;
|
|
}
|
|
if (started) {
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
/* close_begin cancels the exchange and, on Windows, also sends the
|
|
* active token's APPLICATION_CANCEL. Whether the local interrupt or the
|
|
* server's CANCELLED response wins that race, the request ended promptly
|
|
* without a payload — both outcomes are the contract. */
|
|
request_interrupted = (call.status == CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR ||
|
|
call.status == CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED) &&
|
|
call.response == NULL && call.response_length == 0;
|
|
free(call.response);
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_EQ(request_thread_create_rc, 0);
|
|
ASSERT_TRUE(request_thread_join_attempted);
|
|
ASSERT_EQ(request_thread_join_rc, 0);
|
|
ASSERT_TRUE(request_thread_joined);
|
|
ASSERT_TRUE(callback_started);
|
|
ASSERT_TRUE(close_begun);
|
|
/* close_begin intentionally interrupted the in-flight transport, so the
|
|
* two-phase close cannot also receive a DISCONNECT acknowledgement. EOF
|
|
* still closes the server session and must cancel the request/tree. */
|
|
ASSERT_FALSE(close_acknowledged);
|
|
ASSERT_TRUE(request_interrupted);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_EQ(atomic_load(&context.opened), 1);
|
|
ASSERT_EQ(atomic_load(&context.requests), 1);
|
|
ASSERT_EQ(atomic_load(&context.cancelled), 1);
|
|
ASSERT_EQ(atomic_load(&context.closed), 1);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_disconnect_cancels_blocked_non_index_child_and_preserves_other_session) {
|
|
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__linux__)
|
|
SKIP_PLATFORM("requires a queryable copied process image");
|
|
#else
|
|
enum {
|
|
CHILD_READY_BOUND_MS = 5000,
|
|
CHILD_CANCEL_BOUND_MS = 3000,
|
|
CHILD_CLEANUP_BOUND_MS = 5000,
|
|
REQUEST_TIMEOUT_MS = 15000,
|
|
};
|
|
const char *old_cache = getenv("CBM_CACHE_DIR");
|
|
const char *old_path = getenv("PATH");
|
|
const char *old_marker = getenv(RUNTIME_TEST_BLOCKING_GIT_MARKER_ENV);
|
|
bool had_cache = old_cache != NULL;
|
|
bool had_path = old_path != NULL;
|
|
bool had_marker = old_marker != NULL;
|
|
char *saved_cache = old_cache ? cbm_strdup(old_cache) : NULL;
|
|
char *saved_path = old_path ? cbm_strdup(old_path) : NULL;
|
|
char *saved_marker = old_marker ? cbm_strdup(old_marker) : NULL;
|
|
bool snapshots_ok =
|
|
(!had_cache || saved_cache) && (!had_path || saved_path) && (!had_marker || saved_marker);
|
|
|
|
char work[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char root[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char cache[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char bin[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char fake_git[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char marker[RUNTIME_TEST_PATH_CAP] = {0};
|
|
#ifdef _WIN32
|
|
/* cmd.exe searches a native PATH. Keep the copied git probe out of the
|
|
* MSYS2 TEMP ancestry and canonicalize its directory before publication. */
|
|
bool work_ready =
|
|
snapshots_ok && th_secure_runtime_parent_new(work, sizeof(work), "non-index-work");
|
|
#else
|
|
(void)snprintf(work, sizeof(work), "%s/cbm-runtime-non-index-XXXXXX", cbm_tmpdir());
|
|
bool work_ready = snapshots_ok && cbm_mkdtemp(work) != NULL;
|
|
#endif
|
|
int root_written = work_ready ? snprintf(root, sizeof(root), "%s/root", work) : -1;
|
|
int cache_written = work_ready ? snprintf(cache, sizeof(cache), "%s/cache", work) : -1;
|
|
int bin_written = work_ready ? snprintf(bin, sizeof(bin), "%s/bin", work) : -1;
|
|
#ifdef _WIN32
|
|
int git_written = work_ready ? snprintf(fake_git, sizeof(fake_git), "%s/git.exe", bin) : -1;
|
|
#else
|
|
int git_written = work_ready ? snprintf(fake_git, sizeof(fake_git), "%s/git", bin) : -1;
|
|
#endif
|
|
int marker_written =
|
|
work_ready ? snprintf(marker, sizeof(marker), "%s/blocking-git.pid", work) : -1;
|
|
bool paths_ready = work_ready && root_written > 0 && root_written < (int)sizeof(root) &&
|
|
cache_written > 0 && cache_written < (int)sizeof(cache) && bin_written > 0 &&
|
|
bin_written < (int)sizeof(bin) && git_written > 0 &&
|
|
git_written < (int)sizeof(fake_git) && marker_written > 0 &&
|
|
marker_written < (int)sizeof(marker) && cbm_mkdir_p(root, 0700) &&
|
|
cbm_mkdir_p(cache, 0700) && cbm_mkdir_p(bin, 0700) &&
|
|
runtime_test_copy_self_image(fake_git);
|
|
|
|
char path_bin[RUNTIME_TEST_PATH_CAP] = {0};
|
|
#ifdef _WIN32
|
|
bool path_bin_ready = paths_ready && cbm_canonical_path(bin, path_bin, sizeof(path_bin));
|
|
if (path_bin_ready) {
|
|
for (char *cursor = path_bin; *cursor; cursor++) {
|
|
if (*cursor == '/') {
|
|
*cursor = '\\';
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
int path_bin_written = paths_ready ? snprintf(path_bin, sizeof(path_bin), "%s", bin) : -1;
|
|
bool path_bin_ready = path_bin_written > 0 && path_bin_written < (int)sizeof(path_bin);
|
|
#endif
|
|
|
|
char *project = paths_ready ? cbm_project_name_from_path(root) : NULL;
|
|
char db_path[RUNTIME_TEST_PATH_CAP] = {0};
|
|
int db_written = project ? snprintf(db_path, sizeof(db_path), "%s/%s.db", cache, project) : -1;
|
|
cbm_store_t *seed =
|
|
db_written > 0 && db_written < (int)sizeof(db_path) ? cbm_store_open_path(db_path) : NULL;
|
|
bool seeded = seed && cbm_store_upsert_project(seed, project, root) == CBM_STORE_OK;
|
|
cbm_store_close(seed);
|
|
|
|
size_t path_length = strlen(path_bin) + 2U + (saved_path ? strlen(saved_path) : 0U);
|
|
char *test_path = seeded && path_bin_ready ? malloc(path_length) : NULL;
|
|
if (test_path) {
|
|
#ifdef _WIN32
|
|
(void)snprintf(test_path, path_length, "%s;%s", path_bin, saved_path ? saved_path : "");
|
|
#else
|
|
(void)snprintf(test_path, path_length, "%s:%s", path_bin, saved_path ? saved_path : "");
|
|
#endif
|
|
}
|
|
bool environment_ready = test_path && cbm_setenv("CBM_CACHE_DIR", cache, 1) == 0 &&
|
|
cbm_setenv("PATH", test_path, 1) == 0 &&
|
|
cbm_setenv(RUNTIME_TEST_BLOCKING_GIT_MARKER_ENV, marker, 1) == 0;
|
|
|
|
cbm_daemon_application_t *application =
|
|
environment_ready ? cbm_daemon_application_new(NULL) : NULL;
|
|
cbm_daemon_runtime_application_callbacks_t callbacks =
|
|
cbm_daemon_application_runtime_callbacks(application);
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture = {0};
|
|
bool started =
|
|
application && runtime_test_fixture_start_configured(&fixture, "non-index-child-cancel",
|
|
&identity, 8, 5000, &callbacks);
|
|
cbm_daemon_runtime_connect_result_t first_result = {0};
|
|
cbm_daemon_runtime_connect_result_t second_result = {0};
|
|
cbm_daemon_runtime_client_t *first =
|
|
started ? cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &first_result)
|
|
: NULL;
|
|
cbm_daemon_runtime_client_t *second =
|
|
first ? cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &second_result)
|
|
: NULL;
|
|
bool contexts_set =
|
|
first && second &&
|
|
cbm_daemon_application_client_set_context(first, root, root, CBM_MCP_TOOL_PROFILE_ALL, NULL,
|
|
NULL, RUNTIME_TEST_TIMEOUT_MS) ==
|
|
CBM_DAEMON_RUNTIME_APPLICATION_OK &&
|
|
cbm_daemon_application_client_set_context(second, root, root, CBM_MCP_TOOL_PROFILE_ALL,
|
|
NULL, NULL, RUNTIME_TEST_TIMEOUT_MS) ==
|
|
CBM_DAEMON_RUNTIME_APPLICATION_OK;
|
|
bool second_usable_before =
|
|
contexts_set && runtime_real_application_ingest_probe(second) &&
|
|
cbm_daemon_runtime_client_heartbeat(second, RUNTIME_TEST_TIMEOUT_MS);
|
|
|
|
runtime_real_application_call_t call = {
|
|
.client = first,
|
|
.timeout_ms = REQUEST_TIMEOUT_MS,
|
|
.status = CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR,
|
|
};
|
|
atomic_init(&call.completed, false);
|
|
int arguments_written =
|
|
project ? snprintf(call.arguments, sizeof(call.arguments), "{\"project\":\"%s\"}", project)
|
|
: -1;
|
|
cbm_thread_t request_thread;
|
|
int request_thread_create_rc =
|
|
second_usable_before && arguments_written > 0 &&
|
|
arguments_written < (int)sizeof(call.arguments)
|
|
? cbm_thread_create(&request_thread, 128U * 1024U,
|
|
runtime_real_application_detect_changes_thread, &call)
|
|
: -1;
|
|
bool request_thread_started = request_thread_create_rc == 0;
|
|
uint64_t child_process_id = 0;
|
|
bool marker_published =
|
|
request_thread_started &&
|
|
runtime_test_wait_pid_marker(marker, CHILD_READY_BOUND_MS, &child_process_id);
|
|
if (request_thread_started && !marker_published) {
|
|
printf(" blocking git marker missing: image=%s completed=%d status=%d response=%.*s\n",
|
|
fake_git, atomic_load_explicit(&call.completed, memory_order_acquire) ? 1 : 0,
|
|
(int)call.status, (int)call.response_length,
|
|
call.response ? (const char *)call.response : "");
|
|
}
|
|
bool child_identity_exact =
|
|
marker_published && runtime_test_process_image_matches(child_process_id, fake_git);
|
|
|
|
bool first_close_begun = child_identity_exact && cbm_daemon_runtime_client_close_begin(first);
|
|
/* Admission must drop at close_begin on every platform: POSIX signals it
|
|
* through shutdown()/EOF, Windows through the CLOSE_INTENT frame — a
|
|
* named-pipe client has no transport-level half-close to lean on. */
|
|
bool only_second_admitted =
|
|
first_close_begun &&
|
|
cbm_daemon_runtime_service_wait_for_clients(fixture.service, 1, RUNTIME_TEST_TIMEOUT_MS);
|
|
size_t clients_after_admission_wait =
|
|
started ? cbm_daemon_runtime_service_active_clients(fixture.service) : 0;
|
|
size_t connections_after_admission_wait =
|
|
started ? cbm_daemon_runtime_service_active_connections(fixture.service) : 0;
|
|
bool child_gone_without_backstop =
|
|
first_close_begun &&
|
|
runtime_test_wait_process_image_gone(child_process_id, fake_git, CHILD_CANCEL_BOUND_MS);
|
|
bool cleanup_backstop_used = child_identity_exact && !child_gone_without_backstop;
|
|
bool child_cleanup_complete = child_gone_without_backstop ||
|
|
(cleanup_backstop_used && runtime_test_force_terminate_verified(
|
|
child_process_id, fake_git));
|
|
if (child_cleanup_complete && !child_gone_without_backstop) {
|
|
child_cleanup_complete = runtime_test_wait_process_image_gone(child_process_id, fake_git,
|
|
CHILD_CLEANUP_BOUND_MS);
|
|
}
|
|
|
|
bool request_completed = request_thread_started && child_cleanup_complete &&
|
|
runtime_test_wait_atomic_bool(&call.completed, CHILD_CLEANUP_BOUND_MS);
|
|
int request_thread_join_rc = request_completed ? cbm_thread_join(&request_thread) : -1;
|
|
if (request_thread_join_rc == 0) {
|
|
request_thread_started = false;
|
|
}
|
|
bool first_transport_interrupted =
|
|
request_completed && call.status == CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR &&
|
|
call.response == NULL && call.response_length == 0;
|
|
bool first_close_finished = false;
|
|
if (first && !request_thread_started) {
|
|
first_close_finished =
|
|
first_close_begun
|
|
? cbm_daemon_runtime_client_close_finish(first, RUNTIME_TEST_TIMEOUT_MS)
|
|
: cbm_daemon_runtime_client_close(first, RUNTIME_TEST_TIMEOUT_MS);
|
|
first = NULL;
|
|
}
|
|
|
|
bool second_usable_after = second && child_cleanup_complete &&
|
|
runtime_real_application_ingest_probe(second) &&
|
|
cbm_daemon_runtime_client_heartbeat(second, RUNTIME_TEST_TIMEOUT_MS);
|
|
bool second_closed = false;
|
|
if (second) {
|
|
second_closed = cbm_daemon_runtime_client_close(second, RUNTIME_TEST_TIMEOUT_MS);
|
|
second = NULL;
|
|
}
|
|
bool exited =
|
|
started && cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
/* Teardown must be leak-free on EVERY path, including the failure path
|
|
* where the request thread never completed: close_begin interrupts the
|
|
* transport, which forces the blocked call to finish, making the join
|
|
* safe; only close_finish releases the client. Skipping the close while
|
|
* the thread was still marked running leaked the client + its connection
|
|
* (LSan, 200 bytes) whenever an earlier stage of this test failed. */
|
|
if (first) {
|
|
if (!first_close_begun) {
|
|
first_close_begun = cbm_daemon_runtime_client_close_begin(first);
|
|
}
|
|
if (request_thread_started) {
|
|
(void)cbm_thread_join(&request_thread);
|
|
request_thread_started = false;
|
|
}
|
|
if (first_close_begun) {
|
|
(void)cbm_daemon_runtime_client_close_finish(first, RUNTIME_TEST_TIMEOUT_MS);
|
|
} else {
|
|
(void)cbm_daemon_runtime_client_close(first, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
first = NULL;
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
bool application_stopped =
|
|
application && cbm_daemon_application_shutdown(application, RUNTIME_TEST_TIMEOUT_MS);
|
|
cbm_daemon_application_free(application);
|
|
|
|
runtime_test_restore_environment(RUNTIME_TEST_BLOCKING_GIT_MARKER_ENV, saved_marker,
|
|
had_marker);
|
|
runtime_test_restore_environment("PATH", saved_path, had_path);
|
|
runtime_test_restore_environment("CBM_CACHE_DIR", saved_cache, had_cache);
|
|
free(call.response);
|
|
free(test_path);
|
|
free(project);
|
|
free(saved_marker);
|
|
free(saved_path);
|
|
free(saved_cache);
|
|
bool files_removed = !work_ready || th_rmtree(work) == 0;
|
|
|
|
if (!only_second_admitted || !child_gone_without_backstop || !request_completed ||
|
|
!second_usable_after) {
|
|
printf(" non-index cancellation diagnostic: close_begun=%d clients_after_wait=%zu "
|
|
"connections_after_wait=%zu child_gone=%d backstop=%d request_completed=%d "
|
|
"request_status=%d second_after=%d\n",
|
|
first_close_begun ? 1 : 0, clients_after_admission_wait,
|
|
connections_after_admission_wait, child_gone_without_backstop ? 1 : 0,
|
|
cleanup_backstop_used ? 1 : 0, request_completed ? 1 : 0, (int)call.status,
|
|
second_usable_after ? 1 : 0);
|
|
}
|
|
|
|
ASSERT_TRUE(snapshots_ok);
|
|
ASSERT_TRUE(paths_ready);
|
|
ASSERT_TRUE(path_bin_ready);
|
|
ASSERT_TRUE(seeded);
|
|
ASSERT_TRUE(environment_ready);
|
|
ASSERT_TRUE(started);
|
|
ASSERT_EQ(first_result.status, CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED);
|
|
ASSERT_EQ(second_result.status, CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED);
|
|
ASSERT_TRUE(contexts_set);
|
|
ASSERT_TRUE(second_usable_before);
|
|
ASSERT_EQ(request_thread_create_rc, 0);
|
|
ASSERT_TRUE(marker_published);
|
|
ASSERT_TRUE(child_identity_exact);
|
|
ASSERT_TRUE(first_close_begun);
|
|
ASSERT_TRUE(only_second_admitted);
|
|
/* The contained non-index child belongs to the first request and must be
|
|
* gone before the independently owned second session is exercised again. */
|
|
ASSERT_TRUE(child_gone_without_backstop);
|
|
ASSERT_FALSE(cleanup_backstop_used);
|
|
ASSERT_TRUE(child_cleanup_complete);
|
|
ASSERT_TRUE(request_completed);
|
|
ASSERT_EQ(request_thread_join_rc, 0);
|
|
ASSERT_TRUE(first_transport_interrupted);
|
|
/* close_begin interrupted an active exchange, so there is intentionally no
|
|
* DISCONNECT acknowledgement even though server-side teardown completes. */
|
|
ASSERT_FALSE(first_close_finished);
|
|
ASSERT_TRUE(second_usable_after);
|
|
ASSERT_TRUE(second_closed);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_TRUE(application_stopped);
|
|
ASSERT_TRUE(files_removed);
|
|
PASS();
|
|
#endif
|
|
}
|
|
|
|
TEST(daemon_runtime_noncooperative_callback_does_not_detach_or_unbound_stop) {
|
|
static const uint8_t request[] = {'i', 'g', 'n', 'o', 'r', 'e'};
|
|
/* STOP_OBSERVED_MAX_MS is a coarse hang-detector, not a tight latency
|
|
* bound: the invariant is that a deadline-bounded stop RETURNS (proven by
|
|
* reaching the assertions below — a hang would deadlock the test) rather
|
|
* than blocking indefinitely on the stuck callback. The window is well
|
|
* above the STOP_BOUND_MS deadline yet far below a real hang, so heavy
|
|
* scheduler starvation (the CBM_LOCAL_CI_CPUS=4 fidelity pass, CI runners)
|
|
* cannot make it test-significant. */
|
|
enum { STOP_BOUND_MS = 50, STOP_OBSERVED_MAX_MS = 5000 };
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_application_context_t context;
|
|
runtime_application_context_init(&context, true);
|
|
atomic_store_explicit(&context.ignore_first_request_cancel, true, memory_order_release);
|
|
atomic_bool request_thread_completed;
|
|
atomic_init(&request_thread_completed, false);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start_application(&fixture, "application-noncooperative",
|
|
&identity, &context);
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
runtime_application_client_call_t call = {
|
|
.request = request,
|
|
.request_length = (uint32_t)sizeof(request),
|
|
.completed = &request_thread_completed,
|
|
.status = CBM_DAEMON_RUNTIME_APPLICATION_OK,
|
|
};
|
|
cbm_thread_t request_thread;
|
|
int request_thread_create_rc = -1;
|
|
int request_thread_join_rc = -1;
|
|
bool request_thread_started = false;
|
|
bool callback_started = false;
|
|
bool close_begun = false;
|
|
bool stop_returned = true;
|
|
uint64_t stop_elapsed_ms = UINT64_MAX;
|
|
bool exited_before_release = false;
|
|
bool close_finished = false;
|
|
bool exited_after_release = false;
|
|
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
if (client) {
|
|
call.client = client;
|
|
request_thread_create_rc = cbm_thread_create(
|
|
&request_thread, 128U * 1024U, runtime_application_client_request_thread, &call);
|
|
request_thread_started = request_thread_create_rc == 0;
|
|
callback_started =
|
|
request_thread_started &&
|
|
runtime_test_wait_atomic_bool(&context.first_request_started, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (callback_started) {
|
|
close_begun = cbm_daemon_runtime_client_close_begin(client);
|
|
uint64_t stop_started_ms = cbm_now_ms();
|
|
stop_returned = cbm_daemon_runtime_service_stop(fixture.service, STOP_BOUND_MS);
|
|
stop_elapsed_ms = cbm_now_ms() - stop_started_ms;
|
|
exited_before_release =
|
|
cbm_daemon_runtime_service_state(fixture.service) == CBM_DAEMON_RUNTIME_SERVICE_EXITED;
|
|
}
|
|
|
|
/* A bounded stop failure retains every callback/session allocation. The
|
|
* test supplies eventual cooperation so the runner can prove clean join
|
|
* and teardown after observing the production host's force boundary. */
|
|
atomic_store_explicit(&context.release_first_request, true, memory_order_release);
|
|
if (request_thread_started) {
|
|
request_thread_join_rc = cbm_thread_join(&request_thread);
|
|
if (request_thread_join_rc == 0) {
|
|
request_thread_started = false;
|
|
}
|
|
}
|
|
if (request_thread_started) {
|
|
while (!atomic_load_explicit(&request_thread_completed, memory_order_acquire)) {
|
|
struct timespec pause = {.tv_sec = 0, .tv_nsec = 1000000};
|
|
(void)cbm_nanosleep(&pause, NULL);
|
|
}
|
|
}
|
|
if (client) {
|
|
close_finished =
|
|
close_begun ? cbm_daemon_runtime_client_close_finish(client, RUNTIME_TEST_TIMEOUT_MS)
|
|
: cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
client = NULL;
|
|
}
|
|
if (started) {
|
|
exited_after_release =
|
|
cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
free(call.response);
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_EQ(result.status, CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED);
|
|
ASSERT_EQ(request_thread_create_rc, 0);
|
|
ASSERT_TRUE(callback_started);
|
|
ASSERT_TRUE(close_begun);
|
|
ASSERT_FALSE(stop_returned);
|
|
ASSERT_TRUE(stop_elapsed_ms <= STOP_OBSERVED_MAX_MS);
|
|
ASSERT_FALSE(exited_before_release);
|
|
ASSERT_EQ(request_thread_join_rc, 0);
|
|
/* The interrupted transport cannot receive a final disconnect ACK. */
|
|
ASSERT_FALSE(close_finished);
|
|
ASSERT_TRUE(exited_after_release);
|
|
ASSERT_EQ(atomic_load(&context.opened), 1);
|
|
ASSERT_EQ(atomic_load(&context.requests), 1);
|
|
ASSERT_EQ(atomic_load(&context.cancelled), 1);
|
|
ASSERT_EQ(atomic_load(&context.closed), 1);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_application_busy_cap_and_malformed_are_isolated) {
|
|
static const uint8_t blocking_request[] = {'f', 'i', 'r', 's', 't'};
|
|
static const uint8_t busy_request[] = {'b', 'u', 's', 'y'};
|
|
static const uint8_t malformed_request[] = {'x', 'y'};
|
|
static const uint8_t valid_request[] = {0xde, 0xad, 0x00, 0xbe, 0xef};
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_application_context_t context;
|
|
runtime_application_context_init(&context, true);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start_application(&fixture, "application-isolation",
|
|
&identity, &context);
|
|
cbm_daemon_runtime_client_t *owner = NULL;
|
|
cbm_daemon_runtime_connect_result_t owner_result = {0};
|
|
cbm_daemon_ipc_connection_t *raw = NULL;
|
|
bool first_sent = false;
|
|
bool callback_started = false;
|
|
bool busy_sent = false;
|
|
bool busy_rejected = false;
|
|
bool malformed_sent = false;
|
|
bool bad_peer_released = false;
|
|
uint8_t sentinel = 0x5a;
|
|
uint8_t *oversize_response = (uint8_t *)&sentinel;
|
|
uint32_t oversize_response_length = UINT32_MAX;
|
|
cbm_daemon_runtime_application_status_t oversize_status = CBM_DAEMON_RUNTIME_APPLICATION_OK;
|
|
uint8_t *valid_response = NULL;
|
|
uint32_t valid_response_length = 0;
|
|
cbm_daemon_runtime_application_status_t valid_status =
|
|
CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
|
|
bool owner_survived = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
owner = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &owner_result);
|
|
raw = runtime_test_raw_client_connect(fixture.endpoint, &identity);
|
|
}
|
|
if (owner && raw) {
|
|
first_sent = runtime_test_raw_application_send(raw, blocking_request,
|
|
(uint32_t)sizeof(blocking_request),
|
|
(uint32_t)sizeof(blocking_request));
|
|
callback_started =
|
|
first_sent &&
|
|
runtime_test_wait_atomic_bool(&context.first_request_started, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (callback_started) {
|
|
busy_sent = runtime_test_raw_application_send(
|
|
raw, busy_request, (uint32_t)sizeof(busy_request), (uint32_t)sizeof(busy_request));
|
|
busy_rejected = busy_sent && runtime_test_raw_application_receive_status(
|
|
raw, CBM_DAEMON_RUNTIME_APPLICATION_BUSY);
|
|
}
|
|
if (busy_rejected) {
|
|
malformed_sent = runtime_test_raw_application_send(
|
|
raw, malformed_request, (uint32_t)sizeof(malformed_request),
|
|
(uint32_t)sizeof(malformed_request) + 1U);
|
|
}
|
|
cbm_daemon_ipc_connection_close(raw);
|
|
raw = NULL;
|
|
if (malformed_sent) {
|
|
bad_peer_released = cbm_daemon_runtime_service_wait_for_clients(fixture.service, 1,
|
|
RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (owner && bad_peer_released) {
|
|
oversize_status = cbm_daemon_runtime_client_application_request(
|
|
owner, &sentinel, CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX + 1U, &oversize_response,
|
|
&oversize_response_length, RUNTIME_TEST_TIMEOUT_MS);
|
|
valid_status = cbm_daemon_runtime_client_application_request(
|
|
owner, valid_request, (uint32_t)sizeof(valid_request), &valid_response,
|
|
&valid_response_length, RUNTIME_TEST_TIMEOUT_MS);
|
|
owner_survived = oversize_status == CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR &&
|
|
oversize_response == NULL && oversize_response_length == 0 &&
|
|
valid_status == CBM_DAEMON_RUNTIME_APPLICATION_OK &&
|
|
valid_response_length == sizeof(valid_request) && valid_response &&
|
|
memcmp(valid_response, valid_request, sizeof(valid_request)) == 0 &&
|
|
cbm_daemon_runtime_client_heartbeat(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
free(valid_response);
|
|
valid_response = NULL;
|
|
(void)cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
owner = NULL;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
free(valid_response);
|
|
if (owner) {
|
|
(void)cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
cbm_daemon_ipc_connection_close(raw);
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(first_sent);
|
|
ASSERT_TRUE(callback_started);
|
|
ASSERT_TRUE(busy_sent);
|
|
ASSERT_TRUE(busy_rejected);
|
|
ASSERT_TRUE(malformed_sent);
|
|
ASSERT_TRUE(bad_peer_released);
|
|
ASSERT_TRUE(owner_survived);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_EQ(atomic_load(&context.opened), 2);
|
|
ASSERT_EQ(atomic_load(&context.requests), 2);
|
|
ASSERT_EQ(atomic_load(&context.cancelled), 2);
|
|
ASSERT_EQ(atomic_load(&context.closed), 2);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_malformed_and_zero_cancel_close_only_offending_connections) {
|
|
static const uint8_t valid_request[] = {'s', 'u', 'r', 'v', 'i', 'v', 'e'};
|
|
static const uint8_t malformed_cancel[7] = {1};
|
|
static const uint8_t zero_token_cancel[8] = {0};
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_application_context_t context;
|
|
runtime_application_context_init(&context, false);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start_application(&fixture, "application-cancel-isolation",
|
|
&identity, &context);
|
|
cbm_daemon_runtime_connect_result_t owner_result = {0};
|
|
cbm_daemon_runtime_client_t *owner = NULL;
|
|
cbm_daemon_ipc_connection_t *raw = NULL;
|
|
bool malformed_connected = false;
|
|
bool malformed_sent = false;
|
|
bool malformed_released = false;
|
|
bool malformed_closed = false;
|
|
bool zero_connected = false;
|
|
bool zero_sent = false;
|
|
bool zero_released = false;
|
|
bool zero_closed = false;
|
|
uint8_t *valid_response = NULL;
|
|
uint32_t valid_response_length = 0;
|
|
cbm_daemon_runtime_application_status_t valid_status =
|
|
CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
|
|
bool owner_survived = false;
|
|
bool owner_closed = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
owner = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &owner_result);
|
|
}
|
|
if (owner) {
|
|
raw = runtime_test_raw_client_connect(fixture.endpoint, &identity);
|
|
malformed_connected = raw != NULL;
|
|
}
|
|
if (raw) {
|
|
malformed_sent = cbm_daemon_ipc_send_frame(
|
|
raw, CBM_DAEMON_FRAME_REQUEST, CBM_DAEMON_RUNTIME_OP_APPLICATION_CANCEL,
|
|
malformed_cancel, (uint32_t)sizeof(malformed_cancel));
|
|
}
|
|
if (malformed_sent) {
|
|
malformed_released = cbm_daemon_runtime_service_wait_for_clients(fixture.service, 1,
|
|
RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (malformed_released) {
|
|
cbm_daemon_frame_t frame = {0};
|
|
uint8_t *payload = NULL;
|
|
int received = cbm_daemon_ipc_receive_frame(raw, RUNTIME_TEST_TIMEOUT_MS, &frame, &payload);
|
|
malformed_closed = received != 1;
|
|
free(payload);
|
|
}
|
|
cbm_daemon_ipc_connection_close(raw);
|
|
raw = NULL;
|
|
|
|
if (owner && malformed_closed) {
|
|
raw = runtime_test_raw_client_connect(fixture.endpoint, &identity);
|
|
zero_connected = raw != NULL;
|
|
}
|
|
if (raw) {
|
|
zero_sent = cbm_daemon_ipc_send_frame(
|
|
raw, CBM_DAEMON_FRAME_REQUEST, CBM_DAEMON_RUNTIME_OP_APPLICATION_CANCEL,
|
|
zero_token_cancel, (uint32_t)sizeof(zero_token_cancel));
|
|
}
|
|
if (zero_sent) {
|
|
zero_released = cbm_daemon_runtime_service_wait_for_clients(fixture.service, 1,
|
|
RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (zero_released) {
|
|
cbm_daemon_frame_t frame = {0};
|
|
uint8_t *payload = NULL;
|
|
int received = cbm_daemon_ipc_receive_frame(raw, RUNTIME_TEST_TIMEOUT_MS, &frame, &payload);
|
|
zero_closed = received != 1;
|
|
free(payload);
|
|
}
|
|
cbm_daemon_ipc_connection_close(raw);
|
|
raw = NULL;
|
|
|
|
if (owner && zero_closed) {
|
|
valid_status = cbm_daemon_runtime_client_application_request(
|
|
owner, valid_request, (uint32_t)sizeof(valid_request), &valid_response,
|
|
&valid_response_length, RUNTIME_TEST_TIMEOUT_MS);
|
|
owner_survived = valid_status == CBM_DAEMON_RUNTIME_APPLICATION_OK &&
|
|
valid_response_length == sizeof(valid_request) && valid_response &&
|
|
memcmp(valid_response, valid_request, sizeof(valid_request)) == 0 &&
|
|
cbm_daemon_runtime_client_heartbeat(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
free(valid_response);
|
|
if (owner) {
|
|
owner_closed = cbm_daemon_runtime_client_close(owner, RUNTIME_TEST_TIMEOUT_MS);
|
|
owner = NULL;
|
|
}
|
|
if (started) {
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
cbm_daemon_ipc_connection_close(raw);
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(malformed_connected);
|
|
ASSERT_TRUE(malformed_sent);
|
|
ASSERT_TRUE(malformed_released);
|
|
ASSERT_TRUE(malformed_closed);
|
|
ASSERT_TRUE(zero_connected);
|
|
ASSERT_TRUE(zero_sent);
|
|
ASSERT_TRUE(zero_released);
|
|
ASSERT_TRUE(zero_closed);
|
|
ASSERT_TRUE(owner_survived);
|
|
ASSERT_TRUE(owner_closed);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_EQ(atomic_load(&context.opened), 3);
|
|
ASSERT_EQ(atomic_load(&context.requests), 1);
|
|
ASSERT_EQ(atomic_load(&context.request_cancels), 0);
|
|
ASSERT_EQ(atomic_load(&context.cancelled), 3);
|
|
ASSERT_EQ(atomic_load(&context.closed), 3);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_kernel_process_fingerprint_is_stable_and_fail_closed) {
|
|
char first[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
char repeated[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
char invalid[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
bool first_ok = cbm_daemon_runtime_process_build_fingerprint(runtime_test_process_id(), first);
|
|
bool repeated_ok =
|
|
cbm_daemon_runtime_process_build_fingerprint(runtime_test_process_id(), repeated);
|
|
bool invalid_rejected = !cbm_daemon_runtime_process_build_fingerprint(UINT64_MAX, invalid);
|
|
|
|
ASSERT_TRUE(first_ok);
|
|
ASSERT_TRUE(repeated_ok);
|
|
ASSERT_TRUE(runtime_test_is_fingerprint(first));
|
|
ASSERT_STR_EQ(first, repeated);
|
|
ASSERT_TRUE(invalid_rejected);
|
|
ASSERT_STR_EQ(invalid, "");
|
|
PASS();
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
TEST(daemon_runtime_process_fingerprint_supports_extended_length_image) {
|
|
static const char segment[] = "/segment-abcdefghijklmnopqrstuvwxyz-0123456789";
|
|
char root[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char directory[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char image_path[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char event_name[128] = {0};
|
|
int root_written =
|
|
snprintf(root, sizeof(root), "%s/cbm-runtime-long-image-XXXXXX", cbm_tmpdir());
|
|
bool root_created =
|
|
root_written > 0 && root_written < (int)sizeof(root) && cbm_mkdtemp(root) != NULL;
|
|
bool path_built = root_created && snprintf(directory, sizeof(directory), "%s", root) > 0;
|
|
for (size_t index = 0; path_built && index < 7U; index++) {
|
|
size_t used = strlen(directory);
|
|
int appended = snprintf(directory + used, sizeof(directory) - used, "%s", segment);
|
|
path_built = appended > 0 && (size_t)appended < sizeof(directory) - used;
|
|
}
|
|
bool directory_created = path_built && cbm_mkdir_p(directory, 0700);
|
|
int image_written = directory_created ? snprintf(image_path, sizeof(image_path),
|
|
"%s/test-runner-long-image.exe", directory)
|
|
: -1;
|
|
wchar_t *ordinary_wide = image_written > 0 && image_written < (int)sizeof(image_path)
|
|
? cbm_utf8_to_wide(image_path)
|
|
: NULL;
|
|
bool exceeds_legacy_limit = ordinary_wide && wcslen(ordinary_wide) >= MAX_PATH;
|
|
free(ordinary_wide);
|
|
bool copied = exceeds_legacy_limit && runtime_test_windows_copy_self(image_path);
|
|
const char *expected = runtime_test_self_build();
|
|
|
|
int event_written =
|
|
snprintf(event_name, sizeof(event_name), "Local\\cbm-runtime-long-image-%lu-%llu",
|
|
(unsigned long)GetCurrentProcessId(), (unsigned long long)GetTickCount64());
|
|
HANDLE ready_event = copied && expected && expected[0] && event_written > 0 &&
|
|
event_written < (int)sizeof(event_name)
|
|
? CreateEventA(NULL, TRUE, FALSE, event_name)
|
|
: NULL;
|
|
bool event_private = ready_event && GetLastError() != ERROR_ALREADY_EXISTS;
|
|
PROCESS_INFORMATION process;
|
|
memset(&process, 0, sizeof(process));
|
|
bool spawned =
|
|
event_private && runtime_test_windows_spawn_image_holder(image_path, event_name, &process);
|
|
bool ready = spawned && WaitForSingleObject(ready_event, 5000U) == WAIT_OBJECT_0;
|
|
|
|
wchar_t process_path[32768];
|
|
DWORD process_path_length = (DWORD)(sizeof(process_path) / sizeof(process_path[0]));
|
|
bool queried_long_image =
|
|
ready &&
|
|
QueryFullProcessImageNameW(process.hProcess, 0U, process_path, &process_path_length) != 0 &&
|
|
process_path_length >= MAX_PATH;
|
|
char observed[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
bool fingerprinted = queried_long_image && cbm_daemon_runtime_process_build_fingerprint(
|
|
(uint64_t)process.dwProcessId, observed);
|
|
bool exact = fingerprinted && strcmp(observed, expected) == 0;
|
|
|
|
bool stopped = !spawned;
|
|
if (spawned) {
|
|
bool termination_requested = TerminateProcess(process.hProcess, 30U) != 0;
|
|
stopped =
|
|
termination_requested && WaitForSingleObject(process.hProcess, 5000U) == WAIT_OBJECT_0;
|
|
(void)CloseHandle(process.hProcess);
|
|
}
|
|
if (ready_event) {
|
|
(void)CloseHandle(ready_event);
|
|
}
|
|
bool image_removed = !copied || cbm_unlink(image_path) == 0;
|
|
bool tree_removed = true;
|
|
if (root_created) {
|
|
char cleanup[RUNTIME_TEST_PATH_CAP];
|
|
(void)snprintf(cleanup, sizeof(cleanup), "%s", directory_created ? directory : root);
|
|
for (;;) {
|
|
tree_removed = cbm_rmdir(cleanup) == 0 && tree_removed;
|
|
if (strcmp(cleanup, root) == 0) {
|
|
break;
|
|
}
|
|
char *separator = strrchr(cleanup, '/');
|
|
if (!separator || separator < cleanup + strlen(root)) {
|
|
tree_removed = false;
|
|
break;
|
|
}
|
|
*separator = '\0';
|
|
}
|
|
}
|
|
|
|
ASSERT_TRUE(root_created);
|
|
ASSERT_TRUE(path_built);
|
|
ASSERT_TRUE(directory_created);
|
|
ASSERT_TRUE(exceeds_legacy_limit);
|
|
ASSERT_TRUE(copied);
|
|
ASSERT_TRUE(runtime_test_is_fingerprint(expected));
|
|
ASSERT_TRUE(event_private);
|
|
ASSERT_TRUE(spawned);
|
|
ASSERT_TRUE(ready);
|
|
ASSERT_TRUE(queried_long_image);
|
|
ASSERT_TRUE(fingerprinted);
|
|
ASSERT_TRUE(exact);
|
|
ASSERT_TRUE(stopped);
|
|
ASSERT_TRUE(image_removed);
|
|
ASSERT_TRUE(tree_removed);
|
|
PASS();
|
|
}
|
|
#endif
|
|
|
|
#ifdef __APPLE__
|
|
/* RED: the former macOS fast path accepted the daemon vnode in any RX mapping,
|
|
* even when a differently fingerprinted main executable owned the connection.
|
|
* Re-signing the changed copy keeps the probe runnable on arm64 while ensuring
|
|
* that only the deliberately injected mapping has the active identity. */
|
|
TEST(daemon_runtime_mac_fast_path_rejects_foreign_main_image_mapping_active) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
char directory[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char active_image[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char foreign_image[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char foreign_fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
int directory_written =
|
|
snprintf(directory, sizeof(directory), "%s/cbm-runtime-mapped-image-XXXXXX", cbm_tmpdir());
|
|
bool directory_created = directory_written > 0 && directory_written < (int)sizeof(directory) &&
|
|
cbm_mkdtemp(directory) != NULL;
|
|
int foreign_written = directory_created ? snprintf(foreign_image, sizeof(foreign_image),
|
|
"%s/foreign-client", directory)
|
|
: -1;
|
|
bool active_resolved = runtime_test_self_image_path(active_image);
|
|
bool foreign_copied = active_resolved && foreign_written > 0 &&
|
|
foreign_written < (int)sizeof(foreign_image) &&
|
|
runtime_test_copy_executable(active_image, foreign_image);
|
|
/* A distinct signing identifier changes the Mach-O signature bytes while
|
|
* keeping the copied executable valid under macOS strict validation. */
|
|
bool foreign_changed = foreign_copied;
|
|
bool foreign_signed = foreign_changed && runtime_test_mac_ad_hoc_sign(foreign_image);
|
|
bool foreign_fingerprinted =
|
|
foreign_signed && cbm_daemon_build_fingerprint_file(foreign_image, foreign_fingerprint);
|
|
bool fingerprint_differs =
|
|
foreign_fingerprinted && strcmp(foreign_fingerprint, identity.build_fingerprint) != 0;
|
|
|
|
runtime_test_fixture_t fixture;
|
|
memset(&fixture, 0, sizeof(fixture));
|
|
bool fixture_attempted = directory_created && active_resolved && fingerprint_differs;
|
|
bool started =
|
|
fixture_attempted && runtime_test_fixture_start(&fixture, "mapped-foreign", &identity);
|
|
int foreign_exit = -1;
|
|
bool foreign_ran =
|
|
started && runtime_test_run_mapped_hello_image(foreign_image, active_image, &fixture,
|
|
&identity, &foreign_exit);
|
|
|
|
if (fixture_attempted) {
|
|
runtime_test_fixture_finish(&fixture);
|
|
}
|
|
if (foreign_written > 0 && foreign_written < (int)sizeof(foreign_image)) {
|
|
(void)cbm_unlink(foreign_image);
|
|
}
|
|
if (directory_created) {
|
|
(void)cbm_rmdir(directory);
|
|
}
|
|
|
|
ASSERT_TRUE(directory_created);
|
|
ASSERT_TRUE(active_resolved);
|
|
ASSERT_TRUE(foreign_copied);
|
|
ASSERT_TRUE(foreign_changed);
|
|
ASSERT_TRUE(foreign_signed);
|
|
ASSERT_TRUE(foreign_fingerprinted);
|
|
ASSERT_TRUE(fingerprint_differs);
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(foreign_ran);
|
|
ASSERT_EQ(foreign_exit, 26);
|
|
PASS();
|
|
}
|
|
#endif
|
|
|
|
TEST(daemon_runtime_copied_image_fallback_accepts_identical_and_rejects_changed) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t identical_fixture;
|
|
bool identical_started =
|
|
runtime_test_fixture_start(&identical_fixture, "copied-identical", &identity);
|
|
char identical_path[RUNTIME_TEST_PATH_CAP] = {0};
|
|
int identical_path_written = identical_started
|
|
#ifdef _WIN32
|
|
? snprintf(identical_path, sizeof(identical_path),
|
|
"%s/client-copy.exe", identical_fixture.parent)
|
|
#else
|
|
? snprintf(identical_path, sizeof(identical_path),
|
|
"%s/client-copy", identical_fixture.parent)
|
|
#endif
|
|
: -1;
|
|
bool identical_copied = identical_path_written > 0 &&
|
|
identical_path_written < (int)sizeof(identical_path) &&
|
|
runtime_test_copy_self_image(identical_path);
|
|
char identical_fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
bool identical_bytes =
|
|
identical_copied &&
|
|
cbm_daemon_build_fingerprint_file(identical_path, identical_fingerprint) &&
|
|
strcmp(identical_fingerprint, identity.build_fingerprint) == 0;
|
|
int identical_exit = -1;
|
|
bool identical_ran =
|
|
identical_bytes && runtime_test_run_hello_image(identical_path, &identical_fixture,
|
|
&identity, &identical_exit);
|
|
bool identical_exited =
|
|
identical_ran && identical_exit == 0 &&
|
|
cbm_daemon_runtime_service_wait_exited(identical_fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
(void)cbm_unlink(identical_path);
|
|
runtime_test_fixture_finish(&identical_fixture);
|
|
|
|
#ifdef __linux__
|
|
runtime_test_fixture_t changed_fixture;
|
|
bool changed_started =
|
|
runtime_test_fixture_start(&changed_fixture, "copied-changed", &identity);
|
|
char changed_path[RUNTIME_TEST_PATH_CAP] = {0};
|
|
int changed_path_written = changed_started ? snprintf(changed_path, sizeof(changed_path),
|
|
"%s/client-copy", changed_fixture.parent)
|
|
: -1;
|
|
bool changed_copied = changed_path_written > 0 &&
|
|
changed_path_written < (int)sizeof(changed_path) &&
|
|
runtime_test_copy_self_image(changed_path) &&
|
|
runtime_test_append_image_marker(changed_path);
|
|
char changed_fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
bool changed_bytes = changed_copied &&
|
|
cbm_daemon_build_fingerprint_file(changed_path, changed_fingerprint) &&
|
|
strcmp(changed_fingerprint, identity.build_fingerprint) != 0;
|
|
int changed_exit = -1;
|
|
bool changed_ran = changed_bytes && runtime_test_run_hello_image(changed_path, &changed_fixture,
|
|
&identity, &changed_exit);
|
|
(void)cbm_unlink(changed_path);
|
|
runtime_test_fixture_finish(&changed_fixture);
|
|
#endif
|
|
|
|
ASSERT_TRUE(identical_started);
|
|
ASSERT_TRUE(identical_copied);
|
|
ASSERT_TRUE(identical_bytes);
|
|
ASSERT_TRUE(identical_ran);
|
|
ASSERT_EQ(identical_exit, 0);
|
|
ASSERT_TRUE(identical_exited);
|
|
#ifdef __linux__
|
|
ASSERT_TRUE(changed_started);
|
|
ASSERT_TRUE(changed_copied);
|
|
ASSERT_TRUE(changed_bytes);
|
|
ASSERT_TRUE(changed_ran);
|
|
ASSERT_EQ(changed_exit, 26);
|
|
#endif
|
|
PASS();
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path) {
|
|
char directory[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char image_path[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char replacement_path[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char event_name[128] = {0};
|
|
int directory_written =
|
|
snprintf(directory, sizeof(directory), "%s/cbm-runtime-image-XXXXXX", cbm_tmpdir());
|
|
bool setup = directory_written > 0 && directory_written < (int)sizeof(directory) &&
|
|
cbm_mkdtemp(directory) != NULL;
|
|
int image_written =
|
|
setup ? snprintf(image_path, sizeof(image_path), "%s/image.exe", directory) : -1;
|
|
int replacement_written = setup ? snprintf(replacement_path, sizeof(replacement_path),
|
|
"%s/replacement.exe", directory)
|
|
: -1;
|
|
int event_written =
|
|
snprintf(event_name, sizeof(event_name), "Local\\cbm-runtime-image-%lu-%llu",
|
|
(unsigned long)GetCurrentProcessId(), (unsigned long long)GetTickCount64());
|
|
setup = setup && image_written > 0 && image_written < (int)sizeof(image_path) &&
|
|
replacement_written > 0 && replacement_written < (int)sizeof(replacement_path) &&
|
|
event_written > 0 && event_written < (int)sizeof(event_name) &&
|
|
runtime_test_windows_copy_self(image_path);
|
|
|
|
FILE *replacement_file = setup ? cbm_fopen(replacement_path, "wb") : NULL;
|
|
bool replacement_written_ok =
|
|
replacement_file && fputs("cbm-windows-replacement-image", replacement_file) >= 0;
|
|
if (replacement_file) {
|
|
replacement_written_ok = fclose(replacement_file) == 0 && replacement_written_ok;
|
|
}
|
|
setup = setup && replacement_written_ok;
|
|
|
|
char original[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
char replacement[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
char observed[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
setup = setup && cbm_daemon_build_fingerprint_file(image_path, original) &&
|
|
cbm_daemon_build_fingerprint_file(replacement_path, replacement) &&
|
|
strcmp(original, replacement) != 0;
|
|
|
|
HANDLE ready_event = setup ? CreateEventA(NULL, TRUE, FALSE, event_name) : NULL;
|
|
bool event_private = ready_event && GetLastError() != ERROR_ALREADY_EXISTS;
|
|
PROCESS_INFORMATION process;
|
|
memset(&process, 0, sizeof(process));
|
|
bool spawned =
|
|
event_private && runtime_test_windows_spawn_image_holder(image_path, event_name, &process);
|
|
bool ready = spawned && WaitForSingleObject(ready_event, 5000) == WAIT_OBJECT_0;
|
|
bool replaced = ready && runtime_test_windows_posix_replace(replacement_path, image_path);
|
|
bool fingerprinted = ready && cbm_daemon_runtime_process_build_fingerprint(
|
|
(uint64_t)process.dwProcessId, observed);
|
|
bool replacement_safe = replaced ? (!fingerprinted || strcmp(observed, original) == 0)
|
|
: (fingerprinted && strcmp(observed, original) == 0);
|
|
|
|
bool stopped = !spawned;
|
|
if (spawned) {
|
|
DWORD exit_code = 0;
|
|
bool running =
|
|
GetExitCodeProcess(process.hProcess, &exit_code) != 0 && exit_code == STILL_ACTIVE;
|
|
bool termination_requested = !running || TerminateProcess(process.hProcess, 26) != 0;
|
|
stopped =
|
|
termination_requested && WaitForSingleObject(process.hProcess, 5000) == WAIT_OBJECT_0;
|
|
(void)CloseHandle(process.hProcess);
|
|
}
|
|
if (ready_event) {
|
|
(void)CloseHandle(ready_event);
|
|
}
|
|
(void)cbm_unlink(replacement_path);
|
|
(void)cbm_unlink(image_path);
|
|
(void)cbm_rmdir(directory);
|
|
|
|
ASSERT_TRUE(setup);
|
|
ASSERT_TRUE(event_private);
|
|
ASSERT_TRUE(spawned);
|
|
ASSERT_TRUE(ready);
|
|
ASSERT_TRUE(stopped);
|
|
ASSERT_TRUE(replacement_safe);
|
|
ASSERT_TRUE(!fingerprinted || strcmp(observed, replacement) != 0);
|
|
PASS();
|
|
}
|
|
#elif defined(__APPLE__) || defined(__linux__)
|
|
TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path) {
|
|
char directory[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char image_path[RUNTIME_TEST_PATH_CAP] = {0};
|
|
char replacement_path[RUNTIME_TEST_PATH_CAP] = {0};
|
|
int directory_written =
|
|
snprintf(directory, sizeof(directory), "%s/cbm-runtime-image-XXXXXX", cbm_tmpdir());
|
|
bool setup = directory_written > 0 && directory_written < (int)sizeof(directory) &&
|
|
cbm_mkdtemp(directory) != NULL;
|
|
int image_written =
|
|
setup ? snprintf(image_path, sizeof(image_path), "%s/image", directory) : -1;
|
|
int replacement_written =
|
|
setup ? snprintf(replacement_path, sizeof(replacement_path), "%s/replacement", directory)
|
|
: -1;
|
|
setup = setup && image_written > 0 && image_written < (int)sizeof(image_path) &&
|
|
replacement_written > 0 && replacement_written < (int)sizeof(replacement_path) &&
|
|
runtime_test_copy_executable("/bin/cat", image_path) &&
|
|
runtime_test_copy_executable("/bin/echo", replacement_path);
|
|
|
|
char original[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
char replacement[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
char observed[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
setup = setup && cbm_daemon_build_fingerprint_file(image_path, original);
|
|
int release_fd = -1;
|
|
pid_t child = setup ? runtime_test_spawn_blocked_executable(image_path, &release_fd) : -1;
|
|
setup = setup && child > 0 && rename(replacement_path, image_path) == 0 &&
|
|
cbm_daemon_build_fingerprint_file(image_path, replacement) &&
|
|
strcmp(original, replacement) != 0;
|
|
bool fingerprinted =
|
|
setup && cbm_daemon_runtime_process_build_fingerprint((uint64_t)child, observed);
|
|
|
|
runtime_test_stop_blocked_executable(child, release_fd);
|
|
(void)unlink(replacement_path);
|
|
(void)unlink(image_path);
|
|
(void)rmdir(directory);
|
|
|
|
ASSERT_TRUE(setup);
|
|
#ifdef __linux__
|
|
/* /proc/<pid>/exe is an openable kernel link to the mapped inode, even
|
|
* after that inode has been unlinked by the atomic replacement. */
|
|
ASSERT_TRUE(fingerprinted);
|
|
ASSERT_STR_EQ(observed, original);
|
|
#else
|
|
/* macOS exposes mapped vnode identity but not a public handle to the
|
|
* mapped executable. If the old vnode no longer has an openable path we
|
|
* must fail closed; resolving and hashing the new path is forbidden. */
|
|
ASSERT_TRUE(!fingerprinted || strcmp(observed, original) == 0);
|
|
#endif
|
|
ASSERT_TRUE(!fingerprinted || strcmp(observed, replacement) != 0);
|
|
PASS();
|
|
}
|
|
#endif
|
|
|
|
TEST(daemon_runtime_close_begin_releases_admission_with_inflight_request) {
|
|
static const uint8_t request[] = {'b', 'l', 'o', 'c', 'k'};
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_application_context_t context;
|
|
runtime_application_context_init(&context, true);
|
|
atomic_bool request_thread_completed;
|
|
atomic_init(&request_thread_completed, false);
|
|
runtime_test_fixture_t fixture;
|
|
bool started = runtime_test_fixture_start_application(&fixture, "close-intent-admission",
|
|
&identity, &context);
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
cbm_daemon_runtime_connect_result_t result = {0};
|
|
runtime_application_client_call_t call = {
|
|
.request = request,
|
|
.request_length = (uint32_t)sizeof(request),
|
|
.completed = &request_thread_completed,
|
|
.status = CBM_DAEMON_RUNTIME_APPLICATION_OK,
|
|
};
|
|
cbm_thread_t request_thread;
|
|
int request_thread_create_rc = -1;
|
|
bool request_thread_started = false;
|
|
bool callback_started = false;
|
|
bool close_begun = false;
|
|
bool admission_released_at_begin = false;
|
|
bool request_thread_joined = false;
|
|
bool request_ended_without_payload = false;
|
|
bool exited = false;
|
|
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &result);
|
|
}
|
|
if (client) {
|
|
call.client = client;
|
|
request_thread_create_rc = cbm_thread_create(
|
|
&request_thread, 128U * 1024U, runtime_application_client_request_thread, &call);
|
|
request_thread_started = request_thread_create_rc == 0;
|
|
callback_started =
|
|
request_thread_started &&
|
|
runtime_test_wait_atomic_bool(&context.first_request_started, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
if (callback_started) {
|
|
close_begun = cbm_daemon_runtime_client_close_begin(client);
|
|
}
|
|
/* The parity contract: after close_begin alone — before the handle
|
|
* closes — the daemon has released this client's admission. POSIX learns
|
|
* through shutdown()/EOF; Windows through the CLOSE_INTENT frame. */
|
|
admission_released_at_begin = close_begun && cbm_daemon_runtime_service_wait_for_clients(
|
|
fixture.service, 0, RUNTIME_TEST_TIMEOUT_MS);
|
|
if (request_thread_started) {
|
|
request_thread_joined = cbm_thread_join(&request_thread) == 0;
|
|
request_thread_started = false;
|
|
}
|
|
request_ended_without_payload =
|
|
request_thread_joined &&
|
|
(call.status == CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR ||
|
|
call.status == CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED) &&
|
|
call.response == NULL && call.response_length == 0;
|
|
if (client) {
|
|
(void)(close_begun ? cbm_daemon_runtime_client_close_finish(client, RUNTIME_TEST_TIMEOUT_MS)
|
|
: cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS));
|
|
client = NULL;
|
|
}
|
|
if (started) {
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
free(call.response);
|
|
runtime_test_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_EQ(request_thread_create_rc, 0);
|
|
ASSERT_TRUE(callback_started);
|
|
ASSERT_TRUE(close_begun);
|
|
ASSERT_TRUE(admission_released_at_begin);
|
|
ASSERT_TRUE(request_thread_joined);
|
|
ASSERT_TRUE(request_ended_without_payload);
|
|
ASSERT_TRUE(exited);
|
|
ASSERT_EQ(atomic_load(&context.opened), 1);
|
|
ASSERT_EQ(atomic_load(&context.requests), 1);
|
|
ASSERT_EQ(atomic_load(&context.cancelled), 1);
|
|
ASSERT_EQ(atomic_load(&context.closed), 1);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_permanent_service_survives_last_disconnect_until_stop) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
runtime_test_fixture_permanent = true;
|
|
bool started = runtime_test_fixture_start(&fixture, "permanent-lifecycle", &identity);
|
|
runtime_test_fixture_permanent = false;
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
cbm_daemon_runtime_connect_result_t connect_result = {0};
|
|
bool survived = false;
|
|
bool status_ok = false;
|
|
bool stop_ok = false;
|
|
bool exited = false;
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &connect_result);
|
|
}
|
|
if (client) {
|
|
(void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
client = NULL;
|
|
/* Give a wrongly-armed teardown time to fire before asserting. */
|
|
survived =
|
|
!cbm_daemon_runtime_service_wait_exited(fixture.service, 300U) &&
|
|
cbm_daemon_runtime_service_state(fixture.service) == CBM_DAEMON_RUNTIME_SERVICE_RUNNING;
|
|
cbm_daemon_runtime_status_t status = {0};
|
|
status_ok = cbm_daemon_runtime_request_status(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &status) &&
|
|
status.permanent && !status.stopping && status.committed_clients == 0;
|
|
cbm_daemon_runtime_stop_result_t stop_result = {0};
|
|
stop_ok = cbm_daemon_runtime_request_stop(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &stop_result) &&
|
|
stop_result.accepted && !stop_result.busy;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(survived);
|
|
ASSERT_TRUE(status_ok);
|
|
ASSERT_TRUE(stop_ok);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_runtime_stop_refuses_while_committed_clients_exist) {
|
|
cbm_daemon_build_identity_t identity =
|
|
runtime_test_identity("2.4.0", runtime_test_self_build());
|
|
runtime_test_fixture_t fixture;
|
|
runtime_test_fixture_permanent = true;
|
|
bool started = runtime_test_fixture_start(&fixture, "stop-refuse-busy", &identity);
|
|
runtime_test_fixture_permanent = false;
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
cbm_daemon_runtime_connect_result_t connect_result = {0};
|
|
bool refused = false;
|
|
bool accepted_after_close = false;
|
|
bool exited = false;
|
|
if (started) {
|
|
client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &connect_result);
|
|
}
|
|
if (client) {
|
|
cbm_daemon_runtime_stop_result_t busy_result = {0};
|
|
refused = cbm_daemon_runtime_request_stop(fixture.endpoint, &identity,
|
|
RUNTIME_TEST_TIMEOUT_MS, &busy_result) &&
|
|
busy_result.busy && !busy_result.accepted && busy_result.committed_clients == 1 &&
|
|
busy_result.client_count == 1 &&
|
|
busy_result.client_pids[0] == (uint32_t)connect_result.authenticated_process_id;
|
|
(void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS);
|
|
client = NULL;
|
|
cbm_daemon_runtime_stop_result_t stop_result = {0};
|
|
accepted_after_close =
|
|
cbm_daemon_runtime_service_wait_for_clients(fixture.service, 0,
|
|
RUNTIME_TEST_TIMEOUT_MS) &&
|
|
cbm_daemon_runtime_request_stop(fixture.endpoint, &identity, RUNTIME_TEST_TIMEOUT_MS,
|
|
&stop_result) &&
|
|
stop_result.accepted && !stop_result.busy;
|
|
exited = cbm_daemon_runtime_service_wait_exited(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
|
|
}
|
|
runtime_test_fixture_finish(&fixture);
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(refused);
|
|
ASSERT_TRUE(accepted_after_close);
|
|
ASSERT_TRUE(exited);
|
|
PASS();
|
|
}
|
|
|
|
SUITE(daemon_runtime) {
|
|
RUN_TEST(daemon_runtime_permanent_service_survives_last_disconnect_until_stop);
|
|
RUN_TEST(daemon_runtime_stop_refuses_while_committed_clients_exist);
|
|
RUN_TEST(daemon_host_early_coordination_failure_is_durable);
|
|
RUN_TEST(daemon_host_refuses_unopenable_runtime_config_database);
|
|
RUN_TEST(daemon_host_http_reconcile_rate_limits_and_retries_transient_failures);
|
|
RUN_TEST(daemon_host_http_retry_backoff_is_bounded);
|
|
RUN_TEST(daemon_host_http_reconcile_retains_busy_server_until_free_succeeds);
|
|
#ifndef _WIN32
|
|
RUN_TEST(daemon_host_failed_listener_reservation_starts_no_background_work);
|
|
RUN_TEST(daemon_host_persistent_cleanup_release_failure_is_process_bounded);
|
|
RUN_TEST(daemon_host_forced_shutdown_is_logged_flushed_and_process_bounded);
|
|
#endif
|
|
RUN_TEST(daemon_runtime_kernel_process_fingerprint_is_stable_and_fail_closed);
|
|
#ifdef _WIN32
|
|
RUN_TEST(daemon_runtime_process_fingerprint_supports_extended_length_image);
|
|
#endif
|
|
#ifdef __APPLE__
|
|
RUN_TEST(daemon_runtime_mac_fast_path_rejects_foreign_main_image_mapping_active);
|
|
#endif
|
|
RUN_TEST(daemon_runtime_copied_image_fallback_accepts_identical_and_rejects_changed);
|
|
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
|
|
RUN_TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path);
|
|
#endif
|
|
RUN_TEST(daemon_runtime_convenience_service_owns_participant_guard);
|
|
RUN_TEST(daemon_runtime_rendezvous_layout_is_frozen_and_detailed_abi_independent);
|
|
RUN_TEST(daemon_runtime_exact_hello_issues_connection_bound_identity);
|
|
RUN_TEST(daemon_runtime_image_rejection_reaches_client_issue1383);
|
|
RUN_TEST(daemon_runtime_unverifiable_image_is_admitted_issue1539);
|
|
RUN_TEST(daemon_runtime_unexpected_frame_payload_is_freed_once);
|
|
RUN_TEST(daemon_runtime_activation_rejects_forged_and_malformed_without_stop);
|
|
RUN_TEST(daemon_runtime_activation_ack_snapshots_then_interrupts_all_clients);
|
|
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
|
|
RUN_TEST(daemon_runtime_activation_accepts_authenticated_different_build);
|
|
#endif
|
|
RUN_TEST(daemon_runtime_future_generation_gets_stable_explicit_conflict);
|
|
RUN_TEST(daemon_runtime_matching_clients_share_one_service_endpoint);
|
|
RUN_TEST(daemon_runtime_same_version_different_build_is_visible_and_logged);
|
|
RUN_TEST(daemon_runtime_conflict_log_failure_uses_operation_log_fallback);
|
|
RUN_TEST(daemon_runtime_disconnect_releases_only_connection_subscriptions);
|
|
RUN_TEST(daemon_runtime_final_disconnect_automatically_exits_within_bound);
|
|
RUN_TEST(daemon_runtime_authenticated_idle_connection_outlives_lease_interval);
|
|
RUN_TEST(daemon_runtime_connection_cap_covers_slow_hello_and_stopping_is_terminal);
|
|
RUN_TEST(daemon_runtime_rejects_forged_identity_extension);
|
|
RUN_TEST(daemon_runtime_application_response_roundtrip_is_byte_exact);
|
|
RUN_TEST(daemon_runtime_final_disconnect_rejects_blocked_provisional_session);
|
|
RUN_TEST(daemon_runtime_request_cancel_is_exact_and_session_remains_usable);
|
|
RUN_TEST(daemon_runtime_presend_request_cancel_is_sticky_and_nonterminal);
|
|
RUN_TEST(daemon_runtime_allows_only_one_unstarted_application_token);
|
|
RUN_TEST(daemon_runtime_consumes_busy_application_token_before_response);
|
|
RUN_TEST(daemon_runtime_close_begin_retains_storage_and_rejects_late_exchange);
|
|
RUN_TEST(daemon_runtime_disconnect_cancels_blocked_application_before_exit);
|
|
RUN_TEST(daemon_runtime_close_begin_releases_admission_with_inflight_request);
|
|
RUN_TEST(daemon_runtime_disconnect_cancels_blocked_non_index_child_and_preserves_other_session);
|
|
RUN_TEST(daemon_runtime_noncooperative_callback_does_not_detach_or_unbound_stop);
|
|
RUN_TEST(daemon_runtime_application_busy_cap_and_malformed_are_isolated);
|
|
RUN_TEST(daemon_runtime_malformed_and_zero_cancel_close_only_offending_connections);
|
|
}
|