Skip to content

Commit fe4396f

Browse files
authored
Merge branch 'main' into fix/1287-persisted-coverage-summary
2 parents b619493 + 07cac7d commit fe4396f

26 files changed

Lines changed: 1402 additions & 104 deletions

‎.gitattributes‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Shell scripts must stay LF: with git's Windows default (core.autocrlf=true)
2+
# they check out as CRLF and bash inside the build containers rejects them.
3+
*.sh text eol=lf
4+
15
# Vendored tree-sitter grammars — machine-generated, hide from stats and diffs
26
internal/cbm/vendored/grammars/**/parser.c linguist-generated=true diff=false
37
internal/cbm/vendored/grammars/**/scanner.c linguist-generated=true diff=false

‎scripts/security-fuzz-random.sh‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ fi
2727

2828
echo "=== Fuzz Testing ($DURATION seconds) ==="
2929

30-
FUZZ_TMPDIR=$(mktemp -d)
31-
trap 'rm -rf "$FUZZ_TMPDIR"' EXIT
30+
# shellcheck source=test-runtime.sh
31+
source "$(dirname "${BASH_SOURCE[0]}")/test-runtime.sh"
32+
cbm_test_runtime_init
33+
FUZZ_TMPDIR="$CBM_TEST_RUNTIME_ROOT"
34+
trap 'cbm_test_runtime_cleanup "$BINARY"' EXIT
3235

3336
CRASHES=0
3437
ITERATIONS=0

‎scripts/security-fuzz.sh‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ PASS=0
2222
TOTAL=0
2323

2424
# Temp directory for input files (avoids pipe/stdin issues with timeout)
25-
FUZZ_TMPDIR=$(mktemp -d)
26-
trap 'rm -rf "$FUZZ_TMPDIR"' EXIT
25+
# shellcheck source=test-runtime.sh
26+
source "$(dirname "${BASH_SOURCE[0]}")/test-runtime.sh"
27+
cbm_test_runtime_init
28+
FUZZ_TMPDIR="$CBM_TEST_RUNTIME_ROOT"
29+
trap 'cbm_test_runtime_cleanup "$BINARY"' EXIT
2730
FUZZ_HOME="$FUZZ_TMPDIR/home"
28-
FUZZ_CACHE="$FUZZ_TMPDIR/cache"
29-
mkdir -p "$FUZZ_HOME" "$FUZZ_CACHE"
31+
mkdir "$FUZZ_HOME"
32+
export HOME="$FUZZ_HOME"
3033

3134
# Helper: send a payload to the MCP server and check it doesn't crash.
3235
# Uses temp file + perl alarm for portable timeout (works on macOS + Linux).
@@ -49,11 +52,9 @@ test_payload() {
4952
# Run with 10s timeout: GNU timeout → perl alarm fallback
5053
local ec=0
5154
if command -v timeout &>/dev/null; then
52-
HOME="$FUZZ_HOME" CBM_CACHE_DIR="$FUZZ_CACHE" \
53-
timeout 10 "$BINARY" < "$tmpinput" > "$tmpoutput" 2>&1 || ec=$?
55+
timeout 10 "$BINARY" < "$tmpinput" > "$tmpoutput" 2>&1 || ec=$?
5456
else
55-
HOME="$FUZZ_HOME" CBM_CACHE_DIR="$FUZZ_CACHE" \
56-
perl -e 'alarm(10); exec @ARGV' -- "$BINARY" \
57+
perl -e 'alarm(10); exec @ARGV' -- "$BINARY" \
5758
< "$tmpinput" > "$tmpoutput" 2>&1 || ec=$?
5859
fi
5960

@@ -88,8 +89,7 @@ test_invalid_index_interactive() {
8889
local tmpoutput="$FUZZ_TMPDIR/output_${TOTAL}.jsonl"
8990
local ec=0
9091

91-
HOME="$FUZZ_HOME" CBM_CACHE_DIR="$FUZZ_CACHE" \
92-
python3 "$(dirname "$0")/test_mcp_interactive.py" "$BINARY" \
92+
python3 "$(dirname "$0")/test_mcp_interactive.py" "$BINARY" \
9393
--scenario invalid-index --repo-path /nonexistent/path/abc123 \
9494
--response-timeout 45 --exit-timeout 15 > "$tmpoutput" 2>&1 || ec=$?
9595

‎scripts/security-install.sh‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
33

4-
# Layer 4: Install output audit — verifies install --dry-run writes only to expected paths.
4+
# Layer 4: Install output audit — runs a sandboxed real install and audits its writes.
55
#
66
# Checks:
77
# 1. All output file paths are in the expected set
@@ -19,8 +19,11 @@ fi
1919

2020
echo "=== Layer 4: Install Output Audit ==="
2121

22-
TMPDIR=$(mktemp -d)
23-
trap 'rm -rf "$TMPDIR"' EXIT
22+
# shellcheck source=test-runtime.sh
23+
source "$(dirname "${BASH_SOURCE[0]}")/test-runtime.sh"
24+
cbm_test_runtime_init
25+
TMPDIR="$CBM_TEST_RUNTIME_ROOT"
26+
trap 'cbm_test_runtime_cleanup "$BINARY"' EXIT
2427

2528
# Set HOME to tmpdir so install writes there instead of real home
2629
export HOME="$TMPDIR/home"

‎scripts/security-network.sh‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ if ! command -v strace &>/dev/null; then
3030
exit 0
3131
fi
3232

33-
TMPDIR=$(mktemp -d)
34-
trap 'rm -rf "$TMPDIR"' EXIT
33+
# shellcheck source=test-runtime.sh
34+
source "$(dirname "${BASH_SOURCE[0]}")/test-runtime.sh"
35+
cbm_test_runtime_init
36+
TMPDIR="$CBM_TEST_RUNTIME_ROOT"
37+
trap 'cbm_test_runtime_cleanup "$BINARY"' EXIT
3538

3639
# Create a minimal test project
3740
mkdir -p "$TMPDIR/project/src"

‎scripts/test-runtime.sh‎

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/env bash
2+
3+
# Sourced by test harnesses. Host paths stay separate from paths exported to a
4+
# native Windows product binary.
5+
CBM_TEST_RUNTIME_ROOT=""
6+
CBM_TEST_RUNTIME_DIR_HOST=""
7+
CBM_TEST_CACHE_DIR_HOST=""
8+
_CBM_TEST_RUNTIME_CREATED_ROOT=""
9+
_CBM_TEST_RUNTIME_PRODUCT_RUNTIME=""
10+
_CBM_TEST_RUNTIME_PRODUCT_CACHE=""
11+
12+
_cbm_test_runtime_error() {
13+
echo "test-runtime: $*" >&2
14+
}
15+
16+
_cbm_test_runtime_discard_partial() {
17+
local root="$1" parent="$2" name="${1##*/}"
18+
[[ -n "$root" && "$root" != "$name" && "${root%/*}" == "$parent" && ! -L "$root" ]] || return 0
19+
case "$name" in cbmrt.?*|cbmrt-?*) rm -rf -- "$root" >/dev/null 2>&1 || true ;; esac
20+
}
21+
22+
cbm_test_runtime_init() {
23+
[[ -z "$_CBM_TEST_RUNTIME_CREATED_ROOT" ]] ||
24+
{ _cbm_test_runtime_error "runtime already initialized"; return 1; }
25+
local platform windows=0 parent root="" runtime_host cache_host
26+
local product_runtime product_cache helper_dir helper_native root_native
27+
platform="$(uname -s)" ||
28+
{ _cbm_test_runtime_error "cannot identify the host platform"; return 1; }
29+
case "$platform" in
30+
MINGW*|MSYS*|CYGWIN*) windows=1 ;;
31+
Darwin) parent="/private/tmp" ;;
32+
*) parent="/tmp" ;;
33+
esac
34+
if (( windows )); then
35+
command -v cygpath >/dev/null 2>&1 ||
36+
{ _cbm_test_runtime_error "cygpath is required on Windows"; return 1; }
37+
if [[ -n "${CBM_CI_TEMP_ROOT:-}" ]]; then
38+
parent="$(cygpath -u "$CBM_CI_TEMP_ROOT")" ||
39+
{ _cbm_test_runtime_error "cannot convert CBM_CI_TEMP_ROOT"; return 1; }
40+
[[ -d "$parent" && ! -L "$parent" ]] ||
41+
{ _cbm_test_runtime_error "CBM_CI_TEMP_ROOT is not a real directory"; return 1; }
42+
root="$(mktemp -d "${parent%/}/cbmrt.XXXXXX")" ||
43+
{ _cbm_test_runtime_error "cannot create a private Windows CI root"; return 1; }
44+
else
45+
helper_dir="$(CDPATH= cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || return 1
46+
helper_native="$(cygpath -w "$helper_dir/ci/new-protected-temp-root.ps1")" ||
47+
{ _cbm_test_runtime_error "cannot convert protected-root helper"; return 1; }
48+
root_native="$(MSYS2_ARG_CONV_EXCL='*' powershell.exe -NoProfile \
49+
-ExecutionPolicy Bypass -File "$helper_native" -Prefix 'cbmrt-')" ||
50+
{ _cbm_test_runtime_error "protected Windows root creation failed"; return 1; }
51+
root_native="${root_native//$'\r'/}"
52+
[[ -n "$root_native" && "$root_native" != *$'\n'* ]] ||
53+
{ _cbm_test_runtime_error "protected-root helper returned an invalid path"; return 1; }
54+
root="$(cygpath -u "$root_native")" ||
55+
{ _cbm_test_runtime_error "cannot convert protected Windows root"; return 1; }
56+
parent="${root%/*}"
57+
fi
58+
else
59+
root="$(mktemp -d "${parent%/}/cbmrt.XXXXXX")" ||
60+
{ _cbm_test_runtime_error "cannot create private root under $parent"; return 1; }
61+
chmod 700 "$root" || {
62+
_cbm_test_runtime_discard_partial "$root" "$parent"
63+
_cbm_test_runtime_error "cannot protect runtime root $root"
64+
return 1
65+
}
66+
fi
67+
runtime_host="$root/runtime"
68+
cache_host="$root/cache"
69+
mkdir "$runtime_host" "$cache_host" || {
70+
_cbm_test_runtime_discard_partial "$root" "$parent"
71+
_cbm_test_runtime_error "cannot create runtime/cache directories"
72+
return 1
73+
}
74+
if (( ! windows )); then
75+
chmod 700 "$runtime_host" "$cache_host" || {
76+
_cbm_test_runtime_discard_partial "$root" "$parent"
77+
_cbm_test_runtime_error "cannot protect runtime/cache directories"
78+
return 1
79+
}
80+
product_runtime="$runtime_host"
81+
product_cache="$cache_host"
82+
else
83+
product_runtime="$(cygpath -m "$runtime_host")" || {
84+
_cbm_test_runtime_discard_partial "$root" "$parent"
85+
_cbm_test_runtime_error "cannot convert the runtime directory"
86+
return 1
87+
}
88+
product_cache="$(cygpath -m "$cache_host")" || {
89+
_cbm_test_runtime_discard_partial "$root" "$parent"
90+
_cbm_test_runtime_error "cannot convert the cache directory"
91+
return 1
92+
}
93+
fi
94+
CBM_TEST_RUNTIME_ROOT="$root"
95+
CBM_TEST_RUNTIME_DIR_HOST="$runtime_host"
96+
CBM_TEST_CACHE_DIR_HOST="$cache_host"
97+
_CBM_TEST_RUNTIME_CREATED_ROOT="$root"
98+
_CBM_TEST_RUNTIME_PRODUCT_RUNTIME="$product_runtime"
99+
_CBM_TEST_RUNTIME_PRODUCT_CACHE="$product_cache"
100+
export CBM_RUNTIME_DIR="$product_runtime" CBM_CACHE_DIR="$product_cache"
101+
}
102+
103+
_cbm_test_runtime_daemon() {
104+
CBM_RUNTIME_DIR="$_CBM_TEST_RUNTIME_PRODUCT_RUNTIME" \
105+
CBM_CACHE_DIR="$_CBM_TEST_RUNTIME_PRODUCT_CACHE" "$1" daemon "$2"
106+
}
107+
108+
cbm_test_runtime_cleanup() {
109+
local binary="${1:-}" root="$_CBM_TEST_RUNTIME_CREATED_ROOT"
110+
local name="${_CBM_TEST_RUNTIME_CREATED_ROOT##*/}" runtime_entry="" active=0
111+
[[ -n "$root" ]] || return 0
112+
case "$name" in
113+
cbmrt.?*|cbmrt-?*) ;;
114+
*) _cbm_test_runtime_error "refusing cleanup for unexpected basename: $name"; return 0 ;;
115+
esac
116+
if [[ "$root" != "$CBM_TEST_RUNTIME_ROOT" || -L "$root" ]]; then
117+
_cbm_test_runtime_error "refusing cleanup for an unexpected runtime root: $root"
118+
return 0
119+
fi
120+
if [[ ! -e "$root" ]]; then
121+
_CBM_TEST_RUNTIME_CREATED_ROOT=""
122+
return 0
123+
fi
124+
if [[ ! -d "$root" || "$CBM_TEST_RUNTIME_DIR_HOST" != "$root/runtime" ||
125+
"$CBM_TEST_CACHE_DIR_HOST" != "$root/cache" ||
126+
-L "$CBM_TEST_RUNTIME_DIR_HOST" || -L "$CBM_TEST_CACHE_DIR_HOST" ]]; then
127+
_cbm_test_runtime_error "refusing cleanup after runtime path replacement: $root"
128+
return 0
129+
fi
130+
if [[ -d "$CBM_TEST_RUNTIME_DIR_HOST" ]] &&
131+
! runtime_entry="$(find "$CBM_TEST_RUNTIME_DIR_HOST" -mindepth 1 -print -quit 2>/dev/null)"; then
132+
_cbm_test_runtime_error "cannot inspect runtime state; leaving $root"
133+
return 0
134+
fi
135+
if [[ -n "$runtime_entry" ]]; then
136+
[[ -x "$binary" ]] || {
137+
_cbm_test_runtime_error "cannot check the private daemon; leaving $root"
138+
return 0
139+
}
140+
if _cbm_test_runtime_daemon "$binary" status >/dev/null 2>&1; then
141+
active=1
142+
_cbm_test_runtime_daemon "$binary" stop >/dev/null 2>&1 || true
143+
for _attempt in {1..50}; do
144+
if ! _cbm_test_runtime_daemon "$binary" status >/dev/null 2>&1; then
145+
active=0
146+
break
147+
fi
148+
sleep 0.1
149+
done
150+
fi
151+
fi
152+
if (( active )); then
153+
_cbm_test_runtime_error "private daemon is still active; leaving $root"
154+
return 0
155+
fi
156+
if rm -rf -- "$root"; then
157+
_CBM_TEST_RUNTIME_CREATED_ROOT=""
158+
else
159+
_cbm_test_runtime_error "cleanup failed; leaving $root"
160+
fi
161+
return 0
162+
}

