fix: stabilize cross-platform daemon coordination
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
@@ -213,8 +213,12 @@ jobs:
|
||||
unzip -o "codebase-memory-mcp${SUFFIX}-windows-${ARCH}.zip"
|
||||
test -s codebase-memory-mcp.exe
|
||||
test -s codebase-memory-mcp.payload.exe
|
||||
./codebase-memory-mcp.payload.exe --version
|
||||
./codebase-memory-mcp.exe --version
|
||||
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
|
||||
LAUNCH_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-version.XXXXXX")"
|
||||
trap 'rm -rf "$LAUNCH_DIR"' EXIT
|
||||
cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe "$LAUNCH_DIR/"
|
||||
"$LAUNCH_DIR/codebase-memory-mcp.payload.exe" --version
|
||||
"$LAUNCH_DIR/codebase-memory-mcp.exe" --version
|
||||
|
||||
- name: Start artifact server
|
||||
shell: msys2 {0}
|
||||
@@ -240,8 +244,12 @@ jobs:
|
||||
- name: Smoke test
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
SMOKE_TEMP_ROOT="$(cygpath -u "$RUNNER_TEMP")" \
|
||||
scripts/smoke-test.sh ./codebase-memory-mcp.exe
|
||||
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
|
||||
SMOKE_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-smoke.XXXXXX")"
|
||||
trap 'rm -rf "$SMOKE_DIR"' EXIT
|
||||
cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe "$SMOKE_DIR/"
|
||||
SMOKE_TEMP_ROOT="$SMOKE_DIR" \
|
||||
scripts/smoke-test.sh "$SMOKE_DIR/codebase-memory-mcp.exe"
|
||||
env:
|
||||
SMOKE_DOWNLOAD_URL: http://127.0.0.1:18080
|
||||
SMOKE_UPDATE_FIXTURE_DIR: /tmp/smoke-server
|
||||
@@ -252,7 +260,12 @@ jobs:
|
||||
run: |
|
||||
scripts/security-strings.sh ./codebase-memory-mcp.exe
|
||||
scripts/security-strings.sh ./codebase-memory-mcp.payload.exe
|
||||
scripts/security-install.sh ./codebase-memory-mcp.exe
|
||||
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
|
||||
SECURITY_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-security.XXXXXX")"
|
||||
trap 'rm -rf "$SECURITY_DIR"' EXIT
|
||||
cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe "$SECURITY_DIR/"
|
||||
TMPDIR="$SECURITY_DIR" \
|
||||
scripts/security-install.sh "$SECURITY_DIR/codebase-memory-mcp.exe"
|
||||
|
||||
- name: Windows Defender scan
|
||||
shell: pwsh
|
||||
|
||||
@@ -115,16 +115,16 @@ jobs:
|
||||
run: |
|
||||
scripts/build.sh CC=clang CXX=clang++
|
||||
# MSYS2 /tmp is intentionally shared. The launcher correctly rejects
|
||||
# a bundle below that writable ancestor, so stage in the runner's
|
||||
# account-private native temp directory instead.
|
||||
SMOKE_ROOT="$(cygpath -u "$RUNNER_TEMP")"
|
||||
SMOKE_DIR="$(mktemp -d "$SMOKE_ROOT/cbm-pr-smoke.XXXXXX")"
|
||||
# a bundle below that writable ancestor. RUNNER_TEMP is also below
|
||||
# the shared D:\a tree, so stage in the runner account's profile.
|
||||
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
|
||||
SMOKE_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-pr-smoke.XXXXXX")"
|
||||
trap 'rm -rf "$SMOKE_DIR"' EXIT
|
||||
cp build/c/codebase-memory-mcp-launcher.exe \
|
||||
"$SMOKE_DIR/codebase-memory-mcp.exe"
|
||||
cp build/c/codebase-memory-mcp.exe \
|
||||
"$SMOKE_DIR/codebase-memory-mcp.payload.exe"
|
||||
SMOKE_TEMP_ROOT="$SMOKE_ROOT" \
|
||||
SMOKE_TEMP_ROOT="$SMOKE_DIR" \
|
||||
scripts/smoke-test.sh "$SMOKE_DIR/codebase-memory-mcp.exe"
|
||||
|
||||
ci-ok:
|
||||
|
||||
@@ -3083,7 +3083,9 @@ elif [ -f "$REPO_ROOT/install.ps1" ] && command -v powershell.exe &>/dev/null; t
|
||||
# Pass the known-correct arch: powershell runs under x64 emulation on ARM64, so
|
||||
# install.ps1's own detection can't tell it's arm64. DL_ARCH is authoritative here.
|
||||
HOME="$PS1_TEST_HOME" CBM_DOWNLOAD_URL="$WIN_URL" CBM_ARCH="$DL_ARCH" \
|
||||
powershell.exe -ExecutionPolicy ByPass -File "$WIN_SCRIPT" "--dir=$WIN_DIR" 2>&1 || true
|
||||
powershell.exe -NoProfile -ExecutionPolicy ByPass -Command \
|
||||
'$env:TEMP=$args[0]; $env:TMP=$args[0]; & $args[1] $args[2]' \
|
||||
"$WIN_HOME" "$WIN_SCRIPT" "--dir=$WIN_DIR" 2>&1 || true
|
||||
|
||||
# 13g: binary placed
|
||||
PS1_BIN="$PS1_TEST_DIR/codebase-memory-mcp.exe"
|
||||
|
||||
@@ -133,7 +133,11 @@ Write-Host "Payload: $bin" -ForegroundColor Green
|
||||
Write-Host "Launcher: $launcherBin" -ForegroundColor Green
|
||||
Write-Host "ABI mismatch fixture: $abiMismatchLauncher" -ForegroundColor Green
|
||||
|
||||
$guardBundle = Join-Path $tmp ("cbm-windows-guards-" + [guid]::NewGuid().ToString("N"))
|
||||
$localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
|
||||
if (-not $localAppData) { throw "could not resolve the current user's local application-data directory" }
|
||||
$guardRoot = Join-Path $localAppData "Temp"
|
||||
New-Item -ItemType Directory -Path $guardRoot -Force | Out-Null
|
||||
$guardBundle = Join-Path $guardRoot ("cbm-windows-guards-" + [guid]::NewGuid().ToString("N"))
|
||||
New-Item -ItemType Directory -Path $guardBundle | Out-Null
|
||||
$guardBin = Join-Path $guardBundle "codebase-memory-mcp.exe"
|
||||
$guardPayload = Join-Path $guardBundle "codebase-memory-mcp.payload.exe"
|
||||
@@ -141,7 +145,15 @@ Copy-Item -LiteralPath $launcherBin -Destination $guardBin
|
||||
Copy-Item -LiteralPath $bin -Destination $guardPayload
|
||||
Write-Host "Guard bundle: $guardBin" -ForegroundColor Green
|
||||
|
||||
$previousTemp = $env:TEMP
|
||||
$previousTmp = $env:TMP
|
||||
$previousTmpDir = $env:TMPDIR
|
||||
try {
|
||||
# The launcher deliberately rejects GitHub's shared D:\a ancestry. Keep
|
||||
# launcher fixtures and Python-created descendants inside this account.
|
||||
$env:TEMP = $guardRoot
|
||||
$env:TMP = $guardRoot
|
||||
$env:TMPDIR = $guardRoot
|
||||
$env:PYTHONUTF8 = "1" # encode argv/stdio as UTF-8
|
||||
$env:CBM_INDEX_SUPERVISOR = "0" # in-process indexing (see .DESCRIPTION)
|
||||
|
||||
@@ -204,6 +216,9 @@ if (-not $GuardsOnly) {
|
||||
}
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $guardBundle -Recurse -Force -ErrorAction SilentlyContinue
|
||||
$env:TEMP = $previousTemp
|
||||
$env:TMP = $previousTmp
|
||||
$env:TMPDIR = $previousTmpDir
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
+7
-8
@@ -1984,14 +1984,13 @@ static int posix_socket_record_publication_recover(
|
||||
|
||||
static int posix_linkat_no_follow(int source_dir_fd, const char *source_name,
|
||||
int destination_dir_fd, const char *destination_name) {
|
||||
int flags = 0;
|
||||
#if defined(__APPLE__) && defined(AT_SYMLINK_NOFOLLOW_ANY)
|
||||
/* Darwin documents that linkat(..., 0) may be rejected by some
|
||||
* filesystems. This explicit flag preserves the intended no-follow
|
||||
* semantics while making the operation portable to those filesystems. */
|
||||
flags = AT_SYMLINK_NOFOLLOW_ANY;
|
||||
#endif
|
||||
return linkat(source_dir_fd, source_name, destination_dir_fd, destination_name, flags);
|
||||
/* linkat without AT_SYMLINK_FOLLOW links the source entry itself, which is
|
||||
* the required no-follow behavior. Do not pass Darwin's broader
|
||||
* AT_SYMLINK_NOFOLLOW_ANY constant: macOS 14 headers define it, but that
|
||||
* kernel rejects it for linkat with EINVAL. The retained and validated
|
||||
* source state plus post-link inode/link-count checks retain the
|
||||
* fail-closed publication contract. */
|
||||
return linkat(source_dir_fd, source_name, destination_dir_fd, destination_name, 0);
|
||||
}
|
||||
|
||||
static void posix_bound_socket_unlink_if_matches(int dir_fd, const char *socket_name, dev_t device,
|
||||
|
||||
@@ -1354,8 +1354,8 @@ static bool runtime_activation_peer_matches_claim(cbm_daemon_runtime_service_t *
|
||||
}
|
||||
bool active_image =
|
||||
runtime_process_image_reference_matches_process(&service->active_image, process_id);
|
||||
if (active_image && strcmp(claimed_build, service->identity.build_fingerprint) == 0) {
|
||||
return true;
|
||||
if (active_image) {
|
||||
return strcmp(claimed_build, service->identity.build_fingerprint) == 0;
|
||||
}
|
||||
char peer_fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
|
||||
return cbm_daemon_runtime_process_build_fingerprint(process_id, peer_fingerprint) &&
|
||||
@@ -1569,7 +1569,6 @@ static void *runtime_connection_worker(void *opaque) {
|
||||
received = cbm_daemon_ipc_receive_frame(worker->connection, CBM_DAEMON_IPC_WAIT_FOREVER,
|
||||
&frame, &payload);
|
||||
if (received != 1 || frame.type != CBM_DAEMON_FRAME_REQUEST) {
|
||||
free(payload);
|
||||
break;
|
||||
}
|
||||
switch (frame.flags) {
|
||||
|
||||
@@ -176,7 +176,7 @@ static bool launcher_bounded_ace_sid_is_trusted(const ACE_HEADER *header, PSID c
|
||||
IsWellKnownSid((PSID)sid, WinCreatorOwnerSid)));
|
||||
}
|
||||
|
||||
static bool launcher_security_is_safe(HANDLE file, bool require_current_owner) {
|
||||
static bool launcher_security_is_safe(HANDLE file, bool require_current_owner, DWORD mutation) {
|
||||
void *token_user = NULL;
|
||||
PSID user_sid = NULL;
|
||||
if (!launcher_current_user(&token_user, &user_sid)) {
|
||||
@@ -196,10 +196,6 @@ static bool launcher_security_is_safe(HANDLE file, bool require_current_owner) {
|
||||
: launcher_sid_is_trusted(owner, user_sid)) &&
|
||||
dacl && IsValidAcl(dacl) &&
|
||||
GetAclInformation(dacl, &information, sizeof(information), AclSizeInformation) != 0;
|
||||
const DWORD mutation = GENERIC_ALL | GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA |
|
||||
FILE_ADD_FILE | FILE_ADD_SUBDIRECTORY | FILE_DELETE_CHILD |
|
||||
FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES | DELETE | WRITE_DAC |
|
||||
WRITE_OWNER | ACCESS_SYSTEM_SECURITY;
|
||||
for (DWORD index = 0; secure && index < information.AceCount; index++) {
|
||||
void *opaque = NULL;
|
||||
if (!GetAce(dacl, index, &opaque) || !opaque) {
|
||||
@@ -239,8 +235,14 @@ static bool launcher_security_is_safe(HANDLE file, bool require_current_owner) {
|
||||
return secure;
|
||||
}
|
||||
|
||||
static DWORD launcher_private_mutation_rights(void) {
|
||||
return GENERIC_ALL | GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_ADD_FILE |
|
||||
FILE_ADD_SUBDIRECTORY | FILE_DELETE_CHILD | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES |
|
||||
DELETE | WRITE_DAC | WRITE_OWNER | ACCESS_SYSTEM_SECURITY;
|
||||
}
|
||||
|
||||
static bool launcher_security_is_private(HANDLE file) {
|
||||
return launcher_security_is_safe(file, true);
|
||||
return launcher_security_is_safe(file, true, launcher_private_mutation_rights());
|
||||
}
|
||||
|
||||
static HANDLE launcher_open_regular(const wchar_t *path, DWORD access, bool require_private) {
|
||||
@@ -319,11 +321,20 @@ static bool launcher_path_tree_plain(const wchar_t *file_path) {
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
|
||||
BY_HANDLE_FILE_INFORMATION information;
|
||||
DWORD mutation = launcher_private_mutation_rights();
|
||||
if (index < directory_length) {
|
||||
/* Default C:\\Users ACLs grant cross-account add-subdirectory on
|
||||
* this intermediate component. That cannot replace the existing
|
||||
* next component. No other write right is relaxed, and the final
|
||||
* executable directory remains fully private so a peer cannot
|
||||
* plant DLL or .exe.local redirection artifacts beside CBM. */
|
||||
mutation &= ~((DWORD)FILE_ADD_SUBDIRECTORY);
|
||||
}
|
||||
valid = component != INVALID_HANDLE_VALUE && GetFileType(component) == FILE_TYPE_DISK &&
|
||||
GetFileInformationByHandle(component, &information) != 0 &&
|
||||
(information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 &&
|
||||
(information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0 &&
|
||||
launcher_security_is_safe(component, false);
|
||||
launcher_security_is_safe(component, false, mutation);
|
||||
if (component != INVALID_HANDLE_VALUE)
|
||||
(void)CloseHandle(component);
|
||||
path[index] = saved;
|
||||
|
||||
@@ -1323,6 +1323,7 @@ static void app_fake_worker_destroy(void *opaque, cbm_daemon_application_worker_
|
||||
cbm_usleep(1000);
|
||||
}
|
||||
(void)app_project_lock_release(&worker->project_lock_lease);
|
||||
cbm_index_worker_result_free(&worker->result);
|
||||
free(worker->project_key);
|
||||
free(handle);
|
||||
}
|
||||
|
||||
+107
-10
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "test_helpers.h"
|
||||
#include "test_daemon_runtime_contract.h"
|
||||
|
||||
#include "daemon/application.h"
|
||||
#include "daemon/host.h"
|
||||
@@ -53,6 +54,8 @@ 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,
|
||||
@@ -245,6 +248,22 @@ static bool runtime_test_windows_copy_self(const char *destination) {
|
||||
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);
|
||||
@@ -657,7 +676,7 @@ static bool runtime_test_run_hello_image(const char *image_path,
|
||||
NULL, &startup, &process) != 0;
|
||||
free(command);
|
||||
free(application);
|
||||
bool waited = started && WaitForSingleObject(process.hProcess, 10000) == WAIT_OBJECT_0;
|
||||
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) {
|
||||
@@ -671,6 +690,7 @@ static bool runtime_test_run_hello_image(const char *image_path,
|
||||
#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);
|
||||
@@ -722,11 +742,7 @@ static bool runtime_test_run_activation_image(const char *image_path,
|
||||
NULL, &startup, &process) != 0;
|
||||
free(command);
|
||||
free(application);
|
||||
bool waited = started && WaitForSingleObject(process.hProcess, 10000) == WAIT_OBJECT_0;
|
||||
if (started && !waited) {
|
||||
(void)TerminateProcess(process.hProcess, 30);
|
||||
(void)WaitForSingleObject(process.hProcess, 5000);
|
||||
}
|
||||
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) {
|
||||
@@ -742,7 +758,7 @@ static bool runtime_test_run_activation_image(const char *image_path,
|
||||
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(10);
|
||||
(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);
|
||||
@@ -1342,10 +1358,22 @@ 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);
|
||||
if (state != CBM_DAEMON_RUNTIME_SERVICE_EXITED) {
|
||||
(void)cbm_daemon_runtime_service_stop(fixture->service, RUNTIME_TEST_TIMEOUT_MS);
|
||||
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);
|
||||
}
|
||||
}
|
||||
(void)cbm_daemon_runtime_service_free(fixture->service);
|
||||
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);
|
||||
@@ -1691,6 +1719,74 @@ TEST(daemon_runtime_exact_hello_issues_connection_bound_identity) {
|
||||
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());
|
||||
@@ -4087,6 +4183,7 @@ SUITE(daemon_runtime) {
|
||||
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_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__)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef TEST_DAEMON_RUNTIME_CONTRACT_H
|
||||
#define TEST_DAEMON_RUNTIME_CONTRACT_H
|
||||
|
||||
/* A copied sanitizer executable can take tens of seconds to fingerprint. The
|
||||
* parent watchdog covers both the HELLO exchange and its bounded close. */
|
||||
#define TF_RUNTIME_IMAGE_EXCHANGE_TIMEOUT_MS 60000U
|
||||
#define TF_RUNTIME_IMAGE_WATCHDOG_MS (2U * TF_RUNTIME_IMAGE_EXCHANGE_TIMEOUT_MS + 5000U)
|
||||
#define TF_RUNTIME_IMAGE_WATCHDOG_SECONDS ((TF_RUNTIME_IMAGE_WATCHDOG_MS + 999U) / 1000U)
|
||||
|
||||
#endif /* TEST_DAEMON_RUNTIME_CONTRACT_H */
|
||||
+7
-3
@@ -10,6 +10,7 @@ int tf_skip_count = 0;
|
||||
|
||||
#include "test_framework.h"
|
||||
#include "test_helpers.h"
|
||||
#include "test_daemon_runtime_contract.h"
|
||||
#include "foundation/compat.h" /* cbm_setenv — #845 supervisor kill switch */
|
||||
#include "foundation/compat_fs.h" /* cbm_fopen — worker response file */
|
||||
#include "foundation/mem.h" /* cbm_mem_init — worker budget */
|
||||
@@ -391,10 +392,13 @@ static int tf_maybe_run_runtime_hello_client(int argc, char **argv) {
|
||||
cbm_daemon_runtime_connect_result_t result;
|
||||
memset(&result, 0, sizeof(result));
|
||||
cbm_daemon_runtime_client_t *client =
|
||||
endpoint ? cbm_daemon_runtime_client_connect(endpoint, &identity, 5000, &result) : NULL;
|
||||
endpoint ? cbm_daemon_runtime_client_connect(endpoint, &identity,
|
||||
TF_RUNTIME_IMAGE_EXCHANGE_TIMEOUT_MS, &result)
|
||||
: NULL;
|
||||
bool accepted = client && result.status == CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED &&
|
||||
result.hello_status == CBM_DAEMON_HELLO_COMPATIBLE;
|
||||
bool closed = !client || cbm_daemon_runtime_client_close(client, 5000);
|
||||
bool closed =
|
||||
!client || cbm_daemon_runtime_client_close(client, TF_RUNTIME_IMAGE_EXCHANGE_TIMEOUT_MS);
|
||||
cbm_daemon_ipc_endpoint_free(endpoint);
|
||||
if (!accepted) {
|
||||
return 26;
|
||||
@@ -425,7 +429,7 @@ static int tf_maybe_run_runtime_activation_client(int argc, char **argv) {
|
||||
bool exchanged =
|
||||
endpoint && cbm_daemon_runtime_request_activation_shutdown(
|
||||
endpoint, &identity, (cbm_daemon_runtime_activation_action_t)action_value,
|
||||
5000, &result);
|
||||
TF_RUNTIME_IMAGE_EXCHANGE_TIMEOUT_MS, &result);
|
||||
cbm_daemon_ipc_endpoint_free(endpoint);
|
||||
return exchanged && result.accepted ? 0 : 29;
|
||||
}
|
||||
|
||||
@@ -82,12 +82,25 @@ if [[ ! -s "$ENV_LOG" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while IFS=$'\t' read -r child_home child_cache; do
|
||||
if [[ -z "$child_home" || "$child_home" == "$CALLER_HOME" ]]; then
|
||||
normalize_path() {
|
||||
local path=${1%$'\r'}
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
cygpath -u "$path" 2>/dev/null && return 0
|
||||
fi
|
||||
printf '%s\n' "${path//\\//}"
|
||||
}
|
||||
|
||||
CALLER_HOME_NORMALIZED=$(normalize_path "$CALLER_HOME")
|
||||
CALLER_CACHE_NORMALIZED=$(normalize_path "$CALLER_CACHE")
|
||||
|
||||
while IFS=$'\t' read -r child_home_raw child_cache_raw; do
|
||||
child_home=$(normalize_path "$child_home_raw")
|
||||
child_cache=$(normalize_path "$child_cache_raw")
|
||||
if [[ -z "$child_home" || "$child_home" == "$CALLER_HOME_NORMALIZED" ]]; then
|
||||
echo "FAIL: security-fuzz exposed the caller HOME to a fuzz target"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$child_cache" || "$child_cache" == "$CALLER_CACHE" ]]; then
|
||||
if [[ -z "$child_cache" || "$child_cache" == "$CALLER_CACHE_NORMALIZED" ]]; then
|
||||
echo "FAIL: security-fuzz exposed the caller CBM_CACHE_DIR to a fuzz target"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -349,6 +349,20 @@ require(
|
||||
in windows_test_driver,
|
||||
"the permanent-launcher guard must fail instead of skip on driver/precondition errors",
|
||||
)
|
||||
require(
|
||||
all(
|
||||
needle in windows_test_driver
|
||||
for needle in (
|
||||
"[Environment+SpecialFolder]::LocalApplicationData",
|
||||
'$guardRoot = Join-Path $localAppData "Temp"',
|
||||
'$env:TEMP = $guardRoot',
|
||||
'$env:TMP = $guardRoot',
|
||||
'$env:TMPDIR = $guardRoot',
|
||||
)
|
||||
),
|
||||
"Windows launcher guards must keep staged and Python-created fixtures "
|
||||
"beneath the current account profile",
|
||||
)
|
||||
|
||||
# Launcher supervision has two distinct failure directions: killing the
|
||||
# launcher must kill its payload job, and killing only the launcher's immediate
|
||||
@@ -423,7 +437,13 @@ ancestor_security_contracts = {
|
||||
"FILE_FLAG_OPEN_REPARSE_POINT",
|
||||
),
|
||||
"src/launcher/windows_launcher.c": (
|
||||
"launcher_security_is_safe(component, false)",
|
||||
"launcher_security_is_safe(component, false, mutation)",
|
||||
"launcher_private_mutation_rights()",
|
||||
"if (index < directory_length)",
|
||||
"mutation &= ~((DWORD)FILE_ADD_SUBDIRECTORY)",
|
||||
"FILE_ADD_FILE",
|
||||
"FILE_DELETE_CHILD",
|
||||
"DLL or .exe.local",
|
||||
"ACCESS_SYSTEM_SECURITY",
|
||||
"956008885U",
|
||||
"FILE_FLAG_OPEN_REPARSE_POINT",
|
||||
@@ -448,10 +468,7 @@ windows_match = re.search(
|
||||
windows_smoke = windows_match.group(1) if windows_match else ""
|
||||
require(bool(windows_smoke), "_smoke.yml must contain the smoke-windows job")
|
||||
require(
|
||||
re.search(
|
||||
r"scripts/smoke-test\.sh\s+(?:\"|')?\./codebase-memory-mcp\.exe(?:\"|')?",
|
||||
windows_smoke,
|
||||
) is not None,
|
||||
'scripts/smoke-test.sh "$SMOKE_DIR/codebase-memory-mcp.exe"' in windows_smoke,
|
||||
f"Windows release smoke must execute the canonical {launcher}",
|
||||
)
|
||||
require(
|
||||
@@ -466,13 +483,62 @@ require(
|
||||
windows_release_smoke_blocks = [
|
||||
re.sub(r"\s+", " ", re.sub(r"\\\s*\n\s*", " ", block)).strip()
|
||||
for block in smoke_blocks
|
||||
if "scripts/smoke-test.sh ./codebase-memory-mcp.exe" in block
|
||||
if 'scripts/smoke-test.sh "$SMOKE_DIR/codebase-memory-mcp.exe"' in block
|
||||
]
|
||||
require(
|
||||
len(windows_release_smoke_blocks) == 1
|
||||
and 'SMOKE_TEMP_ROOT="$(cygpath -u "$RUNNER_TEMP")" '
|
||||
'scripts/smoke-test.sh ./codebase-memory-mcp.exe' in windows_release_smoke_blocks[0],
|
||||
"Windows release smoke must keep every launcher fixture under runner-private temp",
|
||||
and all(
|
||||
needle in windows_release_smoke_blocks[0]
|
||||
for needle in (
|
||||
'PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"',
|
||||
'SMOKE_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-smoke.XXXXXX")"',
|
||||
'cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe "$SMOKE_DIR/"',
|
||||
'SMOKE_TEMP_ROOT="$SMOKE_DIR" '
|
||||
'scripts/smoke-test.sh "$SMOKE_DIR/codebase-memory-mcp.exe"',
|
||||
)
|
||||
),
|
||||
"Windows release smoke must keep every launcher fixture beneath the current account profile",
|
||||
)
|
||||
windows_release_version_blocks = [
|
||||
re.sub(r"\s+", " ", re.sub(r"\\\s*\n\s*", " ", block)).strip()
|
||||
for block in smoke_blocks
|
||||
if 'LAUNCH_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-version.XXXXXX")"' in block
|
||||
]
|
||||
require(
|
||||
len(windows_release_version_blocks) == 1
|
||||
and all(
|
||||
needle in windows_release_version_blocks[0]
|
||||
for needle in (
|
||||
'PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"',
|
||||
'cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe "$LAUNCH_DIR/"',
|
||||
'"$LAUNCH_DIR/codebase-memory-mcp.payload.exe" --version',
|
||||
'"$LAUNCH_DIR/codebase-memory-mcp.exe" --version',
|
||||
)
|
||||
),
|
||||
"Windows release version checks must execute the pair beneath the current account profile",
|
||||
)
|
||||
require(
|
||||
"$RUNNER_TEMP" not in windows_smoke,
|
||||
"Windows release smoke must not treat GitHub's shared RUNNER_TEMP ancestry as private",
|
||||
)
|
||||
windows_release_security_blocks = [
|
||||
re.sub(r"\s+", " ", re.sub(r"\\\s*\n\s*", " ", block)).strip()
|
||||
for block in smoke_blocks
|
||||
if 'scripts/security-install.sh "$SECURITY_DIR/codebase-memory-mcp.exe"' in block
|
||||
]
|
||||
require(
|
||||
len(windows_release_security_blocks) == 1
|
||||
and all(
|
||||
needle in windows_release_security_blocks[0]
|
||||
for needle in (
|
||||
'PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"',
|
||||
'SECURITY_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-security.XXXXXX")"',
|
||||
'cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe "$SECURITY_DIR/"',
|
||||
'TMPDIR="$SECURITY_DIR" '
|
||||
'scripts/security-install.sh "$SECURITY_DIR/codebase-memory-mcp.exe"',
|
||||
)
|
||||
),
|
||||
"Windows release install audit must execute the pair beneath the current account profile",
|
||||
)
|
||||
|
||||
# Native update transport remains HTTPS-only in production. Release smoke may
|
||||
@@ -492,6 +558,11 @@ require(
|
||||
and 'CBM_DOWNLOAD_URL="$UPDATE_DOWNLOAD_URL"' in smoke_script,
|
||||
"Phase 14 native update must use an explicit file:// fixture override",
|
||||
)
|
||||
require(
|
||||
"'$env:TEMP=$args[0]; $env:TMP=$args[0]; & $args[1] $args[2]'" in smoke_script
|
||||
and '"$WIN_HOME" "$WIN_SCRIPT" "--dir=$WIN_DIR"' in smoke_script,
|
||||
"Windows install.ps1 smoke must set native TEMP/TMP inside PowerShell",
|
||||
)
|
||||
require(
|
||||
'CBM_DOWNLOAD_URL="$SMOKE_DOWNLOAD_URL"' in smoke_script
|
||||
and '"$SMOKE_DOWNLOAD_URL/$DL_ARCHIVE"' in smoke_script,
|
||||
@@ -562,21 +633,21 @@ if pr_windows_blocks:
|
||||
pr_windows_block = re.sub(r"\\\s*\n\s*", " ", pr_windows_blocks[0])
|
||||
pr_windows_block = re.sub(r"\s+", " ", pr_windows_block).strip()
|
||||
staging_steps = (
|
||||
'SMOKE_ROOT="$(cygpath -u "$RUNNER_TEMP")"',
|
||||
'SMOKE_DIR="$(mktemp -d "$SMOKE_ROOT/cbm-pr-smoke.XXXXXX")"',
|
||||
'PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"',
|
||||
'SMOKE_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-pr-smoke.XXXXXX")"',
|
||||
'trap \'rm -rf "$SMOKE_DIR"\' EXIT',
|
||||
'cp build/c/codebase-memory-mcp-launcher.exe '
|
||||
'"$SMOKE_DIR/codebase-memory-mcp.exe"',
|
||||
'cp build/c/codebase-memory-mcp.exe '
|
||||
'"$SMOKE_DIR/codebase-memory-mcp.payload.exe"',
|
||||
'SMOKE_TEMP_ROOT="$SMOKE_ROOT" '
|
||||
'SMOKE_TEMP_ROOT="$SMOKE_DIR" '
|
||||
'scripts/smoke-test.sh "$SMOKE_DIR/codebase-memory-mcp.exe"',
|
||||
)
|
||||
positions = [pr_windows_block.find(step) for step in staging_steps]
|
||||
require(
|
||||
all(position >= 0 for position in positions),
|
||||
"Windows PR smoke must stage launcher and payload under release names "
|
||||
"in the runner-private native temp directory and invoke the canonical launcher",
|
||||
"beneath the current account profile and invoke the canonical launcher",
|
||||
)
|
||||
require(
|
||||
all(left < right for left, right in zip(positions, positions[1:])),
|
||||
@@ -586,6 +657,10 @@ if pr_windows_blocks:
|
||||
pr_windows_block.count("scripts/smoke-test.sh") == 1,
|
||||
"Windows PR smoke must invoke smoke-test exactly once through the launcher",
|
||||
)
|
||||
require(
|
||||
"$RUNNER_TEMP" not in pr_windows_block,
|
||||
"Windows PR smoke must not treat GitHub's shared RUNNER_TEMP ancestry as private",
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("Windows launcher bundle contract FAILED:", file=sys.stderr)
|
||||
|
||||
@@ -397,6 +397,88 @@ def assert_untrusted_ancestor_acl_rejected(
|
||||
print("PASS: launcher rejected an untrusted mutation ACE on an ancestor")
|
||||
|
||||
|
||||
def assert_add_only_ancestor_acl_allowed(source_launcher, source_payload, env, work):
|
||||
ancestor = work / "cross-account add-only ancestor"
|
||||
launcher, _ = copy_portable_pair(source_launcher, source_payload, ancestor / "bundle")
|
||||
grant = run(["icacls", ancestor, "/grant", "*S-1-1-0:(AD)"], env)
|
||||
require(
|
||||
grant.returncode == 0,
|
||||
"could not install native Everyone-add-subdirectory ancestor fixture: %s"
|
||||
% output_text(grant)[-600:],
|
||||
)
|
||||
try:
|
||||
for candidate, spelling in (
|
||||
(launcher, "normal"),
|
||||
("\\\\?\\" + str(launcher.resolve()), "extended DOS"),
|
||||
):
|
||||
result = run([candidate, "--version"], env)
|
||||
require(
|
||||
result.returncode == 0,
|
||||
"%s launcher path treated sibling creation as replacement access: %s"
|
||||
% (spelling, output_text(result)[-600:]),
|
||||
)
|
||||
finally:
|
||||
remove = run(["icacls", ancestor, "/remove:g", "*S-1-1-0"], env)
|
||||
require(
|
||||
remove.returncode == 0,
|
||||
"could not remove native Everyone-add-subdirectory ancestor fixture",
|
||||
)
|
||||
print("PASS: launcher allowed add-only sibling creation without weakening path integrity")
|
||||
|
||||
|
||||
def assert_targeted_ancestor_acl_rejected(path, launcher, right, description, env):
|
||||
grant = run(["icacls", path, "/grant", "*S-1-1-0:(%s)" % right], env)
|
||||
require(
|
||||
grant.returncode == 0,
|
||||
"could not install native Everyone-%s fixture: %s"
|
||||
% (description, output_text(grant)[-600:]),
|
||||
)
|
||||
try:
|
||||
for candidate, spelling in (
|
||||
(launcher, "normal"),
|
||||
("\\\\?\\" + str(launcher.resolve()), "extended DOS"),
|
||||
):
|
||||
result = run([candidate, "--version"], env)
|
||||
require(
|
||||
result.returncode != 0,
|
||||
"%s launcher accepted cross-account %s"
|
||||
% (spelling, description),
|
||||
)
|
||||
finally:
|
||||
remove = run(["icacls", path, "/remove:g", "*S-1-1-0"], env)
|
||||
require(
|
||||
remove.returncode == 0,
|
||||
"could not remove native Everyone-%s fixture" % description,
|
||||
)
|
||||
require(
|
||||
run([launcher, "--version"], env).returncode == 0,
|
||||
"launcher did not recover after the %s ACE was removed" % description,
|
||||
)
|
||||
|
||||
|
||||
def assert_file_add_and_executable_parent_acl_rejected(
|
||||
source_launcher, source_payload, env, work
|
||||
):
|
||||
file_add_ancestor = work / "cross-account file-add ancestor"
|
||||
launcher, _ = copy_portable_pair(
|
||||
source_launcher, source_payload, file_add_ancestor / "bundle"
|
||||
)
|
||||
assert_targeted_ancestor_acl_rejected(
|
||||
file_add_ancestor, launcher, "WD", "file creation on an intermediate ancestor", env
|
||||
)
|
||||
|
||||
executable_parent = work / "cross-account executable parent"
|
||||
launcher, _ = copy_portable_pair(source_launcher, source_payload, executable_parent)
|
||||
assert_targeted_ancestor_acl_rejected(
|
||||
executable_parent,
|
||||
launcher,
|
||||
"AD",
|
||||
"subdirectory creation beside the executable",
|
||||
env,
|
||||
)
|
||||
print("PASS: launcher kept file-add and executable-parent ACL boundaries strict")
|
||||
|
||||
|
||||
def process_entries():
|
||||
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
kernel32.CreateToolhelp32Snapshot.argtypes = [wintypes.DWORD, wintypes.DWORD]
|
||||
@@ -979,6 +1061,12 @@ def main():
|
||||
env, cache = isolated_environment(work)
|
||||
assert_release_descriptor(source_launcher, source_payload, env, cache)
|
||||
assert_portable_mutations_refuse(source_payload, env, cache, work)
|
||||
assert_add_only_ancestor_acl_allowed(
|
||||
source_launcher, source_payload, env, work
|
||||
)
|
||||
assert_file_add_and_executable_parent_acl_rejected(
|
||||
source_launcher, source_payload, env, work
|
||||
)
|
||||
assert_untrusted_ancestor_acl_rejected(
|
||||
source_launcher, source_payload, env, work
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user