Skip to content

Commit

Permalink
executors: implement compiler sandboxing
Browse files Browse the repository at this point in the history
Current limitations:
* doesn't sandbox renames
* doesn't sandbox utimensat
* doesn't sandbox various f* syscalls, e.g. fchmod

Nonetheless, it plugs the obvious holes like allowing #include on any
path readable by the judge. The functionality that remains un-sanboxed
would likely require a compiler exploit to abuse.
  • Loading branch information
Xyene committed Sep 18, 2021
1 parent 6216c6f commit 4868122
Show file tree
Hide file tree
Showing 23 changed files with 233 additions and 109 deletions.
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ignore =
C818 # trailing comma on bare tuple prohibited
per-file-ignores =
# F403: import *, F405: name comes from import *
./dmoj/executors/compiled_executor.py:F403,F405
./dmoj/cptbox/isolate.py:F403,F405
./dmoj/cptbox/tracer.py:F403,F405
# F821: undefined name, flake8 incorrectly thinks name is deleted
Expand Down
7 changes: 6 additions & 1 deletion dmoj/cptbox/filesystem_policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def _add_rule(self, rule: FilesystemAccessRule) -> None:
if rule.path == '/':
return self._finalize_root_rule(rule)

path = rule.path
path = os.path.expanduser(rule.path)
assert os.path.abspath(path) == path, 'FilesystemAccessRule must specify a normalized, absolute path to rule'
*directory_path, final_component = path.split('/')[1:]

Expand All @@ -69,6 +69,11 @@ def _add_rule(self, rule: FilesystemAccessRule) -> None:

self._finalize_rule(node, final_component, rule)

# Add symlink targets too
real_path = os.path.realpath(path)
if real_path != path:
self._add_rule(type(rule)(real_path))

