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 69fb153 commit 5ee622e
Show file tree
Hide file tree
Showing 17 changed files with 259 additions and 57 deletions.
8 changes: 6 additions & 2 deletions .freebsd.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from dmoj import judgeenv
from dmoj.citest import ci_test
from dmoj.executors.filesystem_policies import FilesystemPolicy, ExactFile

EXECUTORS = [
'AWK',
Expand Down Expand Up @@ -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...')
Expand Down
16 changes: 7 additions & 9 deletions dmoj/cptbox/isolate.py
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 5 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 FilesystemPolicy, ExactFile


class Executor(ScriptExecutor):
Expand Down Expand Up @@ -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):
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
5 changes: 4 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 FilesystemPolicy, ExactFile


class Executor(ScriptExecutor):
Expand All @@ -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']
5 changes: 4 additions & 1 deletion dmoj/executors/PERL.py
Original file line number Diff line number Diff line change
@@ -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']

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
9 changes: 5 additions & 4 deletions dmoj/executors/RKT.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import os

from dmoj.executors.compiled_executor import CompiledExecutor
from dmoj.executors.filesystem_policies import FilesystemPolicy, 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 = FilesystemPolicy([
RecursiveDir("/etc/racket"),
ExactFile("/etc/passwd")
])

command = 'racket'

Expand Down
5 changes: 4 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 FilesystemPolicy, ExactFile


class Executor(ScriptExecutor):
Expand All @@ -12,7 +13,9 @@ 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 = FilesystemPolicy([
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
5 changes: 4 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 FilesystemPolicy, ExactFile


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

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

def get_compile_args(self):
tprologc = self.runtime_dict['tprologc']
Expand Down
136 changes: 136 additions & 0 deletions dmoj/executors/filesystem_policies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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.rules = rules

self.built = False

def __add__(self, other):
assert not self.built and not other.built, "Refusing to merge built policies: merge before building"

return FilesystemPolicy(self.rules + other.rules)

def build(self):
self.root = Dir()

for rule in self.rules:
self._add_rule(rule)

self.built = True

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

node = self.root
path = rule.path

assert path[0] == '/', "Rule must specify an absolute path to rule"

*directory_path, final_component = path[1:].split("/")

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 _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 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
9 changes: 7 additions & 2 deletions dmoj/executors/java_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 5ee622e

Please sign in to comment.