]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
core: add User,Group,SupplementaryGroups,Nice to varlink Unit.StartTransient
authorMichael Vogt <michael@amutable.com>
Mon, 18 May 2026 16:32:23 +0000 (18:32 +0200)
committerMichael Vogt <michael@amutable.com>
Wed, 20 May 2026 13:27:20 +0000 (15:27 +0200)
This commit adds more writable fields to the io.systemd.Unit.StartTransient
varlink method. With this its possible to set:
User,Group,SupplementaryGroups,Nice values.

Plus tests for them.

src/core/varlink-unit.c
test/units/TEST-26-SYSTEMCTL.sh

index c90ff987e8405214a9fd0ec97249078b66f75168..da0c0d234a51e23e8cb9a5875a67175dd6ef1696 100644 (file)
 #include "manager.h"
 #include "path-util.h"
 #include "pidref.h"
+#include "process-util.h"
 #include "selinux-access.h"
 #include "service.h"
 #include "set.h"
 #include "strv.h"
 #include "unit-name.h"
 #include "unit.h"
+#include "user-util.h"
 #include "varlink-automount.h"
 #include "varlink-cgroup.h"
 #include "varlink-common.h"
@@ -685,6 +687,11 @@ typedef struct TransientExecContextParameters {
         bool set_credentials_encrypted_set;
         TransientSetCredential *set_credentials_encrypted;
         size_t n_set_credentials_encrypted;
+
+        const char *user;
+        const char *group;
+        char **supplementary_groups;
+        int nice;        /* INT_MAX means unset */
 } TransientExecContextParameters;
 
 static void transient_set_credential_array_free(TransientSetCredential *items, size_t n) {
@@ -696,6 +703,7 @@ static void transient_set_credential_array_free(TransientSetCredential *items, s
 static void transient_exec_context_parameters_done(TransientExecContextParameters *p) {
         assert(p);
         strv_free(p->environment);
+        strv_free(p->supplementary_groups);
         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);
 }
@@ -862,12 +870,16 @@ static int dispatch_transient_set_credential_encrypted(const char *name, sd_json
 }
 
 static int dispatch_transient_exec_context(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
-        /* Key names compatible with D-Bus property names */
+        /* Key names compatible with D-Bus property names. */
         static const sd_json_dispatch_field exec_dispatch[] = {
-                { "WorkingDirectory",       SD_JSON_VARIANT_OBJECT,        dispatch_transient_working_directory,      0, 0 },
-                { "Environment",            _SD_JSON_VARIANT_TYPE_INVALID, dispatch_transient_environment,            0, 0 },
-                { "SetCredential",          SD_JSON_VARIANT_ARRAY,         dispatch_transient_set_credential,         0, 0 },
-                { "SetCredentialEncrypted", SD_JSON_VARIANT_ARRAY,         dispatch_transient_set_credential_encrypted, 0, 0 },
+                { "Environment",            _SD_JSON_VARIANT_TYPE_INVALID, dispatch_transient_environment,              0,                                                              0             },
+                { "Group",                  SD_JSON_VARIANT_STRING,        json_dispatch_const_user_group_name,         offsetof(TransientExecContextParameters, group),                SD_JSON_RELAX },
+                { "Nice",                   SD_JSON_VARIANT_INTEGER,       sd_json_dispatch_int32,                      offsetof(TransientExecContextParameters, nice),                 0             },
+                { "SetCredential",          SD_JSON_VARIANT_ARRAY,         dispatch_transient_set_credential,           0,                                                              0             },
+                { "SetCredentialEncrypted", SD_JSON_VARIANT_ARRAY,         dispatch_transient_set_credential_encrypted, 0,                                                              0             },
+                { "SupplementaryGroups",    SD_JSON_VARIANT_ARRAY,         sd_json_dispatch_strv,                       offsetof(TransientExecContextParameters, supplementary_groups), 0             },
+                { "User",                   SD_JSON_VARIANT_STRING,        json_dispatch_const_user_group_name,         offsetof(TransientExecContextParameters, user),                 SD_JSON_RELAX },
+                { "WorkingDirectory",       SD_JSON_VARIANT_OBJECT,        dispatch_transient_working_directory,        0,                                                              0             },
                 {}
         };
 
@@ -1026,6 +1038,56 @@ static int transient_exec_context_apply_properties(Unit *u, ExecContext *c, Tran
                         return r;
         }
 
+        /* User/Group names already validated by json_dispatch_const_user_group_name() in the dispatch table:
+         * either NULL or a non-empty validated name. */
+        if (p->user) {
+                r = free_and_strdup(&c->user, p->user);
+                if (r < 0)
+                        return r;
+
+                unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE|UNIT_ESCAPE_SPECIFIERS, "User",
+                                    "User=%s", p->user);
+        }
+
+        if (p->group) {
+                r = free_and_strdup(&c->group, p->group);
+                if (r < 0)
+                        return r;
+
+                unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE|UNIT_ESCAPE_SPECIFIERS, "Group",
+                                    "Group=%s", p->group);
+        }
+
+        if (p->supplementary_groups) {
+                _cleanup_free_ char *joined = NULL;
+
+                STRV_FOREACH(g, p->supplementary_groups)
+                        if (!isempty(*g) && !valid_user_group_name(*g, VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX))
+                                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Invalid supplementary group name: %s", *g);
+
+                r = strv_extend_strv(&c->supplementary_groups, p->supplementary_groups, /* filter_duplicates= */ true);
+                if (r < 0)
+                        return r;
+
+                joined = strv_join(p->supplementary_groups, " ");
+                if (!joined)
+                        return -ENOMEM;
+
+                unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE|UNIT_ESCAPE_SPECIFIERS, "SupplementaryGroups",
+                                    "SupplementaryGroups=%s", joined);
+        }
+
+        if (p->nice != INT_MAX) {
+                if (!nice_is_valid(p->nice))
+                        return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid Nice= value: %i", p->nice);
+
+                c->nice = p->nice;
+                c->nice_set = true;
+
+                unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE, "Nice", "Nice=%i", p->nice);
+        }
+
         return 0;
 }
 
