From f3c8d0ca55ec7b0bcc53065d1919f092c6133913 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sun, 17 May 2026 23:35:13 +0800 Subject: [PATCH] fix(core): close TOCTOU race in delay() abort handling Move the initial signal.aborted check inside the Promise constructor and add a re-check after addEventListener to close the race window where an abort between the check and listener registration would be silently lost. Also add clarifying comments on the intentional attempt:1 usage in retry delay calculations. --- packages/core/src/utils/retry.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/core/src/utils/retry.ts b/packages/core/src/utils/retry.ts index 43be64591..92b1ae541 100644 --- a/packages/core/src/utils/retry.ts +++ b/packages/core/src/utils/retry.ts @@ -93,11 +93,10 @@ export function isUnattendedMode(): boolean { * @returns A promise that resolves after the delay. */ function delay(ms: number, signal?: AbortSignal): Promise { - if (signal?.aborted) { - return Promise.reject(new Error('Retry aborted by signal')); - } - return new Promise((resolve, reject) => { + if (signal?.aborted) { + return reject(new Error('Retry aborted by signal')); + } const cleanup = () => { signal?.removeEventListener('abort', onAbort); }; @@ -112,6 +111,12 @@ function delay(ms: number, signal?: AbortSignal): Promise { resolve(); }, ms); signal?.addEventListener('abort', onAbort, { once: true }); + // Re-check after listener registration to close the TOCTOU race window. + if (signal?.aborted) { + clearTimeout(timeout); + cleanup(); + reject(new Error('Retry aborted by signal')); + } }); } @@ -208,6 +213,8 @@ export async function retryWithBackoff( shouldRetryOnContent(result as GenerateContentResponse) ) { const delayMs = getRetryDelayMs({ + // attempt: 1 — currentDelay already tracks exponential growth; + // getRetryDelayMs is called here only for jitter calculation. attempt: 1, initialDelayMs: currentDelay, maxDelayMs, @@ -316,6 +323,8 @@ export async function retryWithBackoff( } else { logRetryAttempt(attempt, error, retryClassification, errorStatus); const delayMs = getRetryDelayMs({ + // attempt: 1 — currentDelay already tracks exponential growth; + // getRetryDelayMs is called here only for jitter calculation. attempt: 1, initialDelayMs: currentDelay, maxDelayMs,