Skip to content
Merged
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
6 changes: 5 additions & 1 deletion Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -1601,8 +1601,12 @@ fuzz:
$(MAKE) -f Makefile.cbm build/fuzz/cbm-fuzz BUILD_DIR=build/fuzz \
CC=$(FUZZ_CC) CXX=$(FUZZ_CXX) SANITIZE="$(FUZZ_SANITIZE)"

# -fsanitize-link-c++-runtime: the link runs through the C driver, which on
# Linux leaves out UBSan's C++ runtime, but the C++ objects (preprocessor,
# simplecpp) carry -fsanitize=vptr checks that need it
# (__ubsan_vptr_type_cache, __ubsan_handle_dynamic_type_cache_miss).
$(BUILD_DIR)/cbm-fuzz: $(FUZZ_SRC) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_TEST) -fsanitize=fuzzer -o $@ \
$(CC) $(CFLAGS_TEST) -fsanitize=fuzzer -fsanitize-link-c++-runtime -o $@ \
$(FUZZ_SRC) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_TEST) \
Expand Down
13 changes: 12 additions & 1 deletion scripts/memwaste.sh
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,19 @@ cleanup() {
}
trap cleanup EXIT

# One explicit memory budget on every venue. Left to the daemon, the budget
# follows the machine's RAM: 1792 MB on the 7 GB macos-14 runner, about 4 GB on
# the 16 GB Linux and Windows runners. The event layer itself holds 0.5-1 GB, so
# on macOS the index stopped with over_memory_budget before it measured anything
# (dry runs 2026-10-04 and 2026-10-12). A venue's RAM must not decide whether
# the report exists. 4096 is what the larger runners already derived; the index
# completes under it on macOS with the runner's 3 workers. CBM_MEM_BUDGET_MB
# still overrides.
MEMWASTE_BUDGET_MB="${CBM_MEM_BUDGET_MB:-4096}"

INDEX_RC=0
CBM_MEMWASTE=1 CBM_MEMWASTE_OUT="$DUMP_ENV" CBM_CACHE_DIR="$CACHE_DIR" CBM_RUNTIME_DIR="$RUNTIME" \
CBM_MEM_BUDGET_MB="$MEMWASTE_BUDGET_MB" \
CBM_MEMWASTE=1 CBM_MEMWASTE_OUT="$DUMP_ENV" CBM_CACHE_DIR="$CACHE_DIR" CBM_RUNTIME_DIR="$RUNTIME" \
CBM_LOG_LEVEL=error "$RUN_BIN" cli index_repository --repo-path "$REPO" --mode full \
>"$OUT/index.json" || INDEX_RC=$?
if [ "$INDEX_RC" -ne 0 ]; then
Expand Down
14 changes: 10 additions & 4 deletions scripts/security-strings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -274,11 +274,17 @@ echo "--- Credential pattern detection ---"
CRED_PATTERNS='password=|secret=|api_key=|apikey=|auth_token=|private_key='
if grep -iE "$CRED_PATTERNS" "$STRINGS_FILE" > "$SEC_CREDS" 2>/dev/null; then
# Monaco 0.56.0 quick-input password mode is a boolean/UI property, not a
# credential. Reviewed full lines: assets/index-B4p3fUhm.js:938 (15,643
# bytes) and :940 (72,461 bytes). These hashes exempt no command or URL.
# credential. Reviewed full lines of the graph-ui bundle as CI builds it
# since #2068: 15,644 and 72,472 bytes; every match is the input box's
# password-mode flag (_password=!1, password=!!e.password,
# inputBox.password=this.password, inputBox.password=!1). The same two lines
# appear on every platform. These hashes exempt no command or URL.
# A rebuilt bundle changes these bytes: the smoke log prints the lines,
# re-review them and replace both hashes (the 09-09 pair for
# assets/index-B4p3fUhm.js stopped matching with #2068).
filter_reviewed_lines "$SEC_CREDS" \
91e70ce96dc9392c3091e0e06a838ad43a0c625b007e9e2ff6256a6d4824e9f2 \
6e393242a92967b3d1bd03420d0bdfc42b80f18407ef393652580483d2177633
8b4df6dea02b9433ac5b7ddb9f186c0f38c9f016fa8c610aa9ef987ae4bd81cf \
e4ef9bacb65321a35ae24f154aede5ea157d7c2f7e084c1c6adcd4d33b520325
fi
if [ -s "$SEC_CREDS" ]; then
echo "BLOCKED: Credential patterns found in binary:"
Expand Down
50 changes: 33 additions & 17 deletions src/cypher/cypher.c
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,15 @@ static const cbm_token_t *expect(parser_t *p, cbm_token_type_t type) {
return NULL;
}

