diff options
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | snekbox/api/resources/eval.py | 28 | ||||
-rw-r--r-- | snekbox/nsjail.py | 18 | ||||
-rw-r--r-- | tests/test_integration.py | 32 |
4 files changed, 47 insertions, 35 deletions
@@ -52,9 +52,9 @@ The above command will make the API accessible on the host via `http://localhost ### Python multi-version support -By default, the binary that runs within nsjail is the binary specified by `DEFAULT_BINARY_PATH` at the top of [`nsjail.py`]. This can be overridden by specifying `binary_path` in the request body of calls to `POST /eval` or by setting the `binary_path` kwarg if calling `NSJail.python3()` directly. +By default, the executable that runs within nsjail is defined by `DEFAULT_EXECUTABLE_PATH` at the top of [`nsjail.py`]. This can be overridden by specifying `executable_path` in the request body of calls to `POST /eval` or by setting the `executable_path` kwarg if calling `NSJail.python3()` directly. -Any binary that exists within the container is a valid value for `binary_path`. The main use case of this feature is currently to specify the version of Python to use. +Any executable that exists within the container is a valid value for `executable_path`. The main use case of this feature is currently to specify the version of Python to use. Python versions currently available can be found in the [`Dockerfile`] by looking for build stages that match `builder-py-*`. These binaries are then copied into the `base` build stage further down. diff --git a/snekbox/api/resources/eval.py b/snekbox/api/resources/eval.py index 3172f60..b53899a 100644 --- a/snekbox/api/resources/eval.py +++ b/snekbox/api/resources/eval.py @@ -6,7 +6,7 @@ from pathlib import Path import falcon from falcon.media.validators.jsonschema import validate -from snekbox.nsjail import DEFAULT_BINARY_PATH, NsJail +from snekbox.nsjail import DEFAULT_EXECUTABLE_PATH, NsJail from snekbox.snekio import FileAttachment, ParsingError __all__ = ("EvalResource",) @@ -44,7 +44,7 @@ class EvalResource: "required": ["path"], }, }, - "binary_path": {"type": "string"}, + "executable_path": {"type": "string"}, }, "anyOf": [ {"required": ["input"]}, @@ -125,24 +125,24 @@ class EvalResource: body.setdefault("args", ["-c"]) body["args"].append(body["input"]) - binary_path = body.get("binary_path") - if not binary_path: - binary_path = DEFAULT_BINARY_PATH + executable_path = body.get("executable_path") + if not executable_path: + executable_path = DEFAULT_EXECUTABLE_PATH else: - binary_path = Path(binary_path) - if not binary_path.exists(): - raise falcon.HTTPBadRequest(title="binary_path does not exist") - if not binary_path.is_file(): - raise falcon.HTTPBadRequest(title="binary_path is not a file") - if not binary_path.stat().st_mode & 0o100 == 0o100: - raise falcon.HTTPBadRequest(title="binary_path is not executable") - binary_path = binary_path.resolve().as_posix() + executable_path = Path(executable_path) + if not executable_path.exists(): + raise falcon.HTTPBadRequest(title="executable_path does not exist") + if not executable_path.is_file(): + raise falcon.HTTPBadRequest(title="executable_path is not a file") + if not executable_path.stat().st_mode & 0o100 == 0o100: + raise falcon.HTTPBadRequest(title="executable_path is not executable") + executable_path = executable_path.resolve().as_posix() try: result = self.nsjail.python3( py_args=body["args"], files=[FileAttachment.from_dict(file) for file in body.get("files", [])], - binary_path=binary_path, + executable_path=executable_path, ) except ParsingError as e: raise falcon.HTTPBadRequest(title="Request file is invalid", description=str(e)) diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py index fe95f80..adbf69e 100644 --- a/snekbox/nsjail.py +++ b/snekbox/nsjail.py @@ -26,7 +26,7 @@ log = logging.getLogger(__name__) LOG_PATTERN = re.compile( r"\[(?P<level>(I)|[DWEF])\]\[.+?\](?(2)|(?P<func>\[\d+\] .+?:\d+ )) ?(?P<msg>.+)" ) -DEFAULT_BINARY_PATH = "/snekbin/python/default/bin/python" +DEFAULT_EXECUTABLE_PATH = "/snekbin/python/default/bin/python" class NsJail: @@ -174,7 +174,7 @@ class NsJail: nsjail_args: Iterable[str], log_path: str, fs_home: str, - binary_path: str, + executable_path: str, ) -> Sequence[str]: if self.cgroup_version == 2: nsjail_args = ("--use_cgroupv2", *nsjail_args) @@ -203,7 +203,7 @@ class NsJail: log_path, *nsjail_args, "--", - binary_path, + executable_path, *iter_lstrip(py_args), ] @@ -262,7 +262,7 @@ class NsJail: py_args: Iterable[str], files: Iterable[FileAttachment] = (), nsjail_args: Iterable[str] = (), - binary_path: Path = DEFAULT_BINARY_PATH, + executable_path: Path = DEFAULT_EXECUTABLE_PATH, ) -> EvalResult: """ Execute Python 3 code in an isolated environment and return the completed process. @@ -271,14 +271,20 @@ class NsJail: py_args: Arguments to pass to Python. files: FileAttachments to write to the sandbox prior to running Python. nsjail_args: Overrides for the NsJail configuration. - binary_path: The path to the binary to execute under. + executable_path: The path to the executable to run within nsjail. """ with NamedTemporaryFile() as nsj_log, MemFS( instance_size=self.memfs_instance_size, home=self.memfs_home, output=self.memfs_output, ) as fs: - args = self._build_args(py_args, nsjail_args, nsj_log.name, str(fs.home), binary_path) + args = self._build_args( + py_args, + nsjail_args, + nsj_log.name, + str(fs.home), + executable_path, + ) try: files_written = self._write_files(fs.home, files) diff --git a/tests/test_integration.py b/tests/test_integration.py index e173dd3..0d8f700 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -52,8 +52,8 @@ class IntegrationTests(unittest.TestCase): self.assertTrue(all(status == 200 for status in statuses)) self.assertTrue(all(json.loads(response)["returncode"] == 0 for response in responses)) - def test_multi_binary_support(self): - """Test eval requests with different binary paths set.""" + def test_alternate_executable_support(self): + """Test eval requests with different executable paths set.""" with run_gunicorn(): get_python_version_body = { "input": "import sys; print('.'.join(map(str, sys.version_info[:2])))" @@ -62,17 +62,19 @@ class IntegrationTests(unittest.TestCase): ( get_python_version_body, "3.12\n", - "test default binary is used when binary_path not specified", + "test default executable is used when executable_path not specified", ), ( - get_python_version_body | {"binary_path": "/snekbin/python/3.12/bin/python"}, + get_python_version_body + | {"executable_path": "/snekbin/python/3.12/bin/python"}, "3.12\n", - "test default binary is used when explicitly set", + "test default executable is used when explicitly set", ), ( - get_python_version_body | {"binary_path": "/snekbin/python/3.13/bin/python"}, + get_python_version_body + | {"executable_path": "/snekbin/python/3.13/bin/python"}, "3.13\n", - "test alternative binary is used when set", + "test alternative executable is used when set", ), ] for body, expected, msg in cases: @@ -81,21 +83,25 @@ class IntegrationTests(unittest.TestCase): self.assertEqual(status, 200) self.assertEqual(json.loads(response)["stdout"], expected) - def invalid_binary_paths(self): - """Test that passing invalid binary paths result in no code execution.""" + def invalid_executable_paths(self): + """Test that passing invalid executable paths result in no code execution.""" with run_gunicorn(): cases = [ - ("/abc/def", "test non-existent files are not run", "binary_path does not exist"), - ("/snekbin", "test directories are not run", "binary_path is not a file"), + ( + "/abc/def", + "test non-existent files are not run", + "executable_path does not exist", + ), + ("/snekbin", "test directories are not run", "executable_path is not a file"), ( "/etc/hostname", "test non-executable files are not run", - "binary_path is not executable", + "executable_path is not executable", ), ] for path, msg, expected in cases: with self.subTest(msg=msg, path=path, expected=expected): - body = {"args": ["-c", "echo", "hi"], "binary_path": path} + body = {"args": ["-c", "echo", "hi"], "executable_path": path} response, status = snekbox_request(body) self.assertEqual(status, 400) self.assertEqual(json.loads(response)["stdout"], expected) |