From 4c55629ef10ef69802f25ac4284adb6eb3f7b344 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:24:16 -0700 Subject: [PATCH] fix(daemon): enforce stderr retention bound --- .../daemon/daemon-child-exit-observer.test.ts | 27 +++++++++++++++++++ src/main/daemon/daemon-child-exit-observer.ts | 13 ++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/main/daemon/daemon-child-exit-observer.test.ts b/src/main/daemon/daemon-child-exit-observer.test.ts index 225cbdd1d7a..af9e4c61c3f 100644 --- a/src/main/daemon/daemon-child-exit-observer.test.ts +++ b/src/main/daemon/daemon-child-exit-observer.test.ts @@ -46,6 +46,33 @@ describe('observeDaemonChildExit', () => { ) }) + it('retains only the configured bytes from an oversized stderr chunk', () => { + const child = createChild() + const onExit = vi.fn() + const observer = observeDaemonChildExit(child, onExit, 8) + + observer.markReady() + child.stderr.write(Buffer.from(`${'x'.repeat(1024 * 1024)}FATAL`)) + child.emit('exit', 134, null) + child.stderr.emit('end') + + expect(onExit).toHaveBeenCalledWith(expect.objectContaining({ stderrTail: 'xxxFATAL' })) + }) + + it('reports once when stderr closes before the child exits', () => { + const child = createChild() + const onExit = vi.fn() + const observer = observeDaemonChildExit(child, onExit) + + observer.markReady() + child.stderr.emit('close') + child.emit('exit', 134, null) + child.emit('exit', 134, null) + + expect(onExit).toHaveBeenCalledOnce() + expect(onExit).toHaveBeenCalledWith(expect.objectContaining({ stderrTail: '' })) + }) + it('swallows stderr errors after process exit while awaiting drain', () => { const child = createChild() const onExit = vi.fn() diff --git a/src/main/daemon/daemon-child-exit-observer.ts b/src/main/daemon/daemon-child-exit-observer.ts index 60d891b72fc..593e854815a 100644 --- a/src/main/daemon/daemon-child-exit-observer.ts +++ b/src/main/daemon/daemon-child-exit-observer.ts @@ -28,6 +28,17 @@ export type DaemonChildExitObserver = { stop(options?: { destroyStderr?: boolean }): void } +function appendStderrTail(current: Buffer, chunk: Buffer, maxBytes: number): Buffer { + if (maxBytes <= 0) { + return Buffer.alloc(0) + } + if (chunk.length >= maxBytes) { + return Buffer.from(chunk.subarray(-maxBytes)) + } + const currentBytes = Math.min(current.length, maxBytes - chunk.length) + return Buffer.concat([current.subarray(-currentBytes), chunk], currentBytes + chunk.length) +} + export function observeDaemonChildExit( child: ObservableDaemonChild, recordExit: (observation: DaemonChildExitObservation) => void, @@ -42,7 +53,7 @@ export function observeDaemonChildExit( const onStderr = (chunk: Buffer | string): void => { const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) - stderrTail = Buffer.concat([stderrTail, bytes]).subarray(-maxStderrBytes) + stderrTail = appendStderrTail(stderrTail, bytes, maxStderrBytes) } const onStderrError = (): void => {} const finalizeExit = (): void => {