Skip to content

Commit 52948c7

Browse files
committed
Implement graceful memfd fallback for FreeBSD
1 parent 8f4ba33 commit 52948c7

File tree

6 files changed

+111
-43
lines changed

6 files changed

+111
-43
lines changed

dmoj/cptbox/_cptbox.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ AT_FDCWD: int
100100
bsd_get_proc_cwd: Callable[[int], str]
101101
bsd_get_proc_fdno: Callable[[int, int], str]
102102

103-
memory_fd_create: Callable[[], int]
104-
memory_fd_seal: Callable[[int], None]
103+
memfd_create: Callable[[], int]
104+
memfd_seal: Callable[[int], None]
105105

106106
class BufferProxy:
107107
def _get_real_buffer(self): ...

dmoj/cptbox/_cptbox.pyx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ cdef extern from 'helper.h' nogil:
134134
PTBOX_SPAWN_FAIL_EXECVE
135135
PTBOX_SPAWN_FAIL_SETAFFINITY
136136

137-
int _memory_fd_create "memory_fd_create"()
138-
int _memory_fd_seal "memory_fd_seal"(int fd)
137+
int cptbox_memfd_create()
138+
int cptbox_memfd_seal(int fd)
139139

140140

141141
cdef extern from 'fcntl.h' nogil:
@@ -215,14 +215,14 @@ def bsd_get_proc_fdno(pid_t pid, int fd):
215215
free(buf)
216216
return res
217217

218-
def memory_fd_create():
219-
cdef int fd = _memory_fd_create()
218+
def memfd_create():
219+
cdef int fd = cptbox_memfd_create()
220220
if fd < 0:
221221
PyErr_SetFromErrno(OSError)
222222
return fd
223223

224-
def memory_fd_seal(int fd):
225-
cdef int result = _memory_fd_seal(fd)
224+
def memfd_seal(int fd):
225+
cdef int result = cptbox_memfd_seal(fd)
226226
if result == -1:
227227
PyErr_SetFromErrno(OSError)
228228

dmoj/cptbox/helper.cpp

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -328,19 +328,16 @@ char *bsd_get_proc_fdno(pid_t pid, int fdno) {
328328
return bsd_get_proc_fd(pid, 0, fdno);
329329
}
330330