‎scripts/test.sh‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Usage: scripts/test.sh [--suites LIST] [--arch ARCH] [VAR=VAL ...]
1414
1515
The canonical test entry: identical in local CI, PR CI, dry run and release.
1616
DEFAULT (no --suites) is exactly what CI runs: static contract checks
17-
(Step 0a-0s), a CLEAN sanitizer build, every suite via the parallel harness,
17+
(Step 0a-0t), a CLEAN sanitizer build, every suite via the parallel harness,
1818
then the prod-binary regression guards (Steps 4-6).
1919
2020
Modes:
@@ -257,6 +257,9 @@ bash "$ROOT/tests/test_vt_candidate_selection_contract.sh"
257257
echo "=== Step 0s: release gate-chain ordering contract ==="
258258
bash "$ROOT/tests/test_release_gate_chain_contract.sh"
259259

260+
echo "=== Step 0t: test runtime isolation contract (#1691) ==="
261+
bash "$ROOT/tests/test_runtime_isolation_contract.sh"
262+
260263
# Verify compiler supports target arch
261264
verify_compiler "$CC"
262265

‎src/cli/cli.c‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12561,6 +12561,8 @@ static void cli_add_typed(yyjson_mut_doc *out, yyjson_mut_val *obj, const char *
1256112561
yyjson_mut_obj_add(obj, yyjson_mut_strcpy(out, key), vv);
1256212562
}
1256312563

