fix(engine): reject stdin input, decode stdout correctly, bound its size - #2916
Conversation
miga-heygen
left a comment
There was a problem hiding this comment.
Stack 5/6: stdin rejection, StringDecoder, stdout bound — Review
Clean fix, no concerns.
Verified
-
filePath === "-"rejected before spawn — correct.--doesn't help here because ffprobe rewrites-tofd:after option parsing. The error message is clear:stdin is not a supported input path. -
stdio: ["ignore", "pipe", "pipe"]— closes stdin pipe. Prevents any future stdin-reading invocation from blocking, not just the-case. -
StringDecoderreplaces per-chunktoString()— fixes multi-byte character corruption at 64 KiB pipe boundaries. Good call NOT adding an untestable assertion: U+FFFD is valid JSON string content, and no public surface exposes mangled tag values. - Stdout bound: 8M chars, checked AFTER appending (the author's own first draft caught the off-by-one — pre-append check lets a single oversized chunk through). Truncated stdout throws rather than parsing garbage.
-
decoder.end()called after the process exits to flush any trailing partial character. - Tests:
-rejection (0 spawn calls), stdio shape assertion, size bound exceeded.
Ships clean.
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Clean series of three related engine-helper hardenings. - rejected up front rather than hanging 30s on unwritten stdin; stdio: ["ignore", "pipe", "pipe"] so no future invocation can block on stdin either; StringDecoder("utf8") preserving multi-byte character boundaries across pipe chunks; post-append size bound with a truncated-then-thrown recovery. The write-up on why the bound is checked after appending (a single chunk can already exceed it) is worth its own comment and does its job.
Nits
- Truncation leaves the child running until the deadline. On
stdoutTruncated, subsequent chunks are dropped and the buffer is cleared, but the child process keeps producing output until eithermanaged.wait()returns naturally or the 30s deadline trips. Signallingmanaged(orproc.kill()) at the truncation point would end early and skip the wasted disk/CPU. Byte-frugal, non-correctness — just tidier. - The rejection guard is
filePath === "-"specifically, not "all stdin protocols".pipe:0,pipe:,fd:0also make ffprobe read stdin — but those are protocol strings a user would only reach deliberately, andstdio: ["ignore", ...]now turns them into a fast error rather than a hang, so the belt-and-braces holds. Calling out only so it's explicit that the guard closes the accidental case, not the deliberate one.
What I didn't verify
- The StringDecoder-corruption claim is documented in the commit body rather than tested — your reasoning (U+FFFD survives JSON.parse, and no public surface exposes the mangled tag value) is fine. If a future reviewer wants it pinned, a small test seam exposing raw stdout plus one multi-byte-boundary fixture would do it — not a defect, additive coverage.
— Review by Rames D Jusso
miguel-heygen
left a comment
There was a problem hiding this comment.
Approved exact head b150c488a3f10d6d96323ea699bd7481e04e320c against stack parent #2915. The bare-- rejection, ignored stdin, split-UTF-8 decoding, bounded stdout, and existing ManagedChildProcess abort/error lifecycle compose correctly. The stream remains drained after truncation, so there is no pipe-backpressure deadlock. Exact-head CI is terminal green, the PR is mergeable, and there are no unresolved review threads.
Non-blocking residuals: oversized output is discarded but the child runs until exit/deadline; the multibyte-boundary fix still lacks a regression assertion; and the size-bound error can mask a later abort/nonzero-exit diagnostic. Approval is for this exact head and remains dependent on the lower stack landing cleanly.
4054257 to
e19eea7
Compare
b150c48 to
fbb2735
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-review
Approved at exact rebased head fbb27353ac0a0b3d4625476f7e5331be05b2cabc.
git range-diff confirms the reviewed invocation patch is byte-equivalent to the previously approved commit. Bare stdin rejection, ignored child stdin, multibyte-safe decoding, the 8 MiB stdout bound, and abort/timeout behavior remain intact. Exact-head CI is terminal green. No P0-P2 findings.
Three issues in runFfprobe's process and stream handling. A filePath of exactly "-" hung for 30 seconds. `--` stops option parsing, so "-intro.mp4" is safe, but ffprobe rewrites "-" to `fd:` AFTER option parsing and reads stdin — and stdin was an inherited pipe the parent never writes to and never ends. The probe ran to the deadline and failed with an empty diagnostic, because ffprobe never errored so stderr was blank: 30010 ms and no message, against 28 ms for a normal missing-file error. Rejected up front, and the child now gets stdio ["ignore", ...] so no future invocation can block on stdin either. stdout was decoded per chunk. `stdout += data.toString()` decodes each 64 KiB pipe chunk independently, so a multi-byte character straddling a boundary became U+FFFD on both sides — verified: 200 KB of 3-byte characters produced 15 replacements and a string 9 characters longer than the source. -show_format output above ~64 KiB with non-ASCII tag text returns silently mangled values, since JSON.parse still succeeds. Now accumulated through StringDecoder. Note on testing that one: U+FFFD is valid JSON string content, and nothing on extractMediaMetadata's public surface exposes a tag value, so there is no assertion that fails against the old implementation. Rather than add a test that cannot fail, it is stated here and the bound below is what the new test covers. stdout was unbounded. stderr is capped by ManagedChildProcess but stdout was not, and analyzeKeyframeIntervals emits one line per frame — an all-intra ProRes proxy can produce an arbitrarily large string. Capped at 8M characters, which real -show_streams JSON is nowhere near. Tests: "-" rejected without spawning, the stdio shape, and the size bound. Reverting the stdin guards fails 1. The first draft of the bound checked before appending, so a single oversized chunk passed — the test caught it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e19eea7 to
361fd49
Compare
fbb2735 to
9e27542
Compare
The base branch was changed.
Stack 5/6, on top of #2915. The
-case is the residual hole in #2740's--fix; the stream handling is pre-existing.A filePath of exactly
-hangs for 30 seconds--stops option parsing, so-intro.mp4is safe. But ffprobe rewrites-tofd:after option parsing and then reads stdin — andspawnpassed nostdio, so the child's stdin was a pipe the parent never writes to and never ends.Verified against current
main:extractMediaMetadata("-")ffprobe deadline with code null:The diagnostic is empty because ffprobe never errored, so stderr is blank. Rejected up front now, and the child gets
stdio: ["ignore", "pipe", "pipe"]so no future invocation can block on stdin either.The test added in #2740 covers
/tmp/-dangerous-name.mp4, which--genuinely protects. Bare-was the untested gap.stdout was decoded per chunk
Each 64 KiB pipe chunk decodes independently, so a multi-byte character straddling a boundary becomes U+FFFD on both sides. Reproduced: 200,001 bytes of 3-byte characters produced 15 replacement characters and a string 9 chars longer than the source.
-show_formatoutput above ~64 KiB with non-ASCII tag text returns silently mangled values —JSON.parsestill succeeds, so nothing surfaces it. Now accumulated throughStringDecoder.On testing this one: U+FFFD is valid JSON string content, and nothing on
extractMediaMetadata's public surface exposes a tag value, so there is no assertion that fails against the old implementation. Rather than add a test that cannot fail, it is documented and the bound below is what the new test covers. Happy to export a seam if a reviewer would rather have it pinned.stdout was unbounded
stderr is capped by
ManagedChildProcess; stdout was not.analyzeKeyframeIntervalsemits one line per frame, so an all-intra ProRes proxy can produce an arbitrarily large string. Capped at 8M characters — real-show_streamsJSON is nowhere near it.Verification
-rejected without spawning, the stdio shape, and the size bound. Reverting the stdin guards fails 1. Engine suite: 1280 pass.My first draft of the bound checked before appending, so a single oversized chunk passed straight through — the test caught it.
🤖 Generated with Claude Code