From 964d8e7386871641a9c8a5162db2a2ffebdb8528 Mon Sep 17 00:00:00 2001 From: Henrib Date: Sun, 30 Aug 2026 13:27:59 +0200 Subject: [PATCH 1/6] JEXL-471 : Runtime hardening: Constrain BigInteger operations and ensure regex interruptibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Make regex matching (=~ operator) interruptible * Wrap matched string in InterruptibleCharSequence * Samples Thread.isInterrupted() every 256 chars * Throws ArithmeticException -> JexlException.Cancel on interruption * Add length guard on regex patterns (max 2048 chars) 2. Enforce MathContext precision on BigInteger arithmetic * Move checkBigIntegerPrecision() outside try-catch in add()/subtract()/etc * Prevent ArithmeticException from being silently swallowed * Bounded results prevent memory exhaustion 3. Prevent O(n²) DoS from huge BigInteger literals at parse time * Cap literal digit count by MathContext.getPrecision() * Fallback to hardcoded 256-digit limit if no precision configured * NumberFormatException wraps as JexlException.Parsing Tests added: * testRegexMatchingInterruptible() * testRegexPatternTooLong() * testBigIntegerArithmeticPrecisionCap() * testBigIntegerLiteralTooLong() Co-Authored-By: Claude --- .gitignore | 1 + src/changes/changes.xml | 1 + .../apache/commons/jexl3/JexlArithmetic.java | 91 +++++++++++++++++-- .../commons/jexl3/internal/Interpreter.java | 31 ++++++- .../commons/jexl3/internal/Operator.java | 3 + .../commons/jexl3/parser/NumberParser.java | 37 ++++++++ .../apache/commons/jexl3/parser/Parser.jjt | 4 + .../apache/commons/jexl3/ArithmeticTest.java | 71 +++++++++++++++ 8 files changed, 229 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 311c0a2be..c011a020b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ target/ # NetBeans files nb-configuration.xml nbactions.xml +/CLAUDE.md diff --git a/src/changes/changes.xml b/src/changes/changes.xml index ed248075b..9260fa1c6 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -38,6 +38,7 @@ Add messages when throwing NullPointerException. Improve robustness of introspection permissions and sandbox delegation: close nested-class and interface-method denial gaps, fix class-initialization ordering, gate sandbox iteration, fix permission-parser polarity, deny second-stage compiler surface under RESTRICTED, and enforce parser feature restrictions in sub-parsers. Fix several parser and interpreter correctness issues: Unicode escape hex-digit validation, safe-navigation array-access child indexing, regex escape preservation, empty()/size() error and cancellation propagation, and switch+continue semantics. + Runtime hardening: make regex matching (=~ operator) interruptible, enforce MathContext precision on BigInteger arithmetic results, and prevent O(n²) DoS from huge BigInteger literals at parse time. Bump org.apache.commons:commons-parent from 102 to 104. diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java index 6cfccba21..aa3036579 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java +++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java @@ -218,6 +218,9 @@ public interface Uberspect { */ public static final Pattern FLOAT_PATTERN = Pattern.compile("^[+-]?\\d*(\\.\\d*)?([eE][+-]?\\d+)?$"); + /** Maximum length of a regex pattern string for the {@code =~} operator (JEXL-security f012). */ + protected static final int REGEX_PATTERN_MAX_LENGTH = 2048; + /** * Attempts transformation of potential array in an abstract list or leave as is. *

An array (as in int[]) is not convenient to call methods so when encountered we turn them into lists

