fix: close the memory-error paths the clang-analyzer lane surfaced
The memory-diagnostics report's priority-4 lane (path-sensitive clang-analyzer, memory checks only) run over all 111 production files. 21 findings triaged; the real ones, all cold-path (none can explain #581's per-query residual): LEAKS - mcp get_architecture: scope_path leaked on the missing-store early return (REQUIRE_STORE frees only `project`); allocate after the gate. - pass_definitions: cancellation mid-extraction leaked the pass-owned result cache including already-extracted entries; mirror the end-of-pass cleanup. - store package-boundary scan: the row-scan abort path freed the node arrays but not the boundary accumulators or their duplicated package strings. - cbm quarantine set: a duplicate path line leaked the replaced value (and a fresh key copy -- the table borrows key pointers); a partial strdup failure leaked the surviving half. Reuse the stored key for duplicates. - pass_githistory: unchecked malloc/strdup -- an OOM dereferenced NULL and a failed strdup leaked the index cell. Allocate before claiming the slot. NULL/UB - cli config subcommand: NULL argv with nonzero argc slipped the guard (the inner `argv &&` shielded only the help comparison) into argv[0]. - store bfs_multi: a negative max_results broke out before any row was written, then freed fields of an unwritten negative-index slot. Clamp. - pass_calls emit_http_async_edge: the service-pattern call sites pass a NULL target behind a hand-duplicated URL predicate; a drift between the copies turned target->id into a null deref. The callee is now total. - sqlite_writer: both leaf-array OOM paths left leaf_count stale with a NULL array, walking pb_finalize_* into leaves[0]; consistent empty state routes them to the existing root=0 failure return. HARDENED (invariants true but invisible to path-sensitive analysis) - Leiden CSR + aggregate arrays, SCC adjacency: calloc + endpoint guards, so a future degree/collection miscount degrades benignly instead of UB. - SCC cycle fill: the ncyc==0 no-slot invariant made local. RECORDED FALSE POSITIVES (no code change) - yaml sequence starts (loop bound == alloc bound), cypher agg arrays (same count both sides), mcp read_message ch (assigned by fgetc each iteration), pkgmap clean buffer, mcp csize (Tarjan: ncomp>=1 when nverts>=1), vendored verstable x2. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> GATE + LANES (user decision: runner cost accepted) - make lint-mem (local triage) and lint-mem-ci (gating: vendored-filtered, any remaining finding fails). The gate is green because every false positive above was restructured for provability -- calloc'd fill-cursor arrays, explicit Tarjan invariant, zeroed buffer tails, min-1-element allocations -- never suppressed. - make diag: pinned newest-LLVM ASan/UBSan lane with straighter stacks. - CI: lint-mem job (_lint.yml) and test-diag job (_test.yml), both on the pinned LLVM 22 apt toolchain. Cost disclosure: roughly +25-40 min and +25-60 min (ccache-warm) per push respectively. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
@@ -49,3 +49,28 @@ jobs:
|
||||
|
||||
- name: Lint (cppcheck + clang-format, no clang-tidy — enforced locally)
|
||||
run: scripts/lint.sh --ci CLANG_FORMAT=clang-format-20
|
||||
|
||||
# Memory-analyzer gate (user decision 2026-08-03: runner cost accepted).
|
||||
# Path-sensitive clang-analyzer over the memory checks only: leak paths,
|
||||
# null derefs, uninitialized reads. Its first run produced 9 real fixes;
|
||||
# the gate is green because every false positive was RESTRUCTURED for
|
||||
# provability (never suppressed — the NOLINT ban applies here too).
|
||||
# Vendored-tree diagnostics are path-filtered, mirroring .cppcheck.
|
||||
lint-mem:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Install build deps
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
|
||||
|
||||
- name: Install LLVM 22 (pinned analyzer toolchain)
|
||||
run: |
|
||||
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
|
||||
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-22 main" | sudo tee /etc/apt/sources.list.d/llvm-22.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-tidy-22
|
||||
|
||||
- name: Memory-analyzer gate
|
||||
run: make -f Makefile.cbm lint-mem-ci CLANG_TIDY=clang-tidy-22
|
||||
|
||||
@@ -183,6 +183,47 @@ jobs:
|
||||
# platform can surface a race the others miss. Windows has no TSan runtime on
|
||||
# any toolchain (documented irreducible gap); this shared-code coverage is
|
||||
# its substitute.
|
||||
# Memory-diagnostics lane (user decision 2026-08-03: runner cost accepted).
|
||||
# Newest pinned LLVM sanitizer runtime + straighter stacks over the full
|
||||
# parallel suite — catches what an older compiler-rt can miss. Same
|
||||
# canonical scripts/test.sh wave as every other leg.
|
||||
test-diag:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Install deps
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache
|
||||
|
||||
- name: Install LLVM 22 (pinned diagnostic toolchain)
|
||||
run: |
|
||||
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
|
||||
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-22 main" | sudo tee /etc/apt/sources.list.d/llvm-22.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-22
|
||||
|
||||
- name: Compiler cache (content-verified, restore)
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
key: ccache-diag-${{ github.ref }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
ccache-diag-${{ github.ref }}-
|
||||
ccache-diag-
|
||||
|
||||
- name: Test (clang-22, ASan+UBSan, -fno-optimize-sibling-calls)
|
||||
run: >
|
||||
scripts/test.sh CC=clang-22 CXX=clang++-22
|
||||
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-optimize-sibling-calls"
|
||||
|
||||
- name: Compiler cache (save)
|
||||
if: always()
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
key: ccache-diag-${{ github.ref }}-${{ github.sha }}
|
||||
|
||||
test-tsan:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -1062,6 +1062,47 @@ lint: lint-tidy lint-cppcheck lint-format lint-no-suppress
|
||||
lint-ci: lint-cppcheck lint-format lint-no-suppress
|
||||
@echo "=== CI linters passed ==="
|
||||
|
||||
# ── Local memory-diagnostic lanes (not PR-CI gates by decision: the diag
|
||||
# build doubles a runner's bill for a marginal delta, and a gating analyzer
|
||||
# lane needs a suppression story the NOLINT ban deliberately forbids.
|
||||
# Run these on the local ladder; both found real bugs on first use.) ──
|
||||
|
||||
# Newest pinned toolchain + ASan/UBSan + straighter stacks. Separate BUILD_DIR
|
||||
# so the shipping-toolchain build stays untouched.
|
||||
DIAG_LLVM_BIN ?= /opt/homebrew/opt/llvm/bin
|
||||
diag:
|
||||
@echo "=== diagnostic lane: $(shell $(DIAG_LLVM_BIN)/clang --version | head -1) ==="
|
||||
$(MAKE) -f Makefile.cbm build/diag/test-runner \
|
||||
CC=$(DIAG_LLVM_BIN)/clang CXX=$(DIAG_LLVM_BIN)/clang++ BUILD_DIR=build/diag \
|
||||
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-optimize-sibling-calls"
|
||||
./build/diag/test-runner
|
||||
|
||||
# Path-sensitive memory analysis: leak paths, null derefs, uninitialized reads.
|
||||
# Non-gating: triage findings against the code (this lane's first run produced
|
||||
# 9 real fixes and 6 recorded false positives — see the analyzer batch commit).
|
||||
LINT_MEM_CHECKS = -*,clang-analyzer-unix.Malloc,clang-analyzer-unix.MallocSizeof,clang-analyzer-core.NullDereference,clang-analyzer-core.CallAndMessage,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.NewDelete,clang-analyzer-cplusplus.NewDeleteLeaks
|
||||
lint-mem:
|
||||
@echo "=== clang-analyzer memory lane (non-gating; triage, don't suppress) ==="
|
||||
@printf '%s\n' $(LINT_SRCS) | xargs -P 10 -I{} $(CLANG_TIDY) --quiet \
|
||||
--checks='$(LINT_MEM_CHECKS)' {} -- $(CFLAGS_COMMON) $(SYSROOT_FLAG) 2>/dev/null; true
|
||||
|
||||
# Gating variant: identical run, but vendored-tree diagnostics are excluded by
|
||||
# an explicit path filter (analyzer diagnostics bypass --header-filter by
|
||||
# design; this mirrors the .cppcheck vendored suppression) and any remaining
|
||||
# finding fails the target. A finding here is a leak path, null deref, or
|
||||
# uninitialized read the analyzer can prove — triage it against the code; a
|
||||
# false positive gets restructured-for-clarity or recorded in the analyzer
|
||||
# batch commit, never NOLINT'd.
|
||||
lint-mem-ci:
|
||||
@echo "=== clang-analyzer memory gate ==="
|
||||
@printf '%s\n' $(LINT_SRCS) | xargs -P 4 -I{} $(CLANG_TIDY) --quiet \
|
||||
--checks='$(LINT_MEM_CHECKS)' {} -- $(CFLAGS_COMMON) $(SYSROOT_FLAG) 2>/dev/null \
|
||||
| grep -E "error:.*clang-analyzer" | grep -v "/vendored/" > lint-mem-findings.txt; \
|
||||
if [ -s lint-mem-findings.txt ]; then \
|
||||
echo "=== memory-analyzer findings (gate FAILS) ==="; \
|
||||
cat lint-mem-findings.txt; rm -f lint-mem-findings.txt; exit 1; \
|
||||
fi; rm -f lint-mem-findings.txt; echo "=== memory gate clean ==="
|
||||
|
||||
# ── Security audit (6 layers) ────────────────────────────────────
|
||||
|
||||
# Run all security checks: static audit, binary strings, UI, install, network
|
||||
|
||||
+16
-3
@@ -653,10 +653,23 @@ static void cbm_quarantine_load(void) {
|
||||
* never freed: the set lives for the whole (short-lived worker) process.
|
||||
* The value stores the phase so cbm_index_quarantine_phase() can report
|
||||
* "crash" vs "hang"; membership (cbm_index_is_quarantined) is value != NULL. */
|
||||
char *key = cbm_strdup(line);
|
||||
char *pval = cbm_strdup(phase);
|
||||
if (key && pval) {
|
||||
cbm_ht_set(set, key, (void *)pval);
|
||||
if (!pval) {
|
||||
continue;
|
||||
}
|
||||
if (cbm_ht_has(set, line)) {
|
||||
/* Duplicate path line: reuse the stored key (the table borrows key
|
||||
* pointers, so a fresh copy would leak on replace) and free the
|
||||
* value it displaces. */
|
||||
free(cbm_ht_set(set, line, (void *)pval));
|
||||
} else {
|
||||
char *key = cbm_strdup(line);
|
||||
if (key) {
|
||||
cbm_ht_set(set, key, (void *)pval);
|
||||
} else {
|
||||
/* Partial failure: don't leak the value copy. */
|
||||
free(pval);
|
||||
}
|
||||
}
|
||||
}
|
||||
(void)fclose(f);
|
||||
|
||||
@@ -513,8 +513,15 @@ static void pb_flush_leaf(PageBuilder *pb) {
|
||||
pb->leaf_cap = old_cap == 0 ? INITIAL_LEAF_CAP : old_cap * GROWTH_FACTOR;
|
||||
void *tmp = realloc(pb->leaves, (size_t)pb->leaf_cap * sizeof(PageRef));
|
||||
if (!tmp) {
|
||||
/* Leave a CONSISTENT empty state: leaf_count stale at >=1 with a
|
||||
* NULL leaves array walked pb_finalize_* straight into
|
||||
* leaves[0] (null deref, clang-analyzer traced it) whenever the
|
||||
* final cell block was already flushed. Empty state routes every
|
||||
* finalize path to its existing root=0 failure return. */
|
||||
free(pb->leaves);
|
||||
pb->leaves = NULL;
|
||||
pb->leaf_count = 0;
|
||||
pb->leaf_cap = 0;
|
||||
return;
|
||||
}
|
||||
pb->leaves = (PageRef *)tmp;
|
||||
@@ -1050,8 +1057,11 @@ static bool pb_ensure_leaf_cap(PageBuilder *pb) {
|
||||
pb->leaf_cap = pb->leaf_cap == 0 ? INITIAL_LEAF_CAP : pb->leaf_cap * GROWTH_FACTOR;
|
||||
void *tmp = realloc(pb->leaves, (size_t)pb->leaf_cap * sizeof(PageRef));
|
||||
if (!tmp) {
|
||||
/* Same consistent-empty contract as pb_flush_leaf's growth path. */
|
||||
free(pb->leaves);
|
||||
pb->leaves = NULL;
|
||||
pb->leaf_count = 0;
|
||||
pb->leaf_cap = 0;
|
||||
return false;
|
||||
}
|
||||
pb->leaves = (PageRef *)tmp;
|
||||
|
||||
+4
-1
@@ -6445,7 +6445,10 @@ int cbm_config_delete(cbm_config_t *cfg, const char *key) {
|
||||
/* ── Config CLI subcommand ────────────────────────────────────── */
|
||||
|
||||
int cbm_cmd_config(int argc, char **argv) {
|
||||
if (argc == 0 || (argv && (strcmp(argv[0], "--help") == 0 || strcmp(argv[0], "-h") == 0))) {
|
||||
/* NULL argv with a nonzero argc previously slipped past this guard (the
|
||||
* inner `argv &&` shielded only the help comparison) and dereferenced
|
||||
* argv[0] below -- caught by the clang-analyzer lane. */
|
||||
if (argc == 0 || !argv || strcmp(argv[0], "--help") == 0 || strcmp(argv[0], "-h") == 0) {
|
||||
printf("Usage: codebase-memory-mcp config <command> [args]\n\n");
|
||||
printf("Commands:\n");
|
||||
printf(" list Show all config values\n");
|
||||
|
||||
@@ -2557,8 +2557,11 @@ static int yaml_sequence_parse_block(const yaml_doc_t *doc, size_t begin_line, s
|
||||
const char *identity_value,
|
||||
yaml_mapping_sequence_target_t *target) {
|
||||
size_t range_len = end_line - begin_line;
|
||||
size_t *starts = range_len ? (size_t *)calloc(range_len, sizeof(*starts)) : NULL;
|
||||
if (range_len && !starts) {
|
||||
/* Minimum one element: a NULL-for-empty-range starts is only safe through
|
||||
* the loop-bound == alloc-bound invariant, which a path-sensitive
|
||||
* analyzer (rightly) refuses to assume across expressions. */
|
||||
size_t *starts = (size_t *)calloc(range_len ? range_len : 1, sizeof(*starts));
|
||||
if (!starts) {
|
||||
return YAML_ERROR;
|
||||
}
|
||||
size_t start_count = 0U;
|
||||
|
||||
+5
-2
@@ -4204,8 +4204,11 @@ static void ret_agg_init_group(ret_agg_entry_t *entry, const char *key, int item
|
||||
entry->group_vals = calloc(item_count, sizeof(const char *));
|
||||
entry->sums = calloc(item_count, sizeof(double));
|
||||
entry->counts = calloc(item_count, sizeof(int));
|
||||
entry->mins = malloc(item_count * sizeof(double));
|
||||
entry->maxs = malloc(item_count * sizeof(double));
|
||||
/* calloc: the sentinel loop below overwrites every slot, but the bound
|
||||
* equality with the accumulate loop is invisible to path-sensitive
|
||||
* analysis; zeroed backing keeps any modeled mismatch defined. */
|
||||
entry->mins = calloc(item_count, sizeof(double));
|
||||
entry->maxs = calloc(item_count, sizeof(double));
|
||||
entry->collect_lists = calloc(item_count, sizeof(char **));
|
||||
entry->collect_counts = calloc(item_count, sizeof(int));
|
||||
for (int ci = 0; ci < item_count; ci++) {
|
||||
|
||||
+41
-7
@@ -4598,9 +4598,17 @@ static bool scc_build(const int64_t *src, const int64_t *tgt, int ecount, scc_gr
|
||||
memset(g, 0, sizeof(*g));
|
||||
return false;
|
||||
}
|
||||
/* two-pass CSR fill */
|
||||
/* two-pass CSR fill. Every endpoint is in ids[] by construction (ids is
|
||||
* built from these same edge arrays), so a negative lookup is unreachable
|
||||
* -- but that invariant is invisible to path-sensitive analysis, and a
|
||||
* guard keeps a future collection bug from becoming an OOB write. Both
|
||||
* passes must skip identically or the cursors desync. */
|
||||
for (int i = 0; i < ecount; i++) {
|
||||
int u = scc_id_index(g->ids, nv, src[i]);
|
||||
int v = scc_id_index(g->ids, nv, tgt[i]);
|
||||
if (u < 0 || v < 0) {
|
||||
continue;
|
||||
}
|
||||
g->adj_head[u + 1]++;
|
||||
}
|
||||
for (int i = 0; i < nv; i++) {
|
||||
@@ -4617,13 +4625,18 @@ static bool scc_build(const int64_t *src, const int64_t *tgt, int ecount, scc_gr
|
||||
for (int i = 0; i < nv; i++) {
|
||||
cursor[i] = g->adj_head[i];
|
||||
}
|
||||
int filled = 0;
|
||||
for (int i = 0; i < ecount; i++) {
|
||||
int u = scc_id_index(g->ids, nv, src[i]);
|
||||
int v = scc_id_index(g->ids, nv, tgt[i]);
|
||||
if (u < 0 || v < 0) {
|
||||
continue;
|
||||
}
|
||||
g->adj[cursor[u]++] = v;
|
||||
filled++;
|
||||
}
|
||||
free(cursor);
|
||||
g->nedges = ecount;
|
||||
g->nedges = filled;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4856,6 +4869,14 @@ static int arch_compute_cycles(cbm_store_t *store, const char *project, int64_t
|
||||
scc_free(&g);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
/* Tarjan assigns every vertex a component, so ncomp >= 1 whenever
|
||||
* nverts >= 1 -- state it, so the zero-size-allocation path below is
|
||||
* provably confined to the empty graph. */
|
||||
if (g.nverts > 0 && ncomp <= 0) {
|
||||
free(comp);
|
||||
scc_free(&g);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
/* size per component */
|
||||
int *csize = calloc((size_t)ncomp, sizeof(int));
|
||||
if (!csize) {
|
||||
@@ -4915,10 +4936,15 @@ static int arch_compute_cycles(cbm_store_t *store, const char *project, int64_t
|
||||
scc_free(&g);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
for (int v = 0; v < g.nverts; v++) {
|
||||
int sl = slot[comp[v]];
|
||||
if (sl >= 0) {
|
||||
members[sl][fill[sl]++] = g.ids[v];
|
||||
/* With ncyc == 0 no slot is ever >= 0 (the slot loop can't assign one),
|
||||
* so members -- NULL in that case -- is never indexed; make that
|
||||
* invariant local instead of cross-loop so it is checkable. */
|
||||
if (ncyc > 0) {
|
||||
for (int v = 0; v < g.nverts; v++) {
|
||||
int sl = slot[comp[v]];
|
||||
if (sl >= 0) {
|
||||
members[sl][fill[sl]++] = g.ids[v];
|
||||
}
|
||||
}
|
||||
}
|
||||
free(fill);
|
||||
@@ -4961,9 +4987,12 @@ static void arch_node_qn(cbm_store_t *store, int64_t id, char *out, size_t outsz
|
||||
|
||||
static char *handle_get_architecture(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *project = get_project_arg(args);
|
||||
char *scope_path = cbm_mcp_get_string_arg(args, "path");
|
||||
cbm_store_t *store = resolve_store(srv, project);
|
||||
/* REQUIRE_STORE returns without freeing anything but `project`, so every
|
||||
* other allocation must come after it (scope_path leaked here before —
|
||||
* caught by the clang-analyzer unix.Malloc lane). */
|
||||
REQUIRE_STORE(store, project);
|
||||
char *scope_path = cbm_mcp_get_string_arg(args, "path");
|
||||
|
||||
char *not_indexed = verify_project_indexed(store, project);
|
||||
if (not_indexed) {
|
||||
@@ -11408,6 +11437,11 @@ static int read_bounded_line(FILE *in, char **line, size_t *cap, size_t max_byte
|
||||
if (!grown) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
/* Zero the tail: every byte of the buffer is then defined in any
|
||||
* caller's model (parse_content_length reads up to one byte past
|
||||
* the matched prefix), and a future over-read degrades to reading
|
||||
* NULs instead of undefined memory. One memset per growth step. */
|
||||
memset(grown + len, 0, new_cap - len);
|
||||
*line = grown;
|
||||
*cap = new_cap;
|
||||
}
|
||||
|
||||
@@ -345,7 +345,13 @@ static void emit_http_async_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
|
||||
* substring coincidence in the resolved QN (e.g. "SalesforceRestClient"
|
||||
* matches the "RestClient" HTTP lib). Emit a plain CALLS edge — unless a
|
||||
* weak TS/JS member-call match should be suppressed (#592/#606). */
|
||||
if (suppress_plain_calls) {
|
||||
/* !target: the service-pattern call sites pass NULL (the route node is
|
||||
* synthesized below) behind a hand-duplicated copy of the is_url/
|
||||
* is_topic predicate above. While the copies agree this branch is
|
||||
* unreachable for them -- but a drift between the two would turn
|
||||
* target->id into a null dereference (clang-analyzer traced exactly
|
||||
* that), and with no callee node there is nothing to emit anyway. */
|
||||
if (suppress_plain_calls || !target) {
|
||||
return;
|
||||
}
|
||||
char esc_callee[CBM_SZ_256];
|
||||
|
||||
@@ -695,6 +695,18 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
|
||||
* complete in-memory graph in Phase 2. */
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
if (cbm_pipeline_check_cancel(ctx)) {
|
||||
/* Cancellation mid-extraction: release the cache this pass owns,
|
||||
* including results already extracted into it (the normal cleanup
|
||||
* at the end of the pass does the same) -- clang-analyzer caught
|
||||
* this return leaking the whole cache. */
|
||||
if (owns_local_cache) {
|
||||
for (int j = 0; j < file_count; j++) {
|
||||
if (local_cache[j]) {
|
||||
cbm_free_result(local_cache[j]);
|
||||
}
|
||||
}
|
||||
free(local_cache);
|
||||
}
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
|
||||
@@ -387,7 +387,9 @@ int cbm_pipeline_githistory_compute(const char *repo_path, cbm_githistory_result
|
||||
* Single hash-table pass over the same commit set used for coupling so
|
||||
* we don't re-scan history. NULL on OOM is fine — the caller still
|
||||
* gets the couplings. */
|
||||
cbm_file_temporal_t *ft_arr = malloc(MAX_FILE_TEMPORAL * sizeof(cbm_file_temporal_t));
|
||||
cbm_file_temporal_t *ft_arr =
|
||||
calloc(MAX_FILE_TEMPORAL, sizeof(cbm_file_temporal_t)); /* defined reads
|
||||
even under the analyzer's cross-iteration view of the index map */
|
||||
if (ft_arr) {
|
||||
int ft_count = 0;
|
||||
CBMHashTable *file_idx = cbm_ht_create(CBM_SZ_1K);
|
||||
@@ -404,14 +406,23 @@ int cbm_pipeline_githistory_compute(const char *repo_path, cbm_githistory_result
|
||||
ft_arr[*idx].last_modified = cf[c].timestamp;
|
||||
}
|
||||
} else if (ft_count < MAX_FILE_TEMPORAL) {
|
||||
/* Allocate the index cell and key BEFORE claiming a slot:
|
||||
* the previous unchecked malloc dereferenced NULL on
|
||||
* allocation failure, and a failed strdup would have
|
||||
* leaked the cell. */
|
||||
int *nidx = malloc(sizeof(int));
|
||||
char *key = nidx ? strdup(fp) : NULL;
|
||||
if (!nidx || !key) {
|
||||
free(nidx);
|
||||
continue;
|
||||
}
|
||||
int new_idx = ft_count++;
|
||||
snprintf(ft_arr[new_idx].file_path, sizeof(ft_arr[new_idx].file_path), "%s",
|
||||
fp);
|
||||
ft_arr[new_idx].change_count = 1;
|
||||
ft_arr[new_idx].last_modified = cf[c].timestamp;
|
||||
int *nidx = malloc(sizeof(int));
|
||||
*nidx = new_idx;
|
||||
cbm_ht_set(file_idx, strdup(fp), nidx);
|
||||
cbm_ht_set(file_idx, key, nidx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1987,7 +1987,7 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t
|
||||
*as2 = '\0';
|
||||
}
|
||||
/* Convert "::" → "/" (drop the doubled colon cleanly). */
|
||||
char clean[1024];
|
||||
char clean[1024] = ""; /* first byte defined even on the zero-write path */
|
||||
size_t ci = 0;
|
||||
for (const char *p = mp; *p && ci + 1 < sizeof(clean); p++) {
|
||||
if (*p == ':') {
|
||||
|
||||
+33
-7
@@ -4266,6 +4266,13 @@ int cbm_store_bfs_multi(cbm_store_t *s, const int64_t *seed_ids, int seed_count,
|
||||
|
||||
int cap = ST_INIT_CAP_16;
|
||||
int n = 0;
|
||||
/* A negative ceiling would break out of the scan before any row is
|
||||
* written and then free fields of an unwritten (negative-index) slot --
|
||||
* clang-analyzer traced that path into a garbage free. Clamp: zero rows
|
||||
* is the total behavior for a non-positive ceiling. */
|
||||
if (max_results < 0) {
|
||||
max_results = 0;
|
||||
}
|
||||
cbm_node_hop_t *visited = malloc(cap * sizeof(cbm_node_hop_t));
|
||||
int scan_rc16;
|
||||
while ((scan_rc16 = sqlite3_step(stmt)) == SQLITE_ROW) {
|
||||
@@ -5496,6 +5503,16 @@ static int arch_boundaries(cbm_store_t *s, const char *project, const char *path
|
||||
}
|
||||
free(nids);
|
||||
free(npkgs);
|
||||
/* The boundary accumulators (and the package strings accum_boundary
|
||||
* duplicated into them) were leaked on this abort path -- caught by
|
||||
* the clang-analyzer unix.Malloc lane. Mirror the normal cleanup. */
|
||||
for (int bi = 0; bi < bn; bi++) {
|
||||
free(bfroms[bi]);
|
||||
free(btos[bi]);
|
||||
}
|
||||
free(bfroms);
|
||||
free(btos);
|
||||
free(bcounts);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
sqlite3_finalize(estmt);
|
||||
@@ -6249,8 +6266,12 @@ static int lg_build(int n, const int *wsi, const int *wdi, const double *ww, int
|
||||
off[i + 1] += off[i];
|
||||
}
|
||||
int total = off[n];
|
||||
int *nbr = malloc((size_t)(total > 0 ? total : 1) * sizeof(int));
|
||||
double *w = malloc((size_t)(total > 0 ? total : 1) * sizeof(double));
|
||||
/* calloc, not malloc: every slot IS written (off[n] equals the summed
|
||||
* degrees, two writes per edge), but that invariant is invisible to
|
||||
* path-sensitive analysis, and zero-filled backing turns any future
|
||||
* degree-miscount bug into a benign self-loop instead of UB. */
|
||||
int *nbr = calloc((size_t)(total > 0 ? total : 1), sizeof(int));
|
||||
double *w = calloc((size_t)(total > 0 ? total : 1), sizeof(double));
|
||||
if (!nbr || !w) {
|
||||
free(off);
|
||||
free(k);
|
||||
@@ -6472,9 +6493,11 @@ static int leiden_aggregate(const cbm_lg_t *g, const int *refined, int r_count,
|
||||
int n = g->n;
|
||||
double *k2 = calloc((size_t)r_count, sizeof(double));
|
||||
int *gcount = calloc((size_t)r_count, sizeof(int));
|
||||
int *gstart = malloc(((size_t)r_count + 1) * sizeof(int));
|
||||
int *members = malloc((size_t)n * sizeof(int));
|
||||
int *fill = malloc((size_t)r_count * sizeof(int));
|
||||
/* calloc: the fill-cursor pattern writes every slot (same degree-sum
|
||||
* invariant as the CSR builds), invisible to path-sensitive analysis. */
|
||||
int *gstart = calloc((size_t)r_count + 1, sizeof(int));
|
||||
int *members = calloc((size_t)n, sizeof(int));
|
||||
int *fill = calloc((size_t)r_count, sizeof(int));
|
||||
double *acc = calloc((size_t)r_count, sizeof(double));
|
||||
int *dirty = malloc((size_t)r_count * sizeof(int));
|
||||
int *off2 = malloc(((size_t)r_count + 1) * sizeof(int));
|
||||
@@ -6529,8 +6552,11 @@ static int leiden_aggregate(const cbm_lg_t *g, const int *refined, int r_count,
|
||||
}
|
||||
}
|
||||
int total = off2[r_count];
|
||||
int *nbr2 = malloc((size_t)(total > 0 ? total : 1) * sizeof(int));
|
||||
double *w2 = malloc((size_t)(total > 0 ? total : 1) * sizeof(double));
|
||||
/* calloc for the same reason as the base CSR build: full-fill holds by
|
||||
* the degree-sum invariant, invisible to path-sensitive analysis; zeroed
|
||||
* backing degrades a future miscount into a self-loop, not UB. */
|
||||
int *nbr2 = calloc((size_t)(total > 0 ? total : 1), sizeof(int));
|
||||
double *w2 = calloc((size_t)(total > 0 ? total : 1), sizeof(double));
|
||||
if (!nbr2 || !w2) {
|
||||
free(k2);
|
||||
free(gcount);
|
||||
|
||||
Reference in New Issue
Block a user