mirror of
https://github.com/systemd/systemd.git
synced 2026-11-06 05:17:24 +00:00
core: apply SystemCallFilter= to io_uring operations
Many io_uring operations can do the work of a system call: IORING_OP_OPENAT opens a file like openat(), IORING_OP_SOCKET creates a socket like socket(), and so on. SystemCallFilter= doesn't apply to io_uring operations. A service that can use io_uring can thus do things that its system call filter denies. On kernels with io_uring BPF filters, let's map each operation to the system calls that it can do the work of, and deny the operation if the filter denies any of them. IORING_OP_URING_CMD maps to ioctl() and also to setsockopt(), getsockopt(), and getsockname(), because it runs those socket calls on sockets. We don't deny operations that we don't know about, even for an allow list. A denied request fails with EACCES, while the kernel keeps reporting the operation as supported, so programs couldn't fall back. Our seccomp allow lists return ENOSYS for unknown system calls for the same reason. We skip system calls that are denied with an error number of 0. A denied io_uring request fails with EACCES and can't pretend to succeed. We also don't register any io_uring filters if the system call filter denies io_uring_setup(), because the service can't create rings then.
This commit is contained in:
1 parent
d2fe659ff6
commit
f80d935027
6 files changed
+251
-3
No files matched your search
@@ -2995,6 +2995,21 @@ RestrictNamespaces=~cgroup net</programlisting>
|
||||
<para>This feature makes use of the Secure Computing Mode 2 interfaces of the kernel ('seccomp
|
||||
filtering') and is useful for enforcing a minimal sandboxing environment.</para>
|
||||
|
||||
<para>On kernels that support io_uring BPF filters (Linux 7.0 and newer), this setting also applies
|
||||
to the operations of
|
||||
<citerefentry project='man-pages'><refentrytitle>io_uring</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
|
||||
if the filter allows <function>io_uring_setup()</function>. An operation that can do the work of
|
||||
one or more system calls, such as <constant>IORING_OP_OPENAT</constant> or
|
||||
<constant>IORING_OP_SOCKET</constant>, is denied if the filter denies any of its equivalent system
|
||||
calls. Operations of newer kernels that systemd doesn't know are not affected. Denied requests
|
||||
always fail with <constant>EACCES</constant>, regardless of
|
||||
<varname>SystemCallErrorNumber=</varname> and of the action of the system call. System calls that
|
||||
are denied with an error number of 0 do not affect io_uring operations, since a denied request
|
||||
can't pretend to succeed. The restriction applies to all io_uring instances that the processes of
|
||||
the unit create, but not to io_uring instances that they receive from other processes. On such
|
||||
kernels, <constant>IORING_REGISTER_RESTRICTIONS</constant> without an io_uring instance fails with
|
||||
<constant>EPERM</constant> for the processes of the unit.</para>
|
||||
|
||||
<para>Note that on systems supporting multiple ABIs (such as x86/x86-64) it is recommended to turn
|
||||
off alternative ABIs for services, so that they cannot be used to circumvent the restrictions of this
|
||||
option. Specifically, it is recommended to combine this option with
|
||||
|
||||
+116
-3
@@ -1,6 +1,7 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <grp.h>
|
||||
#include <linux/io_uring.h>
|
||||
#include <linux/securebits.h>
|
||||
#include <poll.h>
|
||||
#include <sched.h>
|
||||
@@ -63,6 +64,7 @@
|
||||
#include "namespace-util.h"
|
||||
#include "nsflags.h"
|
||||
#include "nsresource.h"
|
||||
#include "nulstr-util.h"
|
||||
#include "open-file.h"
|
||||
#include "osc-context.h"
|
||||
#include "pam-util.h"
|
||||
@@ -1784,9 +1786,56 @@ static bool context_has_no_new_privileges(const ExecContext *c) {
|
||||
return context_has_seccomp(c);
|
||||
}
|
||||
|
||||
static bool check_io_uring_bpf_filters(const char *setting) {
|
||||
#if HAVE_SECCOMP
|
||||
static bool syscall_filter_allows(const ExecContext *c, int id) {
|
||||
int e;
|
||||
|
||||
assert(c);
|
||||
|
||||
if (!hashmap_contains(c->syscall_filter, INT_TO_PTR(id + 1)))
|
||||
return !c->syscall_allow_list;
|
||||
|
||||
/* With an error number of 0, the system call does nothing and pretends to succeed. A denied io_uring
|
||||
* request fails with EACCES instead. A service that suppresses fsync() with an error number of 0
|
||||
* would then see its io_uring fsync requests fail. */
|
||||
e = PTR_TO_INT(hashmap_get(c->syscall_filter, INT_TO_PTR(id + 1)));
|
||||
if (e == 0)
|
||||
return true;
|
||||
|
||||
/* An allow list contains the allowed system calls with the value -1. Its other entries come from
|
||||
* assignments like "~uname:EILSEQ" and deny the system call. */
|
||||
return c->syscall_allow_list && e < 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool syscall_filter_denies_io_uring(const ExecContext *c) {
|
||||
assert(c);
|
||||
|
||||
#if HAVE_SECCOMP
|
||||
int id;
|
||||
|
||||
if (!exec_context_has_syscall_filters(c) || !is_seccomp_available())
|
||||
return false;
|
||||
|
||||
id = sym_seccomp_syscall_resolve_name("io_uring_setup");
|
||||
return id >= 0 && !syscall_filter_allows(c, id);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool check_io_uring_bpf_filters(const ExecContext *c, const ExecParameters *p, const char *setting) {
|
||||
assert(c);
|
||||
assert(p);
|
||||
assert(setting);
|
||||
|
||||
/* If SystemCallFilter= denies io_uring_setup(), the processes of the unit can't create rings, and
|
||||
* io_uring filters have no effect. */
|
||||
if (syscall_filter_denies_io_uring(c)) {
|
||||
log_debug("SystemCallFilter= denies io_uring_setup(), not applying %s to io_uring requests.", setting);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!p->io_uring_bpf_filter_supported)
|
||||
log_debug("io_uring BPF filters are not supported, not applying %s to io_uring requests.", setting);
|
||||
|
||||
@@ -1800,7 +1849,7 @@ static int apply_io_uring_address_families(const ExecContext *c, const ExecParam
|
||||
if (!exec_context_has_address_families(c))
|
||||
return 0;
|
||||
|
||||
if (!check_io_uring_bpf_filters("RestrictAddressFamilies="))
|
||||
if (!check_io_uring_bpf_filters(c, p, "RestrictAddressFamilies="))
|
||||
return 0;
|
||||
|
||||
return io_uring_restrict_address_families(c->address_families, c->address_families_allow_list);
|
||||
@@ -1813,7 +1862,7 @@ static int apply_io_uring_suid_sgid(const ExecContext *c, const ExecParameters *
|
||||
if (!c->restrict_suid_sgid)
|
||||
return 0;
|
||||
|
||||
if (!check_io_uring_bpf_filters("RestrictSUIDSGID="))
|
||||
if (!check_io_uring_bpf_filters(c, p, "RestrictSUIDSGID="))
|
||||
return 0;
|
||||
|
||||
return io_uring_restrict_suid_sgid();
|
||||
@@ -2008,6 +2057,64 @@ static int apply_restrict_suid_sgid(const ExecContext *c, const ExecParameters *
|
||||
return seccomp_restrict_suid_sgid();
|
||||
}
|
||||
|
||||
static bool syscall_filter_allows_all(const ExecContext *c, const char *syscalls) {
|
||||
assert(c);
|
||||
assert(syscalls);
|
||||
|
||||
NULSTR_FOREACH(name, syscalls) {
|
||||
int id;
|
||||
|
||||
/* libseccomp returns negative numbers for system calls that the native architecture does
|
||||
* not have. */
|
||||
id = sym_seccomp_syscall_resolve_name(name);
|
||||
if (id < 0)
|
||||
continue;
|
||||
|
||||
if (!syscall_filter_allows(c, id))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int apply_io_uring_syscall_filter(const ExecContext *c, const ExecParameters *p) {
|
||||
_cleanup_set_free_ Set *denied = NULL;
|
||||
int r;
|
||||
|
||||
assert(c);
|
||||
assert(p);
|
||||
|
||||
if (!exec_context_has_syscall_filters(c))
|
||||
return 0;
|
||||
|
||||
r = dlopen_libseccomp(LOG_DEBUG);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "libseccomp is not available, not applying SystemCallFilter= to io_uring requests: %m");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!check_io_uring_bpf_filters(c, p, "SystemCallFilter="))
|
||||
return 0;
|
||||
|
||||
/* An io_uring operation can do the work of each of its equivalent system calls, so we deny it if
|
||||
* the filter denies any of them. */
|
||||
for (int op = 0; op < IORING_OP_LAST; op++) {
|
||||
const char *syscalls = io_uring_op_syscalls_to_string(op);
|
||||
|
||||
if (!syscalls || syscall_filter_allows_all(c, syscalls))
|
||||
continue;
|
||||
|
||||
r = set_ensure_put(&denied, /* hash_ops= */ NULL, INT_TO_PTR(op + 1));
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
/* A denied io_uring request fails with EACCES, but the kernel still reports the operation as
|
||||
* supported. Programs therefore can't fall back from an unknown operation that we deny. We only deny
|
||||
* known operations, even for an allow list of system calls. */
|
||||
return io_uring_restrict_ops(denied, /* allow_list= */ false);
|
||||
}
|
||||
|
||||
static int apply_protect_sysctl(const ExecContext *c, const ExecParameters *p) {
|
||||
assert(c);
|
||||
assert(p);
|
||||
@@ -6977,6 +7084,12 @@ int exec_invoke(
|
||||
}
|
||||
|
||||
#if HAVE_SECCOMP
|
||||
r = apply_io_uring_syscall_filter(context, params);
|
||||
if (r < 0) {
|
||||
*exit_status = EXIT_SECCOMP;
|
||||
return log_error_errno(r, "Failed to apply system call filters to io_uring: %m");
|
||||
}
|
||||
|
||||
/* This really should remain as close to the execve() as possible, to make sure our own code is affected
|
||||
* by the filter as little as possible. */
|
||||
r = apply_syscall_filter(context, params);
|
||||
|
||||
@@ -19,6 +19,67 @@
|
||||
#include "process-util.h"
|
||||
#include "set.h"
|
||||
#include "signal-util.h"
|
||||
#include "string-table.h"
|
||||
|
||||
/* Each opcode maps to the system calls that it can do the work of. An opcode that is missing here has no
|
||||
* system call equivalent, because it only manages the ring itself. */
|
||||
static const char* const io_uring_op_syscalls_table[IORING_OP_LAST] = {
|
||||
[IORING_OP_READV] = "readv\0preadv\0preadv2\0",
|
||||
[IORING_OP_WRITEV] = "writev\0pwritev\0pwritev2\0",
|
||||
[IORING_OP_FSYNC] = "fsync\0fdatasync\0",
|
||||
[IORING_OP_READ_FIXED] = "read\0pread64\0",
|
||||
[IORING_OP_WRITE_FIXED] = "write\0pwrite64\0",
|
||||
[IORING_OP_POLL_ADD] = "poll\0ppoll\0ppoll_time64\0",
|
||||
[IORING_OP_SYNC_FILE_RANGE] = "sync_file_range\0sync_file_range2\0",
|
||||
[IORING_OP_SENDMSG] = "sendmsg\0",
|
||||
[IORING_OP_RECVMSG] = "recvmsg\0",
|
||||
[IORING_OP_ACCEPT] = "accept\0accept4\0",
|
||||
[IORING_OP_CONNECT] = "connect\0",
|
||||
[IORING_OP_FALLOCATE] = "fallocate\0",
|
||||
[IORING_OP_OPENAT] = "open\0openat\0",
|
||||
[IORING_OP_CLOSE] = "close\0",
|
||||
[IORING_OP_STATX] = "statx\0",
|
||||
[IORING_OP_READ] = "read\0pread64\0",
|
||||
[IORING_OP_WRITE] = "write\0pwrite64\0",
|
||||
[IORING_OP_FADVISE] = "fadvise64\0fadvise64_64\0arm_fadvise64_64\0",
|
||||
[IORING_OP_MADVISE] = "madvise\0",
|
||||
[IORING_OP_SEND] = "sendto\0",
|
||||
[IORING_OP_RECV] = "recvfrom\0",
|
||||
[IORING_OP_OPENAT2] = "openat2\0",
|
||||
[IORING_OP_EPOLL_CTL] = "epoll_ctl\0",
|
||||
[IORING_OP_SPLICE] = "splice\0",
|
||||
[IORING_OP_TEE] = "tee\0",
|
||||
[IORING_OP_SHUTDOWN] = "shutdown\0",
|
||||
[IORING_OP_RENAMEAT] = "rename\0renameat\0renameat2\0",
|
||||
[IORING_OP_UNLINKAT] = "unlink\0unlinkat\0rmdir\0",
|
||||
[IORING_OP_MKDIRAT] = "mkdir\0mkdirat\0",
|
||||
[IORING_OP_SYMLINKAT] = "symlink\0symlinkat\0",
|
||||
[IORING_OP_LINKAT] = "link\0linkat\0",
|
||||
[IORING_OP_FSETXATTR] = "fsetxattr\0",
|
||||
[IORING_OP_SETXATTR] = "setxattr\0setxattrat\0",
|
||||
[IORING_OP_FGETXATTR] = "fgetxattr\0",
|
||||
[IORING_OP_GETXATTR] = "getxattr\0getxattrat\0",
|
||||
[IORING_OP_SOCKET] = "socket\0",
|
||||
[IORING_OP_URING_CMD] = "ioctl\0setsockopt\0getsockopt\0getsockname\0",
|
||||
[IORING_OP_SEND_ZC] = "sendto\0",
|
||||
[IORING_OP_SENDMSG_ZC] = "sendmsg\0",
|
||||
[IORING_OP_READ_MULTISHOT] = "read\0",
|
||||
[IORING_OP_WAITID] = "waitid\0",
|
||||
[IORING_OP_FUTEX_WAIT] = "futex\0futex_time64\0",
|
||||
[IORING_OP_FUTEX_WAKE] = "futex\0futex_time64\0",
|
||||
[IORING_OP_FUTEX_WAITV] = "futex_waitv\0",
|
||||
[IORING_OP_FTRUNCATE] = "ftruncate\0ftruncate64\0",
|
||||
[IORING_OP_BIND] = "bind\0",
|
||||
[IORING_OP_LISTEN] = "listen\0",
|
||||
[IORING_OP_RECV_ZC] = "recvfrom\0",
|
||||
[IORING_OP_EPOLL_WAIT] = "epoll_wait\0epoll_pwait\0epoll_pwait2\0",
|
||||
[IORING_OP_READV_FIXED] = "readv\0preadv\0preadv2\0",
|
||||
[IORING_OP_WRITEV_FIXED] = "writev\0pwritev\0pwritev2\0",
|
||||
[IORING_OP_PIPE] = "pipe\0pipe2\0",
|
||||
[IORING_OP_URING_CMD128] = "ioctl\0setsockopt\0getsockopt\0getsockname\0",
|
||||
};
|
||||
|
||||
DEFINE_STRING_TABLE_LOOKUP_TO_STRING(io_uring_op_syscalls, int);
|
||||
|
||||
/* Filters load 32-bit words from struct io_uring_bpf_ctx in native byte order. Flags and mode of an open
|
||||
* request are 64-bit fields, and all bits that we check are in their lower half. */
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include "forward.h"
|
||||
|
||||
const char* io_uring_op_syscalls_to_string(int i) _const_;
|
||||
|
||||
int io_uring_bpf_filter_supported(void);
|
||||
|
||||
int io_uring_restrict_ops(Set *ops, bool allow_list);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <linux/io_uring.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <stdlib.h>
|
||||
@@ -230,6 +231,34 @@ TEST(filter_sets) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(io_uring_op_syscalls) {
|
||||
_cleanup_hashmap_free_ Hashmap *system_service = NULL;
|
||||
|
||||
ASSERT_NOT_NULL(system_service = hashmap_new(/* hash_ops= */ NULL));
|
||||
if (is_seccomp_available())
|
||||
ASSERT_OK(seccomp_filter_set_add_by_name(system_service, /* add= */ true, "@system-service"));
|
||||
|
||||
for (int op = 0; op < IORING_OP_LAST; op++) {
|
||||
const char *syscalls = io_uring_op_syscalls_to_string(op);
|
||||
|
||||
if (!syscalls)
|
||||
continue;
|
||||
|
||||
NULSTR_FOREACH(name, syscalls) {
|
||||
log_debug("io_uring opcode %i: %s", op, name);
|
||||
ASSERT_TRUE(nulstr_contains(syscall_filter_sets[SYSCALL_FILTER_SET_KNOWN].value, name));
|
||||
|
||||
if (!is_seccomp_available())
|
||||
continue;
|
||||
|
||||
/* SystemCallFilter=@system-service must not deny any io_uring operation. */
|
||||
int id = sym_seccomp_syscall_resolve_name(name);
|
||||
if (id >= 0)
|
||||
ASSERT_TRUE(hashmap_contains(system_service, INT_TO_PTR(id + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(filter_sets_ordered) {
|
||||
/* Ensure "@default" always remains at the beginning of the list */
|
||||
ASSERT_EQ(SYSCALL_FILTER_SET_DEFAULT, 0);
|
||||
|
||||
@@ -49,6 +49,8 @@ IORING_OFF_CQ_RING = 0x8000000
|
||||
IORING_OFF_SQES = 0x10000000
|
||||
|
||||
IORING_OP_NOP = 0
|
||||
IORING_OP_FSYNC = 3
|
||||
IORING_OP_READV = 1
|
||||
IORING_OP_OPENAT = 18
|
||||
IORING_OP_OPENAT2 = 28
|
||||
IORING_OP_SOCKET = 45
|
||||
@@ -103,6 +105,15 @@ if command == "probe":
|
||||
print("supported" if r < 0 and ctypes.get_errno() == 14 else "unsupported")
|
||||
elif command == "nop":
|
||||
submit(IORING_OP_NOP)
|
||||
elif command == "fsync":
|
||||
submit(IORING_OP_FSYNC, fd=os.open(sys.argv[2], os.O_RDONLY | os.O_CLOEXEC))
|
||||
elif command == "readv":
|
||||
fd = os.open(sys.argv[2], os.O_RDONLY | os.O_CLOEXEC)
|
||||
buf = ctypes.create_string_buffer(1)
|
||||
class IOVec(ctypes.Structure):
|
||||
_fields_ = [("base", ctypes.c_void_p), ("length", ctypes.c_size_t)]
|
||||
iov = IOVec(ctypes.addressof(buf), 1)
|
||||
submit(IORING_OP_READV, fd=fd, addr=ctypes.addressof(iov), length=1)
|
||||
elif command == "socket":
|
||||
family = {"unix": socket.AF_UNIX, "inet": socket.AF_INET, "inet6": socket.AF_INET6}[sys.argv[2]]
|
||||
submit(IORING_OP_SOCKET, fd=family, off=socket.SOCK_DGRAM | socket.SOCK_CLOEXEC)
|
||||
@@ -152,5 +163,22 @@ for op in openat openat2; do
|
||||
(( $(run python3 "$HELPER" "$op" "$WORKDIR/suid-$op" 4755) >= 0 ))
|
||||
done
|
||||
|
||||
# SystemCallFilter= applies to the io_uring operations that can do the work of the denied system calls.
|
||||
# The cases set SystemCallErrorNumber=, because python3 itself can make a system call that the filter denies.
|
||||
# With the default action, the filter would kill python3 before the helper prints the result.
|
||||
[[ "$(run -p SystemCallFilter=~socket -p SystemCallErrorNumber=EPERM python3 "$HELPER" socket unix)" == "$EACCES" ]]
|
||||
[[ "$(run -p SystemCallFilter=~socket -p SystemCallErrorNumber=EPERM python3 "$HELPER" nop)" == 0 ]]
|
||||
(( $(run -p SystemCallFilter=@system-service -p SystemCallErrorNumber=EPERM python3 "$HELPER" socket unix) >= 0 ))
|
||||
[[ "$(run -p SystemCallFilter=@system-service -p SystemCallFilter=~@network-io -p SystemCallErrorNumber=EPERM python3 "$HELPER" socket unix)" == "$EACCES" ]]
|
||||
# An allow list denies system calls that are listed with an error number.
|
||||
[[ "$(run -p SystemCallFilter=@system-service -p SystemCallFilter=~socket:EPERM -p SystemCallErrorNumber=EPERM python3 "$HELPER" socket unix)" == "$EACCES" ]]
|
||||
# The dynamic loader uses pread64(), so denying preadv() lets Python start.
|
||||
[[ "$(run python3 "$HELPER" readv /dev/null)" == 0 ]]
|
||||
[[ "$(run -p SystemCallFilter=~preadv -p SystemCallErrorNumber=EPERM python3 "$HELPER" readv /dev/null)" == "$EACCES" ]]
|
||||
# A system call that is denied with an error number of 0 pretends to succeed, which io_uring can't do.
|
||||
[[ "$(run -p SystemCallFilter=~fsync -p SystemCallErrorNumber=EPERM python3 "$HELPER" fsync "$HELPER")" == "$EACCES" ]]
|
||||
[[ "$(run -p SystemCallFilter=~fsync:0 python3 "$HELPER" fsync "$HELPER")" == 0 ]]
|
||||
[[ "$(run -p SystemCallFilter=~io_uring_setup -p SystemCallErrorNumber=ENOSYS python3 "$HELPER" nop)" == "setup -38" ]]
|
||||
|
||||
# Services that run as an unprivileged user get the filters as well.
|
||||
[[ "$(run -p User=testuser -p RestrictAddressFamilies=AF_UNIX python3 "$HELPER" socket inet)" == "$EACCES" ]]
|
||||
Reference in new issue
Block a user