@@ -1125,6 +1187,7 @@ int vl_method_start_transient_unit(sd_varlink *link, sd_json_variant *parameters
                 .notify_unit_changes = -1,
                 .context.service.type = _SERVICE_TYPE_INVALID,
                 .context.service.remain_after_exit = -1,
+                .context.exec.nice = INT_MAX,
         };
         Manager *manager = ASSERT_PTR(userdata);
         const char *bad_field = NULL;
index d489cc3ec8083db4fa884ca0b4c260cc19c7fc00..93afa655a2094ab963edb81869ffe6b3b8129a78 100755 (executable)
@@ -656,6 +656,26 @@ timeout 30 bash -c "until systemctl is-active varlink-transient-cred.service; do
 grep '^secret-value$' "$CRED_OUTPUT" >/dev/null
 rm -f "$CRED_OUTPUT"
 
+# Exec.User, Exec.Group, Exec.SupplementaryGroups, Exec.Nice
+# The nobody group is different on different distros so resolve here.
+NOBODY_GROUP=$(id -gn nobody)
+defer_transient_cleanup varlink-transient-ids.service
+ids_payload=$(jq -cn --arg g "$NOBODY_GROUP" \
+    '{context:{ID:"varlink-transient-ids.service",
+               Exec:{User:"nobody",Group:$g,SupplementaryGroups:[$g],Nice:5},
+               Service:{Type:"oneshot",RemainAfterExit:true,
+                        ExecStart:[{path:"/bin/true"}]}}}')
+result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient "$ids_payload")
+echo "$result" | jq -e '.context.Exec.User == "nobody"'
+echo "$result" | jq --arg g "$NOBODY_GROUP" -e '.context.Exec.Group == $g'
+echo "$result" | jq --arg g "$NOBODY_GROUP" -e '.context.Exec.SupplementaryGroups == [$g]'
+echo "$result" | jq -e '.context.Exec.Nice == 5'
+timeout 30 bash -c 'until systemctl is-active varlink-transient-ids.service; do sleep 0.5; done'
+systemctl show -P User varlink-transient-ids.service | grep '^nobody$' >/dev/null
+systemctl show -P Group varlink-transient-ids.service | grep "^${NOBODY_GROUP}$" >/dev/null
+systemctl show -P SupplementaryGroups varlink-transient-ids.service | grep "${NOBODY_GROUP}" >/dev/null
+systemctl show -P Nice varlink-transient-ids.service | grep '^5$' >/dev/null
+
 # Error cases: verify specific varlink error types
 set +o pipefail
 varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
@@ -677,6 +697,14 @@ varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
 defer_transient_cleanup varlink-transient-bad-env.service
 varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
     '{"context":{"ID":"varlink-transient-bad-env.service","Exec":{"Environment":["not_an_env_var"]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "io.systemd.Unit.BadUnitSetting"
+# Invalid User= name is rejected at JSON dispatch time as a parameter error
+defer_transient_cleanup varlink-transient-bad-user.service
+varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-bad-user.service","Exec":{"User":"bad/user"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "Invalid argument"
+# Out-of-range Nice= value is rejected
+defer_transient_cleanup varlink-transient-bad-nice.service
+varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-bad-nice.service","Exec":{"Nice":100},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "io.systemd.Unit.BadUnitSetting"
 # Invalid credential ID
 defer_transient_cleanup varlink-transient-bad-cred-id.service
 varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \