diff --git a/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoContextFactory.java b/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoContextFactory.java new file mode 100644 index 0000000000..c7bbb055fb --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoContextFactory.java @@ -0,0 +1,201 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco 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. + * + * Alfresco 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 Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.jscript; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.mozilla.javascript.Callable; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ContextFactory; +import org.mozilla.javascript.Scriptable; + +/** + * Custom factory that allows to apply configured limits during script executions + * + * @see ContextFactory + */ +public class AlfrescoContextFactory extends ContextFactory +{ + private static final Log LOGGER = LogFactory.getLog(AlfrescoContextFactory.class); + + private int optimizationLevel = -1; + private int maxScriptExecutionSeconds = -1; + private int maxStackDepth = -1; + private long maxMemoryUsedInBytes = -1L; + private int observeInstructionCount = -1; + + private AlfrescoScriptThreadMxBeanWrapper threadMxBeanWrapper; + + private final int INTERPRETIVE_MODE = -1; + + @Override + protected Context makeContext() + { + AlfrescoScriptContext context = new AlfrescoScriptContext(); + + context.setOptimizationLevel(optimizationLevel); + + // Needed for both time and memory measurement + if (maxScriptExecutionSeconds > 0 || maxMemoryUsedInBytes > 0L) + { + if (observeInstructionCount > 0) + { + LOGGER.info("Enabling observer count..."); + context.setGenerateObserverCount(true); + context.setInstructionObserverThreshold(observeInstructionCount); + } + else + { + LOGGER.info("Disabling observer count..."); + context.setGenerateObserverCount(false); + } + } + + // Memory limit + if (maxMemoryUsedInBytes > 0) + { + context.setThreadId(Thread.currentThread().getId()); + } + + // Max stack depth + if (maxStackDepth > 0) + { + if (optimizationLevel != INTERPRETIVE_MODE) + { + LOGGER.warn("Changing optimization level from " + optimizationLevel + " to " + INTERPRETIVE_MODE); + } + // stack depth can only be set when no optimizations are applied + context.setOptimizationLevel(INTERPRETIVE_MODE); + context.setMaximumInterpreterStackDepth(maxStackDepth); + } + + return context; + } + + @Override + protected void observeInstructionCount(Context cx, int instructionCount) + { + AlfrescoScriptContext acx = (AlfrescoScriptContext) cx; + + if (acx.isLimitsEnabled()) + { + // Time limit + if (maxScriptExecutionSeconds > 0) + { + long currentTime = System.currentTimeMillis(); + if (currentTime - acx.getStartTime() > maxScriptExecutionSeconds * 1000) + { + throw new Error("Maximum script time of " + maxScriptExecutionSeconds + " seconds exceeded"); + } + } + + // Memory + if (maxMemoryUsedInBytes > 0 && threadMxBeanWrapper != null && threadMxBeanWrapper.isThreadAllocatedMemorySupported()) + { + + if (acx.getStartMemory() <= 0) + { + acx.setStartMemory(threadMxBeanWrapper.getThreadAllocatedBytes(acx.getThreadId())); + } + else + { + long currentAllocatedBytes = threadMxBeanWrapper.getThreadAllocatedBytes(acx.getThreadId()); + if (currentAllocatedBytes - acx.getStartMemory() >= maxMemoryUsedInBytes) + { + throw new Error("Memory limit of " + maxMemoryUsedInBytes + " bytes reached"); + } + } + } + } + } + + @Override + protected Object doTopCall(Callable callable, Context cx, Scriptable scope, Scriptable thisObj, Object[] args) + { + AlfrescoScriptContext acx = (AlfrescoScriptContext) cx; + acx.setStartTime(System.currentTimeMillis()); + return super.doTopCall(callable, cx, scope, thisObj, args); + } + + public int getOptimizationLevel() + { + return optimizationLevel; + } + + public void setOptimizationLevel(int optimizationLevel) + { + this.optimizationLevel = optimizationLevel; + } + + public int getMaxScriptExecutionSeconds() + { + return maxScriptExecutionSeconds; + } + + public void setMaxScriptExecutionSeconds(int maxScriptExecutionSeconds) + { + this.maxScriptExecutionSeconds = maxScriptExecutionSeconds; + } + + public int getMaxStackDepth() + { + return maxStackDepth; + } + + public void setMaxStackDepth(int maxStackDepth) + { + this.maxStackDepth = maxStackDepth; + } + + public long getMaxMemoryUsedInBytes() + { + return maxMemoryUsedInBytes; + } + + public void setMaxMemoryUsedInBytes(long maxMemoryUsedInBytes) + { + this.maxMemoryUsedInBytes = maxMemoryUsedInBytes; + if (maxMemoryUsedInBytes > 0) + { + this.threadMxBeanWrapper = new AlfrescoScriptThreadMxBeanWrapper(); + if (!threadMxBeanWrapper.isThreadAllocatedMemorySupported()) + { + LOGGER.warn("com.sun.management.ThreadMXBean was not found on the classpath. " + + "This means that the limiting the memory usage for a script will NOT work."); + } + } + } + + public int getObserveInstructionCount() + { + return observeInstructionCount; + } + + public void setObserveInstructionCount(int observeInstructionCount) + { + this.observeInstructionCount = observeInstructionCount; + } +} \ No newline at end of file diff --git a/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoScriptContext.java b/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoScriptContext.java new file mode 100644 index 0000000000..cc5a4d6b20 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoScriptContext.java @@ -0,0 +1,81 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco 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. + * + * Alfresco 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 Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.jscript; + +import org.mozilla.javascript.Context; + +/** + * Custom Rhino context that holds data as start time and memory + * + * @see Context + */ +public class AlfrescoScriptContext extends Context +{ + private long startTime; + private long threadId; + private long startMemory; + private boolean limitsEnabled = false; + + public long getStartTime() + { + return startTime; + } + + public void setStartTime(long startTime) + { + this.startTime = startTime; + } + + public long getThreadId() + { + return threadId; + } + + public void setThreadId(long threadId) + { + this.threadId = threadId; + } + + public long getStartMemory() + { + return startMemory; + } + + public void setStartMemory(long startMemory) + { + this.startMemory = startMemory; + } + + public boolean isLimitsEnabled() + { + return limitsEnabled; + } + + public void setLimitsEnabled(boolean limitsEnabled) + { + this.limitsEnabled = limitsEnabled; + } +} \ No newline at end of file diff --git a/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoScriptThreadMxBeanWrapper.java b/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoScriptThreadMxBeanWrapper.java new file mode 100644 index 0000000000..0d2bbd9321 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoScriptThreadMxBeanWrapper.java @@ -0,0 +1,78 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco 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. + * + * Alfresco 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 Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.jscript; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; + +/** + * Allows to monitor memory usage + */ +public class AlfrescoScriptThreadMxBeanWrapper +{ + + private ThreadMXBean threadMXBean = null; + private boolean threadAllocatedMemorySupported = false; + + private final String THREAD_MX_BEAN_SUN = "com.sun.management.ThreadMXBean"; + + public AlfrescoScriptThreadMxBeanWrapper() + { + checkThreadAllocatedMemory(); + } + + public long getThreadAllocatedBytes(long threadId) + { + if (threadMXBean != null && threadAllocatedMemorySupported) + { + return ((com.sun.management.ThreadMXBean) threadMXBean).getThreadAllocatedBytes(threadId); + } + + return -1; + } + + public void checkThreadAllocatedMemory() + { + try + { + Class clazz = Class.forName(THREAD_MX_BEAN_SUN); + if (clazz != null) + { + this.threadAllocatedMemorySupported = true; + this.threadMXBean = (com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean(); + } + } + catch (Exception e) + { + this.threadAllocatedMemorySupported = false; + } + } + + public boolean isThreadAllocatedMemorySupported() + { + return threadAllocatedMemorySupported; + } +} \ No newline at end of file diff --git a/repository/src/main/java/org/alfresco/repo/jscript/RhinoScriptProcessor.java b/repository/src/main/java/org/alfresco/repo/jscript/RhinoScriptProcessor.java index dee9b5cffc..59728e18b9 100644 --- a/repository/src/main/java/org/alfresco/repo/jscript/RhinoScriptProcessor.java +++ b/repository/src/main/java/org/alfresco/repo/jscript/RhinoScriptProcessor.java @@ -57,10 +57,12 @@ import org.alfresco.service.namespace.QName; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.mozilla.javascript.Context; +import org.mozilla.javascript.ContextFactory; import org.mozilla.javascript.ImporterTopLevel; import org.mozilla.javascript.Script; import org.mozilla.javascript.Scriptable; -import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.Undefined; import org.mozilla.javascript.WrapFactory; import org.mozilla.javascript.WrappedException; import org.springframework.beans.factory.InitializingBean; @@ -108,7 +110,24 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess /** Cache of runtime compiled script instances */ private final Map scriptCache = new ConcurrentHashMap(256); - + /** Rhino optimization level */ + private int optimizationLevel = -1; + + /** Maximum seconds a script is allowed to run */ + private int maxScriptExecutionSeconds = -1; + + /** Maximum of call stack depth (in terms of number of call frames) */ + private int maxStackDepth = -1; + + /** Maximum memory (bytes) a script can use */ + private long maxMemoryUsedInBytes = -1L; + + /** Number of (bytecode) instructions that will trigger the observer */ + private int observerInstructionCount = 100; + + /** Custom context factory */ + public static AlfrescoContextFactory contextFactory; + /** * Set the default store reference * @@ -143,6 +162,51 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess { this.shareSealedScopes = shareSealedScopes; } + + /** + * @param optimizationLevel + * -1 interpretive mode, 0 no optimizations, 1-9 optimizations performed + */ + public void setOptimizationLevel(int optimizationLevel) + { + this.optimizationLevel = optimizationLevel; + } + + /** + * @param maxScriptExecutionSeconds + * the number of seconds a script is allowed to run + */ + public void setMaxScriptExecutionSeconds(int maxScriptExecutionSeconds) + { + this.maxScriptExecutionSeconds = maxScriptExecutionSeconds; + } + + /** + * @param maxStackDepth + * the number of call stack depth allowed + */ + public void setMaxStackDepth(int maxStackDepth) + { + this.maxStackDepth = maxStackDepth; + } + + /** + * @param maxMemoryUsedInBytes + * the number of memory a script can use + */ + public void setMaxMemoryUsedInBytes(long maxMemoryUsedInBytes) + { + this.maxMemoryUsedInBytes = maxMemoryUsedInBytes; + } + + /** + * @param observerInstructionCount + * the number of instructions that will trigger {@link ContextFactory#observeInstructionCount} + */ + public void setObserverInstructionCount(int observerInstructionCount) + { + this.observerInstructionCount = observerInstructionCount; + } /** * @see org.alfresco.service.cmr.repository.ScriptProcessor#reset() @@ -449,6 +513,8 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess private Object executeScriptImpl(Script script, Map model, boolean secure, String debugScriptName) throws AlfrescoRuntimeException { + Scriptable scope = null; + long startTime = 0; if (callLogger.isDebugEnabled()) { @@ -465,14 +531,16 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess // Create a thread-specific scope from one of the shared scopes. // See http://www.mozilla.org/rhino/scopes.html cx.setWrapFactory(secure ? wrapFactory : sandboxFactory); - Scriptable scope; + + // Enables or disables execution limits based on secure flag + enableLimits(cx, secure); + if (this.shareSealedScopes) { Scriptable sharedScope = secure ? this.nonSecureScope : this.secureScope; scope = cx.newObject(sharedScope); scope.setPrototype(sharedScope); scope.setParentScope(null); - } else { @@ -545,7 +613,8 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess throw new AlfrescoRuntimeException(err.getMessage(), err); } finally - { + { + unsetScope(model, scope); Context.exit(); if (callLogger.isDebugEnabled()) @@ -638,6 +707,9 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess */ public void afterPropertiesSet() throws Exception { + // Initialize context factory + initContextFactory(); + // Initialize the secure scope Context cx = Context.enter(); try @@ -695,4 +767,129 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess } return scope; } + + /** + * Clean supplied scope and unset it from any model instance where it has been injected before + * + * @param model + * Data model containing objects from where scope will be unset + * @param scope + * The scope to clean + */ + private void unsetScope(Map model, Scriptable scope) + { + if (scope != null) + { + Object[] ids = scope.getIds(); + if (ids != null) + { + for (Object id : ids) + { + try + { + deleteProperty(scope, id.toString()); + } + catch (Exception e) + { + logger.info("Unable to delete id: " + id, e); + } + } + } + } + + if (model != null) + { + for (String key : model.keySet()) + { + try + { + deleteProperty(scope, key); + + Object obj = model.get(key); + if (obj instanceof Scopeable) + { + ((Scopeable) obj).setScope(null); + } + } + catch (Exception e) + { + logger.info("Unable to unset model object " + key + " : ", e); + } + } + } + } + + /** + * Deletes a property from the supplied scope, if property is not removable, then is set to null + * + * @param scope + * the scope object from where property will be removed + * @param name + * the property name to delete + */ + private void deleteProperty(Scriptable scope, String name) + { + if (scope != null && name != null) + { + if (!ScriptableObject.deleteProperty(scope, name)) + { + ScriptableObject.putProperty(scope, name, null); + } + scope.delete(name); + } + } + + /** + * Initializes the context factory with limits configuration + */ + private synchronized void initContextFactory() + { + if (contextFactory == null) + { + contextFactory = new AlfrescoContextFactory(); + contextFactory.setOptimizationLevel(optimizationLevel); + + if (maxScriptExecutionSeconds > 0) + { + contextFactory.setMaxScriptExecutionSeconds(maxScriptExecutionSeconds); + } + + if (maxMemoryUsedInBytes > 0L) + { + contextFactory.setMaxMemoryUsedInBytes(maxMemoryUsedInBytes); + } + + if (maxStackDepth > 0) + { + contextFactory.setMaxStackDepth(maxStackDepth); + } + + if (maxScriptExecutionSeconds > 0 || maxMemoryUsedInBytes > 0L) + { + contextFactory.setObserveInstructionCount(observerInstructionCount); + } + + ContextFactory.initGlobal(contextFactory); + } + } + + /** + * If script is considered secure no limits will be applied, otherwise, the limits are enabled and the script can be + * interrupted in case a limit has been reached. + * + * @param cx + * the Rhino scope + * @param secure + * true if script execution is considered secure (e.g, deployed at classpath level) + */ + private void enableLimits(Context cx, boolean secure) + { + if (cx != null) + { + if (cx instanceof AlfrescoScriptContext) + { + ((AlfrescoScriptContext) cx).setLimitsEnabled(!secure); + } + } + } } \ No newline at end of file diff --git a/repository/src/main/resources/alfresco/repository.properties b/repository/src/main/resources/alfresco/repository.properties index 5ea296393a..43c4d7a69f 100644 --- a/repository/src/main/resources/alfresco/repository.properties +++ b/repository/src/main/resources/alfresco/repository.properties @@ -1351,3 +1351,18 @@ import.zip.compressionRatioThreshold=100 # "zip bomb" and the import extraction process cancelled. No value (or a negative long) will be taken to mean that no # limit should be applied. import.zip.uncompressedBytesLimit= + +# Rhino optimization level +scripts.execution.optimizationLevel=0 + +# Max seconds a script is allowed to run +scripts.execution.maxScriptExecutionSeconds=-1 + +# Max call stack depth +scripts.execution.maxStackDepth=-1 + +# Max memory (bytes) a script can use +scripts.execution.maxMemoryUsedInBytes=-1 + +# Number of instructions that will trigger the observer +scripts.execution.observerInstructionCount=-1 diff --git a/repository/src/main/resources/alfresco/script-services-context.xml b/repository/src/main/resources/alfresco/script-services-context.xml index 2a2b930696..cbff40693a 100644 --- a/repository/src/main/resources/alfresco/script-services-context.xml +++ b/repository/src/main/resources/alfresco/script-services-context.xml @@ -45,6 +45,21 @@ ${spaces.company_home.childname} + + ${scripts.execution.optimizationLevel} + + + ${scripts.execution.maxScriptExecutionSeconds} + + + ${scripts.execution.maxStackDepth} + + + ${scripts.execution.maxMemoryUsedInBytes} + + + ${scripts.execution.observerInstructionCount} +