diff --git a/src/core/varlink-unit.c b/src/core/varlink-unit.c index c90ff987e8..da0c0d234a 100644 --- a/src/core/varlink-unit.c +++ b/src/core/varlink-unit.c @@ -18,12 +18,14 @@ #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; diff --git a/test/units/TEST-26-SYSTEMCTL.sh b/test/units/TEST-26-SYSTEMCTL.sh index d489cc3ec8..93afa655a2 100755 --- a/test/units/TEST-26-SYSTEMCTL.sh +++ b/test/units/TEST-26-SYSTEMCTL.sh @@ -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 \