예제 #1
0
    def grade(self, grader_path, grader_config, student_response):
        tests = []
        errors = []
        correct = 0
        score = 0
        if grader_path.endswith('/correct'):
            correct = 1
            score = 1
            tests.append(('short', 'long', True, 'expected', 'actual'))
            tests.append(('short', '', True, 'expected', 'actual'))
        elif grader_path.endswith('/incorrect'):
            tests.append(('short', 'long', False, 'expected', 'actual'))
            errors.append('THIS IS AN ERROR')
            errors.append('\x00\xc3\x83\xc3\xb8\x02')

        try:
            from codejail import jail_code
        except ImportError:
            tests.append(("codejail", "codejail not installed", True, "", ""))
        else:
            if jail_code.is_configured("python"):
                tests.append(("codejail", "codejail configured", True, "", ""))
            else:
                tests.append(
                    ("codejail", "codejail not configured", True, "", ""))

        results = {
            'correct': correct,
            'score': score,
            'tests': tests,
            'errors': errors,
        }
        return results
예제 #2
0
def run(command, code, data=None, files=None, output_limit=None, **kwargs):
    assert jail_code.is_configured('python')
    files = files or []
    files.append((code, 'quiz.py'))
    if data is not None:
        data = utils.encode(data)
    runcode = inspect.getsource(stepicrun)
    argv = ['--command={0}'.format(command), '--code-path=quiz.py']
    for k, v in kwargs.items():
        argv.append('--{0}={1}'.format(k, v))

    result = jail_code_wrapper("python",
                               code=runcode,
                               argv=argv,
                               files=files,
                               stdin=data)
    if result.status != 0:
        stderr = result.stderr.decode()
        if result.time_limit_exceeded:
            stderr = "Time limit exceeded\n" + stderr
        msg = "{} failed:\n{}".format(command, stderr)
        raise JailedCodeFailed(msg)
    if output_limit and result.stdout and len(result.stdout) > output_limit:
        raise JailedCodeFailed("output is too large")
    try:
        decoded = utils.decode(result.stdout) if result.stdout else None
    except ValueError:
        raise JailedCodeFailed("failed to decode edyrun stdout:\n{}".format(
            result.stdout))

    return decoded
예제 #3
0
def run(command, code, data=None, files=None, output_limit=None, **kwargs):
    assert jail_code.is_configured("python")
    files = files or []
    files.append((code, "quiz.py"))
    if data is not None:
        data = utils.encode(data)
    runcode = inspect.getsource(stepicrun)
    argv = ["--command={0}".format(command), "--code-path=quiz.py"]
    for k, v in kwargs.items():
        argv.append("--{0}={1}".format(k, v))

    result = jail_code_wrapper("python", code=runcode, argv=argv, files=files, stdin=data)
    if result.status != 0:
        stderr = result.stderr.decode()
        if result.time_limit_exceeded:
            stderr = "Time limit exceeded\n" + stderr
        msg = "{} failed:\n{}".format(command, stderr)
        raise JailedCodeFailed(msg)
    if output_limit and result.stdout and len(result.stdout) > output_limit:
        raise JailedCodeFailed("output is too large")
    try:
        decoded = utils.decode(result.stdout) if result.stdout else None
    except ValueError:
        raise JailedCodeFailed("failed to decode edyrun stdout:\n{}".format(result.stdout))

    return decoded
예제 #4
0
    def test_cant_do_something_forbidden(self):
        # Can't test for forbiddenness if CodeJail isn't configured for python.
        if not is_configured("python"):
            pytest.skip()

        g = {}
        with self.assertRaises(SafeExecException) as cm:
            safe_exec("import os; files = os.listdir('/')", g)
        assert "OSError" in text_type(cm.exception)
        assert "Permission denied" in text_type(cm.exception)
예제 #5
0
    def test_cant_do_something_forbidden(self):
        # Can't test for forbiddenness if CodeJail isn't configured for python.
        if not is_configured("python"):
            raise SkipTest

        g = {}
        with self.assertRaises(SafeExecException) as cm:
            safe_exec("import os; files = os.listdir('/')", g)
        self.assertIn("OSError", cm.exception.message)
        self.assertIn("Permission denied", cm.exception.message)
예제 #6
0
    def test_cant_do_something_forbidden(self):
        # Can't test for forbiddenness if CodeJail isn't configured for python.
        if not is_configured("python"):
            raise SkipTest

        g = {}
        with self.assertRaises(SafeExecException) as cm:
            safe_exec("import os; files = os.listdir('/')", g)
        self.assertIn("OSError", cm.exception.message)
        self.assertIn("Permission denied", cm.exception.message)
