Skip to content

Commit

Permalink
API for limiting resource usage.
Browse files Browse the repository at this point in the history
You can now enforce crude resource usage limits for scripts executing in the JS
VM. Its not overly granular and has some caveats, but it appears to work.

To limit the time a script can run:

    >>> rt = spidermonkey.Runtime()
    >>> cx = rt.new_context()
    >>> cx.max_time(5) # Specified in seconds, returns the previous value.
    0
    >>> cx.execute("while(true) {}")
    ...
    RuntimeError

To limit the amount of memory is similar:

    >>> rt = spidermonkey.Runtime()
    >>> cx = rt.new_context()
    >>> cx.max_memory(10000) # Specified in bytes, returns the previous value.
    0
    >>> cx.execute("var f = []; var b = 100000; while(b-- > 0) f.push(b*0.9)")
    ...
    RuntimeError

At the moment this looks like its limiting resources per context when in reality
its per runtime. So if you limit to usage on a context, and some other script in
the context is running above that limit, the limited context will fail.

You can also get the current values without setting:

    >>> cx.max_time(10)
    ...
    >>> cx.max_time()
    10
    >>> cx.max_memory(10000)
    ...
    >>> cx.max_memory()
    10000

Some minor tweaks to make the memory limit specified on the Runtime and to
improve the reported errors to follow shortly.

[#12]
  • Loading branch information
Paul Davis committed May 11, 2009
1 parent 4d97105 commit e203f17
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 11 deletions.
133 changes: 126 additions & 7 deletions spidermonkey/context.c
Expand Up @@ -7,7 +7,11 @@
*/

#include "spidermonkey.h"

#include <time.h> // After spidermonkey.h so after Python.h

#include "libjs/jsobj.h"
#include "libjs/jscntxt.h"

JSBool
get_prop(JSContext* jscx, JSObject* jsobj, jsval key, jsval* rval)
Expand Down Expand Up @@ -159,6 +163,56 @@ js_global_class = {
JSCLASS_NO_OPTIONAL_MEMBERS
};

#define MAX(a, b) ((a) > (b) ? (a) : (b))
JSBool
branch_cb(JSContext* jscx, JSScript* script)
{
Context* pycx = (Context*) JS_GetContextPrivate(jscx);
time_t now = time(NULL);

if(pycx == NULL)
{
JS_ReportError(jscx, "Failed to find Python context.");
return JS_FALSE;
}

// Get out quick if we don't have any quotas.
if(pycx->max_time == 0 && pycx->max_heap == 0)
{
return JS_TRUE;
}

// Only check occasionally for resource usage.
pycx->branch_count++;
if((pycx->branch_count > 0x3FFF) != 1)
{
return JS_TRUE;
}

pycx->branch_count = 0;

if(pycx->max_heap > 0 && jscx->runtime->gcBytes > pycx->max_heap)
{
// First see if garbage collection gets under the threshold.
JS_GC(jscx);
if(jscx->runtime->gcBytes > pycx->max_heap)
{
return JS_FALSE;
}
}

if(
pycx->max_time > 0
&& pycx->start_time > 0
&& pycx->max_time < now - pycx->start_time
)
{
return JS_FALSE;
}

return JS_TRUE;
}

PyObject*
Context_new(PyTypeObject* type, PyObject* args, PyObject* kwargs)
{
Expand Down Expand Up @@ -234,6 +288,13 @@ Context_new(PyTypeObject* type, PyObject* args, PyObject* kwargs)
if(global != NULL) Py_INCREF(global);
self->global = global;

// Setup counters for resource limits
self->branch_count = 0;
self->max_time = 0;
self->start_time = 0;
self->max_heap = 0;

JS_SetBranchCallback(self->cx, branch_cb);
JS_SetErrorReporter(self->cx, report_error_cb);

Py_INCREF(runtime);
Expand Down Expand Up @@ -268,7 +329,7 @@ Context_dealloc(Context* self)
Py_XDECREF(self->global);
Py_XDECREF(self->objects);
Py_XDECREF(self->classes);
Py_DECREF(self->rt);
Py_XDECREF(self->rt);
}

PyObject*
Expand Down Expand Up @@ -365,6 +426,7 @@ Context_execute(Context* self, PyObject* args, PyObject* kwargs)
JSObject* root = NULL;
JSString* script = NULL;
jschar* schars = NULL;
JSBool started_counter = JS_FALSE;
size_t slen;
jsval rval;

Expand All @@ -380,6 +442,13 @@ Context_execute(Context* self, PyObject* args, PyObject* kwargs)
cx = self->cx;
root = self->root;

// Mark us for time consumption
if(self->start_time == 0)
{
started_counter = JS_TRUE;
self->start_time = time(NULL);
}

if(!JS_EvaluateUCScript(cx, root, schars, slen, "<JavaScript>", 1, &rval))
{
if(!PyErr_Occurred())
Expand All @@ -389,21 +458,23 @@ Context_execute(Context* self, PyObject* args, PyObject* kwargs)
goto error;
}

if(PyErr_Occurred())
{
PyErr_PrintEx(0);
exit(-1);
}
if(PyErr_Occurred()) goto error;

ret = js2py(self, rval);

JS_EndRequest(self->cx);
JS_MaybeGC(self->cx);
goto success;

error:
JS_EndRequest(self->cx);
success:

if(started_counter)
{
self->start_time = 0;
}

return ret;
}

