From bb923aeb594571b64b8737432e6dc564d26b10f0 Mon Sep 17 00:00:00 2001 From: Kenzie Togami Date: Thu, 2 Nov 2017 14:39:37 -0700 Subject: [PATCH] Attach a configurable timeout to expression evaluation --- .../sk89q/worldedit/LocalConfiguration.java | 1 + .../internal/expression/Expression.java | 41 +++++++- .../internal/expression/runtime/For.java | 3 + .../expression/runtime/SimpleFor.java | 3 + .../internal/expression/runtime/While.java | 6 ++ .../util/PropertiesConfiguration.java | 1 + .../worldedit/util/YAMLConfiguration.java | 2 + .../expression/ExpressionPlatform.java | 96 +++++++++++++++++++ 8 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionPlatform.java diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalConfiguration.java index 81b4d892a..3ef39d94b 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalConfiguration.java @@ -127,6 +127,7 @@ public abstract class LocalConfiguration { public String navigationWand = ItemTypes.COMPASS.getId(); public int navigationWandMaxDistance = 50; public int scriptTimeout = 3000; + public int calculationTimeout = 100; public Set allowedDataCycleBlocks = new HashSet<>(); public String saveDir = "schematics"; public String scriptsDir = "craftscripts"; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java index 463792213..27266e642 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java @@ -19,6 +19,8 @@ package com.sk89q.worldedit.internal.expression; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.internal.expression.lexer.Lexer; import com.sk89q.worldedit.internal.expression.lexer.tokens.Token; import com.sk89q.worldedit.internal.expression.parser.Parser; @@ -34,6 +36,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * Compiles and evaluates expressions. @@ -69,6 +78,11 @@ import java.util.Stack; public class Expression { private static final ThreadLocal> instance = new ThreadLocal<>(); + private static final ExecutorService evalThread = Executors.newCachedThreadPool( + new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("worldedit-expression-eval-%d") + .build()); private final Map variables = new HashMap<>(); private final String[] variableNames; @@ -115,9 +129,30 @@ public class Expression { pushInstance(); try { - return root.getValue(); - } catch (ReturnException e) { - return e.getValue(); + Future result = evalThread.submit(new Callable() { + @Override + public Double call() throws Exception { + return root.getValue(); + } + }); + try { + return result.get(WorldEdit.getInstance().getConfiguration().calculationTimeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof ReturnException) { + return ((ReturnException) cause).getValue(); + } + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + throw new RuntimeException(cause); + } catch (TimeoutException e) { + result.cancel(true); + throw new EvaluationException(-1, "Calculations exceeded time limit."); + } } finally { popInstance(); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/For.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/For.java index 868c83f96..6334a7350 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/For.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/For.java @@ -50,6 +50,9 @@ public class For extends Node { if (iterations > 256) { throw new EvaluationException(getPosition(), "Loop exceeded 256 iterations."); } + if (Thread.interrupted()) { + throw new EvaluationException(getPosition(), "Calculations exceeded time limit."); + } ++iterations; try { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/SimpleFor.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/SimpleFor.java index 36cf5da81..1576c3484 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/SimpleFor.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/SimpleFor.java @@ -53,6 +53,9 @@ public class SimpleFor extends Node { if (iterations > 256) { throw new EvaluationException(getPosition(), "Loop exceeded 256 iterations."); } + if (Thread.interrupted()) { + throw new EvaluationException(getPosition(), "Calculations exceeded time limit."); + } ++iterations; try { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/While.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/While.java index 4c277058a..5da3dae01 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/While.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/While.java @@ -49,6 +49,9 @@ public class While extends Node { if (iterations > 256) { throw new EvaluationException(getPosition(), "Loop exceeded 256 iterations."); } + if (Thread.interrupted()) { + throw new EvaluationException(getPosition(), "Calculations exceeded time limit."); + } ++iterations; try { @@ -66,6 +69,9 @@ public class While extends Node { if (iterations > 256) { throw new EvaluationException(getPosition(), "Loop exceeded 256 iterations."); } + if (Thread.interrupted()) { + throw new EvaluationException(getPosition(), "Calculations exceeded time limit."); + } ++iterations; try { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java index 65f44c691..fee1917aa 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java @@ -109,6 +109,7 @@ public class PropertiesConfiguration extends LocalConfiguration { navigationWandMaxDistance = getInt("nav-wand-distance", navigationWandMaxDistance); navigationUseGlass = getBool("nav-use-glass", navigationUseGlass); scriptTimeout = getInt("scripting-timeout", scriptTimeout); + calculationTimeout = getInt("calculation-timeout", calculationTimeout); saveDir = getString("schematic-save-dir", saveDir); scriptsDir = getString("craftscript-dir", scriptsDir); butcherDefaultRadius = getInt("butcher-default-radius", butcherDefaultRadius); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java index 41745a0aa..9fa16df5f 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java @@ -105,6 +105,8 @@ public class YAMLConfiguration extends LocalConfiguration { scriptTimeout = config.getInt("scripting.timeout", scriptTimeout); scriptsDir = config.getString("scripting.dir", scriptsDir); + calculationTimeout = config.getInt("calculation.timeout", calculationTimeout); + saveDir = config.getString("saving.dir", saveDir); allowSymlinks = config.getBoolean("files.allow-symbolic-links", false); diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionPlatform.java b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionPlatform.java new file mode 100644 index 000000000..6d806d8ab --- /dev/null +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionPlatform.java @@ -0,0 +1,96 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression; + +import com.google.common.collect.ImmutableMap; +import com.sk89q.worldedit.LocalConfiguration; +import com.sk89q.worldedit.entity.Player; +import com.sk89q.worldedit.extension.platform.AbstractPlatform; +import com.sk89q.worldedit.extension.platform.Capability; +import com.sk89q.worldedit.extension.platform.Preference; +import com.sk89q.worldedit.util.command.Dispatcher; +import com.sk89q.worldedit.world.World; + +import java.util.Map; + +final class ExpressionPlatform extends AbstractPlatform { + + @Override + public int resolveItem(String name) { + return 0; + } + + @Override + public boolean isValidMobType(String type) { + return false; + } + + @Override + public void reload() { + } + + @Override + public Player matchPlayer(Player player) { + return null; + } + + @Override + public World matchWorld(World world) { + return null; + } + + @Override + public void registerCommands(Dispatcher dispatcher) { + } + + @Override + public void registerGameHooks() { + } + + @Override + public LocalConfiguration getConfiguration() { + return new LocalConfiguration() { + + @Override + public void load() { + } + }; + } + + @Override + public String getVersion() { + return "INVALID"; + } + + @Override + public String getPlatformName() { + return "Expression Test"; + } + + @Override + public String getPlatformVersion() { + return "INVALID"; + } + + @Override + public Map getCapabilities() { + return ImmutableMap.of(Capability.CONFIGURATION, Preference.PREFER_OTHERS); + } +}