]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sysext: add polkit support to varlink service 39796/head
authorLuca Boccassi <luca.boccassi@gmail.com>
Wed, 12 Nov 2025 22:19:12 +0000 (22:19 +0000)
committerLuca Boccassi <bluca@debian.org>
Wed, 17 Dec 2025 23:22:05 +0000 (23:22 +0000)
src/shared/varlink-io.systemd.sysext.c
src/sysext/io.systemd.sysext.policy [new file with mode: 0644]
src/sysext/meson.build
src/sysext/sysext.c
test/units/TEST-50-DISSECT.dissect.sh
test/units/TEST-50-DISSECT.sh
units/systemd-sysext.socket

index 90eb8177d1e9cf37bcba7f7f773cb8d166d3d7df..13d39a89ff5573d45522b9f833e691865506db5b 100644 (file)
@@ -1,5 +1,6 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include "bus-polkit.h"
 #include "varlink-io.systemd.sysext.h"
 
 static SD_VARLINK_DEFINE_ENUM_TYPE(
@@ -19,24 +20,28 @@ static SD_VARLINK_DEFINE_METHOD(
                 SD_VARLINK_DEFINE_INPUT_BY_TYPE(class, ImageClass, SD_VARLINK_NULLABLE),
                 SD_VARLINK_DEFINE_INPUT(force, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
                 SD_VARLINK_DEFINE_INPUT(noReload, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
-                SD_VARLINK_DEFINE_INPUT(noexec, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE));
+                SD_VARLINK_DEFINE_INPUT(noexec, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
+                VARLINK_DEFINE_POLKIT_INPUT);
 
 static SD_VARLINK_DEFINE_METHOD(
                 Unmerge,
                 SD_VARLINK_DEFINE_INPUT_BY_TYPE(class, ImageClass, SD_VARLINK_NULLABLE),
-                SD_VARLINK_DEFINE_INPUT(noReload, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE));
+                SD_VARLINK_DEFINE_INPUT(noReload, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
+                VARLINK_DEFINE_POLKIT_INPUT);
 
 static SD_VARLINK_DEFINE_METHOD(
                 Refresh,
                 SD_VARLINK_DEFINE_INPUT_BY_TYPE(class, ImageClass, SD_VARLINK_NULLABLE),
                 SD_VARLINK_DEFINE_INPUT(force, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
                 SD_VARLINK_DEFINE_INPUT(noReload, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
-                SD_VARLINK_DEFINE_INPUT(noexec, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE));
+                SD_VARLINK_DEFINE_INPUT(noexec, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
+                VARLINK_DEFINE_POLKIT_INPUT);
 
 static SD_VARLINK_DEFINE_METHOD_FULL(
                 List,
                 SD_VARLINK_REQUIRES_MORE,
                 SD_VARLINK_DEFINE_INPUT_BY_TYPE(class, ImageClass, SD_VARLINK_NULLABLE),
+                VARLINK_DEFINE_POLKIT_INPUT,
                 SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(Class, ImageClass, 0),
                 SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(Type, ImageType, 0),
                 SD_VARLINK_DEFINE_OUTPUT(Name, SD_VARLINK_STRING, 0),
diff --git a/src/sysext/io.systemd.sysext.policy b/src/sysext/io.systemd.sysext.policy
new file mode 100644 (file)
index 0000000..317b9d4
--- /dev/null
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
+<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+        "https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
+
+<!--
+  SPDX-License-Identifier: LGPL-2.1-or-later
+
+  This file is part of systemd.
+
+  systemd is free software; you can redistribute it and/or modify it
+  under the terms of the GNU Lesser General Public License as published by
+  the Free Software Foundation; either version 2.1 of the License, or
+  (at your option) any later version.
+-->
+
+<policyconfig>
+
+        <vendor>The systemd Project</vendor>
+        <vendor_url>https://systemd.io</vendor_url>
+
+        <action id="io.systemd.sysext.manage">
+                <description gettext-domain="systemd">Allow managing (merging, unmerging, ...) of system extension images.</description>
+                <message gettext-domain="systemd">Authentication is required for an application to manage a system extension image.</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+        </action>
+
+        <action id="io.systemd.sysext.read">
+                <description gettext-domain="systemd">Allow reading (listing, ...) of system extension images.</description>
+                <message gettext-domain="systemd">Authentication is required for an application to list system extension images.</message>
+                <defaults>
+                        <allow_any>yes</allow_any>
+                        <allow_inactive>yes</allow_inactive>
+                        <allow_active>yes</allow_active>
+                </defaults>
+        </action>
+
+        <action id="io.systemd.confext.manage">
+                <description gettext-domain="systemd">Allow managing (merging, unmerging, ...) of configuration extension images.</description>
+                <message gettext-domain="systemd">Authentication is required for an application to manage a configuration extension image.</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+        </action>
+
+        <action id="io.systemd.confext.read">
+                <description gettext-domain="systemd">Allow reading (listing, ...) of configuration extension images.</description>
+                <message gettext-domain="systemd">Authentication is required for an application to list configuration extension images.</message>
+                <defaults>
+                        <allow_any>yes</allow_any>
+                        <allow_inactive>yes</allow_inactive>
+                        <allow_active>yes</allow_active>
+                </defaults>
+        </action>
+</policyconfig>
index daaabec777a72a02b672562402c1140ec4042e54..75d06163c0c5eb8388592ec5278c8b84f29a6635 100644 (file)
@@ -18,4 +18,7 @@ if conf.get('ENABLE_SYSEXT') == 1
         install_symlink('systemd-confext',
                         pointing_to : 'systemd-sysext',
                         install_dir : bindir)
+
+        install_data('io.systemd.sysext.policy',
+                install_dir : polkitpolicydir)
 endif
index 2f6bee58c46d5222118110f0f9ce89cc938b9b03..7b60dd346b50cf3d3d584adf3b332b357ad5f5c3 100644 (file)
@@ -14,6 +14,7 @@
 #include "blkid-util.h"
 #include "blockdev-util.h"
 #include "build.h"
+#include "bus-polkit.h"
 #include "bus-unit-util.h"
 #include "bus-util.h"
 #include "capability-util.h"
@@ -117,6 +118,8 @@ static const struct {
         const char *full_identifier;
         const char *short_identifier;
         const char *short_identifier_plural;
+        const char *polkit_rw_action_id;
+        const char *polkit_ro_action_id;
         const char *blurb;
         const char *dot_directory_name;
         const char *directory_name;
@@ -132,6 +135,8 @@ static const struct {
                 .full_identifier = "systemd-sysext",
                 .short_identifier = "sysext",
                 .short_identifier_plural = "extensions",
+                .polkit_rw_action_id = "io.systemd.sysext.manage",
+                .polkit_ro_action_id = "io.systemd.sysext.read",
                 .blurb = "Merge system extension images into /usr/ and /opt/.",
                 .dot_directory_name = ".systemd-sysext",
                 .level_env = "SYSEXT_LEVEL",
@@ -146,6 +151,8 @@ static const struct {
                 .full_identifier = "systemd-confext",
                 .short_identifier = "confext",
                 .short_identifier_plural = "confexts",
+                .polkit_rw_action_id = "io.systemd.confext.manage",
+                .polkit_ro_action_id = "io.systemd.confext.read",
                 .blurb = "Merge configuration extension images into /etc/.",
                 .dot_directory_name = ".systemd-confext",
                 .level_env = "CONFEXT_LEVEL",
@@ -623,13 +630,16 @@ static int vl_method_unmerge(sd_varlink *link, sd_json_variant *parameters, sd_v
         static const sd_json_dispatch_field dispatch_table[] = {
                 { "class",    SD_JSON_VARIANT_STRING,  sd_json_dispatch_const_string, offsetof(MethodUnmergeParameters, class),     0 },
                 { "noReload", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool,      offsetof(MethodUnmergeParameters, no_reload), 0 },
+                VARLINK_DISPATCH_POLKIT_FIELD,
                 {}
         };
         MethodUnmergeParameters p = {
                 .no_reload = -1,
         };
+        Hashmap **polkit_registry = ASSERT_PTR(userdata);
         _cleanup_strv_free_ char **hierarchies = NULL;
         ImageClass image_class = arg_image_class;
+        bool no_reload;
         int r;
 
         assert(link);
@@ -638,13 +648,24 @@ static int vl_method_unmerge(sd_varlink *link, sd_json_variant *parameters, sd_v
         if (r != 0)
                 return r;
 
+        no_reload = p.no_reload >= 0 ? p.no_reload : arg_no_reload;
+
         r = parse_image_class_parameter(link, p.class, &image_class, &hierarchies);
         if (r < 0)
                 return r;
 
-        r = unmerge(image_class,
-                    hierarchies ?: arg_hierarchies,
-                    p.no_reload >= 0 ? p.no_reload : arg_no_reload);
+        r = varlink_verify_polkit_async(
+                        link,
+                        /* bus= */ NULL,
+                        image_class_info[image_class].polkit_rw_action_id,
+                        (const char**) STRV_MAKE(
+                                "verb", "unmerge",
+                                "noReload", one_zero(no_reload)),
+                        polkit_registry);
+        if (r <= 0)
+                return r;
+
+        r = unmerge(image_class, hierarchies ?: arg_hierarchies, no_reload);
         if (r < 0)
                 return r;
 
@@ -2261,6 +2282,7 @@ static int parse_merge_parameters(sd_varlink *link, sd_json_variant *parameters,
                 { "force",    SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate,     offsetof(MethodMergeParameters, force),     0 },
                 { "noReload", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate,     offsetof(MethodMergeParameters, no_reload), 0 },
                 { "noexec",   SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate,     offsetof(MethodMergeParameters, noexec),    0 },
+                VARLINK_DISPATCH_POLKIT_FIELD,
                 {}
         };
 
@@ -2272,6 +2294,7 @@ static int parse_merge_parameters(sd_varlink *link, sd_json_variant *parameters,
 }
 
 static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+        Hashmap **polkit_registry = ASSERT_PTR(userdata);
         _cleanup_hashmap_free_ Hashmap *images = NULL;
         MethodMergeParameters p = {
                 .force = -1,
@@ -2280,7 +2303,8 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var
         };
         _cleanup_strv_free_ char **hierarchies = NULL;
         ImageClass image_class = arg_image_class;
-        int r;
+        bool force, no_reload;
+        int r, noexec;
 
         assert(link);
 
@@ -2292,6 +2316,23 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var
         if (r < 0)
                 return r;
 
+        force = p.force >= 0 ? p.force : arg_force;
+        no_reload = p.no_reload >= 0 ? p.no_reload : arg_no_reload;
+        noexec = p.noexec >= 0 ? p.noexec : arg_noexec;
+
+        r = varlink_verify_polkit_async(
+                        link,
+                        /* bus= */ NULL,
+                        image_class_info[image_class].polkit_rw_action_id,
+                        (const char**) STRV_MAKE(
+                                "verb", "merge",
+                                "force", one_zero(force),
+                                "noReload", one_zero(no_reload),
+                                "noexec", one_zero(noexec > 0)),
+                        polkit_registry);
+        if (r <= 0)
+                return r;
+
         r = image_discover_and_read_metadata(image_class, &images);
         if (r < 0)
                 return r;
@@ -2306,12 +2347,7 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var
         if (r > 0)
                 return sd_varlink_errorbo(link, "io.systemd.sysext.AlreadyMerged", SD_JSON_BUILD_PAIR_STRING("hierarchy", which));
 
-        r = merge(image_class,
-                  hierarchies ?: arg_hierarchies,
-                  p.force >= 0 ? p.force : arg_force,
-                  p.no_reload >= 0 ? p.no_reload : arg_no_reload,
-                  p.noexec >= 0 ? p.noexec : arg_noexec,
-                  images);
+        r = merge(image_class, hierarchies ?: arg_hierarchies, force, no_reload, noexec, images);
         if (r < 0)
                 return r;
 
@@ -2381,9 +2417,11 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v
                 .no_reload = -1,
                 .noexec = -1,
         };
+        Hashmap **polkit_registry = ASSERT_PTR(userdata);
         _cleanup_strv_free_ char **hierarchies = NULL;
         ImageClass image_class = arg_image_class;
-        int r;
+        bool force, no_reload;
+        int r, noexec;
 
         assert(link);
 
@@ -2395,11 +2433,24 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v
         if (r < 0)
                 return r;
 
-        r = refresh(image_class,
-                    hierarchies ?: arg_hierarchies,
-                    p.force >= 0 ? p.force : arg_force,
-                    p.no_reload >= 0 ? p.no_reload : arg_no_reload,
-                    p.noexec >= 0 ? p.noexec : arg_noexec);
+        force = p.force >= 0 ? p.force : arg_force;
+        no_reload = p.no_reload >= 0 ? p.no_reload : arg_no_reload;
+        noexec = p.noexec >= 0 ? p.noexec : arg_noexec;
+
+        r = varlink_verify_polkit_async(
+                        link,
+                        /* bus= */ NULL,
+                        image_class_info[image_class].polkit_rw_action_id,
+                        (const char**) STRV_MAKE(
+                                "verb", "refresh",
+                                "force", one_zero(force),
+                                "noReload", one_zero(no_reload),
+                                "noexec", one_zero(noexec > 0)),
+                        polkit_registry);
+        if (r <= 0)
+                return r;
+
+        r = refresh(image_class, hierarchies ?: arg_hierarchies, force, no_reload, noexec);
         if (r < 0)
                 return r;
 
@@ -2445,9 +2496,11 @@ static int vl_method_list(sd_varlink *link, sd_json_variant *parameters, sd_varl
 
         static const sd_json_dispatch_field dispatch_table[] = {
                 { "class", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, 0, 0 },
+                VARLINK_DISPATCH_POLKIT_FIELD,
                 {}
         };
         _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+        Hashmap **polkit_registry = ASSERT_PTR(userdata);
         int r;
 
         assert(link);
@@ -2462,6 +2515,15 @@ static int vl_method_list(sd_varlink *link, sd_json_variant *parameters, sd_varl
         if (r < 0)
                 return r;
 
+        r = varlink_verify_polkit_async(
+                        link,
+                        /* bus= */ NULL,
+                        image_class_info[image_class].polkit_ro_action_id,
+                        (const char**) STRV_MAKE("verb", "list"),
+                        polkit_registry);
+        if (r <= 0)
+                return r;
+
         _cleanup_hashmap_free_ Hashmap *images = NULL;
         r = image_discover(RUNTIME_SCOPE_SYSTEM, image_class, arg_root, &images);
         if (r < 0)
@@ -2727,10 +2789,14 @@ static int run(int argc, char *argv[]) {
 
         if (arg_varlink) {
                 _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL;
+                _cleanup_hashmap_free_ Hashmap *polkit_registry = NULL;
 
                 /* Invocation as Varlink service */
 
-                r = varlink_server_new(&varlink_server, SD_VARLINK_SERVER_ROOT_ONLY, NULL);
+                r = varlink_server_new(
+                                &varlink_server,
+                                SD_VARLINK_SERVER_ACCOUNT_UID|SD_VARLINK_SERVER_INHERIT_USERDATA,
+                                &polkit_registry);
                 if (r < 0)
                         return log_error_errno(r, "Failed to allocate Varlink server: %m");
 
index 5dd26281abfa2506f018fa6bff1f0a43f7672558..47ea82c3b77c2b75bbd36e9160513e11e9f6e9eb 100755 (executable)
@@ -811,6 +811,33 @@ grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file
 varlinkctl call /run/systemd/io.systemd.sysext io.systemd.sysext.Unmerge '{}'
 (! grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file )
 
+# And again, but unprivileged, if we have pidfds and polkit support
+# Also check that the required policy is installed, as packages might be out of date with the test
+if systemd-analyze compare-versions "$(uname -r)" ge 6.5 && \
+        systemd-analyze compare-versions "$(pkcheck --version | awk '{print $3}')" ge 124 && \
+        test -f /usr/share/polkit-1/actions/io.systemd.sysext.policy; then
+    mkdir -p /etc/polkit-1/rules.d
+    cat >/etc/polkit-1/rules.d/sysext-unpriv.rules <<'EOF'
+polkit.addRule(function(action, subject) {
+    if (action.id == "io.systemd.sysext.manage" &&
+        subject.user == "testuser") {
+        return polkit.Result.YES;
+    }
+});
+EOF
+    systemctl try-reload-or-restart polkit.service
+    run0 -u testuser varlinkctl call --more /run/systemd/io.systemd.sysext io.systemd.sysext.List '{}'
+    (! grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file )
+    run0 -u testuser varlinkctl call /run/systemd/io.systemd.sysext io.systemd.sysext.Merge '{"allowInteractiveAuthentication": true}'
+    grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file
+    run0 -u testuser varlinkctl call /run/systemd/io.systemd.sysext io.systemd.sysext.Refresh '{"allowInteractiveAuthentication": true}'
+    grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file
+    run0 -u testuser varlinkctl call /run/systemd/io.systemd.sysext io.systemd.sysext.Unmerge '{"allowInteractiveAuthentication": true}'
+    (! grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file )
+    rm -f /etc/polkit-1/rules.d/sysext-unpriv.rules
+    systemctl try-reload-or-restart polkit.service
+fi
+
 # Check that extensions cannot contain os-release
 mkdir -p /run/extensions/app-reject/usr/lib/{extension-release.d/,systemd/system}
 echo "ID=_any" >/run/extensions/app-reject/usr/lib/extension-release.d/extension-release.app-reject
index b42aa6e7b323de8b443ccfecc432bb2c922c955a..40b502529782d398d0a0d6a5d8291620eaecba8d 100755 (executable)
@@ -26,6 +26,8 @@ at_exit() {
 
     rm -rf "$IMAGE_DIR"
 
+    rm -f /etc/polkit-1/rules.d/sysext-unpriv.rules
+
     loginctl disable-linger testuser
 }
 
index 78475cf1967ff813bd186a6f1580ab8767a216d0..97d1695780e65953e7a5059422b86a45c51ed852 100644 (file)
@@ -18,7 +18,7 @@ ConditionCapability=CAP_SYS_ADMIN
 [Socket]
 ListenStream=/run/systemd/io.systemd.sysext
 FileDescriptorName=varlink
-SocketMode=0600
+SocketMode=0666
 Accept=yes
 MaxConnectionsPerSource=16