From f31cf6f9fe805539d50ed55b62b05ce12daf7cb0 Mon Sep 17 00:00:00 2001 From: Miciah Dashiel Butler Masters Date: Sat, 18 Jul 2009 23:41:01 +0000 Subject: [PATCH] Heartbeat code using JS_TriggerOperationCallback Implement new heartbeat code to catch runaway execution of document ECMAScript code. The old code uses JS_SetBranchCallback which is deprecated in new versions of SpiderMonkey. The new code uses setitimer(2) and the JS_SetOperationCallback and JS_TriggerOperationCallback interfaces, introduced in SpiderMonkey 1.8.1. Compatibility with both the old JS_SetBranchCallback and the new interfaces is maintained. --- Makefile.config.in | 1 + configure.in | 12 +++ src/ecmascript/ecmascript.h | 4 + src/ecmascript/spidermonkey.c | 38 ++++++- src/ecmascript/spidermonkey/Makefile | 2 + src/ecmascript/spidermonkey/heartbeat.c | 125 ++++++++++++++++++++++++ src/ecmascript/spidermonkey/heartbeat.h | 24 +++++ 7 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 src/ecmascript/spidermonkey/heartbeat.c create mode 100644 src/ecmascript/spidermonkey/heartbeat.h diff --git a/Makefile.config.in b/Makefile.config.in index 37af7f84..b383e60c 100644 --- a/Makefile.config.in +++ b/Makefile.config.in @@ -117,6 +117,7 @@ CONFIG_DOM = @CONFIG_DOM@ CONFIG_ECMASCRIPT = @CONFIG_ECMASCRIPT@ CONFIG_ECMASCRIPT_SEE = @CONFIG_ECMASCRIPT_SEE@ CONFIG_ECMASCRIPT_SMJS = @CONFIG_ECMASCRIPT_SMJS@ +CONFIG_ECMASCRIPT_SMJS_HEARTBEAT = @CONFIG_ECMASCRIPT_SMJS@ CONFIG_EXMODE = @CONFIG_EXMODE@ CONFIG_FASTMEM = @CONFIG_FASTMEM@ CONFIG_FINGER = @CONFIG_FINGER@ diff --git a/configure.in b/configure.in index 4a7143db..08c4bbc1 100644 --- a/configure.in +++ b/configure.in @@ -283,6 +283,7 @@ AC_CHECK_FUNCS(snprintf vsnprintf asprintf vasprintf) AC_CHECK_FUNCS(getifaddrs getpwnam inet_pton inet_ntop) AC_CHECK_FUNCS(fflush fsync fseeko ftello sigaction) AC_CHECK_FUNCS(gettimeofday clock_gettime) +AC_CHECK_FUNCS(setitimer, HAVE_SETITIMER=yes) AC_CHECK_FUNCS([cygwin_conv_to_full_win32_path]) @@ -654,6 +655,8 @@ AC_MSG_RESULT($cf_result) CONFIG_SPIDERMONKEY="$cf_result" if test "$cf_result" = "yes"; then AC_CHECK_FUNCS([[JS_ReportAllocationOverflow]]) + AC_CHECK_FUNCS(JS_SetBranchCallback) + AC_CHECK_FUNCS(JS_TriggerOperationCallback, HAVE_JS_TRIGGEROPERATIONCALLBACK=yes) fi EL_RESTORE_FLAGS @@ -668,6 +671,15 @@ EL_CONFIG_DEPENDS(CONFIG_ECMASCRIPT, [CONFIG_ECMASCRIPT_SEE CONFIG_ECMASCRIPT_SM AC_SUBST(CONFIG_ECMASCRIPT_SEE) AC_SUBST(CONFIG_ECMASCRIPT_SMJS) +if test "x$CONFIG_ECMASCRIPT_SMJS" = xyes && + test "x$HAVE_JS_TRIGGEROPERATIONCALLBACK" = xyes && + test "x$HAVE_SETITIMER" = xyes; then + EL_CONFIG(CONFIG_ECMASCRIPT_SMJS_HEARTBEAT, [ECMAScript heartbeat support]) +else + CONFIG_ECMASCRIPT_SMJS_HEARTBEAT=no +fi +AC_SUBST(CONFIG_ECMASCRIPT_SMJS_HEARTBEAT) + # =================================================================== # Optional Spidermonkey-based ECMAScript browser scripting diff --git a/src/ecmascript/ecmascript.h b/src/ecmascript/ecmascript.h index 2f1c052b..8bf25de2 100644 --- a/src/ecmascript/ecmascript.h +++ b/src/ecmascript/ecmascript.h @@ -32,7 +32,11 @@ struct ecmascript_interpreter { /* The code evaluated by setTimeout() */ struct string code; +#if defined(CONFIG_ECMASCRIPT_SMJS_HEARTBEAT) + struct heartbeat *heartbeat; +#elif defined(HAVE_JS_SETBRANCHCALLBACK) time_t exec_start; +#endif /* This is a cross-rerenderings accumulator of * @document.onload_snippets (see its description for juicy details). diff --git a/src/ecmascript/spidermonkey.c b/src/ecmascript/spidermonkey.c index 56b29560..97454eaf 100644 --- a/src/ecmascript/spidermonkey.c +++ b/src/ecmascript/spidermonkey.c @@ -25,6 +25,7 @@ #include "ecmascript/spidermonkey.h" #include "ecmascript/spidermonkey/document.h" #include "ecmascript/spidermonkey/form.h" +#include "ecmascript/spidermonkey/heartbeat.h" #include "ecmascript/spidermonkey/location.h" #include "ecmascript/spidermonkey/navigator.h" #include "ecmascript/spidermonkey/unibar.h" @@ -109,6 +110,7 @@ reported: JS_ClearPendingException(ctx); } +#if !defined(CONFIG_ECMASCRIPT_SMJS_HEARTBEAT) && defined(HAVE_JS_SETBRANCHCALLBACK) static JSBool safeguard(JSContext *ctx, JSScript *script) { @@ -133,6 +135,7 @@ setup_safeguard(struct ecmascript_interpreter *interpreter, interpreter->exec_start = time(NULL); JS_SetBranchCallback(ctx, safeguard); } +#endif static void @@ -173,6 +176,9 @@ spidermonkey_get_interpreter(struct ecmascript_interpreter *interpreter) * some kind of bytecode cache. (If we will ever do that.) */ JS_SetOptions(ctx, JSOPTION_VAROBJFIX | JSOPTION_COMPILE_N_GO); JS_SetErrorReporter(ctx, error_reporter); +#if defined(CONFIG_ECMASCRIPT_SMJS_HEARTBEAT) + JS_SetOperationCallback(ctx, heartbeat_callback); +#endif window_obj = JS_NewObject(ctx, (JSClass *) &window_class, NULL, NULL); if (!window_obj) goto release_and_fail; @@ -264,10 +270,17 @@ spidermonkey_eval(struct ecmascript_interpreter *interpreter, assert(interpreter); if (!js_module_init_ok) return; ctx = interpreter->backend_data; +#if defined(CONFIG_ECMASCRIPT_SMJS_HEARTBEAT) + interpreter->heartbeat = add_heartbeat(interpreter); +#elif defined(HAVE_JS_SETBRANCHCALLBACK) setup_safeguard(interpreter, ctx); +#endif interpreter->ret = ret; JS_EvaluateScript(ctx, JS_GetGlobalObject(ctx), code->source, code->length, "", 0, &rval); +#if defined(CONFIG_ECMASCRIPT_SMJS_HEARTBEAT) + done_heartbeat(interpreter->heartbeat); +#endif } @@ -275,17 +288,25 @@ unsigned char * spidermonkey_eval_stringback(struct ecmascript_interpreter *interpreter, struct string *code) { + JSBool ret; JSContext *ctx; jsval rval; assert(interpreter); if (!js_module_init_ok) return NULL; ctx = interpreter->backend_data; - setup_safeguard(interpreter, ctx); interpreter->ret = NULL; - if (JS_EvaluateScript(ctx, JS_GetGlobalObject(ctx), - code->source, code->length, "", 0, &rval) - == JS_FALSE) { +#if defined(CONFIG_ECMASCRIPT_SMJS_HEARTBEAT) + interpreter->heartbeat = add_heartbeat(interpreter); +#elif defined(HAVE_JS_SETBRANCHCALLBACK) + setup_safeguard(interpreter, ctx); +#endif + ret = JS_EvaluateScript(ctx, JS_GetGlobalObject(ctx), + code->source, code->length, "", 0, &rval); +#if defined(CONFIG_ECMASCRIPT_SMJS_HEARTBEAT) + done_heartbeat(interpreter->heartbeat); +#endif + if (ret == JS_FALSE) { return NULL; } if (JSVAL_IS_VOID(rval)) { @@ -309,14 +330,21 @@ spidermonkey_eval_boolback(struct ecmascript_interpreter *interpreter, assert(interpreter); if (!js_module_init_ok) return 0; ctx = interpreter->backend_data; - setup_safeguard(interpreter, ctx); interpreter->ret = NULL; fun = JS_CompileFunction(ctx, NULL, "", 0, NULL, code->source, code->length, "", 0); if (!fun) return -1; +#if defined(CONFIG_ECMASCRIPT_SMJS_HEARTBEAT) + interpreter->heartbeat = add_heartbeat(interpreter); +#elif defined(HAVE_JS_SETBRANCHCALLBACK) + setup_safeguard(interpreter, ctx); +#endif ret = JS_CallFunction(ctx, NULL, fun, 0, NULL, &rval); +#if defined(CONFIG_ECMASCRIPT_SMJS_HEARTBEAT) + done_heartbeat(interpreter->heartbeat); +#endif if (ret == 2) { /* onClick="history.back()" */ return 0; } diff --git a/src/ecmascript/spidermonkey/Makefile b/src/ecmascript/spidermonkey/Makefile index f1c0fef3..377ca804 100644 --- a/src/ecmascript/spidermonkey/Makefile +++ b/src/ecmascript/spidermonkey/Makefile @@ -2,6 +2,8 @@ top_builddir=../../.. include $(top_builddir)/Makefile.config INCLUDES += $(SPIDERMONKEY_CFLAGS) +OBJS-$(CONFIG_ECMASCRIPT_SMJS_HEARTBEAT) += heartbeat.o + OBJS = document.o form.o location.o navigator.o unibar.o window.o include $(top_srcdir)/Makefile.lib diff --git a/src/ecmascript/spidermonkey/heartbeat.c b/src/ecmascript/spidermonkey/heartbeat.c new file mode 100644 index 00000000..242fc794 --- /dev/null +++ b/src/ecmascript/spidermonkey/heartbeat.c @@ -0,0 +1,125 @@ +/* The SpiderMonkey ECMAScript backend heartbeat fuctionality. */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include /* setitimer(2) */ + +#include "elinks.h" + +#include "ecmascript/spidermonkey/util.h" + +#include "config/options.h" +#include "document/view.h" +#include "ecmascript/ecmascript.h" +#include "ecmascript/spidermonkey.h" +#include "ecmascript/spidermonkey/heartbeat.h" +#include "osdep/signals.h" +#include "session/session.h" +#include "util/lists.h" +#include "util/math.h" /* int_upper_bound */ +#include "util/memory.h" +#include "viewer/text/vs.h" + + + +static INIT_LIST_OF(struct heartbeat, heartbeats); + +static struct itimerval heartbeat_timer = { { 1, 0 }, { 1, 0 } }; + + +/* This callback is installed by JS_SetOperationCallback and triggered + * by JS_TriggerOperationCallback in the heartbeat code below. Returning + * JS_FALSE terminates script execution immediately. */ +JSBool +heartbeat_callback(JSContext *ctx) +{ + return JS_FALSE; +} + +/* Callback for SIGVTALRM. Go through all heartbeats, decrease each + * one's TTL, and call JS_TriggerOperationCallback if a heartbeat's TTL + * goes to 0. */ +static void +check_heartbeats(void *data) +{ + struct heartbeat *hb; + + foreach (hb, heartbeats) { + assert(hb->interpreter); + + --hb->ttl; + + if (hb->ttl <= 0) { + if (hb->interpreter->vs + && hb->interpreter->vs->doc_view + && hb->interpreter->vs->doc_view->session + && hb->interpreter->vs->doc_view->session->tab + && hb->interpreter->vs->doc_view->session->tab->term) { + struct session *ses = hb->interpreter->vs->doc_view->session; + struct terminal *term = ses->tab->term; + int max_exec_time = get_opt_int("ecmascript.max_exec_time", ses); + + ecmascript_timeout_dialog(term, max_exec_time); + } + + JS_TriggerOperationCallback(hb->interpreter->backend_data); + } + } + + install_signal_handler(SIGVTALRM, check_heartbeats, NULL, 1); +} + +/* Create a new heartbeat for the given interpreter. */ +struct heartbeat * +add_heartbeat(struct ecmascript_interpreter *interpreter) +{ + struct session *ses; + struct heartbeat *hb; + + assert(interpreter); + + if (!interpreter->vs || !interpreter->vs->doc_view) + ses = NULL; + else + ses = interpreter->vs->doc_view->session; + + hb = mem_alloc(sizeof(struct heartbeat)); + if (!hb) return NULL; + + hb->ttl = get_opt_int("ecmascript.max_exec_time", ses); + hb->interpreter = interpreter; + + add_to_list(heartbeats, hb); + + /* Update the heartbeat timer. */ + if (list_is_singleton(*hb)) { + heartbeat_timer.it_value.tv_sec = 1; + setitimer(ITIMER_VIRTUAL, &heartbeat_timer, NULL); + } + + /* We install the handler every call to add_heartbeat instead of only on + * module initialisation because other code may set other handlers for + * the signal. */ + install_signal_handler(SIGVTALRM, check_heartbeats, NULL, 1); + + return hb; +} + +/* Destroy the given heartbeat. */ +void +done_heartbeat(struct heartbeat *hb) +{ + assert(hb->interpreter); + + /* Stop the heartbeat timer if this heartbeat is the only one. */ + if (list_is_singleton(*hb)) { + heartbeat_timer.it_value.tv_sec = 0; + setitimer(ITIMER_VIRTUAL, &heartbeat_timer, NULL); + } + + del_from_list(hb); + hb->interpreter->heartbeat = NULL; + mem_free(hb); +} diff --git a/src/ecmascript/spidermonkey/heartbeat.h b/src/ecmascript/spidermonkey/heartbeat.h new file mode 100644 index 00000000..f7c8b127 --- /dev/null +++ b/src/ecmascript/spidermonkey/heartbeat.h @@ -0,0 +1,24 @@ +#ifndef EL__ECMASCRIPT_SPIDERMONKEY_HEARTBEAT_H +#define EL__ECMASCRIPT_SPIDERMONKEY_HEARTBEAT_H + +#include "ecmascript/spidermonkey/util.h" + +#include "ecmascript/spidermonkey.h" + +struct heartbeat { + LIST_HEAD(struct heartbeat); + + int ttl; /* Time to live. This value is assigned when the + * script begins execution and is decremented every + * second. When it reaches 0, script execution is + * terminated. */ + + struct ecmascript_interpreter *interpreter; +}; + +struct heartbeat *add_heartbeat(struct ecmascript_interpreter *interpreter); +void done_heartbeat(struct heartbeat *hb); + +JSBool heartbeat_callback(JSContext *ctx); + +#endif