Skip to content

fix: prevent unbounded PersistentProtocol replay buffer - #324160

Open
KevinWang-wpq wants to merge 1 commit into
microsoft:mainfrom
KevinWang-wpq:codex/persistent-protocol-unack-buffer-limit
Open

fix: prevent unbounded PersistentProtocol replay buffer#324160
KevinWang-wpq wants to merge 1 commit into
microsoft:mainfrom
KevinWang-wpq:codex/persistent-protocol-unack-buffer-limit

Conversation

@KevinWang-wpq

@KevinWang-wpq KevinWang-wpq commented Jul 3, 2026

Copy link
Copy Markdown

Fixes a memory leak in PersistentProtocol.

Details

PersistentProtocol keeps every regular message in _outgoingUnackMsg until the peer acknowledges it. That queue is also the replay buffer used when a connection reconnects.

When a connection is dead, congested, or stuck reconnecting, acknowledgements can stop advancing while callers continue to send messages. In that state, _outgoingUnackMsg keeps retaining every unacknowledged payload, including large VSBuffer instances, until the protocol is disposed.

Change

The change bounds the replay buffer by both message count and retained payload bytes. It also tracks the retained bytes so timeout diagnostics can show how much payload memory is currently held by unacknowledged messages.

When either replay-buffer limit is exceeded, PersistentProtocol emits a dedicated unacknowledgedMessageReplayBufferOverflow timeout reason, clears the replay buffer, and advances the local acknowledgement state so the dropped entries are not kept alive.

For remote connections, replay-buffer overflow is treated as unrecoverable instead of reconnecting into a stream that is missing replay messages.

Before

A dead or congested peer could stop sending acknowledgements while new messages were still appended to _outgoingUnackMsg. The replay buffer could then grow without a memory bound and retain all queued message payloads.

After

The replay buffer is capped by configurable/default count and byte limits. When the cap is exceeded, the buffer is released, the timeout log includes the retained byte count, and node IPC regression tests cover both byte-limit and count-limit overflow paths.

Copilot AI review requested due to automatic review settings July 3, 2026 06:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 hardens PersistentProtocol (the reconnection-capable IPC protocol used by the remote agent connection) against unbounded memory growth. The protocol keeps unacknowledged outgoing messages in a replay buffer so they can be re-sent after a reconnection; if a peer stops acknowledging (dead or heavily backpressured), that buffer could grow without limit. The change adds count- and byte-based caps and, when exceeded, emits a socket timeout and drops the buffer.

Changes:

  • Track retained payload bytes (_outgoingUnackMsgBytes) alongside the unacknowledged-message queue, and add configurable/default caps (MaxOutgoingUnacknowledgedMessages = 10000, MaxOutgoingUnacknowledgedBytes = 256MB).
  • On overflow, fire an UNACKNOWLEDGED_MESSAGE socket timeout, clear the replay buffer, and add a Queue.clear() helper; surface retained bytes in the SocketTimeoutEvent and timeout logging.
  • Add a node IPC regression test for the byte-limit path.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/vs/base/parts/ipc/common/ipc.net.ts Adds byte tracking, overflow limits/options, Queue.clear(), overflow-check and buffer-clear methods, and the new unacknowledgedMsgBytes event field.
src/vs/platform/remote/common/remoteAgentConnection.ts Includes unacknowledgedMsgBytes in the socket-timeout log line.
src/vs/base/parts/ipc/test/node/ipc.net.test.ts New regression test verifying the replay buffer is dropped when the byte limit is exceeded.

Comment thread src/vs/base/parts/ipc/common/ipc.net.ts Outdated
Comment thread src/vs/base/parts/ipc/common/ipc.net.ts
Comment thread src/vs/base/parts/ipc/common/ipc.net.ts
@KevinWang-wpq
KevinWang-wpq force-pushed the codex/persistent-protocol-unack-buffer-limit branch from c85f5d4 to 9166625 Compare July 3, 2026 06:19
Fixes a memory leak in `PersistentProtocol`.

`PersistentProtocol` keeps every regular message in `_outgoingUnackMsg`
until the peer acknowledges it. That queue is also the replay buffer used
when a connection reconnects.

When a connection is dead, congested, or stuck reconnecting,
acknowledgements can stop advancing while callers continue to send
messages. In that state, `_outgoingUnackMsg` keeps retaining every
unacknowledged payload until the protocol is disposed.

Bound the replay buffer by both message count and retained payload bytes.
When either limit is exceeded, emit a dedicated replay-buffer-overflow
timeout reason, clear the buffer, and treat the overflow as unrecoverable
for remote connections.

Add node IPC regression tests for both byte-limit and count-limit overflow
paths.
@KevinWang-wpq KevinWang-wpq changed the title Bound PersistentProtocol replay buffer fix: prevent unbounded PersistentProtocol replay buffer Jul 3, 2026
@KevinWang-wpq
KevinWang-wpq force-pushed the codex/persistent-protocol-unack-buffer-limit branch from 9166625 to f0c58de Compare July 3, 2026 06:59
@KevinWang-wpq
KevinWang-wpq requested a review from Copilot July 3, 2026 07:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@KevinWang-wpq

Copy link
Copy Markdown
Author

Adding the anonymized heap-snapshot evidence that motivated this fix.

Heap snapshot evidence

In a renderer heap snapshot, the dominant retained object was the reconnect replay buffer owned by PersistentProtocol:

Object Retained size Notes
Queue@6324627 8.81 GB Held from PersistentProtocol._outgoingUnackMsg
PersistentProtocol@4699437 8.81 GB via _outgoingUnackMsg The protocol's unacknowledged-message replay buffer
First queued payload ~142 MB A queued ProtocolMessage retained a large Uint8Array/buffer payload
Remaining linked-list payloads ~8.67 GB Additional queued messages retained through QueueElement.next

The relevant retainer chain was:

Queue@6324627 (8.81 GB)
  <- [_outgoingUnackMsg] PersistentProtocol@4699437
    <- [protocol] PersistentConnection@4206011
      <- PersistentConnection._instances
        <- Window / GC root

This matches the failure mode addressed here: regular messages are appended to _outgoingUnackMsg for reconnect replay, and if acknowledgements stop advancing while sends continue, the queue can retain all unacknowledged payloads without a memory bound.

This PR adds the missing bound in two dimensions:

  • max unacknowledged replay message count
  • max retained unacknowledged payload bytes

It also treats replay-buffer overflow as unrecoverable for remote connections, so the connection does not reconnect into a stream whose replay messages were already dropped.

@Giuspepe could you please take a look when you have a chance? Copilot's latest re-review on the updated commit generated no new comments, and the PR now includes regression coverage for both byte-limit and count-limit overflow paths.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants