Skip to content

Commit 077e9f1

Browse files
authored
Merge branch 'main' into fm/cbm-worktree-ignore-1
2 parents d21918c + 8042aa8 commit 077e9f1

24 files changed

Lines changed: 1696 additions & 702 deletions

‎Makefile.cbm‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ endif
7575
# CBM_BIND_TS_ALLOCATOR=1: bind the tree-sitter runtime to mimalloc (#424). Only
7676
# the prod build uses mimalloc (MI_OVERRIDE=1); the test build is CRT+ASan, where
7777
# binding would create an alloc/free mismatch, so the guard is prod-only.
78-
CFLAGS_PROD = $(CFLAGS_COMMON) -O2 -DCBM_BIND_TS_ALLOCATOR=1 $(TEST_SEAM_DEFINE) $(CFLAGS_EXTRA)
78+
CFLAGS_PROD = $(CFLAGS_COMMON) -O2 -DCBM_BIND_TS_ALLOCATOR=1 $(TEST_SEAM_DEFINE) \
79+
$(GLOBAL_OVERRIDE_DEFINE) $(CFLAGS_EXTRA)
7980
CXXFLAGS_PROD = $(CXXFLAGS_COMMON) -O2
8081

8182
# Test flags: debug + sanitizers (override SANITIZE= to disable on Windows)
@@ -386,9 +387,34 @@ UI_SRCS = \
386387
# derives it from the MI_OVERRIDE option); MI_OVERRIDE alone is not read by the
387388
# source — it is kept here only as this project's prod/test marker.
388389
MIMALLOC_SRC = vendored/mimalloc/src/static.c
390+
# Linux belongs on this list too (#1360). The "Unix relies on static-link-order
391+
# override" strategy described above never actually worked: mimalloc defines
392+
# malloc/free ONLY when MI_MALLOC_OVERRIDE is set (it gates alloc-override.c),
393+
# so with the define off there were no strong symbols for the link order to
394+
# prefer. Measured on the shipped v0.9.1-rc.1 artifacts — linux-arm64 glibc AND
395+
# musl-static both report 0/6 allocator-owned size classes, i.e. ordinary malloc
396+
# was going to libc and every purge/reclaim option set in cbm_mem_init applied
397+
# only to the bound sqlite/tree-sitter populations.
398+
#
399+
# The macOS objection does not transfer. That abort is specific to the two-level
400+
# namespace, where this binary's free becomes mi_free while system libraries keep
401+
# allocating from the system allocator, so a pointer crossing the boundary hits
402+
# "mi_free: invalid pointer". ELF is a flat namespace: interposition is
403+
# process-wide and all-or-nothing, which is the configuration mimalloc is
404+
# normally deployed in. macOS therefore stays off, deliberately and permanently.
405+
#
406+
# TWO defines, switched together in ONE place so they cannot drift:
407+
# MI_MALLOC_OVERRIDE reaches the mimalloc translation unit and turns the
408+
# override on; CBM_MEM_GLOBAL_OVERRIDE reaches OUR sources and tells the startup
409+
# audit what to expect. CBM_MEM_GLOBAL_OVERRIDE goes only into CFLAGS_PROD,
410+
# because MIMALLOC_CFLAGS_TEST compiles mimalloc with -DMI_OVERRIDE=0 and no
411+
# override define at all — a test binary has no global override BY CONSTRUCTION
412+
# and must never be told to expect one, or it warns on every run.
389413
MIMALLOC_OVERRIDE_DEFINE :=
390-
ifeq ($(IS_MINGW),yes)
414+
GLOBAL_OVERRIDE_DEFINE :=
415+
ifneq ($(filter yes,$(IS_MINGW) $(IS_LINUX)),)
391416
MIMALLOC_OVERRIDE_DEFINE := -DMI_MALLOC_OVERRIDE=1
417+
GLOBAL_OVERRIDE_DEFINE := -DCBM_MEM_GLOBAL_OVERRIDE=1
392418
endif
393419
MIMALLOC_CFLAGS = -std=c11 -O2 -w \
394420
-Ivendored/mimalloc/include \

‎internal/cbm/zstd_store.c‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
#include <stddef.h>
88
#include <stdint.h>
99

