From 4ef0c0a8b184721e081e65a79a5fd6d968a4474a Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Thu, 12 Nov 2026 02:42:10 -0400 Subject: [PATCH] fix: prevent overflow when parsing extreme decimal exponents --- .../io/questdb/client/std/DecimalParser.java | 21 +++++--- .../test/std/DecimalExponentLimitsTest.java | 53 +++++++++++++++++++ 2 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 core/src/test/java/io/questdb/client/test/std/DecimalExponentLimitsTest.java diff --git a/core/src/main/java/io/questdb/client/std/DecimalParser.java b/core/src/main/java/io/questdb/client/std/DecimalParser.java index 86fabdcbc..d2ac7431d 100644 --- a/core/src/main/java/io/questdb/client/std/DecimalParser.java +++ b/core/src/main/java/io/questdb/client/std/DecimalParser.java @@ -196,9 +196,9 @@ public static long parse(Decimal decimal, CharSequence cs, int lo, int hi, int p int literalScale = dot == -1 ? 0 : (digitHi - dot - 1); int literalDigits = digitHi - digitLo - (dot == -1 ? 0 : 1); - int virtualScale = literalScale; + long virtualScale = literalScale; - int exp = 0; + long exp = 0; if (lo != hi) { // Parses exponent if ((cs.charAt(lo) | 32) == 'e') { @@ -222,7 +222,7 @@ public static long parse(Decimal decimal, CharSequence cs, int lo, int hi, int p // If lossy is enabled, we can strip digits after the provided scale (as long as it is provided) if (lossy && scale >= 0 && virtualScale > scale) { - int drop = virtualScale - scale; + long drop = virtualScale - scale; while (drop > 0 && digitHi > digitLo) { digitHi--; if (digitHi == dot) { @@ -237,7 +237,12 @@ public static long parse(Decimal decimal, CharSequence cs, int lo, int hi, int p // We still need to adjust the exponent if the user specifies a target scale. if (scale == -1) { - scale = virtualScale; + if (virtualScale > decimal.getMaxScale()) { + throw NumericException.instance() + .put("decimal '").put(cs) + .put("' exceeds maximum allowed scale of ").put(decimal.getMaxScale()); + } + scale = (int) virtualScale; } else if (virtualScale <= scale) { exp += scale - virtualScale; } else { @@ -258,8 +263,8 @@ public static long parse(Decimal decimal, CharSequence cs, int lo, int hi, int p // precision, as long as it's lower. // Compute the final precision of the decimal - int pow = literalDigits + exp; - final int finalPrecision = Math.max(pow, scale + 1); + long pow = literalDigits + exp; + final long finalPrecision = Math.max(pow, (long) scale + 1); if (precision != -1 && finalPrecision > precision) { throw NumericException.instance() .put("decimal '").put(cs) @@ -278,7 +283,7 @@ public static long parse(Decimal decimal, CharSequence cs, int lo, int hi, int p continue; } - decimal.addPowerOfTenMultiple(--pow, cs.charAt(p) - '0'); + decimal.addPowerOfTenMultiple((int) --pow, cs.charAt(p) - '0'); } decimal.setScale(scale); @@ -286,7 +291,7 @@ public static long parse(Decimal decimal, CharSequence cs, int lo, int hi, int p decimal.negate(); } - return Numbers.encodeLowHighInts(finalPrecision, scale); + return Numbers.encodeLowHighInts((int) finalPrecision, scale); } /** diff --git a/core/src/test/java/io/questdb/client/test/std/DecimalExponentLimitsTest.java b/core/src/test/java/io/questdb/client/test/std/DecimalExponentLimitsTest.java new file mode 100644 index 000000000..284b9cb36 --- /dev/null +++ b/core/src/test/java/io/questdb/client/test/std/DecimalExponentLimitsTest.java @@ -0,0 +1,53 @@ +/*+***************************************************************************** + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2026 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package io.questdb.client.test.std; + +import io.questdb.client.std.Decimal128; +import io.questdb.client.std.Decimal256; +import io.questdb.client.std.Decimal64; +import io.questdb.client.std.NumericException; +import org.junit.Assert; +import org.junit.Test; + +public class DecimalExponentLimitsTest { + + @Test + public void testExponentAtIntLimitsIsOutOfRange() { + for (String value : new String[]{"1e2147483647", "-1e2147483647", "1e-2147483648", "1.5e-2147483647"}) { + assertOutOfRange(value, () -> new Decimal64().ofString(value)); + assertOutOfRange(value, () -> new Decimal128().ofString(value)); + assertOutOfRange(value, () -> new Decimal256().ofString(value)); + } + } + + private static void assertOutOfRange(String value, Runnable parse) { + try { + parse.run(); + Assert.fail("expected NumericException for " + value); + } catch (NumericException expected) { + // Out-of-range decimal text must be rejected with NumericException. + } + } +}