Fix XtermPane IPC listener leak on unmount-during-spawn/adopt

Pre-release audit finding: after `unlistenData = await onPaneData(...)` (and
the exit listener) there was no destroyed re-check, so if the pane unmounted
during the await the sync cleanup captured a null unlisten and the
pane://{id}/data subscription leaked. Unlisten before returning in both the
adopt and spawn paths.

Also logs the deferred (low-risk) transfer-refcount leak as a known follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-28 20:34:36 +01:00
parent e6d0040021
commit 309b6024d4
2 changed files with 28 additions and 1 deletions

View file

@ -186,11 +186,22 @@ export default function XtermPane({
term?.write(b64ToBytes(b64));
onDataReceivedRef.current?.();
});
if (destroyed) return;
// `destroyed` may have flipped during the await — the sync cleanup
// already ran and captured a null unlisten, so unlisten here or the
// subscription leaks.
if (destroyed) {
unlistenData?.();
return;
}
unlistenExit = await onPaneExit(paneId, () => {
term?.write("\r\n\x1b[33m[pane exited]\x1b[0m\r\n");
onStatusRef.current?.(`pane ${paneId} exited`, false);
});
if (destroyed) {
unlistenData?.();
unlistenExit?.();
return;
}
// Match the PTY to our cell grid (the source window may have had
// different dimensions).
try {
@ -227,11 +238,20 @@ export default function XtermPane({
term?.write(b64ToBytes(b64));
onDataReceivedRef.current?.();
});
if (destroyed) {
unlistenData?.();
return;
}
unlistenExit = await onPaneExit(paneId, () => {
term?.write("\r\n\x1b[33m[pane exited]\x1b[0m\r\n");
onStatusRef.current?.(`pane ${paneId} exited`, false);
});
if (destroyed) {
unlistenData?.();
unlistenExit?.();
return;
}
}
term?.onData((data) => {