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