Skip to content

fix: clamp setTimeout value to prevent negative timeout crash#39273

Merged
yury-s merged 2 commits intomicrosoft:mainfrom
veeceey:fix/issue-39166-negative-settimeout
Feb 25, 2026
Merged

fix: clamp setTimeout value to prevent negative timeout crash#39273
yury-s merged 2 commits intomicrosoft:mainfrom
veeceey:fix/issue-39166-negative-settimeout

Conversation

@veeceey
Copy link
Contributor

@veeceey veeceey commented Feb 14, 2026

Problem

When a Node.js process has been running for more than ~24.8 days (2^31-1 milliseconds), monotonicTime() exceeds the max signed 32-bit integer. In raceAgainstDeadline(), the timeout is computed as:

const timeout = (deadline || kMaxDeadline) - monotonicTime();

When monotonicTime() exceeds kMaxDeadline (e.g. no explicit timeout was set, so deadline is 0 and falls back to kMaxDeadline), this produces a negative value. Node.js then coerces it to 1ms and fires the timer immediately, causing operations like browserType.connect() to fail with:

browserType.connect: Timeout undefinedms exceeded

Fix

Clamp the computed timeout to [0, kMaxDeadline]:

  • Lower bound of 0 prevents negative values that Node.js coerces to 1ms
  • Upper bound of kMaxDeadline (2^31-1) keeps it within setTimeout's valid range

How I verified this

Reproduced the issue by overriding performance.now to return a value > 2^31-1, confirming connect() fails before the fix and succeeds after.

Fixes #39166

new Promise<{ timedOut: true }>(resolve => {
const kMaxDeadline = 2147483647; // 2^31-1
const timeout = (deadline || kMaxDeadline) - monotonicTime();
const rawTimeout = (deadline || kMaxDeadline) - monotonicTime();
Copy link
Member

Choose a reason for hiding this comment

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

It is better to not set a timer at all when there is no deadline, here and in other places across the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call -- updated to skip the timer entirely when there's no deadline instead of clamping. If deadline is 0, the promise executor returns early so no setTimeout is created at all. The race then just waits on the callback.

throw new Error(`Could not resolve ${this._selector} to DOM Element`);
try {
return await task(handle, deadline ? deadline - monotonicTime() : 0);
return await task(handle, deadline ? Math.max(deadline - monotonicTime(), 0) : 0);
Copy link
Member

Choose a reason for hiding this comment

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

Why did this change? I don't think Math.max(deadline - monotonicTime(), 0) will be different from deadline - monotonicTime().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, this was a pointless change — the timeout value here is just passed through to the task, not to setTimeout. Reverted it back to the original.

this._controller.abort(timeoutError);
}
}, deadline - monotonicTime());
}, Math.max(deadline - monotonicTime(), 0));
Copy link
Member

Choose a reason for hiding this comment

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

ditto

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same fix here — when remaining time is <= 0, the timeout error is fired immediately without scheduling a timer.

const timeout = (deadline || kMaxDeadline) - monotonicTime();
timer = setTimeout(() => resolve({ timedOut: true }), timeout);
const timeout = Math.max(deadline - monotonicTime(), 0);
timer = setTimeout(() => resolve({ timedOut: true }), Math.min(timeout, kMaxDeadline));
Copy link
Member

Choose a reason for hiding this comment

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

I thought we agreed to not set timer at all. Here we are at risk of firing in 0ms.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, clamping to 0 still fires the timer. Updated to check deadline - monotonicTime() <= 0 and resolve immediately with { timedOut: true } — no setTimeout at all when the deadline has already passed.

if (this.metadata.pauseStartTime && !this.metadata.pauseEndTime)
return;
const remaining = deadline - monotonicTime();
if (remaining <= 0) {
Copy link
Member

Choose a reason for hiding this comment

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

We can't be doing this, we've already measured monotonicTime once and built deadline on top of it, so we should never end up in this branch. If there is deadline, ever so little, we set timer. Otherwise, if deadline is 0 we do not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, sorry about that. Reverted progress.ts entirely -- if deadline is set then the timer should always have positive remaining time since it was just computed. The only change now is in timeoutRunner.ts: skip the timer when deadline is 0 instead of computing a ~24-day timeout from kMaxDeadline.

@veeceey
Copy link
Contributor Author

veeceey commented Feb 23, 2026

You're absolutely right — measuring monotonicTime twice defeats the purpose. I'll rework this to keep it simple: if deadline is set (deadline > 0), create the timer; if deadline is 0, skip the timer entirely. That aligns with the original measurement and avoids the redundant second check. Will push an updated approach shortly.

When deadline is 0, return early from the promise executor instead of
computing a huge timeout via (0 || kMaxDeadline). This avoids setting
an unnecessary ~24-day timer. When deadline is set, cap the timeout
with Math.min to stay within setTimeout's 32-bit limit.
@veeceey veeceey force-pushed the fix/issue-39166-negative-settimeout branch from 81656f6 to 18e6a9b Compare February 23, 2026 02:29
When deadline is 0 (no timeout), skip the timer entirely instead of
computing a fallback from kMaxDeadline. When deadline is set, use
deadline - monotonicTime() directly as the timer value. Remove the
kMaxDeadline constant since it's no longer needed.
@github-actions
Copy link
Contributor

Test results for "tests 1"

2 flaky ⚠️ [firefox-library] › library/beforeunload.spec.ts:188 › does not get stalled by beforeUnload `@firefox-ubuntu-22.04-node20`
⚠️ [firefox-library] › library/inspector/cli-codegen-3.spec.ts:981 › cli codegen › should record when manual popover with fullscreen backdrop is open `@firefox-ubuntu-22.04-node20`

37556 passed, 838 skipped


Merge workflow run.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: negative setTimeout in timeoutRunner causing browserType.connect() crash

3 participants