Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 124 additions & 22 deletions Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -109,38 +109,139 @@ 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.
# 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 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.
# 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 \
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/mem_profile.c \
src/foundation/arena.c \
src/foundation/hash_table.c \
src/foundation/str_intern.c \
Expand Down Expand Up @@ -324,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
Expand All @@ -349,7 +450,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 \
Expand All @@ -359,12 +461,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 <regex.h>)
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
Expand Down Expand Up @@ -577,7 +679,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) \
Expand Down Expand Up @@ -706,7 +808,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)
Expand Down Expand Up @@ -822,14 +924,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)

Expand Down
2 changes: 2 additions & 0 deletions src/cli/activation_transaction.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions src/cli/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ static int cbm_powershell_quote_word(const char *value, char *out, size_t out_si
#include <stdlib.h>
#include <string.h> // strtok_r
#include <sys/stat.h> // mode_t, S_IXUSR
#ifdef __FreeBSD__
#include <sys/types.h>
#include <sys/sysctl.h>
#endif
#include <time.h>
#include <wchar.h>
#include <zlib.h> // MAX_WBITS
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions src/daemon/ipc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading