From ea0fe1cb59de565163242d62d31b6f835e2bd874 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 10 Oct 2026 11:12:02 +0000 Subject: [PATCH] fix(file): reject reads that do not fit the result buffer file_read returned success after clipping to the caller buffer. The agent passes a 4096-byte slot, so a later write_file could replace the file with that prefix and delete the rest. Co-authored-by: esadrianno --- src/tools/file.c | 9 ++++++++- tests/test_file.c | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/tools/file.c b/src/tools/file.c index 4e07d83..cef427a 100644 --- a/src/tools/file.c +++ b/src/tools/file.c @@ -259,7 +259,14 @@ static int file_read(const char *path, char *result_buf, size_t max_len) return -1; } size_t to_read = (size_t)sz; - if (to_read >= max_len - 1) to_read = max_len - 1; + /* A prefix with return 0 is the whole file to the model. write_file would + * then replace the original and drop the unread tail. */ + if (to_read + 1 > max_len) { + fclose(f); + snprintf(result_buf, max_len, + "{\"error\":\"file exceeds result buffer\",\"bytes\":%ld}", sz); + return -1; + } size_t n = fread(result_buf, 1, to_read, f); fclose(f); result_buf[n] = '\0'; diff --git a/tests/test_file.c b/tests/test_file.c index 0907d27..a97221e 100644 --- a/tests/test_file.c +++ b/tests/test_file.c @@ -563,6 +563,48 @@ static void test_write_in_workspace_symlink_alias_rejected(void) rmdir(tmpdir); } +static void test_read_rejects_file_larger_than_result_buffer(void) +{ + char tmpdir[64]; + char notes_path[PATH_MAX]; + char config_path[PATH_MAX]; + char args[PATH_MAX + 64]; + char buf[32]; + char exact[8]; + FILE *f; + config_t *cfg; + const tool_t *t; + int i; + + snprintf(tmpdir, sizeof(tmpdir), "/tmp/sc_test_readcap_%d", (int)getpid()); + if (mkdir(tmpdir, 0755) != 0 && errno != EEXIST) + return; + snprintf(notes_path, sizeof(notes_path), "%s/notes.md", tmpdir); + f = fopen(notes_path, "w"); + MU_ASSERT(f != NULL, "create oversized notes.md"); + for (i = 0; i < 64; i++) + fputc('A', f); + fclose(f); + cfg = make_ws_config(tmpdir, config_path, sizeof(config_path)); + MU_ASSERT(cfg != NULL, "readcap: load config"); + tool_file_set_config(cfg); + t = tool_file_get(); + snprintf(args, sizeof(args), "{\"operation\":\"read_file\",\"path\":\"%s\"}", notes_path); + MU_ASSERT(t->execute(args, buf, sizeof(buf)) == -1, "oversize read fails"); + MU_ASSERT(strstr(buf, "error") != NULL, "oversize read reports an error"); + MU_ASSERT(strstr(buf, "AAAAAAAAAAAAAAAA") == NULL, "oversize read is not a raw prefix"); + f = fopen(notes_path, "w"); + MU_ASSERT(f != NULL, "rewrite exact-fit notes.md"); + fputs("0123456", f); + fclose(f); + MU_ASSERT(t->execute(args, exact, sizeof(exact)) == 0, "exact-fit read succeeds"); + MU_ASSERT(strcmp(exact, "0123456") == 0, "exact-fit read returns the whole file"); + config_free(cfg); + unlink(config_path); + unlink(notes_path); + rmdir(tmpdir); +} + int main(void) { MU_RUN(test_file_read_write_list); @@ -577,6 +619,7 @@ int main(void) MU_RUN(test_file_write_failure_preserves_existing); MU_RUN(test_write_does_not_clobber_sibling_tmp); MU_RUN(test_write_in_workspace_symlink_alias_rejected); + MU_RUN(test_read_rejects_file_larger_than_result_buffer); printf("%d tests run, %d failed\n", tests_run, tests_failed); return tests_failed ? 1 : 0; }