Skip to content

Commit 0c95e3d

Browse files
committed
Initial implementation of landlock calls
1 parent 757da7d commit 0c95e3d

12 files changed

+207
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ $ pip install -e .
108108
Several environment variables can be specified to control the compilation of the sandbox:
109109

110110
* `DMOJ_USE_SECCOMP`; set it to `no` if you're building on a pre-Linux 3.5 kernel to disable `seccomp` filtering in favour of pure `ptrace` (slower).
111-
This flag has no effect when building outside of Linux.
111+
* `DMOJ_USE_LANDLOCK`; set it to `yes` if you're building on Linux after `5.13` to enable `landlock` sandboxing, which is faster.
112112
* `DMOJ_TARGET_ARCH`; use it to override the default architecture specified for compiling the sandbox (via `-march`).
113113
Usually this is `native`, but will not be specified on ARM unless `DMOJ_TARGET_ARCH` is set (a generic, slow build will be compiled instead).
114114

dmoj/cptbox/_cptbox.pyx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# cython: language_level=3
22
from cpython.exc cimport PyErr_NoMemory, PyErr_SetFromErrno
3+
from cpython.ref cimport PyObject
34
from libc.stdio cimport FILE, fopen, fclose, fgets, sprintf
45
from libc.stdlib cimport malloc, free, strtoul
56
from libc.string cimport strncmp, strlen
@@ -68,9 +69,12 @@ cdef extern from 'ptbox.h' nogil:
6869
bint was_initialized()
6970
bool use_seccomp()
7071
bool use_seccomp(bool enabled)
72+
bool use_landlock()
73+
bool use_landlock(bool enabled)
7174

7275
cdef bint PTBOX_FREEBSD
7376
cdef bint PTBOX_SECCOMP
77+
cdef bint PTBOX_LANDLOCK
7478
cdef int MAX_SYSCALL
7579

7680
cdef int PTBOX_EVENT_ATTACH
@@ -121,6 +125,13 @@ cdef extern from 'helper.h' nogil:
121125
bool use_seccomp
122126
int abi_for_seccomp
123127
int *seccomp_handlers
128+
bool use_landlock
129+
char** read_exact_files
130+
char** read_exact_dirs
131+
char** read_recursive_dirs
132+
char** write_exact_files
133+
char** write_exact_dirs
134+
char** write_recursive_dirs
124135

125136
void cptbox_closefrom(int lowfd)
126137
int cptbox_child_run(child_config *)
@@ -510,6 +521,16 @@ cdef class Process:
510521
for i in range(MAX_SYSCALL):
511522
config.seccomp_handlers[i] = handlers[i]
512523

524+
config.use_landlock = self._use_landlock()
525+
if config.use_landlock:
526+
config.read_exact_files = alloc_byte_array(self.read_exact_files)
527+
config.read_exact_dirs = alloc_byte_array(self.read_exact_dirs)
528+
config.read_recursive_dirs = alloc_byte_array(self.read_recursive_dirs)
529+
config.write_exact_files = alloc_byte_array(self.write_exact_files)
530+
config.write_exact_dirs = alloc_byte_array(self.write_exact_dirs)
531+
config.write_recursive_dirs = alloc_byte_array(self.write_recursive_dirs)
532+
533+
513534
if self.process.spawn(pt_child, &config):
514535
raise RuntimeError('failed to spawn child')
515536
finally:
@@ -537,6 +558,18 @@ cdef class Process:
537558
if not self.process.use_seccomp(enabled):
538559
raise RuntimeError("Can't change whether seccomp is used after process is created.")
539560

561+
cdef inline bool _use_landlock(self):
562+
return self.process.use_landlock()
563+
564+
@property
565+
def use_landlock(self):
566+
return self.process.use_landlock()
567+
568+
@use_landlock.setter
569+
def use_landlock(self, bool enabled):
570+
if not self.process.use_landlock(enabled):
571+
raise RuntimeError("Can't change whether landlock is used after process is created.")
572+
540573
@property
541574
def was_initialized(self):
542575
return self.process.was_initialized()

dmoj/cptbox/helper.cpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
# define FD_DIR "/proc/self/fd"
3131
#endif
3232

