diff --git a/docs/TRANSIENT-SETTINGS.md b/docs/TRANSIENT-SETTINGS.md
index 3ea51163499..d4e75c559d7 100644
--- a/docs/TRANSIENT-SETTINGS.md
+++ b/docs/TRANSIENT-SETTINGS.md
@@ -212,6 +212,7 @@ All execution-related settings are available for transient units.
✓ RestrictRealtime=
✓ RestrictSUIDSGID=
✓ RestrictAddressFamilies=
+✓ IOUringFilter=
✓ RootHash=
✓ RootHashSignature=
✓ RootVerity=
diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml
index e84dd415b50..087315e97e7 100644
--- a/man/org.freedesktop.systemd1.xml
+++ b/man/org.freedesktop.systemd1.xml
@@ -3537,6 +3537,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly (bas) RestrictAddressFamilies = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly (bas) IOUringFilter = ...;
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(sst) RuntimeDirectorySymlink = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s RuntimeDirectoryPreserve = '...';
@@ -4188,6 +4190,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
+
+
@@ -4930,6 +4934,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
+
+
@@ -5858,6 +5864,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly (bas) RestrictAddressFamilies = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly (bas) IOUringFilter = ...;
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(sst) RuntimeDirectorySymlink = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s RuntimeDirectoryPreserve = '...';
@@ -6521,6 +6529,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
+
+
@@ -7239,6 +7249,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
+
+
@@ -7986,6 +7998,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly (bas) RestrictAddressFamilies = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly (bas) IOUringFilter = ...;
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(sst) RuntimeDirectorySymlink = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s RuntimeDirectoryPreserve = '...';
@@ -8573,6 +8587,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
+
+
@@ -9193,6 +9209,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
+
+
@@ -10062,6 +10080,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly (bas) RestrictAddressFamilies = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly (bas) IOUringFilter = ...;
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(sst) RuntimeDirectorySymlink = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s RuntimeDirectoryPreserve = '...';
@@ -10631,6 +10651,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
+
+
@@ -11233,6 +11255,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
+
+
@@ -13032,6 +13056,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
LUOSession was added in version 262.
RestartRandomizedDelayUSec was added in version 262.
ConfigurationDirectorySymlink was added in version 263.
+ IOUringFilter was added in version 263.
Socket Unit Objects
@@ -13112,6 +13137,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
XAttrListen, and
XAttrAccept were added in version 262.
ConfigurationDirectorySymlink was added in version 263.
+ IOUringFilter was added in version 263.
Mount Unit Objects
@@ -13184,6 +13210,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
CPUSetPartition, and
OOMRules were added in version 261.
ConfigurationDirectorySymlink was added in version 263.
+ IOUringFilter was added in version 263.
Swap Unit Objects
@@ -13254,6 +13281,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
CPUSetPartition, and
OOMRules were added in version 261.
ConfigurationDirectorySymlink was added in version 263.
+ IOUringFilter was added in version 263.
Slice Unit Objects
diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml
index e130e2ea7e3..7d557e45945 100644
--- a/man/systemd.exec.xml
+++ b/man/systemd.exec.xml
@@ -2526,6 +2526,48 @@ BindReadOnlyPaths=/var/lib/systemd
+
+ IOUringFilter=
+
+ Restricts the
+ io_uring7
+ operations that the processes of this unit may submit. Takes a comma- or space-separated list of
+ operation names. The name of an operation is its IORING_OP_ constant in lower
+ case and without the prefix, for example read for
+ IORING_OP_READ, or uring_cmd for
+ IORING_OP_URING_CMD. If the list is prefixed with ~, the
+ listed operations are denied (deny-listing). Otherwise, only the listed operations are allowed
+ (allow-listing). An allow list also denies the operations of newer kernels that systemd doesn't
+ know, even though the kernel reports them as supported. Operation names that systemd doesn't know
+ are ignored.
+
+ This option may be specified more than once, in which case the lists are merged. If the first
+ assignment starts with ~, the list is a deny list, otherwise it is an allow list.
+ Later assignments of the other type remove the listed operations from the list. If the empty string
+ is assigned, the list is reset and all prior assignments have no effect. This setting does not
+ affect commands prefixed with +.
+
+ Requests for denied operations fail with EACCES. 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. IORING_REGISTER_RESTRICTIONS without an
+ io_uring instance fails with EPERM for the processes of the unit.
+
+ This setting requires a kernel with io_uring BPF filters (Linux 7.0 or newer). On older
+ kernels, io_uring_setup() fails with ENOSYS for the
+ processes of the unit instead, so that programs fall back to regular system calls. If seccomp is not
+ available either, this setting has no effect.
+
+ Example: to deny passthrough commands to drivers and zero-copy network operations, use:
+ IOUringFilter=~uring_cmd uring_cmd128 send_zc sendmsg_zc recv_zc
+
+
+ SystemCallFilter=, RestrictAddressFamilies=, and
+ RestrictSUIDSGID= apply to the equivalent io_uring operations as well. To deny
+ io_uring entirely, use SystemCallFilter=~io_uring_setup.
+
+
+
+
RestrictFileSystems=
@@ -3008,7 +3050,8 @@ RestrictNamespaces=~cgroup net
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, IORING_REGISTER_RESTRICTIONS without an io_uring instance fails with
- EPERM for the processes of the unit.
+ EPERM for the processes of the unit. To filter individual io_uring operations,
+ use IOUringFilter=.
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
diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c
index 72bf4229021..15bb4fbf1ff 100644
--- a/src/core/dbus-execute.c
+++ b/src/core/dbus-execute.c
@@ -23,6 +23,7 @@
#include "hexdecoct.h"
#include "hostname-util.h"
#include "image-policy.h"
+#include "io-uring-filter.h"
#include "ioprio-util.h"
#include "iovec-util.h"
#include "journal-file.h"
@@ -329,6 +330,41 @@ static int property_get_smack_process_label(
return sd_bus_message_append(reply, "(bs)", c->smack_process_label_ignore, c->smack_process_label);
}
+static int property_get_io_uring_filter(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *reterr_error) {
+
+ ExecContext *c = ASSERT_PTR(userdata);
+ _cleanup_strv_free_ char **l = NULL;
+ int r;
+
+ assert(bus);
+ assert(reply);
+
+ r = sd_bus_message_open_container(reply, 'r', "bas");
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append(reply, "b", c->io_uring_filter_allow_list);
+ if (r < 0)
+ return r;
+
+ l = exec_context_get_io_uring_filter(c);
+ if (!l)
+ return -ENOMEM;
+
+ r = sd_bus_message_append_strv(reply, l);
+ if (r < 0)
+ return r;
+
+ return sd_bus_message_close_container(reply);
+}
+
static int property_get_address_families(
sd_bus *bus,
const char *path,
@@ -1356,6 +1392,7 @@ const sd_bus_vtable bus_exec_vtable[] = {
SD_BUS_PROPERTY("Personality", "s", property_get_personality, offsetof(ExecContext, personality), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("LockPersonality", "b", bus_property_get_bool, offsetof(ExecContext, lock_personality), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("RestrictAddressFamilies", "(bas)", property_get_address_families, 0, SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("IOUringFilter", "(bas)", property_get_io_uring_filter, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("RuntimeDirectorySymlink", "a(sst)", property_get_exec_dir_symlink, offsetof(ExecContext, directories[EXEC_DIRECTORY_RUNTIME]), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("RuntimeDirectoryPreserve", "s", bus_property_get_exec_preserve_mode, offsetof(ExecContext, runtime_directory_preserve_mode), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("RuntimeDirectoryMode", "u", bus_property_get_mode, offsetof(ExecContext, directories[EXEC_DIRECTORY_RUNTIME].mode), SD_BUS_VTABLE_PROPERTY_CONST),
@@ -2446,6 +2483,72 @@ int bus_exec_context_set_transient_property(
return 1;
}
+ if (streq(name, "IOUringFilter")) {
+ _cleanup_strv_free_ char **l = NULL;
+ int allow_list;
+
+ r = sd_bus_message_enter_container(message, 'r', "bas");
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read(message, "b", &allow_list);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read_strv(message, &l);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_exit_container(message);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(s, l)
+ if (io_uring_op_from_string(*s) < 0)
+ return sd_bus_error_setf(reterr_error, SD_BUS_ERROR_INVALID_ARGS,
+ "Invalid io_uring operation: %s", *s);
+
+ if (!UNIT_WRITE_FLAGS_NOOP(flags)) {
+ _cleanup_free_ char *joined = NULL;
+
+ if (strv_isempty(l)) {
+ c->io_uring_filter_allow_list = false;
+ c->io_uring_filter = set_free(c->io_uring_filter);
+
+ unit_write_setting(u, flags, name, "IOUringFilter=");
+ return 1;
+ }
+
+ if (!c->io_uring_filter) {
+ c->io_uring_filter = set_new(/* hash_ops= */ NULL);
+ if (!c->io_uring_filter)
+ return -ENOMEM;
+
+ c->io_uring_filter_allow_list = allow_list;
+ }
+
+ STRV_FOREACH(s, l) {
+ int op = io_uring_op_from_string(*s);
+ assert(op >= 0);
+
+ if (allow_list == c->io_uring_filter_allow_list) {
+ r = set_put(c->io_uring_filter, INT_TO_PTR(op + 1));
+ if (r < 0)
+ return r;
+ } else
+ set_remove(c->io_uring_filter, INT_TO_PTR(op + 1));
+ }
+
+ joined = strv_join(l, " ");
+ if (!joined)
+ return -ENOMEM;
+
+ unit_write_settingf(u, flags, name, "IOUringFilter=%s%s", allow_list ? "" : "~", joined);
+ }
+
+ return 1;
+ }
+
if (streq(name, "MountFlags"))
return bus_set_transient_mount_propagation_flag(u, name, &c->mount_propagation_flag, message, flags, reterr_error);
diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c
index 27edd3922ea..cf3d28f3dbf 100644
--- a/src/core/exec-invoke.c
+++ b/src/core/exec-invoke.c
@@ -1756,7 +1756,7 @@ static bool context_has_syscall_logs(const ExecContext *c) {
static bool context_has_seccomp(const ExecContext *c) {
assert(c);
- /* We need NNP if we have any form of seccomp and are unprivileged */
+ /* We need NNP if we have any form of seccomp or io_uring filters and are unprivileged */
return c->lock_personality ||
c->memory_deny_write_execute ||
c->private_devices ||
@@ -1771,7 +1771,8 @@ static bool context_has_seccomp(const ExecContext *c) {
c->restrict_suid_sgid ||
!set_isempty(c->syscall_archs) ||
exec_context_has_syscall_filters(c) ||
- context_has_syscall_logs(c);
+ context_has_syscall_logs(c) ||
+ exec_context_io_uring_filter_set(c);
}
static bool context_has_no_new_privileges(const ExecContext *c) {
@@ -1868,6 +1869,45 @@ static int apply_io_uring_suid_sgid(const ExecContext *c, const ExecParameters *
return io_uring_restrict_suid_sgid();
}
+static int apply_io_uring_filter(const ExecContext *c, const ExecParameters *p) {
+ assert(c);
+ assert(p);
+
+ if (!exec_context_io_uring_filter_set(c))
+ return 0;
+
+ if (check_io_uring_bpf_filters(c, p, "IOUringFilter="))
+ return io_uring_restrict_ops(c->io_uring_filter, c->io_uring_filter_allow_list);
+
+ if (syscall_filter_denies_io_uring(c))
+ return 0;
+
+#if HAVE_SECCOMP
+ /* The kernel can't filter io_uring operations, so we deny io_uring_setup() to enforce
+ * IOUringFilter=. Programs fall back to regular system calls when io_uring_setup() fails with
+ * ENOSYS. */
+ if (is_seccomp_available()) {
+ _cleanup_hashmap_free_ Hashmap *filter = NULL;
+ int r;
+
+ log_debug("Denying io_uring_setup() to enforce IOUringFilter=.");
+
+ filter = hashmap_new(/* hash_ops= */ NULL);
+ if (!filter)
+ return -ENOMEM;
+
+ r = seccomp_filter_set_add_by_name(filter, /* add= */ true, "io_uring_setup");
+ if (r < 0)
+ return r;
+
+ return seccomp_load_syscall_filter_set_raw(SCMP_ACT_ALLOW, filter, SCMP_ACT_ERRNO(ENOSYS), /* log_missing= */ false);
+ }
+#endif
+
+ log_debug("Seccomp is not available, not applying IOUringFilter=.");
+ return 0;
+}
+
#if HAVE_SECCOMP
static bool seccomp_allows_drop_privileges(const ExecContext *c) {
@@ -7083,6 +7123,12 @@ int exec_invoke(
return log_error_errno(r, "Failed to apply SUID/SGID restrictions to io_uring: %m");
}
+ r = apply_io_uring_filter(context, params);
+ if (r < 0) {
+ *exit_status = EXIT_SECCOMP;
+ return log_error_errno(r, "Failed to apply io_uring filters: %m");
+ }
+
#if HAVE_SECCOMP
r = apply_io_uring_syscall_filter(context, params);
if (r < 0) {
diff --git a/src/core/execute-serialize.c b/src/core/execute-serialize.c
index a7477a83072..2221d078198 100644
--- a/src/core/execute-serialize.c
+++ b/src/core/execute-serialize.c
@@ -2442,6 +2442,17 @@ static int exec_context_serialize(const ExecContext *c, FILE *f) {
if (r < 0)
return r;
+ void *op;
+ SET_FOREACH(op, c->io_uring_filter) {
+ r = serialize_item_format(f, "exec-context-io-uring-filter", "%d", PTR_TO_INT(op) - 1);
+ if (r < 0)
+ return r;
+ }
+
+ r = serialize_bool_elide(f, "exec-context-io-uring-filter-allow-list", c->io_uring_filter_allow_list);
+ if (r < 0)
+ return r;
+
r = serialize_item(f, "exec-context-user-namespace-path", c->user_namespace_path);
if (r < 0)
return r;
@@ -3496,6 +3507,23 @@ static int exec_context_deserialize(ExecContext *c, FILE *f) {
if (r < 0)
return r;
c->address_families_allow_list = r;
+ } else if ((val = startswith(l, "exec-context-io-uring-filter="))) {
+ int op;
+
+ r = safe_atoi(val, &op);
+ if (r < 0)
+ return r;
+ if (op < 0)
+ return -EINVAL;
+
+ r = set_ensure_put(&c->io_uring_filter, /* hash_ops= */ NULL, INT_TO_PTR(op + 1));
+ if (r < 0)
+ return r;
+ } else if ((val = startswith(l, "exec-context-io-uring-filter-allow-list="))) {
+ r = parse_boolean(val);
+ if (r < 0)
+ return r;
+ c->io_uring_filter_allow_list = r;
} else if ((val = startswith(l, "exec-context-network-namespace-path="))) {
r = free_and_strdup(&c->network_namespace_path, val);
if (r < 0)
diff --git a/src/core/execute.c b/src/core/execute.c
index acdf60195b3..3a7ca78239c 100644
--- a/src/core/execute.c
+++ b/src/core/execute.c
@@ -508,7 +508,8 @@ int exec_spawn(
if (FLAGS_SET(params->flags, EXEC_APPLY_SANDBOXING) &&
!FLAGS_SET(command->flags, EXEC_COMMAND_FULLY_PRIVILEGED) &&
(exec_context_has_address_families(context) ||
- context->restrict_suid_sgid || exec_context_has_syscall_filters(context))) {
+ context->restrict_suid_sgid || exec_context_has_syscall_filters(context) ||
+ exec_context_io_uring_filter_set(context))) {
r = io_uring_bpf_filter_supported();
if (r < 0)
return log_unit_error_errno(unit, r, "Failed to check whether io_uring BPF filters are supported: %m");
@@ -749,6 +750,7 @@ void exec_context_done(ExecContext *c) {
c->syscall_archs = set_free(c->syscall_archs);
c->syscall_log = hashmap_free(c->syscall_log);
c->address_families = set_free(c->address_families);
+ c->io_uring_filter = set_free(c->io_uring_filter);
FOREACH_ARRAY(d, c->directories, _EXEC_DIRECTORY_TYPE_MAX)
exec_directory_done(d);
@@ -1666,6 +1668,14 @@ void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix) {
}
#endif
+ if (exec_context_io_uring_filter_set(c)) {
+ _cleanup_strv_free_ char **l = exec_context_get_io_uring_filter(c);
+ _cleanup_free_ char *joined = strv_join(l, " ");
+
+ fprintf(f, "%sIOUringFilter: %s%s\n",
+ prefix, c->io_uring_filter_allow_list ? "" : "~", strnull(joined));
+ }
+
if (c->user_namespace_path)
fprintf(f,
"%sUserNamespacePath: %s\n",
@@ -2117,6 +2127,28 @@ char** exec_context_get_syscall_log(const ExecContext *c) {
return l ? TAKE_PTR(l) : strv_new(NULL);
}
+char** exec_context_get_io_uring_filter(const ExecContext *c) {
+ _cleanup_strv_free_ char **l = NULL;
+ void *op;
+
+ assert(c);
+
+ SET_FOREACH(op, c->io_uring_filter) {
+ const char *name;
+
+ name = io_uring_op_to_string(PTR_TO_INT(op) - 1);
+ if (!name)
+ continue;
+
+ if (strv_extend(&l, name) < 0)
+ return NULL;
+ }
+
+ strv_sort(l);
+
+ return l ? TAKE_PTR(l) : strv_new(NULL);
+}
+
char** exec_context_get_address_families(const ExecContext *c) {
_cleanup_strv_free_ char **l = NULL;
void *af;
@@ -2166,6 +2198,13 @@ bool exec_context_restrict_filesystems_set(const ExecContext *c) {
!set_isempty(c->restrict_filesystems);
}
+bool exec_context_io_uring_filter_set(const ExecContext *c) {
+ assert(c);
+
+ return c->io_uring_filter_allow_list ||
+ !set_isempty(c->io_uring_filter);
+}
+
bool exec_context_with_rootfs(const ExecContext *c) {
assert(c);
diff --git a/src/core/execute.h b/src/core/execute.h
index 08c2cc1fea4..e0fc7cfb9cc 100644
--- a/src/core/execute.h
+++ b/src/core/execute.h
@@ -383,6 +383,9 @@ typedef struct ExecContext {
bool address_families_allow_list:1;
Set *address_families;
+ bool io_uring_filter_allow_list:1;
+ Set *io_uring_filter; /* io_uring opcodes plus one */
+
char *user_namespace_path;
char *network_namespace_path;
char *ipc_namespace_path;
@@ -620,11 +623,13 @@ char** exec_context_get_syscall_filter(const ExecContext *c);
char** exec_context_get_syscall_archs(const ExecContext *c);
char** exec_context_get_syscall_log(const ExecContext *c);
char** exec_context_get_address_families(const ExecContext *c);
+char** exec_context_get_io_uring_filter(const ExecContext *c);
char** exec_context_get_restrict_filesystems(const ExecContext *c);
bool exec_context_has_address_families(const ExecContext *c);
bool exec_context_has_syscall_filters(const ExecContext *c);
bool exec_context_restrict_namespaces_set(const ExecContext *c);
bool exec_context_restrict_filesystems_set(const ExecContext *c);
+bool exec_context_io_uring_filter_set(const ExecContext *c);
bool exec_context_with_rootfs(const ExecContext *c);
bool exec_context_with_rootfs_strict(const ExecContext *c);
diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in
index 84a885110cb..9e7b8aa3224 100644
--- a/src/core/load-fragment-gperf.gperf.in
+++ b/src/core/load-fragment-gperf.gperf.in
@@ -98,6 +98,7 @@
{% endif %}
{{type}}.DelegateNamespaces, config_parse_namespace_flags, 0, offsetof({{type}}, exec_context.delegate_namespaces)
{{type}}.RestrictFileSystems, config_parse_restrict_filesystems, 0, offsetof({{type}}, exec_context)
+{{type}}.IOUringFilter, config_parse_io_uring_filter, 0, offsetof({{type}}, exec_context)
{{type}}.LimitCPU, config_parse_rlimit, RLIMIT_CPU, offsetof({{type}}, exec_context.rlimit)
{{type}}.LimitFSIZE, config_parse_rlimit, RLIMIT_FSIZE, offsetof({{type}}, exec_context.rlimit)
{{type}}.LimitDATA, config_parse_rlimit, RLIMIT_DATA, offsetof({{type}}, exec_context.rlimit)
diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c
index 386db061a06..0ed6b327504 100644
--- a/src/core/load-fragment.c
+++ b/src/core/load-fragment.c
@@ -35,6 +35,7 @@
#include "hashmap.h"
#include "hexdecoct.h"
#include "hostname-util.h"
+#include "io-uring-filter.h"
#include "ioprio-util.h"
#include "iovec-util.h"
#include "ip-protocol-list.h"
@@ -3576,6 +3577,76 @@ int config_parse_address_families(
}
#endif
+int config_parse_io_uring_filter(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ ExecContext *c = ASSERT_PTR(data);
+ bool invert = false;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ c->io_uring_filter = set_free(c->io_uring_filter);
+ c->io_uring_filter_allow_list = false;
+ return 0;
+ }
+
+ if (rvalue[0] == '~') {
+ invert = true;
+ rvalue++;
+ }
+
+ if (!c->io_uring_filter) {
+ c->io_uring_filter = set_new(/* hash_ops= */ NULL);
+ if (!c->io_uring_filter)
+ return log_oom();
+
+ c->io_uring_filter_allow_list = !invert;
+ }
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *word = NULL;
+ int op;
+
+ r = extract_first_word(&p, &word, /* separators= */ WHITESPACE ",", EXTRACT_UNQUOTE);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid syntax, ignoring: %s", rvalue);
+ return 0;
+ }
+ if (r == 0)
+ return 0;
+
+ op = io_uring_op_from_string(word);
+ if (op < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, op,
+ "Unknown io_uring operation, ignoring: %s", word);
+ continue;
+ }
+
+ /* An assignment of the other list type removes the listed operations from the list. */
+ if (invert != c->io_uring_filter_allow_list) {
+ r = set_put(c->io_uring_filter, INT_TO_PTR(op + 1));
+ if (r < 0)
+ return log_oom();
+ } else
+ set_remove(c->io_uring_filter, INT_TO_PTR(op + 1));
+ }
+}
+
int config_parse_namespace_flags(
const char *unit,
const char *filename,
@@ -6420,6 +6491,7 @@ void unit_dump_config_items(FILE *f) {
{ config_parse_syscall_log, "SYSCALLS" },
{ config_parse_address_families, "FAMILIES" },
#endif
+ { config_parse_io_uring_filter, "OPERATIONS" },
{ config_parse_namespace_flags, "NAMESPACES" },
{ config_parse_restrict_filesystems, "FILESYSTEMS" },
{ config_parse_cg_weight, "WEIGHT" },
diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h
index 15dd411cc5a..fcdb23e4c52 100644
--- a/src/core/load-fragment.h
+++ b/src/core/load-fragment.h
@@ -99,6 +99,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_exec_selinux_context);
CONFIG_PARSER_PROTOTYPE(config_parse_exec_apparmor_profile);
CONFIG_PARSER_PROTOTYPE(config_parse_exec_smack_process_label);
CONFIG_PARSER_PROTOTYPE(config_parse_address_families);
+CONFIG_PARSER_PROTOTYPE(config_parse_io_uring_filter);
CONFIG_PARSER_PROTOTYPE(config_parse_exec_preserve_mode);
CONFIG_PARSER_PROTOTYPE(config_parse_exec_directories);
CONFIG_PARSER_PROTOTYPE(config_parse_exec_quota);
diff --git a/src/core/varlink-execute.c b/src/core/varlink-execute.c
index 49e8094619b..fae5f79330d 100644
--- a/src/core/varlink-execute.c
+++ b/src/core/varlink-execute.c
@@ -11,6 +11,7 @@
#include "exec-credential.h"
#include "execute.h"
#include "image-policy.h"
+#include "io-uring-filter.h"
#include "ioprio-util.h"
#include "iovec-util.h"
#include "json-util.h"
@@ -437,6 +438,28 @@ static int address_families_build_json(sd_json_variant **ret, const char *name,
SD_JSON_BUILD_PAIR_STRV("addressFamilies", l));
}
+static int io_uring_filter_build_json(sd_json_variant **ret, const char *name, void *userdata) {
+ _cleanup_strv_free_ char **l = NULL;
+ ExecContext *c = ASSERT_PTR(userdata);
+
+ assert(ret);
+ assert(name);
+
+ if (!exec_context_io_uring_filter_set(c)) {
+ *ret = NULL;
+ return 0;
+ }
+
+ l = exec_context_get_io_uring_filter(c);
+ if (!l)
+ return -ENOMEM;
+
+ return sd_json_buildo(
+ ret,
+ SD_JSON_BUILD_PAIR_BOOLEAN("isAllowList", c->io_uring_filter_allow_list),
+ SD_JSON_BUILD_PAIR_STRV("operations", l));
+}
+
static int restrict_filesystems_build_json(sd_json_variant **ret, const char *name, void *userdata) {
ExecContext *c = ASSERT_PTR(userdata);
_cleanup_strv_free_ char **l = NULL;
@@ -927,6 +950,7 @@ int unit_exec_context_build_json(sd_json_variant **ret, const char *name, void *
JSON_BUILD_PAIR_ENUM("ProtectControlGroups", protect_control_groups_to_string(c->protect_control_groups)),
JSON_BUILD_PAIR_CALLBACK_NON_NULL("RestrictAddressFamilies", address_families_build_json, c),
JSON_BUILD_PAIR_CALLBACK_NON_NULL("RestrictFileSystems", restrict_filesystems_build_json, c),
+ JSON_BUILD_PAIR_CALLBACK_NON_NULL("IOUringFilter", io_uring_filter_build_json, c),
JSON_BUILD_PAIR_CALLBACK_NON_NULL("RestrictNamespaces", namespace_flags_build_json, ULONG_TO_PTR(c->restrict_namespaces)),
JSON_BUILD_PAIR_CALLBACK_NON_NULL("DelegateNamespaces", namespace_flags_build_json, ULONG_TO_PTR(c->delegate_namespaces)),
JSON_BUILD_PAIR_ENUM("PrivatePBF", private_bpf_to_string(c->private_bpf)),
@@ -1020,6 +1044,9 @@ struct TransientExecContextParameters {
const char *user;
const char *group;
char **supplementary_groups;
+ bool io_uring_filter_set;
+ bool io_uring_filter_allow_list;
+ char **io_uring_filter;
const char *pam_name;
/* Absolute-path strings: must validate via path_is_absolute(). */
const char *ipc_namespace_path;
@@ -1096,6 +1123,7 @@ TransientExecContextParameters* transient_exec_context_parameters_free(Transient
strv_free(p->environment);
strv_free(p->supplementary_groups);
+ strv_free(p->io_uring_filter);
transient_set_credential_array_free(p->set_credentials, p->n_set_credentials);
transient_set_credential_array_free(p->set_credentials_encrypted, p->n_set_credentials_encrypted);
free(p->bad_field);
@@ -1104,6 +1132,35 @@ TransientExecContextParameters* transient_exec_context_parameters_free(Transient
return mfree(p);
}
+static int dispatch_transient_io_uring_filter(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
+ static const sd_json_dispatch_field dispatch[] = {
+ { "isAllowList", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(TransientExecContextParameters, io_uring_filter_allow_list), SD_JSON_MANDATORY },
+ { "operations", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(TransientExecContextParameters, io_uring_filter), SD_JSON_MANDATORY },
+ {}
+ };
+
+ TransientExecContextParameters *p = ASSERT_PTR(userdata);
+ const char *bad_field = NULL;
+ int r;
+
+ r = sd_json_dispatch_full(variant, dispatch, /* bad= */ NULL, flags, p, &bad_field);
+ if (r < 0) {
+ if (bad_field)
+ varlink_propagate_bad_field(bad_field, /* child_bad_field= */ NULL, &p->bad_field);
+ return r;
+ }
+
+ STRV_FOREACH(s, p->io_uring_filter)
+ if (io_uring_op_from_string(*s) < 0) {
+ varlink_propagate_bad_field("operations", /* child_bad_field= */ NULL, &p->bad_field);
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "IOUringFilter: invalid io_uring operation: %s", *s);
+ }
+
+ p->io_uring_filter_set = true;
+ return 0;
+}
+
static int dispatch_transient_working_directory(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
static const sd_json_dispatch_field dispatch[] = {
{ "path", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(TransientWorkingDirectory, path), 0 },
@@ -1337,6 +1394,45 @@ static int apply_exec_umask(Unit *u, ExecContext *c, TransientExecContextParamet
return 0;
}
+static int apply_exec_io_uring_filter(Unit *u, ExecContext *c, TransientExecContextParameters *p) {
+ _cleanup_free_ char *joined = NULL;
+ int r;
+
+ assert(c);
+ assert(p);
+
+ if (!p->io_uring_filter_set)
+ return 0;
+
+ c->io_uring_filter = set_free(c->io_uring_filter);
+ c->io_uring_filter_allow_list = false;
+
+ /* Like in unit files and on D-Bus, an empty list resets the setting. */
+ if (strv_isempty(p->io_uring_filter)) {
+ unit_write_setting(u, UNIT_RUNTIME|UNIT_PRIVATE, "IOUringFilter", "IOUringFilter=");
+ return 0;
+ }
+
+ c->io_uring_filter_allow_list = p->io_uring_filter_allow_list;
+
+ STRV_FOREACH(s, p->io_uring_filter) {
+ int op = io_uring_op_from_string(*s);
+ assert(op >= 0);
+
+ r = set_ensure_put(&c->io_uring_filter, /* hash_ops= */ NULL, INT_TO_PTR(op + 1));
+ if (r < 0)
+ return r;
+ }
+
+ joined = strv_join(p->io_uring_filter, " ");
+ if (!joined)
+ return -ENOMEM;
+
+ unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE, "IOUringFilter", "IOUringFilter=\nIOUringFilter=%s%s",
+ p->io_uring_filter_allow_list ? "" : "~", joined);
+ return 0;
+}
+
static int apply_exec_working_directory(Unit *u, ExecContext *c, TransientExecContextParameters *p) {
assert(p);
@@ -1553,6 +1649,7 @@ static const TransientExecProperty exec_properties[] = {
EXEC_PROPERTY_TRISTATE_BOOL("RestrictRealtime", restrict_realtime),
EXEC_PROPERTY_TRISTATE_BOOL("RestrictSUIDSGID", restrict_suid_sgid),
EXEC_PROPERTY_TRISTATE_BOOL("RemoveIPC", remove_ipc),
+ EXEC_PROPERTY ("IOUringFilter", io_uring_filter, SD_JSON_VARIANT_OBJECT),
EXEC_PROPERTY_SETTABLE ("Environment", environment, SD_JSON_VARIANT_ARRAY),
EXEC_PROPERTY_ENUM ("StandardInput", std_input, dispatch_exec_input),
EXEC_PROPERTY_ENUM ("StandardOutput", std_output, dispatch_exec_output),
diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c
index e31eff0d4e1..9c7c460daa3 100644
--- a/src/shared/bus-unit-util.c
+++ b/src/shared/bus-unit-util.c
@@ -2705,6 +2705,7 @@ static const BusProperty execute_properties[] = {
{ "SystemCallFilter", bus_append_filter_list },
{ "SystemCallLog", bus_append_filter_list },
{ "RestrictNetworkInterfaces", bus_append_filter_list },
+ { "IOUringFilter", bus_append_filter_list },
{ "RestrictNamespaces", bus_append_namespace_list },
{ "DelegateNamespaces", bus_append_namespace_list },
{ "BindPaths", bus_append_bind_paths },
diff --git a/src/shared/io-uring-filter.c b/src/shared/io-uring-filter.c
index c82a99eeee3..9d28785f3af 100644
--- a/src/shared/io-uring-filter.c
+++ b/src/shared/io-uring-filter.c
@@ -21,6 +21,76 @@
#include "signal-util.h"
#include "string-table.h"
+static const char* const io_uring_op_table[IORING_OP_LAST] = {
+ [IORING_OP_NOP] = "nop",
+ [IORING_OP_READV] = "readv",
+ [IORING_OP_WRITEV] = "writev",
+ [IORING_OP_FSYNC] = "fsync",
+ [IORING_OP_READ_FIXED] = "read_fixed",
+ [IORING_OP_WRITE_FIXED] = "write_fixed",
+ [IORING_OP_POLL_ADD] = "poll_add",
+ [IORING_OP_POLL_REMOVE] = "poll_remove",
+ [IORING_OP_SYNC_FILE_RANGE] = "sync_file_range",
+ [IORING_OP_SENDMSG] = "sendmsg",
+ [IORING_OP_RECVMSG] = "recvmsg",
+ [IORING_OP_TIMEOUT] = "timeout",
+ [IORING_OP_TIMEOUT_REMOVE] = "timeout_remove",
+ [IORING_OP_ACCEPT] = "accept",
+ [IORING_OP_ASYNC_CANCEL] = "async_cancel",
+ [IORING_OP_LINK_TIMEOUT] = "link_timeout",
+ [IORING_OP_CONNECT] = "connect",
+ [IORING_OP_FALLOCATE] = "fallocate",
+ [IORING_OP_OPENAT] = "openat",
+ [IORING_OP_CLOSE] = "close",
+ [IORING_OP_FILES_UPDATE] = "files_update",
+ [IORING_OP_STATX] = "statx",
+ [IORING_OP_READ] = "read",
+ [IORING_OP_WRITE] = "write",
+ [IORING_OP_FADVISE] = "fadvise",
+ [IORING_OP_MADVISE] = "madvise",
+ [IORING_OP_SEND] = "send",
+ [IORING_OP_RECV] = "recv",
+ [IORING_OP_OPENAT2] = "openat2",
+ [IORING_OP_EPOLL_CTL] = "epoll_ctl",
+ [IORING_OP_SPLICE] = "splice",
+ [IORING_OP_PROVIDE_BUFFERS] = "provide_buffers",
+ [IORING_OP_REMOVE_BUFFERS] = "remove_buffers",
+ [IORING_OP_TEE] = "tee",
+ [IORING_OP_SHUTDOWN] = "shutdown",
+ [IORING_OP_RENAMEAT] = "renameat",
+ [IORING_OP_UNLINKAT] = "unlinkat",
+ [IORING_OP_MKDIRAT] = "mkdirat",
+ [IORING_OP_SYMLINKAT] = "symlinkat",
+ [IORING_OP_LINKAT] = "linkat",
+ [IORING_OP_MSG_RING] = "msg_ring",
+ [IORING_OP_FSETXATTR] = "fsetxattr",
+ [IORING_OP_SETXATTR] = "setxattr",
+ [IORING_OP_FGETXATTR] = "fgetxattr",
+ [IORING_OP_GETXATTR] = "getxattr",
+ [IORING_OP_SOCKET] = "socket",
+ [IORING_OP_URING_CMD] = "uring_cmd",
+ [IORING_OP_SEND_ZC] = "send_zc",
+ [IORING_OP_SENDMSG_ZC] = "sendmsg_zc",
+ [IORING_OP_READ_MULTISHOT] = "read_multishot",
+ [IORING_OP_WAITID] = "waitid",
+ [IORING_OP_FUTEX_WAIT] = "futex_wait",
+ [IORING_OP_FUTEX_WAKE] = "futex_wake",
+ [IORING_OP_FUTEX_WAITV] = "futex_waitv",
+ [IORING_OP_FIXED_FD_INSTALL] = "fixed_fd_install",
+ [IORING_OP_FTRUNCATE] = "ftruncate",
+ [IORING_OP_BIND] = "bind",
+ [IORING_OP_LISTEN] = "listen",
+ [IORING_OP_RECV_ZC] = "recv_zc",
+ [IORING_OP_EPOLL_WAIT] = "epoll_wait",
+ [IORING_OP_READV_FIXED] = "readv_fixed",
+ [IORING_OP_WRITEV_FIXED] = "writev_fixed",
+ [IORING_OP_PIPE] = "pipe",
+ [IORING_OP_NOP128] = "nop128",
+ [IORING_OP_URING_CMD128] = "uring_cmd128",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(io_uring_op, int);
+
/* 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] = {
diff --git a/src/shared/io-uring-filter.h b/src/shared/io-uring-filter.h
index fd72e18c33a..47ec0eeb50e 100644
--- a/src/shared/io-uring-filter.h
+++ b/src/shared/io-uring-filter.h
@@ -3,6 +3,9 @@
#include "forward.h"
+const char* io_uring_op_to_string(int i) _const_;
+int io_uring_op_from_string(const char *s) _pure_;
+
const char* io_uring_op_syscalls_to_string(int i) _const_;
int io_uring_bpf_filter_supported(void);
diff --git a/src/shared/varlink-io.systemd.Unit.c b/src/shared/varlink-io.systemd.Unit.c
index 719f65c8bbb..51fa2853c11 100644
--- a/src/shared/varlink-io.systemd.Unit.c
+++ b/src/shared/varlink-io.systemd.Unit.c
@@ -571,6 +571,13 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE(
SD_VARLINK_FIELD_COMMENT("The list of filesystems"),
SD_VARLINK_DEFINE_FIELD(filesystems, SD_VARLINK_STRING, SD_VARLINK_ARRAY));
+static SD_VARLINK_DEFINE_STRUCT_TYPE(
+ IOUringOperationList,
+ SD_VARLINK_FIELD_COMMENT("Whether the list of io_uring operations is an allow list"),
+ SD_VARLINK_DEFINE_FIELD(isAllowList, SD_VARLINK_BOOL, 0),
+ SD_VARLINK_FIELD_COMMENT("The list of io_uring operations"),
+ SD_VARLINK_DEFINE_FIELD(operations, SD_VARLINK_STRING, SD_VARLINK_ARRAY));
+
static SD_VARLINK_DEFINE_STRUCT_TYPE(
SystemCallList,
SD_VARLINK_FIELD_COMMENT("Whether the list of system calls is an allow list"),
@@ -816,6 +823,8 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE(
SD_VARLINK_DEFINE_FIELD_BY_TYPE(RestrictAddressFamilies, AddressFamilyList, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man"PROJECT_VERSION_STR"systemd.exec.html#RestrictFileSystems="),
SD_VARLINK_DEFINE_FIELD_BY_TYPE(RestrictFilesystems, FilesystemList, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man"PROJECT_VERSION_STR"systemd.exec.html#IOUringFilter="),
+ SD_VARLINK_DEFINE_FIELD_BY_TYPE(IOUringFilter, IOUringOperationList, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man"PROJECT_VERSION_STR"systemd.exec.html#RestrictNamespaces="),
SD_VARLINK_DEFINE_FIELD(RestrictNamespaces, SD_VARLINK_STRING, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man"PROJECT_VERSION_STR"systemd.exec.html#DelegateNamespaces="),
@@ -2120,6 +2129,7 @@ SD_VARLINK_DEFINE_INTERFACE(
&vl_type_TemporaryFilesystem,
&vl_type_AddressFamilyList,
&vl_type_FilesystemList,
+ &vl_type_IOUringOperationList,
&vl_type_SystemCallList,
&vl_type_EnvironmentFile,
&vl_type_LogFilterPattern,
diff --git a/src/systemctl/systemctl-show.c b/src/systemctl/systemctl-show.c
index f8458aa966b..b6c94fa69d0 100644
--- a/src/systemctl/systemctl-show.c
+++ b/src/systemctl/systemctl-show.c
@@ -1276,7 +1276,7 @@ static int print_property(
return 1;
- } else if (STR_IN_SET(name, "SystemCallFilter", "SystemCallLog", "RestrictAddressFamilies", "RestrictNetworkInterfaces", "RestrictFileSystems")) {
+ } else if (STR_IN_SET(name, "SystemCallFilter", "SystemCallLog", "RestrictAddressFamilies", "RestrictNetworkInterfaces", "RestrictFileSystems", "IOUringFilter")) {
_cleanup_strv_free_ char **l = NULL;
int allow_list;
diff --git a/src/test/test-bus-unit-util.c b/src/test/test-bus-unit-util.c
index 463d29e368e..1c35d94dd10 100644
--- a/src/test/test-bus-unit-util.c
+++ b/src/test/test-bus-unit-util.c
@@ -667,6 +667,8 @@ TEST(execute_properties) {
"RestrictFileSystems=ext4",
"RestrictFileSystems=ext4 xfs",
"RestrictFileSystems=~tmpfs",
+ "IOUringFilter=read write",
+ "IOUringFilter=~uring_cmd,socket",
"SystemCallFilter=@system-service",
"SystemCallFilter=read write open close",
"SystemCallFilter=read,write,open,close",
@@ -1456,6 +1458,10 @@ TEST(values_filter_lists) {
"SystemCallLog", "(bas)", "[true,['@system-service']]");
test_setting(UNIT_SERVICE, "RestrictFileSystems=~tmpfs,ext4",
"RestrictFileSystems", "(bas)", "[false,['tmpfs','ext4']]");
+ test_setting(UNIT_SERVICE, "IOUringFilter=read,write",
+ "IOUringFilter", "(bas)", "[true,['read','write']]");
+ test_setting(UNIT_SERVICE, "IOUringFilter=~uring_cmd socket",
+ "IOUringFilter", "(bas)", "[false,['uring_cmd','socket']]");
test_setting(UNIT_SERVICE, "RestrictNetworkInterfaces=~eth0 lo",
"RestrictNetworkInterfaces", "(bas)", "[false,['eth0','lo']]");
test_setting(UNIT_SERVICE, "RestrictNetworkInterfaces=~iface,wo,comma 'iface,w,comma'",
diff --git a/src/test/test-io-uring-filter.c b/src/test/test-io-uring-filter.c
index a136716c29d..36a6c3303a0 100644
--- a/src/test/test-io-uring-filter.c
+++ b/src/test/test-io-uring-filter.c
@@ -103,6 +103,16 @@ static bool have_io_uring_bpf_filters(void) {
if (!have_io_uring_bpf_filters()) \
return (void) log_tests_skipped("io_uring BPF filters are not available")
+TEST(io_uring_op_names) {
+ for (int op = 0; op < IORING_OP_LAST; op++) {
+ const char *name = ASSERT_NOT_NULL(io_uring_op_to_string(op));
+ ASSERT_EQ(io_uring_op_from_string(name), op);
+ }
+
+ ASSERT_NULL(io_uring_op_to_string(IORING_OP_LAST));
+ ASSERT_ERROR(io_uring_op_from_string("foobar"), EINVAL);
+}
+
TEST(restrict_ops_deny_list) {
int r;
diff --git a/src/test/test-load-fragment.c b/src/test/test-load-fragment.c
index fe4d5153faf..d77681aadbb 100644
--- a/src/test/test-load-fragment.c
+++ b/src/test/test-load-fragment.c
@@ -11,6 +11,7 @@
#include "capability-util.h"
#include "cgroup.h"
#include "conf-parser.h"
+#include "execute.h"
#include "fileio.h"
#include "format-util.h"
#include "hashmap.h"
@@ -720,6 +721,36 @@ TEST(config_parse_capability_set) {
ASSERT_EQ(capability_bounding_set, make_cap(CAP_NET_RAW) | make_cap(CAP_NET_ADMIN));
}
+static void check_io_uring_filter(ExecContext *c, const char *rvalue, bool allow_list, const char *expected) {
+ _cleanup_strv_free_ char **l = NULL;
+ _cleanup_free_ char *joined = NULL;
+
+ ASSERT_OK(config_parse_io_uring_filter(/* unit= */ NULL, "fake", 1, "section", 1,
+ "IOUringFilter", /* ltype= */ 0, rvalue,
+ c, /* userdata= */ NULL));
+
+ ASSERT_EQ((bool) c->io_uring_filter_allow_list, allow_list);
+ l = ASSERT_NOT_NULL(exec_context_get_io_uring_filter(c));
+ joined = ASSERT_NOT_NULL(strv_join(l, " "));
+ ASSERT_STREQ(joined, expected);
+}
+
+TEST(config_parse_io_uring_filter) {
+ ExecContext c = {};
+
+ check_io_uring_filter(&c, "read write", /* allow_list= */ true, "read write");
+ check_io_uring_filter(&c, "socket,nop", /* allow_list= */ true, "nop read socket write");
+ check_io_uring_filter(&c, "~write", /* allow_list= */ true, "nop read socket");
+ check_io_uring_filter(&c, "foobar openat", /* allow_list= */ true, "nop openat read socket");
+ check_io_uring_filter(&c, "", /* allow_list= */ false, "");
+
+ check_io_uring_filter(&c, "~uring_cmd socket", /* allow_list= */ false, "socket uring_cmd");
+ check_io_uring_filter(&c, "socket", /* allow_list= */ false, "uring_cmd");
+ check_io_uring_filter(&c, "~send_zc", /* allow_list= */ false, "send_zc uring_cmd");
+
+ c.io_uring_filter = set_free(c.io_uring_filter);
+}
+
TEST(config_parse_rlimit) {
struct rlimit * rl[_RLIMIT_MAX] = {};
diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service
index a3b3aa88c39..9252869e182 100644
--- a/test/fuzz/fuzz-unit-file/directives-all.service
+++ b/test/fuzz/fuzz-unit-file/directives-all.service
@@ -119,6 +119,7 @@ IOAccounting=
IODeviceWeight=
IOReadBandwidthMax=
IOReadIOPSMax=
+IOUringFilter=
IOWeight=
IOWriteBandwidthMax=
IOWriteIOPSMax=
diff --git a/test/units/TEST-07-PID1.io-uring.sh b/test/units/TEST-07-PID1.io-uring.sh
index 7433c77cc45..9d7b0745d32 100755
--- a/test/units/TEST-07-PID1.io-uring.sh
+++ b/test/units/TEST-07-PID1.io-uring.sh
@@ -139,6 +139,8 @@ run -p DynamicUser=yes -p User=io-uring-limits -p LimitNPROC=1 -p RestrictAddres
run -p TasksMax=1 -p RestrictAddressFamilies=AF_UNIX true
if [[ "$(run python3 "$HELPER" probe)" != "supported" ]]; then
+ # Without io_uring BPF filters, IOUringFilter= denies io_uring_setup() with ENOSYS.
+ [[ "$(run -p IOUringFilter=~nop python3 "$HELPER" nop)" == "setup -38" ]]
echo "kernel does not support io_uring BPF filters, skipping" >&2
exit 77
fi
@@ -180,5 +182,19 @@ done
[[ "$(run -p SystemCallFilter=~fsync:0 python3 "$HELPER" fsync "$HELPER")" == 0 ]]
[[ "$(run -p SystemCallFilter=~io_uring_setup -p SystemCallErrorNumber=ENOSYS python3 "$HELPER" nop)" == "setup -38" ]]
+# IOUringFilter= works as a deny list and as an allow list.
+[[ "$(run -p IOUringFilter=~nop python3 "$HELPER" nop)" == "$EACCES" ]]
+(( $(run -p IOUringFilter=~nop python3 "$HELPER" socket unix) >= 0 ))
+[[ "$(run -p IOUringFilter=nop python3 "$HELPER" nop)" == 0 ]]
+[[ "$(run -p IOUringFilter=nop python3 "$HELPER" socket unix)" == "$EACCES" ]]
+[[ "$(run -p IOUringFilter=nop -p IOUringFilter=~nop python3 "$HELPER" nop)" == "$EACCES" ]]
+[[ "$(run -p IOUringFilter=~nop -p IOUringFilter= python3 "$HELPER" nop)" == 0 ]]
+
# Services that run as an unprivileged user get the filters as well.
+[[ "$(run -p User=testuser -p IOUringFilter=~nop python3 "$HELPER" nop)" == "$EACCES" ]]
[[ "$(run -p User=testuser -p RestrictAddressFamilies=AF_UNIX python3 "$HELPER" socket inet)" == "$EACCES" ]]
+
+# The property is visible over D-Bus.
+systemd-run --unit=io-uring-filter-test --remain-after-exit -p IOUringFilter="~uring_cmd socket" true
+[[ "$(systemctl show -P IOUringFilter io-uring-filter-test.service)" == "~socket uring_cmd" ]]
+systemctl stop io-uring-filter-test.service
diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl-unit-start-transient.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit-start-transient.sh
index ee01b0932f2..09db81ed1e3 100755
--- a/test/units/TEST-74-AUX-UTILS.varlinkctl-unit-start-transient.sh
+++ b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit-start-transient.sh
@@ -130,6 +130,9 @@ testcase_dispatch_validation() {
expect_invalid_parameter \
'{"context":{"ID":"varlink-transient-bad-dispatch.service","Exec":{"WorkingDirectory":{"path":"","missingOK":true}}}}' \
"context.Exec.WorkingDirectory.path"
+ expect_invalid_parameter \
+ '{"context":{"ID":"varlink-transient-bad-dispatch.service","Exec":{"IOUringFilter":{"isAllowList":true,"operations":["read","foobar"]}}}}' \
+ "context.Exec.IOUringFilter.operations"
}
testcase_authorization() {
@@ -241,6 +244,14 @@ testcase_exec_properties() {
fragment=$(systemctl show -P FragmentPath varlink-transient-stdio.service)
grep '^StandardInput=null$' "$fragment" >/dev/null
+ # StartTransient accepts Exec.IOUringFilter.
+ defer_transient_cleanup varlink-transient-io-uring.service
+ result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+ '{"context":{"ID":"varlink-transient-io-uring.service","Exec":{"IOUringFilter":{"isAllowList":false,"operations":["uring_cmd","socket"]}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
+ echo "$result" | jq -e '.context.Exec.IOUringFilter == {"isAllowList":false,"operations":["socket","uring_cmd"]}'
+ timeout 30 bash -c 'until systemctl is-active varlink-transient-io-uring.service; do sleep 0.5; done'
+ [[ "$(systemctl show -P IOUringFilter varlink-transient-io-uring.service)" == "~socket uring_cmd" ]]
+
# The combined output types are spelled with a "+" in unit files, but with a "_" in the Varlink IDL (and
# hence in our own replies). The latter spelling must be accepted, so that replies can be fed back in.
defer_transient_cleanup varlink-transient-stdio-combined.service