Files
codebase-memory-mcp/tests/test_build_dir_safety.sh
Martin Vogel 34f9ad1b3d fix(daemon,win): CI-green fixes and a daemon-stability guard
Fixes surfaced while bringing the shared-coordination-daemon branch
green across macOS, the Linux containers, and the real Windows-ARM64 VM,
plus a new stability guard that caught one of them.

- daemon(win): the CLI teardown transition latched a failed release as
  permanent, but a Windows participant-state release must briefly
  try-hold the shared startup/legacy gates and legitimately collides
  with a concurrent one-shot's teardown; the release is retriable by
  contract (it always retains the transition), so parallel one-shot
  commands no longer report "CLI coordination cleanup failed" despite
  succeeding. Found by the new stability guard's churn section.
- cli/main(win): install/update/uninstall and the Augment hook-script
  removal derive the managed launcher's identity from its plain
  drive-form path (cli_windows_plain_utf8), not the \\?\ extended-length
  form, so agent-config ownership matches on Windows and uninstall no
  longer leaves the MCP entry or owned hook scripts behind.
- security: the `daemon start --open` browser launch is now shell-free
  (ShellExecuteW on Windows, cbm_exec_no_shell elsewhere) instead of
  system(); the CORS origin check spells out its two literal loopback
  URLs so the static URL audit sees a complete value. Both clear the
  Layer-1 allow-list audit.
- coordination: version-cohort lock retries now sleep a per-process
  jittered interval — fixed-period retries can phase-lock two
  participants so one starves. The activation-quiesce test's
  observation window now covers worst-case candidate staging (three
  tamper-defense hashes over a ~1 GB sanitizer binary exceed the old
  30 s budget on container I/O).
- smoke/soak: the agent-config smoke matches Windows config paths
  through their escaped-backslash quoted form; the soak parser skips the
  update-available banner before the JSON summary and gained a one-shot
  CLI admission-churn phase inside the RSS/FD leak window. The build-dir
  safety fixture creates a real symlink/junction (MSYS2 ln -s otherwise
  deep-copies) so its traversal-refusal contract is genuinely exercised,
  and the symlinked-agent-roots test demotes its planted links to an
  unprivileged owner when run as root so the refusal is really tested.
- tests(win): a new daemon-stability guard exercises the parameter
  surface, hook fail-open with its rate-limited notice, start-twice and
  occupied-port handling, busy-stop refusal against a live MCP session,
  kill -9 crash recovery, and sequential/parallel one-shot churn.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-22 17:48:20 +02:00

107 lines
3.8 KiB
Bash

#!/usr/bin/env bash
# Regression guard: build cleanup paths must stay within one direct child of
# the repository's build/ directory. A malformed BUILD_DIR must be rejected
# before any rm invocation can observe it.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck source=../scripts/path-safety.sh
source "$ROOT/scripts/path-safety.sh"
for path in build/c build/linux-arm64 build/linux-amd64 build/win-cross; do
cbm_require_safe_build_dir "$path"
done
for path in \
"" \
build \
build/ \
. \
.. \
../outside \
/tmp/outside \
build/../outside \
build/./c \
build//c \
build/nested/c \
'build\..\outside'; do
if cbm_require_safe_build_dir "$path" >/dev/null 2>&1; then
echo "FAIL: unsafe BUILD_DIR was accepted: '$path'" >&2
exit 1
fi
done
# Create a real directory link: a native symlink, or an NTFS junction on
# Windows (MSYS reports junctions as symlinks). MSYS2's default `ln -s`
# silently DEEP-COPIES the target, which would turn these traversal fixtures
# into vacuous copies that the validator rightly accepts. Returns nonzero
# when this environment cannot create a real link at all — the traversal
# scenario then cannot exist, and the caller skips instead of asserting
# against a copy.
make_real_dir_link() {
local target="$1" link="$2"
if MSYS=winsymlinks:nativestrict ln -s -- "$target" "$link" 2>/dev/null &&
[ -L "$link" ]; then
return 0
fi
rm -rf -- "$link"
case "$(uname -s)" in
MINGW* | MSYS* | CYGWIN*)
cmd //c "mklink /J \"$(cygpath -w "$link")\" \"$(cygpath -w "$target")\"" \
>/dev/null 2>&1 || true
;;
esac
[ -L "$link" ]
}
# Lexical containment is not enough: an attacker-controlled or accidental
# build/ symlink would make ROOT/build/c traverse outside the repository.
symlink_fixture="$(mktemp -d "${TMPDIR:-/tmp}/cbm-build-dir-symlink.XXXXXX")"
trap 'rm -rf -- "$symlink_fixture"' EXIT
mkdir -p "$symlink_fixture/repo" "$symlink_fixture/outside"
if make_real_dir_link "$symlink_fixture/outside" "$symlink_fixture/repo/build"; then
if (
cd "$symlink_fixture/repo"
cbm_require_safe_build_dir build/c "$symlink_fixture/repo"
); then
echo "FAIL: BUILD_DIR accepted a symlinked build/ ancestor" >&2
exit 1
fi
else
echo "SKIP: no real symlink/junction support here; ancestor-traversal fixture not exercisable"
fi
mkdir -p "$symlink_fixture/safe-repo/build" "$symlink_fixture/final-target"
printf 'keep\n' >"$symlink_fixture/final-target/sentinel"
if make_real_dir_link "$symlink_fixture/final-target" "$symlink_fixture/safe-repo/build/c"; then
if ! cbm_remove_build_dir "$symlink_fixture/safe-repo" build/c; then
echo "FAIL: safe removal rejected a final-component symlink" >&2
exit 1
fi
if [ -e "$symlink_fixture/safe-repo/build/c" ] ||
[ ! -f "$symlink_fixture/final-target/sentinel" ]; then
echo "FAIL: final-component symlink removal followed the link" >&2
exit 1
fi
else
echo "SKIP: no real symlink/junction support here; final-component fixture not exercisable"
fi
mkdir -p "$symlink_fixture/safe-repo/build/ordinary"
printf 'remove\n' >"$symlink_fixture/safe-repo/build/ordinary/artifact"
cbm_remove_build_dir "$symlink_fixture/safe-repo" build/ordinary
if [ -e "$symlink_fixture/safe-repo/build/ordinary" ]; then
echo "FAIL: ordinary build child was not removed" >&2
exit 1
fi
# Direct Makefile cleanup is a public developer entrypoint too; it must use
# the same guard instead of interpolating an untrusted value into a shell.
if make -s -f "$ROOT/Makefile.cbm" clean-c BUILD_DIR=build/nested/c >/dev/null 2>&1; then
echo "FAIL: make clean-c accepted an unsafe BUILD_DIR" >&2
exit 1
fi
echo "Build directory safety contract passed"