From 6645f04b022ca3b3b819993e58ba9f0470407a6d Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 30 Oct 2026 10:54:49 -0400 Subject: [PATCH] fix: Clamp an over-large start wait time instead of overflowing LDClient's constructor converted config.startWait to milliseconds with Duration.toMillis(), which throws ArithmeticException for a duration too large to represent in milliseconds (longer than ~292 million years). The conversion ran after dataSystem.start(), so the exception escaped the constructor with the data system already started and no reference available to close it, leaking the client. The start wait is now converted inside the wait block, clamping a value at or beyond the largest representable millisecond duration to Long.MAX_VALUE rather than converting it. A start wait that large already behaved as the maximum representable wait, so this adds no new behavior; it only replaces the overflow-and-leak with that existing bounded maximum. Normal values are unaffected. --- .../com/launchdarkly/sdk/server/LDClient.java | 19 +++++++++++++++---- .../launchdarkly/sdk/server/LDClientTest.java | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java index 18f314ca..593673ff 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java @@ -31,6 +31,7 @@ import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -72,6 +73,10 @@ public final class LDClient implements LDClientInterface { private static final int EXCESSIVE_INIT_WAIT_MILLIS = 60000; + // The largest start wait the SDK can represent in milliseconds. A longer wait is clamped to it rather than + // converted, because converting it would overflow and throw (and would do so after the data system starts). + private static final Duration MAX_MILLIS_START_WAIT = Duration.ofMillis(Long.MAX_VALUE); + /** * Creates a new client instance that connects to LaunchDarkly with the default configuration. *

@@ -259,15 +264,21 @@ public LDClient(String sdkKey, LDConfig config) { // Start the data system Future startFuture = dataSystem.start(); if (!config.startWait.isZero() && !config.startWait.isNegative()) { + // Convert the start wait to milliseconds, clamping a wait too large to represent to the maximum rather + // than converting it, which would overflow and throw. Only the positive boundary matters here, since + // zero and negative waits are excluded above. + long startWaitMillis = config.startWait.compareTo(MAX_MILLIS_START_WAIT) >= 0 + ? Long.MAX_VALUE + : config.startWait.toMillis(); if (!dataSystem.isInitialized()) { baseLogger.info("Waiting up to {} milliseconds for LaunchDarkly client to start...", - config.startWait.toMillis()); - if (config.startWait.toMillis() > EXCESSIVE_INIT_WAIT_MILLIS) { - baseLogger.warn("LaunchDarkly client created with start wait time of {} milliseconds. We recommend a timeout of less than {} milliseconds.", config.startWait.toMillis(), EXCESSIVE_INIT_WAIT_MILLIS); + startWaitMillis); + if (startWaitMillis > EXCESSIVE_INIT_WAIT_MILLIS) { + baseLogger.warn("LaunchDarkly client created with start wait time of {} milliseconds. We recommend a timeout of less than {} milliseconds.", startWaitMillis, EXCESSIVE_INIT_WAIT_MILLIS); } } try { - startFuture.get(config.startWait.toMillis(), TimeUnit.MILLISECONDS); + startFuture.get(startWaitMillis, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { baseLogger.error("Timeout encountered waiting for LaunchDarkly client initialization"); } catch (Exception e) { diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java index e313aa68..a53c80f2 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java @@ -331,6 +331,25 @@ public void willWaitForDataSourceIfWaitMillisIsGreaterThanZero() throws Exceptio mocks.verifyAll(); } + @Test + public void startWaitTooLargeForMillisIsClampedRatherThanOverflowing() throws Exception { + // A start wait time too large to represent in milliseconds must be clamped, not converted directly: + // converting it throws ArithmeticException, and it would do so after the data source has started, leaving + // the data source running with no reference available to close it. + LDConfig.Builder config = new LDConfig.Builder() + .startWait(Duration.ofSeconds(Long.MAX_VALUE)); + + expect(dataSource.start()).andReturn(initFuture); + expect(initFuture.get(Long.MAX_VALUE, TimeUnit.MILLISECONDS)).andReturn(null); + expect(dataSource.isInitialized()).andReturn(false).anyTimes(); + mocks.replayAll(); + + client = createMockClient(config); + assertFalse(client.isInitialized()); + + mocks.verifyAll(); + } + @Test public void noWaitForDataSourceIfWaitMillisIsNegative() throws Exception { LDConfig.Builder config = new LDConfig.Builder()