예제 #7
0
    def test_cant_do_something_forbidden(self):
        # Can't test for forbiddenness if CodeJail isn't configured for python.
        if not is_configured("python"):
            pytest.skip()

        g = {}
        with self.assertRaises(SafeExecException) as cm:
            safe_exec("import os; files = os.listdir('/')", g)
        assert "OSError" in text_type(cm.exception)
        assert "Permission denied" in text_type(cm.exception)
예제 #8
0
    def test_cant_do_something_forbidden(self):
        '''
        Demonstrates that running unsafe code inside the code jail
        throws SafeExecException, protecting the calling process.
        '''
        # Can't test for forbiddenness if CodeJail isn't configured for python.
        if not jail_code.is_configured("python"):
            pytest.skip()

        g = {}
        with pytest.raises(SafeExecException) as cm:
            safe_exec('import sys; sys.exit(1)', g)
        assert "SystemExit" not in text_type(cm)
        assert "Couldn't execute jailed code" in text_type(cm)
예제 #9
0
    g_dict = json_safe(globals_dict)

    with temp_directory() as tmpdir:
        with change_directory(tmpdir):
            # Copy the files here.
            for filename in files or ():
                dest = os.path.join(tmpdir, os.path.basename(filename))
                shutil.copyfile(filename, dest)

            original_path = sys.path
            if python_path:
                sys.path.extend(python_path)
            try:
                exec code in g_dict
            except Exception as e:
                # Wrap the exception in a SafeExecException, but we don't
                # try here to include the traceback, since this is just a
                # substitute implementation.
                msg = "{0.__class__.__name__}: {0!s}".format(e)
                raise SafeExecException(msg)
            finally:
                sys.path = original_path

    globals_dict.update(json_safe(g_dict))


# Running Python code in the sandbox makes it difficult to debug.
NO_SAFE_PYTHON = not jail_code.is_configured("python")
if ALWAYS_BE_UNSAFE or NO_SAFE_PYTHON:   # pragma: no cover
    safe_exec = not_safe_exec
예제 #10
0
 def setUp(self):
     super(JailCodeHelpers, self).setUp()
     if not is_configured("python"):
         raise SkipTest
예제 #11
0
def jail_code(command, code=None, files=None, extra_files=None, argv=None,
              stdin=None, slug=None):
    """
    Run code in a jailed subprocess.

    `command` is an abstract command ("python", "node", ...) that must have
    been configured using `configure`.

    `code` is a string containing the code to run.  If no code is supplied,
    then the code to run must be in one of the `files` copied, and must be
    named in the `argv` list.

    `files` is a list of file paths, they are all copied to the jailed
    directory.  Note that no check is made here that the files don't contain
    sensitive information.  The caller must somehow determine whether to allow
    the code access to the files.  Symlinks will be copied as symlinks.  If the
    linked-to file is not accessible to the sandbox, the symlink will be
    unreadable as well.

    `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.

    `argv` is the command-line arguments to supply.

    `stdin` is a string, the data to provide as the stdin for the process.

    `slug` is an arbitrary string, a description that's meaningful to the
    caller, that will be used in log messages.

    Return an object with:

        .stdout: stdout of the program, a string
        .stderr: stderr of the program, a string
        .status: exit status of the process: an int, 0 for success

    """
    if not is_configured(command):
        raise Exception("jail_code needs to be configured for %r" % command)

    # We make a temp directory to serve as the home of the sandboxed code.
    # It has a writable "tmp" directory within it for temp files.

    with temp_directory() as homedir:

        # Make directory readable by other users ('sandbox' user needs to be
        # able to read it).
        os.chmod(homedir, 0775)

        # Make a subdir to use for temp files, world-writable so that the
        # sandbox user can write to it.
        tmptmp = os.path.join(homedir, "tmp")
        os.mkdir(tmptmp)
        os.chmod(tmptmp, 0777)

        argv = argv or []

        # All the supporting files are copied into our directory.
        for filename in files or ():
            dest = os.path.join(homedir, os.path.basename(filename))
            if os.path.islink(filename):
                os.symlink(os.readlink(filename), dest)
            elif os.path.isfile(filename):
                shutil.copy(filename, homedir)
            else:
                shutil.copytree(filename, dest, symlinks=True)

        # Create the main file.
        if code:
            with open(os.path.join(homedir, "jailed_code"), "wb") as jailed:
                jailed.write(code)

            argv = ["jailed_code"] + argv

        # Create extra files requested by the caller:
        for name, content in extra_files or ():
            with open(os.path.join(homedir, name), "wb") as extra:
                extra.write(content)

        cmd = []

        # Build the command to run.
        user = COMMANDS[command]['user']
        if user:
            # Run as the specified user
            cmd.extend(['sudo', '-u', user])

        # Point TMPDIR at our temp directory.
        cmd.extend(['TMPDIR=tmp'])
        # Start with the command line dictated by "python" or whatever.
        cmd.extend(COMMANDS[command]['cmdline_start'])
        # Add the code-specific command line pieces.
        cmd.extend(argv)

        # Run the subprocess.
        subproc = subprocess.Popen(
            cmd, preexec_fn=set_process_limits, cwd=homedir, env={},
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
        )

        if slug:
            log.info("Executing jailed code %s in %s, with PID %s", slug, homedir, subproc.pid)

        # Start the time killer thread.
        realtime = LIMITS["REALTIME"]
        if realtime:
            killer = ProcessKillerThread(subproc, limit=realtime)
            killer.start()

        result = JailResult()
        result.stdout, result.stderr = subproc.communicate(stdin)
        result.status = subproc.returncode

    return result