331-
int memory_fd_create(void) {
331+
int cptbox_memfd_create(void) {
332332
#ifdef __FreeBSD__
333-
char filename[] = "/tmp/cptbox-memoryfd-XXXXXXXX";
334-
int fd = mkstemp(filename);
335-
if (fd >= 0)
336-
unlink(filename);
337-
return fd;
333+
errno = ENOSYS;
334+
return -1;
338335
#else
339336
return memfd_create("cptbox memory_fd", MFD_ALLOW_SEALING);
340337
#endif
341338
}
342339

343-
int memory_fd_seal(int fd) {
340+
int cptbox_memfd_seal(int fd) {
344341
#ifdef __FreeBSD__
345342
errno = ENOSYS;
346343
return -1;

dmoj/cptbox/helper.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ int cptbox_child_run(const struct child_config *config);
3535
char *bsd_get_proc_cwd(pid_t pid);
3636
char *bsd_get_proc_fdno(pid_t pid, int fdno);
3737

38-
int memory_fd_create(void);
39-
int memory_fd_seal(int fd);
38+
int cptbox_memfd_create(void);
39+
int cptbox_memfd_seal(int fd);
4040

4141
#endif

dmoj/cptbox/utils.py

Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,42 @@
1-
import errno
21
import io
32
import mmap
43
import os
4+
from abc import ABCMeta, abstractmethod
5+
from tempfile import NamedTemporaryFile, TemporaryFile
56
from typing import Optional
67

7-
from dmoj.cptbox._cptbox import memory_fd_create, memory_fd_seal
8+
from dmoj.cptbox._cptbox import memfd_create, memfd_seal
89

910

10-
class MemoryIO(io.FileIO):
11-
def __init__(self, prefill: Optional[bytes] = None, seal=False) -> None:
12-
super().__init__(memory_fd_create(), 'r+')
11+
def _make_fd_readonly(fd):
12+
new_fd = os.open(f'/proc/self/fd/{fd}', os.O_RDONLY)
13+
try:
14+
os.dup2(new_fd, fd)
15+
finally:
16+
os.close(new_fd)
17+
18+
19+
class MmapableIO(io.FileIO, metaclass=ABCMeta):
20+
def __init__(self, fd, *, prefill: Optional[bytes] = None, seal=False) -> None:
21+
super().__init__(fd, 'r+')
22+
1323
if prefill:
1424
self.write(prefill)
1525
if seal:
1626
self.seal()
1727

18-
def seal(self) -> None:
19-
fd = self.fileno()
20-
try:
21-
memory_fd_seal(fd)
22-
except OSError as e:
23-
if e.errno == errno.ENOSYS:
24-
# FreeBSD
25-
self.seek(0, os.SEEK_SET)
26-
return
27-
raise
28+
@classmethod
29+
@abstractmethod
30+
def usable_with_name(cls) -> bool:
31+
...
2832

29-
new_fd = os.open(f'/proc/self/fd/{fd}', os.O_RDONLY)
30-
try:
31-
os.dup2(new_fd, fd)
32-
finally:
33-
os.close(new_fd)
33+
@abstractmethod
34+
def seal(self) -> None:
35+
...
3436

37+
@abstractmethod
3538
def to_path(self) -> str:
36-
return f'/proc/{os.getpid()}/fd/{self.fileno()}'
39+
...
3740

3841
def to_bytes(self) -> bytes:
3942
try:
@@ -43,3 +46,71 @@ def to_bytes(self) -> bytes:
4346
if e.args[0] == 'cannot mmap an empty file':
4447
return b''
4548
raise
49+
50+
51+
class NamedFileIO(MmapableIO):
52+
_name: str
53+
54+
def __init__(self, *, prefill: Optional[bytes] = None, seal=False) -> None:
55+
with NamedTemporaryFile(delete=False) as f:
56+
self._name = f.name
57+
super().__init__(os.dup(f.fileno()), prefill=prefill, seal=seal)
58+
59+
def seal(self) -> None:
60+
self.seek(0, os.SEEK_SET)
61+
62+
def close(self) -> None:
63+
super().close()
64+
os.unlink(self._name)
65+
66+
def to_path(self) -> str:
67+
return self._name
68+
69+
@classmethod
70+
def usable_with_name(cls):
71+
return True
72+
73+
74+
class UnnamedFileIO(MmapableIO):
75+
def __init__(self, *, prefill: Optional[bytes] = None, seal=False) -> None:
76+
with TemporaryFile() as f:
77+
super().__init__(os.dup(f.fileno()), prefill=prefill, seal=seal)
78+
79+
def seal(self) -> None:
80+
self.seek(0, os.SEEK_SET)
81+
_make_fd_readonly(self.fileno())
82+
83+
def to_path(self) -> str:
84+
return f'/proc/{os.getpid()}/fd/{self.fileno()}'
85+
86+
@classmethod
87+
def usable_with_name(cls):
88+
with cls() as f:
89+
return os.path.exists(f.to_path())
90+
91+
92+
class MemfdIO(MmapableIO):
93+
def __init__(self, *, prefill: Optional[bytes] = None, seal=False) -> None:
94+
super().__init__(memfd_create(), prefill=prefill, seal=seal)
95+
96+
def seal(self) -> None:
97+
fd = self.fileno()
98+
memfd_seal(fd)
99+
_make_fd_readonly(fd)
100+
101+
def to_path(self) -> str:
102+
return f'/proc/{os.getpid()}/fd/{self.fileno()}'
103+
104+
@classmethod
105+
def usable_with_name(cls):
106+
try:
107+
with cls() as f:
108+
return os.path.exists(f.to_path())
109+
except OSError:
110+
return False
111+
112+
113+
# Try to use memfd if possible, otherwise fallback to unlinked temporary files
114+
# (UnnamedFileIO). On FreeBSD and some other systems, /proc/[pid]/fd doesn't
115+
# exist, so to_path() will not work. We fall back to NamedFileIO in that case.
116+
MemoryIO = next((i for i in (MemfdIO, UnnamedFileIO, NamedFileIO) if i.usable_with_name()))

dmoj/problem.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from dmoj import checkers
3131
from dmoj.checkers import Checker
3232
from dmoj.config import ConfigNode, InvalidInitException
33-
from dmoj.cptbox.utils import MemoryIO
33+
from dmoj.cptbox.utils import MemoryIO, MmapableIO
3434
from dmoj.judgeenv import env, get_problem_root
3535
from dmoj.utils.helper_files import compile_with_auxiliary_files, parse_helper_file_error
3636
from dmoj.utils.module import load_module_from_file
@@ -275,7 +275,7 @@ def open(self, key: str):
275275
return self.archive.open(zipinfo)
276276
raise KeyError('file "%s" could not be found in "%s"' % (key, self.problem_root_dir))
277277

278-
def as_fd(self, key: str, normalize: bool = False) -> MemoryIO:
278+
def as_fd(self, key: str, normalize: bool = False) -> MmapableIO:
279279
memory = MemoryIO()
280280
with self.open(key) as f:
281281
if normalize:
@@ -344,7 +344,7 @@ class TestCase(BaseTestCase):
344344
batch: int
345345
output_prefix_length: int
346346
has_binary_data: bool
347-
_input_data_fd: Optional[MemoryIO]
347+
_input_data_fd: Optional[MmapableIO]
348348
_generated: Optional[Tuple[bytes, bytes]]
349349

350350
def __init__(self, count: int, batch_no: int, config: ConfigNode, problem: Problem):
@@ -451,14 +451,14 @@ def _run_generator(self, gen: Union[str, ConfigNode], args: Optional[Iterable[st
451451
def input_data(self) -> bytes:
452452
return self.input_data_fd().to_bytes()
453453

454-
def input_data_fd(self) -> MemoryIO:
454+
def input_data_fd(self) -> MmapableIO:
455455
if self._input_data_fd:
456456
return self._input_data_fd
457457

458458
result = self._input_data_fd = self._make_input_data_fd()
459459
return result
460460

461-
def _make_input_data_fd(self) -> MemoryIO:
461+
def _make_input_data_fd(self) -> MmapableIO:
462462
gen = self.config.generator
463463

464464
# don't try running the generator if we specify an output file explicitly,

0 commit comments

Comments
 (0)