def _assert_rule_type(self, rule: FilesystemAccessRule) -> None:
if os.path.exists(rule.path):
is_dir = os.path.isdir(rule.path)
Expand Down
32 changes: 19 additions & 13 deletions dmoj/cptbox/isolate.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ def __init__(self, read_fs, write_fs=None, writable=(1, 2)):
sys_lstat: self.check_file_access('lstat', 0),
sys_lstat64: self.check_file_access('lstat64', 0),
sys_fstatat: self.check_file_access_at('fstatat'),
sys_mkdir: ACCESS_EPERM,
sys_unlink: ACCESS_EPERM,
sys_tgkill: self.do_kill,
sys_kill: self.do_kill,
sys_prctl: self.do_prctl,
Expand All @@ -82,6 +80,7 @@ def __init__(self, read_fs, write_fs=None, writable=(1, 2)):
sys_sched_getscheduler: ALLOW,
sys_sched_get_priority_min: ALLOW,
sys_sched_get_priority_max: ALLOW,
sys_sched_setscheduler: ALLOW,
sys_timerfd_create: ALLOW,
sys_timer_create: ALLOW,
sys_timer_settime: ALLOW,
Expand Down Expand Up @@ -158,6 +157,7 @@ def __init__(self, read_fs, write_fs=None, writable=(1, 2)):
if 'freebsd' in sys.platform:
self.update(
{
sys_mkdir: ACCESS_EPERM,
sys_obreak: ALLOW,
sys_sysarch: ALLOW,
sys_sysctl: ALLOW, # TODO: More strict?
Expand Down Expand Up @@ -205,7 +205,9 @@ def is_write_flags(self, open_flags: int) -> bool:

return False

def check_file_access(self, syscall, argument, is_open=False) -> HandlerCallback:
def check_file_access(self, syscall, argument, is_write=None, is_open=False) -> HandlerCallback:
assert is_write is None or not is_open

def check(debugger: Debugger) -> bool:
file_ptr = getattr(debugger, 'uarg%d' % argument)
try:
Expand All @@ -217,7 +219,7 @@ def check(debugger: Debugger) -> bool:
log.warning('Denied access via syscall %s to path with invalid unicode: %r', syscall, e.object)
return ACCESS_ENOENT(debugger)

file, error = self._file_access_check(file, debugger, is_open)
file, error = self._file_access_check(file, debugger, is_open, is_write=is_write)
if not error:
return True

Expand All @@ -226,18 +228,21 @@ def check(debugger: Debugger) -> bool:

return check

def check_file_access_at(self, syscall, is_open=False) -> HandlerCallback:
def check_file_access_at(self, syscall, argument=1, is_open=False, is_write=None) -> HandlerCallback:
def check(debugger: Debugger) -> bool:
file_ptr = getattr(debugger, 'uarg%d' % argument)
try:
file = debugger.readstr(debugger.uarg1)
file = debugger.readstr(file_ptr)
except MaxLengthExceeded as e:
log.warning('Denied access via syscall %s to overly long path: %r', syscall, e.args[0])
return ACCESS_ENAMETOOLONG(debugger)
except UnicodeDecodeError as e:
log.warning('Denied access via syscall %s to path with invalid unicode: %r', syscall, e.object)
return ACCESS_ENOENT(debugger)

file, error = self._file_access_check(file, debugger, is_open, dirfd=debugger.arg0, flag_reg=2)
file, error = self._file_access_check(
file, debugger, is_open, is_write=is_write, dirfd=debugger.arg0, flag_reg=2
)
if not error:
return True

Expand All @@ -247,7 +252,7 @@ def check(debugger: Debugger) -> bool:
return check

def _file_access_check(
self, rel_file, debugger, is_open, flag_reg=1, dirfd=AT_FDCWD
self, rel_file, debugger, is_open, is_write=None, flag_reg=1, dirfd=AT_FDCWD
) -> Tuple[str, Optional[ErrnoHandlerCallback]]:
# Either process called open(NULL, ...), or we failed to read the path
# in cptbox. Either way this call should not be allowed; if the path
Expand All @@ -257,7 +262,8 @@ def _file_access_check(
if rel_file is None:
return '(nil)', ACCESS_EFAULT

is_write = is_open and self.is_write_flags(getattr(debugger, 'uarg%d' % flag_reg))
if is_write is None and is_open:
is_write = self.is_write_flags(getattr(debugger, 'uarg%d' % flag_reg))
fs_jail = self.write_fs_jail if is_write else self.read_fs_jail

try:
Expand All @@ -273,11 +279,11 @@ def _file_access_check(
# the same file, and check the accessibility of both.
#
# This works, except when the child process uses /proc/self, which refers to something else in this process.
# Therefore, we "project" it by changing it to /proc/[pid] for computing the realpath and doing the samefile
# Therefore, we "project" it by changing it to /proc/[tid] for computing the realpath and doing the samefile
# check. However, we still keep it as /proc/self when checking access rules.
projected = normalized = '/' + os.path.normpath(file).lstrip('/')
if normalized.startswith('/proc/self'):
file = os.path.join(f'/proc/{debugger.pid}', os.path.relpath(file, '/proc/self'))
file = os.path.join(f'/proc/{debugger.tid}', os.path.relpath(file, '/proc/self'))
projected = '/' + os.path.normpath(file).lstrip('/')
real = os.path.realpath(file)

Expand All @@ -299,14 +305,14 @@ def _file_access_check(
return normalized, ACCESS_EACCES

if normalized != real:
proc_dir = f'/proc/{debugger.pid}'
proc_dir = f'/proc/{debugger.tid}'
if real.startswith(proc_dir):
real = os.path.join('/proc/self', os.path.relpath(real, proc_dir))

if not fs_jail.check(real):
return real, ACCESS_EACCES

return real, None
return normalized, None

def get_full_path(self, debugger: Debugger, file: str, dirfd: int = AT_FDCWD) -> str:
dirfd = (dirfd & 0x7FFFFFFF) - (dirfd & 0x80000000)
Expand Down
4 changes: 4 additions & 0 deletions dmoj/executors/CBL.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import subprocess

from dmoj.cptbox.filesystem_policies import ExactFile
from dmoj.executors.compiled_executor import CompiledExecutor


Expand All @@ -9,6 +10,9 @@ class Executor(CompiledExecutor):
command = 'cobc'
address_grace = 131072
compile_output_index = 0
compiler_read_fs = [
ExactFile('/etc/gnucobol/default.conf'),
]
test_program = """\
IDENTIFICATION DIVISION.
PROGRAM-ID. HELLO-WORLD.
Expand Down
4 changes: 4 additions & 0 deletions dmoj/executors/D.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dmoj.cptbox.filesystem_policies import ExactFile
from dmoj.executors.compiled_executor import CompiledExecutor


Expand All @@ -6,6 +7,9 @@ class Executor(CompiledExecutor):
name = 'D'
address_grace = 32768
command = 'dmd'
compiler_read_fs = [
ExactFile('/etc/dmd.conf'),
]
test_program = """\
import std.stdio;
Expand Down
10 changes: 8 additions & 2 deletions dmoj/executors/DART.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from dmoj.cptbox.filesystem_policies import ExactFile, RecursiveDir
from dmoj.executors.compiled_executor import CompiledExecutor


# Running DART normally results in unholy memory usage
# Thankfully compiling it results in something...far more sane
# Running Dart normally results in unholy memory usage
# Thankfully compiling it results in something... far more sane
class Executor(CompiledExecutor):
ext = 'dart'
name = 'DART'
nproc = -1 # Dart uses a really, really large number of threads
command = 'dart'
compiler_read_fs = [
# Dart shells out...
ExactFile('/bin/sh'),
RecursiveDir('/proc/self/fd'),
]
test_program = """
void main() {
print("echo: Hello, World!");
Expand Down
4 changes: 4 additions & 0 deletions dmoj/executors/GROOVY.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import subprocess
from pathlib import Path

from dmoj.executors.java_executor import JavaExecutor
from dmoj.utils.unicode import utf8text
Expand Down Expand Up @@ -29,6 +30,9 @@ def get_cmdline(self, **kwargs):
def get_compile_args(self):
return [self.get_compiler(), self._code]

def get_compile_env(self):
return {'JAVA_HOME': str(Path(os.path.realpath(self.get_vm())).parent.parent)}

@classmethod
def get_versionable_commands(cls):
return [('groovyc', cls.get_compiler()), ('java', cls.get_vm())]
Expand Down
6 changes: 6 additions & 0 deletions dmoj/executors/HASK.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dmoj.cptbox.filesystem_policies import RecursiveDir
from dmoj.executors.compiled_executor import CompiledExecutor
from dmoj.executors.mixins import NullStdoutMixin

Expand All @@ -6,6 +7,11 @@ class Executor(NullStdoutMixin, CompiledExecutor):
ext = 'hs'
name = 'HASK'
command = 'ghc'
compiler_read_fs = [
RecursiveDir('/proc/self/task'),
RecursiveDir('/var/lib/ghc'),
]

test_program = """\
main = do
a <- getContents
Expand Down
5 changes: 5 additions & 0 deletions dmoj/executors/KOTLIN.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dmoj.cptbox.filesystem_policies import ExactFile
from dmoj.executors.java_executor import JavaExecutor


Expand All @@ -7,6 +8,10 @@ class Executor(JavaExecutor):

compiler = 'kotlinc'
compiler_time_limit = 20
compiler_read_fs = [
ExactFile('/bin/uname'),
ExactFile('/bin/bash'),
]
vm = 'kotlin_vm'

test_program = """\
Expand Down
4 changes: 4 additions & 0 deletions dmoj/executors/OCAML.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dmoj.cptbox.filesystem_policies import RecursiveDir
from dmoj.executors.compiled_executor import CompiledExecutor


Expand All @@ -8,6 +9,9 @@ class Executor(CompiledExecutor):
ext = 'ml'
name = 'OCAML'
command = 'ocamlfind'
compiler_read_fs = [
RecursiveDir('~/.opam'),
]
test_program = """
open! Base
open! Core
Expand Down
4 changes: 4 additions & 0 deletions dmoj/executors/PAS.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dmoj.cptbox.filesystem_policies import ExactFile
from dmoj.executors.compiled_executor import CompiledExecutor
from dmoj.executors.mixins import NullStdoutMixin

Expand All @@ -6,6 +7,9 @@ class Executor(NullStdoutMixin, CompiledExecutor):
ext = 'pas'
name = 'PAS'
command = 'fpc'
compiler_read_fs = [
ExactFile('/etc/fpc.cfg'),
]
test_program = """\
var line : string;
begin
Expand Down
4 changes: 4 additions & 0 deletions dmoj/executors/RKT.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ class Executor(CompiledExecutor):
ext = 'rkt'
name = 'RKT'
fs = [RecursiveDir('/etc/racket'), ExactFile('/etc/passwd')]
compiler_read_fs = [
RecursiveDir('/etc/racket'),
RecursiveDir('~/.local/share/racket'),
]

command = 'racket'

Expand Down
8 changes: 8 additions & 0 deletions dmoj/executors/RUST.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os

from dmoj.cptbox.filesystem_policies import ExactFile, RecursiveDir
from dmoj.executors.compiled_executor import CompiledExecutor
from dmoj.utils.os_ext import bool_env

Expand Down Expand Up @@ -77,6 +78,13 @@ class Executor(CompiledExecutor):
command = 'cargo'
test_program = HELLO_WORLD_PROGRAM
compiler_time_limit = 20
compiler_read_fs = [
RecursiveDir('/home'),
ExactFile('/etc/resolv.conf'),
]
compiler_write_fs = [
RecursiveDir('~/.cargo'),
]

def create_files(self, problem_id, source_code, *args, **kwargs):
os.mkdir(self._file('src'))
Expand Down
9 changes: 9 additions & 0 deletions dmoj/executors/SCALA.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import subprocess

from dmoj.cptbox.filesystem_policies import ExactFile, RecursiveDir
from dmoj.executors.java_executor import JavaExecutor
from dmoj.utils.unicode import utf8text

Expand All @@ -12,6 +13,14 @@ class Executor(JavaExecutor):

compiler = 'scalac'
compiler_time_limit = 20
compiler_read_fs = [
ExactFile('/bin/uname'),
ExactFile('/bin/readlink'),
ExactFile('/bin/grep'),
ExactFile('/bin/stty'),
ExactFile('/bin/bash'),
RecursiveDir('/etc/alternatives'),
]
vm = 'scala_vm'

test_program = """\
Expand Down
4 changes: 4 additions & 0 deletions dmoj/executors/SCM.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dmoj.cptbox.filesystem_policies import RecursiveDir
from dmoj.executors.compiled_executor import CompiledExecutor


Expand All @@ -6,6 +7,9 @@ class Executor(CompiledExecutor):
name = 'SCM'
command = 'chicken-csc'
command_paths = ['chicken-csc', 'csc']
compiler_read_fs = [
RecursiveDir('/var/lib/chicken'),
]
test_program = '(import chicken.io) (map print (read-lines))'

def get_compile_args(self):
Expand Down
5 changes: 5 additions & 0 deletions dmoj/executors/SWIFT.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from dmoj.cptbox.filesystem_policies import RecursiveDir
from dmoj.executors.compiled_executor import CompiledExecutor


class Executor(CompiledExecutor):
ext = 'swift'
name = 'SWIFT'
command = 'swiftc'
compiler_read_fs = [
RecursiveDir('~/.cache'),
]
compiler_write_fs = compiler_read_fs
test_program = 'print(readLine()!)'

def get_compile_args(self):
Expand Down
5 changes: 5 additions & 0 deletions dmoj/executors/ZIG.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dmoj.cptbox.filesystem_policies import RecursiveDir
from dmoj.executors.compiled_executor import CompiledExecutor


Expand All @@ -6,6 +7,10 @@ class Executor(CompiledExecutor):
name = 'ZIG'
command = 'zig'
compiler_time_limit = 30
compiler_read_fs = [
RecursiveDir('~/.cache'),
]
compiler_write_fs = compiler_read_fs
test_program = """
const std = @import("std");
Expand Down
Loading

0 comments on commit 4868122

Please sign in to comment.