Expand All @@ -414,6 +485,42 @@ Context_gc(Context* self, PyObject* args, PyObject* kwargs)
return (PyObject*) self;
}

PyObject*
Context_max_memory(Context* self, PyObject* args, PyObject* kwargs)
{
PyObject* ret = NULL;
long curr_max = -1;
long new_max = -1;

if(!PyArg_ParseTuple(args, "|l", &new_max)) goto done;

curr_max = self->max_heap;
if(new_max >= 0) self->max_heap = new_max;

ret = PyLong_FromLong(curr_max);

done:
return ret;
}

PyObject*
Context_max_time(Context* self, PyObject* args, PyObject* kwargs)
{
PyObject* ret = NULL;
time_t curr_max = (time_t) -1;
time_t new_max = (time_t) -1;

if(!PyArg_ParseTuple(args, "|I", &new_max)) goto done;

curr_max = self->max_time;
if(new_max != ((time_t) -1)) self->max_time = new_max;

ret = PyLong_FromLong((long) curr_max);

done:
return ret;
}

static PyMemberDef Context_members[] = {
{NULL}
};
Expand Down Expand Up @@ -443,6 +550,18 @@ static PyMethodDef Context_methods[] = {
METH_VARARGS,
"Force garbage collection of the JS context."
},
{
"max_memory",
(PyCFunction)Context_max_memory,
METH_VARARGS,
"Get/Set the maximum memory allocation allowed for a context."
},
{
"max_time",
(PyCFunction)Context_max_time,
METH_VARARGS,
"Get/Set the maximum time a context can execute for."
},
{NULL}
};

Expand Down
4 changes: 4 additions & 0 deletions spidermonkey/context.h
Expand Up @@ -22,6 +22,10 @@ typedef struct {
JSObject* root;
PyDictObject* classes;
PySetObject* objects;
uint32 branch_count;
long max_heap;
time_t max_time;
time_t start_time;
} Context;

PyObject* Context_get_class(Context* cx, const char* key);
Expand Down
16 changes: 16 additions & 0 deletions spidermonkey/jsfunction.c
Expand Up @@ -63,6 +63,7 @@ Function_call(Function* self, PyObject* args, PyObject* kwargs)
jsval func;
jsval* argv = NULL;
jsval rval;
JSBool started_counter = JS_FALSE;

JS_BeginRequest(self->obj.cx->cx);

Expand Down Expand Up @@ -90,6 +91,14 @@ Function_call(Function* self, PyObject* args, PyObject* kwargs)
func = self->obj.val;
cx = self->obj.cx->cx;
parent = JSVAL_TO_OBJECT(self->parent);

// Mark us for execution time if not already marked
if(self->obj.cx->start_time == 0)
{
started_counter = JS_TRUE;
self->obj.cx->start_time = time(NULL);
}

