From 7814dacf15e68d8f89951b23c366bc070c43abb1 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Thu, 6 Aug 2026 16:06:43 +0200 Subject: [PATCH] fix: size the compress path with size_t and prune the cache from the walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two loose ends from the bounds work. The decompression side already took size_t so a >2 GiB capacity could not wrap through int, but compression still took int for both the source length and the destination capacity, and the artifact export cast a size_t database size down to reach it. A database past 2 GiB would have handed the encoder a negative length. Both lengths and the bound helper are size_t now, and the function returns int64_t like its decompressing counterpart. The discovery walk now prunes the cache directory by absolute path. A custom CBM_CACHE_DIR may sit inside a repository — tests do it routinely — and walking into it pulls every other project's graph database into this project's file list. This is the narrow form of a concern that was briefly implemented as refusing any root that contained the cache; refusing a whole root was too blunt, and not walking the cache is what the concern actually asks for. The test fails without the prune: a .go file planted under the cache is otherwise discovered, and "cache" is not in the built-in skip list, so the assertion is not vacuous. Signed-off-by: Martin Vogel --- internal/cbm/zstd_store.c | 10 +++++----- internal/cbm/zstd_store.h | 8 +++++--- src/discover/discover.c | 28 ++++++++++++++++++++++++++- src/pipeline/artifact.c | 4 ++-- tests/test_discover.c | 40 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 11 deletions(-) diff --git a/internal/cbm/zstd_store.c b/internal/cbm/zstd_store.c index 509537419..9a1001040 100644 --- a/internal/cbm/zstd_store.c +++ b/internal/cbm/zstd_store.c @@ -7,12 +7,12 @@ #include #include -int cbm_zstd_compress(const char *src, int srcLen, char *dst, int dstCap, int level) { - size_t rc = ZSTD_compress(dst, (size_t)dstCap, src, (size_t)srcLen, level); +int64_t cbm_zstd_compress(const char *src, size_t srcLen, char *dst, size_t dstCap, int level) { + size_t rc = ZSTD_compress(dst, dstCap, src, srcLen, level); if (ZSTD_isError(rc)) { return 0; } - return (int)rc; + return (int64_t)rc; } 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) { return (size_t)n; } -size_t cbm_zstd_compress_bound(int inputSize) { - return ZSTD_compressBound((size_t)inputSize); +size_t cbm_zstd_compress_bound(size_t inputSize) { + return ZSTD_compressBound(inputSize); } diff --git a/internal/cbm/zstd_store.h b/internal/cbm/zstd_store.h index ef427df04..42381649f 100644 --- a/internal/cbm/zstd_store.h +++ b/internal/cbm/zstd_store.h @@ -5,8 +5,10 @@ #include // Zstd compression at specified level (1=fast .. 22=best). -// Returns compressed size on success, 0 on error. -int cbm_zstd_compress(const char *src, int srcLen, char *dst, int dstCap, int level); +// srcLen/dstCap are size_t for the same reason decompression's are: a >2 GiB +// database would otherwise wrap through int and hand the encoder a capacity that +// disagrees with the real buffer. Returns compressed size on success, 0 on error. +int64_t cbm_zstd_compress(const char *src, size_t srcLen, char *dst, size_t dstCap, int level); // Zstd decompression. srcLen/dstCap are size_t so a >2 GiB destination capacity // 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 size_t cbm_zstd_frame_content_size(const char *src, size_t srcLen); // Maximum compressed size bound for given input size. -size_t cbm_zstd_compress_bound(int inputSize); +size_t cbm_zstd_compress_bound(size_t inputSize); #endif // CBM_ZSTD_STORE_H diff --git a/src/discover/discover.c b/src/discover/discover.c index 15243d3b6..76896b98b 100644 --- a/src/discover/discover.c +++ b/src/discover/discover.c @@ -13,6 +13,7 @@ #include "foundation/constants.h" #include "foundation/compat_fs.h" +#include "foundation/workspace.h" #include "foundation/platform.h" #ifdef _WIN32 #include "foundation/win_utf8.h" @@ -562,6 +563,30 @@ static bool is_safety_core_dir(const char *name) { } /* Check if a directory entry should be skipped (hardcoded dirs + gitignore). */ +/* The cache directory holds every indexed project's graph database. When a custom + * CBM_CACHE_DIR sits inside a repository — which happens in tests and is legal in + * production — walking into it would pull other projects' databases into this + * project's file list. Prune it by absolute path. + * + * This is the narrow remedy for a concern that was briefly implemented as + * refusing any root containing the cache: refusing a whole root was too blunt, + * and not walking the cache is what the concern actually asks for. */ +static bool dir_is_cache_tree(const char *abs_path) { + const char *cache = cbm_workspace_cache_dir(); + if (!cache || !cache[0] || !abs_path || !abs_path[0]) { + return false; + } + size_t n = strlen(cache); + while (n > 1 && (cache[n - 1] == '/' || cache[n - 1] == '\\')) { + n--; + } + if (strncmp(abs_path, cache, n) != 0) { + return false; + } + /* Boundary-aware so "x" is not treated as living under "". */ + return abs_path[n] == '\0' || abs_path[n] == '/' || abs_path[n] == '\\'; +} + static bool should_skip_directory(const char *entry_name, const char *rel_path, const cbm_discover_opts_t *opts, const cbm_gitignore_t *gitignore, const cbm_gitignore_t *global_gi, @@ -853,7 +878,8 @@ static void walk_dir_process_entry(cbm_dirent_t *entry, const walk_frame_t *fram } if (S_ISDIR(st.st_mode)) { - if (!should_skip_directory(entry->name, rel_path, opts, gitignore, global_gi, cbmignore, + if (!dir_is_cache_tree(abs_path) && + !should_skip_directory(entry->name, rel_path, opts, gitignore, global_gi, cbmignore, frame->local_gi, frame->local_gi_prefix)) { walk_push_subdir(ws, abs_path, rel_path, frame, out); } else { diff --git a/src/pipeline/artifact.c b/src/pipeline/artifact.c index 66094b4f7..94fcf7263 100644 --- a/src/pipeline/artifact.c +++ b/src/pipeline/artifact.c @@ -589,14 +589,14 @@ int cbm_artifact_export(const char *db_path, const char *repo_path, const char * } /* Compress with zstd */ - size_t bound = cbm_zstd_compress_bound((int)db_size); + size_t bound = cbm_zstd_compress_bound(db_size); char *compressed = malloc(bound); if (!compressed) { free(db_data); return artifact_export_fail("compress", NULL, "alloc_compressed_buffer", 0); } - int clen = cbm_zstd_compress(db_data, (int)db_size, compressed, (int)bound, compression_level); + int64_t clen = cbm_zstd_compress(db_data, db_size, compressed, bound, compression_level); free(db_data); if (clen <= 0) { diff --git a/tests/test_discover.c b/tests/test_discover.c index 57cbefe75..00384a3da 100644 --- a/tests/test_discover.c +++ b/tests/test_discover.c @@ -297,6 +297,45 @@ TEST(pattern_dts_full) { /* ── File discovery (integration) — cross-platform via test_helpers.h ── */ +/* A custom CBM_CACHE_DIR may legitimately sit inside a repository — tests do it + * routinely. Walking into it would pull every other project's graph database into + * this project's file list, so the walk prunes it by absolute path. */ +TEST(discover_prunes_the_cache_tree) { + char *base = th_mktempdir("cbm_disc_cache"); + ASSERT(base != NULL); + + th_write_file(TH_PATH(base, "src/main.go"), "package main\n"); + /* Source-looking files inside the cache must not be discovered. */ + th_write_file(TH_PATH(base, "cache/other_project/leaked.go"), "package leaked\n"); + + const char *saved = getenv("CBM_CACHE_DIR"); + char *saved_copy = saved ? strdup(saved) : NULL; + char cache_dir[1024]; + snprintf(cache_dir, sizeof(cache_dir), "%s/cache", base); + cbm_setenv("CBM_CACHE_DIR", cache_dir, 1); + + cbm_discover_opts_t opts = {0}; + cbm_file_info_t *files = NULL; + int count = 0; + int rc = cbm_discover(base, &opts, &files, &count); + + if (saved_copy) { + cbm_setenv("CBM_CACHE_DIR", saved_copy, 1); + free(saved_copy); + } else { + cbm_unsetenv("CBM_CACHE_DIR"); + } + + ASSERT_EQ(rc, 0); + for (int i = 0; i < count; i++) { + ASSERT(strstr(files[i].rel_path, "leaked.go") == NULL); + } + ASSERT_EQ(count, 1); /* only src/main.go */ + cbm_discover_free(files, count); + th_cleanup(base); + PASS(); +} + TEST(discover_simple) { char *base = th_mktempdir("cbm_disc_simple"); ASSERT(base != NULL); @@ -1359,6 +1398,7 @@ TEST(discover_many_nested_gitignores_do_not_exhaust_matcher_ownership) { /* ── Suite ─────────────────────────────────────────────────────── */ SUITE(discover) { + RUN_TEST(discover_prunes_the_cache_tree); /* Directory skip — always */ RUN_TEST(skip_git); RUN_TEST(skip_node_modules);