Commit 0c490dd4 by Ned Batchelder

Merge pull request #25 from edx/ned/importable-zip-files

Importable zip files
parents 480be9d4 671eeb26
...@@ -36,7 +36,8 @@ class SafeExecException(Exception): ...@@ -36,7 +36,8 @@ class SafeExecException(Exception):
pass pass
def safe_exec(code, globals_dict, files=None, python_path=None, slug=None): def safe_exec(code, globals_dict, files=None, python_path=None, slug=None,
extra_files=None):
""" """
Execute code as "exec" does, but safely. Execute code as "exec" does, but safely.
...@@ -49,13 +50,19 @@ def safe_exec(code, globals_dict, files=None, python_path=None, slug=None): ...@@ -49,13 +50,19 @@ def safe_exec(code, globals_dict, files=None, python_path=None, slug=None):
determine whether the file is appropriate or safe to copy. The caller must determine whether the file is appropriate or safe to copy. The caller must
determine which files to provide to the code. determine which files to provide to the code.
`python_path` is a list of directory paths. They will be copied just as `python_path` is a list of directory or file paths. These names will be
`files` are, but will also be added to `sys.path` so that modules there can added to `sys.path` so that modules they contain can be imported. Only
be imported. directories and zip files are supported. If the name is not provided in
`extras_files`, it will be copied just as if it had been listed in `files`.
`slug` is an arbitrary string, a description that's meaningful to the `slug` is an arbitrary string, a description that's meaningful to the
caller, that will be used in log messages. caller, that will be used in log messages.
`extra_files` is a list of pairs, each pair is a filename and a bytestring
of contents to write into that file. These files will be created in the
temp directory and cleaned up automatically. No subdirectories are
supported in the filename.
Returns None. Changes made by `code` are visible in `globals_dict`. If Returns None. Changes made by `code` are visible in `globals_dict`. If
the code raises an exception, this function will raise `SafeExecException` the code raises an exception, this function will raise `SafeExecException`
with the stderr of the sandbox process, which usually includes the original with the stderr of the sandbox process, which usually includes the original
...@@ -63,7 +70,12 @@ def safe_exec(code, globals_dict, files=None, python_path=None, slug=None): ...@@ -63,7 +70,12 @@ def safe_exec(code, globals_dict, files=None, python_path=None, slug=None):
""" """
the_code = [] the_code = []
files = list(files or ()) files = list(files or ())
extra_files = extra_files or ()
python_path = python_path or ()
extra_names = set(name for name, contents in extra_files)
the_code.append(textwrap.dedent( the_code.append(textwrap.dedent(
""" """
...@@ -89,10 +101,11 @@ def safe_exec(code, globals_dict, files=None, python_path=None, slug=None): ...@@ -89,10 +101,11 @@ def safe_exec(code, globals_dict, files=None, python_path=None, slug=None):
code, g_dict = json.load(sys.stdin) code, g_dict = json.load(sys.stdin)
""")) """))
for pydir in python_path or (): for pydir in python_path:
pybase = os.path.basename(pydir) pybase = os.path.basename(pydir)
the_code.append("sys.path.append(%r)\n" % pybase) the_code.append("sys.path.append(%r)\n" % pybase)
files.append(pydir) if pybase not in extra_names:
files.append(pydir)
the_code.append(textwrap.dedent( the_code.append(textwrap.dedent(
# Execute the sandboxed code. # Execute the sandboxed code.
...@@ -135,6 +148,7 @@ def safe_exec(code, globals_dict, files=None, python_path=None, slug=None): ...@@ -135,6 +148,7 @@ def safe_exec(code, globals_dict, files=None, python_path=None, slug=None):
res = jail_code.jail_code( res = jail_code.jail_code(
"python", code=jailed_code, stdin=stdin, files=files, slug=slug, "python", code=jailed_code, stdin=stdin, files=files, slug=slug,
extra_files=extra_files,
) )
if res.status != 0: if res.status != 0:
raise SafeExecException( raise SafeExecException(
...@@ -175,7 +189,8 @@ def json_safe(d): ...@@ -175,7 +189,8 @@ def json_safe(d):
return json.loads(json.dumps(jd)) return json.loads(json.dumps(jd))
def not_safe_exec(code, globals_dict, files=None, python_path=None, slug=None): def not_safe_exec(code, globals_dict, files=None, python_path=None, slug=None,
extra_files=None):
""" """
Another implementation of `safe_exec`, but not safe. Another implementation of `safe_exec`, but not safe.
...@@ -193,6 +208,10 @@ def not_safe_exec(code, globals_dict, files=None, python_path=None, slug=None): ...@@ -193,6 +208,10 @@ def not_safe_exec(code, globals_dict, files=None, python_path=None, slug=None):
for filename in files or (): for filename in files or ():
dest = os.path.join(tmpdir, os.path.basename(filename)) dest = os.path.join(tmpdir, os.path.basename(filename))
shutil.copyfile(filename, dest) shutil.copyfile(filename, dest)
for filename, contents in extra_files or ():
dest = os.path.join(tmpdir, filename)
with open(dest, "w") as f:
f.write(contents)
original_path = sys.path original_path = sys.path
if python_path: if python_path:
......
"""Test safe_exec.py""" """Test safe_exec.py"""
from cStringIO import StringIO
import os.path import os.path
import textwrap import textwrap
import unittest import unittest
import zipfile
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
from codejail import safe_exec from codejail import safe_exec
...@@ -78,6 +81,45 @@ class SafeExecTests(unittest.TestCase): ...@@ -78,6 +81,45 @@ class SafeExecTests(unittest.TestCase):
msg = str(what_happened.exception) msg = str(what_happened.exception)
self.assertIn("ValueError: That's not how you pour soup!", msg) self.assertIn("ValueError: That's not how you pour soup!", msg)
def test_extra_files(self):
globs = {}
extras = [
("extra.txt", "I'm extra!\n"),
("also.dat", "\x01\xff\x02\xfe"),
]
self.safe_exec(textwrap.dedent("""\
with open("extra.txt") as f:
extra = f.read()
with open("also.dat") as f:
also = f.read().encode("hex")
"""), globs, extra_files=extras)
self.assertEqual(globs['extra'], "I'm extra!\n")
self.assertEqual(globs['also'], "01ff02fe")
def test_extra_files_as_pythonpath_zipfile(self):
zipstring = StringIO()
zipf = zipfile.ZipFile(zipstring, "w")
zipf.writestr("zipped_module1.py", textwrap.dedent("""\
def func1(x):
return 2*x + 3
"""))
zipf.writestr("zipped_module2.py", textwrap.dedent("""\
def func2(s):
return "X" + s + s + "X"
"""))
zipf.close()
globs = {}
extras = [("code.zip", zipstring.getvalue())]
self.safe_exec(textwrap.dedent("""\
import zipped_module1 as zm1
import zipped_module2 as zm2
a = zm1.func1(10)
b = zm2.func2("hello")
"""), globs, python_path=["code.zip"], extra_files=extras)
self.assertEqual(globs['a'], 23)
self.assertEqual(globs['b'], "XhellohelloX")
class TestSafeExec(SafeExecTests, unittest.TestCase): class TestSafeExec(SafeExecTests, unittest.TestCase):
"""Run SafeExecTests, with the real safe_exec.""" """Run SafeExecTests, with the real safe_exec."""
......
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