12564+
static bool cli_stdin_allowed_for_schema(const char *schema_str);
12565+
1256412566
bool cbm_cli_args_from_stdin_allowed(const char *tool_name, bool stdin_is_tty) {
1256512567
/* An interactive run has nothing piped to read: consulting the terminal
1256612568
* would just sit waiting for the user to type JSON. Unchanged by #1359 —
@@ -12589,6 +12591,17 @@ bool cbm_cli_args_from_stdin_allowed(const char *tool_name, bool stdin_is_tty) {
1258912591
return false;
1259012592
}
1259112593

12594+
return cli_stdin_allowed_for_schema(schema_str);
12595+
}
12596+
12597+
/* Schema→decision core of the stdin gate, split out so the zero-argument
12598+
* branch stays TESTED even when no shipped tool is zero-argument anymore:
12599+
* #1181 gave list_projects pagination parameters, retiring the last
12600+
* empty-properties schema, and the #1359 regression tests had leaned on it
12601+
* as their live example. The product contract is unchanged — a schema that
12602+
* declares no properties means stdin has nothing to carry and the read is
12603+
* pure deadlock; the tests now exercise this path directly. */
12604+
static bool cli_stdin_allowed_for_schema(const char *schema_str) {
1259212605
yyjson_doc *schema_doc = yyjson_read(schema_str, strlen(schema_str), 0);
1259312606
if (!schema_doc) {
1259412607
/* The schemas are compile-time constants, so a parse failure means a
@@ -12604,6 +12617,12 @@ bool cbm_cli_args_from_stdin_allowed(const char *tool_name, bool stdin_is_tty) {
1260412617
return accepts_arguments;
1260512618
}
1260612619

12620+
#ifdef CBM_CLI_ENABLE_TEST_API
12621+
bool cbm_cli_stdin_allowed_for_schema_for_test(const char *schema_str) {
12622+
return cli_stdin_allowed_for_schema(schema_str);
12623+
}
12624+
#endif
12625+
1260712626
char *cbm_cli_build_args_json(const char *tool_name, int argc, char **argv, char **err_out) {
1260812627
if (err_out) {
1260912628
*err_out = NULL;

‎src/discover/discover.c‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,11 @@ static const char *file_skip_reason(const char *entry_name, const char *rel_path
689689
static CBMLanguage detect_file_language(const char *entry_name, const char *abs_path) {
690690
CBMLanguage lang = cbm_language_for_filename(entry_name);
691691
if (lang == CBM_LANG_COUNT) {
692-
return CBM_LANG_COUNT;
692+
/* Filename/extension detection failed: fall back to a conservative
693+
* shebang probe so extensionless scripts get indexed (#1199). Filename
694+
* detection stays authoritative — this runs only when it returns
695+
* unknown. */
696+
return cbm_language_from_shebang(abs_path);
693697
}
694698
/* Special: .m files need content-based disambiguation */
695699
const char *dot = strrchr(entry_name, '.');

‎src/discover/discover.h‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ CBMLanguage cbm_disambiguate_cls(const char *path);
5050
* On read failure, defaults to CBM_LANG_BITBAKE. */
5151
CBMLanguage cbm_disambiguate_inc(const char *path);
5252

53+
/* Detect a supported script language from a file's shebang (#!...) first line.
54+
* Conservative fallback used only when filename/extension detection is unknown
55+
* (see detect_file_language); it never overrides extension or special-filename
56+
* matches. Opens the file read-only and reads only a bounded first line.
57+
* Recognizes the interpreter *basename* (not the parent path):
58+
* python / python2 / python3 / dotted versions (python3.12) -> CBM_LANG_PYTHON
59+
* sh / bash / dash / ksh / zsh -> CBM_LANG_BASH
60+
* node / nodejs -> CBM_LANG_JAVASCRIPT
61+
* ruby -> RUBY, perl -> PERL, php -> PHP, lua -> LUA
62+
* Handles direct paths, "env <interp>", "env -S <interp> <args>", and CRLF.
63+
* Fails closed (returns CBM_LANG_COUNT) on read error, missing/malformed
64+
* shebang, an embedded NUL in the first line, or an unknown interpreter. */
65+
CBMLanguage cbm_language_from_shebang(const char *path);
66+
5367
/* ── Gitignore pattern matching ──────────────────────────────────── */
5468

5569
typedef struct cbm_gitignore cbm_gitignore_t;

0 commit comments

Comments
 (0)