Bound stdin (and the --file twin) at 1 MiB - #647
Merged
Merged
Conversation
readStdinContent was a bare io.ReadAll with no cap, and all 36 "-"-accepting call sites funnel through it. `yes | basecamp api post /valid --data -` is a perfectly valid invocation that read until the process died — which is why the recent pre-read ordering work (#641, #645) did not touch it: nothing here is a doomed invocation to reject early, the read itself was unbounded. Refuse rather than truncate. Silently posting the first megabyte would write partial content to Basecamp and report success, and a note or message is unrecoverable once saved. The cap counts bytes read, so it lands before the trailing-newline trim: an overflow that is only a trailing "\n" is still a refusal, because telling it apart from any other overflow would mean reading past the cap. `notes set --file <path>` was the file twin of the same read — a bare os.ReadFile — so it gets the same bound and the same error shape. Which side of the "-" the bytes arrive on no longer changes whether they are accepted. Worth naming: `boost create -` now stops at 1 MiB before its 16-rune check, instead of reading an unbounded stream and copying it to a string in order to reject 16 characters. maxStdinContent is declared alongside the stdin machinery rather than reusing maxAgentHookInput. Same value today, different purpose — one bounds a JSON envelope an agent harness writes, the other bounds prose a person pipes — and they should stay free to move apart. The reads left alone are internal/editor/editor.go and the TUI composer: both read back what the user's own $EDITOR just wrote, which is not a streaming source.
There was a problem hiding this comment.
Pull request overview
Bounds stdin and notes set --file input at 1 MiB to prevent unbounded memory consumption.
Changes:
- Rejects stdin exceeding 1 MiB without truncation.
- Applies the same limit to note files.
- Adds boundary and overflow tests.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
internal/commands/stdin.go |
Adds the shared stdin limit and overflow error. |
internal/commands/stdin_test.go |
Tests exact-limit and overflow behavior. |
internal/commands/notes.go |
Adds bounded file reading for notes. |
internal/commands/notes_test.go |
Tests note-file size boundaries. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Caps
readStdinContentatmaxStdinContent = 1 << 20, using theio.LimitReader(r, cap+1)-then-compare idiom already in this package(
agent_hook.go:238,upgrade_selfupdate.go:339/:359). All 36--acceptingcall sites funnel through it, so they all inherit the bound.
notes set --file <path>was the file twin of the same read — a bareos.ReadFile— so it gets the same bound and the same error shape via a smallnotesFileContenthelper. Which side of the-the bytes arrive on no longerchanges whether they're accepted.
Why
readStdinContentwas a bareio.ReadAllwith no cap.yes | basecamp api post /valid --data -is a perfectly valid invocationthat read until the process died. That's why the recent pre-read ordering work
(#641, #645) didn't touch it: nothing here is a doomed invocation to reject
early — the read itself was unbounded.
The sharpest illustration:
boost create -read the entire stream, copied it toa string, and then rejected it for exceeding 16 characters
(
boost.go:307). It now stops at 1 MiB before the 16-rune check.Refuse, don't truncate. Silently posting the first megabyte would write
partial content to Basecamp and report success — unrecoverable once saved.
maxStdinContentis declared alongside the stdin machinery rather than reusingmaxAgentHookInput. Same value today, different purpose — one bounds a JSONenvelope an agent harness writes, the other bounds prose a person pipes — and
they should stay free to move apart.
Left alone deliberately:
internal/editor/editor.go:49and the TUI composer atwidget/composer.go:719. Both read back what the user's own$EDITORjustwrote, which is not a streaming source.
The trailing-newline edge
The cap counts bytes read, so it lands before the trailing-newline trim: a
body of exactly 1 MiB plus a trailing
\nis a refusal. That's the onlyimplementable answer — telling "the overflow is just a newline" apart from any
other overflow requires reading past the cap, which is the thing being
prevented. Documented in the doc comment and pinned by a subtest so a future
reader doesn't "fix" it.
Testing
make checkpasses (viabin/ci), including the AST ordering backstopTestSyntacticArgChecksPrecedeStdinReadsBoundary pairs, not just rejection cases —
stdin_test.goasserts exactlymaxStdinContentis accepted and one byte over is a usage error;notes_test.godoes the same for--file(the file helper's+1/compare iswritten separately and could be off by one in the accept direction). The
boundary was verified by mutation: bumping the const to
1<<20 + 1failsall three assertions, so an off-by-one in either direction is caught.
Smoke (against a dead endpoint, so a proceeding read fails at the network and a
capped read fails before it):
The
--filemessage names the flag and path rather than mirroringstdin for %sverbatim — parallel shape, but it says what the caller can acton.
Summary by cubic
Bound stdin and
notes --filereads at 1 MiB to prevent unbounded reads and make file vs stdin behave the same. Previously we read all input; now we refuse inputs over the cap rather than truncating.-placeholders now go through cappedreadStdinContent; on overflow we return a usage error: "stdin for exceeds 1048576 bytes".notes set --file <path>now reads vianotesFileContentwith the same cap; overflow error: "--file exceeds 1048576 bytes".internal/editor/editor.goor the TUI composer; they read back from$EDITOR, not a stream.internal/commands/stdin_test.goandinternal/commands/notes_test.go.Migration: If you pipe more than 1 MiB, the command now fails with a usage error. Reduce the input size or split content before invoking the command.
Written for commit 35ee365. Summary will update on new commits.