/* Free the first n entries of a property array and the array itself. */
static void free_prop_entries(cbm_prop_filter_t *arr, int n) {
for (int i = 0; i < n; i++) {
safe_str_free(&arr[i].key);
safe_str_free(&arr[i].value);
}
free(arr);
}

/* Parse inline properties: {key: "value", ...} */
static int parse_props(parser_t *p, cbm_prop_filter_t **out, int *count) {
*out = NULL;
Expand All @@ -521,30 +530,27 @@ static int parse_props(parser_t *p, cbm_prop_filter_t **out, int *count) {
}

while (!check(p, TOK_RBRACE) && !check(p, TOK_EOF)) {
/* Every failure frees the entries already parsed, not only the array. */
const cbm_token_t *key = expect(p, TOK_IDENT);
if (!key) {
free(arr);
free_prop_entries(arr, n);
return CBM_NOT_FOUND;
}
if (!expect(p, TOK_COLON)) {
free(arr);
free_prop_entries(arr, n);
return CBM_NOT_FOUND;
}
const cbm_token_t *val = expect(p, TOK_STRING);
if (!val) {
free(arr);
free_prop_entries(arr, n);
return CBM_NOT_FOUND;
}

if (n >= cap) {
int new_cap = cap * PAIR_LEN;
void *tmp = realloc(arr, new_cap * sizeof(cbm_prop_filter_t));
if (!tmp) {
for (int i = 0; i < n; i++) {
safe_str_free(&arr[i].key);
safe_str_free(&arr[i].value);
}
free(arr);
free_prop_entries(arr, n);
return CBM_NOT_FOUND;
}
arr = tmp;
Expand All @@ -555,11 +561,7 @@ static int parse_props(parser_t *p, cbm_prop_filter_t **out, int *count) {
if (!new_key || !new_val) {
safe_str_free(&new_key);
safe_str_free(&new_val);
for (int i = 0; i < n; i++) {
safe_str_free(&arr[i].key);
safe_str_free(&arr[i].value);
}
free(arr);
free_prop_entries(arr, n);
return CBM_NOT_FOUND;
}
arr[n].key = new_key;
Expand Down Expand Up @@ -1864,16 +1866,23 @@ static int parse_return(parser_t *p, cbm_return_clause_t **out) {
return parse_return_or_with(p, out, false);
}

/* Parse a single MATCH pattern into pat */
static void free_pattern(cbm_pattern_t *pat);

/* Parse a single MATCH pattern into pat. On failure pat is freed and zeroed:
* the caller counts a pattern only after it parsed, so a partial one would
* otherwise leak (a named path "p = (...)" failed this way on every query). */
static int parse_match_pattern(parser_t *p, cbm_pattern_t *pat) {
memset(pat, 0, sizeof(*pat));
int node_cap = CYP_INIT_CAP4;
int rel_cap = CYP_INIT_CAP4;
pat->nodes = malloc(node_cap * sizeof(cbm_node_pattern_t));
pat->rels = calloc(rel_cap, sizeof(cbm_rel_pattern_t));

/* A failed parse_node / parse_rel leaves its element zeroed except for
* what it allocated, so counting that element frees its partial state. */
if (parse_node(p, &pat->nodes[0]) < 0) {
return CBM_NOT_FOUND;
pat->node_count = SKIP_ONE;
goto fail;
}
pat->node_count = SKIP_ONE;

Expand All @@ -1883,7 +1892,8 @@ static int parse_match_pattern(parser_t *p, cbm_pattern_t *pat) {
pat->rels = safe_realloc(pat->rels, rel_cap * sizeof(cbm_rel_pattern_t));
}
if (parse_rel(p, &pat->rels[pat->rel_count]) < 0) {
return CBM_NOT_FOUND;
pat->rel_count++;
goto fail;
}
pat->rel_count++;

Expand All @@ -1892,11 +1902,17 @@ static int parse_match_pattern(parser_t *p, cbm_pattern_t *pat) {
pat->nodes = safe_realloc(pat->nodes, node_cap * sizeof(cbm_node_pattern_t));
}
if (parse_node(p, &pat->nodes[pat->node_count]) < 0) {
return CBM_NOT_FOUND;
pat->node_count++;
goto fail;
}
pat->node_count++;
}
return 0;

fail:
free_pattern(pat);
memset(pat, 0, sizeof(*pat));
return CBM_NOT_FOUND;
}

/* Parse UNWIND [...] AS var clause into query */
Expand Down
6 changes: 5 additions & 1 deletion src/foundation/sha256.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
#if defined(__clang__)
#define SHA256_ARM_TARGET __attribute__((target("sha2")))
#else
#define SHA256_ARM_TARGET __attribute__((target("+sha2")))
/* "+crypto", not "+sha2": GCC 11's arm_neon.h declares the SHA2 intrinsics
* under "+nothing+crypto", so a "+sha2" function cannot inline them ("target
* specific option mismatch"). "+crypto" implies "+sha2" on every GCC; the
* function uses only SHA2 instructions and the runtime HWCAP check gates it. */
#define SHA256_ARM_TARGET __attribute__((target("+crypto")))
#endif
#elif defined(__x86_64__) && (defined(__clang__) || defined(__GNUC__))
#define SHA256_HW_X86 1
Expand Down
30 changes: 30 additions & 0 deletions tests/test_cypher.c
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,35 @@ TEST(cypher_parse_error) {
PASS();
}

/* A pattern that fails partway frees what it already parsed. Each query
* fails at a different point; under LeakSanitizer the first four leaked
* before the fix (the first is the fuzz seed that found it), and the fifth
* reaches the same property-list failure through a second MATCH. */
TEST(cypher_parse_error_frees_partial_pattern) {
static const char *const queries[] = {
/* named path: unsupported, fails at the first node */
"MATCH p = (a:Class)-[:INHERITS*1..3]->(b:Class) RETURN a.name, length(p)",
/* property list fails after one parsed entry */
"MATCH (a:Function {name: \"x\", 7}) RETURN a",
/* relationship cut off after its variable */
"MATCH (a:Function)-[r:CALLS",
/* second node's label missing, after a parsed relationship */
"MATCH (a:Function)-[:CALLS]->(b:",
/* a second MATCH clause's pattern fails inside its properties */
"MATCH (a:Function) MATCH (b:Function {name: \"y\" 5}) RETURN a",
};
for (size_t i = 0; i < sizeof(queries) / sizeof(queries[0]); i++) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse(queries[i], &q, &err);
ASSERT_EQ(rc, -1);
ASSERT_NOT_NULL(err);
free(err);
cbm_query_free(q);
}
PASS();
}

/* ══════════════════════════════════════════════════════════════════
* EXECUTION TESTS (end-to-end against store)
* ══════════════════════════════════════════════════════════════════ */
Expand Down Expand Up @@ -5116,6 +5145,7 @@ SUITE(cypher) {
RUN_TEST(cypher_parse_return_distinct);
RUN_TEST(cypher_parse_inline_props);
RUN_TEST(cypher_parse_error);
RUN_TEST(cypher_parse_error_frees_partial_pattern);
/* Execution */
RUN_TEST(cypher_exec_deadline_aborts_runaway_query_issue601);
RUN_TEST(cypher_exec_deadline_allows_normal_query_issue601);
Expand Down
Loading