Skip to content

Commit 07197ee

Browse files
authored
Merge pull request DeusData#1344 from DeusData/harden/av-margin-rc2
harden: remove capability that should never have shipped
2 parents f2e97ff + 958a511 commit 07197ee

16 files changed

Lines changed: 1160 additions & 241 deletions

File tree

‎Makefile.cbm‎

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,12 @@ GCC_ONLY_FLAGS := -Wno-format-truncation -Wno-unused-result \
4343
-Wno-stringop-truncation -Wno-alloc-size-larger-than
4444
endif
4545

46+
# -Wdate-time: with -Werror, any use of __DATE__/__TIME__/__TIMESTAMP__ fails the
47+
# build. Build-time entropy is how one identical source tree produced a new hash
48+
# on every compile (see the local patch in vendored/mimalloc/src/options.c) —
49+
# once removed, it must not creep back through a new call site.
4650
CFLAGS_COMMON = -std=c11 -D_DEFAULT_SOURCE -D_GNU_SOURCE -Wall -Wextra -Werror \
47-
-Wno-unused-parameter -Wno-sign-compare \
51+
-Wno-unused-parameter -Wno-sign-compare -Wdate-time \
4852
$(GCC_ONLY_FLAGS) \
4953
-Isrc -Ivendored -Ivendored/sqlite3 \
5054
-Ivendored/mimalloc/include \
@@ -54,19 +58,32 @@ CXXFLAGS_COMMON = -std=c++14 -Wall -Wextra -Werror \
5458
-Wno-unused-parameter \
5559
-I$(CBM_DIR) -I$(TS_INCLUDE)
5660

61+
# Test seams are OPT-IN, never opt-out. Some suites drive behaviour that only a
62+
# test should be able to ask for (fork an orphan the watchdog must reap, publish
63+
# a lease-ownership marker). That code has no production caller and reads exactly
64+
# like malware to a generic classifier, so it must not be in a shipped binary.
65+
# Opt-IN means the failure mode of forgetting the flag is a CLEAN binary rather
66+
# than a leaky one — the opposite choice would make every future release depend
67+
# on someone remembering. scripts/test.sh passes TEST_SEAMS=1 for the suites that
68+
# need it; the test-runner always has them.
69+
TEST_SEAM_DEFINE :=
70+
ifeq ($(TEST_SEAMS),1)
71+
TEST_SEAM_DEFINE := -DCBM_ENABLE_TEST_SEAMS=1
72+
endif
73+
5774
# Production flags (CFLAGS_EXTRA allows CI to inject -DCBM_VERSION)
5875
# CBM_BIND_TS_ALLOCATOR=1: bind the tree-sitter runtime to mimalloc (#424). Only
5976
# the prod build uses mimalloc (MI_OVERRIDE=1); the test build is CRT+ASan, where
6077
# binding would create an alloc/free mismatch, so the guard is prod-only.
61-
CFLAGS_PROD = $(CFLAGS_COMMON) -O2 -DCBM_BIND_TS_ALLOCATOR=1 $(CFLAGS_EXTRA)
78+
CFLAGS_PROD = $(CFLAGS_COMMON) -O2 -DCBM_BIND_TS_ALLOCATOR=1 $(TEST_SEAM_DEFINE) $(CFLAGS_EXTRA)
6279
CXXFLAGS_PROD = $(CXXFLAGS_COMMON) -O2
6380