if(!JS_CallFunctionValue(cx, parent, func, argc, argv, &rval))
{
PyErr_SetString(PyExc_RuntimeError, "Failed to execute JS Function.");
Expand All @@ -105,6 +114,13 @@ Function_call(Function* self, PyObject* args, PyObject* kwargs)
if(argv != NULL) free(argv);
JS_EndRequest(self->obj.cx->cx);
success:

// Reset the time counter if we started it.
if(started_counter)
{
self->obj.cx->start_time = 0;
}

Py_XDECREF(item);
return ret;
}
Expand Down
7 changes: 5 additions & 2 deletions spidermonkey/runtime.c
Expand Up @@ -11,12 +11,15 @@
PyObject*
Runtime_new(PyTypeObject* type, PyObject* args, PyObject* kwargs)
{
Runtime* self;
Runtime* self = NULL;
unsigned int stacksize = 0x2000000; // 32 MiB heap size.

if(!PyArg_ParseTuple(args, "|I", &stacksize)) goto error;

self = (Runtime*) type->tp_alloc(type, 0);
if(self == NULL) goto error;

self->rt = JS_NewRuntime(1000000);
self->rt = JS_NewRuntime(stacksize);
if(self->rt == NULL) goto error;

goto success;
Expand Down
3 changes: 2 additions & 1 deletion tests/t.py
Expand Up @@ -71,6 +71,7 @@ def raises(exctype, func, *args, **kwargs):
except exctype, inst:
pass
else:
func_name = getattr(func, "func_name", "<builtin_function>")
raise AssertionError("Function %s did not raise %s" % (
func.func_name, exctype.__name__))
func_name, exctype.__name__))

45 changes: 44 additions & 1 deletion tests/test-context.py
Expand Up @@ -3,6 +3,7 @@
# This file is part of the python-spidermonkey package released
# under the MIT license.
import t
import time

@t.rt()
def test_no_provided_runtime(rt):
Expand All @@ -21,8 +22,50 @@ def test_basic_execution(cx):
t.eq(cx.execute("var x = 4; x * x;"), 16)
t.lt(cx.execute("22/7;") - 3.14285714286, 0.00000001)


@t.cx()
def test_reentry(cx):
cx.execute("var x = 42;")
t.eq(cx.execute("x;"), 42)

@t.cx()
def test_get_set_limits(cx):
t.eq(cx.max_time(), 0)
t.eq(cx.max_memory(), 0)
t.eq(cx.max_time(10), 0) # Accessors return previous value.
t.eq(cx.max_time(), 10)
t.eq(cx.max_memory(10), 0)
t.eq(cx.max_memory(), 10)

@t.cx()
def test_exceed_time(cx):
cx.max_time(1)
t.raises(RuntimeError, cx.execute, "while(true) {}")

@t.cx()
def test_does_not_exceed_time(cx):
cx.max_time(1)
func = cx.execute("function() {return 1;}")
time.sleep(1.1)
cx.execute("var f = 2;");
time.sleep(1.1)
func()
time.sleep(1.1)
cx.execute("f;");

@t.cx()
def test_exceed_memory(cx):
cx.max_memory(10000)
script = "var f = []; var b = 1000000; while(b-- > 0) f[f.length] = b*0.9;"
t.raises(Exception, cx.execute, script)

@t.cx()
def test_small_limit(cx):
cx.max_memory(1)
t.raises(Exception, cx.execute, "far f = 0.3; f;");

@t.cx()
def test_does_not_exceed_memory(cx):
cx.max_memory(10000)
script = "var f = 2; f;"
cx.execute(script)

17 changes: 17 additions & 0 deletions tests/test-runtime.py
Expand Up @@ -7,3 +7,20 @@
@t.rt()
def test_creating_runtime(rt):
t.ne(rt, None)

def test_create_no_memory():
rt = t.spidermonkey.Runtime(1)
t.raises(RuntimeError, rt.new_context)

def test_exceed_memory():
# This test actually tests nothing. I'm leaving it for a bit to
# see if I hear about the bug noted below.
rt = t.spidermonkey.Runtime(10000)
cx = rt.new_context()
script = "var b = []; var f = 10; while(f-- > 0) b.push(2.456);"
# I had this script below original and it triggers some sort of
# bug in the JS VM. I even reduced the test case outside of
# python-spidermonkey to show it. No word from the SM guys.
# script = "var b = []; for(var f in 100000) b.push(2.456);"
cx.execute(script)

0 comments on commit e203f17

Please sign in to comment.