Skip to content

fix(file): reject reads that do not fit the result buffer - #120

Merged
adriannoes merged 3 commits into
developmentfrom
fix/file-read-bounds
Oct 17, 2026
Merged

adriannoes merged 3 commits into
developmentfrom
fix/file-read-bounds

Conversation

@adriannoes

Copy link
Copy Markdown
Collaborator

Summary

Consolidates one cursor high-severity fix (originally cursor/high-severity-issues-ed43, PR #114) as an operator-authored PR.

  • 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.

Testing

  • CI=true make test — all 45 test binaries passed (includes test_file)
  • make static — passed

Supersedes #114.

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.

Supersedes #114.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Solid, focused fail-closed fix for a real silent data-loss bug: file_read no longer returns success with a clipped prefix that a later write_file could turn into a truncated file. Same pattern as session JSON (#70) and ASAP reply (#61) refuse-vs-truncate. Local ./build/test_file passed (83 tests, 0 failed; link with -lm when -lcurl is missing).

Must Fix

None.

Should Fix

  1. Add an Unreleased Fixed bullet in CHANGELOG.md for this behavior (same gap as #119). Other high-severity refuse-vs-truncate fixes in this tree are changelogged.
  2. In tests/test_file.c, buf[32] is smaller than the error JSON ({"error":"file exceeds result buffer","bytes":64} is ~49 bytes), so snprintf truncates the message and the assertions only prove a substring "error". Use a result buffer large enough for the full error (and still smaller than the 64-byte file), assert "bytes" / the error string, and add a one-over case (size == max_len fails, size == max_len - 1 already covered).

Nice to Have

  • Mention the effective ~4095-byte read cap (agent TOOL_RESULT_SIZE is 4096) in the file tool description so the model does not retry blindly on large notes.
  • Offset/length parameters for large files are out of scope here; fail-closed is the right v1 tradeoff.

Positive Highlights

  • Correct root-cause fix: reject when to_read + 1 > max_len, keep exact-fit success, close the file before writing the error.
  • Regression test locks both oversize failure and exact-fit success; single-commit PR that supersedes #114 without extra scope.
Open in Web View Automation 

Sent by Cursor Automation: Code review

Comment thread src/tools/file.c
Comment on lines +264 to +268
if (to_read + 1 > max_len) {
fclose(f);
snprintf(result_buf, max_len,
"{\"error\":\"file exceeds result buffer\",\"bytes\":%ld}", sz);
return -1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail-closed here is the right contract: a successful read must be the whole file (agent slot is TOOL_RESULT_SIZE 4096 in agent.c). Exact fit (size == max_len - 1) still succeeds; size >= max_len correctly errors. Good match to the refuse-vs-truncate pattern used elsewhere.

Comment thread tests/test_file.c Outdated
Comment on lines +572 to +595
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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should Fix: buf is 32 bytes, but the error JSON is longer (~49 for bytes":64), so this path only checks a truncated prefix. Prefer something like char err[128] (still < 64 so the oversize path triggers) and assert strstr(buf, "file exceeds result buffer") and strstr(buf, "bytes"). Optionally add a size == sizeof(exact) one-over case next to the exact-fit success.

@adriannoes adriannoes changed the title fix(file): reject reads that do not fit the result buffer (supersedes #114) fix(file): reject reads that do not fit the result buffer Oct 16, 2026
@adriannoes adriannoes self-assigned this Oct 16, 2026
Add the Unreleased Fixed bullet for the file read bounds fix so the
behavior change is changelogged like the other refuse-vs-truncate
fixes in this tree. Review of #120 flagged the missing entry; docs
only, no behavior change.
The oversize regression used a 32-byte result buffer, smaller than
the ~49-byte error JSON, so snprintf truncated the message and the
assertions only proved a bare "error" substring. Give the oversize
case a 56-byte buffer (still below the 64-byte fixture) so the exact
refuse-to-truncate error and the "bytes" field can be asserted, and
cover the last boundary: a read of exactly max_len bytes fails where
max_len - 1 already succeeds. Test-only change per review of #120.
@adriannoes

adriannoes commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Review response — done in 0104890 and fd3a098: changelog bullet, and the oversize test now asserts the full error JSON (including "bytes") with a buffer that fits it, plus the size == max_len one-over case.

Deferred: noting the ~4095-byte read cap in the tool description (model-facing prompt change).

@adriannoes
adriannoes merged commit 5bcd79b into development Oct 17, 2026
1 check passed
@adriannoes
adriannoes deleted the fix/file-read-bounds branch October 17, 2026 14:28
adriannoes added a commit that referenced this pull request Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant