]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
fix: guard setInterval delays against 32-bit integer overflow
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 13 Feb 2026 15:05:47 +0000 (16:05 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 13 Feb 2026 15:09:03 +0000 (16:09 +0100)
src/charging-station/ChargingStation.ts
src/utils/Constants.ts
src/utils/Utils.ts
src/utils/index.ts
tests/utils/Utils.test.ts

index 18083c609813c4c25edc73edb5f0a6b79159de0a..6154c0ea93d898d6714022dd394b0bd2ae0adea5 100644 (file)
@@ -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
index 91072789eeee4aa23c9ae1a34dc1c66da87087ed..054c6a98e65b39e10fa4a3727233bdd4d949f0fe 100644 (file)
@@ -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'
index 1fbcf22aaf9b1c9616c27d4d062f998f27f358aa..84aabf3700484e458446dcf881671eeb3bf7c678 100644 (file)
@@ -21,6 +21,7 @@ import {
   type UUIDv4,
   WebSocketCloseEventStatusString,
 } from '../types/index.js'
+import { Constants } from './Constants.js'
 
 type NonEmptyArray<T> = [T, ...T[]]
 type ReadonlyNonEmptyArray<T> = 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
index f248b5ce873fb6f207f3d8d2279406ea3d0b2ba8..6a78a224c033a6c604a08d09d3da30bfe02c2d3e 100644 (file)
@@ -27,6 +27,7 @@ export {
 } from './MessageChannelUtils.js'
 export { average, max, median, min, percentile, std } from './StatisticUtils.js'
 export {
+  clampToSafeTimerValue,
   clone,
   convertToBoolean,
   convertToDate,
index f6368dba7149b2cc131ef9874d0a5ee521d87fff..43d34c394a6a65783e928231515381151c60af0d 100644 (file)
@@ -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<number>([1, 2, 3, 5, 4], (a, b) => a - b)).toBe(false)
     expect(isArraySorted<number>([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)
+  })
 })