mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-28 07:03:20 +08:00
fix(fs): reject truncated directory entries
Return only exactly represented directory names from cbm_readdir so callers never stat, open, list, or delete a fabricated prefix path. This centralizes the Windows UTF-8 expansion edge case in compat_fs instead of duplicating checks at each caller. Also make MCP bad-project available_projects hints append only complete escaped JSON strings and expose available_projects_truncated when the compact hint omits entries. Validation: make -f Makefile.cbm build/c/test-runner; CBM_ONLY_SUITE=platform ./build/c/test-runner; CBM_ONLY_SUITE=mcp ./build/c/test-runner; bash scripts/check-source-safety.sh; git diff --check. Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
+54
-31
@@ -13,6 +13,33 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static bool cbm_dirent_name_len(const char *name, size_t *out_len) {
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
const char *end = memchr(name, '\0', CBM_DIRENT_NAME_MAX);
|
||||
if (!end) {
|
||||
return false;
|
||||
}
|
||||
if (out_len) {
|
||||
*out_len = (size_t)(end - name);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cbm_dirent_name_fits(const char *name) {
|
||||
return cbm_dirent_name_len(name, NULL);
|
||||
}
|
||||
|
||||
static bool cbm_dirent_set_name(cbm_dirent_t *entry, const char *name) {
|
||||
size_t nlen = 0;
|
||||
if (!entry || !cbm_dirent_name_len(name, &nlen)) {
|
||||
return false;
|
||||
}
|
||||
memcpy(entry->name, name, nlen + SKIP_ONE);
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
/* ── Windows implementation ────────────────────────────────── */
|
||||
@@ -81,38 +108,37 @@ cbm_dirent_t *cbm_readdir(cbm_dir_t *d) {
|
||||
if (!d || d->done) {
|
||||
return NULL;
|
||||
}
|
||||
if (!d->first) {
|
||||
if (!FindNextFileW(d->find_handle, &d->find_data)) {
|
||||
|
||||
for (;;) {
|
||||
if (!d->first) {
|
||||
if (!FindNextFileW(d->find_handle, &d->find_data)) {
|
||||
d->done = true;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
d->first = false;
|
||||
|
||||
if (d->find_data.cFileName[0] == L'.' &&
|
||||
(d->find_data.cFileName[1] == L'\0' ||
|
||||
(d->find_data.cFileName[1] == L'.' && d->find_data.cFileName[2] == L'\0'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char *u8 = cbm_wide_to_utf8(d->find_data.cFileName);
|
||||
if (!u8) {
|
||||
d->done = true;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
d->first = false;
|
||||
|
||||
while (d->find_data.cFileName[0] == L'.' &&
|
||||
(d->find_data.cFileName[1] == L'\0' ||
|
||||
(d->find_data.cFileName[1] == L'.' && d->find_data.cFileName[2] == L'\0'))) {
|
||||
if (!FindNextFileW(d->find_handle, &d->find_data)) {
|
||||
d->done = true;
|
||||
return NULL;
|
||||
bool copied = cbm_dirent_set_name(&d->entry, u8);
|
||||
free(u8);
|
||||
if (!copied) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
char *u8 = cbm_wide_to_utf8(d->find_data.cFileName);
|
||||
if (!u8) {
|
||||
d->done = true;
|
||||
return NULL;
|
||||
d->entry.is_dir = (d->find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
d->entry.d_type = 0;
|
||||
return &d->entry;
|
||||
}
|
||||
size_t nlen = strlen(u8);
|
||||
if (nlen >= CBM_DIRENT_NAME_MAX) {
|
||||
nlen = CBM_DIRENT_NAME_MAX - SKIP_ONE;
|
||||
}
|
||||
memcpy(d->entry.name, u8, nlen);
|
||||
d->entry.name[nlen] = '\0';
|
||||
free(u8);
|
||||
d->entry.is_dir = (d->find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
d->entry.d_type = 0;
|
||||
return &d->entry;
|
||||
}
|
||||
|
||||
void cbm_closedir(cbm_dir_t *d) {
|
||||
@@ -286,12 +312,9 @@ cbm_dirent_t *cbm_readdir(cbm_dir_t *d) {
|
||||
(de->d_name[SKIP_ONE] == '.' && de->d_name[PAIR_LEN] == '\0'))) {
|
||||
continue;
|
||||
}
|
||||
size_t nlen = strlen(de->d_name);
|
||||
if (nlen >= CBM_DIRENT_NAME_MAX) {
|
||||
nlen = CBM_DIRENT_NAME_MAX - SKIP_ONE;
|
||||
if (!cbm_dirent_set_name(&d->entry, de->d_name)) {
|
||||
continue;
|
||||
}
|
||||
memcpy(d->entry.name, de->d_name, nlen);
|
||||
d->entry.name[nlen] = '\0';
|
||||
d->entry.is_dir = (de->d_type == DT_DIR);
|
||||
d->entry.d_type = de->d_type;
|
||||
return &d->entry;
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "foundation/constants.h"
|
||||
|
||||
/* ── Directory iteration ──────────────────────────────────────── */
|
||||
|
||||
/* Max filename length (MAX_PATH on Windows, NAME_MAX on POSIX). */
|
||||
#define CBM_DIRENT_NAME_MAX 260
|
||||
#define CBM_DIRENT_NAME_MAX (CBM_SZ_256 + CBM_SZ_4)
|
||||
|
||||
typedef struct cbm_dir cbm_dir_t;
|
||||
|
||||
@@ -31,6 +33,9 @@ cbm_dir_t *cbm_opendir(const char *path);
|
||||
* valid until the next cbm_readdir call on the same handle. */
|
||||
cbm_dirent_t *cbm_readdir(cbm_dir_t *d);
|
||||
|
||||
/* True when name can be represented exactly in cbm_dirent_t.name. */
|
||||
bool cbm_dirent_name_fits(const char *name);
|
||||
|
||||
/* Close directory handle. */
|
||||
void cbm_closedir(cbm_dir_t *d);
|
||||
|
||||
|
||||
+58
-19
@@ -1551,11 +1551,19 @@ static bool is_project_db_file(const char *name, size_t len);
|
||||
/* Forward decl — definition lives below in trace_path helpers. */
|
||||
static void free_node_contents(cbm_node_t *n);
|
||||
|
||||
/* Scan cache dir for .db files, writing comma-separated quoted names into out.
|
||||
* Returns the number of projects found. */
|
||||
static int collect_db_project_names(const char *dir_path, char *out, size_t out_sz) {
|
||||
/* Scan cache dir for .db files, writing complete quoted JSON names into out.
|
||||
* Returns the total projects found; out may list fewer when truncated is set. */
|
||||
static int collect_db_project_names(const char *dir_path, char *out, size_t out_sz,
|
||||
bool *truncated) {
|
||||
int count = 0;
|
||||
int offset = 0;
|
||||
int listed = 0;
|
||||
size_t offset = 0;
|
||||
if (truncated) {
|
||||
*truncated = false;
|
||||
}
|
||||
if (out && out_sz > 0) {
|
||||
out[0] = '\0';
|
||||
}
|
||||
cbm_dir_t *d = cbm_opendir(dir_path);
|
||||
if (!d) {
|
||||
return 0;
|
||||
@@ -1567,20 +1575,48 @@ static int collect_db_project_names(const char *dir_path, char *out, size_t out_
|
||||
if (!is_project_db_file(n, len)) {
|
||||
continue;
|
||||
}
|
||||
if ((size_t)offset >= out_sz)
|
||||
break; /* bounds check before write */
|
||||
if (count > 0 && offset < (int)out_sz - MCP_SEPARATOR) {
|
||||
count++;
|
||||
|
||||
char project_name[CBM_DIRENT_NAME_MAX];
|
||||
size_t project_len = len - MCP_DB_EXT;
|
||||
if (project_len >= sizeof(project_name)) {
|
||||
if (truncated) {
|
||||
*truncated = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
memcpy(project_name, n, project_len);
|
||||
project_name[project_len] = '\0';
|
||||
|
||||
char escaped[CBM_SZ_1K];
|
||||
int escaped_len = cbm_json_escape(escaped, (int)sizeof(escaped), project_name);
|
||||
if (escaped_len <= 0 && project_name[0] != '\0') {
|
||||
if (truncated) {
|
||||
*truncated = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t item_len = (size_t)escaped_len + CBM_QUOTE_PAIR;
|
||||
if (listed > 0) {
|
||||
item_len += SKIP_ONE;
|
||||
}
|
||||
if (!out || out_sz == 0 || offset + item_len >= out_sz) {
|
||||
if (truncated) {
|
||||
*truncated = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (listed > 0) {
|
||||
out[offset++] = ',';
|
||||
}
|
||||
int wrote = snprintf(out + offset, out_sz - (size_t)offset, "\"%.*s\"",
|
||||
(int)(len - MCP_DB_EXT), n);
|
||||
if (wrote > 0) {
|
||||
offset += wrote;
|
||||
if ((size_t)offset >= out_sz) {
|
||||
offset = (int)out_sz - 1; /* clamp on truncation */
|
||||
}
|
||||
}
|
||||
count++;
|
||||
out[offset++] = '"';
|
||||
memcpy(out + offset, escaped, (size_t)escaped_len);
|
||||
offset += (size_t)escaped_len;
|
||||
out[offset++] = '"';
|
||||
out[offset] = '\0';
|
||||
listed++;
|
||||
}
|
||||
cbm_closedir(d);
|
||||
return count;
|
||||
@@ -1623,7 +1659,8 @@ static char *build_project_list_error_srv(cbm_mcp_server_t *srv, const char *rea
|
||||
cache_dir(dir_path, sizeof(dir_path));
|
||||
|
||||
char projects[CBM_SZ_4K] = "";
|
||||
int count = collect_db_project_names(dir_path, projects, sizeof(projects));
|
||||
bool projects_truncated = false;
|
||||
int count = collect_db_project_names(dir_path, projects, sizeof(projects), &projects_truncated);
|
||||
|
||||
/* Optional: session_project and _context fields for richer error context */
|
||||
char session_frag[256] = "";
|
||||
@@ -1646,8 +1683,10 @@ static char *build_project_list_error_srv(cbm_mcp_server_t *srv, const char *rea
|
||||
if (count > 0) {
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"error\":\"%s\",\"hint\":\"Use list_projects to see all indexed projects, "
|
||||
"then pass the project name.\",\"available_projects\":[%s],\"count\":%d%s%s}",
|
||||
reason, projects, count, session_frag, context_frag);
|
||||
"then pass the project name.\",\"available_projects\":[%s],\"count\":%d%s%s%s}",
|
||||
reason, projects, count,
|
||||
projects_truncated ? ",\"available_projects_truncated\":true" : "",
|
||||
session_frag, context_frag);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"error\":\"%s\",\"hint\":\"No projects indexed yet. "
|
||||
|
||||
@@ -2928,6 +2928,19 @@ TEST(tool_bad_project_name_no_overflow_issue235) {
|
||||
"\"project\":\"definitely-not-a-real-project-xyz\"}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
ASSERT_NOT_NULL(strstr(resp, "not found"));
|
||||
char *inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
yyjson_doc *doc = yyjson_read(inner, strlen(inner), 0);
|
||||
ASSERT_NOT_NULL(doc);
|
||||
yyjson_val *root = yyjson_doc_get_root(doc);
|
||||
ASSERT_NOT_NULL(root);
|
||||
ASSERT_EQ((int)yyjson_get_int(yyjson_obj_get(root, "count")), ISSUE235_N);
|
||||
ASSERT_TRUE(yyjson_get_bool(yyjson_obj_get(root, "available_projects_truncated")));
|
||||
yyjson_val *projects = yyjson_obj_get(root, "available_projects");
|
||||
ASSERT_TRUE(yyjson_is_arr(projects));
|
||||
ASSERT_TRUE((int)yyjson_arr_size(projects) < ISSUE235_N);
|
||||
yyjson_doc_free(doc);
|
||||
free(inner);
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "../src/foundation/compat.h" /* cbm_setenv / cbm_unsetenv (Windows-portable) */
|
||||
#include "../src/foundation/compat_fs.h"
|
||||
#include "../src/foundation/platform.h"
|
||||
#include "../src/foundation/system_info_internal.h"
|
||||
#include <stdlib.h>
|
||||
@@ -163,6 +164,21 @@ TEST(platform_getenv_fits) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(platform_dirent_name_fits_boundary) {
|
||||
char fits[CBM_DIRENT_NAME_MAX];
|
||||
char too_long[CBM_DIRENT_NAME_MAX + SKIP_ONE];
|
||||
|
||||
memset(fits, 'a', sizeof(fits) - SKIP_ONE);
|
||||
fits[sizeof(fits) - SKIP_ONE] = '\0';
|
||||
ASSERT_TRUE(cbm_dirent_name_fits(fits));
|
||||
|
||||
memset(too_long, 'b', sizeof(too_long) - SKIP_ONE);
|
||||
too_long[sizeof(too_long) - SKIP_ONE] = '\0';
|
||||
ASSERT_FALSE(cbm_dirent_name_fits(too_long));
|
||||
ASSERT_FALSE(cbm_dirent_name_fits(NULL));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── cgroup-aware detection (Linux only) ─────────────────────────── */
|
||||
|
||||
#ifdef __linux__
|
||||
@@ -348,6 +364,7 @@ SUITE(platform) {
|
||||
RUN_TEST(platform_default_workers_env_invalid);
|
||||
RUN_TEST(platform_default_workers_env_unset);
|
||||
RUN_TEST(platform_getenv_fits);
|
||||
RUN_TEST(platform_dirent_name_fits_boundary);
|
||||
#ifdef __linux__
|
||||
RUN_TEST(cgroup_v2_cpu_quota);
|
||||
RUN_TEST(cgroup_v2_cpu_quota_rounds_up);
|
||||
|
||||
Reference in New Issue
Block a user