From ec8642a9f65ae766fde3daae6537db7fa8c906cb Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Wed, 22 Jul 2026 09:59:14 -0700 Subject: [PATCH 1/6] Add site setting to configure server-side script execution timeout (#7862) ## Rationale The Rhino sandbox terminates any server-side JavaScript (such as trigger scripts) after a hard-coded 60 seconds of wall-clock time, which includes database and other Java operations invoked by the script. Long-running but legitimate operations, such as bulk imports that fire per-row triggers, can exceed this budget with no recourse. This change makes the timeout a configurable site setting so admins can raise it, or disable it entirely, without a code change. ## Related Pull Requests None. ## Changes - New `scriptExecutionTimeout` site setting (default 60 seconds, 0 disables) exposed on the Site Settings admin page and as a `SiteSettings.scriptExecutionTimeout` startup property. - The Rhino sandbox resolves the timeout once per script execution instead of using the hard-coded constant, so setting changes take effect immediately for new script runs; the elapsed-time comparison is promoted to `long` to avoid overflow with large values. - The Site Settings form binds the new field as a nullable `Integer`, so a POST that omits the parameter leaves the stored setting unchanged rather than silently disabling the timeout. - Added `RhinoService.TestCase.timeoutTest` (BVT) covering both a short timeout aborting a runaway script and 0 disabling the watchdog. --- api/src/org/labkey/api/settings/AppProps.java | 5 ++ .../org/labkey/api/settings/AppPropsImpl.java | 6 +++ .../api/settings/SiteSettingsProperties.java | 8 +++ .../api/settings/WriteableAppProps.java | 7 +++ .../labkey/core/admin/AdminController.java | 22 ++++++++ .../org/labkey/core/admin/customizeSite.jsp | 5 ++ .../org/labkey/core/script/RhinoService.java | 50 +++++++++++++++++-- 7 files changed, 100 insertions(+), 3 deletions(-) diff --git a/api/src/org/labkey/api/settings/AppProps.java b/api/src/org/labkey/api/settings/AppProps.java index 3ae76372530..27480cc7b45 100644 --- a/api/src/org/labkey/api/settings/AppProps.java +++ b/api/src/org/labkey/api/settings/AppProps.java @@ -173,6 +173,11 @@ static WriteableAppProps getWriteableInstance() /** Timeout in seconds for read-only HTTP requests, after which resources like DB connections and spawned processes will be killed. Set to 0 to disable. */ int getReadOnlyHttpRequestTimeout(); + int DEFAULT_SCRIPT_EXECUTION_TIMEOUT = 60; + + /** Timeout in seconds for server-side JavaScript (e.g. trigger scripts), measured in wall-clock time including database and other Java operations invoked by the script. Set to 0 to disable. */ + int getScriptExecutionTimeout(); + int getMaxBLOBSize(); ExceptionReportingLevel getExceptionReportingLevel(); diff --git a/api/src/org/labkey/api/settings/AppPropsImpl.java b/api/src/org/labkey/api/settings/AppPropsImpl.java index 60b22b957fe..ac90bf67310 100644 --- a/api/src/org/labkey/api/settings/AppPropsImpl.java +++ b/api/src/org/labkey/api/settings/AppPropsImpl.java @@ -321,6 +321,12 @@ public int getReadOnlyHttpRequestTimeout() return lookupIntValue(readOnlyHttpRequestTimeout, 0); } + @Override + public int getScriptExecutionTimeout() + { + return lookupIntValue(scriptExecutionTimeout, DEFAULT_SCRIPT_EXECUTION_TIMEOUT); + } + @Override public int getMaxBLOBSize() { diff --git a/api/src/org/labkey/api/settings/SiteSettingsProperties.java b/api/src/org/labkey/api/settings/SiteSettingsProperties.java index 961ecb54868..8cbc673f4aa 100644 --- a/api/src/org/labkey/api/settings/SiteSettingsProperties.java +++ b/api/src/org/labkey/api/settings/SiteSettingsProperties.java @@ -90,6 +90,14 @@ public void setValue(WriteableAppProps writeable, String value) writeable.setReadOnlyHttpRequestTimeout(Integer.parseInt(value)); } }, + scriptExecutionTimeout("Timeout in seconds for server-side JavaScript such as trigger scripts. Measured in wall-clock time, including database and other Java operations invoked by the script. Set to 0 to disable.") + { + @Override + public void setValue(WriteableAppProps writeable, String value) + { + writeable.setScriptExecutionTimeout(Integer.parseInt(value)); + } + }, maxBLOBSize("Maximum file size, in bytes, to allow in database BLOBs") { @Override diff --git a/api/src/org/labkey/api/settings/WriteableAppProps.java b/api/src/org/labkey/api/settings/WriteableAppProps.java index e3691f745b0..92df26b757b 100644 --- a/api/src/org/labkey/api/settings/WriteableAppProps.java +++ b/api/src/org/labkey/api/settings/WriteableAppProps.java @@ -94,6 +94,13 @@ public void setReadOnlyHttpRequestTimeout(int timeout) storeIntValue(readOnlyHttpRequestTimeout, timeout); } + public void setScriptExecutionTimeout(int timeout) + { + if (timeout < 0) + throw new IllegalArgumentException("scriptExecutionTimeout must be >= 0"); + storeIntValue(scriptExecutionTimeout, timeout); + } + public void setMaxBLOBSize(int maxSize) { if (maxSize < 0) diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index 64d0cdcc26e..97175dc35ba 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -1381,6 +1381,14 @@ public void validateCommand(SiteSettingsForm form, Errors errors) { errors.reject(ERROR_MSG, "Memory logging frequency must be non-negative"); } + if (form.getScriptExecutionTimeout() == null) + { + errors.reject(ERROR_MSG, "Script execution timeout is required; set to 0 to disable the timeout"); + } + else if (form.getScriptExecutionTimeout() < 0) + { + errors.reject(ERROR_MSG, "Script execution timeout must be non-negative"); + } } @Override @@ -1415,6 +1423,7 @@ public boolean handlePost(SiteSettingsForm form, BindException errors) throws Ex props.setSSLPort(form.getSslPort()); props.setMemoryUsageDumpInterval(form.getMemoryUsageDumpInterval()); props.setReadOnlyHttpRequestTimeout(form.getReadOnlyHttpRequestTimeout()); + props.setScriptExecutionTimeout(form.getScriptExecutionTimeout()); props.setMaxBLOBSize(form.getMaxBLOBSize()); props.setSelfReportExceptions(form.isSelfReportExceptions()); @@ -2384,6 +2393,7 @@ public static class SiteSettingsForm private int _sslPort; private int _memoryUsageDumpInterval; private int _readOnlyHttpRequestTimeout; + private Integer _scriptExecutionTimeout; private int _maxBLOBSize; private String _exceptionReportingLevel; private String _usageReportingLevel; @@ -2524,6 +2534,18 @@ public int getReadOnlyHttpRequestTimeout() return _readOnlyHttpRequestTimeout; } + /** Null when the request omits or blanks the parameter; rejected in validateCommand so an omitted value can never bind to 0 and silently disable the timeout. */ + @Nullable + public Integer getScriptExecutionTimeout() + { + return _scriptExecutionTimeout; + } + + public void setScriptExecutionTimeout(@Nullable Integer timeout) + { + _scriptExecutionTimeout = timeout; + } + public void setReadOnlyHttpRequestTimeout(int timeout) { _readOnlyHttpRequestTimeout = timeout; diff --git a/core/src/org/labkey/core/admin/customizeSite.jsp b/core/src/org/labkey/core/admin/customizeSite.jsp index 8bc659eb5ac..1de822de1e6 100644 --- a/core/src/org/labkey/core/admin/customizeSite.jsp +++ b/core/src/org/labkey/core/admin/customizeSite.jsp @@ -312,6 +312,11 @@ Click the Save button at any time to accept the current settings and continue. + + + + diff --git a/core/src/org/labkey/core/script/RhinoService.java b/core/src/org/labkey/core/script/RhinoService.java index 497a6d99b1b..71d41196269 100644 --- a/core/src/org/labkey/core/script/RhinoService.java +++ b/core/src/org/labkey/core/script/RhinoService.java @@ -40,11 +40,15 @@ import org.labkey.api.resource.Resource; import org.labkey.api.script.ScriptReference; import org.labkey.api.script.ScriptService; +import org.labkey.api.security.User; +import org.labkey.api.settings.AppProps; +import org.labkey.api.settings.WriteableAppProps; import org.labkey.api.test.TestWhen; import org.labkey.api.util.HeartBeat; import org.labkey.api.util.JunitUtil; import org.labkey.api.util.MemTracker; import org.labkey.api.util.Path; +import org.labkey.api.util.TestContext; import org.labkey.api.util.UnexpectedException; import org.labkey.api.view.HttpView; import org.mozilla.javascript.ClassShutter; @@ -199,6 +203,44 @@ public void reportTest() throws Exception test("reportTest"); } + @Test + public void timeoutTest() throws Exception + { + int original = AppProps.getInstance().getScriptExecutionTimeout(); + User user = TestContext.get().getUser(); + + try + { + // Busy-loop bounded at 15 seconds of wall-clock time; the 1-second watchdog should abort it long before that. HeartBeat ticks once per second, so the abort lands after 2-3 real seconds. + setScriptExecutionTimeout(1, user); + try + { + RhinoService.RHINO_FACTORY.getScriptEngine().eval("var start = new Date().getTime(); while (new Date().getTime() - start < 15000) {}"); + fail("Expected script to be terminated by the execution timeout"); + } + catch (Exception e) + { + assertTrue("Unexpected script error: " + e.getMessage(), e.getMessage().contains("Script execution exceeded 1 seconds")); + } + + // 0 disables the watchdog: a loop that runs well past the 1-second timeout above should complete normally + setScriptExecutionTimeout(0, user); + Object result = RhinoService.RHINO_FACTORY.getScriptEngine().eval("var start = new Date().getTime(); while (new Date().getTime() - start < 2500) {} 'completed';"); + assertEquals("completed", result); + } + finally + { + setScriptExecutionTimeout(original, user); + } + } + + private void setScriptExecutionTimeout(int seconds, User user) + { + WriteableAppProps props = AppProps.getWriteableInstance(); + props.setScriptExecutionTimeout(seconds); + props.save(user); + } + private void test(String scriptName) throws ScriptException, NoSuchMethodException { Path js = Path.parse(ScriptService.SCRIPTS_DIR + "/validationTest/" + scriptName + ".js"); @@ -1006,9 +1048,8 @@ protected void observeInstructionCount(Context cx, int instructionCount) { SandboxContext ctx = (SandboxContext)cx; long currentTime = HeartBeat.currentTimeMillis(); - final int timeout = 60; - if (currentTime - ctx.startTime > timeout*1000) - Context.reportError("Script execution exceeded " + timeout + " seconds."); + if (ctx.timeoutSeconds > 0 && currentTime - ctx.startTime > ctx.timeoutSeconds * 1000L) + Context.reportError("Script execution exceeded " + ctx.timeoutSeconds + " seconds."); } @Override @@ -1044,12 +1085,15 @@ public boolean visibleToScripts(String fullClassName) private static class SandboxContext extends Context { private final long startTime; + // resolved once per context; observeInstructionCount runs far too often for a property lookup + private final int timeoutSeconds; private SandboxContext(SandboxContextFactory factory) { super(factory); setLanguageVersion(Context.VERSION_1_8); startTime = HeartBeat.currentTimeMillis(); + timeoutSeconds = AppProps.getInstance().getScriptExecutionTimeout(); } } From d9c1fbc16c7c2fd4d13a33a79efa78d837dbc7ff Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Wed, 22 Jul 2026 13:32:57 -0700 Subject: [PATCH 2/6] Fix provider parameter in addUsers.jsp (#7872) https://github.com/LabKey/internal-issues/issues/1328 --- core/src/org/labkey/core/security/addUsers.jsp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/src/org/labkey/core/security/addUsers.jsp b/core/src/org/labkey/core/security/addUsers.jsp index 80dcd6df68f..25741c0362f 100644 --- a/core/src/org/labkey/core/security/addUsers.jsp +++ b/core/src/org/labkey/core/security/addUsers.jsp @@ -40,11 +40,6 @@ boolean excludeSiteAdmins = !getUser().hasSiteAdminPermission(); // App admins can't clone permissions from site admins %>