This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
The following commit(s) were added to refs/heads/master by this push: new 5904195 JEXL-274: added JexlBuilder option, interpreter code, tests and changes 5904195 is described below commit 5904195b725baf93d9943d4c0ca39aa3e6d4b4a8 Author: henrib <hen...@apache.org> AuthorDate: Tue Sep 18 13:35:45 2018 +0200 JEXL-274: added JexlBuilder option, interpreter code, tests and changes --- RELEASE-NOTES.txt | 5 +-- .../java/org/apache/commons/jexl3/JexlBuilder.java | 20 +++++++++++ .../org/apache/commons/jexl3/JexlException.java | 30 ++++++++++++++++ .../org/apache/commons/jexl3/internal/Engine.java | 5 +++ .../apache/commons/jexl3/internal/Interpreter.java | 41 ++++++++++++++++++++-- src/site/xdoc/changes.xml | 3 ++ .../org/apache/commons/jexl3/Issues200Test.java | 28 +++++++++++++++ 7 files changed, 128 insertions(+), 4 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index d791ca1..334cab7 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -30,9 +30,9 @@ What's new in 3.2: * Interpolated strings in identifiers, as in x.`${prefix}_${suffix}` that in many cases would be equivalent to x[prefix + '_' + suffix] or x[`${prefix}_${suffix}`]. -* Safe-navigation operator '?.' allowing lenient handling of non-existent or null properties so that an expression +* Safe-navigation mode and operator '?.' allowing lenient handling of non-existent or null properties so that an expression like 'x?.y?.z' would behave as 'x.y.z' in nominal execution and return null instead of throwing an exception in error - cases. + cases. The safe() option on the builder enables the behavior making '.' and '[...]' behave as '?.'. * A set of syntactic restrictions can be applied to scripts ranging from not allowing side-effects to not allowing loops enabling fine control over what end-users may be able to enter as expressions/scripts. @@ -40,6 +40,7 @@ New Features in 3.2: ==================== * JEXL-275: Allow safe navigation as option +* JEXL-274: Handle soft and hard stack overflow * JEXL-273: Add do...while(...) loops * JEXL-264: Allow space, quote & double-quote in identifiers * JEXL-260: Automatically inject JexlContext in constructor call when possible diff --git a/src/main/java/org/apache/commons/jexl3/JexlBuilder.java b/src/main/java/org/apache/commons/jexl3/JexlBuilder.java index 928069e..38786a8 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlBuilder.java +++ b/src/main/java/org/apache/commons/jexl3/JexlBuilder.java @@ -99,6 +99,9 @@ public class JexlBuilder { /** The cache size. */ private int cache = -1; + + /** The stack overflow limit. */ + private int stackOverflow = Integer.MAX_VALUE; /** The maximum expression length to hit the expression cache. */ private int cacheThreshold = CACHE_THRESHOLD; @@ -424,6 +427,23 @@ public class JexlBuilder { } /** + * Sets the number of script/expression evaluations that can be stacked. + * @param size if not strictly positive, limit is reached when java StackOverflow is thrown. + * @return this builder + */ + public JexlBuilder stackOverflow(int size) { + this.stackOverflow = size; + return this; + } + + /** + * @return the cache size + */ + public int stackOverflow() { + return stackOverflow; + } + + /** * @return a {@link JexlEngine} instance */ public JexlEngine create() { diff --git a/src/main/java/org/apache/commons/jexl3/JexlException.java b/src/main/java/org/apache/commons/jexl3/JexlException.java index 1d009d0..856d154 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlException.java +++ b/src/main/java/org/apache/commons/jexl3/JexlException.java @@ -322,6 +322,36 @@ public class JexlException extends RuntimeException { } /** + * Thrown when reaching stack-overflow. + * + * @since 3.2 + */ + public static class StackOverflow extends JexlException { + /** + * Creates a new stack overflow exception instance. + * + * @param info the location information + * @param name the unknown method + * @param cause the exception causing the error + */ + public StackOverflow(JexlInfo info, String name, Throwable cause) { + super(info, name, cause); + } + + /** + * @return the specific detailed message + */ + public String getDetail() { + return super.detailedMessage(); + } + + @Override + protected String detailedMessage() { + return "stack overflow " + getDetail(); + } + } + + /** * Thrown when parsing fails due to an invalid assigment. * * @since 3.0 diff --git a/src/main/java/org/apache/commons/jexl3/internal/Engine.java b/src/main/java/org/apache/commons/jexl3/internal/Engine.java index bc9735a..4604c7b 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Engine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Engine.java @@ -90,6 +90,10 @@ public class Engine extends JexlEngine { */ protected final Map<String, Object> functions; /** + * The maximum stack height. + */ + protected final int stackOverflow; + /** * Whether this engine considers unknown variables, methods and constructors as errors. */ protected final boolean strict; @@ -164,6 +168,7 @@ public class Engine extends JexlEngine { this.silent = conf.silent() == null ? false : conf.silent(); this.cancellable = conf.cancellable() == null ? !silent && strict : conf.cancellable(); this.debug = conf.debug() == null ? true : conf.debug(); + this.stackOverflow = conf.stackOverflow() > 0? conf.stackOverflow() : Integer.MAX_VALUE; // core properties: JexlUberspect uber = conf.uberspect() == null ? getUberspect(conf.logger(), conf.strategy()) : conf.uberspect(); ClassLoader loader = conf.loader(); 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 4837bc7..11402f3 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -112,9 +112,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.regex.Pattern; import java.util.concurrent.Callable; -import org.apache.commons.jexl3.JxltEngine; /** @@ -127,6 +125,8 @@ public class Interpreter extends InterpreterBase { protected final Operators operators; /** Cache executors. */ protected final boolean cache; + /** Frame height. */ + protected int fp = 0; /** Symbol values. */ protected final Scope.Frame frame; /** The context to store/retrieve variables. */ @@ -137,6 +137,12 @@ public class Interpreter extends InterpreterBase { protected Map<String, Object> functors; /** + * The thread local interpreter. + */ + protected static final java.lang.ThreadLocal<Interpreter> INTER = + new java.lang.ThreadLocal<Interpreter>(); + + /** * Creates an interpreter. * @param engine the engine creating this interpreter * @param aContext the context to evaluate expression @@ -170,6 +176,17 @@ public class Interpreter extends InterpreterBase { functions = ii.functions; functors = ii.functors; } + + /** + * Swaps the current thread local interpreter. + * @param inter the interpreter or null + * @return the previous thread local interpreter + */ + protected Interpreter putThreadInterpreter(Interpreter inter) { + Interpreter pinter = INTER.get(); + INTER.set(inter); + return pinter; + } /** * Interpret the given script/expression. @@ -183,13 +200,29 @@ public class Interpreter extends InterpreterBase { public Object interpret(JexlNode node) { JexlContext.ThreadLocal tcontext = null; JexlEngine tjexl = null; + Interpreter tinter = null; try { + tinter = putThreadInterpreter(this); + if (tinter != null) { + fp = tinter.fp + 1; + } if (context instanceof JexlContext.ThreadLocal) { tcontext = jexl.putThreadLocal((JexlContext.ThreadLocal) context); } tjexl = jexl.putThreadEngine(jexl); + if (fp > jexl.stackOverflow) { + throw new JexlException.StackOverflow(node.jexlInfo(), "jexl (" + jexl.stackOverflow + ")", null); + } cancelCheck(node); return node.jjtAccept(this, null); + } catch(StackOverflowError xstack) { + JexlException xjexl = new JexlException.StackOverflow(node.jexlInfo(), "jvm", xstack); + if (!isSilent()) { + throw xjexl.clean(); + } + if (logger.isWarnEnabled()) { + logger.warn(xjexl.getMessage(), xjexl.getCause()); + } } catch (JexlException.Return xreturn) { return xreturn.getValue(); } catch (JexlException.Cancel xcancel) { @@ -220,6 +253,10 @@ public class Interpreter extends InterpreterBase { if (context instanceof JexlContext.ThreadLocal) { jexl.putThreadLocal(tcontext); } + if (tinter != null) { + fp = tinter.fp - 1; + } + putThreadInterpreter(tinter); } return null; } diff --git a/src/site/xdoc/changes.xml b/src/site/xdoc/changes.xml index 96c1ab3..7156f73 100644 --- a/src/site/xdoc/changes.xml +++ b/src/site/xdoc/changes.xml @@ -29,6 +29,9 @@ <action dev="henrib" type="add" issue="JEXL-275"> Allow safe navigation as option </action> + <action dev="henrib" type="add" issue="JEXL-274"> + Handle soft and hard stack overflow + </action> <action dev="Dmitri Blinov" type="add" issue="JEXL-175" due-to="Dmitri Blinov"> Add do...while(...) loops </action> diff --git a/src/test/java/org/apache/commons/jexl3/Issues200Test.java b/src/test/java/org/apache/commons/jexl3/Issues200Test.java index a4cfe4f..ea5a84e 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues200Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues200Test.java @@ -568,4 +568,32 @@ public class Issues200Test extends JexlTestCase { Assert.assertTrue(result instanceof JexlScript); } + + @Test + public void test274() throws Exception { + JexlEngine jexl = new JexlBuilder().strict(true).safe(true).stackOverflow(5).create(); + JexlContext ctxt = new MapContext(); + JexlScript script= jexl.createScript("var f = (x)->{ x > 1? x * f(x - 1) : x }; f(a)", "a"); + Object result = null; + result = script.execute(ctxt, 3); + Assert.assertEquals(6, result); + try { + result = script.execute(ctxt, 32); + Assert.fail("should have overflown"); + } catch(JexlException.StackOverflow xstack) { + // expected + String sxs = xstack.toString(); + Assert.assertTrue(sxs.contains("jexl")); + } + jexl = new JexlBuilder().strict(true).create(); + script= jexl.createScript("var f = (x)->{ x * f(x - 1) }; f(a)", "a"); + try { + result = script.execute(ctxt, 32); + Assert.fail("should have overflown"); + } catch(JexlException.StackOverflow xstack) { + // expected + String sxs = xstack.toString(); + Assert.assertTrue(sxs.contains("jvm")); + } + } }