6481
# Test flags: debug + sanitizers (override SANITIZE= to disable on Windows)
6582
SANITIZE = -fsanitize=address,undefined -fno-omit-frame-pointer
6683
EDITOR_TEST_DEFINES = -DCBM_JSON_LIKE_ENABLE_TEST_API=1 \
6784
-DCBM_TOML_EDIT_ENABLE_TEST_API=1 -DCBM_YAML_ENABLE_TEST_API=1 \
6885
-DCBM_TEXT_EDIT_ENABLE_TEST_API=1 -DCBM_CLI_ENABLE_TEST_API=1 \
69-
-DCBM_DIAGNOSTICS_ENABLE_TEST_API=1
86+
-DCBM_DIAGNOSTICS_ENABLE_TEST_API=1 -DCBM_ENABLE_TEST_SEAMS=1
7087
# The build system is the single source of truth for "is this binary
7188
# instrumented": compiler-specific probes (__SANITIZE_ADDRESS__) miss
7289
# clang's feature-check spelling and every non-ASan sanitizer, so the
@@ -120,22 +137,37 @@ ifeq ($(IS_LINUX),yes)
120137
MIMALLOC_WRAP_FLAGS_POSIX := $(foreach sym,$(MIMALLOC_WRAP_SYMS_POSIX),-Wl,--wrap=$(sym))
121138
endif
122139
ifeq ($(IS_MINGW),yes)
123-
WIN32_LIBS := -lws2_32 -lpsapi -lshell32 -ladvapi32 -Wl,--allow-multiple-definition -Wl,--stack,8388608 -static
140+
# --no-insert-timestamp: the PE header otherwise carries the link wall clock, so
141+
# two Windows builds of identical source are never byte-identical and every
142+
# release is a brand-new file to a reputation system. Same motivation as the
143+
# mimalloc __DATE__ patch; ELF and Mach-O have no equivalent field to clear.
144+
WIN32_LIBS := -lws2_32 -lpsapi -lshell32 -ladvapi32 -Wl,--allow-multiple-definition -Wl,--stack,8388608 -Wl,--no-insert-timestamp -static
124145
endif
125146

126147
# STATIC=1 produces a fully static binary (for Alpine/musl portable builds)
127148
ifeq ($(STATIC),1)
128149
STATIC_FLAGS := -static
129150
endif
130151

152+
# W^X: demand a non-executable stack on ELF. This is belt to the braces of the
153+
# .note.GNU-stack annotation in vendored/nomic/code_vectors_blob.S — that note
154+
# fixes the CAUSE (an unannotated object makes ld assume the worst for the whole
155+
# link), this flag fixes the OUTCOME, and the composition gate proves it on the
156+
# shipped artifact. ELF-only: Apple's ld rejects -z noexecstack outright and it
157+
# is meaningless for PE, so it is gated rather than made "common".
158+
ELF_HARDENING_FLAGS :=
159+
ifeq ($(IS_LINUX),yes)
160+
ELF_HARDENING_FLAGS := -Wl,-z,noexecstack
161+
endif
162+
131163
# The POSIX wrap shim exists only so the profiler can observe allocations. It
132164
# must never reach a sanitized build: the Linux/macOS test builds are CRT+ASan,
133165
# and redirecting malloc into mimalloc underneath ASan's own interception mixes
134166
# two allocators on the same pointers. Windows keeps its wrap flags everywhere,
135167
# because there the shim is what makes mimalloc own the allocations at all.
136-
LDFLAGS = -lm -lstdc++ -lpthread -lz $(WIN32_LIBS) $(STATIC_FLAGS) $(MIMALLOC_WRAP_FLAGS)
137-
LDFLAGS_TEST = -lm -lstdc++ -lpthread -lz $(SANITIZE) $(WIN32_LIBS) $(MIMALLOC_WRAP_FLAGS)
138-
LDFLAGS_TSAN = -lm -lstdc++ -lpthread -lz $(TSAN_SANITIZE) $(WIN32_LIBS) $(MIMALLOC_WRAP_FLAGS)
168+
LDFLAGS = -lm -lstdc++ -lpthread -lz $(WIN32_LIBS) $(STATIC_FLAGS) $(MIMALLOC_WRAP_FLAGS) $(ELF_HARDENING_FLAGS)
169+
LDFLAGS_TEST = -lm -lstdc++ -lpthread -lz $(SANITIZE) $(WIN32_LIBS) $(MIMALLOC_WRAP_FLAGS) $(ELF_HARDENING_FLAGS)
170+
LDFLAGS_TSAN = -lm -lstdc++ -lpthread -lz $(TSAN_SANITIZE) $(WIN32_LIBS) $(MIMALLOC_WRAP_FLAGS) $(ELF_HARDENING_FLAGS)
139171

140172
# ── Source files ─────────────────────────────────────────────────
141173

