blob: 0aaa5c0055a3d21b091700506a75140e68e2a634 [file] [log] [blame]
/*
* 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;
}