diff --git a/.freebsd.test.py b/.freebsd.test.py index 9a3f352da..355a82e81 100644 --- a/.freebsd.test.py +++ b/.freebsd.test.py @@ -2,6 +2,7 @@ from dmoj import judgeenv from dmoj.citest import ci_test +from dmoj.executors.filesystem_policies import FilesystemPolicy, ExactFile EXECUTORS = [ 'AWK', @@ -31,13 +32,16 @@ def main(): judgeenv.env['runtime'] = {} - judgeenv.env['extra_fs'] = {'PERL': ['/dev/dtrace/helper$'], 'RUBY2': ['/dev/dtrace/helper$']} + judgeenv.env['extra_fs'] = { + 'PERL': FilesystemPolicy([ExactFile("/dev/dtrace/helper")]), + 'RUBY2': FilesystemPolicy([ExactFile("/dev/dtrace/helper")]) + } logging.basicConfig(level=logging.INFO) print('Using extra allowed filesystems:') for lang, fs in judgeenv.env['extra_fs'].iteritems(): - print('%-6s: %s' % (lang, '|'.join(fs))) + print('%-6s: %s' % (lang, '|'.join([x.path for x in fs.rules]))) print() print('Testing executors...') diff --git a/dmoj/cptbox/isolate.py b/dmoj/cptbox/isolate.py index 4c3f7f03b..345d8f738 100644 --- a/dmoj/cptbox/isolate.py +++ b/dmoj/cptbox/isolate.py @@ -1,14 +1,13 @@ import logging import os -import re import sys from dmoj.cptbox._cptbox import AT_FDCWD, bsd_get_proc_cwd, bsd_get_proc_fdno from dmoj.cptbox.tracer import MaxLengthExceeded from dmoj.cptbox.handlers import ACCESS_EACCES, ACCESS_ENOENT, ACCESS_EPERM, ALLOW - from dmoj.cptbox.syscalls import * from dmoj.utils.unicode import utf8text +from dmoj.executors.filesystem_policies import FilesystemPolicy log = logging.getLogger('dmoj.security') open_write_flags = [os.O_WRONLY, os.O_RDWR, os.O_TRUNC, os.O_CREAT, os.O_EXCL] @@ -185,11 +184,12 @@ def __init__(self, read_fs, write_fs=None, writable=(1, 2)): def _compile_fs_jail(self, fs): if fs: - fs_re = '|'.join(fs) + fs.build() + return fs else: - fs_re = '(?!)' # Disallow accessing everything by default. - - return re.compile(fs_re) + dummy = FilesystemPolicy([]) + dummy.build() + return dummy def is_write_flags(self, open_flags): for flag in open_write_flags: @@ -258,9 +258,7 @@ def _file_access_check(self, rel_file, debugger, is_open, flag_reg=1, dirfd=AT_F is_write = is_open and self.is_write_flags(getattr(debugger, 'uarg%d' % flag_reg)) fs_jail = self.write_fs_jail if is_write else self.read_fs_jail - if fs_jail.match(file) is None: - return file, False - return file, True + return file, fs_jail.check(file) def get_full_path(self, debugger, file, dirfd=AT_FDCWD): dirfd = (dirfd & 0x7FFFFFFF) - (dirfd & 0x80000000) diff --git a/dmoj/executors/COFFEE.py b/dmoj/executors/COFFEE.py index 090da03a9..53c0d4a74 100644 --- a/dmoj/executors/COFFEE.py +++ b/dmoj/executors/COFFEE.py @@ -1,6 +1,7 @@ import os from dmoj.executors.script_executor import ScriptExecutor +from dmoj.executors.filesystem_policies import FilesystemPolicy, ExactFile class Executor(ScriptExecutor): @@ -38,7 +39,10 @@ def get_cmdline(self, **kwargs): return [self.get_command(), self.runtime_dict['coffee'], self._code] def get_fs(self): - return super().get_fs() + [self.runtime_dict['coffee'], self._code] + return super().get_fs() + FilesystemPolicy([ + ExactFile(self.runtime_dict['coffee']), + ExactFile(self._code) + ]) @classmethod def get_versionable_commands(cls): diff --git a/dmoj/executors/DART.py b/dmoj/executors/DART.py index 3310626a6..9889b0b7c 100644 --- a/dmoj/executors/DART.py +++ b/dmoj/executors/DART.py @@ -16,7 +16,6 @@ class Executor(CompiledExecutor): address_grace = 128 * 1024 syscalls = ['epoll_create', 'epoll_ctl', 'epoll_wait', 'timerfd_settime', 'memfd_create', 'ftruncate'] - fs = ['.*/vm-service$'] def get_compile_args(self): return [self.get_command(), '--snapshot=%s' % self.get_compiled_file(), self._code] diff --git a/dmoj/executors/FORTH.py b/dmoj/executors/FORTH.py index e1df148d5..04d7bfb61 100644 --- a/dmoj/executors/FORTH.py +++ b/dmoj/executors/FORTH.py @@ -1,4 +1,5 @@ from dmoj.executors.script_executor import ScriptExecutor +from dmoj.executors.filesystem_policies import FilesystemPolicy, ExactFile class Executor(ScriptExecutor): @@ -10,7 +11,9 @@ class Executor(ScriptExecutor): HELLO ''' - fs = [r'/\.gforth-history$'] + fs = FilesystemPolicy([ + ExactFile('/.gforth-history') + ]) def get_cmdline(self, **kwargs): return [self.get_command(), self._code, '-e', 'bye'] diff --git a/dmoj/executors/PERL.py b/dmoj/executors/PERL.py index f18e30b7c..f2334630a 100644 --- a/dmoj/executors/PERL.py +++ b/dmoj/executors/PERL.py @@ -1,11 +1,14 @@ from dmoj.executors.script_executor import ScriptExecutor +from dmoj.executors.filesystem_policies import FilesystemPolicy, RecursiveDir class Executor(ScriptExecutor): ext = 'pl' name = 'PERL' command = 'perl' - fs = ['/etc/perl/.*?'] + fs = FilesystemPolicy([ + RecursiveDir('/etc/perl') + ]) test_program = 'print<>' syscalls = ['umtx_op'] diff --git a/dmoj/executors/PHP.py b/dmoj/executors/PHP.py index fdc4f9db5..da9e23fc4 100644 --- a/dmoj/executors/PHP.py +++ b/dmoj/executors/PHP.py @@ -7,8 +7,6 @@ class Executor(ScriptExecutor): command = 'php' command_paths = ['php7', 'php5', 'php'] - fs = [r'.*/php[\w-]*\.ini$', r'.*/conf.d/.*\.ini$'] - test_program = ' bool: + if path == "/": + return self._check_final_node(self.root) + + assert self.built, "Must build the policy object before checking" + assert path[0] == '/', "Must pass an absolute path to check" + + path_split = path[1:].split("/") + node = self.root + + for component in path_split: + assert component != "", "Must not have empty components in path to check" + + if isinstance(node, File): + return False + + elif node.access_mode == ACCESS_MODE.RECURSIVE: + return True + + else: + node = node.map.get(component) + + if node is None: + return False + + return self._check_final_node(node) + + def _check_final_node(self, node: Union[Dir, File]) -> bool: + return isinstance(node, File) or node.access_mode != ACCESS_MODE.NONE diff --git a/dmoj/executors/java_executor.py b/dmoj/executors/java_executor.py index 5e45bb6f6..ff78a774a 100644 --- a/dmoj/executors/java_executor.py +++ b/dmoj/executors/java_executor.py @@ -11,6 +11,7 @@ from dmoj.executors.mixins import SingleDigitVersionMixin from dmoj.judgeenv import skip_self_test from dmoj.utils.unicode import utf8bytes, utf8text +from dmoj.executors.filesystem_policies import FilesystemPolicy, ExactFile recomment = re.compile(r'/\*.*?\*/', re.DOTALL | re.U) restring = re.compile(r''''(?:\\.|[^'\\])'|"(?:\\.|[^"\\])*"''', re.DOTALL | re.U) @@ -81,10 +82,14 @@ def get_executable(self): return self.get_vm() def get_fs(self): - return super().get_fs() + [self._agent_file] + return super().get_fs() + FilesystemPolicy([ + ExactFile(self._agent_file) + ]) def get_write_fs(self): - return super().get_write_fs() + [os.path.join(self._dir, 'submission_jvm_crash.log')] + return super().get_write_fs() + FilesystemPolicy([ + ExactFile(os.path.join(self._dir, 'submission_jvm_crash.log')) + ]) def get_agent_flag(self): agent_flag = '-javaagent:%s=policy:%s' % (self._agent_file, self._policy_file) diff --git a/dmoj/executors/mixins.py b/dmoj/executors/mixins.py index 05483c2a3..26f5511b5 100644 --- a/dmoj/executors/mixins.py +++ b/dmoj/executors/mixins.py @@ -11,39 +11,79 @@ from dmoj.judgeenv import env from dmoj.utils import setbufsize_path from dmoj.utils.unicode import utf8bytes +from dmoj.executors.filesystem_policies import FilesystemPolicy, ExactDir, RecursiveDir, ExactFile + +BASE_FILESYSTEM = FilesystemPolicy([ + ExactFile("/dev/null"), + ExactFile("/dev/tty"), + ExactFile("/dev/zero"), + ExactFile("/dev/urandom"), + ExactFile("/dev/random"), + *[RecursiveDir(f"/usr/{d}") for d in os.listdir("/usr") if d != "home"], + + RecursiveDir("/lib"), + RecursiveDir("/lib32"), + RecursiveDir("/lib64"), + RecursiveDir("/opt"), + + ExactDir("/etc"), + ExactFile("/etc/localtime"), + ExactFile("/etc/timezone"), + + ExactDir("/usr"), + ExactDir("/tmp"), + ExactDir("/") +]) + +BASE_WRITE_FILESYSTEM = FilesystemPolicy([ + ExactFile("/dev/stdout"), + ExactFile("/dev/stderr"), + ExactFile("/dev/null") +]) -BASE_FILESYSTEM = [ - '/dev/(?:null|tty|zero|u?random)$', - '/usr/(?!home)', - '/lib(?:32|64)?/', - '/opt/', - '/etc$', - '/etc/(?:localtime|timezone)$', - '/usr$', - '/tmp$', - '/$', -] -BASE_WRITE_FILESYSTEM = ['/dev/stdout$', '/dev/stderr$', '/dev/null$'] if 'freebsd' in sys.platform: - BASE_FILESYSTEM += [r'/etc/s?pwd\.db$', '/dev/hv_tsc$'] + BASE_FILESYSTEM += FilesystemPolicy([ + ExactFile("/etc/spwd.db"), + ExactFile("/etc/pwd.db"), + ExactFile("/dev/hv_tsc") + ]) + else: - BASE_FILESYSTEM += ['/sys/devices/system/cpu(?:$|/online)', '/etc/selinux/config$'] + BASE_FILESYSTEM += FilesystemPolicy([ + ExactDir("/sys/devices/system/cpu"), + ExactFile("/sys/devices/system/cpu/online"), + ExactFile("/etc/selinux/config") + ]) if sys.platform.startswith('freebsd'): - BASE_FILESYSTEM += [r'/etc/libmap\.conf$', r'/var/run/ld-elf\.so\.hints$'] + BASE_FILESYSTEM += FilesystemPolicy([ + ExactFile("/etc/libmap.conf"), + ExactFile("/var/run/ld-elf.so.hints") + ]) else: # Linux and kFreeBSD mounts linux-style procfs. - BASE_FILESYSTEM += [ - '/proc$', - '/proc/self/(?:maps|exe|auxv)$', - '/proc/self$', - '/proc/(?:meminfo|stat|cpuinfo|filesystems|xen|uptime)$', - '/proc/sys/vm/overcommit_memory$', - ] + BASE_FILESYSTEM += FilesystemPolicy([ + ExactDir("/proc"), + ExactDir("/proc/self"), + ExactFile("/proc/self/maps"), + ExactFile("/proc/self/exe"), + ExactFile("/proc/auxv"), + ExactFile("/proc/meminfo"), + ExactFile("/proc/stat"), + ExactFile("/proc/cpuinfo"), + ExactFile("/proc/filesystems"), + ExactFile("/proc/xen"), + ExactFile("/proc/uptime"), + ExactFile("/proc/sys/vm/overcommit_memory") + ]) # Linux-style ld. - BASE_FILESYSTEM += [r'/etc/ld\.so\.(?:nohwcap|preload|cache)$'] + BASE_FILESYSTEM += FilesystemPolicy([ + ExactFile("/etc/ld.so.nohwcap"), + ExactFile("/etc/ld.so.preload"), + ExactFile("/etc/ld.so.cache") + ]) UTF8_LOCALE = 'C.UTF-8' @@ -56,8 +96,8 @@ class PlatformExecutorMixin(metaclass=abc.ABCMeta): data_grace = 0 fsize = 0 personality = 0x0040000 # ADDR_NO_RANDOMIZE - fs: List[str] = [] - write_fs: List[str] = [] + fs: FilesystemPolicy = FilesystemPolicy([]) + write_fs: FilesystemPolicy = FilesystemPolicy([]) syscalls: List[Union[str, Tuple[str, Any]]] = [] def _add_syscalls(self, sec): @@ -75,7 +115,8 @@ def get_security(self, launch_kwargs=None): def get_fs(self): name = self.get_executor_name() - fs = BASE_FILESYSTEM + self.fs + env.get('extra_fs', {}).get(name, []) + [re.escape(self._dir)] + extra_fs = env.get('extra_fs', {}).get(name, FilesystemPolicy([])) + fs = BASE_FILESYSTEM + self.fs + extra_fs + FilesystemPolicy([RecursiveDir(self._dir)]) return fs def get_write_fs(self): diff --git a/dmoj/executors/mono_executor.py b/dmoj/executors/mono_executor.py index 6fd4e6dc9..47c645c4c 100644 --- a/dmoj/executors/mono_executor.py +++ b/dmoj/executors/mono_executor.py @@ -7,6 +7,7 @@ from dmoj.executors.compiled_executor import CompiledExecutor from dmoj.result import Result from dmoj.utils.unicode import utf8text +from dmoj.executors.filesystem_policies import FilesystemPolicy, RecursiveDir reexception = re.compile(r'\bFATAL UNHANDLED EXCEPTION: (.*?):', re.U) @@ -32,7 +33,9 @@ class MonoExecutor(CompiledExecutor): # get flagged as MLE. data_grace = 65536 cptbox_popen_class = MonoTracedPopen - fs = ['/etc/mono/'] + fs = FilesystemPolicy([ + RecursiveDir('/etc/mono') + ]) # Mono sometimes forks during its crashdump procedure, but continues even if # the call to fork fails. syscalls = [ diff --git a/dmoj/executors/script_executor.py b/dmoj/executors/script_executor.py index 18e53bf99..cca047a57 100644 --- a/dmoj/executors/script_executor.py +++ b/dmoj/executors/script_executor.py @@ -1,10 +1,11 @@ import os -import re from typing import List, Optional from dmoj.executors.base_executor import BaseExecutor from dmoj.utils.unicode import utf8bytes +from dmoj.executors.filesystem_policies import FilesystemPolicy, ExactFile, RecursiveDir + class ScriptExecutor(BaseExecutor): def __init__(self, problem_id: str, source_code: bytes, **kwargs): @@ -24,9 +25,9 @@ def get_command(cls) -> Optional[str]: def get_fs(self) -> list: home = self.runtime_dict.get('%s_home' % self.get_executor_name().lower()) - fs = super().get_fs() + [self._code] + fs = super().get_fs() + FilesystemPolicy([ExactFile(self._code)]) if home is not None: - fs.append(re.escape(home)) + fs += FilesystemPolicy([RecursiveDir(home)]) return fs def create_files(self, problem_id: str, source_code: bytes) -> None: diff --git a/dmoj/utils/helper_files.py b/dmoj/utils/helper_files.py index c2888f9bd..eef557a6f 100644 --- a/dmoj/utils/helper_files.py +++ b/dmoj/utils/helper_files.py @@ -4,6 +4,7 @@ from dmoj.error import InternalError from dmoj.result import Result from dmoj.utils.os_ext import strsignal +from dmoj.executors.filesystem_policies import FilesystemPolicy, RecursiveDir def mktemp(data): @@ -41,7 +42,7 @@ def find_runtime(languages): executor = executor.Executor - kwargs = {'fs': executor.fs + [tempfile.gettempdir()]} + kwargs = {'fs': executor.fs + FilesystemPolicy([RecursiveDir(tempfile.gettempdir())])} if issubclass(executor, CompiledExecutor): kwargs['compiler_time_limit'] = compiler_time_limit