@@ -358,9 +390,20 @@ MIMALLOC_CFLAGS_TEST = -std=c11 -g -O1 -w \
358390
# sqlite3 (vendored amalgamation — compiled ourselves for ASan instrumentation)
359391
# SQLITE_ENABLE_FTS5: enables the FTS5 full-text search extension used by the
360392
# BM25 search path in search_graph (see nodes_fts virtual table in store.c).
393+
#
394+
# SQLITE_OMIT_LOAD_EXTENSION: no cbm code calls sqlite3_load_extension or
395+
# sqlite3_enable_load_extension, and we never want a graph DB to be able to pull
396+
# a shared library into the process. Loading is off by default at runtime, but
397+
# "disabled by default" is a setting while "not compiled in" is a property —
398+
# this compiles the machinery out, removing the API surface and part of the
399+
# dlopen/dlsym import surface with it. The composition gate asserts the symbols
400+
# are absent from every release artifact.
401+
SQLITE3_OMIT_FLAGS = -DSQLITE_OMIT_LOAD_EXTENSION
361402
SQLITE3_SRC = vendored/sqlite3/sqlite3.c
362-
SQLITE3_CFLAGS = -std=c11 -O2 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5
363-
SQLITE3_CFLAGS_TEST = -std=c11 -g -O1 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5
403+
SQLITE3_CFLAGS = -std=c11 -O2 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5 \
404+
$(SQLITE3_OMIT_FLAGS)
405+
SQLITE3_CFLAGS_TEST = -std=c11 -g -O1 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5 \
406+
$(SQLITE3_OMIT_FLAGS)
364407

365408
# TRE regex (vendored, Windows only — POSIX uses system <regex.h>)
366409
TRE_SRC = vendored/tre/tre_all.c
@@ -647,7 +690,14 @@ $(BUILD_DIR)/mimalloc.o: $(MIMALLOC_SRC) | $(BUILD_DIR)
647690
$(BUILD_DIR)/tsan_mimalloc.o: $(MIMALLOC_SRC) | $(BUILD_DIR)
648691
$(CC) $(MIMALLOC_CFLAGS_TEST) $(TSAN_SANITIZE) -c -o $@ $<
649692

650-
$(BUILD_DIR)/prod_mimalloc.o: $(MIMALLOC_SRC) | $(BUILD_DIR)
693+
# static.c is an AMALGAMATION: it #includes options.c and friends, none of which
694+
# make can see through the single named prerequisite, and the compile flags live
695+
# in this Makefile. Both gaps bit for real — the __DATE__ removal in options.c and
696+
# SQLITE_OMIT_LOAD_EXTENSION below each silently did nothing on an incremental
697+
# build because the object was considered up to date. Depend on the included
698+
# sources AND on this Makefile so a flag change rebuilds too.
699+
$(BUILD_DIR)/prod_mimalloc.o: $(MIMALLOC_SRC) $(wildcard vendored/mimalloc/src/*.c) \
700+
$(wildcard vendored/mimalloc/include/*.h) Makefile.cbm | $(BUILD_DIR)
651701
$(CC) $(MIMALLOC_CFLAGS) -c -o $@ $<
652702

653703
# sqlite3 object files (vendored amalgamation)
@@ -661,7 +711,7 @@ $(BUILD_DIR)/sqlite3.o: $(SQLITE3_SRC) | $(BUILD_DIR)
661711
$(BUILD_DIR)/tsan_sqlite3.o: $(SQLITE3_SRC) | $(BUILD_DIR)
662712
$(CC) $(SQLITE3_CFLAGS_TEST) $(TSAN_SANITIZE) -c -o $@ $<
663713

664-
$(BUILD_DIR)/prod_sqlite3.o: $(SQLITE3_SRC) | $(BUILD_DIR)
714+
$(BUILD_DIR)/prod_sqlite3.o: $(SQLITE3_SRC) Makefile.cbm | $(BUILD_DIR)
665715
$(CC) $(SQLITE3_CFLAGS) -c -o $@ $<
666716

667717
# TRE regex (only compiled on Windows — POSIX uses system <regex.h>)
@@ -835,7 +885,26 @@ OBJS_VENDORED_PROD = $(MIMALLOC_OBJ_PROD) $(SQLITE3_OBJ_PROD) $(TRE_OBJ_PROD) $(
835885

836886
MAIN_SRC = src/main.c
837887

838-
$(BUILD_DIR)/codebase-memory-mcp: $(MAIN_SRC) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_PROD) | $(BUILD_DIR)
888+
# Rebuild when the build CONFIGURATION changes, not only when sources do. The
889+
# product binary is compiled in ONE shot from sources, so after a TEST_SEAMS or
890+
# version flip make finds the binary newer than every source and skips the recipe
891+
# entirely, handing back a binary built with the PREVIOUS configuration. The
892+
# dangerous direction is a release build silently keeping test seams, which is a
893+
# property no reviewer can see by reading the diff. The stamp's CONTENT is the
894+
# configuration and it is rewritten only when that content changes, so ordinary
895+
# incremental builds are unaffected. (Single-quoted: the signature may contain
896+
# the escaped quotes of -DCBM_VERSION, which are literal inside '', but must not
897+
# contain a literal single quote.)
898+
BUILD_CONFIG_SIG := TEST_SEAMS=$(TEST_SEAMS)|CFLAGS_EXTRA=$(CFLAGS_EXTRA)
899+
.PHONY: build-config-check
900+
build-config-check:
901+
$(BUILD_DIR)/.build-config: build-config-check | $(BUILD_DIR)
902+
@printf '%s\n' '$(BUILD_CONFIG_SIG)' > $@.tmp
903+
@if cmp -s $@.tmp $@; then rm -f $@.tmp; \
904+
else mv -f $@.tmp $@; rm -f $(BUILD_DIR)/codebase-memory-mcp; \
905+
echo "=== build config changed -> full rebuild: $(BUILD_CONFIG_SIG) ==="; fi
906+
907+
$(BUILD_DIR)/codebase-memory-mcp: $(MAIN_SRC) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_PROD) $(BUILD_DIR)/.build-config | $(BUILD_DIR)
839908
$(CC) $(CFLAGS_PROD) -o $@ \
840909
$(MAIN_SRC) $(PROD_SRCS) \
841910
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \

‎README.md‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,23 +155,25 @@ Watcher registration is controlled separately by `auto_watch` (default `true`).
155155

156156
### Keeping Up to Date
157157

158-
**macOS / Linux:**
158+
**Updates run from the install script on every platform, not from inside the running binary.** `codebase-memory-mcp update` validates your flags and then prints the exact command to run:
159159

160160
```bash
161-
codebase-memory-mcp update
161+
# macOS / Linux
162+
bash "<install-dir>/install.sh"
162163
```
163164

164-
**Windows** — updates run from `install.ps1`, not from the running binary, because a running executable cannot replace its own image on Windows. `codebase-memory-mcp update` prints the exact command; it is:
165-
166165
```powershell
166+
# Windows
167167
powershell -ExecutionPolicy Bypass -File "<install-dir>\install.ps1"
168168
```
169169

170-
`install.ps1` is idempotent, so re-running it *is* the update: it stops the daemon, retires the running binary, installs the new one, and cleans up. `install.ps1` sits next to the binary in your install directory. If PowerShell refuses to run it because the file came from the internet, `Unblock-File` it first.
170+
The install script is placed next to the binary at install time, so the printed path resolves beside the executable. It is idempotent, so re-running it *is* the update: it stops the daemon, retires the running binary, installs the new one, and cleans up.
171171

172-
Installed through **npm or pip**? Update with your package manager on every platform (`npm install -g codebase-memory-mcp@latest` / `pip install -U codebase-memory-mcp`).
172+
Why it works this way. On Windows it is a hard requirement — a running executable cannot replace its own image, so the swap has to happen from a process that is not the binary being replaced. On macOS and Linux it is a deliberate choice: an in-process updater is structurally a downloader (fetch an archive, verify it, unpack it, mark a file executable, run it), and shipping that composite in every binary to serve a command most people run a handful of times is a poor trade. The release archives now carry no download URLs at all, and **cbm makes no network request of its own accord** — it does not check for new versions in the background, and nothing phones home. You find out about releases from the install script, your package manager, or GitHub.
173173

174-
The MCP server also checks for updates on startup and notifies on the first tool call if a newer release is available.
174+
If PowerShell refuses to run the script because the file came from the internet, `Unblock-File` it first.
175+
176+
Installed through **npm or pip**? Update with your package manager on every platform (`npm install -g codebase-memory-mcp@latest` / `pip install -U codebase-memory-mcp`).
175177

176178
### Uninstall
177179

@@ -181,6 +183,8 @@ codebase-memory-mcp uninstall
181183

182184
Removes owned agent config entries, skills, hooks, instructions, and the installed binary. Existing graph indexes are listed and deleted only after confirmation.
183185

186+
The install script placed beside the binary is **reported, not deleted** — uninstall prints its path and the `rm` command for it. It is left alone on purpose: it may be your own copy, a symlink into a checkout, or managed by a package manager, and an uninstaller should not delete a file it cannot prove it owns.
187+
184188
## Features
185189

186190
### Graph & analysis

0 commit comments

Comments
 (0)