Skip to content

fix: chunk multiline PTY writes on macOS to avoid 1024-byte buffer corruption#298993

Merged
justschen merged 3 commits intomicrosoft:mainfrom
jcansdale:jcansdale/pty-multiline-test
Mar 10, 2026
Merged

fix: chunk multiline PTY writes on macOS to avoid 1024-byte buffer corruption#298993
justschen merged 3 commits intomicrosoft:mainfrom
jcansdale:jcansdale/pty-multiline-test

Conversation

@jcansdale
Copy link
Copy Markdown
Contributor

@jcansdale jcansdale commented Mar 3, 2026

Fixes #296955

Problem

macOS PTY has a ~1024-byte canonical-mode input buffer. When multiline commands (containing CR/newline characters) exceed this threshold, the shell's line editor echoes characters back, creating backpressure that corrupts the write — data after ~1024 bytes wraps around and replays earlier buffer content, destroying the remainder of the command and leaving the shell stuck.

This affects any multiline terminal input over ~1KB: heredocs, gh issue create --body, inline Python/Node scripts, long git commit messages, etc.

Evidence

The first commit adds a test (without the fix) that failed on macOS CI, reproducing the exact corruption pattern:

+ L19 aaaaaaaaaaaaL19 aaaaaaaaaaaaL19 aaaaaaaaaaaaL19 aaaaaaaaaaa...
- L19 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- L20 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
  ...lines L20-L30 lost entirely...

The test passed on Linux and Windows — only macOS is affected.

Fix

In TerminalProcess.input(), when on macOS and the data is multiline and exceeds 512 bytes, write in 512-byte chunks with 5ms pauses between them. This allows the shell's echo buffer to drain between chunks, preventing the backpressure deadlock.

  • Non-macOS platforms: unaffected (direct write as before)
  • Single-line commands: unaffected (no backpressure from line editor)
  • Binary writes: unaffected

Changed files

  • src/vs/platform/terminal/node/terminalProcess.ts_writeChunked() method + chunking branch in input()
  • src/vs/platform/terminal/test/node/terminalProcess.test.ts (new) — integration tests spawning real PTY processes with 10/20/30-line multiline commands

See also: https://github.com/jcansdale/macos-pty-multiline-bug (standalone reproducers confirming this is a macOS kernel-level issue)

Adds a test that sends multiline commands of varying sizes (10, 20, 30 lines)
through TerminalProcess.input() and verifies the data arrives intact at the
shell. On macOS, multiline commands exceeding ~1024 bytes corrupt due to PTY
canonical-mode input buffer backpressure.

Reproduces: microsoft#296955
Copilot AI review requested due to automatic review settings March 3, 2026 16:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new test file src/vs/platform/terminal/test/node/terminalProcess.test.ts to document and reproduce a macOS PTY bug where multiline commands exceeding approximately 1024 bytes get corrupted (issue #296955). The test spawns a real PTY process via TerminalProcess, sends a heredoc command of varying sizes through TerminalProcess.input(), and verifies the shell received the complete payload by checking the output file contents.

Changes:

  • Adds a new test file with 3 test cases (10-, 20-, and 30-line heredoc commands) to reproduce the PTY 1024-byte buffer bug on macOS/Linux.
…rruption

macOS PTY has a ~1024-byte canonical-mode input buffer. When multiline data
(containing CR characters) exceeds this threshold, the shell's line editor
echoes characters back, creating backpressure that corrupts the write.

Write multiline PTY input in 512-byte chunks with 5ms pauses between them
to allow the echo buffer to drain. Non-macOS platforms and single-line
writes are unaffected.

Fixes microsoft#296955
@jcansdale jcansdale force-pushed the jcansdale/pty-multiline-test branch from a09dc3f to 0fd94f3 Compare March 3, 2026 18:01
@jcansdale jcansdale marked this pull request as ready for review March 3, 2026 19:10
@vs-code-engineering
Copy link
Copy Markdown
Contributor

vs-code-engineering bot commented Mar 3, 2026

📬 CODENOTIFY

The following users are being notified based on files changed in this PR:

@Tyriar

Matched files:

  • src/vs/platform/terminal/node/terminalProcess.ts
}
this._ptyProcess!.write(data.slice(i, i + 512));
if (i + 512 < data.length) {
await timeout(5);
Copy link
Copy Markdown
Collaborator

@meganrogge meganrogge Mar 4, 2026

Choose a reason for hiding this comment

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

does timeout(0) work here?

Copy link
Copy Markdown
Contributor Author

@jcansdale jcansdale Mar 5, 2026

Choose a reason for hiding this comment

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

Tested locally — timeout(0) doesn't work. The kernel genuinely needs wall-clock time to drain the PTY echo buffer, not just an event loop yield.

Results with different values:

timeout() 10 lines (700B) 20 lines (1.3KB) 500 lines (32KB)
0
1
5
  • timeout(0) only sub-1KB passes (below the corruption threshold, so chunking isn't even needed)
  • timeout(1) fails at larger payloads (30+ lines)
  • timeout(5) passes up to 500 lines (32KB), 32x the original corruption threshold

Also bumped the "large" test from 30 lines to 500 lines (32KB) to give more confidence at scale.

@meganrogge meganrogge added this to the 1.112.0 milestone Mar 4, 2026
@meganrogge meganrogge enabled auto-merge (squash) March 4, 2026 21:46
@meganrogge meganrogge disabled auto-merge March 4, 2026 21:46
@austenstone
Copy link
Copy Markdown
Member

Confirmed locally on macOS in Copilot terminal execution: small multiline heredocs succeed, slightly larger multiline heredocs get corrupted before Python can parse them, while a much longer single-line command still works.

Proof from the same session:

  • 70-line heredoc: ✅ passed (lines 70, valid SHA, correct tail)
  • 80-line heredoc: ❌ corrupted in transit with Python seeing mutated source like print('taprint('taoaprinlitlines()[-3:])
  • 1759-char single-line python3 -c ...: ✅ passed

So I can independently confirm the symptom is specifically large multiline terminal input -> transport corruption, not just “big commands fail”. This lines up with #296955 and the chunked-write mitigation in this PR.

Copy link
Copy Markdown
Collaborator

@meganrogge meganrogge left a comment

Choose a reason for hiding this comment

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

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

7 participants