Commit 839c5684 by Ned Batchelder

Hmm, turns out exec wants just one dict to properly simulate Python module execution.

parent 1473fe37
...@@ -462,9 +462,7 @@ class LoncapaProblem(object): ...@@ -462,9 +462,7 @@ class LoncapaProblem(object):
if all_code: if all_code:
try: try:
locals_dict = {} safe_exec.safe_exec(all_code, context, random_seed=self.seed, python_path=python_path)
safe_exec.safe_exec(all_code, context, locals_dict, random_seed=self.seed, python_path=python_path)
context.update(locals_dict)
except Exception as err: except Exception as err:
log.exception("Error while execing script code: " + all_code) log.exception("Error while execing script code: " + all_code)
msg = "Error while executing script code: %s" % str(err).replace('<', '&lt;') msg = "Error while executing script code: %s" % str(err).replace('<', '&lt;')
......
...@@ -936,9 +936,8 @@ class CustomResponse(LoncapaResponse): ...@@ -936,9 +936,8 @@ class CustomResponse(LoncapaResponse):
'expect': expect, 'expect': expect,
'ans': ans, 'ans': ans,
} }
locals_dict = {} safe_exec.safe_exec(code, globals_dict)
safe_exec.safe_exec(code, globals_dict, locals_dict) return globals_dict['cfn_return']
return locals_dict['cfn_return']
return check_function return check_function
self.code = make_check_function(self.context['script_code'], cfn) self.code = make_check_function(self.context['script_code'], cfn)
...@@ -1055,9 +1054,7 @@ class CustomResponse(LoncapaResponse): ...@@ -1055,9 +1054,7 @@ class CustomResponse(LoncapaResponse):
# exec the check function # exec the check function
if isinstance(self.code, basestring): if isinstance(self.code, basestring):
try: try:
locals_dict = {} safe_exec.safe_exec(self.code, self.context)
safe_exec.safe_exec(self.code, self.context, locals_dict)
self.context.update(locals_dict)
except Exception as err: except Exception as err:
self._handle_exec_exception(err) self._handle_exec_exception(err)
...@@ -1762,10 +1759,9 @@ class SchematicResponse(LoncapaResponse): ...@@ -1762,10 +1759,9 @@ class SchematicResponse(LoncapaResponse):
json.loads(student_answers[k]) for k in sorted(self.answer_ids) json.loads(student_answers[k]) for k in sorted(self.answer_ids)
] ]
self.context.update({'submission': submission}) self.context.update({'submission': submission})
locals_dict = {} safe_exec.safe_exec(self.code, self.context)
safe_exec.safe_exec(self.code, self.context, locals_dict)
cmap = CorrectMap() cmap = CorrectMap()
cmap.set_dict(dict(zip(sorted(self.answer_ids), locals_dict['correct']))) cmap.set_dict(dict(zip(sorted(self.answer_ids), self.context['correct'])))
return cmap return cmap
def get_answers(self): def get_answers(self):
......
...@@ -14,13 +14,13 @@ random.Random = random_module.Random ...@@ -14,13 +14,13 @@ random.Random = random_module.Random
del random_module del random_module
""" """
def safe_exec(code, globals_dict, locals_dict, random_seed=None, python_path=None): def safe_exec(code, globals_dict, random_seed=None, python_path=None):
"""Exec python code safely. """Exec python code safely.
""" """
code_prolog = CODE_PROLOG % random_seed code_prolog = CODE_PROLOG % random_seed
codejail.safe_exec.safe_exec( codejail.safe_exec.safe_exec(
code_prolog + code, globals_dict, locals_dict, code_prolog + code, globals_dict,
python_path=python_path, python_path=python_path,
assumed_imports=[ assumed_imports=[
"numpy", "numpy",
......
...@@ -8,39 +8,39 @@ from capa.safe_exec import safe_exec ...@@ -8,39 +8,39 @@ from capa.safe_exec import safe_exec
class TestSafeExec(unittest.TestCase): class TestSafeExec(unittest.TestCase):
def test_set_values(self): def test_set_values(self):
g, l = {}, {} g = {}
safe_exec("a = 17", g, l) safe_exec("a = 17", g)
self.assertEqual(l['a'], 17) self.assertEqual(g['a'], 17)
def test_division(self): def test_division(self):
g, l = {}, {} g = {}
# Future division: 1/2 is 0.5. # Future division: 1/2 is 0.5.
safe_exec("a = 1/2", g, l) safe_exec("a = 1/2", g)
self.assertEqual(l['a'], 0.5) self.assertEqual(g['a'], 0.5)
def test_assumed_imports(self): def test_assumed_imports(self):
g, l = {}, {} g = {}
# Math is always available. # Math is always available.
safe_exec("a = int(math.pi)", g, l) safe_exec("a = int(math.pi)", g)
self.assertEqual(l['a'], 3) self.assertEqual(g['a'], 3)
def test_random_seeding(self): def test_random_seeding(self):
g, l = {}, {} g = {}
r = random.Random(17) r = random.Random(17)
rnums = [r.randint(0, 999) for _ in xrange(100)] rnums = [r.randint(0, 999) for _ in xrange(100)]
# Without a seed, the results are unpredictable # Without a seed, the results are unpredictable
safe_exec("rnums = [random.randint(0, 999) for _ in xrange(100)]", g, l) safe_exec("rnums = [random.randint(0, 999) for _ in xrange(100)]", g)
self.assertNotEqual(l['rnums'], rnums) self.assertNotEqual(g['rnums'], rnums)
# With a seed, the results are predictable # With a seed, the results are predictable
safe_exec("rnums = [random.randint(0, 999) for _ in xrange(100)]", g, l, random_seed=17) safe_exec("rnums = [random.randint(0, 999) for _ in xrange(100)]", g, random_seed=17)
self.assertEqual(l['rnums'], rnums) self.assertEqual(g['rnums'], rnums)
def test_python_lib(self): def test_python_lib(self):
pylib = os.path.dirname(__file__) + "/test_files/pylib" pylib = os.path.dirname(__file__) + "/test_files/pylib"
g, l = {}, {} g = {}
safe_exec( safe_exec(
"import constant; a = constant.THE_CONST", "import constant; a = constant.THE_CONST",
g, l, python_path=[pylib] g, python_path=[pylib]
) )
...@@ -28,19 +28,19 @@ def names_and_modules(assumed_imports): ...@@ -28,19 +28,19 @@ def names_and_modules(assumed_imports):
yield modname, modname yield modname, modname
def safe_exec(code, globals_dict, locals_dict, assumed_imports=None, files=None, python_path=None): def safe_exec(code, globals_dict, assumed_imports=None, files=None, python_path=None):
"""Execute code as "exec" does, but safely. """Execute code as "exec" does, but safely.
`code` is a string of Python code. `globals_dict` and `locals_dict` are `code` is a string of Python code. `globals_dict` is used as the globals
dictionaries to use as the globals and locals. Modifications the code during execution. Modifications the code makes to `globals_dict` are
makes to `locals_dict` are reflected in the dictionary on return. reflected in the dictionary on return.
`assumed_imports` is a list of modules to make available as implicit `assumed_imports` is a list of modules to make available as implicit
imports for the code. Entries are either a name, "mod", which makes imports for the code. Entries are either a name, "mod", which makes
"import mod" part of the code, or a pair, ("f", "fooey"), which makes "import mod" part of the code, or a pair, ("f", "fooey"), which makes
"import fooey as f" part of the code. The module name can be dotted. "import fooey as f" part of the code. The module name can be dotted.
Returns None. Changes made by `code` are visible in `locals_dict`. Returns None. Changes made by `code` are visible in `globals_dict`.
""" """
the_code = [] the_code = []
...@@ -49,7 +49,7 @@ def safe_exec(code, globals_dict, locals_dict, assumed_imports=None, files=None, ...@@ -49,7 +49,7 @@ def safe_exec(code, globals_dict, locals_dict, assumed_imports=None, files=None,
the_code.append(textwrap.dedent("""\ the_code.append(textwrap.dedent("""\
import json import json
import sys import sys
code, g_dict, l_dict = json.load(sys.stdin) code, g_dict = json.load(sys.stdin)
""")) """))
for pydir in python_path or (): for pydir in python_path or ():
...@@ -63,13 +63,14 @@ def safe_exec(code, globals_dict, locals_dict, assumed_imports=None, files=None, ...@@ -63,13 +63,14 @@ def safe_exec(code, globals_dict, locals_dict, assumed_imports=None, files=None,
the_code.append("g_dict['{}'] = LazyModule('{}')\n".format(name, modname)) the_code.append("g_dict['{}'] = LazyModule('{}')\n".format(name, modname))
the_code.append(textwrap.dedent("""\ the_code.append(textwrap.dedent("""\
exec code in g_dict, l_dict exec code in g_dict
ok_types = (type(None), int, long, float, str, unicode, list, tuple, dict) ok_types = (type(None), int, long, float, str, unicode, list, tuple, dict)
l_dict = {k:v for k,v in l_dict.iteritems() if isinstance(v, ok_types)} bad_keys = ("__builtins__",)
json.dump(l_dict, sys.stdout) g_dict = {k:v for k,v in g_dict.iteritems() if isinstance(v, ok_types) and k not in bad_keys}
json.dump(g_dict, sys.stdout)
""")) """))
stdin = json.dumps([code, globals_dict, locals_dict]) stdin = json.dumps([code, globals_dict])
jailed_code = "".join(the_code) jailed_code = "".join(the_code)
# Turn this on to see what's being executed. # Turn this on to see what's being executed.
...@@ -82,10 +83,10 @@ def safe_exec(code, globals_dict, locals_dict, assumed_imports=None, files=None, ...@@ -82,10 +83,10 @@ def safe_exec(code, globals_dict, locals_dict, assumed_imports=None, files=None,
res = jailpy.jailpy(jailed_code, stdin=stdin, files=files) res = jailpy.jailpy(jailed_code, stdin=stdin, files=files)
if res.status != 0: if res.status != 0:
raise Exception("Couldn't excecute jailed code: %s" % res.stderr) raise Exception("Couldn't excecute jailed code: %s" % res.stderr)
locals_dict.update(json.loads(res.stdout)) globals_dict.update(json.loads(res.stdout))
def not_safe_exec(code, globals_dict, locals_dict, assumed_imports=None, files=None, python_path=None): def not_safe_exec(code, globals_dict, assumed_imports=None, files=None, python_path=None):
"""Another implementation of `safe_exec`, but not safe. """Another implementation of `safe_exec`, but not safe.
This can be swapped in for debugging problems in sandboxed Python code. This can be swapped in for debugging problems in sandboxed Python code.
...@@ -111,7 +112,6 @@ def not_safe_exec(code, globals_dict, locals_dict, assumed_imports=None, files=N ...@@ -111,7 +112,6 @@ def not_safe_exec(code, globals_dict, locals_dict, assumed_imports=None, files=N
return json.loads(json.dumps(jd)) return json.loads(json.dumps(jd))
g_dict = straw(globals_dict) g_dict = straw(globals_dict)
l_dict = straw(locals_dict)
for name, modname in names_and_modules(assumed_imports or ()): for name, modname in names_and_modules(assumed_imports or ()):
g_dict[name] = lazymod.LazyModule(modname) g_dict[name] = lazymod.LazyModule(modname)
...@@ -127,11 +127,11 @@ def not_safe_exec(code, globals_dict, locals_dict, assumed_imports=None, files=N ...@@ -127,11 +127,11 @@ def not_safe_exec(code, globals_dict, locals_dict, assumed_imports=None, files=N
if python_path: if python_path:
sys.path.extend(python_path) sys.path.extend(python_path)
try: try:
exec code in g_dict, l_dict exec code in g_dict
finally: finally:
sys.path = original_path sys.path = original_path
locals_dict.update(straw(l_dict)) globals_dict.update(straw(g_dict))
# Running Python code in the sandbox makes it difficult to debug. # Running Python code in the sandbox makes it difficult to debug.
# Change 0 to 1 to run the code directly. # Change 0 to 1 to run the code directly.
......
...@@ -10,49 +10,49 @@ from codejail.safe_exec import safe_exec, not_safe_exec ...@@ -10,49 +10,49 @@ from codejail.safe_exec import safe_exec, not_safe_exec
class SafeExecTests(object): class SafeExecTests(object):
"""The tests for `safe_exec`, will be mixed into specific test classes below.""" """The tests for `safe_exec`, will be mixed into specific test classes below."""
def test_set_values(self): def test_set_values(self):
g, l = {}, {} g = {}
self.safe_exec("a = 17", g, l) self.safe_exec("a = 17", g)
self.assertEqual(l['a'], 17) self.assertEqual(g['a'], 17)
def test_assumed_imports(self): def test_assumed_imports(self):
g, l = {}, {} g = {}
# Using string without importing it is bad. # Using string without importing it is bad.
with self.assertRaises(Exception): with self.assertRaises(Exception):
self.safe_exec("a = string.ascii_lowercase[0]", g, l) self.safe_exec("a = string.ascii_lowercase[0]", g)
# Using string with an assumed import is fine. # Using string with an assumed import is fine.
self.safe_exec("a = string.ascii_lowercase[0]", g, l, assumed_imports=["string"]) self.safe_exec("a = string.ascii_lowercase[0]", g, assumed_imports=["string"])
self.assertEqual(l['a'], 'a') self.assertEqual(g['a'], 'a')
# Can also import with a shorthand. # Can also import with a shorthand.
self.safe_exec("a = op.join('x', 'y')", g, l, assumed_imports=[("op", "os.path")]) self.safe_exec("a = op.join('x', 'y')", g, assumed_imports=[("op", "os.path")])
self.assertEqual(l['a'][0], 'x') self.assertEqual(g['a'][0], 'x')
self.assertEqual(l['a'][-1], 'y') self.assertEqual(g['a'][-1], 'y')
def test_files_are_copied(self): def test_files_are_copied(self):
g, l = {}, {} g = {}
self.safe_exec( self.safe_exec(
"a = 'Look: ' + open('hello.txt').read()", g, l, "a = 'Look: ' + open('hello.txt').read()", g,
files=[os.path.dirname(__file__) + "/hello.txt"] files=[os.path.dirname(__file__) + "/hello.txt"]
) )
self.assertEqual(l['a'], 'Look: Hello there.\n') self.assertEqual(g['a'], 'Look: Hello there.\n')
def test_python_path(self): def test_python_path(self):
g, l = {}, {} g = {}
self.safe_exec( self.safe_exec(
"import module; a = module.const", g, l, "import module; a = module.const", g,
python_path=[os.path.dirname(__file__) + "/pylib"] python_path=[os.path.dirname(__file__) + "/pylib"]
) )
self.assertEqual(l['a'], 42) self.assertEqual(g['a'], 42)
def test_functions_calling_each_other(self): def test_functions_calling_each_other(self):
g, l = {}, {} g = {}
self.safe_exec(textwrap.dedent("""\ self.safe_exec(textwrap.dedent("""\
def f(): def f():
return 1723 return 1723
def g(): def g():
return f() return f()
x = g() x = g()
"""), g, l) """), g)
self.assertEqual(l['x'], 1723) self.assertEqual(g['x'], 1723)
class TestSafeExec(SafeExecTests, unittest.TestCase): class TestSafeExec(SafeExecTests, unittest.TestCase):
......
...@@ -19,11 +19,11 @@ def run_python(request): ...@@ -19,11 +19,11 @@ def run_python(request):
c['results'] = None c['results'] = None
if request.method == 'POST': if request.method == 'POST':
py_code = c['code'] = request.POST.get('code') py_code = c['code'] = request.POST.get('code')
g, l = {}, {} g = {}
try: try:
safe_exec(py_code, g, l) safe_exec(py_code, g)
except Exception as e: except Exception as e:
c['results'] = str(e) c['results'] = str(e)
else: else:
c['results'] = pprint.pformat(l) c['results'] = pprint.pformat(g)
return render_to_response("debug/run_python_form.html", c) return render_to_response("debug/run_python_form.html", c)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment