Skip to content

Commit

Permalink
change regex to filesystem policy matcher, first step of DMOJ#871
Browse files Browse the repository at this point in the history
  • Loading branch information
Riolku committed Sep 5, 2021
1 parent e20c1d6 commit c47625e
Show file tree
Hide file tree
Showing 19 changed files with 289 additions and 57 deletions.
9 changes: 7 additions & 2 deletions .freebsd.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,18 @@

def main():
judgeenv.env['runtime'] = {}
judgeenv.env['extra_fs'] = {'PERL': ['/dev/dtrace/helper$'], 'RUBY2': ['/dev/dtrace/helper$']}
judgeenv.env['extra_fs'] = {
'PERL': [{"exact_file": "/dev/dtrace/helper"}],
'RUBY2': [{"exact_file": "/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)))
for rules in fs:
for access_type, file in rules.iteritems():
print('%-6s: %s: %s' % (lang, access_type, file))
print()

print('Testing executors...')
Expand Down
13 changes: 3 additions & 10 deletions dmoj/cptbox/isolate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging
import os
import re
import sys

from dmoj.cptbox._cptbox import AT_FDCWD, bsd_get_proc_cwd, bsd_get_proc_fdno
Expand All @@ -9,6 +8,7 @@

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]
Expand Down Expand Up @@ -184,12 +184,7 @@ def __init__(self, read_fs, write_fs=None, writable=(1, 2)):
)

def _compile_fs_jail(self, fs):
if fs:
fs_re = '|'.join(fs)
else:
fs_re = '(?!)' # Disallow accessing everything by default.

return re.compile(fs_re)
return FilesystemPolicy(fs or [])

def is_write_flags(self, open_flags):
for flag in open_write_flags:
Expand Down Expand Up @@ -258,9 +253,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)
Expand Down
3 changes: 2 additions & 1 deletion dmoj/executors/COFFEE.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os

from dmoj.executors.script_executor import ScriptExecutor
from dmoj.executors.filesystem_policies import ExactFile


class Executor(ScriptExecutor):
Expand Down Expand Up @@ -38,7 +39,7 @@ 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() + [ExactFile(self.runtime_dict['coffee']), ExactFile(self._code)]

@classmethod
def get_versionable_commands(cls):
Expand Down
1 change: 0 additions & 1 deletion dmoj/executors/DART.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion dmoj/executors/FORTH.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dmoj.executors.script_executor import ScriptExecutor
from dmoj.executors.filesystem_policies import ExactFile


class Executor(ScriptExecutor):
Expand All @@ -10,7 +11,7 @@ class Executor(ScriptExecutor):
HELLO
'''
fs = [r'/\.gforth-history$']
fs = [ExactFile('/.gforth-history')]

def get_cmdline(self, **kwargs):
return [self.get_command(), self._code, '-e', 'bye']
3 changes: 2 additions & 1 deletion dmoj/executors/PERL.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from dmoj.executors.script_executor import ScriptExecutor
from dmoj.executors.filesystem_policies import RecursiveDir


class Executor(ScriptExecutor):
ext = 'pl'
name = 'PERL'
command = 'perl'
fs = ['/etc/perl/.*?']
fs = [RecursiveDir('/etc/perl')]
test_program = 'print<>'
syscalls = ['umtx_op']

Expand Down
2 changes: 0 additions & 2 deletions dmoj/executors/PHP.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<?php while($f = fgets(STDIN)) echo $f;'

def get_cmdline(self, **kwargs):
Expand Down
6 changes: 2 additions & 4 deletions dmoj/executors/RKT.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import os

from dmoj.executors.compiled_executor import CompiledExecutor
from dmoj.executors.filesystem_policies import RecursiveDir, ExactFile


class Executor(CompiledExecutor):
ext = 'rkt'
name = 'RKT'
fs = [os.path.expanduser(r'~/\.racket/'), os.path.expanduser(r'~/\.local/share/racket/'),
'/etc/racket/.*?', '/etc/passwd$']
fs = [RecursiveDir("/etc/racket"), ExactFile("/etc/passwd")]

command = 'racket'

Expand Down
3 changes: 2 additions & 1 deletion dmoj/executors/RUBY2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import re

from dmoj.executors.script_executor import ScriptExecutor
from dmoj.executors.filesystem_policies import ExactFile


class Executor(ScriptExecutor):
Expand All @@ -12,7 +13,7 @@ class Executor(ScriptExecutor):
nproc = -1
command_paths = ['ruby2.%d' % i for i in reversed(range(0, 8))] + ['ruby2%d' % i for i in reversed(range(0, 8))]
syscalls = ['thr_set_name', 'eventfd2']
fs = ['/proc/self/loginuid$']
fs = [ExactFile('/proc/self/loginuid')]

def get_fs(self):
fs = super().get_fs()
Expand Down
1 change: 0 additions & 1 deletion dmoj/executors/SWIFT.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ class Executor(CompiledExecutor):
ext = 'swift'
name = 'SWIFT'
command = 'swiftc'
fs = ['/lib']
test_program = 'print(readLine()!)'

def get_compile_args(self):
Expand Down
3 changes: 2 additions & 1 deletion dmoj/executors/TUR.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from dmoj.executors.compiled_executor import CompiledExecutor
from dmoj.judgeenv import env
from dmoj.executors.filesystem_policies import ExactFile


class Executor(CompiledExecutor):
Expand All @@ -15,7 +16,7 @@ class Executor(CompiledExecutor):
'''

def get_fs(self):
return super().get_fs() + [self._code + 'bc']
return super().get_fs() + [ExactFile(self._code + 'bc')]

def get_compile_args(self):
tprologc = self.runtime_dict['tprologc']
Expand Down
114 changes: 114 additions & 0 deletions dmoj/executors/filesystem_policies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import os
from typing import List, Union


class ACCESS_MODE:
NONE = 0
EXACT = 1
RECURSIVE = 2


class Dir:
def __init__(self):
self.access_mode = ACCESS_MODE.NONE
self.map = {}


class File:
pass


class ExactFile:
def __init__(self, path: str):
self.path = path


class ExactDir:
access_mode = ACCESS_MODE.EXACT

def __init__(self, path: str):
self.path = path


class RecursiveDir:
access_mode = ACCESS_MODE.RECURSIVE

def __init__(self, path: str):
self.path = path


Rule = Union[ExactFile, ExactDir, RecursiveDir]


class FilesystemPolicy:
def __init__(self, rules: List[Rule]):
self.root = Dir()
for rule in rules:
self._add_rule(rule)

def _add_rule(self, rule: Rule):
self._assert_rule_type(rule)
if rule.path == "/":
return self._finalize_root_rule(rule)

path = rule.path
assert os.path.isabs(path), "Rule must specify an absolute path to rule"
*directory_path, final_component = path.split("/")[1:]

node = self.root
for component in directory_path:
assert component != "", "Must not have empty components in rule to add"
new_node = node.map.setdefault(component, Dir())
assert isinstance(new_node, Dir), "Cannot descend into non-directory"
node = new_node

self._finalize_rule(node, final_component, rule)

def _assert_rule_type(self, rule: Rule):
if os.path.exists(rule.path):
is_dir = os.path.isdir(rule.path)
if isinstance(rule, ExactFile):
assert not is_dir, f"Can't apply file rule to directory {rule.path}"
else:
assert is_dir, f"Can't apply directory rule to non-directory {rule.path}"

def _finalize_root_rule(self, rule: Rule):
assert not isinstance(rule, ExactFile), "Root is not a file"
self._finalize_directory_rule(self.root, rule)

def _finalize_rule(self, node: Dir, final_component: str, rule: Rule):
assert final_component != "", "Must not have trailing slashes in rule path"
if isinstance(rule, ExactFile):
new_node = node.map.setdefault(final_component, File())
assert isinstance(new_node, File), "Can't add ExactFile: Dir rule exists"
else:
new_node = node.map.setdefault(final_component, Dir())
assert isinstance(new_node, Dir), "Can't add rule: File rule exists"
self._finalize_directory_rule(new_node, rule)

def _finalize_directory_rule(self, node: Dir, rule: Union[ExactDir, RecursiveDir]):
node.access_mode = max(node.access_mode, rule.access_mode) # Allow the more permissive rule

# `path` should be a canonical path. The output of `realpath` is appropriate
def check(self, path: str) -> bool:
if path == "/":
return self._check_final_node(self.root)

assert os.path.isabs(path), "Must pass an absolute path to check"

node = self.root
for component in path.split("/")[1:]:
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
7 changes: 4 additions & 3 deletions dmoj/executors/java_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,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 ExactFile, ExactDir

recomment = re.compile(r'/\*.*?\*/', re.DOTALL | re.U)
restring = re.compile(r''''(?:\\.|[^'\\])'|"(?:\\.|[^"\\])*"''', re.DOTALL | re.U)
Expand Down Expand Up @@ -82,11 +83,11 @@ def get_executable(self):
return self.get_vm()

def get_fs(self):
return super().get_fs() + [f'{re.escape(self._agent_file)}$'] + \
[f'{re.escape(str(parent))}$' for parent in PurePath(self._agent_file).parents]
return super().get_fs() + [ExactFile(self._agent_file)] + \
[ExactDir(str(parent)) for parent in PurePath(self._agent_file).parents]

def get_write_fs(self):
return super().get_write_fs() + [os.path.join(self._dir, 'submission_jvm_crash.log')]
return super().get_write_fs() + [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)
Expand Down
Loading

0 comments on commit c47625e

Please sign in to comment.