Repository navigation
Expand file tree
/
Copy pathcheck-binary-composition.sh
More file actions
executable file
·568 lines (534 loc) · 26 KB
/
Copy pathcheck-binary-composition.sh
File metadata and controls
executable file
·568 lines (534 loc) · 26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
#!/usr/bin/env bash
# check-binary-composition.sh — release gate: assert what must NOT be inside a
# shipped artifact.
#
# Microsoft Defender's ML classifier flagged the v0.9.1-rc.1 binaries. The
# hardening pass that followed removed capability or opaque asset bytes — an
# executable stack, an in-process updater, test-only environment seams,
# embedded integration programs, and the in-image frontend bundle. Each of
# those regresses invisibly: one restored #include, one Makefile source-list
# edit, one revived call site, and nothing else in CI notices — the binary just
# quietly restores capability or opaque bytes that this hardening boundary is
# intended to exclude. Their removal reduces attack surface and makes release
# contents independently inspectable; it does not establish which feature, if
# any, caused an opaque third-party ML verdict.
#
# This script is the proof that each removal stayed removed. The needle scans
# assert NEGATIVE properties (needle absent), plus one canary string we know
# ships, because an absence check aimed at the wrong file — a compressed
# artifact, a stub, a truncated download — would otherwise pass vacuously and
# read green. The A1* checks assert structural properties of the produced ELF
# instead, for the mirror-image reason: a linker flag that was accepted is not
# evidence that the binary gained anything, so the mitigation is measured in
# the artifact. A missing tool is a hard error on both sides: a skipped
# assertion must never look like a satisfied one.
#
# Usage: scripts/ci/check-binary-composition.sh <binary-or-dir>...
# Directories are scanned recursively; format (ELF / Mach-O / PE) is detected
# per file from its magic bytes and each assertion runs where it is
# meaningful. Exit 0 = every assertion passed, 1 = at least one failed,
# 2 = usage error, missing tool, or nothing checkable was found (a vacuous
# run is a failure, not a pass).
# CBM_COMPOSITION_FIXTURE=1 declares the target a packaging FIXTURE (a stub
# compiled by a contract test, not a release link): A1d-bind-now is then
# reported n/a instead of asserted, because eager binding is a property of
# the release link flags, not of the packaging path the fixture exercises.
# Only contract tests set it; release derivation never does.
set -euo pipefail
case "${1:-}" in
-h | --help)
sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
esac
# ── Needles ─────────────────────────────────────────────────────────
# Every needle below was grep-verified against the tree it comes from: it
# exists ONLY in the code the assertion is about, so a hit means that code is
# linked in, not that some unrelated string happens to look similar.
# S6: worker/Windows test seams read these env vars. A release binary that
# still honours them lets any process on the box steer the indexer's child
# processes and file placement. Their absence is a release-security boundary;
# no claim is made about whether a third-party classifier weighs them.
SEAM_NEEDLES=(
'CBM_TEST_WORKER_DESCENDANT_PID_FILE'
'CBM_TEST_CRASH_ON'
'CBM_TEST_HANG_ON'
)
# The in-process updater combined network download with replacement of its own
# executable. That dual-use behavior is no longer needed in the daemon; updates
# now run from install.sh out-of-process. These four needles cover both halves
# of what was removed:
# the download base URL, the checksum URL built on top of it, the GitHub API
# release query of the daemon's background version check, and that request's
# Accept header (which survives even if the URL is ever assembled at runtime).
UPDATER_NEEDLES=(
'releases/latest/download'
'api.github.com/repos'
'releases/latest'
'Accept: application/vnd.github+json'
)
# SQLite loadable extensions = arbitrary code execution through a database
# file. The amalgamation is built with SQLITE_OMIT_LOAD_EXTENSION; if that ever
# drops out, dlopen()/LoadLibrary() re-enters the store layer.
# NOTE: the stripped release candidate can erase non-exported symbol names, so
# the two API names are not a sufficient proof on their own. The dlopen error
# text still works after stripping — it is compiled in only with the feature.
# When the unstripped candidate is inspected, the symbol checks are stronger.
SQLITE_LOADEXT_NEEDLES=(
'sqlite3_load_extension'
'sqlite3_enable_load_extension'
'unable to open shared library ['
)
# The standard artifact must not contain the UI's HTTP server at all: an
# unauthenticated localhost listener plus a process enumerator is unnecessary
# capability in the standard composition. Needles are split across
# both UI translation units and are independent of each other, so a refactor of
# any single one cannot silently disarm the assertion:
# 'HTTP/1.1 %d %s' httpd.c response-status writer
# 'Request Header Fields Too Large' httpd.c status-text table
# '/api/ui-config', '/api/processes' http_server.c route dispatch
# 'ps -eo …' / '[c]odebase-memory-mcp' the popen() process enumerator's
# shell pipeline, both halves
# Verified unique to src/ui/ across src/, vendored/ and internal/ — the only
# other occurrences in the repo are tests/test_httpd.c and scripts/, neither of
# which is ever linked into a release binary.
UI_HTTP_NEEDLES=(
'HTTP/1.1 %d %s'
'Request Header Fields Too Large'
'/api/ui-config'
'/api/processes'
'ps -eo pid,pcpu,rss,etime,comm'
'[c]odebase-memory-mcp'
)
# Canary: proves the needle scan can actually see this file's strings. Without
# it, handing the gate a gzip, a stub or a 0-byte file would pass every
# absence assertion. 168+ occurrences in a real artifact, 0 in anything else.
CANARY_NEEDLE='codebase-memory-mcp'
# ── Args ────────────────────────────────────────────────────────────
TARGETS=()
for arg in "$@"; do
case "$arg" in
-*)
echo "FAIL: unknown flag $arg (see --help)" >&2
exit 2
;;
*) TARGETS+=("$arg") ;;
esac
done
if [ "${#TARGETS[@]}" -eq 0 ]; then
echo "FAIL: no binaries or directories given (see --help)" >&2
exit 2
fi
# ── ELF program-header reader ───────────────────────────────────────
# Resolved lazily: a Windows/macOS-only run must not fail for want of readelf,
# but an ELF in scope with no reader available must fail loudly (exit 2) rather
# than skip the executable-stack assertion.
ELF_READER=''
ELF_READER_KIND=''
resolve_elf_reader() {
[ -n "$ELF_READER" ] && return 0
for cand in readelf llvm-readelf /opt/homebrew/opt/llvm/bin/llvm-readelf \
/usr/local/opt/llvm/bin/llvm-readelf; do
if command -v "$cand" >/dev/null 2>&1; then
ELF_READER="$cand"
ELF_READER_KIND=readelf
return 0
fi
done
for cand in objdump llvm-objdump /opt/homebrew/opt/llvm/bin/llvm-objdump; do
if command -v "$cand" >/dev/null 2>&1; then
ELF_READER="$cand"
ELF_READER_KIND=objdump
return 0
fi
done
return 1
}
# Echoes the GNU_STACK flag field ("RWE", "RW", "rwx", "rw-"), empty if the
# header is absent or unparseable.
exec_load_bytes() {
# Total size of PT_LOAD segments carrying the execute bit. With
# -z separate-code the executable segment holds only code; without it the
# linker merges .rodata in, so this number balloons to nearly the whole file.
case "$ELF_READER_KIND" in
readelf)
# NOT strtonum(): that is a gawk extension, and CI's awk is mawk, where
# it is undefined -- the sum would silently be 0 and this gate would
# pass vacuously on exactly the artifacts it exists to catch. Emit the
# hex MemSiz fields and convert in the shell.
total=0
for hex in $("$ELF_READER" -lW "$1" 2>/dev/null |
awk '/^ LOAD/ && $0 ~ /R E/ { print $6 }'); do
total=$((total + 16#${hex#0x}))
done
echo "$total"
;;
objdump)
echo unsupported
;;
esac
}
gnu_stack_flags() {
case "$ELF_READER_KIND" in
readelf)
"$ELF_READER" -lW "$1" 2>/dev/null |
awk '/GNU_STACK/ { print $(NF - 1); exit }'
;;
objdump)
# objdump -p prints " STACK off ..." and the flags on the next line.
"$ELF_READER" -p "$1" 2>/dev/null |
awk '/STACK off/ { getline
for (i = 1; i <= NF; i++)
if ($i == "flags") { print $(i + 1); exit } }'
;;
esac
}
# Echoes "<start-dec> <end-dec>" of the PT_GNU_RELRO window, empty if the
# segment is absent, "unsupported" if the resolved reader cannot report it.
# The hex→decimal conversion happens in the shell for the same reason
# exec_load_bytes does it there: strtonum() is a gawk extension and CI's awk is
# mawk, where it is undefined and the arithmetic would silently be 0.
relro_range() {
case "$ELF_READER_KIND" in
readelf)
"$ELF_READER" -lW "$1" 2>/dev/null |
awk '/GNU_RELRO/ { print $3, $6; exit }' |
while read -r vaddr memsz; do
start=$((16#${vaddr#0x}))
echo "$start $((start + 16#${memsz#0x}))"
done
;;
objdump)
echo unsupported
;;
esac
}
# Echoes "<name> <start-dec> <end-dec>" for every GOT section, one per line.
# The leading "[ 5]" index column is stripped BEFORE awk splits the line: its
# width changes with the section count, so field numbers would otherwise shift
# between binaries and the addresses would be read out of the wrong columns.
got_sections() {
"$ELF_READER" -SW "$1" 2>/dev/null |
sed -e 's/^[[:space:]]*\[[[:space:]]*[0-9]*\][[:space:]]*//' |
awk '$1 ~ /^\.got/ && $3 ~ /^[0-9a-fA-F]+$/ && $5 ~ /^[0-9a-fA-F]+$/ { print $1, $3, $5 }' |
while read -r name addr size; do
start=$((16#$addr))
echo "$name $start $((start + 16#$size))"
done
}
# static | now | lazy — how the binary binds at load time.
# Both tests are awk, not `grep -q`: grep exits on its first match, the reader
# upstream takes EPIPE, and under `set -o pipefail` the satisfied case would be
# reported as the failing one. awk consumes its whole input and cannot do that.
bind_now_state() {
if ! "$ELF_READER" -lW "$1" 2>/dev/null |
awk '$1 == "DYNAMIC" { found = 1 } END { exit !found }'; then
echo static
return 0
fi
if "$ELF_READER" -dW "$1" 2>/dev/null |
awk '/\(BIND_NOW\)/ { found = 1 }
/\(FLAGS\)/ && /BIND_NOW/ { found = 1 }
/\(FLAGS_1\)/ && / NOW([ ]|$)/ { found = 1 }
END { exit !found }'; then
echo now
else
echo lazy
fi
}
# ── Reporting ───────────────────────────────────────────────────────
# PASS and FAIL both go to stdout so the per-assertion sequence stays in order
# in a CI log (stderr would interleave nondeterministically); only the final
# verdict is echoed to stderr, which is what a failed step's tail shows.
pass_count=0
fail_count=0
FAILURES=()
report() { # verdict, assertion-id, file-token, message
printf '%-4s %-22s %s: %s\n' "$1" "$2" "$3" "$4"
if [ "$1" = FAIL ]; then
fail_count=$((fail_count + 1))
FAILURES+=("$2 $3: $4")
else
pass_count=$((pass_count + 1))
fi
}
assert_absent() { # file, token, assertion-id, needle
if LC_ALL=C grep -a -q -F -e "$4" "$1"; then
report FAIL "$3" "$2" "'$4' is PRESENT (must not ship)"
else
report PASS "$3" "$2" "'$4' absent"
fi
}
assert_present() { # file, token, assertion-id, needle, why-pass, why-fail
if LC_ALL=C grep -a -q -F -e "$4" "$1"; then
report PASS "$3" "$2" "'$4' present ($5)"
else
report FAIL "$3" "$2" "'$4' is MISSING — $6"
fi
}
# ── Per-file checks ─────────────────────────────────────────────────
detect_format() { # file → elf | macho | pe | other
magic=$(LC_ALL=C od -An -N4 -tx1 "$1" 2>/dev/null | tr -d ' \n')
case "$magic" in
7f454c46) echo elf ;;
cffaedfe | cefaedfe | feedface | feedfacf | cafebabe | bebafeca) echo macho ;;
4d5a*) echo pe ;;
*) echo other ;;
esac
}
checked_files=0
skipped_files=0
check_file() {
file="$1"
# Two path components: the binary is literally named
# "codebase-memory-mcp", so the parent directory disambiguates the log.
token=$(printf '%s' "$file" | awk -F/ '{ if (NF > 1) print $(NF - 1) "/" $NF; else print $NF }')
fmt=$(detect_format "$file")
if [ "$fmt" = other ]; then
printf 'skip %-22s %s: not an ELF/Mach-O/PE binary\n' '-' "$token"
skipped_files=$((skipped_files + 1))
return 0
fi
printf '\n── %s [%s] ──\n' "$token" "$fmt"
checked_files=$((checked_files + 1))
# A0 — anti-vacuity canary; every assertion below is an absence check.
assert_present "$file" "$token" A0-canary "$CANARY_NEEDLE" \
'the needle scan can read this file' \
'this is not one of our artifacts, or its strings are unreadable (packed/compressed/truncated) — every absence assertion below would pass vacuously'
# A1 — executable stack (ELF only). Every Linux artifact of v0.9.1-rc.1
# shipped GNU_STACK RWE. A writable+executable stack is unnecessary here
# and weakens exploit mitigations, independently of any scanner verdict.
if [ "$fmt" = elf ]; then
if ! resolve_elf_reader; then
echo "FAIL: no readelf/llvm-readelf/objdump available; cannot assert" \
"the non-executable stack for $token — refusing to skip it" >&2
exit 2
fi
flags=$(gnu_stack_flags "$file")
# readelf spells the flags "RWE"/"RW", objdump "rwx"/"rw-" — match both.
case "$flags" in
'')
report FAIL A1-noexec-stack "$token" \
"no GNU_STACK program header found ($ELF_READER) — without PT_GNU_STACK the loader may fall back to READ_IMPLIES_EXEC"
;;
*E* | *X* | *e* | *x*)
report FAIL A1-noexec-stack "$token" \
"GNU_STACK is $flags — the stack is EXECUTABLE (link with -z noexecstack; check .S/asm objects for a missing .note.GNU-stack)"
;;
*)
report PASS A1-noexec-stack "$token" "GNU_STACK is $flags (no execute bit)"
;;
esac
else
printf 'n/a %-22s %s: executable-stack check is ELF-only\n' A1-noexec-stack "$token"
fi
# A1b — read-only DATA must not live in the executable mapping. GNU ld
# enables -z separate-code by default on x86-64 but NOT on aarch64, so the
# arm64 binaries shipped ONE R E PT_LOAD spanning the whole image: 259 MB of
# tree-sitter parse tables mapped executable while amd64 mapped the same
# bytes R only. Section flags said A, not AX -- the kernel applies SEGMENT
# permissions, so section flags were never the control. Heuristic on
# purpose: if executable segments cover most of the file, .rodata is in them.
if [ "$fmt" = elf ]; then
exec_bytes=$(exec_load_bytes "$file")
file_bytes=$(wc -c < "$file" | tr -d ' ')
if [ "$exec_bytes" = unsupported ]; then
printf 'n/a %-22s %s: %s cannot report segment sizes\n' \
A1b-rodata-noexec "$token" "$ELF_READER_KIND"
elif [ "${exec_bytes:-0}" -gt 0 ] && [ "$file_bytes" -gt 0 ] &&
[ $((exec_bytes * 100 / file_bytes)) -gt 60 ]; then
report FAIL A1b-rodata-noexec "$token" \
"executable PT_LOAD segments cover $((exec_bytes * 100 / file_bytes))% of the file ($exec_bytes/$file_bytes bytes) — read-only data is mapped executable (link with -z separate-code)"
else
report PASS A1b-rodata-noexec "$token" \
"executable PT_LOAD segments cover $((exec_bytes * 100 / file_bytes))% of the file (read-only data is outside them)"
fi
else
printf 'n/a %-22s %s: segment-permission check is ELF-only\n' A1b-rodata-noexec "$token"
fi
# A1c — RELRO. PT_GNU_RELRO is the window the loader re-maps read-only once
# startup relocation is done. Without it .init_array, .fini_array,
# .data.rel.ro and the GOT stay writable for the whole process lifetime,
# which is what turns a stray write into control-flow hijack. ELF-only:
# Mach-O and PE have no equivalent segment.
relro_window=''
if [ "$fmt" = elf ]; then
relro_window=$(relro_range "$file")
if [ "$relro_window" = unsupported ]; then
printf 'n/a %-22s %s: %s cannot report segment addresses\n' \
A1c-relro "$token" "$ELF_READER_KIND"
elif [ -z "$relro_window" ]; then
report FAIL A1c-relro "$token" \
"no PT_GNU_RELRO program header — .data.rel.ro and the GOT stay writable for the process lifetime (link with -z relro)"
else
report PASS A1c-relro "$token" \
"PT_GNU_RELRO covers $((${relro_window##* } - ${relro_window%% *})) bytes"
fi
else
printf 'n/a %-22s %s: RELRO is a GNU/ELF segment\n' A1c-relro "$token"
fi
# A1d — eager binding, asserted on the OUTCOME instead of on the flag.
# -z now is what folds .got.plt into the RELRO window, but "the linker
# accepted -z now" proves nothing about the artifact, so what is measured
# here is the property itself: no GOT slot may be writable after startup,
# i.e. every .got* section must lie inside A1c's window.
#
# That is not a formality on the shipped artifact. The Linux release
# binaries are linked -static, and a static link on Ubuntu 24.04 / ld 2.42
# emits PT_GNU_RELRO yet places .got.plt immediately PAST its end: in our
# own release-shape binary .got.plt ran 0x11d9ffe8+0x40 against a window
# ending at 0x11DA0000, so 40 bytes of GOT stayed writable for the process
# lifetime. This assertion fails on that binary and passes on the one built
# with -z now, which is the only reason to believe it measures anything.
#
# A binary with a PT_DYNAMIC is additionally required to carry
# BIND_NOW/FLAGS_1 NOW, because there RELRO alone cannot help: lazy binding
# writes the GOT after the loader has already re-protected it. A static
# binary has no PT_DYNAMIC and nothing to bind at runtime, so GOT coverage
# is the whole property there — a distinct reported outcome, never a skip.
#
# Scope: this is a property of the RELEASE link (-z now in Makefile.cbm).
# Contract tests hand the gate a stub compiled straight from a .c file to
# exercise packaging, format detection and the needle scans; a stub is not
# linked with -z now and never ships, so asserting eager binding on it
# tests the fixture's compiler defaults, not the artifact. Such a caller
# declares itself with CBM_COMPOSITION_FIXTURE=1 and A1d is reported n/a —
# a named exemption, printed on every run, never a silent skip. Every
# other assertion still runs on the fixture unchanged.
if [ "${CBM_COMPOSITION_FIXTURE:-0}" = 1 ]; then
printf 'n/a %-22s %s: packaging fixture (CBM_COMPOSITION_FIXTURE=1) — eager binding is asserted on release links only\n' \
A1d-bind-now "$token"
elif [ "$fmt" != elf ]; then
printf 'n/a %-22s %s: BIND_NOW is an ELF dynamic-section property\n' \
A1d-bind-now "$token"
elif [ "$relro_window" = unsupported ]; then
printf 'n/a %-22s %s: %s cannot report section addresses\n' \
A1d-bind-now "$token" "$ELF_READER_KIND"
elif [ -z "$relro_window" ]; then
report FAIL A1d-bind-now "$token" \
"no PT_GNU_RELRO, so no GOT section can be read-only after relocation (see A1c)"
else
relro_start=${relro_window%% *}
relro_end=${relro_window##* }
got_seen=0
got_writable=''
while read -r got_name got_start got_end; do
[ -z "$got_name" ] && continue
got_seen=$((got_seen + 1))
if [ "$got_start" -lt "$relro_start" ] || [ "$got_end" -gt "$relro_end" ]; then
got_writable="$got_writable $got_name"
fi
done <<EOF
$(got_sections "$file")
EOF
bind_state=$(bind_now_state "$file")
if [ "$got_seen" -eq 0 ]; then
# Same anti-vacuity rule as A0: with no GOT section to place, the
# coverage test above is trivially satisfied and would read green.
report FAIL A1d-bind-now "$token" \
"no .got* section found ($ELF_READER) — the coverage test has nothing to check and would pass vacuously; section headers may have been removed"
elif [ -n "$got_writable" ]; then
report FAIL A1d-bind-now "$token" \
"GOT section(s)$got_writable fall outside PT_GNU_RELRO [$relro_start,$relro_end) — they stay WRITABLE after relocation (link with -z now)"
elif [ "$bind_state" = lazy ]; then
report FAIL A1d-bind-now "$token" \
"dynamic binary with neither BIND_NOW nor FLAGS_1 NOW — the GOT is filled lazily, after the loader has already applied RELRO (link with -z now)"
elif [ "$bind_state" = static ]; then
report PASS A1d-bind-now "$token" \
"$got_seen GOT section(s) inside PT_GNU_RELRO; no PT_DYNAMIC, so nothing binds at runtime"
else
report PASS A1d-bind-now "$token" \
"$got_seen GOT section(s) inside PT_GNU_RELRO and the dynamic section requests BIND_NOW"
fi
fi
# A2 — test-only seams.
for needle in "${SEAM_NEEDLES[@]}"; do
assert_absent "$file" "$token" A2-no-test-seams "$needle"
done
# Sweep for seams nobody thought to pin: a new CBM_TEST_* env var added to
# production code lands here on its first release, not on the next audit.
# One narrowly validated seam remains in Windows release artifacts by
# decision: WINDOWS_USER_PATH_RUN_ID redirects the artifact smoke away from
# the tester's actual user PATH. Crash/hang injectors are test-build-only and
# are explicitly forbidden above. Anything else is novel and fails.
seam_allowed='CBM_TEST_WINDOWS_USER_PATH_RUN_ID'
seam_unexpected=''
for found in $(LC_ALL=C grep -a -o -E 'CBM_TEST_[A-Za-z0-9_]+' "$file" 2>/dev/null |
sort -u || true); do
case " $seam_allowed " in
*" $found "*) ;;
*) seam_unexpected="$seam_unexpected $found" ;;
esac
done
if [ -n "$seam_unexpected" ]; then
report FAIL A2-no-test-seams "$token" \
"unexpected CBM_TEST_* seam(s):$seam_unexpected (allowlist: $seam_allowed)"
else
report PASS A2-no-test-seams "$token" 'no CBM_TEST_* seams beyond the smoke allowlist'
fi
# A3 — updater/release URLs.
for needle in "${UPDATER_NEEDLES[@]}"; do
assert_absent "$file" "$token" A3-no-updater-urls "$needle"
done
# A4 — SQLite loadable-extension surface.
for needle in "${SQLITE_LOADEXT_NEEDLES[@]}"; do
assert_absent "$file" "$token" A4-no-sqlite-loadext "$needle"
done
# Positive control for A4: the symbol needles can be absent on a stripped
# candidate even when the feature is compiled in, so that candidate's
# absence assertion can pass vacuously. SQLite's own compile-option table names
# every OMIT_* it was built with — when that table is present (it ships
# unless SQLITE_OMIT_COMPILEOPTION_DIAGS is set) it proves the omission
# positively rather than by absence.
if LC_ALL=C grep -a -q -F -e 'sqlite_compileoption_get' "$file"; then
assert_present "$file" "$token" A4-no-sqlite-loadext 'OMIT_LOAD_EXTENSION' \
'sqlite reports SQLITE_OMIT_LOAD_EXTENSION' \
'sqlite was NOT built with SQLITE_OMIT_LOAD_EXTENSION, so the dlopen()/LoadLibrary() extension loader is linked in'
else
printf 'n/a %-22s %s: %s\n' A4-no-sqlite-loadext "$token" \
"sqlite compile-option table absent; A4 rests on absence needles only"
fi
# A5 — UI/HTTP subsystem. One composition ships and it serves the graph UI
# from embedded assets, so the HTTP server belongs here by construction.
# CBM_CHECK_UI_ABSENT=1 still enforces absence for any future headless build.
if [ "${CBM_CHECK_UI_ABSENT:-0}" = "1" ]; then
for needle in "${UI_HTTP_NEEDLES[@]}"; do
assert_absent "$file" "$token" A5-no-ui-http "$needle"
done
else
printf 'n/a %-22s %s: UI-capable artifact, HTTP server ships here by design\n' \
A5-no-ui-http "$token"
fi
}
# ── Walk the targets ────────────────────────────────────────────────
for target in "${TARGETS[@]}"; do
if [ -d "$target" ]; then
# "! -type d" rather than "-type f": a symlinked or otherwise unusual
# artifact must produce a visible skip line, never vanish from the run.
# sort keeps the report order stable across platforms (find order is not).
while IFS= read -r f; do
[ -n "$f" ] && check_file "$f"
done <<EOF
$(find "$target" ! -type d | LC_ALL=C sort)
EOF
elif [ -f "$target" ]; then
check_file "$target"
else
echo "FAIL: $target is neither a file nor a directory" >&2
exit 2
fi
done
printf '\n'
if [ "$checked_files" -eq 0 ]; then
echo "FAIL: no ELF/Mach-O/PE binary found in the given targets ($skipped_files file(s) skipped)" >&2
echo " a gate that checked nothing is not a green gate" >&2
exit 2
fi
if [ "$fail_count" -ne 0 ]; then
echo "BINARY COMPOSITION GATE FAILED: $fail_count assertion(s) over $checked_files binary/binaries" >&2
for f in "${FAILURES[@]}"; do
echo " - $f" >&2
done
exit 1
fi
echo "BINARY COMPOSITION OK: $pass_count assertion(s) passed over $checked_files binary/binaries"