From 3c7abc1126682d6030fc2c8f8df49a64bbe525cc Mon Sep 17 00:00:00 2001 From: Keenan Gugeler Date: Thu, 10 Mar 2022 00:56:30 -0500 Subject: [PATCH 1/8] cptbox: add landlock helpers A helper header is provided so as to avoid issues with missing headers, so we can compile unconditionally. --- dmoj/cptbox/landlock_header.h | 76 ++++++++++++++++++++++++++++++++ dmoj/cptbox/landlock_helpers.cpp | 42 ++++++++++++++++++ dmoj/cptbox/landlock_helpers.h | 3 ++ setup.py | 1 + 4 files changed, 122 insertions(+) create mode 100644 dmoj/cptbox/landlock_header.h create mode 100644 dmoj/cptbox/landlock_helpers.cpp create mode 100644 dmoj/cptbox/landlock_helpers.h diff --git a/dmoj/cptbox/landlock_header.h b/dmoj/cptbox/landlock_header.h new file mode 100644 index 000000000..c0fd74610 --- /dev/null +++ b/dmoj/cptbox/landlock_header.h @@ -0,0 +1,76 @@ +#pragma once + +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +/* + * Landlock - User space API + * + * Copyright © 2017-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + */ +#if defined(_LINUX_LANDLOCK_H) +#elif __has_include() +#include +#else +typedef unsigned long long __u64; +typedef __signed__ int __s32; +typedef unsigned int __u32; + +struct landlock_ruleset_attr { + __u64 handled_access_fs; +}; +#define LANDLOCK_CREATE_RULESET_VERSION (1U << 0) +enum landlock_rule_type { + LANDLOCK_RULE_PATH_BENEATH = 1, +}; +struct landlock_path_beneath_attr { + __u64 allowed_access; + __s32 parent_fd; +} __attribute__((packed)); +#define LANDLOCK_ACCESS_FS_EXECUTE (1ULL << 0) +#define LANDLOCK_ACCESS_FS_WRITE_FILE (1ULL << 1) +#define LANDLOCK_ACCESS_FS_READ_FILE (1ULL << 2) +#define LANDLOCK_ACCESS_FS_READ_DIR (1ULL << 3) +#define LANDLOCK_ACCESS_FS_REMOVE_DIR (1ULL << 4) +#define LANDLOCK_ACCESS_FS_REMOVE_FILE (1ULL << 5) +#define LANDLOCK_ACCESS_FS_MAKE_CHAR (1ULL << 6) +#define LANDLOCK_ACCESS_FS_MAKE_DIR (1ULL << 7) +#define LANDLOCK_ACCESS_FS_MAKE_REG (1ULL << 8) +#define LANDLOCK_ACCESS_FS_MAKE_SOCK (1ULL << 9) +#define LANDLOCK_ACCESS_FS_MAKE_FIFO (1ULL << 10) +#define LANDLOCK_ACCESS_FS_MAKE_BLOCK (1ULL << 11) +#define LANDLOCK_ACCESS_FS_MAKE_SYM (1ULL << 12) +#endif /* _LINUX_LANDLOCK_H */ + +// Not always defined, depends on ABI version. +#define LANDLOCK_ACCESS_FS_REFER (1ULL << 13) + +#include +#ifndef __NR_landlock_create_ruleset +#define __NR_landlock_create_ruleset 444 +#endif +#ifndef __NR_landlock_add_rule +#define __NR_landlock_add_rule 445 +#endif +#ifndef __NR_landlock_restrict_self +#define __NR_landlock_restrict_self 446 +#endif + +#include +#include +#ifndef landlock_create_ruleset +static inline 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 +static inline 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 +static inline int landlock_restrict_self(const int ruleset_fd, const __u32 flags) { + return syscall(__NR_landlock_restrict_self, ruleset_fd, flags); +} +#endif diff --git a/dmoj/cptbox/landlock_helpers.cpp b/dmoj/cptbox/landlock_helpers.cpp new file mode 100644 index 000000000..4e43ecdbb --- /dev/null +++ b/dmoj/cptbox/landlock_helpers.cpp @@ -0,0 +1,42 @@ +#ifndef __FreeBSD__ + +#include +#include +#include +#include +#include + +#include "landlock_helpers.h" + +int landlock_add_path(struct landlock_path_beneath_attr &rule, const int ruleset_fd, const char *path) { + rule.parent_fd = open(path, O_PATH | O_CLOEXEC); + + if (rule.parent_fd < 0) { + if (errno == ENOENT) + goto close_fd; // missing files are ignored + fprintf(stderr, "Failed to open path '%s' for rule: %s\n", path, strerror(errno)); + return -1; + } + if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &rule, 0)) { + fprintf(stderr, "Failed to add rule '%s' to ruleset: %s\n", path, strerror(errno)); + return -1; + } +close_fd: + close(rule.parent_fd); + return 0; +} + +int landlock_add_rules(const int ruleset_fd, const char **paths, __u64 allowed_access) { + struct landlock_path_beneath_attr rule = { + .allowed_access = allowed_access, + .parent_fd = -1, + }; + + for (const char **pathptr = paths; *pathptr; pathptr++) { + if (landlock_add_path(rule, ruleset_fd, *pathptr)) + return -1; + } + return 0; +} + +#endif diff --git a/dmoj/cptbox/landlock_helpers.h b/dmoj/cptbox/landlock_helpers.h new file mode 100644 index 000000000..ce9dfc250 --- /dev/null +++ b/dmoj/cptbox/landlock_helpers.h @@ -0,0 +1,3 @@ +#include "landlock_header.h" + +int landlock_add_rules(const int ruleset_fd, const char **paths, __u64 access_rule); diff --git a/setup.py b/setup.py index c92c670cb..7454fa9d4 100644 --- a/setup.py +++ b/setup.py @@ -129,6 +129,7 @@ def unavailable(self, e): cptbox_sources = [ '_cptbox.pyx', 'helper.cpp', + 'landlock_helpers.cpp', 'ptdebug.cpp', 'ptdebug_x86.cpp', 'ptdebug_x64.cpp', From 156eaa340436c04305fab680106dbf9447bce221 Mon Sep 17 00:00:00 2001 From: Keenan Gugeler Date: Thu, 10 Mar 2022 01:01:16 -0500 Subject: [PATCH 2/8] cptbox: add landlock core One key note to make here is that WRITE permissions imply READ permissions, because otherwise, calls with O_RDWR will fail even if one rule grants read and another grants write. Another thing to note is that under landlock, only the main process can access /proc. This is because we can't add rules when the children spawn. --- dmoj/cptbox/_cptbox.pyi | 3 ++ dmoj/cptbox/_cptbox.pyx | 42 ++++++++++++++++++++++-- dmoj/cptbox/helper.cpp | 72 ++++++++++++++++++++++++++++++++++++++++- dmoj/cptbox/helper.h | 9 ++++++ 4 files changed, 123 insertions(+), 3 deletions(-) diff --git a/dmoj/cptbox/_cptbox.pyi b/dmoj/cptbox/_cptbox.pyi index 7a52be6bf..ae4246171 100644 --- a/dmoj/cptbox/_cptbox.pyi +++ b/dmoj/cptbox/_cptbox.pyi @@ -95,6 +95,9 @@ PTBOX_SPAWN_FAIL_SECCOMP: int PTBOX_SPAWN_FAIL_TRACEME: int PTBOX_SPAWN_FAIL_EXECVE: int PTBOX_SPAWN_FAIL_SETAFFINITY: int +PTBOX_SPAWN_FAIL_LANDLOCK: int + +def has_landlock() -> bool: ... AT_FDCWD: int bsd_get_proc_cwd: Callable[[int], str] diff --git a/dmoj/cptbox/_cptbox.pyx b/dmoj/cptbox/_cptbox.pyx index 1ba3dcee6..619067286 100644 --- a/dmoj/cptbox/_cptbox.pyx +++ b/dmoj/cptbox/_cptbox.pyx @@ -9,12 +9,12 @@ from libcpp cimport bool from posix.resource cimport rusage from posix.types cimport pid_t -__all__ = ['Process', 'Debugger', 'bsd_get_proc_cwd', 'bsd_get_proc_fdno', 'MAX_SYSCALL_NUMBER', +__all__ = ['Process', 'Debugger', 'bsd_get_proc_cwd', 'bsd_get_proc_fdno', 'has_landlock', 'landlock_version', 'MAX_SYSCALL_NUMBER', 'AT_FDCWD', 'ALL_ABIS', 'SUPPORTED_ABIS', 'NATIVE_ABI', 'PTBOX_ABI_X86', 'PTBOX_ABI_X64', 'PTBOX_ABI_X32', 'PTBOX_ABI_ARM', 'PTBOX_ABI_ARM64', 'PTBOX_ABI_FREEBSD_X64', 'PTBOX_ABI_INVALID', 'PTBOX_ABI_COUNT', 'PTBOX_SPAWN_FAIL_NO_NEW_PRIVS', 'PTBOX_SPAWN_FAIL_SECCOMP', 'PTBOX_SPAWN_FAIL_TRACEME', - 'PTBOX_SPAWN_FAIL_EXECVE', 'PTBOX_SPAWN_FAIL_SETAFFINITY'] + 'PTBOX_SPAWN_FAIL_EXECVE', 'PTBOX_SPAWN_FAIL_SETAFFINITY', 'PTBOX_SPAWN_FAIL_LANDLOCK'] cdef extern from 'ptbox.h' nogil: @@ -121,6 +121,14 @@ cdef extern from 'helper.h' nogil: int *seccomp_handlers unsigned long cpu_affinity_mask + const char **landlock_read_exact_files + const char **landlock_read_exact_dirs + const char **landlock_read_recursive_dirs + const char **landlock_write_exact_files + const char **landlock_write_exact_dirs + const char **landlock_write_recursive_dirs + + int get_landlock_version() void cptbox_closefrom(int lowfd) int cptbox_child_run(child_config *) char *_bsd_get_proc_cwd "bsd_get_proc_cwd"(pid_t pid) @@ -132,6 +140,7 @@ cdef extern from 'helper.h' nogil: PTBOX_SPAWN_FAIL_TRACEME PTBOX_SPAWN_FAIL_EXECVE PTBOX_SPAWN_FAIL_SETAFFINITY + PTBOX_SPAWN_FAIL_LANDLOCK int _memory_fd_create "memory_fd_create"() int _memory_fd_seal "memory_fd_seal"(int fd) @@ -222,6 +231,16 @@ def memory_fd_seal(int fd): if result == -1: PyErr_SetFromErrno(OSError) +def landlock_version(): + cdef int version = get_landlock_version() + if version == -1: + PyErr_SetFromErrno(OSError) + return version + +def has_landlock(): + # ABI 2 is the minimum acceptable version for us. + return landlock_version() >= 2 + cdef class Process @@ -491,6 +510,12 @@ cdef class Process: config.argv = NULL config.envp = NULL config.seccomp_handlers = NULL + config.landlock_read_exact_files = NULL + config.landlock_read_exact_dirs = NULL + config.landlock_read_recursive_dirs = NULL + config.landlock_write_exact_files = NULL + config.landlock_write_exact_dirs = NULL + config.landlock_write_recursive_dirs = NULL try: config.address_space = self._child_address @@ -519,12 +544,25 @@ cdef class Process: for i in range(MAX_SYSCALL): config.seccomp_handlers[i] = handlers[i] + config.landlock_read_exact_files = alloc_byte_array(self.landlock_read_exact_files) + config.landlock_read_exact_dirs = alloc_byte_array(self.landlock_read_exact_dirs) + config.landlock_read_recursive_dirs = alloc_byte_array(self.landlock_read_recursive_dirs) + config.landlock_write_exact_files = alloc_byte_array(self.landlock_write_exact_files) + config.landlock_write_exact_dirs = alloc_byte_array(self.landlock_write_exact_dirs) + config.landlock_write_recursive_dirs = alloc_byte_array(self.landlock_write_recursive_dirs) + if self.process.spawn(pt_child, &config): raise RuntimeError('failed to spawn child') finally: free(config.argv) free(config.envp) free(config.seccomp_handlers) + free(config.landlock_read_exact_files) + free(config.landlock_read_exact_dirs) + free(config.landlock_read_recursive_dirs) + free(config.landlock_write_exact_files) + free(config.landlock_write_exact_dirs) + free(config.landlock_write_recursive_dirs) cpdef _monitor(self): cdef int exitcode diff --git a/dmoj/cptbox/helper.cpp b/dmoj/cptbox/helper.cpp index ef0613ade..cbd8fd837 100644 --- a/dmoj/cptbox/helper.cpp +++ b/dmoj/cptbox/helper.cpp @@ -1,4 +1,5 @@ #include "helper.h" +#include "landlock_helpers.h" #include "ptbox.h" #include @@ -90,13 +91,62 @@ int cptbox_child_run(const struct child_config *config) { kill(getpid(), SIGSTOP); #if !PTBOX_FREEBSD + // landlock setup + int ruleset_fd, rc; + 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_REG | + LANDLOCK_ACCESS_FS_MAKE_SYM | LANDLOCK_ACCESS_FS_MAKE_DIR | LANDLOCK_ACCESS_FS_REFER, + }; + + int landlock_version = get_landlock_version(); + if (landlock_version < 2) { + // ABI not old enough. Skip to seccomp + goto seccomp_setup; + } + + 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; + } + + // Note: WRITE must imply READ. This is required because even if one rule allows writing and another allows reading, + // unless there is a rule that allows both, a call with O_RDWR will fail. + if (landlock_add_rules(ruleset_fd, config->landlock_read_exact_files, + LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_EXECUTE) || + landlock_add_rules(ruleset_fd, config->landlock_read_exact_dirs, LANDLOCK_ACCESS_FS_READ_DIR) || + landlock_add_rules(ruleset_fd, config->landlock_read_recursive_dirs, + LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_DIR) || + landlock_add_rules(ruleset_fd, config->landlock_write_exact_files, + LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_WRITE_FILE) || + landlock_add_rules(ruleset_fd, config->landlock_write_exact_dirs, LANDLOCK_ACCESS_FS_READ_DIR) || + landlock_add_rules(ruleset_fd, config->landlock_write_recursive_dirs, + LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_DIR | + LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_REMOVE_DIR | + LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_READ_DIR | + LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_SYM | LANDLOCK_ACCESS_FS_MAKE_DIR | + LANDLOCK_ACCESS_FS_REFER)) { + // landlock_add_rules logs errors + close(ruleset_fd); + return PTBOX_SPAWN_FAIL_LANDLOCK; + } + + rc = landlock_restrict_self(ruleset_fd, 0); + close(ruleset_fd); + if (rc) { + perror("Failed to enforce ruleset"); + return PTBOX_SPAWN_FAIL_LANDLOCK; + } + +seccomp_setup: scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_TRACE(0)); if (!ctx) { fprintf(stderr, "Failed to initialize seccomp context!"); goto seccomp_init_fail; } - int rc; // By default, the native architecture is added to the filter already, so we add all the non-native ones. // This will bloat the filter due to additional architectures, but a few extra compares in the BPF matters // very little when syscalls are rare and other overhead is expensive. @@ -175,6 +225,26 @@ int cptbox_child_run(const struct child_config *config) { #endif } +int get_landlock_version() { +#if !PTBOX_FREEBSD + char *sandbox_mode = getenv("DMOJ_SANDBOX_MODE"); + if (sandbox_mode != nullptr && strcmp(sandbox_mode, "ptrace+seccomp") == 0) { + // Allow disabling of landlock. + return 0; + } + + int landlock_version = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); + if (landlock_version >= 0) { + return landlock_version; + } + if (errno == ENOSYS || errno == EOPNOTSUPP) + return 0; + return -1; +#else + return 0; // FreeBSD does not have landlock +#endif +} + // From python's _posixsubprocess static int pos_int_from_ascii(char *name) { int num = 0; diff --git a/dmoj/cptbox/helper.h b/dmoj/cptbox/helper.h index a22e06ec6..1cee19961 100644 --- a/dmoj/cptbox/helper.h +++ b/dmoj/cptbox/helper.h @@ -9,6 +9,7 @@ #define PTBOX_SPAWN_FAIL_TRACEME 204 #define PTBOX_SPAWN_FAIL_EXECVE 205 #define PTBOX_SPAWN_FAIL_SETAFFINITY 206 +#define PTBOX_SPAWN_FAIL_LANDLOCK 207 struct child_config { unsigned long memory; @@ -27,8 +28,16 @@ struct child_config { int *seccomp_handlers; // 64 cores ought to be enough for anyone. unsigned long cpu_affinity_mask; + const char **landlock_read_exact_files; + const char **landlock_read_exact_dirs; + const char **landlock_read_recursive_dirs; + const char **landlock_write_exact_files; + const char **landlock_write_exact_dirs; + const char **landlock_write_recursive_dirs; }; +int get_landlock_version(); + void cptbox_closefrom(int lowfd); int cptbox_child_run(const struct child_config *config); From e91eccb5f184c38af533f1c7db69c2277da965a4 Mon Sep 17 00:00:00 2001 From: Keenan Gugeler Date: Thu, 10 Mar 2022 01:01:38 -0500 Subject: [PATCH 3/8] cptbox: add landlock to tracer --- dmoj/cptbox/tracer.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/dmoj/cptbox/tracer.py b/dmoj/cptbox/tracer.py index abac1cfb0..3402721c1 100644 --- a/dmoj/cptbox/tracer.py +++ b/dmoj/cptbox/tracer.py @@ -9,6 +9,7 @@ from typing import Callable, List, Mapping, 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_execve, sys_exit, sys_exit_group, sys_getpid, translator from dmoj.utils.communicate import safe_communicate as _safe_communicate @@ -149,6 +150,10 @@ def __init__( self.protection_fault = None self._security = security + if security is not None: + self.configure_files(security.read_fs, security.write_fs) + else: + self.configure_files([], []) self._callbacks = [[None] * MAX_SYSCALL_NUMBER for _ in range(PTBOX_ABI_COUNT)] if security is None: self._trace_syscalls = False @@ -205,6 +210,22 @@ def _get_seccomp_handlers(self) -> List[int]: handlers[call] = handler.errno return handlers + def configure_files(self, read_fs: List[FilesystemAccessRule], write_fs: List[FilesystemAccessRule]) -> None: + def _get_rule_paths(source: List[FilesystemAccessRule], type: Type[FilesystemAccessRule]) -> List[bytes]: + paths = [] + for rule in source: + if isinstance(rule, type): + paths.append(utf8bytes(rule.path)) + + return paths + + self.landlock_read_exact_files = _get_rule_paths(read_fs, ExactFile) + self.landlock_read_exact_dirs = _get_rule_paths(read_fs, ExactDir) + self.landlock_read_recursive_dirs = _get_rule_paths(read_fs, RecursiveDir) + self.landlock_write_exact_files = _get_rule_paths(write_fs, ExactFile) + self.landlock_write_exact_dirs = _get_rule_paths(write_fs, ExactDir) + self.landlock_write_recursive_dirs = _get_rule_paths(write_fs, RecursiveDir) + def wait(self) -> int: self._died.wait() assert self.returncode is not None @@ -223,6 +244,8 @@ def wait(self) -> int: raise RuntimeError('failed to spawn child') elif self.returncode == PTBOX_SPAWN_FAIL_SETAFFINITY: raise RuntimeError('failed to set child affinity') + elif self.returncode == PTBOX_SPAWN_FAIL_LANDLOCK: + raise RuntimeError('landlock configuration failed') elif self.returncode >= 0: raise RuntimeError('process failed to initialize with unknown exit code: %d' % self.returncode) return self.returncode From 2e83f681b8cef81005f9aba3d4041f2665d67a52 Mon Sep 17 00:00:00 2001 From: Keenan Gugeler Date: Thu, 10 Mar 2022 01:04:42 -0500 Subject: [PATCH 4/8] cptbox: add landlock to normal isolate tracer Note that under the current version of landlock, some syscalls are not handled by landlock, including `stat` and `access`. --- dmoj/cptbox/isolate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dmoj/cptbox/isolate.py b/dmoj/cptbox/isolate.py index 70af3848b..9dd0c66e1 100644 --- a/dmoj/cptbox/isolate.py +++ b/dmoj/cptbox/isolate.py @@ -44,6 +44,8 @@ class FilesystemSyscallKind(Enum): class IsolateTracer(dict): def __init__(self, *, read_fs: Sequence[FilesystemAccessRule], write_fs: Sequence[FilesystemAccessRule]): super().__init__() + self.read_fs = read_fs + self.write_fs = write_fs self.read_fs_jail = self._compile_fs_jail(read_fs) self.write_fs_jail = self._compile_fs_jail(write_fs) From 7e3c513526c0fe8240b099ceeb99c01bff735b77 Mon Sep 17 00:00:00 2001 From: Keenan Gugeler Date: Thu, 10 Mar 2022 01:06:12 -0500 Subject: [PATCH 5/8] cptbox: add landlock to compiler isolate tracer Under landlock, linking and renaming throw EXDEV if `src` and `dst` are not in the same directory. This is unacceptable for us, so we simulate the calls. Because we are simulating, we want to make sure that `flags` is zero for `linkat`, otherwise it's likely that we have done something unintended. --- dmoj/cptbox/compiler_isolate.py | 54 +++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/dmoj/cptbox/compiler_isolate.py b/dmoj/cptbox/compiler_isolate.py index 65a8a4de9..35ee779f5 100644 --- a/dmoj/cptbox/compiler_isolate.py +++ b/dmoj/cptbox/compiler_isolate.py @@ -1,7 +1,7 @@ import struct import sys -from dmoj.cptbox._cptbox import AT_FDCWD, Debugger +from dmoj.cptbox._cptbox import AT_FDCWD, Debugger, has_landlock from dmoj.cptbox.filesystem_policies import ExactFile, FilesystemPolicy, RecursiveDir from dmoj.cptbox.handlers import ACCESS_EFAULT, ACCESS_EPERM, ALLOW from dmoj.cptbox.isolate import DeniedSyscall, FilesystemSyscallKind, IsolateTracer @@ -23,6 +23,42 @@ def __init__(self, *, tmpdir, read_fs, write_fs): write_fs += BASE_WRITE_FILESYSTEM + [RecursiveDir(tmpdir)] super().__init__(read_fs=read_fs, write_fs=write_fs) + if has_landlock(): + self.update( + { + # Directory system calls + sys_mkdir: ALLOW, + sys_mkdirat: ALLOW, + sys_rmdir: ALLOW, + # Link/Rename + sys_link: ALLOW, + sys_linkat: ALLOW, + sys_symlink: ALLOW, + sys_rename: ALLOW, + sys_renameat: ALLOW, + # Unlink + sys_unlink: ALLOW, + sys_unlinkat: ALLOW, + } + ) + else: + self.update( + { + # Directory system calls + sys_mkdir: self.handle_file_access(FilesystemSyscallKind.WRITE, file_reg=0), + sys_mkdirat: self.handle_file_access_at(FilesystemSyscallKind.WRITE, dir_reg=0, file_reg=1), + sys_rmdir: self.handle_file_access(FilesystemSyscallKind.WRITE, file_reg=0), + # Linking system calls + sys_link: self.handle_file_access(FilesystemSyscallKind.WRITE, file_reg=1), + sys_linkat: self.handle_file_access_at(FilesystemSyscallKind.WRITE, dir_reg=2, file_reg=3), + sys_unlink: self.handle_file_access(FilesystemSyscallKind.WRITE, file_reg=0), + sys_unlinkat: self.handle_file_access_at(FilesystemSyscallKind.WRITE, dir_reg=0, file_reg=1), + sys_symlink: self.handle_file_access(FilesystemSyscallKind.WRITE, file_reg=1), + sys_rename: self.handle_rename, + sys_renameat: self.handle_renameat, + } + ) + self.update( { # Process spawning system calls @@ -31,16 +67,6 @@ def __init__(self, *, tmpdir, read_fs, write_fs): sys_execve: ALLOW, sys_getcpu: ALLOW, sys_getpgid: ALLOW, - # Directory system calls - sys_mkdir: self.handle_file_access(FilesystemSyscallKind.WRITE, file_reg=0), - sys_mkdirat: self.handle_file_access_at(FilesystemSyscallKind.WRITE, dir_reg=0, file_reg=1), - sys_rmdir: self.handle_file_access(FilesystemSyscallKind.WRITE, file_reg=0), - # Linking system calls - sys_link: self.handle_file_access(FilesystemSyscallKind.WRITE, file_reg=1), - sys_linkat: self.handle_file_access_at(FilesystemSyscallKind.WRITE, dir_reg=2, file_reg=3), - sys_unlink: self.handle_file_access(FilesystemSyscallKind.WRITE, file_reg=0), - sys_unlinkat: self.handle_file_access_at(FilesystemSyscallKind.WRITE, dir_reg=0, file_reg=1), - sys_symlink: self.handle_file_access(FilesystemSyscallKind.WRITE, file_reg=1), # Miscellaneous other filesystem system calls sys_chdir: self.handle_file_access(FilesystemSyscallKind.READ, file_reg=0), sys_chmod: self.handle_file_access(FilesystemSyscallKind.WRITE, file_reg=0), @@ -53,8 +79,6 @@ def __init__(self, *, tmpdir, read_fs, write_fs): sys_fchmod: self.handle_fchmod, sys_fallocate: ALLOW, sys_ftruncate: ALLOW, - sys_rename: self.handle_rename, - sys_renameat: self.handle_renameat, # I/O system calls sys_readv: ALLOW, sys_pwrite64: ALLOW, @@ -103,6 +127,10 @@ def __init__(self, *, tmpdir, read_fs, write_fs): } ) + def fullpath_from_reg_and_dirfd(self, debugger: Debugger, *, file_reg, dirfd): + rel_file = self.get_rel_file(debugger, reg=file_reg) + return self.get_full_path_unnormalized(debugger, rel_file, dirfd=dirfd) + def handle_rename(self, debugger: Debugger) -> None: self.access_check(self._write_fs_jail_getter, self._dirfd_getter_cwd, file_reg=0)(debugger) self.access_check(self._write_fs_jail_getter, self._dirfd_getter_cwd, file_reg=1)(debugger) From 4687eea53c170e666bdfde57aaa4b7b5a01912b8 Mon Sep 17 00:00:00 2001 From: Keenan Gugeler Date: Thu, 10 Mar 2022 01:07:35 -0500 Subject: [PATCH 6/8] executors: fixup executors for landlock Under landlock, since /proc is not accessible in a subprocess, SCALA calls `mincore`. We allow this in order for it to pass. Also, since `execve` is checked under landlock, we need to add `/bin` to the list of readable directories. --- dmoj/executors/RUST.py | 9 ++++++++- dmoj/executors/SCALA.py | 1 + dmoj/executors/base_executor.py | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/dmoj/executors/RUST.py b/dmoj/executors/RUST.py index fe27698c0..a32ec8808 100644 --- a/dmoj/executors/RUST.py +++ b/dmoj/executors/RUST.py @@ -1,7 +1,8 @@ import fcntl import os +from typing import List -from dmoj.cptbox.filesystem_policies import ExactFile, RecursiveDir +from dmoj.cptbox.filesystem_policies import ExactFile, FilesystemAccessRule, RecursiveDir from dmoj.executors.compiled_executor import CompiledExecutor CARGO_TOML = b"""\ @@ -86,6 +87,12 @@ def get_shared_target(self): # We intentionally don't clean this directory up at any point, since we can re-use it. return self.shared_target + def get_fs(self) -> List[FilesystemAccessRule]: + assert self._executable is not None + # Under landlock we need this for execve to work. + # We use `self._executable` because it is copied when caching executors, but other properties are not. + return super().get_fs() + [ExactFile(self._executable)] + def cleanup(self) -> None: super().cleanup() if self.shared_target is not None: diff --git a/dmoj/executors/SCALA.py b/dmoj/executors/SCALA.py index 54f2142f4..7231ca3ce 100644 --- a/dmoj/executors/SCALA.py +++ b/dmoj/executors/SCALA.py @@ -20,6 +20,7 @@ class Executor(JavaExecutor): ExactFile('/bin/bash'), RecursiveDir('/etc/alternatives'), ] + compiler_syscalls = ['mincore'] vm = 'scala_vm' test_program = """\ diff --git a/dmoj/executors/base_executor.py b/dmoj/executors/base_executor.py index dfa14b33a..65bf7ff81 100644 --- a/dmoj/executors/base_executor.py +++ b/dmoj/executors/base_executor.py @@ -33,6 +33,7 @@ ExactFile('/dev/urandom'), ExactFile('/dev/random'), *USR_DIR, + RecursiveDir('/bin'), # required under landlock when /bin is not a symlink, since we check execve. RecursiveDir('/lib'), RecursiveDir('/lib32'), RecursiveDir('/lib64'), From b366e009a2dd9a2d68ebf0c971cd92fb3f12fd68 Mon Sep 17 00:00:00 2001 From: Keenan Gugeler Date: Tue, 3 Jan 2023 00:04:33 -0500 Subject: [PATCH 7/8] judge: add startup message for landlock --- dmoj/citest.py | 6 ++++++ dmoj/cptbox/__init__.py | 1 + dmoj/judge.py | 6 +++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/dmoj/citest.py b/dmoj/citest.py index bbf8bfdaa..190263d9f 100644 --- a/dmoj/citest.py +++ b/dmoj/citest.py @@ -6,12 +6,18 @@ from dmoj import judgeenv from dmoj.contrib import load_contrib_modules +from dmoj.cptbox import has_landlock from dmoj.executors import executors from dmoj.testsuite import Tester from dmoj.utils.ansi import print_ansi def ci_test(executors_to_test, overrides, allow_fail=frozenset()): + if has_landlock(): + print('Running CI with landlock and seccomp...') + else: + print('Running CI with just seccomp...') + result = {} failed = False failed_executors = [] diff --git a/dmoj/cptbox/__init__.py b/dmoj/cptbox/__init__.py index b239329b8..09cad30ef 100644 --- a/dmoj/cptbox/__init__.py +++ b/dmoj/cptbox/__init__.py @@ -6,6 +6,7 @@ PTBOX_ABI_X32, PTBOX_ABI_X64, PTBOX_ABI_X86, + has_landlock, ) from dmoj.cptbox.handlers import ALLOW, DISALLOW from dmoj.cptbox.isolate import FilesystemSyscallKind, IsolateTracer diff --git a/dmoj/judge.py b/dmoj/judge.py index 1db2ddfe8..0375d67f0 100644 --- a/dmoj/judge.py +++ b/dmoj/judge.py @@ -14,6 +14,7 @@ from dmoj import packet from dmoj.control import JudgeControlRequestHandler +from dmoj.cptbox import has_landlock from dmoj.error import CompileError from dmoj.judgeenv import clear_problem_dirs_cache, env, get_supported_problems_and_mtimes, startup_warnings from dmoj.monitor import Monitor @@ -592,7 +593,10 @@ def main(): # pragma: no cover executors.load_executors() contrib.load_contrib_modules() - print('Running live judge...') + if has_landlock(): + print('Running live judge with landlock and seccomp...') + else: + print('Running live judge with just seccomp...') for warning in judgeenv.startup_warnings: print_ansi('#ansi[Warning: %s](yellow)' % warning) From 244c4a125d9e4d4e40188618c6339a5d7bb09119 Mon Sep 17 00:00:00 2001 From: Keenan Gugeler Date: Wed, 4 Jan 2023 00:24:39 -0500 Subject: [PATCH 8/8] ci: add pure seccomp to arm testing --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b389b93c0..f38e79cbc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,6 +62,7 @@ jobs: strategy: matrix: python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11' ] + sandbox_mode: [ 'ptrace+seccomp' ] steps: - uses: actions/checkout@v3 - name: Download docker image @@ -118,6 +119,7 @@ jobs: strategy: matrix: python-version: [ '3.11' ] + sandbox_mode: [ 'ptrace+seccomp', 'ptrace+seccomp+landlock' ] steps: - uses: actions/checkout@v3 - name: Download docker image @@ -134,6 +136,7 @@ jobs: export LANG=C.UTF-8 export PYTHONIOENCODING=utf8 export PYTHON="/code/python${{ matrix.python-version }}/bin/python${{ matrix.python-version }}" + export DMOJ_SANDBOX_MODE=${{ matrix.sandbox_mode }} "$PYTHON" -m pip install --upgrade pip wheel "$PYTHON" -m pip install cython coverage