33+
#if PTBOX_LANDLOCK
34+
# include "landlock_helpers.h"
35+
#endif
36+
3337
inline void setrlimit2(int resource, rlim_t cur, rlim_t max) {
3438
rlimit limit;
3539
limit.rlim_cur = cur;
@@ -117,6 +121,55 @@ int cptbox_child_run(const struct child_config *config) {
117121
}
118122
#endif
119123

124+
#if PTBOX_LANDLOCK
125+
int ruleset_fd;
126+
struct landlock_ruleset_attr ruleset_attr = {
127+
.handled_access_fs =
128+
LANDLOCK_ACCESS_FS_EXECUTE |
129+
LANDLOCK_ACCESS_FS_WRITE_FILE |
130+
LANDLOCK_ACCESS_FS_READ_FILE |
131+
LANDLOCK_ACCESS_FS_READ_DIR |
132+
LANDLOCK_ACCESS_FS_REMOVE_DIR |
133+
LANDLOCK_ACCESS_FS_REMOVE_FILE |
134+
LANDLOCK_ACCESS_FS_MAKE_CHAR |
135+
LANDLOCK_ACCESS_FS_MAKE_DIR |
136+
LANDLOCK_ACCESS_FS_MAKE_REG |
137+
LANDLOCK_ACCESS_FS_MAKE_SOCK |
138+
LANDLOCK_ACCESS_FS_MAKE_FIFO |
139+
LANDLOCK_ACCESS_FS_MAKE_BLOCK |
140+
LANDLOCK_ACCESS_FS_MAKE_SYM,
141+
};
142+
143+
ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
144+
if (ruleset_fd < 0) {
145+
perror("Failed to create a ruleset");
146+
return PTBOX_SPAWN_FAIL_LANDLOCK;
147+
}
148+
149+
#define READ_EXACT_FILE_RULE LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE
150+
#define READ_EXACT_DIR_RULE LANDLOCK_ACCESS_FS_READ_DIR
151+
#define READ_RECURSIVE_DIR_RULE READ_EXACT_FILE_RULE | READ_EXACT_DIR_RULE
152+
#define WRITE_EXACT_FILE_RULE LANDLOCK_ACCESS_FS_WRITE_FILE
153+
#define WRITE_EXACT_DIR_RULE LANDLOCK_ACCESS_FS_READ_DIR
154+
#define WRITE_RECURSIVE_DIR_RULE WRITE_EXACT_FILE_RULE | WRITE_EXACT_DIR_RULE
155+
156+
if(
157+
add_rules(ruleset_fd, config->read_exact_files, READ_EXACT_FILE_RULE) ||
158+
add_rules(ruleset_fd, config->read_exact_dirs, READ_EXACT_DIR_RULE) ||
159+
add_rules(ruleset_fd, config->read_recursive_dirs, READ_RECURSIVE_DIR_RULE) ||
160+
add_rules(ruleset_fd, config->write_exact_files, WRITE_EXACT_FILE_RULE) ||
161+
add_rules(ruleset_fd, config->write_exact_dirs, WRITE_EXACT_DIR_RULE) ||
162+
add_rules(ruleset_fd, config->write_recursive_dirs, WRITE_RECURSIVE_DIR_RULE)
163+
) {
164+
return PTBOX_SPAWN_FAIL_LANDLOCK;
165+
}
166+
167+
if(landlock_restrict_self(ruleset_fd, 0)) {
168+
perror("Failed to enforce ruleset");
169+
return PTBOX_SPAWN_FAIL_LANDLOCK;
170+
}
171+
#endif
172+
120173
// All these limits should be dropped after initializing seccomp, since seccomp allocates
121174
// memory, and if an arena isn't sufficiently free it could force seccomp into an OOM
122175
// situation where we'd fail to initialize.

dmoj/cptbox/helper.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#define PTBOX_SPAWN_FAIL_SECCOMP 203
77
#define PTBOX_SPAWN_FAIL_TRACEME 204
88
#define PTBOX_SPAWN_FAIL_EXECVE 205
9+
#define PTBOX_SPAWN_FAIL_LANDLOCK 206
910

1011
struct child_config {
1112
unsigned long memory;
@@ -23,6 +24,13 @@ struct child_config {
2324
int stderr_;
2425
bool use_seccomp;
2526
int *seccomp_handlers;
27+
bool use_landlock;
28+
char** read_exact_files;
29+
char** read_exact_dirs;
30+
char** read_recursive_dirs;
31+
char** write_exact_files;
32+
char** write_exact_dirs;
33+
char** write_recursive_dirs;
2634
};
2735

2836
void cptbox_closefrom(int lowfd);

dmoj/cptbox/landlock.h

Whitespace-only changes.

dmoj/cptbox/landlock_helper.c

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#include "landlock_helpers.h"
2+
3+
#if PTBOX_LANDLOCK
4+
#include <linux/landlock.h>
5+
6+
int add_rules(const int ruleset_fd, const char* const* const paths, __u64 access_rule) {
7+
struct landlock_path_beneath_attr path_beneath = {
8+
.parent_fd = -1,
9+
.allowed_access = access_rule,
10+
};
11+
12+
for(const char* const* pathptr = paths; *pathptr; pathptr++) {
13+
path_beneath.parent_fd = open(*pathptr, O_PATH | O_CLOEXEC);
14+
if(path_beneath.parent_fd < 0) {
15+
perror("Failed to open path for rule");
16+
return -1;
17+
}
18+
if(landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0)) {
19+
perror("Failed to add rule to ruleset");
20+
return -1;
21+
}
22+
if(close(path_beneath.parent_fd)) {
23+
// Not Fatal: we have a CLOEXEC flag
24+
perror("Failed to close path for rule");
25+
}
26+
}
27+
28+
return 0;
29+
}
30+
31+
#ifndef landlock_create_ruleset
32+
int landlock_create_ruleset(
33+
const struct landlock_ruleset_attr *const attr,
34+
const size_t size, const __u32 flags)
35+
{
36+
return syscall(__NR_landlock_create_ruleset, attr, size, flags);
37+
}
38+
#endif
39+
40+
#ifndef landlock_add_rule
41+
int landlock_add_rule(const int ruleset_fd,
42+
const enum landlock_rule_type rule_type,
43+
const void *const rule_attr, const __u32 flags)
44+
{
45+
return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type,
46+
rule_attr, flags);
47+
}
48+
#endif
49+
50+
#ifndef landlock_restrict_self
51+
int landlock_restrict_self(const int ruleset_fd,
52+
const __u32 flags)
53+
{
54+
return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
55+
}
56+
#endif
57+
58+
#endif

