fix(depindex): bound invalid automatic dependency limits

Define CBM_MAX_AUTO_DEP_LIMIT in src/depindex/depindex.h and route src/depindex/depindex.c plus src/mcp/mcp.c through cbm_dep_normalize_configured_limit. Values 1..10000 remain caps, 0 remains unlimited, and negative or oversized legacy/internal values fall back to CBM_DEFAULT_AUTO_DEP_LIMIT instead of becoming unbounded.

Parse config integers through cbm_config_parse_decimal_int in src/cli/cli.c so ERANGE and values outside INT_MIN..INT_MAX return the caller default rather than narrowing to int. Reject persisted auto_dep_limit values outside 0..10000, derive the registry range and MCP schema maximum from CBM_MAX_AUTO_DEP_LIMIT, and cover the boundaries in tests/test_cli.c, tests/test_depindex.c, and tests/test_mcp.c.

Verified: cli 243 passed; depindex 40 passed; mcp 273 passed; bash scripts/check-source-safety.sh passed; git diff --check passed.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-22 16:55:05 -04:00
parent d08ecbe594
commit 8d0816db67
7 changed files with 93 additions and 21 deletions
+34 -12
View File
@@ -5346,17 +5346,28 @@ bool cbm_config_get_bool(cbm_config_t *cfg, const char *key, bool default_val) {
return default_val;
}
int cbm_config_get_int(cbm_config_t *cfg, const char *key, int default_val) {
const char *val = cbm_config_get(cfg, key, NULL);
if (!val) {
return default_val;
static bool cbm_config_parse_decimal_int(const char *value, int *out) {
if (!value || !out) {
return false;
}
char *endptr;
long v = strtol(val, &endptr, CLI_STRTOL_BASE);
if (endptr == val || *endptr != '\0') {
errno = 0;
long parsed = strtol(value, &endptr, CLI_STRTOL_BASE);
if (errno == ERANGE || endptr == value || *endptr != '\0' ||
parsed < INT_MIN || parsed > INT_MAX) {
return false;
}
*out = (int)parsed;
return true;
}
int cbm_config_get_int(cbm_config_t *cfg, const char *key, int default_val) {
const char *val = cbm_config_get(cfg, key, NULL);
int parsed = 0;
if (!cbm_config_parse_decimal_int(val, &parsed)) {
return default_val;
}
return (int)v;
return parsed;
}
static bool cbm_config_value_matches_enum(const char *range, const char *value) {
@@ -5378,7 +5389,19 @@ static bool cbm_config_value_matches_enum(const char *range, const char *value)
return false;
}
static bool cbm_config_decimal_integer_in_range(const char *value, long minimum, long maximum) {
int parsed = 0;
if (!cbm_config_parse_decimal_int(value, &parsed)) {
return false;
}
return parsed >= minimum && parsed <= maximum;
}
static bool cbm_config_value_is_valid(const char *key, const char *value) {
if (key && strcmp(key, CBM_CONFIG_AUTO_DEP_LIMIT) == 0 &&
!cbm_config_decimal_integer_in_range(value, 0, CBM_MAX_AUTO_DEP_LIMIT)) {
return false;
}
for (size_t i = 0; CBM_CONFIG_REGISTRY[i].key; i++) {
if (strcmp(CBM_CONFIG_REGISTRY[i].key, key) == 0) {
return cbm_config_value_matches_enum(CBM_CONFIG_REGISTRY[i].range, value);
@@ -10538,7 +10561,7 @@ const cbm_config_entry_t CBM_CONFIG_REGISTRY[] = {
"future automatic dependency indexing; it does not delete dependency projects already indexed."},
{CBM_CONFIG_AUTO_DEP_LIMIT, CBM_STRINGIFY(CBM_DEFAULT_AUTO_DEP_LIMIT), NULL, "Dependencies",
"Max number of packages to auto-index",
"0-10000",
"0-" CBM_STRINGIFY(CBM_MAX_AUTO_DEP_LIMIT),
"When more packages are installed than this limit, the most-imported packages are selected "
"(ranked by project import references, ties broken by name). Raise to 100+ for comprehensive "
"dependency analysis. 0 = unlimited (may be very slow for large dependency trees)."},
@@ -10590,12 +10613,11 @@ int cbm_config_get_effective_int(cbm_config_t *cfg, const char *key, int default
if (!val || !val[0]) {
return default_val;
}
char *endptr;
long parsed = strtol(val, &endptr, CLI_STRTOL_BASE);
if (endptr == val || *endptr != '\0') {
int parsed = 0;
if (!cbm_config_parse_decimal_int(val, &parsed)) {
return default_val;
}
return (int)parsed;
return parsed;
}
/* ── Config CLI subcommand ────────────────────────────────────── */
+14 -6
View File
@@ -739,19 +739,27 @@ int cbm_discover_installed_deps(cbm_pkg_manager_t mgr, const char *project_root,
/* ── Auto-Index ────────────────────────────────────────────────── */
int cbm_dep_normalize_configured_limit(int limit, int default_limit) {
if (limit == 0) {
return -1;
}
if (limit > 0 && limit <= CBM_MAX_AUTO_DEP_LIMIT) {
return limit;
}
return default_limit > 0 && default_limit <= CBM_MAX_AUTO_DEP_LIMIT
? default_limit
: CBM_DEFAULT_AUTO_DEP_LIMIT;
}
int cbm_dep_auto_index_effective_limit(cbm_config_t *cfg, int default_limit) {
if (!cbm_config_get_bool(cfg, CBM_CONFIG_AUTO_INDEX_DEPS,
CBM_DEFAULT_AUTO_INDEX_DEPS)) {
if (!cbm_config_get_bool(cfg, CBM_CONFIG_AUTO_INDEX_DEPS, CBM_DEFAULT_AUTO_INDEX_DEPS)) {
return 0;
}
int limit = cbm_config_get_int(cfg, CBM_CONFIG_AUTO_DEP_LIMIT, default_limit);
/* The direct API keeps max_deps=0 as disabled. The config registry documents
* auto_dep_limit=0 as unlimited, so map configured callers to -1 here. */
if (limit <= 0) {
return -1;
}
return limit;
return cbm_dep_normalize_configured_limit(limit, default_limit);
}
/* Auto-detect ecosystem, discover deps, index each via flush_to_store.
+5
View File
@@ -48,6 +48,7 @@ static const char *CBM_MANIFEST_FILES[] = {
#define CBM_DEFAULT_AUTO_INDEX_DEPS false
#define CBM_DEFAULT_AUTO_INDEX_DEPS_STR "false"
#define CBM_DEFAULT_AUTO_DEP_LIMIT 20
#define CBM_MAX_AUTO_DEP_LIMIT 10000
#define CBM_DEFAULT_DEP_MAX_FILES 1000
/* Config key strings */
@@ -163,4 +164,8 @@ int cbm_dep_link_cross_edges(cbm_store_t *store, const char *project_name);
* Return 0 to disable, -1 for unlimited, or a positive package count. */
int cbm_dep_auto_index_effective_limit(cbm_config_t *cfg, int default_limit);
/* Normalize an enabled auto_dep_limit value: 0 is unlimited, valid positive
* values are retained, and out-of-range values fall back to a bounded default. */
int cbm_dep_normalize_configured_limit(int limit, int default_limit);
#endif /* CBM_DEPINDEX_H */
+5 -3
View File
@@ -1052,8 +1052,10 @@ static const tool_def_t TOOLS[] = {
"Use [\\\"*\\\"] for all indexed projects. Run list_projects to see available projects.\"},"
"\"auto_index_deps\":{\"type\":\"boolean\",\"description\":"
"\"Set false to skip dependency package indexing for this call. Default follows config auto_index_deps.\"},"
"\"auto_dep_limit\":{\"type\":\"integer\",\"description\":"
"\"Dependency package cap for this call. Default follows config auto_dep_limit; 0 means unlimited.\"},"
"\"auto_dep_limit\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":"
CBM_STRINGIFY(CBM_MAX_AUTO_DEP_LIMIT) ",\"description\":"
"\"Dependency package cap for this call from 1 to " CBM_STRINGIFY(CBM_MAX_AUTO_DEP_LIMIT)
". Default follows config auto_dep_limit; 0 means unlimited.\"},"
"\"name\":{\"type\":\"string\",\"description\":"
"\"Override the derived project name. Non-ASCII bytes are encoded and unsafe path characters "
"are normalized.\"},"
@@ -2572,7 +2574,7 @@ static int cbm_mcp_effective_auto_dep_limit(cbm_mcp_server_t *srv, const char *a
if (cbm_mcp_has_arg(args_json, CBM_CONFIG_AUTO_DEP_LIMIT)) {
limit = cbm_mcp_get_int_arg(args_json, CBM_CONFIG_AUTO_DEP_LIMIT, limit);
}
return limit <= 0 ? -1 : limit;
return cbm_dep_normalize_configured_limit(limit, CBM_DEFAULT_AUTO_DEP_LIMIT);
}
/* Query routes deliberately cache read-only stores. Mutation routes must never
+7
View File
@@ -6879,6 +6879,10 @@ TEST(cli_config_get_int) {
cbm_config_set(cfg, "limit", "abc");
ASSERT_EQ(cbm_config_get_int(cfg, "limit", 50000), 50000);
/* Values outside int range must not wrap into a valid limit. */
cbm_config_set(cfg, "limit", "999999999999999999999999");
ASSERT_EQ(cbm_config_get_int(cfg, "limit", 50000), 50000);
cbm_config_close(cfg);
test_rmdir_r(tmpdir);
PASS();
@@ -6962,6 +6966,9 @@ TEST(cli_config_registry_auto_dep_limit_uses_shared_default) {
ASSERT_NOT_NULL(found);
ASSERT_EQ(atoi(found->default_val), CBM_DEFAULT_AUTO_DEP_LIMIT);
char expected_range[CBM_SZ_32];
snprintf(expected_range, sizeof(expected_range), "0-%d", CBM_MAX_AUTO_DEP_LIMIT);
ASSERT_STR_EQ(found->range, expected_range);
PASS();
}
+12
View File
@@ -1090,6 +1090,18 @@ TEST(test_auto_index_deps_config_limit_policy) {
ASSERT_EQ(cbm_dep_auto_index_effective_limit(cfg, CBM_DEFAULT_AUTO_DEP_LIMIT), 7);
ASSERT_EQ(cbm_config_set(cfg, CBM_CONFIG_AUTO_DEP_LIMIT, "0"), 0);
ASSERT_EQ(cbm_dep_auto_index_effective_limit(cfg, CBM_DEFAULT_AUTO_DEP_LIMIT), -1);
ASSERT_EQ(
cbm_dep_normalize_configured_limit(CBM_MAX_AUTO_DEP_LIMIT, CBM_DEFAULT_AUTO_DEP_LIMIT),
CBM_MAX_AUTO_DEP_LIMIT);
ASSERT_EQ(cbm_dep_normalize_configured_limit(-1, CBM_DEFAULT_AUTO_DEP_LIMIT),
CBM_DEFAULT_AUTO_DEP_LIMIT);
ASSERT_EQ(
cbm_dep_normalize_configured_limit(CBM_MAX_AUTO_DEP_LIMIT + 1, CBM_MAX_AUTO_DEP_LIMIT + 1),
CBM_DEFAULT_AUTO_DEP_LIMIT);
ASSERT_NEQ(cbm_config_set(cfg, CBM_CONFIG_AUTO_DEP_LIMIT, "-1"), 0);
char over_limit[CBM_SZ_32];
snprintf(over_limit, sizeof(over_limit), "%d", CBM_MAX_AUTO_DEP_LIMIT + 1);
ASSERT_NEQ(cbm_config_set(cfg, CBM_CONFIG_AUTO_DEP_LIMIT, over_limit), 0);
cbm_config_close(cfg);
cleanup_fixture_dir(cache_tmp);
+16
View File
@@ -610,6 +610,21 @@ TEST(mcp_canonical_input_schemas_cover_implemented_format_and_verbose_options) {
PASS();
}
TEST(mcp_index_repository_auto_dep_limit_schema_uses_shared_bounds) {
const char *schema_json = cbm_mcp_tool_input_schema("index_repository");
ASSERT_NOT_NULL(schema_json);
yyjson_doc *doc = yyjson_read(schema_json, strlen(schema_json), 0);
ASSERT_NOT_NULL(doc);
yyjson_val *properties = yyjson_obj_get(yyjson_doc_get_root(doc), "properties");
ASSERT_TRUE(yyjson_is_obj(properties));
yyjson_val *limit = yyjson_obj_get(properties, CBM_CONFIG_AUTO_DEP_LIMIT);
ASSERT_TRUE(yyjson_is_obj(limit));
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(limit, "minimum")), 0);
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(limit, "maximum")), CBM_MAX_AUTO_DEP_LIMIT);
yyjson_doc_free(doc);
PASS();
}
TEST(mcp_tools_have_behavior_annotations) {
struct {
const char *name;
@@ -12726,6 +12741,7 @@ SUITE(mcp) {
RUN_TEST(mcp_tools_list_latest_metadata);
RUN_TEST(mcp_tool_input_schemas_are_closed_in_classic_and_streamlined_modes);
RUN_TEST(mcp_canonical_input_schemas_cover_implemented_format_and_verbose_options);
RUN_TEST(mcp_index_repository_auto_dep_limit_schema_uses_shared_bounds);
RUN_TEST(mcp_tools_have_behavior_annotations);
RUN_TEST(mcp_index_repository_declares_name_override_issue571);
RUN_TEST(mcp_tools_array_schemas_have_items);