1f674c805e
Microsoft's ML flagged the rc.1 release binaries. The decisive evidence is that the SAME sha256 went from 0/62 clean to Microsoft-detected in about an hour with no byte change, so the verdict lives partly in scanner-side state and no code change can promise a clean result. What code CAN do is stop shipping things that have no business in a release artifact, which is worth doing on its own merits and incidentally widens the classifier margin. Every claim below is verified against a built binary by the new gate, not by reading source. Executable stack (the worst of the findings). vendored/nomic/code_vectors_blob.S is the only assembly in the build and carried no .note.GNU-stack. An unannotated object makes ld assume the worst for the whole link, so EVERY Linux release we have ever shipped had GNU_STACK RWE. Adds the note (cause) plus ELF-only -Wl,-z,noexecstack (outcome); the gate fails the release if it returns. Test seams are now opt-in, never opt-out. TEST_SEAMS=1 defines CBM_ENABLE_TEST_SEAMS; without it the crash-orphan probe -- which forks a child that ignores SIGTERM and loops forever, then writes its pid to a caller-supplied path -- and the lease-ownership marker compile to trivial stubs, so call sites are untouched and the binary holds no fork, no signal handler and no env-var string. Opt-IN is the point: forgetting the flag yields a clean binary rather than a leaky one. scripts/test.sh requests it in the leg that consumes it, and tests/test_worker_watchdog.sh now asserts the capability up front instead of dying later with an opaque "Killed: 9". The daemon's background version check is gone. It spawned curl against api.github.com/repos/.../releases/latest on the first eligible session of every run to say "a newer version exists" -- a release URL and an outbound request in every shipped binary, for something the install scripts already report. The INJECTABLE SEAM survives: update_ops is still honoured, the fakes in tests/test_daemon_application.c still cover notice/ownership/cancellation/replay, and with no provider application_update_subscribe_locked returns early so no generation ever starts. "No network request by default" is now structural. Dead capability out of release builds. The tar.gz/zip extraction block (gzip_decompress through cbm_extract_binary_from_zip, plus its cli.h declarations) moves under CBM_CLI_ENABLE_TEST_API -- verified self-contained, zero uses of any helper outside it, only callers the excluded updater and tests/test_cli.c. Downloading an archive, decompressing it, picking an executable out of it and marking it executable is the canonical dropper composite; it is now absent rather than merely unreachable. SQLite is built with -DSQLITE_OMIT_LOAD_EXTENSION (no caller of load_extension anywhere in src/ or internal/), removing that API surface and part of the dlopen/dlsym surface. Temp files and environment scanning (S2/S3). Predictable paths in mcp.c, artifact.c and diagnostics.c are created privately and exclusively and written through the returned descriptor; pass_envscan.c no longer descends symlinked directories out of the project root, and its fixed 512-byte path buffers no longer truncate into pointer arithmetic that could land outside the buffer. Build-time entropy. mimalloc's version banner baked __DATE__/__TIME__ into every binary, so two builds of identical source seconds apart could never share a hash and no release could inherit a false-positive determination made about its predecessor. Local patch removes it (marked to survive refreshes), -Wdate-time makes any future use a build error, and -Wl,--no-insert-timestamp stops the PE header carrying the link clock. scripts/ci/check-binary-composition.sh is the proof that each removal stays removed, wired into package-release.sh after strip so the local artifact-flow smoke enforces exactly what CI does. It asserts absences plus a CANARY string, so handing it a compressed, stubbed or empty file fails instead of passing vacuously, and a missing tool is a hard error -- a skipped assertion must never look like a satisfied one. Two build-system traps found by that gate, both of which had silently defeated a fix: the product binary is compiled in one shot from sources, so a flag flip did not rebuild it (now tracked by a .build-config stamp that also removes the binary, making it independent of mtime granularity); and prod_sqlite3.o / prod_mimalloc.o depended on a single named source, so SQLITE_OMIT_LOAD_EXTENSION and the mimalloc patch BOTH compiled to nothing on the first incremental build. Source review would have called them done. Deliberately NOT changed. Three seams stay in release artifacts because scripts/smoke-test.sh runs against the real artifact and needs them: CBM_TEST_CRASH_ON and CBM_TEST_HANG_ON inject the faults that prove supervisor recovery, and CBM_TEST_WINDOWS_USER_PATH_RUN_ID is what keeps the PATH smoke from writing the tester's actual PATH. The gate treats those as an allowlist, so a NOVEL seam still fails. The true no-UI standard build is deferred rather than rushed: src/ui/* is in PROD_SRCS and four files outside src/ui reference UI symbols, including the daemon that serves the UI, so that assertion reports instead of failing until the split lands -- a gate everyone knows is red teaches people to ignore gates. No grammar is removed. ObjectScript accounts for essentially all binary growth since the last provably-clean release (+21.4MB rodata, +1.1MB text from two four-line shims), which made it the obvious ablation candidate, but a dry run performed twelve real Defender endpoint scans across standard/UI and amd64/arm64 with ObjectScript, the daemon and the expanded hooks all present and every scan was clean. Nothing there is a deterministic trigger, so cutting a community-contributed language would spend a real feature on unproven margin. Lean is not a candidate either: at 99.6MB of source it is by far the largest grammar, but it shipped in v0.9.0 which scanned 20/20 clean, so removing it would produce a novel unscanned profile instead of restoring a known-good one. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
286 lines
12 KiB
Bash
Executable File
286 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# test.sh — THE canonical test leg. Every venue (local ladder, PR CI, dry run,
|
|
# release) runs tests through this file; iteration happens through its flags,
|
|
# never through a second entry point.
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: scripts/test.sh [--suites LIST] [--arch ARCH] [VAR=VAL ...]
|
|
|
|
The canonical test entry: identical in local CI, PR CI, dry run and release.
|
|
DEFAULT (no --suites) is exactly what CI runs: static contract checks
|
|
(Step 0a-0j), a CLEAN sanitizer build, every suite via the parallel harness,
|
|
then the prod-binary regression guards (Steps 4-6).
|
|
|
|
Modes:
|
|
(default) The venue leg. Clean build (scripts/clean.sh) + all suites +
|
|
all contract steps. This is the shape every gate runs.
|
|
--suites LIST ITERATION mode: comma- or space-separated suite names, e.g.
|
|
--suites daemon,daemon_ipc. Rebuilds the test-runner
|
|
INCREMENTALLY (make dependency tracking, no clean) and runs
|
|
only those suites — seconds, not minutes. Skips the contract
|
|
steps and prod-binary guards; the full default run remains
|
|
the merge gate. Suite names: build/c/test-runner --list-suites.
|
|
--tsan ThreadSanitizer leg (data-race gate): builds and runs the
|
|
widened TSan runner via make test-tsan — the same leg CI's
|
|
tsan jobs and the compose test-tsan service run.
|
|
|
|
Options:
|
|
--arch ARCH Force target arch (arm64 | x86_64), e.g. under Rosetta.
|
|
-h, --help This text.
|
|
|
|
Make passthrough (VAR=VAL, forwarded verbatim):
|
|
CC= CXX= Compiler override, e.g. CC=gcc-14 CXX=g++-14.
|
|
BUILD_DIR= Build in an isolated directory (containers/sanitizer variants).
|
|
SANITIZE= Override sanitizer flags. Platform defaults when unset:
|
|
unix/CLANG64 use the Makefile's ASan+UBSan test flags;
|
|
CLANGARM64 (Windows on ARM, no ASan runtime) gets CI's
|
|
trap-UBSan set (-fsanitize=undefined -fsanitize-trap=undefined
|
|
-fstack-protector-strong -fno-omit-frame-pointer) applied HERE
|
|
so local and CI build identical test binaries. Pass SANITIZE=
|
|
(empty) for a plain build when debugging a trap.
|
|
|
|
Environment:
|
|
CBM_TEST_SEQUENTIAL=1 Single-process runner instead of the parallel harness.
|
|
CBM_RUN_HANG_TEST=1 Opt-in C++ index-hang regression (#410, needs prod).
|
|
CBM_NO_CCACHE=1 Disable the content-verified compiler cache.
|
|
CBM_TEST_SHARD/_LEG Set by CI's sharded legs; leave unset locally.
|
|
|
|
Examples:
|
|
scripts/test.sh # the full venue leg (what CI runs)
|
|
scripts/test.sh --suites daemon_ipc # one suite, incremental, seconds
|
|
scripts/test.sh --suites "arena hash_table" CC=clang CXX=clang++
|
|
scripts/test.sh SANITIZE= --suites daemon_ipc # plain build for trap debugging
|
|
EOF
|
|
}
|
|
|
|
# Parse --help / --suites / --tsan / --arch before sourcing env.sh.
|
|
# STRICT: an unknown flag or a stray word is an immediate usage error, never
|
|
# silently swallowed — agents must know exactly what a run will do.
|
|
SUITES=""
|
|
TSAN=0
|
|
prev_arg=""
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
-h|--help) usage; exit 0 ;;
|
|
--tsan) :;;
|
|
--suites) :;; # next arg is the value, handled below
|
|
--suites=*) SUITES="${arg#--suites=}" ;;
|
|
--arch) :;; # next arg is the value, handled below
|
|
--arch=*) :;; # handled below
|
|
-*)
|
|
echo "test.sh: unknown option '$arg'. Please consult --help." >&2
|
|
exit 2
|
|
;;
|
|
arm64|x86_64)
|
|
if [[ "${prev_arg:-}" != "--arch" && "${prev_arg:-}" != "--suites" ]]; then
|
|
echo "test.sh: unexpected argument '$arg' (did you mean --arch $arg?). Please consult --help." >&2
|
|
exit 2
|
|
fi
|
|
;;
|
|
*=*) :;; # VAR=VAL make passthrough, validated below
|
|
*)
|
|
if [[ "${prev_arg:-}" != "--suites" ]]; then
|
|
echo "test.sh: unexpected argument '$arg'. Please consult --help." >&2
|
|
exit 2
|
|
fi
|
|
;;
|
|
esac
|
|
prev_arg="$arg"
|
|
done
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--tsan) TSAN=1 ;;
|
|
arm64|x86_64)
|
|
if [[ "${prev_arg2:-}" == "--arch" ]]; then
|
|
export CBM_ARCH="$arg"
|
|
fi
|
|
;;
|
|
*)
|
|
if [[ "${prev_arg2:-}" == "--suites" ]]; then
|
|
SUITES="$arg"
|
|
fi
|
|
;;
|
|
esac
|
|
prev_arg2="$arg"
|
|
done
|
|
# Normalize comma separation to the runner's space-separated argv form.
|
|
SUITES="${SUITES//,/ }"
|
|
case "${prev_arg:-}" in
|
|
--suites|--arch)
|
|
echo "test.sh: '$prev_arg' needs a value. Please consult --help." >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
if [ "$TSAN" -eq 1 ] && [ -n "$SUITES" ]; then
|
|
echo "test.sh: --tsan and --suites are separate modes (the TSan leg has its own suite set). Please consult --help." >&2
|
|
exit 2
|
|
fi
|
|
prev_arg=""
|
|
|
|
# Also support --arch=value
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--arch=*) export CBM_ARCH="${arg#--arch=}" ;;
|
|
esac
|
|
done
|
|
|
|
# shellcheck source=env.sh
|
|
source "$ROOT/scripts/env.sh"
|
|
# shellcheck source=path-safety.sh
|
|
source "$ROOT/scripts/path-safety.sh"
|
|
|
|
# Forward CC/CXX and collect make-passthrough args. BUILD_DIR is honored for
|
|
# the explicit target path below so containerized legs can build in their own
|
|
# directory instead of clobbering the host's native build/c artifacts.
|
|
# MAKE_ARGS is an ARRAY so a VAR=VAL whose value contains spaces (the
|
|
# windows-11-arm leg passes SANITIZE with four flags) survives as ONE make
|
|
# argument. The old string accumulation re-split it at every expansion and
|
|
# make swallowed the second flag's leading -f as its makefile option.
|
|
MAKE_ARGS=()
|
|
BUILD_DIR="build/c"
|
|
SANITIZE_GIVEN=0
|
|
prev_arg=""
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
CC=*|CXX=*) export "${arg}" ;;
|
|
--arch|--arch=*) ;; # already handled
|
|
arm64|x86_64) ;; # already handled
|
|
--tsan) ;; # already handled
|
|
--suites|--suites=*) ;; # already handled (value skipped via prev_arg below)
|
|
BUILD_DIR=*) BUILD_DIR="${arg#BUILD_DIR=}"; MAKE_ARGS+=("$arg") ;;
|
|
SANITIZE=*) SANITIZE_GIVEN=1; MAKE_ARGS+=("$arg") ;;
|
|
*=*)
|
|
if [[ "${prev_arg:-}" != "--suites" ]]; then
|
|
MAKE_ARGS+=("$arg") # forward any VAR=VAL to make
|
|
fi
|
|
;;
|
|
esac
|
|
prev_arg="$arg"
|
|
done
|
|
|
|
# Platform default absorbed FROM CI (previously inline in _test.yml, so the
|
|
# local arm64 leg silently built without it — that divergence is why the SQLite
|
|
# page-cache misalignment was fatal only on the windows-11-arm runner): native
|
|
# ARM64 Windows has no ASan runtime, so its sanitizer gate is UBSan in trap
|
|
# mode + stack protector. Applied here, once, for every venue; an explicit
|
|
# SANITIZE=... (or SANITIZE=) argument overrides.
|
|
if [ "$SANITIZE_GIVEN" -eq 0 ] && [ "${MSYSTEM:-}" = "CLANGARM64" ]; then
|
|
MAKE_ARGS+=("SANITIZE=-fsanitize=undefined -fsanitize-trap=undefined -fstack-protector-strong -fno-omit-frame-pointer")
|
|
fi
|
|
|
|
print_env "test.sh"
|
|
|
|
# ── TSan mode (--tsan): the data-race gate ──
|
|
# One entry for every venue: CI's tsan jobs and the compose test-tsan service
|
|
# both run this instead of carrying their own make invocation.
|
|
if [ "$TSAN" -eq 1 ]; then
|
|
echo "=== test.sh: TSan leg (make test-tsan) ==="
|
|
make -j"$NPROC" -f Makefile.cbm "$BUILD_DIR/test-runner-tsan" ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
|
make -f Makefile.cbm test-tsan ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
|
exit "$?"
|
|
fi
|
|
|
|
# ── Iteration mode (--suites): incremental rebuild + subset run ──
|
|
# The documented fast path: no clean, no contract steps, no prod-binary
|
|
# guards — those all still gate every merge through the default full run.
|
|
if [ -n "$SUITES" ]; then
|
|
echo "=== test.sh: ITERATION mode — suites: $SUITES (incremental build) ==="
|
|
make -j"$NPROC" -f Makefile.cbm "$BUILD_DIR/test-runner" ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
|
# shellcheck disable=SC2086 # suite list is deliberately word-split
|
|
"$BUILD_DIR/test-runner" $SUITES
|
|
exit "$?"
|
|
fi
|
|
|
|
# Step 0: fast build/security harness regressions run before the compiler-heavy
|
|
# suite. The Windows package surface is static here; native launcher behavior is
|
|
# exercised by scripts/test-windows.ps1.
|
|
echo "=== Step 0a: build directory safety contract ==="
|
|
bash "$ROOT/tests/test_build_dir_safety.sh"
|
|
|
|
echo "=== Step 0b: Windows VM worktree sync contract ==="
|
|
bash "$ROOT/tests/test_vm_worktree_manifest.sh"
|
|
|
|
echo "=== Step 0c: UI development proxy security contract ==="
|
|
bash "$ROOT/tests/test_ui_dev_proxy_security.sh"
|
|
|
|
echo "=== Step 0d: daemon soak recovery contract ==="
|
|
bash "$ROOT/tests/test_soak_daemon_recovery_contract.sh"
|
|
|
|
echo "=== Step 0e: Windows launcher bundle contract ==="
|
|
bash "$ROOT/tests/test_windows_bundle_contract.sh"
|
|
|
|
echo "=== Step 0f: tree-sitter runtime Makefile dependencies ==="
|
|
bash "$ROOT/tests/test_makefile_ts_runtime_dependencies.sh"
|
|
|
|
echo "=== Step 0g: security fuzz harness self-test ==="
|
|
bash "$ROOT/tests/test_security_fuzz_harness.sh"
|
|
|
|
echo "=== Step 0h: smoke release-fixture contract ==="
|
|
bash "$ROOT/tests/test_smoke_fixture_contract.sh"
|
|
|
|
echo "=== Step 0i: parallel suite scheduler contract ==="
|
|
bash "$ROOT/tests/test_parallel_harness_contract.sh"
|
|
|
|
echo "=== Step 0j: venue parity contract (one harness, every venue) ==="
|
|
bash "$ROOT/tests/test_venue_parity_contract.sh"
|
|
|
|
# Verify compiler supports target arch
|
|
verify_compiler "$CC"
|
|
|
|
# Step 1: Clean (scoped to this leg's build directory)
|
|
BUILD_DIR="$BUILD_DIR" scripts/clean.sh
|
|
|
|
# Step 2 + 3: Build, then run every suite as parallel processes (identical
|
|
# gate quality — see the ZERO-LOSS CONTRACT in scripts/run-tests-parallel.sh:
|
|
# the suite set is enumerated from the runner itself and union-guarded, and
|
|
# pass/fail/skip totals aggregate to the same numbers as the sequential run).
|
|
# CBM_TEST_SEQUENTIAL=1 restores the single-process runner.
|
|
make -j"$NPROC" -f Makefile.cbm "$BUILD_DIR/test-runner" ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
|
if [ "${CBM_TEST_SEQUENTIAL:-0}" = "1" ]; then
|
|
make -f Makefile.cbm test ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
|
else
|
|
make -f Makefile.cbm test-par ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
|
fi
|
|
|
|
# Step 4: C++ large-TU index-hang regression guard (#410). Runs the PROD binary
|
|
# in a subprocess with a wall-clock timeout — a hang must fail, not block the run.
|
|
# Opt-in via CBM_RUN_HANG_TEST=1 (it needs the prod binary, which the ASan unit
|
|
# run above does not build). Skipped by default so the fast unit run stays fast.
|
|
if [ "${CBM_RUN_HANG_TEST:-0}" = "1" ]; then
|
|
echo "=== Step 4: C++ index-hang regression (#410) ==="
|
|
bash "$ROOT/tests/test_cpp_index_hang.sh"
|
|
fi
|
|
|
|
# Step 5: Parent-death watchdog regression (#406/#407). Builds the prod stdio
|
|
# binary and verifies it self-exits when its launching parent is killed.
|
|
#
|
|
# TEST_SEAMS=1: the worker-mode leg below needs the crash-orphan probe, which is
|
|
# compiled out of ordinary builds (it forks a SIGTERM-ignoring child — see
|
|
# src/main.c). Requesting it HERE, in the leg that consumes it, is what keeps
|
|
# release artifacts free of it; scripts/ci/check-binary-composition.sh proves
|
|
# they stay that way.
|
|
echo "=== Step 5: parent-death watchdog regression (#406/#407) ==="
|
|
make -j"$NPROC" -f Makefile.cbm cbm TEST_SEAMS=1 ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
|
WATCHDOG_BINARY="$ROOT/$BUILD_DIR/codebase-memory-mcp"
|
|
CBM_TEST_BINARY="$WATCHDOG_BINARY" bash "$ROOT/tests/test_parent_watchdog.sh"
|
|
|
|
# Step 5b: worker-mode parent-death watchdog (#845). A supervised index worker
|
|
# (`cli --index-worker …`) whose supervisor dies must self-exit instead of
|
|
# indexing on as an orphan. Reuses the prod binary built in Step 5.
|
|
echo "=== Step 5b: worker-mode watchdog regression (#845) ==="
|
|
CBM_TEST_BINARY="$WATCHDOG_BINARY" bash "$ROOT/tests/test_worker_watchdog.sh"
|
|
|
|
# Step 6: security-strings URL allow-list regression. The MSYS2 CLANG64 toolchain
|
|
# bakes its package-tracker URL into the static Windows .exe; the binary string
|
|
# audit must allow-list it (Windows-only — Linux smoke never saw it).
|
|
echo "=== Step 6: security-strings allow-list regression ==="
|
|
bash "$ROOT/tests/test_security_strings_allowlist.sh"
|
|
|
|
echo "=== All tests passed ==="
|