From 0504ab3a832e24202fea3288d8193de7fe311bfe Mon Sep 17 00:00:00 2001 From: Dmitrii Kuvaiskii Date: Wed, 24 Apr 2024 06:00:58 -0700 Subject: [PATCH] [LibOS] Add `sys.disallowed_syscalls = [ ... ]` manifest option This commit adds the manifest syntax `sys.disallowed_syscalls = [ ... ]` to specify system calls that will be disallowed to be executed in Gramine (i.e. a denylist). This resembles, though significantly less flexible, seccomp profiles. Signed-off-by: Dmitrii Kuvaiskii --- Documentation/manifest-syntax.rst | 23 ++++++++ libos/include/libos_internal.h | 2 + libos/src/libos_init.c | 1 + libos/src/libos_parser.c | 14 +++++ libos/src/libos_syscalls.c | 58 +++++++++++++++++++ libos/test/regression/disallowed_syscalls.c | 37 ++++++++++++ .../disallowed_syscalls.manifest.template | 32 ++++++++++ libos/test/regression/meson.build | 1 + libos/test/regression/test_libos.py | 4 ++ libos/test/regression/tests.toml | 1 + libos/test/regression/tests_musl.toml | 1 + python/graminelibos/manifest_check.py | 1 + 12 files changed, 175 insertions(+) create mode 100644 libos/test/regression/disallowed_syscalls.c create mode 100644 libos/test/regression/disallowed_syscalls.manifest.template diff --git a/Documentation/manifest-syntax.rst b/Documentation/manifest-syntax.rst index 9e702f84b7..addbb9e09e 100644 --- a/Documentation/manifest-syntax.rst +++ b/Documentation/manifest-syntax.rst @@ -389,6 +389,29 @@ Python). Could be useful in SGX environments: child processes consume to achieve this, you need to run the whole Gramine inside a proper security sandbox. +Disallowing syscalls +^^^^^^^^^^^^^^^^^^^^ + +:: + + sgx.disallowed_syscalls = [ + "syscall_name", + "syscall_name", + ] + +This syntax specifies the system calls that will be disallowed to be executed in +Gramine (i.e. a denylist). For example, to disallow eventfd completely, specify +two syscall variants: ``sgx.disallowed_syscalls = [ "eventfd", "eventfd2" ]``. +This is similar, though significantly less flexible, to seccomp profiles. + +.. note :: + This option is *not* a replacement for ``sys.disallow_subprocesses`` (see + above). This is because the ``clone()`` syscall has two usages: (1) it is + used to spawn subprocesses by Glibc and many other libraries and runtimes and + (2) it is also used to create threads in the same process. The + ``sys.disallow_subprocesses`` manifest option disables only the first usage, + whereas ``sys.disallowed_syscalls = ["clone"]`` disables both usages. + Root FS mount point ^^^^^^^^^^^^^^^^^^^ diff --git a/libos/include/libos_internal.h b/libos/include/libos_internal.h index cb108441b2..e6562a76ac 100644 --- a/libos/include/libos_internal.h +++ b/libos/include/libos_internal.h @@ -157,6 +157,8 @@ int init_eventfd_mode(void); void warn_unsupported_syscall(unsigned long sysno); void debug_print_syscall_before(unsigned long sysno, ...); void debug_print_syscall_after(unsigned long sysno, ...); +unsigned long get_syscall_number(const char* name); +int init_syscalls(void); #ifndef __alloca #define __alloca __builtin_alloca diff --git a/libos/src/libos_init.c b/libos/src/libos_init.c index b5d3541f8d..d9ff052332 100644 --- a/libos/src/libos_init.c +++ b/libos/src/libos_init.c @@ -503,6 +503,7 @@ noreturn void libos_init(const char* const* argv, const char* const* envp) { strlen(g_pal_public_state->dns_host.hostname)); RUN_INIT(init_eventfd_mode); + RUN_INIT(init_syscalls); log_debug("LibOS initialized"); diff --git a/libos/src/libos_parser.c b/libos/src/libos_parser.c index e9de40c9cd..3089b05646 100644 --- a/libos/src/libos_parser.c +++ b/libos/src/libos_parser.c @@ -1649,6 +1649,20 @@ void warn_unsupported_syscall(unsigned long sysno) { log_warning("Unsupported system call %lu", sysno); } +unsigned long get_syscall_number(const char* name) { + assert(LIBOS_SYSCALL_BOUND == ARRAY_SIZE(syscall_parser_table)); + unsigned long sysno = LIBOS_SYSCALL_BOUND; + for (size_t i = 0; i < LIBOS_SYSCALL_BOUND; i++) { + if (!syscall_parser_table[i].name) + continue; + if (strcmp(name, syscall_parser_table[i].name) == 0) { + sysno = i; + break; + } + } + return sysno; +} + static int buf_write_all(const char* str, size_t size, void* arg) { __UNUSED(arg); diff --git a/libos/src/libos_syscalls.c b/libos/src/libos_syscalls.c index 83be137621..78207c76c7 100644 --- a/libos/src/libos_syscalls.c +++ b/libos/src/libos_syscalls.c @@ -11,7 +11,9 @@ #include "libos_table.h" #include "libos_tcb.h" #include "libos_thread.h" +#include "libos_utils.h" #include "linux_abi/errors.h" +#include "toml_utils.h" typedef arch_syscall_arg_t (*six_args_syscall_t)(arch_syscall_arg_t, arch_syscall_arg_t, arch_syscall_arg_t, arch_syscall_arg_t, @@ -84,3 +86,59 @@ noreturn void return_from_syscall(PAL_CONTEXT* context) { #endif _return_from_syscall(context); } + +int init_syscalls(void) { + assert(g_manifest_root); + int ret; + + toml_table_t* manifest_sys = toml_table_in(g_manifest_root, "sys"); + if (!manifest_sys) + return 0; + + toml_array_t* toml_disallowed_syscalls = toml_array_in(manifest_sys, "disallowed_syscalls"); + if (!toml_disallowed_syscalls) + return 0; + + ssize_t toml_disallowed_syscalls_cnt = toml_array_nelem(toml_disallowed_syscalls); + if (toml_disallowed_syscalls_cnt < 0) + return -EPERM; + if (toml_disallowed_syscalls_cnt == 0) + return 0; + + char* toml_disallowed_syscall_str = NULL; + + for (ssize_t i = 0; i < toml_disallowed_syscalls_cnt; i++) { + toml_raw_t toml_disallowed_syscall_raw = toml_raw_at(toml_disallowed_syscalls, i); + if (!toml_disallowed_syscall_raw) { + log_error("Invalid disallowed syscall in manifest at index %ld", i); + return -EINVAL; + goto out; + } + + ret = toml_rtos(toml_disallowed_syscall_raw, &toml_disallowed_syscall_str); + if (ret < 0) { + log_error("Invalid disallowed syscall in manifest at index %ld (not a string)", i); + return -EINVAL; + goto out; + } + + uint64_t sysno = get_syscall_number(toml_disallowed_syscall_str); + if (sysno >= LIBOS_SYSCALL_BOUND) { + log_error("Unrecognized disallowed syscall `%s` in manifest at index %ld", + toml_disallowed_syscall_str, i); + return -EINVAL; + goto out; + } + + /* force the syscall to be unrecognized by LibOS and thus return -ENOSYS */ + libos_syscall_table[sysno] = NULL; + + free(toml_disallowed_syscall_str); + toml_disallowed_syscall_str = NULL; + } + + ret = 0; +out: + free(toml_disallowed_syscall_str); + return ret; +} diff --git a/libos/test/regression/disallowed_syscalls.c b/libos/test/regression/disallowed_syscalls.c new file mode 100644 index 0000000000..1b61e7e6eb --- /dev/null +++ b/libos/test/regression/disallowed_syscalls.c @@ -0,0 +1,37 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +int main(void) { + int ret; + + errno = 0; + ret = eventfd(0, 0); + if (ret != -1 && errno != ENOSYS) + errx(1, "expected eventfd to fail with -ENOSYS but it returned ret=%d errno=%d", ret, + errno); + + errno = 0; + ret = fork(); + if (ret != -1 && errno != ENOSYS) + errx(1, "expected fork to fail with -ENOSYS but it returned ret=%d errno=%d", ret, errno); + + errno = 0; + ret = getpid(); + if (ret < 0) + errx(1, "expected getpid to succeed but it returned ret=%d errno=%d", ret, errno); + + errno = 0; + ret = gettid(); + if (ret < 0) + errx(1, "expected gettid to succeed but it returned ret=%d errno=%d", ret, errno); + + puts("TEST OK"); + return 0; +} diff --git a/libos/test/regression/disallowed_syscalls.manifest.template b/libos/test/regression/disallowed_syscalls.manifest.template new file mode 100644 index 0000000000..63f6f9b053 --- /dev/null +++ b/libos/test/regression/disallowed_syscalls.manifest.template @@ -0,0 +1,32 @@ +loader.entrypoint = "file:{{ gramine.libos }}" +libos.entrypoint = "{{ entrypoint }}" + +loader.env.LD_LIBRARY_PATH = "/lib" + +fs.mounts = [ + { path = "/lib", uri = "file:{{ gramine.runtimedir(libc) }}" }, + { path = "/{{ entrypoint }}", uri = "file:{{ binary_dir }}/{{ entrypoint }}" }, +] + +sys.disallowed_syscalls = [ + # even though glibc wrapper is called eventfd, glibc translates it into eventfd2; + # we specify both syscall variants to be on the safe side + "eventfd", + "eventfd2", + + # even though glibc wrapper is called fork, glibc translates it into clone; + # we specify all syscall variants to be on the safe side + "fork", + "vfork", + "clone", + "clone3", +] + +sgx.debug = true +sgx.edmm_enable = {{ 'true' if env.get('EDMM', '0') == '1' else 'false' }} + +sgx.trusted_files = [ + "file:{{ gramine.libos }}", + "file:{{ gramine.runtimedir(libc) }}/", + "file:{{ binary_dir }}/{{ entrypoint }}", +] diff --git a/libos/test/regression/meson.build b/libos/test/regression/meson.build index 48b9f54a05..90d0c64b6a 100644 --- a/libos/test/regression/meson.build +++ b/libos/test/regression/meson.build @@ -16,6 +16,7 @@ tests = { }, 'devfs': {}, 'device_passthrough': {}, + 'disallowed_syscalls': {}, 'double_fork': {}, 'epoll_epollet': {}, 'epoll_test': {}, diff --git a/libos/test/regression/test_libos.py b/libos/test/regression/test_libos.py index 73590b8581..7b86323f00 100644 --- a/libos/test/regression/test_libos.py +++ b/libos/test/regression/test_libos.py @@ -1030,6 +1030,10 @@ def test_010_syscall_restart(self): self.assertIn('Got: P', stdout) self.assertIn('TEST 2 OK', stdout) + def test_020_disallowed_syscalls(self): + stdout, _ = self.run_binary(['disallowed_syscalls']) + self.assertIn('TEST OK', stdout) + class TC_40_FileSystem(RegressionTestCase): def test_000_proc(self): stdout, _ = self.run_binary(['proc_common']) diff --git a/libos/test/regression/tests.toml b/libos/test/regression/tests.toml index 9b68f74ee0..f15a54c597 100644 --- a/libos/test/regression/tests.toml +++ b/libos/test/regression/tests.toml @@ -14,6 +14,7 @@ manifests = [ "debug_log_inline", "devfs", "device_passthrough", + "disallowed_syscalls", "double_fork", "env_from_file", "env_from_host", diff --git a/libos/test/regression/tests_musl.toml b/libos/test/regression/tests_musl.toml index b553efa678..a4fba510df 100644 --- a/libos/test/regression/tests_musl.toml +++ b/libos/test/regression/tests_musl.toml @@ -16,6 +16,7 @@ manifests = [ "debug_log_inline", "devfs", "device_passthrough", + "disallowed_syscalls", "double_fork", "env_from_file", "env_from_host", diff --git a/python/graminelibos/manifest_check.py b/python/graminelibos/manifest_check.py index 9477ef349e..6d8e0c65df 100644 --- a/python/graminelibos/manifest_check.py +++ b/python/graminelibos/manifest_check.py @@ -122,6 +122,7 @@ }], 'brk': {'max_size': _size}, 'disallow_subprocesses': bool, + 'disallowed_syscalls': [str], 'enable_extra_runtime_domain_names_conf': bool, 'enable_sigterm_injection': bool, 'experimental__enable_flock': bool,