diff --git a/Makefile.cbm b/Makefile.cbm index 657f4aacc..6a1eb2a24 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) @@ -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 @@ -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 @@ -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 \ @@ -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 @@ -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 \ @@ -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 ) 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 +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) \ @@ -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) @@ -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) 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) { 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; 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); 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 diff --git a/src/foundation/mem_override_posix.c b/src/foundation/mem_override_posix.c index 2d3ab225d..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__) +#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__ */ +#endif /* (defined(__linux__) || defined(__FreeBSD__)) && !defined(CBM_SANITIZED_BUILD) */ 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_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. */ 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];