1/*
2 * Copyright (C) 2015 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "ExecutionTimeLimitTest.h"
28
29#include "InitializeThreading.h"
30#include "JSContextRefPrivate.h"
31#include "JavaScript.h"
32#include "Options.h"
33
34#include <wtf/Atomics.h>
35#include <wtf/CPUTime.h>
36#include <wtf/Condition.h>
37#include <wtf/Lock.h>
38#include <wtf/Threading.h>
39#include <wtf/text/StringBuilder.h>
40
41#if HAVE(MACH_EXCEPTIONS)
42#include <dispatch/dispatch.h>
43#endif
44
45using JSC::Options;
46
47static JSGlobalContextRef context = nullptr;
48
49static JSValueRef currentCPUTimeAsJSFunctionCallback(JSContextRef ctx, JSObjectRef functionObject, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
50{
51 UNUSED_PARAM(functionObject);
52 UNUSED_PARAM(thisObject);
53 UNUSED_PARAM(argumentCount);
54 UNUSED_PARAM(arguments);
55 UNUSED_PARAM(exception);
56
57 ASSERT(JSContextGetGlobalContext(ctx) == context);
58 return JSValueMakeNumber(ctx, CPUTime::forCurrentThread().seconds());
59}
60
61bool shouldTerminateCallbackWasCalled = false;
62static bool shouldTerminateCallback(JSContextRef, void*)
63{
64 shouldTerminateCallbackWasCalled = true;
65 return true;
66}
67
68bool cancelTerminateCallbackWasCalled = false;
69static bool cancelTerminateCallback(JSContextRef, void*)
70{
71 cancelTerminateCallbackWasCalled = true;
72 return false;
73}
74
75int extendTerminateCallbackCalled = 0;
76static bool extendTerminateCallback(JSContextRef ctx, void*)
77{
78 extendTerminateCallbackCalled++;
79 if (extendTerminateCallbackCalled == 1) {
80 JSContextGroupRef contextGroup = JSContextGetGroup(ctx);
81 JSContextGroupSetExecutionTimeLimit(contextGroup, .200f, extendTerminateCallback, 0);
82 return false;
83 }
84 return true;
85}
86
87#if HAVE(MACH_EXCEPTIONS)
88bool dispatchTerminateCallbackCalled = false;
89static bool dispatchTermitateCallback(JSContextRef, void*)
90{
91 dispatchTerminateCallbackCalled = true;
92 return true;
93}
94#endif
95
96struct TierOptions {
97 const char* tier;
98 Seconds timeLimitAdjustment;
99 const char* optionsStr;
100};
101
102static void testResetAfterTimeout(bool& failed)
103{
104 JSValueRef v = nullptr;
105 JSValueRef exception = nullptr;
106 const char* reentryScript = "100";
107 JSStringRef script = JSStringCreateWithUTF8CString(reentryScript);
108 v = JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
109 JSStringRelease(script);
110 if (exception) {
111 printf("FAIL: Watchdog timeout was not reset.\n");
112 failed = true;
113 } else if (!JSValueIsNumber(context, v) || JSValueToNumber(context, v, nullptr) != 100) {
114 printf("FAIL: Script result is not as expected.\n");
115 failed = true;
116 }
117}
118
119int testExecutionTimeLimit()
120{
121 static const TierOptions tierOptionsList[] = {
122 { "LLINT", 0_ms, "--useConcurrentJIT=false --useLLInt=true --useJIT=false" },
123 { "Baseline", 0_ms, "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=false" },
124 { "DFG", 200_ms, "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=true --useFTLJIT=false" },
125#if ENABLE(FTL_JIT)
126 { "FTL", 500_ms, "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=true --useFTLJIT=true" },
127#endif
128 };
129
130 bool failed = false;
131
132 JSC::initializeThreading();
133 Options::initialize(); // Ensure options is initialized first.
134
135 for (auto tierOptions : tierOptionsList) {
136 StringBuilder savedOptionsBuilder;
137 Options::dumpAllOptionsInALine(savedOptionsBuilder);
138
139 Options::setOptions(tierOptions.optionsStr);
140
141 Seconds tierAdjustment = tierOptions.timeLimitAdjustment;
142 Seconds timeLimit;
143
144 context = JSGlobalContextCreateInGroup(nullptr, nullptr);
145
146 JSContextGroupRef contextGroup = JSContextGetGroup(context);
147 JSObjectRef globalObject = JSContextGetGlobalObject(context);
148 ASSERT(JSValueIsObject(context, globalObject));
149
150 JSValueRef exception = nullptr;
151
152 JSStringRef currentCPUTimeStr = JSStringCreateWithUTF8CString("currentCPUTime");
153 JSObjectRef currentCPUTimeFunction = JSObjectMakeFunctionWithCallback(context, currentCPUTimeStr, currentCPUTimeAsJSFunctionCallback);
154 JSObjectSetProperty(context, globalObject, currentCPUTimeStr, currentCPUTimeFunction, kJSPropertyAttributeNone, nullptr);
155 JSStringRelease(currentCPUTimeStr);
156
157 /* Test script on another thread: */
158 timeLimit = 100_ms + tierAdjustment;
159 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), shouldTerminateCallback, 0);
160 {
161#if OS(LINUX) && (CPU(MIPS) || CPU(ARM_THUMB2))
162 Seconds timeAfterWatchdogShouldHaveFired = 500_ms + tierAdjustment;
163#else
164 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
165#endif
166
167 JSStringRef script = JSStringCreateWithUTF8CString("function foo() { while (true) { } } foo();");
168 exception = nullptr;
169 JSValueRef* exn = &exception;
170 shouldTerminateCallbackWasCalled = false;
171 auto thread = Thread::create("Rogue thread", [=] {
172 JSEvaluateScript(context, script, nullptr, nullptr, 1, exn);
173 });
174
175 sleep(timeAfterWatchdogShouldHaveFired);
176
177 if (shouldTerminateCallbackWasCalled)
178 printf("PASS: %s script timed out as expected.\n", tierOptions.tier);
179 else {
180 printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
181 exit(1);
182 }
183
184 if (!exception) {
185 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
186 exit(1);
187 }
188
189 thread->waitForCompletion();
190 testResetAfterTimeout(failed);
191
192 JSStringRelease(script);
193 }
194
195 /* Test script timeout: */
196 timeLimit = 100_ms + tierAdjustment;
197 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), shouldTerminateCallback, 0);
198 {
199 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
200
201 StringBuilder scriptBuilder;
202 scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
203 scriptBuilder.appendFixedPrecisionNumber(timeAfterWatchdogShouldHaveFired.seconds());
204 scriptBuilder.appendLiteral(") break; } } foo();");
205
206 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
207 exception = nullptr;
208 shouldTerminateCallbackWasCalled = false;
209 auto startTime = CPUTime::forCurrentThread();
210 JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
211 auto endTime = CPUTime::forCurrentThread();
212 JSStringRelease(script);
213
214 if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) && shouldTerminateCallbackWasCalled)
215 printf("PASS: %s script timed out as expected.\n", tierOptions.tier);
216 else {
217 if ((endTime - startTime) >= timeAfterWatchdogShouldHaveFired)
218 printf("FAIL: %s script did not time out as expected.\n", tierOptions.tier);
219 if (!shouldTerminateCallbackWasCalled)
220 printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
221 failed = true;
222 }
223
224 if (!exception) {
225 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
226 failed = true;
227 }
228
229 testResetAfterTimeout(failed);
230 }
231
232 /* Test script timeout with tail calls: */
233 timeLimit = 100_ms + tierAdjustment;
234 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), shouldTerminateCallback, 0);
235 {
236 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
237
238 StringBuilder scriptBuilder;
239 scriptBuilder.appendLiteral("var startTime = currentCPUTime();"
240 "function recurse(i) {"
241 "'use strict';"
242 "if (i % 1000 === 0) {"
243 "if (currentCPUTime() - startTime >");
244 scriptBuilder.appendFixedPrecisionNumber(timeAfterWatchdogShouldHaveFired.seconds());
245 scriptBuilder.appendLiteral(" ) { return; }");
246 scriptBuilder.appendLiteral(" }");
247 scriptBuilder.appendLiteral(" return recurse(i + 1); }");
248 scriptBuilder.appendLiteral("recurse(0);");
249
250 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
251 exception = nullptr;
252 shouldTerminateCallbackWasCalled = false;
253 auto startTime = CPUTime::forCurrentThread();
254 JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
255 auto endTime = CPUTime::forCurrentThread();
256 JSStringRelease(script);
257
258 if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) && shouldTerminateCallbackWasCalled)
259 printf("PASS: %s script with infinite tail calls timed out as expected .\n", tierOptions.tier);
260 else {
261 if ((endTime - startTime) >= timeAfterWatchdogShouldHaveFired)
262 printf("FAIL: %s script with infinite tail calls did not time out as expected.\n", tierOptions.tier);
263 if (!shouldTerminateCallbackWasCalled)
264 printf("FAIL: %s script with infinite tail calls' timeout callback was not called.\n", tierOptions.tier);
265 failed = true;
266 }
267
268 if (!exception) {
269 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
270 failed = true;
271 }
272
273 testResetAfterTimeout(failed);
274 }
275
276 /* Test the script timeout's TerminatedExecutionException should NOT be catchable: */
277 timeLimit = 100_ms + tierAdjustment;
278 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), shouldTerminateCallback, 0);
279 {
280 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
281
282 StringBuilder scriptBuilder;
283 scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); try { while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
284 scriptBuilder.appendFixedPrecisionNumber(timeAfterWatchdogShouldHaveFired.seconds());
285 scriptBuilder.appendLiteral(") break; } } catch(e) { } } foo();");
286
287 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
288 exception = nullptr;
289 shouldTerminateCallbackWasCalled = false;
290
291 auto startTime = CPUTime::forCurrentThread();
292 JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
293 auto endTime = CPUTime::forCurrentThread();
294
295 JSStringRelease(script);
296
297 if (((endTime - startTime) >= timeAfterWatchdogShouldHaveFired) || !shouldTerminateCallbackWasCalled) {
298 if (!((endTime - startTime) < timeAfterWatchdogShouldHaveFired))
299 printf("FAIL: %s script did not time out as expected.\n", tierOptions.tier);
300 if (!shouldTerminateCallbackWasCalled)
301 printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
302 failed = true;
303 }
304
305 if (exception)
306 printf("PASS: %s TerminatedExecutionException was not catchable as expected.\n", tierOptions.tier);
307 else {
308 printf("FAIL: %s TerminatedExecutionException was caught.\n", tierOptions.tier);
309 failed = true;
310 }
311
312 testResetAfterTimeout(failed);
313 }
314
315 /* Test script timeout with no callback: */
316 timeLimit = 100_ms + tierAdjustment;
317 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), 0, 0);
318 {
319 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
320
321 StringBuilder scriptBuilder;
322 scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
323 scriptBuilder.appendFixedPrecisionNumber(timeAfterWatchdogShouldHaveFired.seconds());
324 scriptBuilder.appendLiteral(") break; } } foo();");
325
326 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
327 exception = nullptr;
328 shouldTerminateCallbackWasCalled = false;
329
330 auto startTime = CPUTime::forCurrentThread();
331 JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
332 auto endTime = CPUTime::forCurrentThread();
333
334 JSStringRelease(script);
335
336 if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) && !shouldTerminateCallbackWasCalled)
337 printf("PASS: %s script timed out as expected when no callback is specified.\n", tierOptions.tier);
338 else {
339 if ((endTime - startTime) >= timeAfterWatchdogShouldHaveFired)
340 printf("FAIL: %s script did not time out as expected when no callback is specified.\n", tierOptions.tier);
341 else
342 printf("FAIL: %s script called stale callback function.\n", tierOptions.tier);
343 failed = true;
344 }
345
346 if (!exception) {
347 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
348 failed = true;
349 }
350
351 testResetAfterTimeout(failed);
352 }
353
354 /* Test script timeout cancellation: */
355 timeLimit = 100_ms + tierAdjustment;
356 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), cancelTerminateCallback, 0);
357 {
358 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
359
360 StringBuilder scriptBuilder;
361 scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
362 scriptBuilder.appendFixedPrecisionNumber(timeAfterWatchdogShouldHaveFired.seconds());
363 scriptBuilder.appendLiteral(") break; } } foo();");
364
365 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
366 exception = nullptr;
367 cancelTerminateCallbackWasCalled = false;
368
369 auto startTime = CPUTime::forCurrentThread();
370 JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
371 auto endTime = CPUTime::forCurrentThread();
372
373 JSStringRelease(script);
374
375 if (((endTime - startTime) >= timeAfterWatchdogShouldHaveFired) && cancelTerminateCallbackWasCalled && !exception)
376 printf("PASS: %s script timeout was cancelled as expected.\n", tierOptions.tier);
377 else {
378 if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) || exception)
379 printf("FAIL: %s script timeout was not cancelled.\n", tierOptions.tier);
380 if (!cancelTerminateCallbackWasCalled)
381 printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
382 failed = true;
383 }
384
385 if (exception) {
386 printf("FAIL: %s Unexpected TerminatedExecutionException thrown.\n", tierOptions.tier);
387 failed = true;
388 }
389 }
390
391 /* Test script timeout extension: */
392 timeLimit = 100_ms + tierAdjustment;
393 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), extendTerminateCallback, 0);
394 {
395 Seconds timeBeforeExtendedDeadline = 250_ms + tierAdjustment;
396 Seconds timeAfterExtendedDeadline = 600_ms + tierAdjustment;
397 Seconds maxBusyLoopTime = 750_ms + tierAdjustment;
398
399 StringBuilder scriptBuilder;
400 scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
401 scriptBuilder.appendFixedPrecisionNumber(maxBusyLoopTime.seconds()); // in seconds.
402 scriptBuilder.appendLiteral(") break; } } foo();");
403
404 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
405 exception = nullptr;
406 extendTerminateCallbackCalled = 0;
407
408 auto startTime = CPUTime::forCurrentThread();
409 JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
410 auto endTime = CPUTime::forCurrentThread();
411 auto deltaTime = endTime - startTime;
412
413 JSStringRelease(script);
414
415 if ((deltaTime >= timeBeforeExtendedDeadline) && (deltaTime < timeAfterExtendedDeadline) && (extendTerminateCallbackCalled == 2) && exception)
416 printf("PASS: %s script timeout was extended as expected.\n", tierOptions.tier);
417 else {
418 if (deltaTime < timeBeforeExtendedDeadline)
419 printf("FAIL: %s script timeout was not extended as expected.\n", tierOptions.tier);
420 else if (deltaTime >= timeAfterExtendedDeadline)
421 printf("FAIL: %s script did not timeout.\n", tierOptions.tier);
422
423 if (extendTerminateCallbackCalled < 1)
424 printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
425 if (extendTerminateCallbackCalled < 2)
426 printf("FAIL: %s script timeout callback was not called after timeout extension.\n", tierOptions.tier);
427
428 if (!exception)
429 printf("FAIL: %s TerminatedExecutionException was not thrown during timeout extension test.\n", tierOptions.tier);
430
431 failed = true;
432 }
433 }
434
435#if HAVE(MACH_EXCEPTIONS)
436 /* Test script timeout from dispatch queue: */
437 timeLimit = 100_ms + tierAdjustment;
438 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), dispatchTermitateCallback, 0);
439 {
440 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
441
442 StringBuilder scriptBuilder;
443 scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
444 scriptBuilder.appendFixedPrecisionNumber(timeAfterWatchdogShouldHaveFired.seconds());
445 scriptBuilder.appendLiteral(") break; } } foo();");
446
447 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
448 exception = nullptr;
449 dispatchTerminateCallbackCalled = false;
450
451 // We have to do this since blocks can only capture things as const.
452 JSGlobalContextRef& contextRef = context;
453 JSStringRef& scriptRef = script;
454 JSValueRef& exceptionRef = exception;
455
456 Lock syncLock;
457 Lock& syncLockRef = syncLock;
458 Condition synchronize;
459 Condition& synchronizeRef = synchronize;
460 bool didSynchronize = false;
461 bool& didSynchronizeRef = didSynchronize;
462
463 Seconds startTime;
464 Seconds endTime;
465
466 Seconds& startTimeRef = startTime;
467 Seconds& endTimeRef = endTime;
468
469 dispatch_group_t group = dispatch_group_create();
470 dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
471 startTimeRef = CPUTime::forCurrentThread();
472 JSEvaluateScript(contextRef, scriptRef, nullptr, nullptr, 1, &exceptionRef);
473 endTimeRef = CPUTime::forCurrentThread();
474 auto locker = WTF::holdLock(syncLockRef);
475 didSynchronizeRef = true;
476 synchronizeRef.notifyAll();
477 });
478
479 auto locker = holdLock(syncLock);
480 synchronize.wait(syncLock, [&] { return didSynchronize; });
481
482 if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) && dispatchTerminateCallbackCalled)
483 printf("PASS: %s script on dispatch queue timed out as expected.\n", tierOptions.tier);
484 else {
485 if ((endTime - startTime) >= timeAfterWatchdogShouldHaveFired)
486 printf("FAIL: %s script on dispatch queue did not time out as expected.\n", tierOptions.tier);
487 if (!shouldTerminateCallbackWasCalled)
488 printf("FAIL: %s script on dispatch queue timeout callback was not called.\n", tierOptions.tier);
489 failed = true;
490 }
491
492 JSStringRelease(script);
493 }
494#endif
495
496 JSGlobalContextRelease(context);
497
498 Options::setOptions(savedOptionsBuilder.toString().ascii().data());
499 }
500
501 return failed;
502}
503