From 1f39a680df8b7fdbb673498edb9a6d42a3b40ea7 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 13 Feb 2026 16:05:47 +0100 Subject: [PATCH] fix: guard setInterval delays against 32-bit integer overflow --- src/charging-station/ChargingStation.ts | 18 +++++++++++------- src/utils/Constants.ts | 4 ++++ src/utils/Utils.ts | 11 +++++++++++ src/utils/index.ts | 1 + tests/utils/Utils.test.ts | 15 +++++++++++++++ 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 18083c60..6154c0ea 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -76,6 +76,7 @@ import { buildStartedMessage, buildStoppedMessage, buildUpdatedMessage, + clampToSafeTimerValue, clone, Configuration, Constants, @@ -987,7 +988,7 @@ export class ChargingStation extends EventEmitter { error ) }) - }, heartbeatInterval) + }, clampToSafeTimerValue(heartbeatInterval)) logger.info( `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds( heartbeatInterval @@ -1060,7 +1061,7 @@ export class ChargingStation extends EventEmitter { error ) }) - }, interval) + }, clampToSafeTimerValue(interval)) } else { logger.error( `${this.logPrefix()} Charging station ${ @@ -2510,11 +2511,14 @@ export class ChargingStation extends EventEmitter { private startWebSocketPing (): void { const webSocketPingInterval = this.getWebSocketPingInterval() if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) { - this.wsPingSetInterval = setInterval(() => { - if (this.isWebSocketConnectionOpened()) { - this.wsConnection?.ping() - } - }, secondsToMilliseconds(webSocketPingInterval)) + this.wsPingSetInterval = setInterval( + () => { + if (this.isWebSocketConnectionOpened()) { + this.wsConnection?.ping() + } + }, + clampToSafeTimerValue(secondsToMilliseconds(webSocketPingInterval)) + ) logger.info( `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds( webSocketPingInterval diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index 91072789..054c6a98 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -98,6 +98,10 @@ export class Constants { static readonly MAX_RANDOM_INTEGER = 281474976710655 // 2^48 - 1 (randomInit() limit) + // Node.js setInterval/setTimeout maximum safe delay value (2^31-1 ms ≈ 24.8 days) + // Values exceeding this limit cause Node.js to reset the delay to 1ms + static readonly MAX_SETINTERVAL_DELAY = 2147483647 // Ms + static readonly OCPP_VALUE_ABSOLUTE_MAX_LENGTH = 2500 static readonly PERFORMANCE_RECORDS_TABLE = 'performance_records' diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 1fbcf22a..84aabf37 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -21,6 +21,7 @@ import { type UUIDv4, WebSocketCloseEventStatusString, } from '../types/index.js' +import { Constants } from './Constants.js' type NonEmptyArray = [T, ...T[]] type ReadonlyNonEmptyArray = readonly [T, ...(readonly T[])] @@ -380,6 +381,16 @@ export const exponentialDelay = (retryNumber = 0, delayFactor = 100): number => return delay + randomSum } +/** + * Clamps a timer delay value to the safe range for Node.js setInterval/setTimeout. + * @param delayMs - The delay value in milliseconds. + * @returns The clamped delay value, guaranteed to be within [0, 2^31-1] ms. + * @see https://nodejs.org/api/timers.html#settimeoutcallback-delay-args + */ +export const clampToSafeTimerValue = (delayMs: number): number => { + return Math.min(Math.max(0, delayMs), Constants.MAX_SETINTERVAL_DELAY) +} + /** * Generates a cryptographically secure random number in the [0,1[ range * @returns A number in the [0,1[ range diff --git a/src/utils/index.ts b/src/utils/index.ts index f248b5ce..6a78a224 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -27,6 +27,7 @@ export { } from './MessageChannelUtils.js' export { average, max, median, min, percentile, std } from './StatisticUtils.js' export { + clampToSafeTimerValue, clone, convertToBoolean, convertToDate, diff --git a/tests/utils/Utils.test.ts b/tests/utils/Utils.test.ts index f6368dba..43d34c39 100644 --- a/tests/utils/Utils.test.ts +++ b/tests/utils/Utils.test.ts @@ -11,6 +11,7 @@ import type { TimestampedData } from '../../src/types/index.js' import { JSRuntime, runtime } from '../../scripts/runtime.js' import { Constants } from '../../src/utils/Constants.js' import { + clampToSafeTimerValue, clone, convertToBoolean, convertToDate, @@ -440,4 +441,18 @@ await describe('Utils test suite', async () => { expect(isArraySorted([1, 2, 3, 5, 4], (a, b) => a - b)).toBe(false) expect(isArraySorted([2, 1, 3, 4, 5], (a, b) => a - b)).toBe(false) }) + + await it('Verify clampToSafeTimerValue()', () => { + expect(clampToSafeTimerValue(0)).toBe(0) + expect(clampToSafeTimerValue(1000)).toBe(1000) + expect(clampToSafeTimerValue(Constants.MAX_SETINTERVAL_DELAY)).toBe( + Constants.MAX_SETINTERVAL_DELAY + ) + expect(clampToSafeTimerValue(Constants.MAX_SETINTERVAL_DELAY + 1)).toBe( + Constants.MAX_SETINTERVAL_DELAY + ) + expect(clampToSafeTimerValue(Number.MAX_SAFE_INTEGER)).toBe(Constants.MAX_SETINTERVAL_DELAY) + expect(clampToSafeTimerValue(-1)).toBe(0) + expect(clampToSafeTimerValue(-1000)).toBe(0) + }) }) -- 2.53.0