fix: stabilize cross-platform daemon coordination

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-07-18 11:48:59 +02:00
parent 98a28473db
commit ad58e71bea
14 changed files with 386 additions and 59 deletions
+18 -5
View File
@@ -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
+5 -5
View File
@@ -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:
+3 -1
View File
@@ -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"
+16 -1
View File
@@ -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
View File
@@ -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,
+2 -3
View File
@@ -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) {
+18 -7
View File
@@ -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;
+1
View File
@@ -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
View File
@@ -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__)
+10
View File
@@ -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
View File
@@ -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;
}
+16 -3
View File
@@ -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
+88 -13
View File
@@ -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)
+88
View File
@@ -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
)