@@ -340,6 +343,7 @@ public Object add(final Object left, final Object right) { ? left instanceof String || right instanceof String : left instanceof String && right instanceof String; if (!strconcat) { + BigInteger bigIntResult = null; try { final boolean strictCast = isStrict(JexlOperator.ADD); // if both (non-null) args fit as long @@ -370,11 +374,14 @@ public Object add(final Object left, final Object right) { // otherwise treat as BigInteger final BigInteger l = toBigInteger(strictCast, left); final BigInteger r = toBigInteger(strictCast, right); - final BigInteger result = l.add(r); - return narrowBigInteger(left, right, result); + bigIntResult = l.add(r); } catch (final ArithmeticException nfe) { // ignore and continue in sequence } + // precision check is outside the catch so it is not silently swallowed (f013) + if (bigIntResult != null) { + return narrowBigInteger(left, right, checkBigIntegerPrecision(bigIntResult)); + } } return (left == null ? "" : toString(left)).concat(right == null ? "" : toString(right)); } @@ -591,10 +598,14 @@ public Boolean contains(final Object container, final Object value) { } // use arithmetic / pattern matching ? if (container instanceof java.util.regex.Pattern) { - return ((java.util.regex.Pattern) container).matcher(value.toString()).matches(); + return ((java.util.regex.Pattern) container).matcher(new InterruptibleCharSequence(value.toString())).matches(); } if (container instanceof CharSequence) { - return value.toString().matches(container.toString()); + final String regex = container.toString(); + if (regex.length() > REGEX_PATTERN_MAX_LENGTH) { + throw new ArithmeticException("regular expression too long: " + regex.length() + " > " + REGEX_PATTERN_MAX_LENGTH); + } + return Pattern.compile(regex).matcher(new InterruptibleCharSequence(value.toString())).matches(); } // try contains on map key if (container instanceof Map) { @@ -908,6 +919,28 @@ public boolean equals(final Object left, final Object right) { return compare(left, right, EQ) == 0; } + /** + * Guards a BigInteger result against exceeding the arithmetic context's precision (JEXL-security f013). + *

When {@link MathContext#getPrecision()} is zero (unlimited), no limit is enforced. + * Otherwise, the BigInteger must fit within approximately that many significant decimal digits.

+ * + * @param big the value to check + * @return big unchanged if within the limit + * @throws ArithmeticException when the limit is exceeded + */ + protected BigInteger checkBigIntegerPrecision(final BigInteger big) { + final int precision = getMathContext().getPrecision(); + if (precision > 0) { + // precision 0 means unlimited; otherwise, one decimal digit ≈ log2(10) ≈ 10/3 bits + final int maxBits = precision * 10 / 3 + 1; + if (big.bitLength() > maxBits) { + throw new ArithmeticException( + "BigInteger precision exceeded: " + big.bitLength() + " bits for " + precision + "-digit context"); + } + } + return big; + } + /** * The MathContext instance used for +,-,/,*,% operations on big decimals. * @@ -1374,9 +1407,13 @@ public Object multiply(final Object left, final Object right) { final double r = toDouble(strictCast, right); return l * r; } - // otherwise treat as BigInteger + // otherwise treat as BigInteger; pre-check bit-length sum to avoid O(n²) on huge operands final BigInteger l = toBigInteger(strictCast, left); final BigInteger r = toBigInteger(strictCast, right); + final int precision = getMathContext().getPrecision(); + if (precision > 0 && l.bitLength() + r.bitLength() > precision * 10 / 3 + 1) { + throw new ArithmeticException("BigInteger precision exceeded"); + } final BigInteger result = l.multiply(r); return narrowBigInteger(left, right, result); } @@ -1746,9 +1783,9 @@ private BigDecimal parseBigDecimal(final String arg) throws ArithmeticException */ private BigInteger parseBigInteger(final String arg) throws ArithmeticException { try { - return arg.isEmpty()? BigInteger.ZERO : new BigInteger(arg); + return arg.isEmpty() ? BigInteger.ZERO : checkBigIntegerPrecision(new BigInteger(arg)); } catch (final NumberFormatException e) { - throw new CoercionException("BigDecimal coercion: ("+ arg +")", e); + throw new CoercionException("BigInteger coercion: ("+ arg +")", e); } } @@ -2065,7 +2102,7 @@ public Object subtract(final Object left, final Object right) { final BigInteger l = toBigInteger(strictCast, left); final BigInteger r = toBigInteger(strictCast, right); final BigInteger result = l.subtract(r); - return narrowBigInteger(left, right, result); + return narrowBigInteger(left, right, checkBigIntegerPrecision(result)); } /** @@ -2435,4 +2472,42 @@ public Object xor(final Object left, final Object right) { final long r = toLong(right); return l ^ r; } + + /** + * A CharSequence wrapper that throws ArithmeticException if the current thread is interrupted. + * Used as the input to {@code Pattern.matcher()} so that catastrophic-backtracking regex matches + * remain responsive to JEXL cancellation (which sets the thread interrupt flag). + * The interrupt flag is checked every 256 {@code charAt} calls to limit overhead. + */ + private static final class InterruptibleCharSequence implements CharSequence { + private final String seq; + private int count; + + private InterruptibleCharSequence(final String s) { + this.seq = s; + } + + @Override + public char charAt(final int index) { + if ((++count & 0xff) == 0 && Thread.currentThread().isInterrupted()) { + throw new ArithmeticException("Operation interrupted"); + } + return seq.charAt(index); + } + + @Override + public int length() { + return seq.length(); + } + + @Override + public CharSequence subSequence(final int start, final int end) { + return seq.subSequence(start, end); + } + + @Override + public String toString() { + return seq; + } + } } diff --git a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java index 2d18e8869..7a0684c64 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -24,6 +24,7 @@ import java.util.concurrent.Callable; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.regex.Pattern; import org.apache.commons.jexl3.JexlArithmetic; import org.apache.commons.jexl3.JexlContext; @@ -1377,12 +1378,37 @@ protected Object visit(final ASTEQSNode node, final Object data) { @Override protected Object visit(final ASTERNode node, final Object data) { final Object left = node.jjtGetChild(0).jjtAccept(this, data); - final Object right = node.jjtGetChild(1).jjtAccept(this, data); + final JexlNode rightNode = node.jjtGetChild(1); + final Object right = resolvePattern(rightNode, rightNode.jjtAccept(this, data)); // note the arguments inversion between 'in'/'matches' and 'contains' // if x in y then y contains x return operators.contains(node, JexlOperator.CONTAINS, right, left); } + /** + * If the right operand of {@code =~} / {@code !~} is a string literal, compile it to a Pattern once and + * cache the result in the node's value slot (same mechanism as negated numeric literals). + * Dynamic string values (from variables) are returned unchanged. + * The regex string length is validated before compilation (matches JexlArithmetic.REGEX_PATTERN_MAX_LENGTH). + */ + private static Object resolvePattern(final JexlNode rightNode, final Object right) { + if (right instanceof CharSequence && rightNode instanceof JexlNode.Constant) { + final Object cached = rightNode.jjtGetValue(); + if (cached instanceof Pattern) { + return cached; + } + final String regex = right.toString(); + final int maxLen = 2048; + if (regex.length() > maxLen) { + throw new ArithmeticException("regular expression too long: " + regex.length() + " > " + maxLen); + } + final Pattern compiled = Pattern.compile(regex); + rightNode.jjtSetValue(compiled); + return compiled; + } + return right; + } + @Override protected Object visit(final ASTEWNode node, final Object data) { final Object left = node.jjtGetChild(0).jjtAccept(this, data); @@ -1728,7 +1754,8 @@ protected Object visit(final ASTNotNode node, final Object data) { @Override protected Object visit(final ASTNRNode node, final Object data) { final Object left = node.jjtGetChild(0).jjtAccept(this, data); - final Object right = node.jjtGetChild(1).jjtAccept(this, data); + final JexlNode rightNode = node.jjtGetChild(1); + final Object right = resolvePattern(rightNode, rightNode.jjtAccept(this, data)); // note the arguments inversion between (not) 'in'/'matches' and (not) 'contains' // if x not-in y then y not-contains x return operators.contains(node, JexlOperator.NOT_CONTAINS, right, left); diff --git a/src/main/java/org/apache/commons/jexl3/internal/Operator.java b/src/main/java/org/apache/commons/jexl3/internal/Operator.java index 41810ee4e..af07f0835 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Operator.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Operator.java @@ -363,6 +363,9 @@ public boolean contains(final JexlCache.Reference node, final JexlOperator opera // not-contains is !contains return JexlOperator.CONTAINS == operator == contained; } catch (final Exception any) { + if (Thread.currentThread().isInterrupted()) { + throw new JexlException.Cancel(node instanceof JexlNode ? (JexlNode) node : null); + } return operatorError(node, operator, any, false); } } diff --git a/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java b/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java index e79f80fcc..595f72b4d 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java +++ b/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java @@ -23,6 +23,8 @@ import java.text.DecimalFormatSymbols; import java.util.Locale; +import org.apache.commons.jexl3.JexlEngine; + /** * Parses number literals. */ @@ -32,6 +34,30 @@ public final class NumberParser implements Serializable { */ private static final long serialVersionUID = 1L; + /** + * Hard upper bound on BigInteger literal digits when no engine precision is configured. + * Acts as a parse-time DoS guard independent of any MathContext (JEXL-security f014). + */ + static final int MAX_BIGINTEGER_DIGITS = 256; + + /** + * Returns the maximum digit count allowed for a BigInteger literal. + * When a JEXL engine with a bounded MathContext is active on the current thread, the + * engine's precision (in decimal digits) is used as the limit; otherwise MAX_BIGINTEGER_DIGITS applies. + * At runtime, JexlArithmetic.checkBigIntegerPrecision() enforces a stricter bit-length limit + * based on the same precision (f014, f013). + */ + private static int maxBigIntegerDigits() { + final JexlEngine engine = JexlEngine.getThreadEngine(); + if (engine != null) { + final int precision = engine.getArithmetic().getMathContext().getPrecision(); + if (precision > 0) { + return precision; + } + } + return MAX_BIGINTEGER_DIGITS; + } + /** JEXL locale-neutral big decimal format. */ static final DecimalFormat BIGDF = new DecimalFormat("0.0b", new DecimalFormatSymbols(Locale.ROOT)); private static boolean isNegative(final Token token) { @@ -77,6 +103,7 @@ NumberParser assignNatural(final boolean negative, final String natural) { String s = natural; Number result; Class rclass; + final int maxDigits = maxBigIntegerDigits(); // determine the base final int base; if (s.charAt(0) == '0') { @@ -102,6 +129,11 @@ NumberParser assignNatural(final boolean negative, final String natural) { case 'h': case 'H': { rclass = BigInteger.class; + if (last > maxDigits) { + throw new NumberFormatException( + "BigInteger literal too long: " + last + + " > " + maxDigits); + } final BigInteger bi = new BigInteger(s.substring(0, last), base); result = negative? bi.negate() : bi; break; @@ -117,6 +149,11 @@ NumberParser assignNatural(final boolean negative, final String natural) { final long l = Long.parseLong(s, base); result = negative? -l : l; } catch (final NumberFormatException take3) { + if (s.length() > maxDigits) { + throw new NumberFormatException( + "BigInteger literal too long: " + s.length() + + " > " + maxDigits); + } final BigInteger bi = new BigInteger(s, base); result = negative? bi.negate() : bi; } diff --git a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt index 198d3666c..48c4af624 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt +++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt @@ -103,6 +103,10 @@ public final class Parser extends JexlParser JexlInfo ji = et == null ? info : info.at(et.beginLine, et.beginColumn); String msg = et == null ? xparse.getMessage() : et.image; throw new JexlException.Parsing(ji, msg).clean(); + } catch (NumberFormatException xnfe) { + Token et = errorToken(jj_lastpos, jj_scanpos, token.next, token); + JexlInfo ji = et == null ? info : info.at(et.beginLine, et.beginColumn); + throw new JexlException.Parsing(ji, xnfe.getMessage()).clean(); } finally { token_source.defaultLexState = DEFAULT; token_source.ignoredTokens = Collections.emptySet(); diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java index 257dcb4cd..ebf24ff91 100644 --- a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java +++ b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java @@ -2409,4 +2409,75 @@ void setOptions(final JexlOptions options) { assertEquals("zero", jexl.createExpression("array.0").evaluate(jc)); assertEquals("one", jexl.createExpression("array.1").evaluate(jc)); } + + // ----- security fixes f012 / f013 / f014 ----- + + /** + * f014: BigInteger literal with more digits than MAX_BIGINTEGER_DIGITS must be rejected at parse time. + */ + @Test + void testBigIntegerLiteralTooLong() { + // normal H-literal still works + assertNotNull(JEXL.createScript("42H")); + // a literal just over the cap (4096 + 1 digits + 'H') must fail to parse + final char[] digits = new char[4097]; + java.util.Arrays.fill(digits, '1'); + final String huge = new String(digits) + "H"; + assertThrows(JexlException.Parsing.class, () -> JEXL.createScript(huge)); + } + + /** + * f013: BigInteger arithmetic results that exceed the MathContext precision must throw. + */ + @Test + void testBigIntegerArithmeticPrecisionCap() { + // precision=3 caps at ~11 bits (formula: 3 * 10 / 3 + 1 = 11), so results > 2047 are rejected + final JexlArithmetic bounded = new JexlArithmetic(true, new MathContext(3), JexlArithmetic.BIGD_SCALE); + final JexlEngine jexl = new JexlBuilder().arithmetic(bounded).create(); + // small values are fine + assertEquals(new BigInteger("3"), jexl.createScript("a + b", "a", "b") + .execute(null, BigInteger.ONE, BigInteger.valueOf(2L))); + // result > 2047 is rejected: 1500 + 1000 = 2500, bitLength=12 > 11 + assertThrows(JexlException.class, () -> + jexl.createScript("a + b", "a", "b") + .execute(null, BigInteger.valueOf(1500L), BigInteger.valueOf(1000L))); + // multiply pre-check: 64 (7 bits) * 64 (7 bits), sum of operand bits = 14 > 11 + assertThrows(JexlException.class, () -> + jexl.createScript("a * b", "a", "b") + .execute(null, BigInteger.valueOf(64L), BigInteger.valueOf(64L))); + } + + /** + * f012: a regex pattern string longer than REGEX_PATTERN_MAX_LENGTH must throw. + */ + @Test + void testRegexPatternTooLong() { + final JexlEngine jexl = new JexlBuilder().strict(true).create(); + final JexlScript script = jexl.createScript("x =~ y", "x", "y"); + final char[] chars = new char[JexlArithmetic.REGEX_PATTERN_MAX_LENGTH + 1]; + java.util.Arrays.fill(chars, 'a'); + final String longPattern = new String(chars); + assertThrows(JexlException.class, () -> script.execute(null, "abc", longPattern)); + } + + /** + * f012: regex matching must respond to thread interruption so a catastrophic-backtracking + * pattern does not hang a cancellable engine indefinitely. + */ + @Test + void testRegexMatchingInterruptible() { + final JexlEngine jexl = new JexlBuilder().cancellable(true).create(); + // Classic catastrophic-backtracking: (a+)+b against a non-matching string + final JexlScript script = jexl.createScript("x =~ y", "x", "y"); + final String evilPattern = "(a+)+b"; + final char[] chars = new char[20]; + java.util.Arrays.fill(chars, 'a'); + final String evilValue = new String(chars) + "c"; + try { + Thread.currentThread().interrupt(); + assertThrows(JexlException.Cancel.class, () -> script.execute(null, evilValue, evilPattern)); + } finally { + Thread.interrupted(); // clear so it does not leak into other tests + } + } } From b60d958a61a671e32453bf49d8c3ba4f8cc2c6a3 Mon Sep 17 00:00:00 2001 From: Henrib Date: Mon, 31 Aug 2026 11:07:28 +0200 Subject: [PATCH 2/6] JEXL-471 : fix review comments; --- .../apache/commons/jexl3/JexlArithmetic.java | 35 ++++++++++---- .../commons/jexl3/internal/Interpreter.java | 29 ++++++------ .../commons/jexl3/parser/NumberParser.java | 26 ++++++++--- .../apache/commons/jexl3/ArithmeticTest.java | 46 +++++++++++++++---- 4 files changed, 96 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java index aa3036579..a8989bd79 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java +++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java @@ -219,7 +219,7 @@ public interface Uberspect { public static final Pattern FLOAT_PATTERN = Pattern.compile("^[+-]?\\d*(\\.\\d*)?([eE][+-]?\\d+)?$"); /** Maximum length of a regex pattern string for the {@code =~} operator (JEXL-security f012). */ - protected static final int REGEX_PATTERN_MAX_LENGTH = 2048; + public static final int REGEX_PATTERN_MAX_LENGTH = 2048; /** * Attempts transformation of potential array in an abstract list or leave as is. @@ -597,15 +597,17 @@ public Boolean contains(final Object container, final Object value) { return false; } // use arithmetic / pattern matching ? - if (container instanceof java.util.regex.Pattern) { - return ((java.util.regex.Pattern) container).matcher(new InterruptibleCharSequence(value.toString())).matches(); - } - if (container instanceof CharSequence) { - final String regex = container.toString(); - if (regex.length() > REGEX_PATTERN_MAX_LENGTH) { - throw new ArithmeticException("regular expression too long: " + regex.length() + " > " + REGEX_PATTERN_MAX_LENGTH); - } - return Pattern.compile(regex).matcher(new InterruptibleCharSequence(value.toString())).matches(); + Pattern pattern = null; + if (container instanceof Pattern) { + pattern = (Pattern) container; + controlRegexLength(pattern.pattern().length()); + } else if (container instanceof CharSequence) { + String regex = container.toString(); + controlRegexLength(regex.length()); + pattern = Pattern.compile(regex); + } + if (pattern != null) { + return pattern.matcher(new InterruptibleCharSequence(value.toString())).matches(); } // try contains on map key if (container instanceof Map) { @@ -618,6 +620,19 @@ public Boolean contains(final Object container, final Object value) { return collectionContains(container, value); } + /** + * Checks the length of a regex pattern string. + * + * @param length The length of the regex pattern string + * @throws ArithmeticException if the length exceeds {@link #REGEX_PATTERN_MAX_LENGTH} + */ + private static void controlRegexLength(int length) { + if (length > REGEX_PATTERN_MAX_LENGTH) { + throw new ArithmeticException( + "regular expression too long: " + length + " > " + REGEX_PATTERN_MAX_LENGTH); + } + } + /** * The result of +,/,-,*,% when both operands are null. * diff --git a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java index 7a0684c64..9b8fc3324 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -1389,22 +1389,25 @@ protected Object visit(final ASTERNode node, final Object data) { * If the right operand of {@code =~} / {@code !~} is a string literal, compile it to a Pattern once and * cache the result in the node's value slot (same mechanism as negated numeric literals). * Dynamic string values (from variables) are returned unchanged. - * The regex string length is validated before compilation (matches JexlArithmetic.REGEX_PATTERN_MAX_LENGTH). + * The regex string length is validated before compilation (JEXL-security f012). + * Synchronized on the node to ensure thread-safe caching when the same compiled script runs concurrently. */ private static Object resolvePattern(final JexlNode rightNode, final Object right) { if (right instanceof CharSequence && rightNode instanceof JexlNode.Constant) { - final Object cached = rightNode.jjtGetValue(); - if (cached instanceof Pattern) { - return cached; - } - final String regex = right.toString(); - final int maxLen = 2048; - if (regex.length() > maxLen) { - throw new ArithmeticException("regular expression too long: " + regex.length() + " > " + maxLen); - } - final Pattern compiled = Pattern.compile(regex); - rightNode.jjtSetValue(compiled); - return compiled; + synchronized (rightNode) { + final Object cached = rightNode.jjtGetValue(); + if (cached instanceof Pattern) { + return cached; + } + final String regex = right.toString(); + if (regex.length() > JexlArithmetic.REGEX_PATTERN_MAX_LENGTH) { + throw new ArithmeticException("regular expression too long: " + regex.length() + + " > " + JexlArithmetic.REGEX_PATTERN_MAX_LENGTH); + } + final Pattern compiled = Pattern.compile(regex); + rightNode.jjtSetValue(compiled); + return compiled; + } } return right; } diff --git a/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java b/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java index 595f72b4d..a6f23b475 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java +++ b/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java @@ -41,18 +41,30 @@ public final class NumberParser implements Serializable { static final int MAX_BIGINTEGER_DIGITS = 256; /** - * Returns the maximum digit count allowed for a BigInteger literal. + * Returns the maximum digit count allowed for a BigInteger literal in the given base. * When a JEXL engine with a bounded MathContext is active on the current thread, the - * engine's precision (in decimal digits) is used as the limit; otherwise MAX_BIGINTEGER_DIGITS applies. - * At runtime, JexlArithmetic.checkBigIntegerPrecision() enforces a stricter bit-length limit - * based on the same precision (f014, f013). + * engine's precision is converted to an equivalent bit limit, then to max digits for the base. + * Otherwise MAX_BIGINTEGER_DIGITS applies. + * At runtime, JexlArithmetic.checkBigIntegerPrecision() enforces the bit-length limit (f014, f013). */ - private static int maxBigIntegerDigits() { + private static int maxBigIntegerDigits(final int base) { final JexlEngine engine = JexlEngine.getThreadEngine(); if (engine != null) { final int precision = engine.getArithmetic().getMathContext().getPrecision(); if (precision > 0) { - return precision; + // Use the same bit formula as checkBigIntegerPrecision: precision * 10/3 + 1 bits + // For a given base B with log2(B) bits per digit, max_digits = max_bits / log2(B) + final int maxBits = precision * 10 / 3 + 1; + final int bitsPerDigit; + if (base == 16) { + bitsPerDigit = 4; // log2(16) = 4 + } else if (base == 8) { + bitsPerDigit = 3; // log2(8) = 3 + } else { + // base 10: log2(10) ≈ 3.32, approximate as 10/3 + return (maxBits * 3) / 10; + } + return maxBits / bitsPerDigit; } } return MAX_BIGINTEGER_DIGITS; @@ -103,7 +115,6 @@ NumberParser assignNatural(final boolean negative, final String natural) { String s = natural; Number result; Class rclass; - final int maxDigits = maxBigIntegerDigits(); // determine the base final int base; if (s.charAt(0) == '0') { @@ -116,6 +127,7 @@ NumberParser assignNatural(final boolean negative, final String natural) { } else { base = 10; } + final int maxDigits = maxBigIntegerDigits(base); // switch on suffix if any final int last = s.length() - 1; switch (s.charAt(last)) { diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java index ebf24ff91..d0eec10d5 100644 --- a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java +++ b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java @@ -2419,8 +2419,8 @@ void setOptions(final JexlOptions options) { void testBigIntegerLiteralTooLong() { // normal H-literal still works assertNotNull(JEXL.createScript("42H")); - // a literal just over the cap (4096 + 1 digits + 'H') must fail to parse - final char[] digits = new char[4097]; + // a literal just over the cap (256 + 1 digits + 'H') must fail to parse + final char[] digits = new char[256 + 1]; java.util.Arrays.fill(digits, '1'); final String huge = new String(digits) + "H"; assertThrows(JexlException.Parsing.class, () -> JEXL.createScript(huge)); @@ -2465,19 +2465,45 @@ void testRegexPatternTooLong() { * pattern does not hang a cancellable engine indefinitely. */ @Test - void testRegexMatchingInterruptible() { + void testRegexMatchingInterruptible() throws InterruptedException { final JexlEngine jexl = new JexlBuilder().cancellable(true).create(); - // Classic catastrophic-backtracking: (a+)+b against a non-matching string final JexlScript script = jexl.createScript("x =~ y", "x", "y"); + // Catastrophic backtracking pattern on non-matching input to force long character scanning final String evilPattern = "(a+)+b"; - final char[] chars = new char[20]; + // Use a larger set of 'a's to extend matching time + final char[] chars = new char[50]; java.util.Arrays.fill(chars, 'a'); final String evilValue = new String(chars) + "c"; - try { - Thread.currentThread().interrupt(); - assertThrows(JexlException.Cancel.class, () -> script.execute(null, evilValue, evilPattern)); - } finally { - Thread.interrupted(); // clear so it does not leak into other tests + + final java.util.concurrent.atomic.AtomicReference caught = + new java.util.concurrent.atomic.AtomicReference<>(); + final java.util.concurrent.CountDownLatch started = new java.util.concurrent.CountDownLatch(1); + + final Thread t = new Thread(() -> { + try { + started.countDown(); + script.execute(null, evilValue, evilPattern); + } catch (final Exception e) { + caught.set(e); + } + }); + + t.start(); + // Wait for thread to actually start executing + started.await(); + // Give regex matching time to engage (50 'a's with (a+)+b pattern causes backtracking) + Thread.sleep(300); + // Interrupt the matching thread + t.interrupt(); + // Wait for thread to complete (should exit promptly if InterruptibleCharSequence is working) + t.join(5000); + + assertFalse(t.isAlive(), "Thread should have completed after interruption (regex should be interruptible)"); + // The thread may complete without exception if the regex finishes faster than interruption catches it, + // or it may throw Cancel if interrupted during charset access. Both are acceptable here. + if (caught.get() != null) { + assertTrue(caught.get() instanceof JexlException.Cancel, + "If interrupted during matching, expected JexlException.Cancel, got " + caught.get().getClass().getSimpleName()); } } } From 4e6c8bfc3c6d9667b6fd45929a0e0a510b61783f Mon Sep 17 00:00:00 2001 From: Henrib Date: Mon, 31 Aug 2026 13:08:27 +0200 Subject: [PATCH 3/6] [JEXL-471] Complete double-check locking pattern in Pattern cache Proper double-check locking: first volatile read without lock, compile Pattern outside lock, then synchronized recheck-and-set to minimize contention and avoid duplicate compilations when same script runs concurrently. Co-Authored-By: Claude --- .../commons/jexl3/internal/Interpreter.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java index 9b8fc3324..d5a940f93 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -1390,23 +1390,30 @@ protected Object visit(final ASTERNode node, final Object data) { * cache the result in the node's value slot (same mechanism as negated numeric literals). * Dynamic string values (from variables) are returned unchanged. * The regex string length is validated before compilation (JEXL-security f012). - * Synchronized on the node to ensure thread-safe caching when the same compiled script runs concurrently. + * Uses double-check locking: first check without lock, then synchronized recheck-and-set to avoid + * holding the lock during expensive Pattern.compile() when the same compiled script runs concurrently. */ private static Object resolvePattern(final JexlNode rightNode, final Object right) { if (right instanceof CharSequence && rightNode instanceof JexlNode.Constant) { + // First check (volatile read, no lock) + Object cached = rightNode.jjtGetValue(); + if (cached instanceof Pattern) { + return cached; + } + // Compile without holding lock + final String regex = right.toString(); + if (regex.length() > JexlArithmetic.REGEX_PATTERN_MAX_LENGTH) { + throw new ArithmeticException("regular expression too long: " + regex.length() + + " > " + JexlArithmetic.REGEX_PATTERN_MAX_LENGTH); + } + final Pattern compiled = Pattern.compile(regex); + // Double-check and set under lock synchronized (rightNode) { - final Object cached = rightNode.jjtGetValue(); - if (cached instanceof Pattern) { - return cached; + cached = rightNode.jjtGetValue(); + if (!(cached instanceof Pattern)) { + rightNode.jjtSetValue(compiled); } - final String regex = right.toString(); - if (regex.length() > JexlArithmetic.REGEX_PATTERN_MAX_LENGTH) { - throw new ArithmeticException("regular expression too long: " + regex.length() - + " > " + JexlArithmetic.REGEX_PATTERN_MAX_LENGTH); - } - final Pattern compiled = Pattern.compile(regex); - rightNode.jjtSetValue(compiled); - return compiled; + return cached instanceof Pattern ? cached : compiled; } } return right; From 833ea619b396d9ed059a9fe4f6b061666b87f203 Mon Sep 17 00:00:00 2001 From: Henrib Date: Mon, 31 Aug 2026 13:17:14 +0200 Subject: [PATCH 4/6] JEXL-471 : Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../java/org/apache/commons/jexl3/JexlArithmetic.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java index a8989bd79..013eac525 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java +++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java @@ -1426,8 +1426,14 @@ public Object multiply(final Object left, final Object right) { final BigInteger l = toBigInteger(strictCast, left); final BigInteger r = toBigInteger(strictCast, right); final int precision = getMathContext().getPrecision(); - if (precision > 0 && l.bitLength() + r.bitLength() > precision * 10 / 3 + 1) { - throw new ArithmeticException("BigInteger precision exceeded"); + final int lBitLength = l.bitLength(); + final int rBitLength = r.bitLength(); + final int bitLengthLimit = precision * 10 / 3 + 1; + if (precision > 0 && lBitLength + rBitLength > bitLengthLimit) { + throw new ArithmeticException("BigInteger precision exceeded: limit=" + bitLengthLimit + + ", leftBitLength=" + lBitLength + + ", rightBitLength=" + rBitLength + + ", contextPrecision=" + precision); } final BigInteger result = l.multiply(r); return narrowBigInteger(left, right, result); From 5788b85546430315dac17b2ec8ef17e4b06bff12 Mon Sep 17 00:00:00 2001 From: Henrib Date: Mon, 31 Aug 2026 14:25:57 +0200 Subject: [PATCH 5/6] JEXL-471 : Apply batched suggestions from code review (take 2) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../apache/commons/jexl3/JexlArithmetic.java | 2 +- .../apache/commons/jexl3/parser/Parser.jjt | 5 +- .../apache/commons/jexl3/ArithmeticTest.java | 70 ++++++++++--------- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java index 013eac525..11bf858c0 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java +++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java @@ -2523,7 +2523,7 @@ public int length() { @Override public CharSequence subSequence(final int start, final int end) { - return seq.subSequence(start, end); + return new InterruptibleCharSequence(seq.substring(start, end)); } @Override diff --git a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt index 48c4af624..572b525d6 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt +++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt @@ -106,7 +106,10 @@ public final class Parser extends JexlParser } catch (NumberFormatException xnfe) { Token et = errorToken(jj_lastpos, jj_scanpos, token.next, token); JexlInfo ji = et == null ? info : info.at(et.beginLine, et.beginColumn); - throw new JexlException.Parsing(ji, xnfe.getMessage()).clean(); + String msg = et != null && et.image != null && !et.image.isEmpty() + ? et.image + : "invalid number literal"; + throw new JexlException.Parsing(ji, msg).clean(); } finally { token_source.defaultLexState = DEFAULT; token_source.ignoredTokens = Collections.emptySet(); diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java index d0eec10d5..8745bf97a 100644 --- a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java +++ b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java @@ -2468,42 +2468,48 @@ void testRegexPatternTooLong() { void testRegexMatchingInterruptible() throws InterruptedException { final JexlEngine jexl = new JexlBuilder().cancellable(true).create(); final JexlScript script = jexl.createScript("x =~ y", "x", "y"); - // Catastrophic backtracking pattern on non-matching input to force long character scanning final String evilPattern = "(a+)+b"; - // Use a larger set of 'a's to extend matching time - final char[] chars = new char[50]; - java.util.Arrays.fill(chars, 'a'); - final String evilValue = new String(chars) + "c"; - final java.util.concurrent.atomic.AtomicReference caught = - new java.util.concurrent.atomic.AtomicReference<>(); - final java.util.concurrent.CountDownLatch started = new java.util.concurrent.CountDownLatch(1); + for (int size = 256; size <= 4096; size *= 2) { + final char[] chars = new char[size]; + java.util.Arrays.fill(chars, 'a'); + final String evilValue = new String(chars) + "c"; + + final java.util.concurrent.atomic.AtomicReference caught = + new java.util.concurrent.atomic.AtomicReference<>(); + final java.util.concurrent.CountDownLatch started = new java.util.concurrent.CountDownLatch(1); + + final Thread t = new Thread(() -> { + try { + started.countDown(); + script.execute(null, evilValue, evilPattern); + } catch (final Throwable e) { + caught.set(e); + } + }); - final Thread t = new Thread(() -> { - try { - started.countDown(); - script.execute(null, evilValue, evilPattern); - } catch (final Exception e) { - caught.set(e); + t.start(); + started.await(); + + final long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(5); + while (t.isAlive() && System.nanoTime() < deadline) { + t.interrupt(); + Thread.yield(); + t.join(10L); + } + + assertFalse(t.isAlive(), "Thread should have completed after interruption (regex should be interruptible)"); + + final Throwable thrown = caught.get(); + if (thrown instanceof JexlException.Cancel) { + return; + } + if (thrown != null) { + fail("Expected JexlException.Cancel when interrupted during matching, got " + + thrown.getClass().getSimpleName(), thrown); } - }); - - t.start(); - // Wait for thread to actually start executing - started.await(); - // Give regex matching time to engage (50 'a's with (a+)+b pattern causes backtracking) - Thread.sleep(300); - // Interrupt the matching thread - t.interrupt(); - // Wait for thread to complete (should exit promptly if InterruptibleCharSequence is working) - t.join(5000); - - assertFalse(t.isAlive(), "Thread should have completed after interruption (regex should be interruptible)"); - // The thread may complete without exception if the regex finishes faster than interruption catches it, - // or it may throw Cancel if interrupted during charset access. Both are acceptable here. - if (caught.get() != null) { - assertTrue(caught.get() instanceof JexlException.Cancel, - "If interrupted during matching, expected JexlException.Cancel, got " + caught.get().getClass().getSimpleName()); } + + fail("Did not observe JexlException.Cancel during interrupted regex matching"); } } From 4e64e3cb8bd355a3fe2b89d5c0bc527536d4ed7c Mon Sep 17 00:00:00 2001 From: Henrib Date: Mon, 31 Aug 2026 15:53:53 +0200 Subject: [PATCH 6/6] JEXL-471 : fix review comments, last take. --- .../org/apache/commons/jexl3/JexlArithmetic.java | 14 ++++++++++---- .../java/org/apache/commons/jexl3/JexlBuilder.java | 5 +++-- .../apache/commons/jexl3/internal/Interpreter.java | 12 +++++++----- .../apache/commons/jexl3/parser/NumberParser.java | 14 +++++--------- .../org/apache/commons/jexl3/ArithmeticTest.java | 10 +++++----- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java index 11bf858c0..8e05fb575 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java +++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java @@ -221,6 +221,12 @@ public interface Uberspect { /** Maximum length of a regex pattern string for the {@code =~} operator (JEXL-security f012). */ public static final int REGEX_PATTERN_MAX_LENGTH = 2048; + /** + * Hard upper bound on BigInteger literal digits when no engine precision is configured. + * Acts as a parse-time DoS guard independent of any MathContext (JEXL-security f014). + */ + public static final int MAX_BIGINTEGER_DIGITS = 256; + /** * Attempts transformation of potential array in an abstract list or leave as is. *

An array (as in int[]) is not convenient to call methods so when encountered we turn them into lists

@@ -944,10 +950,10 @@ public boolean equals(final Object left, final Object right) { * @throws ArithmeticException when the limit is exceeded */ protected BigInteger checkBigIntegerPrecision(final BigInteger big) { - final int precision = getMathContext().getPrecision(); + final long precision = getMathContext().getPrecision(); if (precision > 0) { // precision 0 means unlimited; otherwise, one decimal digit ≈ log2(10) ≈ 10/3 bits - final int maxBits = precision * 10 / 3 + 1; + final long maxBits = precision * 10 / 3 + 1; if (big.bitLength() > maxBits) { throw new ArithmeticException( "BigInteger precision exceeded: " + big.bitLength() + " bits for " + precision + "-digit context"); @@ -1425,10 +1431,10 @@ public Object multiply(final Object left, final Object right) { // otherwise treat as BigInteger; pre-check bit-length sum to avoid O(n²) on huge operands final BigInteger l = toBigInteger(strictCast, left); final BigInteger r = toBigInteger(strictCast, right); - final int precision = getMathContext().getPrecision(); + final long precision = getMathContext().getPrecision(); final int lBitLength = l.bitLength(); final int rBitLength = r.bitLength(); - final int bitLengthLimit = precision * 10 / 3 + 1; + final long bitLengthLimit = precision * 10 / 3 + 1; if (precision > 0 && lBitLength + rBitLength > bitLengthLimit) { throw new ArithmeticException("BigInteger precision exceeded: limit=" + bitLengthLimit + ", leftBitLength=" + lBitLength diff --git a/src/main/java/org/apache/commons/jexl3/JexlBuilder.java b/src/main/java/org/apache/commons/jexl3/JexlBuilder.java index 3e52f6027..338894bd0 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlBuilder.java +++ b/src/main/java/org/apache/commons/jexl3/JexlBuilder.java @@ -165,7 +165,7 @@ public static void setDefaultOptions(final String... flags) { /** * Builds the JEXL 3.7 hardened default feature set. *

Starting from {@link #FULL} (the pre-3.7 feature set), this disables {@code new(...)}, - * global side-effects, pragmas, and annotations, and enables lexical scoping and lexical shade. + * global side-effects, pragmas, annotations, namespace instantiation, and enables lexical scoping and lexical shade. * Loops are left enabled so scripts can iterate; expressions never allow loops since they are * parsed as a single expression.

* @@ -180,7 +180,8 @@ private static JexlFeatures secureFeatures() { .annotation(false) .loops(true) .lexical(true) - .lexicalShade(true); + .lexicalShade(true) + .namespaceInstantiation(false); } /** The JexlUberspect instance. */ diff --git a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java index d5a940f93..227c337e4 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -1395,10 +1395,12 @@ protected Object visit(final ASTERNode node, final Object data) { */ private static Object resolvePattern(final JexlNode rightNode, final Object right) { if (right instanceof CharSequence && rightNode instanceof JexlNode.Constant) { - // First check (volatile read, no lock) - Object cached = rightNode.jjtGetValue(); - if (cached instanceof Pattern) { - return cached; + // First check with lock to avoid expensive Pattern.compile() if already cached + synchronized (rightNode) { + Object cached = rightNode.jjtGetValue(); + if (cached instanceof Pattern) { + return cached; + } } // Compile without holding lock final String regex = right.toString(); @@ -1409,7 +1411,7 @@ private static Object resolvePattern(final JexlNode rightNode, final Object righ final Pattern compiled = Pattern.compile(regex); // Double-check and set under lock synchronized (rightNode) { - cached = rightNode.jjtGetValue(); + Object cached = rightNode.jjtGetValue(); if (!(cached instanceof Pattern)) { rightNode.jjtSetValue(compiled); } diff --git a/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java b/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java index a6f23b475..9eaf4d49c 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java +++ b/src/main/java/org/apache/commons/jexl3/parser/NumberParser.java @@ -23,6 +23,7 @@ import java.text.DecimalFormatSymbols; import java.util.Locale; +import org.apache.commons.jexl3.JexlArithmetic; import org.apache.commons.jexl3.JexlEngine; /** @@ -34,11 +35,6 @@ public final class NumberParser implements Serializable { */ private static final long serialVersionUID = 1L; - /** - * Hard upper bound on BigInteger literal digits when no engine precision is configured. - * Acts as a parse-time DoS guard independent of any MathContext (JEXL-security f014). - */ - static final int MAX_BIGINTEGER_DIGITS = 256; /** * Returns the maximum digit count allowed for a BigInteger literal in the given base. @@ -50,11 +46,11 @@ public final class NumberParser implements Serializable { private static int maxBigIntegerDigits(final int base) { final JexlEngine engine = JexlEngine.getThreadEngine(); if (engine != null) { - final int precision = engine.getArithmetic().getMathContext().getPrecision(); - if (precision > 0) { + final long precision = engine.getArithmetic().getMathContext().getPrecision(); + if (precision > 0 && precision < Integer.MAX_VALUE) { // Use the same bit formula as checkBigIntegerPrecision: precision * 10/3 + 1 bits // For a given base B with log2(B) bits per digit, max_digits = max_bits / log2(B) - final int maxBits = precision * 10 / 3 + 1; + final int maxBits = (int) (precision * 10 / 3 + 1); final int bitsPerDigit; if (base == 16) { bitsPerDigit = 4; // log2(16) = 4 @@ -67,7 +63,7 @@ private static int maxBigIntegerDigits(final int base) { return maxBits / bitsPerDigit; } } - return MAX_BIGINTEGER_DIGITS; + return JexlArithmetic.MAX_BIGINTEGER_DIGITS; } /** JEXL locale-neutral big decimal format. */ diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java index 8745bf97a..8854bcb74 100644 --- a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java +++ b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java @@ -37,7 +37,9 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -2420,7 +2422,7 @@ void testBigIntegerLiteralTooLong() { // normal H-literal still works assertNotNull(JEXL.createScript("42H")); // a literal just over the cap (256 + 1 digits + 'H') must fail to parse - final char[] digits = new char[256 + 1]; + final char[] digits = new char[JexlArithmetic.MAX_BIGINTEGER_DIGITS + 1]; java.util.Arrays.fill(digits, '1'); final String huge = new String(digits) + "H"; assertThrows(JexlException.Parsing.class, () -> JEXL.createScript(huge)); @@ -2475,10 +2477,8 @@ void testRegexMatchingInterruptible() throws InterruptedException { java.util.Arrays.fill(chars, 'a'); final String evilValue = new String(chars) + "c"; - final java.util.concurrent.atomic.AtomicReference caught = - new java.util.concurrent.atomic.AtomicReference<>(); - final java.util.concurrent.CountDownLatch started = new java.util.concurrent.CountDownLatch(1); - + final AtomicReference caught = new AtomicReference<>(); + final CountDownLatch started = new CountDownLatch(1); final Thread t = new Thread(() -> { try { started.countDown();