dmoj/cptbox/landlock_helpers.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#include <linux/types.h>
2+
3+
int add_rules(const int ruleset_fd, const char* const* const paths, __u64 access_rule);

dmoj/cptbox/ptbox.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@
2929
#include <seccomp.h>
3030
#endif
3131

32+
#ifdef PTBOX_USE_LANDLOCK
33+
#define PTBOX_LANDLOCK 1
34+
#else
35+
#define PTBOX_LANDLOCK 0
36+
#endif
37+
3238
#if PTBOX_FREEBSD
3339
#include "ext_freebsd.h"
3440
#else
@@ -125,6 +131,8 @@ class pt_process {
125131
bool was_initialized() { return _initialized; }
126132
bool use_seccomp() { return _use_seccomp; }
127133
bool use_seccomp(bool enable);
134+
bool use_landlock() { return _use_landlock; }
135+
bool use_landlock(bool enable);
128136
protected:
129137
int dispatch(int event, unsigned long param);
130138
int protection_fault(int syscall, int type = PTBOX_EVENT_PROTECTION);
@@ -141,6 +149,7 @@ class pt_process {
141149
bool _trace_syscalls;
142150
bool _initialized;
143151
bool _use_seccomp;
152+
bool _use_landlock;
144153
};
145154

146155
class pt_debugger {

dmoj/cptbox/ptproc.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ bool pt_process::use_seccomp(bool enabled) {
105105
return true;
106106
}
107107

108+
bool pt_process::use_landlock(bool enabled) {
109+
if (pid) {
110+
// Do not allow updates after the process is spawned.
111+
return false;
112+
}
113+
_use_landlock = PTBOX_LANDLOCK && enabled;
114+
return true;
115+
}
116+
108117
int pt_process::monitor() {
109118
bool in_syscall = false, first = true, spawned = false;
110119
struct timespec start, end, delta;

dmoj/cptbox/tracer.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import Callable, Dict, List, Optional, Tuple, Type
1010

1111
from dmoj.cptbox._cptbox import *
12+
from dmoj.cptbox.filesystem_policies import ExactDir, ExactFile, FilesystemAccessRule, RecursiveDir
1213
from dmoj.cptbox.handlers import ALLOW, DISALLOW, ErrnoHandlerCallback, _CALLBACK
1314
from dmoj.cptbox.syscalls import SYSCALL_COUNT, by_id, sys_exit, sys_exit_group, sys_getpid, translator
1415
from dmoj.utils.communicate import safe_communicate as _safe_communicate
@@ -30,6 +31,7 @@
3031

3132
FREEBSD = sys.platform.startswith('freebsd')
3233
BAD_SECCOMP = sys.platform == 'linux' and tuple(map(int, os.uname().release.partition('-')[0].split('.'))) < (4, 8)
34+
BAD_LANDLOCK = sys.platform == 'linux' and tuple(map(int, os.uname().release.partition('-')[0].split('.'))) < (5, 13)
3335

3436
_address_bits = {
3537
PTBOX_ABI_X86: 32,
@@ -98,8 +100,11 @@ def __init__(
98100
self,
99101
args: List[bytes],
100102
*,
103+
read_fs: List[FilesystemAccessRule] = [],
104+
write_fs: List[FilesystemAccessRule] = [],
101105
executable: bytes,
102106
avoid_seccomp: bool = False,
107+
avoid_landlock: bool = False,
103108
security=None,
104109
time: int = 0,
105110
memory: int = 0,
@@ -117,11 +122,16 @@ def __init__(
117122
) -> None:
118123
self._executable = executable
119124
self.use_seccomp = security is not None and not avoid_seccomp
125+
self.use_landlock = security is not None and not avoid_landlock
120126

121127
if self.use_seccomp and BAD_SECCOMP:
122128
log.warning('Requires Linux 4.8+ to use seccomp, you have: %s', os.uname().release)
123129
self.use_seccomp = False
124130

131+
if self.use_landlock and BAD_LANDLOCK:
132+
log.warning(f'Requires Linux 5.13+ to use landlock, you have: {os.uname().release}')
133+
self.use_landlock = False
134+
125135
self._args = args
126136
self._chdir = cwd
127137
self._env = [
@@ -164,6 +174,9 @@ def __init__(
164174
handler = _CALLBACK
165175
self._handler(abi, call, handler)
166176

177+
if self.use_landlock:
178+
self.load_files(read_fs, write_fs)
179+
167180
self._died = threading.Event()
168181
self._spawned_or_errored = threading.Event()
169182
self._spawn_error = None
@@ -201,6 +214,19 @@ def _get_seccomp_handlers(self) -> List[int]:
201214
handlers[call] = handler.errno
202215
return handlers
203216

217+
218+
def load_files(self, read_fs, write_fs) -> None:
219+
def filter(source: List[FilesystemAccessRule], type) -> List[type]:
220+
return [rule for rule in source if isinstance(rule, type)]
221+
222+
self.read_exact_files = filter(read_fs, ExactFile)
223+
self.read_exact_dirs = filter(read_fs, ExactDir)
224+
self.read_recursive_dirs = filter(read_fs, RecursiveDir)
225+
226+
self.write_exact_files = filter(write_fs, ExactFile)
227+
self.write_exact_dirs = filter(write_fs, ExactDir)
228+
self.write_recursive_dirs = filter(write_fs, RecursiveDir)
229+
204230
def wait(self) -> int:
205231
self._died.wait()
206232
assert self.returncode is not None

dmoj/executors/mixins.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ def launch(self, *args, **kwargs):
158158

159159
return TracedPopen(
160160
[utf8bytes(a) for a in self.get_cmdline(**kwargs) + list(args)],
161+
read_fs=self.get_fs(),
162+
write_fs=self.get_write_fs(),
161163
executable=utf8bytes(self.get_executable()),
162164
security=self.get_security(launch_kwargs=kwargs),
163165
address_grace=self.get_address_grace(),

setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
# Allow manually disabling seccomp on old kernels. WSL 1 doesn't have seccomp.
2626
has_seccomp = sys.platform.startswith('linux') and not is_wsl1 and os.environ.get('DMOJ_USE_SECCOMP') != 'no'
27+
# Must explicitly enable landlock
28+
has_landlock = sys.platform.startswith('linux') and os.environ.get('DMOJ_USE_LANDLOCK') == 'yes'
2729
try:
2830
parallel = int(os.environ['DMOJ_PARALLEL'])
2931
except (KeyError, ValueError):
@@ -169,6 +171,9 @@ def unavailable(self, e):
169171
print('*' * 79)
170172
macros.append(('PTBOX_NO_SECCOMP', None))
171173

174+
if has_landlock:
175+
macros.append(('PTBOX_USE_LANDLOCK', None))
176+
172177
extensions = [
173178
Extension('dmoj.checkers._checker', sources=['dmoj/checkers/_checker.c']),
174179
Extension('dmoj.cptbox._cptbox', sources=cptbox_sources, language='c++', libraries=libs, define_macros=macros),

0 commit comments

Comments
 (0)