mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-09 09:11:00 +08:00
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>
200 lines
7.8 KiB
Bash
Executable File
200 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# test_worker_watchdog.sh — regression guard for the WORKER-mode parent-death
|
|
# watchdog (#845). A supervised index worker (`cli --index-worker
|
|
# index_repository …`) whose supervisor dies must exit on its own instead of
|
|
# indexing on as an orphan (orphaned workers contributed to memory pressure
|
|
# during the 2026-07-04 host panics). The MCP-server watchdog (#406/#407,
|
|
# tests/test_parent_watchdog.sh) did not cover CLI worker mode.
|
|
#
|
|
# Strategy: launch the worker under a wrapper "parent" on a fixture where the
|
|
# test-only injector (CBM_TEST_HANG_ON) busy-spins on one file, so the worker
|
|
# is guaranteed to still be mid-index when the wrapper is killed — the guard
|
|
# cannot pass vacuously via the worker simply finishing. The hidden
|
|
# --index-worker-single-thread + --index-worker-marker recovery knobs give a
|
|
# deterministic "worker is AT the hang file" sync point: the worker writes the
|
|
# rel_path it is about to process before touching it. After kill -9 of the
|
|
# wrapper, the worker-mode watchdog must notice the changed ppid and kill its
|
|
# isolated process group within a few seconds. A test-only worker descendant is
|
|
# kept alive in that group to prove teardown is tree-wide rather than merely a
|
|
# root-process exit. The recovery knobs are passed as the supervisor's hidden
|
|
# worker argv, never by mutating the supervisor environment. Skipped on
|
|
# Windows-like shells (the watchdog is POSIX-only).
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
BINARY="${CBM_TEST_BINARY:-${ROOT}/build/c/codebase-memory-mcp}"
|
|
|
|
case "$(uname -s)" in
|
|
MINGW*|MSYS*|CYGWIN*)
|
|
echo "skipping worker watchdog test on Windows"
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
if [[ ! -x "${BINARY}" ]]; then
|
|
echo "missing binary: ${BINARY}" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# PRECONDITION, not a skip: this test drives the crash-orphan probe, which is
|
|
# compiled out of ordinary builds (it forks a SIGTERM-ignoring child, which must
|
|
# never ship — see src/main.c and TEST_SEAMS in Makefile.cbm). Against a
|
|
# seam-less binary the probe silently no-ops and the test dies with an opaque
|
|
# "Killed: 9" many lines later. Assert the capability up front and say exactly
|
|
# how to get it. Failing (not skipping) is deliberate: a skip here would hide
|
|
# the loss of watchdog coverage entirely.
|
|
if ! LC_ALL=C grep -a -q -F 'CBM_TEST_WORKER_DESCENDANT_PID_FILE' "${BINARY}"; then
|
|
echo "binary lacks the crash-orphan test seam: ${BINARY}" >&2
|
|
echo " rebuild it with: make -f Makefile.cbm cbm TEST_SEAMS=1" >&2
|
|
echo " or run this test through scripts/test.sh, which does that for you." >&2
|
|
exit 2
|
|
fi
|
|
|
|
if command -v shasum >/dev/null 2>&1; then
|
|
BUILD_FINGERPRINT="$(shasum -a 256 "${BINARY}" | awk '{print $1}')"
|
|
elif command -v sha256sum >/dev/null 2>&1; then
|
|
BUILD_FINGERPRINT="$(sha256sum "${BINARY}" | awk '{print $1}')"
|
|
elif command -v openssl >/dev/null 2>&1; then
|
|
BUILD_FINGERPRINT="$(openssl dgst -sha256 "${BINARY}" | awk '{print $NF}')"
|
|
else
|
|
echo "no SHA-256 command available for worker build binding" >&2
|
|
exit 2
|
|
fi
|
|
if [[ ! "${BUILD_FINGERPRINT}" =~ ^[0-9a-f]{64}$ ]]; then
|
|
echo "invalid worker build fingerprint: ${BUILD_FINGERPRINT}" >&2
|
|
exit 2
|
|
fi
|
|
|
|
tmpdir="$(mktemp -d)"
|
|
wrapper_pid=""
|
|
cleanup() {
|
|
if [[ -s "${tmpdir}/child.pid" ]]; then
|
|
local child_pid
|
|
child_pid="$(cat "${tmpdir}/child.pid" 2>/dev/null || true)"
|
|
[[ -n "${child_pid}" ]] && kill -9 "${child_pid}" 2>/dev/null || true
|
|
fi
|
|
if [[ -s "${tmpdir}/descendant.pid" ]]; then
|
|
local descendant_pid
|
|
descendant_pid="$(cat "${tmpdir}/descendant.pid" 2>/dev/null || true)"
|
|
[[ -n "${descendant_pid}" ]] && kill -9 "${descendant_pid}" 2>/dev/null || true
|
|
fi
|
|
[[ -n "${wrapper_pid}" ]] && kill -9 "${wrapper_pid}" 2>/dev/null || true
|
|
rm -rf "${tmpdir}"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# Fixture: one good file + one the injector busy-spins on.
|
|
mkdir -p "${tmpdir}/repo"
|
|
printf 'def good():\n return 1\n' > "${tmpdir}/repo/good.py"
|
|
printf 'def slow():\n return 2\n' > "${tmpdir}/repo/hang_me.py"
|
|
|
|
# Wrapper "parent": launches the worker exactly as the supervisor would
|
|
# (cli --index-worker … --response-out), records the child PID, then waits.
|
|
cat >"${tmpdir}/wrapper.sh" <<'SH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
"${CBM_BINARY}" cli --index-worker \
|
|
--index-worker-build "${BUILD_FINGERPRINT}" \
|
|
index_repository "${ARGS_JSON}" \
|
|
--response-out "${TMPDIR_PATH}/resp" \
|
|
--index-worker-single-thread \
|
|
--index-worker-marker "${TMPDIR_PATH}/marker" \
|
|
>/dev/null 2>"${TMPDIR_PATH}/child.err" &
|
|
echo "$!" >"${TMPDIR_PATH}/child.pid"
|
|
wait
|
|
SH
|
|
chmod +x "${tmpdir}/wrapper.sh"
|
|
|
|
CBM_BINARY="${BINARY}" BUILD_FINGERPRINT="${BUILD_FINGERPRINT}" TMPDIR_PATH="${tmpdir}" \
|
|
ARGS_JSON="{\"repo_path\":\"${tmpdir}/repo\"}" \
|
|
CBM_TEST_HANG_ON=hang_me \
|
|
CBM_TEST_WORKER_DESCENDANT_PID_FILE="${tmpdir}/descendant.pid" \
|
|
"${tmpdir}/wrapper.sh" &
|
|
wrapper_pid=$!
|
|
|
|
# Wait for the worker PID file to appear.
|
|
for _ in {1..50}; do
|
|
[[ -s "${tmpdir}/child.pid" ]] && break
|
|
sleep 0.1
|
|
done
|
|
if [[ ! -s "${tmpdir}/child.pid" ]]; then
|
|
echo "worker pid file was not written" >&2
|
|
[[ -s "${tmpdir}/child.err" ]] && cat "${tmpdir}/child.err" >&2
|
|
exit 3
|
|
fi
|
|
child_pid="$(cat "${tmpdir}/child.pid")"
|
|
|
|
# The validated worker creates its own process group before starting this
|
|
# deliberate long-lived descendant. Both must be alive and in that group before
|
|
# the wrapper is killed, otherwise the tree assertion would be vacuous.
|
|
for _ in {1..50}; do
|
|
[[ -s "${tmpdir}/descendant.pid" ]] && break
|
|
sleep 0.1
|
|
done
|
|
if [[ ! -s "${tmpdir}/descendant.pid" ]]; then
|
|
echo "worker descendant pid file was not written" >&2
|
|
[[ -s "${tmpdir}/child.err" ]] && cat "${tmpdir}/child.err" >&2
|
|
exit 3
|
|
fi
|
|
descendant_pid="$(cat "${tmpdir}/descendant.pid")"
|
|
if ! kill -0 "${descendant_pid}" 2>/dev/null; then
|
|
echo "worker descendant exited before supervisor death" >&2
|
|
exit 3
|
|
fi
|
|
child_pgid="$(ps -p "${child_pid}" -o pgid= 2>/dev/null | tr -d '[:space:]')"
|
|
descendant_pgid="$(ps -p "${descendant_pid}" -o pgid= 2>/dev/null | tr -d '[:space:]')"
|
|
if [[ -z "${child_pgid}" || "${child_pgid}" != "${child_pid}" ||
|
|
"${descendant_pgid}" != "${child_pgid}" ]]; then
|
|
echo "worker tree is not in its expected isolated process group" >&2
|
|
exit 3
|
|
fi
|
|
|
|
# Sync point: once the marker names the hang file, the worker is provably
|
|
# mid-index (busy-spinning in extraction) and long past watchdog installation.
|
|
for _ in {1..100}; do
|
|
if [[ -s "${tmpdir}/marker" ]] && grep -q "hang_me" "${tmpdir}/marker"; then
|
|
break
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
if ! grep -q "hang_me" "${tmpdir}/marker" 2>/dev/null; then
|
|
echo "worker never reached the hang file (marker: $(cat "${tmpdir}/marker" 2>/dev/null || echo '<empty>'))" >&2
|
|
[[ -s "${tmpdir}/child.err" ]] && cat "${tmpdir}/child.err" >&2
|
|
exit 3
|
|
fi
|
|
|
|
# Vacuity guard: the worker must still be ALIVE (busy-spinning) right now —
|
|
# it cannot "pass" by having finished before the parent dies.
|
|
if ! kill -0 "${child_pid}" 2>/dev/null; then
|
|
echo "worker exited before the supervisor was killed — guard would be vacuous" >&2
|
|
[[ -s "${tmpdir}/child.err" ]] && cat "${tmpdir}/child.err" >&2
|
|
exit 3
|
|
fi
|
|
|
|
# Kill the wrapper parent: the orphaned worker must now self-exit.
|
|
kill -9 "${wrapper_pid}"
|
|
wait "${wrapper_pid}" 2>/dev/null || true
|
|
|
|
process_gone_or_zombie() {
|
|
local pid="$1"
|
|
if ! kill -0 "${pid}" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
local state
|
|
state="$(ps -p "${pid}" -o stat= 2>/dev/null | tr -d '[:space:]' || true)"
|
|
[[ "${state}" == Z* ]]
|
|
}
|
|
|
|
deadline=$((SECONDS + 15))
|
|
while (( SECONDS < deadline )); do
|
|
if process_gone_or_zombie "${child_pid}" && process_gone_or_zombie "${descendant_pid}"; then
|
|
echo "ok: worker ${child_pid} and descendant ${descendant_pid} exited after supervisor death"
|
|
exit 0
|
|
fi
|
|
sleep 0.2
|
|
done
|
|
|
|
echo "index worker tree survived supervisor death (worker=${child_pid}, descendant=${descendant_pid})" >&2
|
|
[[ -s "${tmpdir}/child.err" ]] && cat "${tmpdir}/child.err" >&2
|
|
exit 1
|