| /* |
| * Copyright (C) 2015-2021 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "ExecutionTimeLimitTest.h" |
| |
| #include "InitializeThreading.h" |
| #include "JSContextRefPrivate.h" |
| #include "JavaScript.h" |
| #include "Options.h" |
| #include <wtf/CPUTime.h> |
| #include <wtf/Condition.h> |
| #include <wtf/Lock.h> |
| #include <wtf/Threading.h> |
| #include <wtf/text/StringBuilder.h> |
| |
| #if HAVE(MACH_EXCEPTIONS) |
| #include <dispatch/dispatch.h> |
| #endif |
| |
| using JSC::Options; |
| |
| static JSGlobalContextRef context = nullptr; |
| |
| static JSValueRef currentCPUTimeAsJSFunctionCallback(JSContextRef ctx, JSObjectRef functionObject, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) |
| { |
| UNUSED_PARAM(functionObject); |
| UNUSED_PARAM(thisObject); |
| UNUSED_PARAM(argumentCount); |
| UNUSED_PARAM(arguments); |
| UNUSED_PARAM(exception); |
| |
| ASSERT(JSContextGetGlobalContext(ctx) == context); |
| return JSValueMakeNumber(ctx, CPUTime::forCurrentThread().seconds()); |
| } |
| |
| bool shouldTerminateCallbackWasCalled = false; |
| static bool shouldTerminateCallback(JSContextRef, void*) |
| { |
| shouldTerminateCallbackWasCalled = true; |
| return true; |
| } |
| |
| bool cancelTerminateCallbackWasCalled = false; |
| static bool cancelTerminateCallback(JSContextRef, void*) |
| { |
| cancelTerminateCallbackWasCalled = true; |
| return false; |
| } |
| |
| int extendTerminateCallbackCalled = 0; |
| static bool extendTerminateCallback(JSContextRef ctx, void*) |
| { |
| extendTerminateCallbackCalled++; |
| if (extendTerminateCallbackCalled == 1) { |
| JSContextGroupRef contextGroup = JSContextGetGroup(ctx); |
| JSContextGroupSetExecutionTimeLimit(contextGroup, .200f, extendTerminateCallback, nullptr); |
| return false; |
| } |
| return true; |
| } |
| |
| #if HAVE(MACH_EXCEPTIONS) |
| bool dispatchTerminateCallbackCalled = false; |
| static bool dispatchTermitateCallback(JSContextRef, void*) |
| { |
| dispatchTerminateCallbackCalled = true; |
| return true; |
| } |
| #endif |
| |
| struct TierOptions { |
| const char* tier; |
| Seconds timeLimitAdjustment; |
| const char* optionsStr; |
| }; |
| |
| static void testResetAfterTimeout(bool& failed) |
| { |
| JSValueRef v = nullptr; |
| JSValueRef exception = nullptr; |
| const char* reentryScript = "100"; |
| JSStringRef script = JSStringCreateWithUTF8CString(reentryScript); |
| v = JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception); |
| JSStringRelease(script); |
| if (exception) { |
| printf("FAIL: Watchdog timeout was not reset.\n"); |
| failed = true; |
| } else if (!JSValueIsNumber(context, v) || JSValueToNumber(context, v, nullptr) != 100) { |
| printf("FAIL: Script result is not as expected.\n"); |
| failed = true; |
| } |
| } |
| |
| int testExecutionTimeLimit() |
| { |
| static const TierOptions tierOptionsList[] = { |
| { "LLINT", 0_ms, "--useConcurrentJIT=false --useLLInt=true --useJIT=false" }, |
| #if ENABLE(JIT) |
| { "Baseline", 0_ms, "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=false" }, |
| { "DFG", 200_ms, "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=true --useFTLJIT=false" }, |
| #if ENABLE(FTL_JIT) |
| { "FTL", 500_ms, "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=true --useFTLJIT=true" }, |
| #endif |
| #endif // ENABLE(JIT) |
| }; |
| |
| bool failed = false; |
| |
| JSC::initialize(); |
| |
| for (auto tierOptions : tierOptionsList) { |
| StringBuilder savedOptionsBuilder; |
| Options::dumpAllOptionsInALine(savedOptionsBuilder); |
| |
| Options::setOptions(tierOptions.optionsStr); |
| |
| Seconds tierAdjustment = tierOptions.timeLimitAdjustment; |
| Seconds timeLimit; |
| |
| context = JSGlobalContextCreateInGroup(nullptr, nullptr); |
| |
| JSContextGroupRef contextGroup = JSContextGetGroup(context); |
| JSObjectRef globalObject = JSContextGetGlobalObject(context); |
| ASSERT(JSValueIsObject(context, globalObject)); |
| |
| JSValueRef exception = nullptr; |
| |
| JSStringRef currentCPUTimeStr = JSStringCreateWithUTF8CString("currentCPUTime"); |
| JSObjectRef currentCPUTimeFunction = JSObjectMakeFunctionWithCallback(context, currentCPUTimeStr, currentCPUTimeAsJSFunctionCallback); |
| JSObjectSetProperty(context, globalObject, currentCPUTimeStr, currentCPUTimeFunction, kJSPropertyAttributeNone, nullptr); |
| JSStringRelease(currentCPUTimeStr); |
| |
| /* Test script on another thread: */ |
| timeLimit = 100_ms + tierAdjustment; |
| JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), shouldTerminateCallback, nullptr); |
| { |
| #if OS(LINUX) && (CPU(MIPS) || CPU(ARM_THUMB2)) |
| Seconds timeAfterWatchdogShouldHaveFired = 500_ms + tierAdjustment; |
| #else |
| Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment; |
| #endif |
| |
| JSStringRef script = JSStringCreateWithUTF8CString("function foo() { while (true) { } } foo();"); |
| exception = nullptr; |
| JSValueRef* exn = &exception; |
| shouldTerminateCallbackWasCalled = false; |
| auto thread = Thread::create("Rogue thread", [=] { |
| JSEvaluateScript(context, script, nullptr, nullptr, 1, exn); |
| }); |
| |
| sleep(timeAfterWatchdogShouldHaveFired); |
| |
| if (shouldTerminateCallbackWasCalled) |
| printf("PASS: %s script timed out as expected.\n", tierOptions.tier); |
| else { |
| printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier); |
| exit(1); |
| } |
| |
| if (!exception) { |
| printf("FAIL: %s TerminationException was not thrown.\n", tierOptions.tier); |
| exit(1); |
| } |
| |
| thread->waitForCompletion(); |
| testResetAfterTimeout(failed); |
| |
| JSStringRelease(script); |
| } |
| |
| /* Test script timeout: */ |
| timeLimit = 100_ms + tierAdjustment; |
| JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), shouldTerminateCallback, nullptr); |
| { |
| Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment; |
| |
| CString scriptText = makeString( |
| "function foo() {" |
| "var startTime = currentCPUTime();" |
| "while (true) {" |
| "for (var i = 0; i < 1000; i++);" |
| "if (currentCPUTime() - startTime > ", timeAfterWatchdogShouldHaveFired.seconds(), ") break;" |
| "}" |
| "}" |
| "foo();" |
| ).utf8(); |
| |
| JSStringRef script = JSStringCreateWithUTF8CString(scriptText.data()); |
| exception = nullptr; |
| shouldTerminateCallbackWasCalled = false; |
| auto startTime = CPUTime::forCurrentThread(); |
| JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception); |
| auto endTime = CPUTime::forCurrentThread(); |
| JSStringRelease(script); |
| |
| if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) && shouldTerminateCallbackWasCalled) |
| printf("PASS: %s script timed out as expected.\n", tierOptions.tier); |
| else { |
| if ((endTime - startTime) >= timeAfterWatchdogShouldHaveFired) |
| printf("FAIL: %s script did not time out as expected.\n", tierOptions.tier); |
| if (!shouldTerminateCallbackWasCalled) |
| printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier); |
| failed = true; |
| } |
| |
| if (!exception) { |
| printf("FAIL: %s TerminationException was not thrown.\n", tierOptions.tier); |
| failed = true; |
| } |
| |
| testResetAfterTimeout(failed); |
| } |
| |
| /* Test script timeout with tail calls: */ |
| timeLimit = 100_ms + tierAdjustment; |
| JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), shouldTerminateCallback, nullptr); |
| { |
| Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment; |
| |
| CString scriptText = makeString( |
| "var startTime = currentCPUTime();" |
| "function recurse(i) {" |
| "'use strict';" |
| "if (i % 1000 === 0) {" |
| "if (currentCPUTime() - startTime >", timeAfterWatchdogShouldHaveFired.seconds(), ") { return; }" |
| "}" |
| "return recurse(i + 1); }" |
| "recurse(0);" |
| ).utf8(); |
| |
| JSStringRef script = JSStringCreateWithUTF8CString(scriptText.data()); |
| exception = nullptr; |
| shouldTerminateCallbackWasCalled = false; |
| auto startTime = CPUTime::forCurrentThread(); |
| JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception); |
| auto endTime = CPUTime::forCurrentThread(); |
| JSStringRelease(script); |
| |
| if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) && shouldTerminateCallbackWasCalled) |
| printf("PASS: %s script with infinite tail calls timed out as expected .\n", tierOptions.tier); |
| else { |
| if ((endTime - startTime) >= timeAfterWatchdogShouldHaveFired) |
| printf("FAIL: %s script with infinite tail calls did not time out as expected.\n", tierOptions.tier); |
| if (!shouldTerminateCallbackWasCalled) |
| printf("FAIL: %s script with infinite tail calls' timeout callback was not called.\n", tierOptions.tier); |
| failed = true; |
| } |
| |
| if (!exception) { |
| printf("FAIL: %s TerminationException was not thrown.\n", tierOptions.tier); |
| failed = true; |
| } |
| |
| testResetAfterTimeout(failed); |
| } |
| |
| /* Test the script timeout's TerminationException should NOT be catchable: */ |
| timeLimit = 100_ms + tierAdjustment; |
| JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), shouldTerminateCallback, nullptr); |
| { |
| Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment; |
| |
| CString scriptText = makeString( |
| "function foo() {" |
| "var startTime = currentCPUTime();" |
| "try {" |
| "while (true) {" |
| "for (var i = 0; i < 1000; i++);" |
| "if (currentCPUTime() - startTime > ", timeAfterWatchdogShouldHaveFired.seconds(), ") break;" |
| "}" |
| "} catch(e) { }" |
| "}" |
| "foo();" |
| ).utf8(); |
| |
| JSStringRef script = JSStringCreateWithUTF8CString(scriptText.data()); |
| exception = nullptr; |
| shouldTerminateCallbackWasCalled = false; |
| |
| auto startTime = CPUTime::forCurrentThread(); |
| JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception); |
| auto endTime = CPUTime::forCurrentThread(); |
| |
| JSStringRelease(script); |
| |
| if (((endTime - startTime) >= timeAfterWatchdogShouldHaveFired) || !shouldTerminateCallbackWasCalled) { |
| if (!((endTime - startTime) < timeAfterWatchdogShouldHaveFired)) |
| printf("FAIL: %s script did not time out as expected.\n", tierOptions.tier); |
| if (!shouldTerminateCallbackWasCalled) |
| printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier); |
| failed = true; |
| } |
| |
| if (exception) |
| printf("PASS: %s TerminationException was not catchable as expected.\n", tierOptions.tier); |
| else { |
| printf("FAIL: %s TerminationException was caught.\n", tierOptions.tier); |
| failed = true; |
| } |
| |
| testResetAfterTimeout(failed); |
| } |
| |
| /* Test script timeout with no callback: */ |
| timeLimit = 100_ms + tierAdjustment; |
| JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), nullptr, nullptr); |
| { |
| Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment; |
| |
| CString scriptText = makeString( |
| "function foo() {" |
| "var startTime = currentCPUTime();" |
| "while (true) {" |
| "for (var i = 0; i < 1000; i++);" |
| "if (currentCPUTime() - startTime > ", timeAfterWatchdogShouldHaveFired.seconds(), ") break;" |
| "}" |
| "}" |
| "foo();" |
| ).utf8(); |
| |
| JSStringRef script = JSStringCreateWithUTF8CString(scriptText.data()); |
| exception = nullptr; |
| shouldTerminateCallbackWasCalled = false; |
| |
| auto startTime = CPUTime::forCurrentThread(); |
| JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception); |
| auto endTime = CPUTime::forCurrentThread(); |
| |
| JSStringRelease(script); |
| |
| if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) && !shouldTerminateCallbackWasCalled) |
| printf("PASS: %s script timed out as expected when no callback is specified.\n", tierOptions.tier); |
| else { |
| if ((endTime - startTime) >= timeAfterWatchdogShouldHaveFired) |
| printf("FAIL: %s script did not time out as expected when no callback is specified.\n", tierOptions.tier); |
| else |
| printf("FAIL: %s script called stale callback function.\n", tierOptions.tier); |
| failed = true; |
| } |
| |
| if (!exception) { |
| printf("FAIL: %s TerminationException was not thrown.\n", tierOptions.tier); |
| failed = true; |
| } |
| |
| testResetAfterTimeout(failed); |
| } |
| |
| /* Test script timeout cancellation: */ |
| timeLimit = 100_ms + tierAdjustment; |
| JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), cancelTerminateCallback, nullptr); |
| { |
| Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment; |
| |
| CString scriptText = makeString( |
| "function foo() {" |
| "var startTime = currentCPUTime();" |
| "while (true) {" |
| "for (var i = 0; i < 1000; i++);" |
| "if (currentCPUTime() - startTime > ", timeAfterWatchdogShouldHaveFired.seconds(), ") break;" |
| "}" |
| "}" |
| "foo();" |
| ).utf8(); |
| |
| JSStringRef script = JSStringCreateWithUTF8CString(scriptText.data()); |
| exception = nullptr; |
| cancelTerminateCallbackWasCalled = false; |
| |
| auto startTime = CPUTime::forCurrentThread(); |
| JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception); |
| auto endTime = CPUTime::forCurrentThread(); |
| |
| JSStringRelease(script); |
| |
| if (((endTime - startTime) >= timeAfterWatchdogShouldHaveFired) && cancelTerminateCallbackWasCalled && !exception) |
| printf("PASS: %s script timeout was cancelled as expected.\n", tierOptions.tier); |
| else { |
| if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) || exception) |
| printf("FAIL: %s script timeout was not cancelled.\n", tierOptions.tier); |
| if (!cancelTerminateCallbackWasCalled) |
| printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier); |
| failed = true; |
| } |
| |
| if (exception) { |
| printf("FAIL: %s Unexpected TerminationException thrown.\n", tierOptions.tier); |
| failed = true; |
| } |
| } |
| |
| /* Test script timeout extension: */ |
| timeLimit = 100_ms + tierAdjustment; |
| JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), extendTerminateCallback, nullptr); |
| { |
| Seconds timeBeforeExtendedDeadline = 250_ms + tierAdjustment; |
| Seconds timeAfterExtendedDeadline = 600_ms + tierAdjustment; |
| Seconds maxBusyLoopTime = 750_ms + tierAdjustment; |
| |
| CString scriptText = makeString( |
| "function foo() {" |
| "var startTime = currentCPUTime();" |
| "while (true) {" |
| "for (var i = 0; i < 1000; i++);" |
| "if (currentCPUTime() - startTime > ", maxBusyLoopTime.seconds(), ") break;" |
| "}" |
| "}" |
| "foo();" |
| ).utf8(); |
| |
| JSStringRef script = JSStringCreateWithUTF8CString(scriptText.data()); |
| exception = nullptr; |
| extendTerminateCallbackCalled = 0; |
| |
| auto startTime = CPUTime::forCurrentThread(); |
| JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception); |
| auto endTime = CPUTime::forCurrentThread(); |
| auto deltaTime = endTime - startTime; |
| |
| JSStringRelease(script); |
| |
| if ((deltaTime >= timeBeforeExtendedDeadline) && (deltaTime < timeAfterExtendedDeadline) && (extendTerminateCallbackCalled == 2) && exception) |
| printf("PASS: %s script timeout was extended as expected.\n", tierOptions.tier); |
| else { |
| if (deltaTime < timeBeforeExtendedDeadline) |
| printf("FAIL: %s script timeout was not extended as expected.\n", tierOptions.tier); |
| else if (deltaTime >= timeAfterExtendedDeadline) |
| printf("FAIL: %s script did not timeout.\n", tierOptions.tier); |
| |
| if (extendTerminateCallbackCalled < 1) |
| printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier); |
| if (extendTerminateCallbackCalled < 2) |
| printf("FAIL: %s script timeout callback was not called after timeout extension.\n", tierOptions.tier); |
| |
| if (!exception) |
| printf("FAIL: %s TerminationException was not thrown during timeout extension test.\n", tierOptions.tier); |
| |
| failed = true; |
| } |
| } |
| |
| #if HAVE(MACH_EXCEPTIONS) |
| /* Test script timeout from dispatch queue: */ |
| timeLimit = 100_ms + tierAdjustment; |
| JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), dispatchTermitateCallback, nullptr); |
| { |
| Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment; |
| |
| CString scriptText = makeString( |
| "function foo() {" |
| "var startTime = currentCPUTime();" |
| "while (true) {" |
| "for (var i = 0; i < 1000; i++);" |
| "if (currentCPUTime() - startTime > ", timeAfterWatchdogShouldHaveFired.seconds(), ") break;" |
| "}" |
| "}" |
| "foo();" |
| ).utf8(); |
| |
| JSStringRef script = JSStringCreateWithUTF8CString(scriptText.data()); |
| exception = nullptr; |
| dispatchTerminateCallbackCalled = false; |
| |
| // We have to do this since blocks can only capture things as const. |
| JSGlobalContextRef& contextRef = context; |
| JSStringRef& scriptRef = script; |
| JSValueRef& exceptionRef = exception; |
| |
| Lock syncLock; |
| Lock& syncLockRef = syncLock; |
| Condition synchronize; |
| Condition& synchronizeRef = synchronize; |
| bool didSynchronize = false; |
| bool& didSynchronizeRef = didSynchronize; |
| |
| Seconds startTime; |
| Seconds endTime; |
| |
| Seconds& startTimeRef = startTime; |
| Seconds& endTimeRef = endTime; |
| |
| dispatch_group_t group = dispatch_group_create(); |
| dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{ |
| startTimeRef = CPUTime::forCurrentThread(); |
| JSEvaluateScript(contextRef, scriptRef, nullptr, nullptr, 1, &exceptionRef); |
| endTimeRef = CPUTime::forCurrentThread(); |
| Locker locker { syncLockRef }; |
| didSynchronizeRef = true; |
| synchronizeRef.notifyAll(); |
| }); |
| |
| Locker locker { syncLock }; |
| synchronize.wait(syncLock, [&] { return didSynchronize; }); |
| |
| if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) && dispatchTerminateCallbackCalled) |
| printf("PASS: %s script on dispatch queue timed out as expected.\n", tierOptions.tier); |
| else { |
| if ((endTime - startTime) >= timeAfterWatchdogShouldHaveFired) |
| printf("FAIL: %s script on dispatch queue did not time out as expected.\n", tierOptions.tier); |
| if (!shouldTerminateCallbackWasCalled) |
| printf("FAIL: %s script on dispatch queue timeout callback was not called.\n", tierOptions.tier); |
| failed = true; |
| } |
| |
| JSStringRelease(script); |
| } |
| #endif |
| |
| JSGlobalContextRelease(context); |
| |
| Options::setOptions(savedOptionsBuilder.toString().ascii().data()); |
| } |
| |
| return failed; |
| } |