-
Notifications
You must be signed in to change notification settings - Fork 254
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
Switching to using memfd for input data #990
base: master
Are you sure you want to change the base?
Changes from all commits
0efd1d2
651771b
c2690bf
1b73783
013850a
41c88f3
66437a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# Based off https://github.com/django/django/blob/main/django/utils/functional.py, licensed under 3-clause BSD. | ||
from functools import total_ordering | ||
|
||
from dmoj.cptbox._cptbox import BufferProxy | ||
|
||
_SENTINEL = object() | ||
|
||
|
||
@total_ordering | ||
class LazyBytes(BufferProxy): | ||
""" | ||
Encapsulate a function call and act as a proxy for methods that are | ||
called on the result of that function. The function is not evaluated | ||
until one of the methods on the result is called. | ||
""" | ||
|
||
def __init__(self, func): | ||
self.__func = func | ||
self.__value = _SENTINEL | ||
|
||
def __get_value(self): | ||
if self.__value is _SENTINEL: | ||
self.__value = self.__func() | ||
return self.__value | ||
|
||
@classmethod | ||
def _create_promise(cls, method_name): | ||
# Builds a wrapper around some magic method | ||
def wrapper(self, *args, **kw): | ||
# Automatically triggers the evaluation of a lazy value and | ||
# applies the given magic method of the result type. | ||
res = self.__get_value() | ||
return getattr(res, method_name)(*args, **kw) | ||
|
||
return wrapper | ||
|
||
def __cast(self): | ||
return bytes(self.__get_value()) | ||
|
||
def _get_real_buffer(self): | ||
return self.__cast() | ||
|
||
def __bytes__(self): | ||
return self.__cast() | ||
|
||
def __repr__(self): | ||
return repr(self.__cast()) | ||
|
||
def __str__(self): | ||
return str(self.__cast()) | ||
|
||
def __eq__(self, other): | ||
if isinstance(other, LazyBytes): | ||
other = other.__cast() | ||
return self.__cast() == other | ||
|
||
def __lt__(self, other): | ||
if isinstance(other, LazyBytes): | ||
other = other.__cast() | ||
return self.__cast() < other | ||
|
||
def __hash__(self): | ||
return hash(self.__cast()) | ||
|
||
def __mod__(self, rhs): | ||
return self.__cast() % rhs | ||
|
||
def __add__(self, other): | ||
return self.__cast() + other | ||
|
||
def __radd__(self, other): | ||
return other + self.__cast() | ||
|
||
def __deepcopy__(self, memo): | ||
# Instances of this class are effectively immutable. It's just a | ||
# collection of functions. So we don't need to do anything | ||
# complicated for copying. | ||
memo[id(self)] = self | ||
return self | ||
|
||
|
||
for type_ in bytes.mro(): | ||
for method_name in type_.__dict__: | ||
# All __promise__ return the same wrapper method, they | ||
# look up the correct implementation when called. | ||
if hasattr(LazyBytes, method_name): | ||
continue | ||
setattr(LazyBytes, method_name, LazyBytes._create_promise(method_name)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,62 @@ | ||
import errno | ||
import io | ||
import mmap | ||
import os | ||
from tempfile import NamedTemporaryFile | ||
from typing import Optional | ||
|
||
from dmoj.cptbox._cptbox import memory_fd_create, memory_fd_seal | ||
from dmoj.cptbox.tracer import FREEBSD | ||
|
||
|
||
class MemoryIO(io.FileIO): | ||
def __init__(self) -> None: | ||
super().__init__(memory_fd_create(), 'r+') | ||
_name: Optional[str] = None | ||
|
||
def __init__(self, prefill: Optional[bytes] = None, seal=False) -> None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe one or both of these should be required kwargs. I'm thinking the second should. What is the difference between prefilling with nothing, and passing |
||
if FREEBSD: | ||
with NamedTemporaryFile(delete=False) as f: | ||
self._name = f.name | ||
super().__init__(os.dup(f.fileno()), 'r+') | ||
else: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to dup and specify There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Definitely. Otherwise the fd gets closed or the file gets unlinked, respectively. |
||
super().__init__(memory_fd_create(), 'r+') | ||
|
||
if prefill: | ||
self.write(prefill) | ||
if seal: | ||
self.seal() | ||
|
||
def seal(self) -> None: | ||
memory_fd_seal(self.fileno()) | ||
fd = self.fileno() | ||
try: | ||
memory_fd_seal(fd) | ||
except OSError as e: | ||
if e.errno == errno.ENOSYS: | ||
# FreeBSD | ||
self.seek(0, os.SEEK_SET) | ||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not quite sure what this does. Does it deserve more of a comment? |
||
raise | ||
|
||
new_fd = os.open(f'/proc/self/fd/{fd}', os.O_RDONLY) | ||
try: | ||
os.dup2(new_fd, fd) | ||
finally: | ||
os.close(new_fd) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we could use a comment about why this dup is needed. Also, why isn't it implemented in the C function? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. C code is just that much more painful to maintain. |
||
|
||
def close(self) -> None: | ||
super().close() | ||
if self._name: | ||
os.unlink(self._name) | ||
|
||
def to_path(self) -> str: | ||
if self._name: | ||
return self._name | ||
return f'/proc/{os.getpid()}/fd/{self.fileno()}' | ||
|
||
def to_bytes(self) -> bytes: | ||
try: | ||
with mmap.mmap(self.fileno(), 0, access=mmap.ACCESS_READ) as f: | ||
return bytes(f) | ||
except ValueError as e: | ||
if e.args[0] == 'cannot mmap an empty file': | ||
return b'' | ||
raise |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,10 +21,10 @@ def get_fs(self): | |
def get_allowed_syscalls(self): | ||
return super().get_allowed_syscalls() + ['fork', 'waitpid', 'wait4'] | ||
|
||
def get_security(self, launch_kwargs=None): | ||
def get_security(self, launch_kwargs=None, extra_fs=None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not that this is a direct result of this PR, but maybe this should be converted to |
||
from dmoj.cptbox.syscalls import sys_execve, sys_access, sys_eaccess | ||
|
||
sec = super().get_security(launch_kwargs) | ||
sec = super().get_security(launch_kwargs=launch_kwargs, extra_fs=extra_fs) | ||
allowed = set(self.get_allowed_exec()) | ||
|
||
def handle_execve(debugger): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, is this function called on FreeBSD anymore? Are you creating the tempfile in Python instead?