예제 #12
0
                exec code in g_dict
            except Exception as e:
                # Wrap the exception in a SafeExecException, but we don't
                # try here to include the traceback, since this is just a
                # substitute implementation.
                msg = "{0.__class__.__name__}: {0!s}".format(e)
                raise SafeExecException(msg)
            finally:
                sys.path = original_path

    globals_dict.update(json_safe(g_dict))


# If the developer wants us to be unsafe (ALWAYS_BE_UNSAFE), or if there isn't
# a configured jail for Python, then we'll be UNSAFE.
UNSAFE = ALWAYS_BE_UNSAFE or not jail_code.is_configured("python")

if UNSAFE:   # pragma: no cover
    # Make safe_exec actually call not_safe_exec, but log that we're doing so.

    def safe_exec(*args, **kwargs):                 # pylint: disable=E0102
        """An actually-unsafe safe_exec, that warns it's being used."""

        # Because it would be bad if this function were used in production,
        # let's log a warning when it is used.  Developers can live with
        # one more log line.
        slug = kwargs.get('slug', None)
        log.warning("Using codejail/safe_exec.py:not_safe_exec for %s", slug)

        return not_safe_exec(*args, **kwargs)
예제 #13
0
 def setUp(self):
     super().setUp()
     if not is_configured("python"):
         raise SkipTest
예제 #14
0
                exec code in g_dict
            except Exception as e:
                # Wrap the exception in a SafeExecException, but we don't
                # try here to include the traceback, since this is just a
                # substitute implementation.
                msg = "{0.__class__.__name__}: {0!s}".format(e)
                raise SafeExecException(msg)
            finally:
                sys.path = original_path

    globals_dict.update(json_safe(g_dict))


# If the developer wants us to be unsafe (ALWAYS_BE_UNSAFE), or if there isn't
# a configured jail for Python, then we'll be UNSAFE.
UNSAFE = ALWAYS_BE_UNSAFE or not jail_code.is_configured("python")

if UNSAFE:   # pragma: no cover
    # Make safe_exec actually call not_safe_exec, but log that we're doing so.

    def safe_exec(*args, **kwargs):                 # pylint: disable=E0102
        """An actually-unsafe safe_exec, that warns it's being used."""

        # Because it would be bad if this function were used in production,
        # let's log a warning when it is used.  Developers can live with
        # one more log line.
        slug = kwargs.get('slug', None)
        log.warning("Using codejail/safe_exec.py:not_safe_exec for %s", slug)

        return not_safe_exec(*args, **kwargs)
예제 #15
0
 def setUp(self):
     super(JailCodeHelpers, self).setUp()
     if not is_configured("python"):
         raise SkipTest
예제 #16
0
 def assert_configured(cmd):
     self.assertTrue(jail_code.is_configured(cmd),
                     cmd + " is not configured")
