Skip to content

Commit

Permalink
Initial implementation of landlock calls
Browse files Browse the repository at this point in the history
  • Loading branch information
Riolku committed Sep 11, 2021
1 parent 757da7d commit 394de48
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ $ pip install -e .
Several environment variables can be specified to control the compilation of the sandbox:

* `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).
This flag has no effect when building outside of Linux.
* `DMOJ_USE_LANDLOCK`; set it to `yes` if you're building on Linux after `5.13` to enable `landlock` sandboxing, which is faster.
* `DMOJ_TARGET_ARCH`; use it to override the default architecture specified for compiling the sandbox (via `-march`).
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).

Expand Down
33 changes: 33 additions & 0 deletions dmoj/cptbox/_cptbox.pyx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# cython: language_level=3
from cpython.exc cimport PyErr_NoMemory, PyErr_SetFromErrno
from cpython.ref cimport PyObject
from libc.stdio cimport FILE, fopen, fclose, fgets, sprintf
from libc.stdlib cimport malloc, free, strtoul
from libc.string cimport strncmp, strlen
Expand Down Expand Up @@ -68,9 +69,12 @@ cdef extern from 'ptbox.h' nogil:
bint was_initialized()
bool use_seccomp()
bool use_seccomp(bool enabled)
bool use_landlock()
bool use_landlock(bool enabled)

cdef bint PTBOX_FREEBSD
cdef bint PTBOX_SECCOMP
cdef bint PTBOX_LANDLOCK
cdef int MAX_SYSCALL

cdef int PTBOX_EVENT_ATTACH
Expand Down Expand Up @@ -121,6 +125,13 @@ cdef extern from 'helper.h' nogil:
bool use_seccomp
int abi_for_seccomp
int *seccomp_handlers
bool use_landlock
char** read_exact_files
char** read_exact_dirs
char** read_recursive_dirs
char** write_exact_files
char** write_exact_dirs
char** write_recursive_dirs

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

config.use_landlock = self._use_landlock()
if config.use_landlock:
config.read_exact_files = alloc_byte_array(self.read_exact_files)
config.read_exact_dirs = alloc_byte_array(self.read_exact_dirs)
config.read_recursive_dirs = alloc_byte_array(self.read_recursive_dirs)
config.write_exact_files = alloc_byte_array(self.write_exact_files)
config.write_exact_dirs = alloc_byte_array(self.write_exact_dirs)
config.write_recursive_dirs = alloc_byte_array(self.write_recursive_dirs)


if self.process.spawn(pt_child, &config):
raise RuntimeError('failed to spawn child')
finally:
Expand Down Expand Up @@ -537,6 +558,18 @@ cdef class Process:
if not self.process.use_seccomp(enabled):
raise RuntimeError("Can't change whether seccomp is used after process is created.")

cdef inline bool _use_landlock(self):
return self.process.use_landlock()

@property
def use_landlock(self):
return self.process.use_landlock()

@use_landlock.setter
def use_landlock(self, bool enabled):
if not self.process.use_landlock(enabled):
raise RuntimeError("Can't change whether landlock is used after process is created.")

@property
def was_initialized(self):
return self.process.was_initialized()
Expand Down
53 changes: 53 additions & 0 deletions dmoj/cptbox/helper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
# define FD_DIR "/proc/self/fd"
#endif

#if PTBOX_LANDLOCK
# include "landlock_helpers.h"
#endif

inline void setrlimit2(int resource, rlim_t cur, rlim_t max) {
rlimit limit;
limit.rlim_cur = cur;
Expand Down Expand Up @@ -117,6 +121,55 @@ int cptbox_child_run(const struct child_config *config) {
}
#endif

#if PTBOX_LANDLOCK
int ruleset_fd;
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs =
LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_REMOVE_DIR |
LANDLOCK_ACCESS_FS_REMOVE_FILE |
LANDLOCK_ACCESS_FS_MAKE_CHAR |
LANDLOCK_ACCESS_FS_MAKE_DIR |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_MAKE_SOCK |
LANDLOCK_ACCESS_FS_MAKE_FIFO |
LANDLOCK_ACCESS_FS_MAKE_BLOCK |
LANDLOCK_ACCESS_FS_MAKE_SYM,
};

ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0) {
perror("Failed to create a ruleset");
return PTBOX_SPAWN_FAIL_LANDLOCK;
}

#define READ_EXACT_FILE_RULE LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE
#define READ_EXACT_DIR_RULE LANDLOCK_ACCESS_FS_READ_DIR
#define READ_RECURSIVE_DIR_RULE READ_EXACT_FILE_RULE | READ_EXACT_DIR_RULE
#define WRITE_EXACT_FILE_RULE LANDLOCK_ACCESS_FS_WRITE_FILE
#define WRITE_EXACT_DIR_RULE LANDLOCK_ACCESS_FS_READ_DIR
#define WRITE_RECURSIVE_DIR_RULE WRITE_EXACT_FILE_RULE | WRITE_EXACT_DIR_RULE

if(
add_rules(ruleset_fd, config->read_exact_files, READ_EXACT_FILE_RULE) ||
add_rules(ruleset_fd, config->read_exact_dirs, READ_EXACT_DIR_RULE) ||
add_rules(ruleset_fd, config->read_recursive_dirs, READ_RECURSIVE_DIR_RULE) ||
add_rules(ruleset_fd, config->write_exact_files, WRITE_EXACT_FILE_RULE) ||
add_rules(ruleset_fd, config->write_exact_dirs, WRITE_EXACT_DIR_RULE) ||
add_rules(ruleset_fd, config->write_recursive_dirs, WRITE_RECURSIVE_DIR_RULE)
) {
return PTBOX_SPAWN_FAIL_LANDLOCK;
}

if(landlock_restrict_self(ruleset_fd, 0)) {
perror("Failed to enforce ruleset");
return PTBOX_SPAWN_FAIL_LANDLOCK;
}
#endif

// All these limits should be dropped after initializing seccomp, since seccomp allocates
// memory, and if an arena isn't sufficiently free it could force seccomp into an OOM
// situation where we'd fail to initialize.
Expand Down
8 changes: 8 additions & 0 deletions dmoj/cptbox/helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#define PTBOX_SPAWN_FAIL_SECCOMP 203
#define PTBOX_SPAWN_FAIL_TRACEME 204
#define PTBOX_SPAWN_FAIL_EXECVE 205
#define PTBOX_SPAWN_FAIL_LANDLOCK 206

struct child_config {
unsigned long memory;
Expand All @@ -23,6 +24,13 @@ struct child_config {
int stderr_;
bool use_seccomp;
int *seccomp_handlers;
bool use_landlock;
char** read_exact_files;
char** read_exact_dirs;
char** read_recursive_dirs;
char** write_exact_files;
char** write_exact_dirs;
char** write_recursive_dirs;
};

void cptbox_closefrom(int lowfd);
Expand Down
Empty file added dmoj/cptbox/landlock.h
Empty file.
58 changes: 58 additions & 0 deletions dmoj/cptbox/landlock_helper.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include "landlock_helpers.h"

#if PTBOX_LANDLOCK
#include <linux/landlock.h>

int add_rules(const int ruleset_fd, const char* const* const paths, __u64 access_rule) {
struct landlock_path_beneath_attr path_beneath = {
.parent_fd = -1,
.allowed_access = access_rule,
};

for(const char* const* pathptr = paths; *pathptr; pathptr++) {
path_beneath.parent_fd = open(*pathptr, O_PATH | O_CLOEXEC);
if(path_beneath.parent_fd < 0) {
perror("Failed to open path for rule");
return -1;
}
if(landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0)) {
perror("Failed to add rule to ruleset");
return -1;
}
if(close(path_beneath.parent_fd)) {
// Not Fatal: we have a CLOEXEC flag
perror("Failed to close path for rule");
}
}

return 0;
}

#ifndef landlock_create_ruleset
int landlock_create_ruleset(
const struct landlock_ruleset_attr *const attr,
const size_t size, const __u32 flags)
{
return syscall(__NR_landlock_create_ruleset, attr, size, flags);
}
#endif

#ifndef landlock_add_rule
int landlock_add_rule(const int ruleset_fd,
const enum landlock_rule_type rule_type,
const void *const rule_attr, const __u32 flags)
{
return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type,
rule_attr, flags);
}
#endif

#ifndef landlock_restrict_self
int landlock_restrict_self(const int ruleset_fd,
const __u32 flags)
{
return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
}
#endif

#endif
3 changes: 3 additions & 0 deletions dmoj/cptbox/landlock_helpers.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#include <linux/types.h>

int add_rules(const int ruleset_fd, const char* const* const paths, __u64 access_rule);
9 changes: 9 additions & 0 deletions dmoj/cptbox/ptbox.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
#include <seccomp.h>
#endif

#ifdef PTBOX_USE_LANDLOCK
#define PTBOX_LANDLOCK 1
#else
#define PTBOX_LANDLOCK 0
#endif

#if PTBOX_FREEBSD
#include "ext_freebsd.h"
#else
Expand Down Expand Up @@ -125,6 +131,8 @@ class pt_process {
bool was_initialized() { return _initialized; }
bool use_seccomp() { return _use_seccomp; }
bool use_seccomp(bool enable);
bool use_landlock() { return _use_landlock; }
bool use_landlock(bool enable);
protected:
int dispatch(int event, unsigned long param);
int protection_fault(int syscall, int type = PTBOX_EVENT_PROTECTION);
Expand All @@ -141,6 +149,7 @@ class pt_process {
bool _trace_syscalls;
bool _initialized;
bool _use_seccomp;
bool _use_landlock;
};

class pt_debugger {
Expand Down
9 changes: 9 additions & 0 deletions dmoj/cptbox/ptproc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ bool pt_process::use_seccomp(bool enabled) {
return true;
}

bool pt_process::use_landlock(bool enabled) {
if (pid) {
// Do not allow updates after the process is spawned.
return false;
}
_use_landlock = PTBOX_LANDLOCK && enabled;
return true;
}

int pt_process::monitor() {
bool in_syscall = false, first = true, spawned = false;
struct timespec start, end, delta;
Expand Down
25 changes: 25 additions & 0 deletions dmoj/cptbox/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import Callable, Dict, List, Optional, Tuple, Type

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

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

_address_bits = {
PTBOX_ABI_X86: 32,
Expand Down Expand Up @@ -98,8 +100,11 @@ def __init__(
self,
args: List[bytes],
*,
read_fs: List[FilesystemAccessRule] = [],
write_fs: List[FilesystemAccessRule] = [],
executable: bytes,
avoid_seccomp: bool = False,
avoid_landlock: bool = False,
security=None,
time: int = 0,
memory: int = 0,
Expand All @@ -117,11 +122,16 @@ def __init__(
) -> None:
self._executable = executable
self.use_seccomp = security is not None and not avoid_seccomp
self.use_landlock = security is not None and not avoid_landlock

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

if self.use_landlock and BAD_LANDLOCK:
log.warning(f'Requires Linux 5.13+ to use landlock, you have: {os.uname().release}')
self.use_landlock = False

self._args = args
self._chdir = cwd
self._env = [
Expand Down Expand Up @@ -164,6 +174,9 @@ def __init__(
handler = _CALLBACK
self._handler(abi, call, handler)

if self.use_landlock:
self.load_files(read_fs, write_fs)

self._died = threading.Event()
self._spawned_or_errored = threading.Event()
self._spawn_error = None
Expand Down Expand Up @@ -201,6 +214,18 @@ def _get_seccomp_handlers(self) -> List[int]:
handlers[call] = handler.errno
return handlers

def load_files(self, read_fs, write_fs) -> None:
def filter(source: List[FilesystemAccessRule], type) -> List[type]:
return [rule for rule in source if isinstance(rule, type)]

self.read_exact_files = filter(read_fs, ExactFile)
self.read_exact_dirs = filter(read_fs, ExactDir)
self.read_recursive_dirs = filter(read_fs, RecursiveDir)

self.write_exact_files = filter(write_fs, ExactFile)
self.write_exact_dirs = filter(write_fs, ExactDir)
self.write_recursive_dirs = filter(write_fs, RecursiveDir)

def wait(self) -> int:
self._died.wait()
assert self.returncode is not None
Expand Down
2 changes: 2 additions & 0 deletions dmoj/executors/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ def launch(self, *args, **kwargs):

return TracedPopen(
[utf8bytes(a) for a in self.get_cmdline(**kwargs) + list(args)],
read_fs=self.get_fs(),
write_fs=self.get_write_fs(),
executable=utf8bytes(self.get_executable()),
security=self.get_security(launch_kwargs=kwargs),
address_grace=self.get_address_grace(),
Expand Down
5 changes: 5 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

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

if has_landlock:
macros.append(('PTBOX_USE_LANDLOCK', None))

extensions = [
Extension('dmoj.checkers._checker', sources=['dmoj/checkers/_checker.c']),
Extension('dmoj.cptbox._cptbox', sources=cptbox_sources, language='c++', libraries=libs, define_macros=macros),
Expand Down

0 comments on commit 394de48

Please sign in to comment.