Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change regex to filesystem policy matcher, first step of #871 #873

Merged
merged 4 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
117 changes: 117 additions & 0 deletions dmoj/cptbox/filesystem_policies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import os
from enum import Enum
from typing import List, Union


class AccessMode(Enum):
NONE = 0
EXACT = 1
RECURSIVE = 2

# flake8 doesn't recognize `AccessMode` in type annotations here
@classmethod
def more_permissive(cls, mode1, mode2):
return cls(max(mode1.value, mode2.value))


class Dir:
def __init__(self):
self.access_mode = AccessMode.NONE
self.subpath_map = {}


class File:
pass


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


class ExactDir:
access_mode = AccessMode.EXACT

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


class RecursiveDir:
access_mode = AccessMode.RECURSIVE

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


FilesystemAccessRule = Union[ExactFile, ExactDir, RecursiveDir]


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

def _add_rule(self, rule: FilesystemAccessRule) -> None:
self._assert_rule_type(rule)
if rule.path == '/':
return self._finalize_root_rule(rule)

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

node = self.root
for component in directory_path:
new_node = node.subpath_map.setdefault(component, Dir())
assert isinstance(new_node, Dir), 'Cannot add rule: refusing to descend into non-directory'
node = new_node

self._finalize_rule(node, final_component, rule)

def _assert_rule_type(self, rule: FilesystemAccessRule) -> None:
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: FilesystemAccessRule) -> None:
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: FilesystemAccessRule) -> None:
assert final_component != '', 'Must not have trailing slashes in rule path'
if isinstance(rule, ExactFile):
new_node = node.subpath_map.setdefault(final_component, File())
assert isinstance(new_node, File), "Can't add ExactFile: Dir rule exists"
else:
new_node = node.subpath_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]) -> None:
node.access_mode = AccessMode.more_permissive(
node.access_mode, rule.access_mode
) # Allow the more permissive rule

# `path` should be a normalized path
def check(self, path: str) -> bool:
assert os.path.abspath(path) == path, 'Must pass a normalized, absolute path to check'

node = self.root
for component in path.split('/')[1:]:
if isinstance(node, File):
return False
elif node.access_mode == AccessMode.RECURSIVE:
return True
else:
node = node.subpath_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 != AccessMode.NONE
13 changes: 3 additions & 10 deletions dmoj/cptbox/isolate.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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.filesystem_policies import FilesystemPolicy
from dmoj.cptbox.handlers import ACCESS_EACCES, ACCESS_ENAMETOOLONG, ACCESS_ENOENT, ACCESS_EPERM, ALLOW
from dmoj.cptbox.syscalls import *
from dmoj.cptbox.tracer import MaxLengthExceeded
Expand Down Expand Up @@ -183,12 +183,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 @@ -257,9 +252,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,5 +1,6 @@
import os

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

Riolku marked this conversation as resolved.
Show resolved Hide resolved

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,3 +1,4 @@
from dmoj.cptbox.filesystem_policies import ExactFile
from dmoj.executors.script_executor import 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.cptbox.filesystem_policies import RecursiveDir
from dmoj.executors.script_executor import ScriptExecutor


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
10 changes: 2 additions & 8 deletions dmoj/executors/RKT.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import os

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


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
@@ -1,6 +1,7 @@
import os
import re

from dmoj.cptbox.filesystem_policies import ExactFile
from dmoj.executors.script_executor import 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
@@ -1,5 +1,6 @@
import os

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

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
7 changes: 4 additions & 3 deletions dmoj/executors/java_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pathlib import PurePath
from typing import Optional

from dmoj.cptbox.filesystem_policies import ExactDir, ExactFile
from dmoj.error import CompileError, InternalError
from dmoj.executors.compiled_executor import CompiledExecutor
from dmoj.executors.mixins import SingleDigitVersionMixin
Expand Down Expand Up @@ -87,12 +88,12 @@ def get_executable(self):
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]
+ [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