예제 #17
0
    g_dict = json_safe(globals_dict)

    with temp_directory() as tmpdir:
        with change_directory(tmpdir):
            # Copy the files here.
            for filename in files or ():
                dest = os.path.join(tmpdir, os.path.basename(filename))
                shutil.copyfile(filename, dest)

            original_path = sys.path
            if python_path:
                sys.path.extend(python_path)
            try:
                exec code in g_dict
            except Exception as e:
                # Wrap the exception in a SafeExecException, but we don't
                # try here to include the traceback, since this is just a
                # substitute implementation.
                msg = "{0.__class__.__name__}: {0!s}".format(e)
                raise SafeExecException(msg)
            finally:
                sys.path = original_path

    globals_dict.update(json_safe(g_dict))


# Running Python code in the sandbox makes it difficult to debug.
NO_SAFE_PYTHON = not jail_code.is_configured("python")
if ALWAYS_BE_UNSAFE or NO_SAFE_PYTHON:  # pragma: no cover
    safe_exec = not_safe_exec
예제 #18
0
 def assert_configured(cmd):
     self.assertTrue(jail_code.is_configured(cmd), cmd + " is not configured")
예제 #19
0
def jail_code(command,
              code=None,
              files=None,
              extra_files=None,
              argv=None,
              stdin=None,
              slug=None):
    """
    Run code in a jailed subprocess.

    `command` is an abstract command ("python", "node", ...) that must have
    been configured using `configure`.

    `code` is a string containing the code to run.  If no code is supplied,
    then the code to run must be in one of the `files` copied, and must be
    named in the `argv` list.

    `files` is a list of file paths, they are all copied to the jailed
    directory.  Note that no check is made here that the files don't contain
    sensitive information.  The caller must somehow determine whether to allow
    the code access to the files.  Symlinks will be copied as symlinks.  If the
    linked-to file is not accessible to the sandbox, the symlink will be
    unreadable as well.

    `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.

    `argv` is the command-line arguments to supply.

    `stdin` is a string, the data to provide as the stdin for the process.

    `slug` is an arbitrary string, a description that's meaningful to the
    caller, that will be used in log messages.

    Return an object with:

        .stdout: stdout of the program, a string
        .stderr: stderr of the program, a string
        .status: exit status of the process: an int, 0 for success

    """
    if not is_configured(command):
        raise Exception("jail_code needs to be configured for %r" % command)

    # We make a temp directory to serve as the home of the sandboxed code.
    # It has a writable "tmp" directory within it for temp files.

    with temp_directory() as homedir:

        # Make directory readable by other users ('sandbox' user needs to be
        # able to read it).
        os.chmod(homedir, 0775)

        # Make a subdir to use for temp files, world-writable so that the
        # sandbox user can write to it.
        tmptmp = os.path.join(homedir, "tmp")
        os.mkdir(tmptmp)
        os.chmod(tmptmp, 0777)

        argv = argv or []

        # All the supporting files are copied into our directory.
        for filename in files or ():
            dest = os.path.join(homedir, os.path.basename(filename))
            if os.path.islink(filename):
                os.symlink(os.readlink(filename), dest)
            elif os.path.isfile(filename):
                shutil.copy(filename, homedir)
            else:
                shutil.copytree(filename, dest, symlinks=True)

        # Create the main file.
        if code:
            with open(os.path.join(homedir, "jailed_code"), "wb") as jailed:
                jailed.write(code)

            argv = ["jailed_code"] + argv

        # Create extra files requested by the caller:
        for name, content in extra_files or ():
            with open(os.path.join(homedir, name), "wb") as extra:
                extra.write(content)

        cmd = []

        # Build the command to run.
        user = COMMANDS[command]['user']
        if user:
            # Run as the specified user
            cmd.extend(['sudo', '-u', user])

        # Point TMPDIR at our temp directory.
        cmd.extend(['TMPDIR=tmp'])
        # Start with the command line dictated by "python" or whatever.
        cmd.extend(COMMANDS[command]['cmdline_start'])
        # Add the code-specific command line pieces.
        cmd.extend(argv)

        # Run the subprocess.
        subproc = subprocess.Popen(
            cmd,
            preexec_fn=set_process_limits,
            cwd=homedir,
            env={},
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )

        if slug:
            log.info("Executing jailed code %s in %s, with PID %s", slug,
                     homedir, subproc.pid)

        # Start the time killer thread.
        realtime = LIMITS["REALTIME"]
        if realtime:
            killer = ProcessKillerThread(subproc, limit=realtime)
            killer.start()

        result = JailResult()
        result.stdout, result.stderr = subproc.communicate(stdin)
        result.status = subproc.returncode

    return result