10-
int cbm_zstd_compress(const char *src, int srcLen, char *dst, int dstCap, int level) {
11-
size_t rc = ZSTD_compress(dst, (size_t)dstCap, src, (size_t)srcLen, level);
10+
int64_t cbm_zstd_compress(const char *src, size_t srcLen, char *dst, size_t dstCap, int level) {
11+
size_t rc = ZSTD_compress(dst, dstCap, src, srcLen, level);
1212
if (ZSTD_isError(rc)) {
1313
return 0;
1414
}
15-
return (int)rc;
15+
return (int64_t)rc;
1616
}
1717

1818
int64_t cbm_zstd_decompress(const char *src, size_t srcLen, char *dst, size_t dstCap) {
@@ -31,6 +31,6 @@ size_t cbm_zstd_frame_content_size(const char *src, size_t srcLen) {
3131
return (size_t)n;
3232
}
3333

34-
size_t cbm_zstd_compress_bound(int inputSize) {
35-
return ZSTD_compressBound((size_t)inputSize);
34+
size_t cbm_zstd_compress_bound(size_t inputSize) {
35+
return ZSTD_compressBound(inputSize);
3636
}

‎internal/cbm/zstd_store.h‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
#include <stdint.h>
66

77
// Zstd compression at specified level (1=fast .. 22=best).
8-
// Returns compressed size on success, 0 on error.
9-
int cbm_zstd_compress(const char *src, int srcLen, char *dst, int dstCap, int level);
8+
// srcLen/dstCap are size_t for the same reason decompression's are: a >2 GiB
9+
// database would otherwise wrap through int and hand the encoder a capacity that
10+
// disagrees with the real buffer. Returns compressed size on success, 0 on error.
11+
int64_t cbm_zstd_compress(const char *src, size_t srcLen, char *dst, size_t dstCap, int level);
1012

1113
// Zstd decompression. srcLen/dstCap are size_t so a >2 GiB destination capacity
1214
// is never truncated (a large DB artifact, or a crafted one, would otherwise
@@ -20,6 +22,6 @@ int64_t cbm_zstd_decompress(const char *src, size_t srcLen, char *dst, size_t ds
2022
size_t cbm_zstd_frame_content_size(const char *src, size_t srcLen);
2123

2224
// Maximum compressed size bound for given input size.
23-
size_t cbm_zstd_compress_bound(int inputSize);
25+
size_t cbm_zstd_compress_bound(size_t inputSize);
2426

2527
#endif // CBM_ZSTD_STORE_H

‎scripts/smoke-test.sh‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,58 @@ if ! echo "$OUTPUT" | grep -qE 'v?[0-9]+\.[0-9]+|dev'; then
187187
fi
188188
echo "OK"
189189

190+
echo ""
191+
echo "=== Phase 1b: allocator override matches this platform's contract ==="
192+
# Asserts the SHIPPED binary's actual allocator wiring, per platform:
193+
#
194+
# Windows, Linux -> ordinary malloc MUST reach mimalloc (all size classes
195+
# owned). If it does not, every purge/reclaim option in
196+
# cbm_mem_init is decoration and freed pages stay committed
197+
# — that is #581, which hid in production for months
198+
# precisely because nothing asserted it on a real artifact.
199+
# macOS -> it MUST NOT. Enabling the override there aborts on the
200+
# first pointer crossing the two-level-namespace boundary
201+
# ("mi_free: invalid pointer"), so "owned" here would mean
202+
# we shipped a binary that crashes on index.
203+
#
204+
# Both directions fail. A silent flip either way is a release blocker, which is
205+
# why this lives in smoke (real artifact, all platforms) and not only in a unit
206+
# test built from source.
207+
ALLOC_LOG=$("$BINARY" cli list_projects 2>&1 >/dev/null || true)
208+
case "$(uname -s)" in
209+
Darwin)
210+
if echo "$ALLOC_LOG" | grep -q 'mem.allocator.not_owned'; then
211+
echo "FAIL: macOS emitted the not_owned WARNING; expected the by-design"
212+
echo " bound-populations INFO line (see #1360)"
213+
exit 1
214+
fi
215+
if echo "$ALLOC_LOG" | grep -q 'mem.allocator.owned'; then
216+
echo "FAIL: macOS reports ordinary malloc as allocator-owned. The override"
217+
echo " must stay OFF here: under the two-level namespace it aborts"
218+
echo " with 'mi_free: invalid pointer' on the first crossing pointer."
219+
exit 1
220+
fi
221+
echo "OK: macOS serves ordinary malloc from the system allocator, no warning"
222+
;;
223+
MINGW*|MSYS*|CYGWIN*|Linux)
224+
if echo "$ALLOC_LOG" | grep -q 'mem.allocator.not_owned'; then
225+
echo "FAIL: ordinary malloc does NOT reach mimalloc on $(uname -s)."
226+
echo " Allocator tuning is inert and freed pages will stay committed (#581/#1360)."
227+
echo "$ALLOC_LOG" | grep 'mem.allocator' | head -2
228+
exit 1
229+
fi
230+
if echo "$ALLOC_LOG" | grep -q 'mem.allocator.bound_populations_only'; then
231+
echo "FAIL: $(uname -s) reports bound-populations-only; the global override"
232+
echo " is expected to be compiled in on this platform (#1360)."
233+
exit 1
234+
fi
235+
echo "OK: ordinary malloc reaches the allocator on $(uname -s)"
236+
;;
237+
*)
238+
echo "SKIP: no allocator contract defined for $(uname -s)"
239+
;;
240+
esac
241+
190242
if [ "$SMOKE_MODE" != "--agent-config-only" ]; then
191243
echo ""
192244
echo "=== Phase 2: index test project ==="

‎src/cli/cli.c‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5294,6 +5294,53 @@ static bool cbm_install_subagent_reminder_script(const char *home, const char *b
52945294
return cbm_write_owned_hook_script_with_legacy(script_path, script, legacy, 1U);
52955295
}
52965296

5297+
/* #1387 dry-run predicate: would writing this hook script succeed, or would
5298+
* the owned-document migration refuse it because the file on disk is not ours?
5299+
* Read-only — it must never touch the filesystem, since a dry run promises
5300+
* exactly that. An unreadable/unsafe state counts as "would not succeed": the
5301+
* preview should warn rather than promise. */
5302+
static bool cbm_hook_script_write_would_succeed(const char *home, const char *binary_path,
5303+
const char *script_name) {
5304+
if (!home || !binary_path || !script_name) {
5305+
return false;
5306+
}
5307+
char config_dir[CLI_BUF_1K];
5308+
cbm_claude_config_dir(home, config_dir, sizeof(config_dir));
5309+
if (!config_dir[0]) {
5310+
return false;
5311+
}
5312+
char script_path[CLI_BUF_1K];
5313+
int written =
5314+
snprintf(script_path, sizeof(script_path), "%s/hooks/%s", config_dir, script_name);
5315+
if (written <= 0 || (size_t)written >= sizeof(script_path)) {
5316+
return false;
5317+
}
5318+
const char *prefix = cmm_gate_script_prefix;
5319+
if (strcmp(script_name, CMM_SESSION_REMINDER_SCRIPT) == 0) {
5320+
prefix = cmm_session_script_prefix;
5321+
} else if (strcmp(script_name, CMM_SUBAGENT_REMINDER_SCRIPT) == 0) {
5322+
prefix = cmm_subagent_script_prefix;
5323+
}
5324+
char script[CLI_BUF_8K];
5325+
if (cbm_build_current_hook_script(prefix, binary_path, script, sizeof(script)) != CLI_OK) {
5326+
return false;
5327+
}
5328+
/* Released shapes are accepted by the real write, so they must be accepted
5329+
* here too or the preview would warn about a script that upgrades fine. */
5330+
char released[CLI_BUF_8K];
5331+
const char *candidates[2];
5332+
size_t candidate_count = 0U;
5333+
if (strcmp(script_name, CMM_HOOK_GATE_SCRIPT) == 0 &&
5334+
cbm_build_released_gate_script(binary_path, released, sizeof(released)) == CLI_OK) {
5335+
candidates[candidate_count++] = released;
5336+
} else if (strcmp(script_name, CMM_SESSION_REMINDER_SCRIPT) == 0) {
5337+
candidates[candidate_count++] = cmm_released_session_script;
5338+
} else if (strcmp(script_name, CMM_SUBAGENT_REMINDER_SCRIPT) == 0) {
5339+
candidates[candidate_count++] = cmm_released_subagent_script;
5340+
}
5341+
return cbm_text_owned_document_status(script_path, script, candidates, candidate_count) == 0;
5342+
}
5343+
52975344
int cbm_upsert_claude_subagent_hooks(const char *settings_path) {
52985345
char command[CLI_BUF_8K];
52995346
char previous_command[CLI_BUF_8K];
@@ -7195,6 +7242,18 @@ static void install_claude_code_config(const char *home, const char *binary_path
71957242
bool gate_ok = dry_run;
71967243
bool session_ok = dry_run;
71977244
bool subagent_ok = dry_run;
7245+
if (dry_run) {
7246+
/* #1387 (second half): the dry run claimed EVERY hook group as
7247+
* installable because these flags were simply `true`. When the on-disk
7248+
* script is not ours the real install refuses to rewrite it, so the
7249+
* preview promised what the run could not deliver and the reporter had
7250+
* no way to see the loss coming. Predict each refusal read-only. */
7251+
gate_ok = cbm_hook_script_write_would_succeed(home, binary_path, CMM_HOOK_GATE_SCRIPT);
7252+
session_ok =
7253+
cbm_hook_script_write_would_succeed(home, binary_path, CMM_SESSION_REMINDER_SCRIPT);
7254+
subagent_ok =
7255+
cbm_hook_script_write_would_succeed(home, binary_path, CMM_SUBAGENT_REMINDER_SCRIPT);
7256+
}
71987257
if (!dry_run) {
71997258
char hook_path[CLI_BUF_1K];
72007259
gate_ok = cbm_install_hook_gate_script(home, binary_path);
@@ -7241,6 +7300,22 @@ static void install_claude_code_config(const char *home, const char *binary_path
72417300
if (subagent_ok) {
72427301
printf(" hooks: SubagentStart (MCP usage reminder for subagents)\n");
72437302
}
7303+
if (dry_run) {
7304+
/* Name every script the real run would refuse. Silence is what made
7305+
* #1387 invisible in advance: the group simply went unmentioned, which
7306+
* reads as "nothing to do" rather than "this will be skipped". */
7307+
static const char *const preview_scripts[] = {
7308+
CMM_HOOK_GATE_SCRIPT, CMM_SESSION_REMINDER_SCRIPT, CMM_SUBAGENT_REMINDER_SCRIPT};
7309+
const bool preview_ok[] = {gate_ok, session_ok, subagent_ok};
7310+
for (size_t i = 0U; i < sizeof(preview_scripts) / sizeof(preview_scripts[0]); i++) {
7311+
if (!preview_ok[i]) {
7312+
printf(" hooks: %s/hooks/%s would be skipped — the file there is not ours "
7313+
"(modified, or written by another install), so the rewrite is refused "
7314+
"and existing hook entries are left untouched\n",
7315+
config_dir, preview_scripts[i]);
7316+
}
7317+
}
7318+
}
72447319

72457320
/* Migration nudge: when CLAUDE_CONFIG_DIR is set and a legacy ~/.claude tree
72467321
* still exists, mention it so users can clean up stale artifacts. */

‎src/cli/config_text_edit.c‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,58 @@ static int text_matches_candidate(const char *data, size_t data_len, const char
12221222
return TEXT_OK;
12231223
}
12241224

1225+
/* Read-only classification: WOULD a migrate of this document succeed?
1226+
* Returns TEXT_OK when the path is absent or already byte-identical to the
1227+
* current or an exact released document (a write would proceed), TEXT_UNOWNED
1228+
* when it holds other bytes (a write would be refused), TEXT_ERROR on unsafe
1229+
* state. Mirrors text_migrate_owned_document's decision WITHOUT writing, so
1230+
* `install --dry-run` can predict a refusal instead of promising an install it
1231+
* cannot deliver (#1387). */
1232+
int cbm_text_owned_document_status(const char *file_path, const char *current_content,
1233+
const char *const *released_contents, size_t released_count) {
1234+
size_t current_len = 0U;
1235+
if (!text_valid_path(file_path) ||
1236+
text_bounded_strlen(current_content, TEXT_MAX_BYTES, &current_len) != TEXT_OK ||
1237+
text_validate_bytes(current_content, current_len, 1) != TEXT_OK ||
1238+
(released_count > 0U && !released_contents)) {
1239+
return TEXT_ERROR;
1240+
}
1241+
1242+
char *old_data = NULL;
1243+
size_t old_len = 0U;
1244+
text_file_snapshot_t snapshot;
1245+
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK ||
1246+
text_validate_bytes(old_data, old_len, 1) != TEXT_OK) {
1247+
free(old_data);
1248+
return TEXT_ERROR;
1249+
}
1250+
if (!snapshot.exists) {
1251+
free(old_data);
1252+
return TEXT_OK; /* absent -> the write creates it */
1253+
}
1254+
bool matches = false;
1255+
if (text_matches_candidate(old_data, old_len, current_content, &matches) != TEXT_OK) {
1256+
free(old_data);
1257+
return TEXT_ERROR;
1258+
}
1259+
if (matches) {
1260+
free(old_data);
1261+
return TEXT_OK;
1262+
}
1263+
for (size_t i = 0U; i < released_count; i++) {
1264+
if (text_matches_candidate(old_data, old_len, released_contents[i], &matches) != TEXT_OK) {
1265+
free(old_data);
1266+
return TEXT_ERROR;
1267+
}
1268+
if (matches) {
1269+
free(old_data);
1270+
return TEXT_OK; /* an exact released doc upgrades */
1271+
}
1272+
}
1273+
free(old_data);
1274+
return TEXT_UNOWNED;
1275+
}
1276+
12251277
static int text_migrate_owned_document(const char *file_path, const char *current_content,
12261278
const char *const *released_contents, size_t released_count,
12271279
int override_mode, unsigned int requested_mode) {

‎src/cli/config_text_edit.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ int cbm_text_ensure_owned_document(const char *file_path, const char *owned_cont
2929
/* Create current content, preserve it when already current, or atomically
3030
* upgrade only an exact byte-for-byte previously released document. Returns
3131
* 1 for user-modified/unowned content. */
32+
/* Read-only: would a migrate succeed? 0 = yes (absent/current/exact released),
33+
* 1 = refused (other bytes present), -1 = unsafe. Lets `install --dry-run`
34+
* predict a refusal instead of promising an install it cannot deliver. */
35+
int cbm_text_owned_document_status(const char *file_path, const char *current_content,
36+
const char *const *released_contents, size_t released_count);
3237
int cbm_text_migrate_owned_document(const char *file_path, const char *current_content,
3338
const char *const *released_contents, size_t released_count);
3439
/* As above, but publish the owned document with the requested low permission

‎src/daemon/bootstrap.c‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ cbm_daemon_process_role_t cbm_daemon_process_role(int argc, char *const argv[])
162162
"install",
163163
"uninstall",
164164
"update",
165+
/* allow-root writes one line of user-level config and reads nothing from a
166+
* project, so it needs no daemon. Listed here rather than routed through
167+
* the daemon so enrolling a root cannot depend on daemon state. */
168+
"allow-root",
165169
};
166170
/* Stop at the first top-level mode token. Tool names, flag values, and JSON
167171
* following `cli` are opaque user input: a search query named "install"

‎src/daemon/version_cohort.c‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,14 @@ cbm_version_cohort_status_t cbm_version_cohort_acquire(cbm_version_cohort_manage
457457
CBM_PRIVATE_FILE_LOCK_EX, &lease->lifetime);
458458
cbm_version_cohort_status_t status = version_cohort_status_from_lock(lock_status);
459459
if (status == CBM_VERSION_COHORT_OK) {
460+
/* No live holder: this participant CLAIMS the cohort and NO identity
461+
* comparison happens — a mismatched build is admitted, not refused.
462+
* That is correct when nothing is running, and it is the only path by
463+
* which a mismatched client can join. Name it: "was the lifetime lock
464+
* held?" is exactly what separates a local run (conflict raised) from
465+
* a CI run (client admitted), and it was not observable in any log. */
466+
cbm_log_info("version_cohort.claimed_unheld", "build",
467+
identity->build_fingerprint ? identity->build_fingerprint : "<null>");
460468
status = version_cohort_claim_new(lease, identity, deadline_ms);
461469
} else if (status == CBM_VERSION_COHORT_BUSY) {
462470
lock_status =

0 commit comments

Comments
 (0)