From 7a0371ffd31935a0cfe348c73e167cb88cc0e70d Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Thu, 16 Jul 2026 04:16:51 +0100 Subject: [PATCH 01/13] build: add optional hardened production flags Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- Makefile.cbm | 118 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 101 insertions(+), 17 deletions(-) diff --git a/Makefile.cbm b/Makefile.cbm index 657f4aacc..cf191acf2 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -58,8 +58,8 @@ CXXFLAGS_COMMON = -std=c++14 -Wall -Wextra -Werror \ # CBM_BIND_TS_ALLOCATOR=1: bind the tree-sitter runtime to mimalloc (#424). Only # the prod build uses mimalloc (MI_OVERRIDE=1); the test build is CRT+ASan, where # binding would create an alloc/free mismatch, so the guard is prod-only. -CFLAGS_PROD = $(CFLAGS_COMMON) -O2 -DCBM_BIND_TS_ALLOCATOR=1 $(CFLAGS_EXTRA) -CXXFLAGS_PROD = $(CXXFLAGS_COMMON) -O2 +CFLAGS_PROD = $(CFLAGS_COMMON) $(HARDEN_CFLAGS) -O2 -DCBM_BIND_TS_ALLOCATOR=1 $(CFLAGS_EXTRA) +CXXFLAGS_PROD = $(CXXFLAGS_COMMON) $(HARDEN_CFLAGS) -O2 # Test flags: debug + sanitizers (override SANITIZE= to disable on Windows) SANITIZE = -fsanitize=address,undefined -fno-omit-frame-pointer @@ -109,38 +109,121 @@ MIMALLOC_WRAP_FLAGS := ifeq ($(IS_MINGW),yes) MIMALLOC_WRAP_FLAGS := $(foreach sym,$(MIMALLOC_WRAP_SYMS),-Wl,--wrap=$(sym)) endif -# Linux wraps a smaller set for MEASUREMENT only (see mem_override_posix.c): +# Linux and FreeBSD wrap a smaller set for MEASUREMENT only (see mem_override_posix.c): # allocations already reach mimalloc here, but the profiler needs the same # observation point as Windows or the platform comparison is not like-for-like. # macOS ld has no --wrap, so it stays census-only. -IS_LINUX := $(shell uname -s 2>/dev/null | grep -q Linux && echo yes || echo no) +IS_POSIX_WRAP := $(shell uname -s 2>/dev/null | grep -qE 'Linux|FreeBSD' && echo yes || echo no) MIMALLOC_WRAP_SYMS_POSIX := malloc calloc realloc free strdup MIMALLOC_WRAP_FLAGS_POSIX := -ifeq ($(IS_LINUX),yes) +ifeq ($(IS_POSIX_WRAP),yes) MIMALLOC_WRAP_FLAGS_POSIX := $(foreach sym,$(MIMALLOC_WRAP_SYMS_POSIX),-Wl,--wrap=$(sym)) endif ifeq ($(IS_MINGW),yes) WIN32_LIBS := -lws2_32 -lpsapi -lshell32 -ladvapi32 -Wl,--allow-multiple-definition -Wl,--stack,8388608 -static endif +IS_FREEBSD := $(shell uname -s 2>/dev/null | grep -q 'FreeBSD' && echo yes || echo no) +EXECINFO_LIBS := +ifeq ($(IS_FREEBSD),yes) +EXECINFO_LIBS := -lexecinfo +endif # STATIC=1 produces a fully static binary (for Alpine/musl portable builds) ifeq ($(STATIC),1) STATIC_FLAGS := -static endif +# Production hardening flags. +# +# Enabled by default for ELF production builds. +# macOS Mach-O and Windows PE builds are intentionally unchanged. +# +# Downstream package maintainers can disable hardening with: +# +# make PRODUCTION_HARDENING=0 +# +# Individual compiler and linker capabilities are detected empirically +# using the active toolchain. Unsupported flags are skipped. + +PRODUCTION_HARDENING ?= 1 + +HARDEN_CFLAGS := +HARDEN_CXXFLAGS := +HARDEN_LDFLAGS := + +ifeq ($(PRODUCTION_HARDENING),1) + +# Detect ELF target output purely via compiler preprocessor macros. +ELF_BUILD := $(shell echo | $(CC) -dM -E -x c - 2>/dev/null | grep -q '__ELF__' && echo yes || echo no) + +ifeq ($(ELF_BUILD),yes) + +comma := , + +# Unified probe macro to detect compiler and linker flags empirically. +probe_flag = $(shell \ + echo 'int main(void){return 0;}' | \ + $(1) -Werror -O2 $(if $(filter compile,$(3)),-c) $(4) -x $(2) - -o /dev/null \ + 2>/dev/null && echo yes || echo no) + +# Candidate compile flags +C_HARDEN_CANDIDATES := -fstack-protector-strong -D_FORTIFY_SOURCE=2 +CXX_HARDEN_CANDIDATES := -fstack-protector-strong -D_FORTIFY_SOURCE=2 + +# Candidate link flags (using $(comma) to prevent GNU Make argument splitting) +LINK_RELRO_FLAG := -Wl$(comma)-z$(comma)relro +LINK_NOW_FLAG := -Wl$(comma)-z$(comma)now +LINK_NOEXEC_FLAG:= -Wl$(comma)-z$(comma)noexecstack + +# Probe C compiler flags +$(foreach flag,$(C_HARDEN_CANDIDATES),\ + $(if $(filter yes,$(call probe_flag,$(CC),c,compile,$(flag))),\ + $(eval HARDEN_CFLAGS += $(flag)))) + +# Probe C++ compiler flags +$(foreach flag,$(CXX_HARDEN_CANDIDATES),\ + $(if $(filter yes,$(call probe_flag,$(CXX),c++,compile,$(flag))),\ + $(eval HARDEN_CXXFLAGS += $(flag)))) + +# Probe PIE support (requires compiler and linker co-verification) +ifeq ($(call probe_flag,$(CC),c,compile,-fPIE),yes) +ifeq ($(call probe_flag,$(CC),c,link,-fPIE -pie),yes) +HARDEN_CFLAGS += -fPIE +HARDEN_CXXFLAGS += -fPIE +HARDEN_LDFLAGS += -pie +endif +endif + +# Probe Linker hardening flags +ifeq ($(call probe_flag,$(CC),c,link,$(LINK_RELRO_FLAG)),yes) +HARDEN_LDFLAGS += $(LINK_RELRO_FLAG) +endif + +ifeq ($(call probe_flag,$(CC),c,link,$(LINK_NOW_FLAG)),yes) +HARDEN_LDFLAGS += $(LINK_NOW_FLAG) +endif + +ifeq ($(call probe_flag,$(CC),c,link,$(LINK_NOEXEC_FLAG)),yes) +HARDEN_LDFLAGS += $(LINK_NOEXEC_FLAG) +endif + +endif # ELF_BUILD +endif # PRODUCTION_HARDENING + # The POSIX wrap shim exists only so the profiler can observe allocations. It -# must never reach a sanitized build: the Linux/macOS test builds are CRT+ASan, +# must never reach a sanitized build: the Linux/macOS/FreeBSD test builds are CRT+ASan, # and redirecting malloc into mimalloc underneath ASan's own interception mixes # two allocators on the same pointers. Windows keeps its wrap flags everywhere, # because there the shim is what makes mimalloc own the allocations at all. -LDFLAGS = -lm -lstdc++ -lpthread -lz $(WIN32_LIBS) $(STATIC_FLAGS) $(MIMALLOC_WRAP_FLAGS) -LDFLAGS_TEST = -lm -lstdc++ -lpthread -lz $(SANITIZE) $(WIN32_LIBS) $(MIMALLOC_WRAP_FLAGS) -LDFLAGS_TSAN = -lm -lstdc++ -lpthread -lz $(TSAN_SANITIZE) $(WIN32_LIBS) $(MIMALLOC_WRAP_FLAGS) +LDFLAGS = $(HARDEN_LDFLAGS) -lm -lstdc++ -lpthread -lz $(WIN32_LIBS) $(EXECINFO_LIBS) $(STATIC_FLAGS) $(MIMALLOC_WRAP_FLAGS) $(MIMALLOC_WRAP_FLAGS_POSIX) +LDFLAGS_TEST = -lm -lstdc++ -lpthread -lz $(EXECINFO_LIBS) $(SANITIZE) $(WIN32_LIBS) $(MIMALLOC_WRAP_FLAGS) +LDFLAGS_TSAN = -lm -lstdc++ -lpthread -lz $(EXECINFO_LIBS) $(TSAN_SANITIZE) $(WIN32_LIBS) $(MIMALLOC_WRAP_FLAGS) # ── Source files ───────────────────────────────────────────────── FOUNDATION_SRCS = \ src/foundation/mem_override_win.c \ + src/foundation/mem_override_posix.c \ src/foundation/arena.c \ src/foundation/hash_table.c \ src/foundation/str_intern.c \ @@ -349,7 +432,8 @@ MIMALLOC_CFLAGS = -std=c11 -O2 -w \ -Ivendored/mimalloc/include \ -Ivendored/mimalloc/src \ -DMI_OVERRIDE=1 \ - $(MIMALLOC_OVERRIDE_DEFINE) + $(MIMALLOC_OVERRIDE_DEFINE) \ + $(HARDEN_CFLAGS) MIMALLOC_CFLAGS_TEST = -std=c11 -g -O1 -w \ -Ivendored/mimalloc/include \ -Ivendored/mimalloc/src \ @@ -359,12 +443,12 @@ MIMALLOC_CFLAGS_TEST = -std=c11 -g -O1 -w \ # SQLITE_ENABLE_FTS5: enables the FTS5 full-text search extension used by the # BM25 search path in search_graph (see nodes_fts virtual table in store.c). SQLITE3_SRC = vendored/sqlite3/sqlite3.c -SQLITE3_CFLAGS = -std=c11 -O2 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5 +SQLITE3_CFLAGS = -std=c11 -O2 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5 $(HARDEN_CFLAGS) SQLITE3_CFLAGS_TEST = -std=c11 -g -O1 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5 # TRE regex (vendored, Windows only — POSIX uses system ) TRE_SRC = vendored/tre/tre_all.c -TRE_CFLAGS = -std=c11 -g -O1 -w -Ivendored/tre +TRE_CFLAGS = -std=c11 -g -O1 -w -Ivendored/tre $(HARDEN_CFLAGS) # yyjson (vendored) YYJSON_SRC = vendored/yyjson/yyjson.c @@ -577,7 +661,7 @@ BUILD_DIR = build/c # ── Object file compilation (grammars need relaxed warnings) ───── # Grammar + tree-sitter runtime: compiled without -Werror (upstream code has warnings) -GRAMMAR_CFLAGS = -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) +GRAMMAR_CFLAGS = -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) $(HARDEN_CFLAGS) GRAMMAR_CFLAGS_TEST = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \ $(SANITIZE) GRAMMAR_CFLAGS_TSAN = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \ @@ -706,7 +790,7 @@ $(BUILD_DIR)/tsan_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c | $(BUILD_DIR) # nomic-embed-code pretrained vector blob UNIXCODER_OBJ = $(BUILD_DIR)/unixcoder_blob.o $(UNIXCODER_OBJ): $(UNIXCODER_BLOB_SRC) vendored/nomic/code_vectors.bin | $(BUILD_DIR) - $(CC) -c -o $@ $< + $(CC) $(HARDEN_CFLAGS) -c -o $@ $< OBJS_VENDORED_TEST = $(MIMALLOC_OBJ_TEST) $(SQLITE3_OBJ_TEST) $(TRE_OBJ_TEST) $(GRAMMAR_OBJS_TEST) $(TS_RUNTIME_OBJ_TEST) $(LSP_OBJ_TEST) $(PP_OBJ_TEST) $(LZ4_OBJ_TEST) $(ZSTD_OBJ_TEST) $(UNIXCODER_OBJ) OBJS_VENDORED_TSAN = $(MIMALLOC_OBJ_TSAN) $(SQLITE3_OBJ_TSAN) $(TRE_OBJ_TSAN) $(GRAMMAR_OBJS_TSAN) $(TS_RUNTIME_OBJ_TSAN) $(LSP_OBJ_TSAN) $(PP_OBJ_TSAN) $(LZ4_OBJ_TSAN) $(ZSTD_OBJ_TSAN) $(UNIXCODER_OBJ) @@ -822,14 +906,14 @@ $(BUILD_DIR)/prod_preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR) # Vendored LZ4 (compiled separately, not unity-built via lz4_store.c) LZ4_OBJ_PROD = $(BUILD_DIR)/prod_lz4.o $(BUILD_DIR)/prod_lz4hc.o $(BUILD_DIR)/prod_lz4.o: $(CBM_DIR)/vendored/lz4/lz4.c | $(BUILD_DIR) - $(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR) -c -o $@ $< + $(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR) $(HARDEN_CFLAGS) -c -o $@ $< $(BUILD_DIR)/prod_lz4hc.o: $(CBM_DIR)/vendored/lz4/lz4hc.c | $(BUILD_DIR) - $(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR)/vendored/lz4 -c -o $@ $< + $(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR)/vendored/lz4 $(HARDEN_CFLAGS) -c -o $@ $< # Vendored zstd (compiled separately, not unity-built via zstd_store.c) ZSTD_OBJ_PROD = $(BUILD_DIR)/prod_zstd.o $(BUILD_DIR)/prod_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c | $(BUILD_DIR) - $(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR)/vendored/zstd -c -o $@ $< + $(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR)/vendored/zstd $(HARDEN_CFLAGS) -c -o $@ $< OBJS_VENDORED_PROD = $(MIMALLOC_OBJ_PROD) $(SQLITE3_OBJ_PROD) $(TRE_OBJ_PROD) $(GRAMMAR_OBJS_PROD) $(TS_RUNTIME_OBJ_PROD) $(LSP_OBJ_PROD) $(PP_OBJ_PROD) $(LZ4_OBJ_PROD) $(ZSTD_OBJ_PROD) $(UNIXCODER_OBJ) From c30ff2951fb39207544eb6aeb39a2ba692315374 Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:12:05 +0100 Subject: [PATCH 02/13] build: implement capability-based ELF production hardening Enable production hardening by default for ELF targets (Linux and BSDs), using empirical compiler and linker capability probing instead of OS or toolchain assumptions. Key changes: - Detect ELF target format via compiler preprocessor (__ELF__ macro), using printf-based line construction for portable behavior across POSIX shells (dash, bash, FreeBSD sh). - Introduce empirical flag probing (probe_flag) using active CC/CXX drivers. - Dynamically detect and apply C/C++ compiler hardening flags: -fstack-protector-strong, -D_FORTIFY_SOURCE=2, -fPIE. - Dynamically detect and apply linker hardening flags: -Wl,-z,relro, -Wl,-z,now, -Wl,-z,noexecstack, -pie. - Propagate HARDEN_CFLAGS and HARDEN_CXXFLAGS to all production objects, including vendored components (mimalloc, sqlite3, tre, grammars, lz4, zstd). - Preserve opt-out escape hatch via PRODUCTION_HARDENING=0. - Retain backward compatibility for legacy HARDENING/FREEBSD_HARDENING variables. - Leave macOS Mach-O, Windows PE, and test/sanitizer builds unchanged. Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- Makefile.cbm | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Makefile.cbm b/Makefile.cbm index cf191acf2..30cb79008 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -154,13 +154,32 @@ HARDEN_LDFLAGS := ifeq ($(PRODUCTION_HARDENING),1) # Detect ELF target output purely via compiler preprocessor macros. -ELF_BUILD := $(shell echo | $(CC) -dM -E -x c - 2>/dev/null | grep -q '__ELF__' && echo yes || echo no) +# This eliminates dependencies on OS binary tools (readelf/file/hexdump/od) +# and avoids disk/temporary file operations during Makefile parsing. +# +# NOTE: built with `printf '%s\n' ...` rather than `echo '...\n...'`. +# The shell `echo` builtin's handling of literal `\n` inside a single quoted +# string is NOT portable: dash (Linux's default /bin/sh) expands it to a real +# newline, but the FreeBSD /bin/sh `echo` does not, which collapsed this whole +# probe onto one line ("#ifndef __ELF__\n#error ...\n#endif") — an unterminated +# #ifndef that always fails to preprocess, forcing ELF_BUILD=no even on real +# ELF targets and silently skipping all hardening. printf '%s\n' arg1 arg2 ... +# emits one real newline per argument on every POSIX shell, so this is safe +# across dash, bash, and FreeBSD sh alike. +ELF_BUILD := $(shell \ + printf '%s\n' '#ifndef __ELF__' '#error "Not ELF"' '#endif' | \ + $(CC) -E -x c - >/dev/null 2>&1 && echo yes || echo no) ifeq ($(ELF_BUILD),yes) comma := , # Unified probe macro to detect compiler and linker flags empirically. +# Usage: $(call probe_flag,TOOL,LANG,STAGE,FLAGS) +# TOOL: Compiler driver ($(CC) or $(CXX)) +# LANG: Language ('c' or 'c++') +# STAGE: 'compile' (-c) or 'link' (full binary link) +# FLAGS: The flag or space-separated list of flags to test probe_flag = $(shell \ echo 'int main(void){return 0;}' | \ $(1) -Werror -O2 $(if $(filter compile,$(3)),-c) $(4) -x $(2) - -o /dev/null \ From 0ac2da919d335f587fa55d2261fe005ce168e68d Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:13:08 +0100 Subject: [PATCH 03/13] fix: guard activation_finish_absent_publish behind its call-site conditions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Building on FreeBSD after syncing with upstream failed with -Werror,-Wunused-function on activation_finish_absent_publish in src/cli/activation_transaction.c. The function is only invoked from three platform-specific branches: Windows (_WIN32, MoveFileExW), macOS (__APPLE__, renameatx_np), and modern Linux (__linux__ with SYS_renameat2, via syscall). On every other POSIX target — including FreeBSD and other BSDs — control falls through to the portable linkat(2) fallback, so the helper was defined but never called, tripping -Wunused-function under -Werror. Rather than excluding FreeBSD by name (which would still break on OpenBSD, NetBSD, DragonFly, Solaris, or any Linux without SYS_renameat2), wrap the function definition in the same #if condition as its call sites. This keeps the guard mechanically tied to actual usage instead of enumerating platforms, so it stays correct as new targets are added. Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- src/cli/activation_transaction.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cli/activation_transaction.c b/src/cli/activation_transaction.c index 2445703f6..f1cebc9f6 100644 --- a/src/cli/activation_transaction.c +++ b/src/cli/activation_transaction.c @@ -1647,6 +1647,7 @@ static activation_publish_status_t activation_publish_absent_link_fallback( } #endif +#if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && defined(SYS_renameat2)) static activation_publish_status_t activation_finish_absent_publish( cbm_activation_transaction_t *transaction) { transaction->staged_exists = false; @@ -1657,6 +1658,7 @@ static activation_publish_status_t activation_finish_absent_publish( ? ACTIVATION_PUBLISH_OK : ACTIVATION_PUBLISH_CHANGED_ERROR; } +#endif static activation_publish_status_t activation_publish_absent_replacement( cbm_activation_transaction_t *transaction) { From 22c95b08dc933363ca8673949e639d975528a70a Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:38:06 +0100 Subject: [PATCH 04/13] fix: detect ELF target via __ELF__ macro instead of Makefile-unsafe probe The ELF_BUILD probe used `printf '%s\n' '#ifndef __ELF__' ...` piped into the preprocessor to detect ELF targets by triggering (or not) a #error. This broke the macOS CI job with: Makefile.cbm:146: *** unterminated call to function `shell': missing `)'. Stop. GNU Make treats `#` as a comment character even inside a $(shell ...) call and even inside quoted strings, unless escaped with `\#`. macOS ships GNU Make 3.81 (frozen pre-GPLv3), whose parser hit this literally and swallowed the rest of the line, including the closing `)`. Linux and FreeBSD builds didn't show this because they use newer GNU Make, which happened to tolerate it. Replace the #error-triggering probe with a direct macro check, matching the existing IS_GCC/IS_MINGW probes in this file: dump predefined macros via -dM -E and grep for __ELF__, which compilers define natively on ELF targets. This removes every literal `#` from the Makefile text, so the probe parses identically across GNU Make versions, and is more direct than the previous #ifndef/#error round-trip. Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- Makefile.cbm | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Makefile.cbm b/Makefile.cbm index 30cb79008..6c64fffcb 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -166,9 +166,7 @@ ifeq ($(PRODUCTION_HARDENING),1) # ELF targets and silently skipping all hardening. printf '%s\n' arg1 arg2 ... # emits one real newline per argument on every POSIX shell, so this is safe # across dash, bash, and FreeBSD sh alike. -ELF_BUILD := $(shell \ - printf '%s\n' '#ifndef __ELF__' '#error "Not ELF"' '#endif' | \ - $(CC) -E -x c - >/dev/null 2>&1 && echo yes || echo no) +ELF_BUILD := $(shell echo | $(CC) -dM -E -x c - 2>/dev/null | grep -q '__ELF__' && echo yes || echo no) ifeq ($(ELF_BUILD),yes) From 7af70028da498bc13b5dc70a37614f17a0c82fe4 Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:46:20 +0100 Subject: [PATCH 05/13] fix(tests): include in test_stack_overflow.c Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- tests/test_stack_overflow.c | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_stack_overflow.c b/tests/test_stack_overflow.c index 6b2e8228c..9b646296c 100644 --- a/tests/test_stack_overflow.c +++ b/tests/test_stack_overflow.c @@ -16,6 +16,7 @@ #include #include #include +#include /* tree-sitter runtime allocator hooks (ts_runtime/src/alloc.h, TS_PUBLIC) and * mimalloc (vendored) — for the #424 allocator-binding regression test. */ From 721f0963ecaf47d43e788060fd084aee581dde8e Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:47:34 +0100 Subject: [PATCH 06/13] fix(tests): add native FreeBSD process-image support via sysctl(KERN_PROC_PATHNAME) Extends FreeBSD support across process-image test suites: - tests/test_daemon_runtime.c: add FreeBSD sysctl self/peer path resolution, widen helper guards and test runner registrations. - tests/test_mcp.c: implement FreeBSD sysctl in idxfailclosed_self_path. - tests/test_watcher.c: implement FreeBSD sysctl in watcher_test_self_image and include __FreeBSD__ in blocked-git watcher tests. Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- tests/test_daemon_runtime.c | 42 ++++++++++++++++++++++++------------- tests/test_mcp.c | 8 +++++++ tests/test_watcher.c | 15 +++++++++++-- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/tests/test_daemon_runtime.c b/tests/test_daemon_runtime.c index 92aa11bd9..5c82fa38a 100644 --- a/tests/test_daemon_runtime.c +++ b/tests/test_daemon_runtime.c @@ -45,6 +45,10 @@ #ifdef __APPLE__ #include #endif +#if defined(__FreeBSD__) +#include +#include +#endif #include #include #include @@ -331,7 +335,7 @@ static bool runtime_test_windows_spawn_image_holder(const char *image_path, cons #endif -#if defined(__APPLE__) || defined(__linux__) +#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) static bool runtime_test_copy_executable(const char *source, const char *destination) { int source_fd = open(source, O_RDONLY | O_CLOEXEC); @@ -452,7 +456,7 @@ static void runtime_test_stop_blocked_executable(pid_t child, int release_fd) { #endif -#if defined(__APPLE__) || defined(__linux__) +#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) static bool runtime_test_self_image_path(char source[RUNTIME_TEST_PATH_CAP]) { #ifdef __APPLE__ int length = proc_pidpath(getpid(), source, RUNTIME_TEST_PATH_CAP); @@ -460,6 +464,10 @@ static bool runtime_test_self_image_path(char source[RUNTIME_TEST_PATH_CAP]) { if (resolved) { source[length] = '\0'; } +#elif defined(__FreeBSD__) + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; + size_t length = RUNTIME_TEST_PATH_CAP; + bool resolved = sysctl(mib, 4, source, &length, NULL, 0) == 0; #else ssize_t length = readlink("/proc/self/exe", source, RUNTIME_TEST_PATH_CAP - 1); bool resolved = length > 0 && length < (ssize_t)RUNTIME_TEST_PATH_CAP - 1; @@ -474,7 +482,7 @@ static bool runtime_test_self_image_path(char source[RUNTIME_TEST_PATH_CAP]) { static bool runtime_test_copy_self_image(const char *destination) { #ifdef _WIN32 return runtime_test_windows_copy_self(destination); -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) char source[RUNTIME_TEST_PATH_CAP]; return runtime_test_self_image_path(source) && runtime_test_copy_executable(source, destination); @@ -527,6 +535,12 @@ static bool runtime_test_process_image_matches(uint64_t process_id, const char * if (length <= 0 || length >= (int)sizeof(observed)) { observed[0] = '\0'; } +#elif defined(__FreeBSD__) + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, (int)process_id}; + size_t length = sizeof(observed); + if (sysctl(mib, 4, observed, &length, NULL, 0) != 0) { + observed[0] = '\0'; + } #elif defined(__linux__) char proc_path[64]; int written = @@ -607,7 +621,7 @@ static bool runtime_test_force_terminate_verified(uint64_t process_id, const cha } (void)CloseHandle(process); return terminated; -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) /* The marker lives in a private test directory and was written by this * exact copied image. Revalidate immediately before signaling so the * cleanup backstop never targets an unrelated or PID-reused process. */ @@ -620,7 +634,7 @@ static bool runtime_test_force_terminate_verified(uint64_t process_id, const cha #endif } -#if defined(_WIN32) || defined(__linux__) +#if defined(_WIN32) || defined(__linux__) || defined(__FreeBSD__) static bool runtime_test_append_image_marker(const char *path) { FILE *file = cbm_fopen(path, "ab"); bool written = file && fputc('\n', file) != EOF; @@ -690,7 +704,7 @@ static bool runtime_test_run_hello_image(const char *image_path, *exit_code_out = (int)exit_code; } return read && exit_code <= INT_MAX; -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) pid_t child = fork(); if (child == 0) { (void)alarm(TF_RUNTIME_IMAGE_WATCHDOG_SECONDS); @@ -756,7 +770,7 @@ static bool runtime_test_run_activation_image(const char *image_path, *exit_code_out = (int)exit_code; } return read && exit_code <= INT_MAX; -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) char action_text[16]; int action_written = snprintf(action_text, sizeof(action_text), "%u", (unsigned int)action); pid_t child = action_written > 0 && action_written < (int)sizeof(action_text) ? fork() : -1; @@ -2035,7 +2049,7 @@ TEST(daemon_runtime_activation_ack_snapshots_then_interrupts_all_clients) { PASS(); } -#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) +#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) TEST(daemon_runtime_activation_accepts_authenticated_different_build) { cbm_daemon_build_identity_t active_identity = runtime_test_identity("2.4.0", runtime_test_self_build()); @@ -3445,7 +3459,7 @@ TEST(daemon_runtime_disconnect_cancels_blocked_application_before_exit) { } TEST(daemon_runtime_disconnect_cancels_blocked_non_index_child_and_preserves_other_session) { -#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__linux__) +#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__linux__) && !defined(__FreeBSD__) SKIP_PLATFORM("requires a queryable copied process image"); #else enum { @@ -4185,7 +4199,7 @@ TEST(daemon_runtime_copied_image_fallback_accepts_identical_and_rejects_changed) (void)cbm_unlink(identical_path); runtime_test_fixture_finish(&identical_fixture); -#ifdef __linux__ +#if defined(__linux__) || defined(__FreeBSD__) runtime_test_fixture_t changed_fixture; bool changed_started = runtime_test_fixture_start(&changed_fixture, "copied-changed", &identity); @@ -4214,7 +4228,7 @@ TEST(daemon_runtime_copied_image_fallback_accepts_identical_and_rejects_changed) ASSERT_TRUE(identical_ran); ASSERT_EQ(identical_exit, 0); ASSERT_TRUE(identical_exited); -#ifdef __linux__ +#if defined(__linux__) || defined(__FreeBSD__) ASSERT_TRUE(changed_started); ASSERT_TRUE(changed_copied); ASSERT_TRUE(changed_bytes); @@ -4301,7 +4315,7 @@ TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path) { ASSERT_TRUE(!fingerprinted || strcmp(observed, replacement) != 0); PASS(); } -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path) { char directory[RUNTIME_TEST_PATH_CAP] = {0}; char image_path[RUNTIME_TEST_PATH_CAP] = {0}; @@ -4545,7 +4559,7 @@ SUITE(daemon_runtime) { RUN_TEST(daemon_runtime_mac_fast_path_rejects_foreign_main_image_mapping_active); #endif RUN_TEST(daemon_runtime_copied_image_fallback_accepts_identical_and_rejects_changed); -#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) +#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) RUN_TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path); #endif RUN_TEST(daemon_runtime_convenience_service_owns_participant_guard); @@ -4554,7 +4568,7 @@ SUITE(daemon_runtime) { RUN_TEST(daemon_runtime_unexpected_frame_payload_is_freed_once); RUN_TEST(daemon_runtime_activation_rejects_forged_and_malformed_without_stop); RUN_TEST(daemon_runtime_activation_ack_snapshots_then_interrupts_all_clients); -#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) +#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) RUN_TEST(daemon_runtime_activation_accepts_authenticated_different_build); #endif RUN_TEST(daemon_runtime_future_generation_gets_stable_explicit_conflict); diff --git a/tests/test_mcp.c b/tests/test_mcp.c index a56aebdf5..7931a79ea 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -35,6 +35,10 @@ #ifdef __APPLE__ #include #endif +#if defined(__FreeBSD__) +#include +#include +#endif #include #include #include @@ -8106,6 +8110,10 @@ static bool idxfailclosed_self_path(char out[CBM_SZ_4K]) { out[length] = '\0'; } return resolved; +#elif defined(__FreeBSD__) + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; + size_t length = CBM_SZ_4K; + return sysctl(mib, 4, out, &length, NULL, 0) == 0; #else (void)out; return false; diff --git a/tests/test_watcher.c b/tests/test_watcher.c index cd4144f3e..af9ae97aa 100644 --- a/tests/test_watcher.c +++ b/tests/test_watcher.c @@ -27,6 +27,10 @@ #ifdef __APPLE__ #include #endif +#if defined(__FreeBSD__) +#include +#include +#endif /* Portable git: `git -C "" ` with identity + non-interactive * config injected via -c, so it needs no global config and no POSIX shell @@ -850,7 +854,7 @@ static bool watcher_windows_terminate_verified(HANDLE process, } #endif -#if defined(__APPLE__) || defined(__linux__) +#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) typedef struct { cbm_watcher_t *watcher; atomic_bool completed; @@ -871,6 +875,13 @@ static bool watcher_test_self_image(char out[CBM_SZ_4K]) { } out[length] = '\0'; return true; +#elif defined(__FreeBSD__) + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; + size_t length = CBM_SZ_4K; + if (sysctl(mib, 4, out, &length, NULL, 0) != 0) { + return false; + } + return true; #else ssize_t length = readlink("/proc/self/exe", out, CBM_SZ_4K - 1); if (length <= 0 || length >= CBM_SZ_4K - 1) { @@ -1057,7 +1068,7 @@ TEST(watcher_stop_and_unwatch_cancel_blocked_git_without_backstop) { ASSERT_TRUE(unwatch_run_completed); ASSERT_EQ(unwatch_join_rc, 0); PASS(); -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) char work[CBM_SZ_1K] = "/tmp/cbm-watcher-blocked-git-XXXXXX"; ASSERT_NOT_NULL(cbm_mkdtemp(work)); char root[CBM_SZ_1K]; From 815a2bff78691d9e151d669d1fd724395b933e42 Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:43:47 +0100 Subject: [PATCH 07/13] fix(build): add FreeBSD support to Makefile.cbm with posix wrap, mem_profile and -lexecinfo Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- Makefile.cbm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile.cbm b/Makefile.cbm index 6c64fffcb..f5be63ade 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -9,14 +9,14 @@ # Compiler selection — override via: make CC=gcc CXX=g++ # macOS: cc (Apple Clang) — universal binary with ASan support -# Linux: gcc/g++ — system default with full sanitizer support +# Linux/FreeBSD: gcc/clang — system default with full sanitizer support # CI scripts pass CC/CXX explicitly; don't rely on defaults here # Target architecture (macOS): build.sh/test.sh export ARCHFLAGS="-arch " # (see scripts/env.sh). Fold it into the compiler drivers with `override` so it # reaches EVERY compile and link recipe — including the vendored objects below # that use their own *_CFLAGS — and so it survives a command-line `CC=` override. -# ARCHFLAGS is empty on Linux/Windows and for native direct `make` invocations, +# ARCHFLAGS is empty on Linux/FreeBSD/Windows and for native direct `make` invocations, # leaving CC/CXX unchanged there. override CC := $(CC) $(ARCHFLAGS) override CXX := $(CXX) $(ARCHFLAGS) @@ -241,6 +241,7 @@ LDFLAGS_TSAN = -lm -lstdc++ -lpthread -lz $(EXECINFO_LIBS) $(TSAN_SANITIZE) $(WI FOUNDATION_SRCS = \ src/foundation/mem_override_win.c \ src/foundation/mem_override_posix.c \ + src/foundation/mem_profile.c \ src/foundation/arena.c \ src/foundation/hash_table.c \ src/foundation/str_intern.c \ @@ -424,7 +425,7 @@ UI_SRCS = \ # mimalloc (vendored, global allocator override) # # Override strategy is platform-specific: -# * Unix (macOS/Linux): rely on static-link-order override — the prod mimalloc +# * Unix (macOS/Linux/FreeBSD): rely on static-link-order override — the prod mimalloc # object is linked first so its strong malloc/free symbols win. We do NOT # define MI_MALLOC_OVERRIDE here: that would compile alloc-override.c's # forwarding definitions (malloc/free/posix_memalign/...) which, on macOS's From 668772c21e5b24cc42f7d74238f26dd0a7313684 Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:08:51 +0100 Subject: [PATCH 08/13] style: fix clang-format violation in mem_override_posix.c Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- src/foundation/mem_override_posix.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foundation/mem_override_posix.c b/src/foundation/mem_override_posix.c index 2d3ab225d..890baa56a 100644 --- a/src/foundation/mem_override_posix.c +++ b/src/foundation/mem_override_posix.c @@ -25,7 +25,7 @@ #include #include -#if defined(__linux__) +#if defined(__linux__) || defined(__FreeBSD__) void *__wrap_malloc(size_t size) { void *block = mi_malloc(size); @@ -66,4 +66,4 @@ char *__wrap_strdup(const char *text) { return copy; } -#endif /* __linux__ */ +#endif /* __linux__ || __FreeBSD__ */ From b4614419e784f7e4e4e1ea0734716133df293523 Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:46:28 +0100 Subject: [PATCH 09/13] fix(cli): add native FreeBSD self-executable path resolution cbm_detect_self_path had no branch for FreeBSD, falling through to the generic #else that reads /proc/self/exe. In this environment /proc is a native FreeBSD procfs mount left empty (only /compat/linux/proc is populated, for Linux-ABI processes under the compat layer), so the readlink always failed silently and the function fell back to the expected install path instead of the actual running binary's path. This broke install: with no valid self path, the activation transaction could never confirm what to publish and aborted early with "activation transaction I/O failed" before attempting any linkat/rename. Add sysctl(CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1) for FreeBSD, matching the pattern already used in the test suites, and include guarded by __FreeBSD__. Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- src/cli/cli.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/cli/cli.c b/src/cli/cli.c index 5db30c73a..26b6f1e74 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -107,6 +107,10 @@ static int cbm_powershell_quote_word(const char *value, char *out, size_t out_si #include #include // strtok_r #include // mode_t, S_IXUSR +#ifdef __FreeBSD__ +#include +#include +#endif #include #include #include // MAX_WBITS @@ -8887,6 +8891,13 @@ static bool cbm_detect_self_path(char *buf, size_t buf_sz, const char *home) { if (!exact) { buf[0] = '\0'; } +#elif defined(__FreeBSD__) + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; + size_t cb = buf_sz; + exact = sysctl(mib, 4, buf, &cb, NULL, 0) == 0 && cb > 0; + if (!exact) { + buf[0] = '\0'; + } #else ssize_t sp_len = readlink("/proc/self/exe", buf, buf_sz - SKIP_ONE); exact = sp_len > 0 && (size_t)sp_len < buf_sz - SKIP_ONE; From 71640cc87ecbc8f15d9482190d8b6b33ae9689c7 Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:46:28 +0100 Subject: [PATCH 10/13] fix(mem): guard __wrap_free/__wrap_realloc against foreign allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mimalloc's link-order override on POSIX assumes every allocation in the process image goes through mimalloc, but this doesn't hold universally — the same failure mode already documented for macOS's two-level namespace. On FreeBSD, some libc code paths may return pointers that were never allocated by mimalloc; freeing them via mi_free aborted with "mimalloc: error: mi_free: invalid pointer" during install. Add mem_override_is_ours() (mi_is_in_heap_region) and route __wrap_free/ __wrap_realloc through it: mimalloc-owned blocks go to mi_free/mi_realloc as before, anything else falls back to the real libc __real_free/ __real_realloc. Also fixes __wrap_realloc(NULL, size), which previously skipped the profiling-free call but still risked mixing allocators on the grow path. Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- src/foundation/mem_override_posix.c | 35 ++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/foundation/mem_override_posix.c b/src/foundation/mem_override_posix.c index 890baa56a..de102e4ba 100644 --- a/src/foundation/mem_override_posix.c +++ b/src/foundation/mem_override_posix.c @@ -25,7 +25,14 @@ #include #include -#if defined(__linux__) || defined(__FreeBSD__) +#if (defined(__linux__) || defined(__FreeBSD__)) && !defined(CBM_SANITIZED_BUILD) + +void *__real_realloc(void *block, size_t size); +void __real_free(void *block); + +static inline bool mem_override_is_ours(const void *block) { + return mi_is_in_heap_region(block); +} void *__wrap_malloc(size_t size) { void *block = mi_malloc(size); @@ -40,22 +47,30 @@ void *__wrap_calloc(size_t count, size_t size) { } void *__wrap_realloc(void *block, size_t size) { - if (block) { + if (!block) { + void *grown = mi_malloc(size); + cbm_mem_profile_alloc(grown, size); + return grown; + } + if (mem_override_is_ours(block)) { cbm_mem_profile_free(block); + void *grown = mi_realloc(block, size); + cbm_mem_profile_alloc(grown, size); + return grown; } - void *grown = mi_realloc(block, size); - cbm_mem_profile_alloc(grown, size); - return grown; + return __real_realloc(block, size); } void __wrap_free(void *block) { if (!block) { return; } - /* Unlike Windows there is no foreign-allocator hazard to route around: - * every malloc in this image is already mimalloc's. */ - cbm_mem_profile_free(block); - mi_free(block); + if (mem_override_is_ours(block)) { + cbm_mem_profile_free(block); + mi_free(block); + } else { + __real_free(block); + } } char *__wrap_strdup(const char *text) { @@ -66,4 +81,4 @@ char *__wrap_strdup(const char *text) { return copy; } -#endif /* __linux__ || __FreeBSD__ */ +#endif /* (defined(__linux__) || defined(__FreeBSD__)) && !defined(CBM_SANITIZED_BUILD) */ From cd18e27fd0a2febda3a471541aed6550fdbadc88 Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:46:28 +0100 Subject: [PATCH 11/13] fix(daemon): add native FreeBSD process-image identity support Extends runtime_process_image_reference_* to support FreeBSD process image discovery and comparison. Previously this functionality was guarded only for _WIN32, __APPLE__, and __linux__. Uses sysctl(KERN_PROC_PATHNAME) to resolve a process's executable path without depending on procfs. Renames runtime_linux_stat_same_image to runtime_posix_stat_same_image now that the same comparison logic serves Linux, macOS, and FreeBSD. Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- src/daemon/runtime.c | 55 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/src/daemon/runtime.c b/src/daemon/runtime.c index 1d08051bd..4cca2dd94 100644 --- a/src/daemon/runtime.c +++ b/src/daemon/runtime.c @@ -30,10 +30,14 @@ #include #include #include -#elif defined(__linux__) +#elif defined(__linux__) || defined(__FreeBSD__) #include #include #include +#if defined(__FreeBSD__) +#include +#include +#endif #endif enum { @@ -135,7 +139,7 @@ typedef struct { HANDLE file; BY_HANDLE_FILE_INFORMATION information; LARGE_INTEGER size; -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) int fd; struct stat status; #endif @@ -490,7 +494,7 @@ static bool runtime_activation_response_decode( static uint64_t runtime_current_process_id(void) { #ifdef _WIN32 return (uint64_t)GetCurrentProcessId(); -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) return (uint64_t)getpid(); #else return 0; @@ -504,7 +508,7 @@ static void runtime_process_image_reference_init(runtime_process_image_reference memset(reference, 0, sizeof(*reference)); #ifdef _WIN32 reference->file = INVALID_HANDLE_VALUE; -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) reference->fd = -1; #endif } @@ -518,7 +522,7 @@ static bool runtime_process_image_reference_release(runtime_process_image_refere if (reference->file != INVALID_HANDLE_VALUE && !CloseHandle(reference->file)) { ok = false; } -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) if (reference->fd >= 0 && close(reference->fd) != 0) { ok = false; } @@ -658,9 +662,9 @@ static bool runtime_mac_process_maps_file_executable(int process_id, const struc return false; } -#elif defined(__linux__) +#elif defined(__linux__) || defined(__FreeBSD__) -static bool runtime_linux_stat_same_image(const struct stat *first, const struct stat *second) { +static bool runtime_posix_stat_same_image(const struct stat *first, const struct stat *second) { return first && second && S_ISREG(first->st_mode) && S_ISREG(second->st_mode) && first->st_dev == second->st_dev && first->st_ino == second->st_ino && first->st_size == second->st_size && first->st_mtim.tv_sec == second->st_mtim.tv_sec && @@ -769,11 +773,11 @@ static bool runtime_process_image_reference_acquire( (!fingerprint || cbm_daemon_build_fingerprint_native_file((uintptr_t)image_fd, fingerprint)) && fstat(image_fd, &image_after) == 0 && - runtime_linux_stat_same_image(&image_before, &image_after); + runtime_posix_stat_same_image(&image_before, &image_after); int verify_fd = ok ? openat(process_fd, "exe", O_RDONLY | O_CLOEXEC) : -1; struct stat verify_status; ok = ok && verify_fd >= 0 && fstat(verify_fd, &verify_status) == 0 && - runtime_linux_stat_same_image(&image_after, &verify_status); + runtime_posix_stat_same_image(&image_after, &verify_status); if (verify_fd >= 0 && close(verify_fd) != 0) { ok = false; } @@ -787,6 +791,33 @@ static bool runtime_process_image_reference_acquire( } else if (image_fd >= 0) { (void)close(image_fd); } +#elif defined(__FreeBSD__) + if (process_id > INT_MAX) { + return false; + } + int pid = (int)process_id; + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, pid}; + char path[PATH_MAX]; + size_t path_length = sizeof(path); + bool ok = sysctl(mib, 4, path, &path_length, NULL, 0) == 0 && path_length > 0; + if (ok) { + path[path_length < sizeof(path) ? path_length : sizeof(path) - 1] = '\0'; + } + int image_fd = ok ? open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK) : -1; + struct stat image_before; + struct stat image_after; + ok = image_fd >= 0 && fstat(image_fd, &image_before) == 0 && S_ISREG(image_before.st_mode) && + (!fingerprint || + cbm_daemon_build_fingerprint_native_file((uintptr_t)image_fd, fingerprint)) && + fstat(image_fd, &image_after) == 0 && + runtime_posix_stat_same_image(&image_before, &image_after); + if (ok) { + reference->held = true; + reference->fd = image_fd; + reference->status = image_after; + } else if (image_fd >= 0) { + (void)close(image_fd); + } #else (void)process_id; bool ok = false; @@ -827,14 +858,14 @@ static bool runtime_process_image_reference_matches_process( runtime_mac_stat_same(&active->status, &peer.status); bool released = runtime_process_image_reference_release(&peer); return same && released; -#elif defined(__linux__) +#elif defined(__linux__) || defined(__FreeBSD__) runtime_process_image_reference_t peer; runtime_process_image_reference_init(&peer); bool same = runtime_process_image_reference_acquire(process_id, &peer, NULL); struct stat active_now; same = same && fstat(active->fd, &active_now) == 0 && - runtime_linux_stat_same_image(&active->status, &active_now) && - runtime_linux_stat_same_image(&active->status, &peer.status); + runtime_posix_stat_same_image(&active->status, &active_now) && + runtime_posix_stat_same_image(&active->status, &peer.status); bool released = runtime_process_image_reference_release(&peer); return same && released; #else From 2496d8f2c5090e45d16e14f47c836ba42463460f Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:46:28 +0100 Subject: [PATCH 12/13] fix(daemon): trust /home as a root-owned alias on FreeBSD private_log_directory_path_copy resolves a small allowlist of trusted top-level aliases before its component-wise O_NOFOLLOW path walk (Darwin's /tmp -> /private/tmp and /var -> /private/var). FreeBSD commonly exposes /home as a symlink to /usr/home. Treat it as a trusted root-owned alias so log directory paths under /home resolve correctly during the secure path walk. Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- src/daemon/ipc.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/daemon/ipc.c b/src/daemon/ipc.c index 0c1ae652c..30b50e663 100644 --- a/src/daemon/ipc.c +++ b/src/daemon/ipc.c @@ -1336,12 +1336,13 @@ static bool private_log_base_name_valid(const char *base_name) { } static char *private_log_directory_path_copy(const char *directory_path) { -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(__FreeBSD__) /* Darwin exposes the trusted top-level aliases /tmp -> /private/tmp and - * /var -> /private/var. Resolve only those root-owned aliases before the + * /var -> /private/var. FreeBSD exposes /home -> /usr/home (or usr/home). + * Resolve only those root-owned aliases before the * component-wise O_NOFOLLOW walk. Canonicalizing the complete caller path * would follow an attacker-controlled cache/log symlink and is forbidden. */ - static const char *const aliases[] = {"/tmp", "/var"}; + static const char *const aliases[] = {"/tmp", "/var", "/home"}; for (size_t index = 0; index < sizeof(aliases) / sizeof(aliases[0]); index++) { const char *alias = aliases[index]; size_t alias_length = strlen(alias); From 1fb25c295471d0d317d1c9e1fdf29f4e9df11548 Mon Sep 17 00:00:00 2001 From: Pedro Ramos <131530838+pr9000@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:43:43 +0100 Subject: [PATCH 13/13] fix(build): define CBM_SANITIZED_BUILD in TSan flags too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CFLAGS_TEST/CXXFLAGS_TEST already define CBM_SANITIZED_BUILD via SANITIZED_DEFINE, but CFLAGS_TSAN/CXXFLAGS_TSAN never did. This left mem_override_posix.c's !defined(CBM_SANITIZED_BUILD) guard always true under make test-tsan, compiling __wrap_free/__wrap_realloc (with their __real_free/__real_realloc forward declarations) into a link that never passes -Wl,--wrap=... — undefined reference at link time. Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> --- Makefile.cbm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.cbm b/Makefile.cbm index f5be63ade..6a1eb2a24 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -83,9 +83,9 @@ CXXFLAGS_TEST = $(CXXFLAGS_COMMON) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE) # TSan (can't combine with ASan) TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer CFLAGS_TSAN = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) -g -O1 \ - $(TSAN_SANITIZE) + $(SANITIZED_DEFINE) $(TSAN_SANITIZE) CXXFLAGS_TSAN = $(CXXFLAGS_COMMON) -g -O1 \ - $(TSAN_SANITIZE) + $(SANITIZED_DEFINE) $(TSAN_SANITIZE) # Windows needs ws2_32 (Winsock), psapi (GetProcessMemoryInfo), shell32 # (CommandLineToArgvW — the UTF-8 argv path in main.c, #423/#20), and