mirror of
https://github.com/stablyai/orca.git
synced 2026-10-06 00:02:43 +00:00
perf(ssh): bound the system-ssh port-forward stderr tail (#7647)
The stderr handler in system-ssh-port-forward-provider.ts stays attached for the forward process's entire lifetime and appends every chunk to an unbounded string, only ever read to build the exit-error detail. A chatty/warning-spamming remote sshd over a long-lived `ssh -N -L` forward could grow it without bound. Fix: keep only the most-recent 64 KB tail, mirroring MAX_RELAY_STARTUP_BUFFER_BYTES in ssh-relay-deploy-helpers.ts. The tail is what the error message surfaces anyway. Test (red->green): emitting >64 KB of stderr then exiting keeps the recent tail marker and drops the oldest, detail bounded below the produced size; without the cap the whole string is retained.
This commit is contained in:
@@ -267,6 +267,27 @@ describe('SshPortForwardManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps only a bounded tail of a chatty forward stderr (memory-leak regression)', async () => {
|
||||
const onForwardClosed = vi.fn()
|
||||
manager.setCallbacks({ onForwardClosed })
|
||||
const forward = createFakeSystemSshForward()
|
||||
startSystemSshPortForwardProcessMock.mockReturnValue(forward)
|
||||
const conn = createSystemSshConn()
|
||||
|
||||
await manager.addForward('conn-1', conn as never, 3000, '127.0.0.1', 8080)
|
||||
// A long-lived forward against a chatty remote sshd: emit >64 KB of stderr.
|
||||
forward.process.stderr.emit('data', Buffer.from(`HEAD_MARKER${'x'.repeat(70 * 1024)}`))
|
||||
forward.process.stderr.emit('data', Buffer.from('TAIL_MARKER'))
|
||||
forward.process.emit('exit', 255)
|
||||
|
||||
const detail = onForwardClosed.mock.calls[0]?.[1]?.detail as string
|
||||
// The oldest bytes are trimmed; the most-recent tail is retained.
|
||||
expect(detail).toContain('TAIL_MARKER')
|
||||
expect(detail).not.toContain('HEAD_MARKER')
|
||||
// Bounded well below the ~70 KB produced.
|
||||
expect(detail.length).toBeLessThan(66 * 1024)
|
||||
})
|
||||
|
||||
it('lists forwards filtered by connectionId', async () => {
|
||||
const conn = createMockConn()
|
||||
await manager.addForward('conn-1', conn as never, 3000, 'localhost', 8080)
|
||||
|
||||
@@ -33,9 +33,17 @@ export class SystemSshPortForwardProvider implements SshPortForwardProvider {
|
||||
)
|
||||
|
||||
await forward.waitForStartup()
|
||||
// Why: this stderr stays attached for the forward's whole lifetime but is
|
||||
// only used to build the exit-error detail, so keep a bounded tail — a chatty
|
||||
// remote sshd could otherwise grow it unbounded on a long-lived forward
|
||||
// (mirrors MAX_RELAY_STARTUP_BUFFER_BYTES in ssh-relay-deploy-helpers).
|
||||
const MAX_STDERR_TAIL_BYTES = 64 * 1024
|
||||
let stderr = ''
|
||||
const onStderr = (chunk: Buffer): void => {
|
||||
stderr += chunk.toString('utf-8')
|
||||
if (stderr.length > MAX_STDERR_TAIL_BYTES) {
|
||||
stderr = stderr.slice(-MAX_STDERR_TAIL_BYTES)
|
||||
}
|
||||
}
|
||||
forward.process.stderr?.on('data', onStderr)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user