core: add absolute-path properties to varlink StartTransient

Similar to PR#42360 this commit adds missing properties for
absolute path handling in io.systemd.Unit.StartTransient for
the `Exec` context and a macro helper to share the common code.

The properties added are:
IPCNamespacePath, NetworkNamespacePath, RootDirectory, RootHashPath,
RootHashSignaturePath, RootImage, RootVerity, UserNamespacePath,
RootMStack.

Note that RootHashPath, RootHashSignaturePath need custom apply
functions because the varlink name "RootHashPath" differs from the
name that needs to be written into the unit file ("RootHash=")
and the iovec must be cleared.
This commit is contained in:
Michael Vogt
2026-07-01 16:14:37 +02:00
committed by Zbigniew Jędrzejewski-Szmek
parent ef7b9f679b
commit ab344eb750
2 changed files with 120 additions and 2 deletions
+102
View File
@@ -691,6 +691,16 @@ typedef struct TransientExecContextParameters {
const char *user;
const char *group;
char **supplementary_groups;
/* Absolute-path strings: must validate via path_is_absolute(). */
const char *ipc_namespace_path;
const char *network_namespace_path;
const char *root_directory;
const char *root_hash_path;
const char *root_hash_sig_path;
const char *root_image;
const char *root_mstack;
const char *root_verity;
const char *user_namespace_path;
bool nice_set;
int nice;
bool oom_score_adjust_set;
@@ -1210,6 +1220,83 @@ DEFINE_APPLY_EXEC_TRISTATE_BOOL(restrict_realtime, "RestrictRealtime");
DEFINE_APPLY_EXEC_TRISTATE_BOOL(restrict_suid_sgid, "RestrictSUIDSGID");
DEFINE_APPLY_EXEC_TRISTATE_BOOL(root_ephemeral, "RootEphemeral");
/* Generate an apply fn for an exec-context absolute-path string: validate via path_is_absolute(),
* copy into ExecContext, write "Name=<path>". Mirrors D-Bus bus_set_transient_path. */
#define DEFINE_APPLY_EXEC_ABSOLUTE_PATH(field, JsonName) \
static int apply_exec_##field(Unit *u, ExecContext *c, TransientExecContextParameters *p) { \
int r; \
assert(c); \
assert(p); \
if (!p->field) \
return 0; \
if (!path_is_absolute(p->field)) \
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), \
JsonName ": expects an absolute path"); \
r = free_and_strdup(&c->field, p->field); \
if (r < 0) \
return r; \
unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE|UNIT_ESCAPE_SPECIFIERS, JsonName, \
JsonName "=%s", p->field); \
return 0; \
}
DEFINE_APPLY_EXEC_ABSOLUTE_PATH(ipc_namespace_path, "IPCNamespacePath");
DEFINE_APPLY_EXEC_ABSOLUTE_PATH(network_namespace_path, "NetworkNamespacePath");
DEFINE_APPLY_EXEC_ABSOLUTE_PATH(root_directory, "RootDirectory");
DEFINE_APPLY_EXEC_ABSOLUTE_PATH(root_image, "RootImage");
DEFINE_APPLY_EXEC_ABSOLUTE_PATH(root_mstack, "RootMStack");
DEFINE_APPLY_EXEC_ABSOLUTE_PATH(root_verity, "RootVerity");
DEFINE_APPLY_EXEC_ABSOLUTE_PATH(user_namespace_path, "UserNamespacePath");
/* RootHashPath and RootHashSignaturePath need custom apply fns: the corresponding unit-file
* directive is "RootHash="/"RootHashSignature=" (not the JSON name), and setting the path form
* must clear the byte-buffer (iovec) form to match the D-Bus semantics. */
static int apply_exec_root_hash_path(Unit *u, ExecContext *c, TransientExecContextParameters *p) {
int r;
assert(c);
assert(p);
if (!p->root_hash_path)
return 0;
if (!path_is_absolute(p->root_hash_path))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
"RootHashPath: expects an absolute path");
r = free_and_strdup(&c->root_hash_path, p->root_hash_path);
if (r < 0)
return r;
iovec_done(&c->root_hash);
unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE|UNIT_ESCAPE_SPECIFIERS, "RootHash",
"RootHash=%s", p->root_hash_path);
return 0;
}
static int apply_exec_root_hash_sig_path(Unit *u, ExecContext *c, TransientExecContextParameters *p) {
int r;
assert(c);
assert(p);
if (!p->root_hash_sig_path)
return 0;
if (!path_is_absolute(p->root_hash_sig_path))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
"RootHashSignaturePath: expects an absolute path");
r = free_and_strdup(&c->root_hash_sig_path, p->root_hash_sig_path);
if (r < 0)
return r;
iovec_done(&c->root_hash_sig);
unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE|UNIT_ESCAPE_SPECIFIERS, "RootHashSignature",
"RootHashSignature=%s", p->root_hash_sig_path);
return 0;
}
/* Per-property descriptor for fields of the Exec context.
*
* json_name - JSON key inside the Exec object (e.g. "Nice").
@@ -1254,9 +1341,20 @@ typedef struct TransientExecProperty {
{ json, "Exec." json, SD_JSON_VARIANT_STRING, dispatch_fn, \
offsetof(TransientExecContextParameters, field), json_flags, apply_exec_##field }
/* Absolute-path string property: parsed via sd_json_dispatch_const_string + path_is_absolute(). */
#define EXEC_PROPERTY_ABSOLUTE_PATH(json, field) \
{ json, "Exec." json, SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, \
offsetof(TransientExecContextParameters, field), 0, apply_exec_##field }
static const TransientExecProperty exec_properties[] = {
EXEC_PROPERTY ("WorkingDirectory", SD_JSON_VARIANT_OBJECT, dispatch_transient_working_directory, 0, 0, apply_exec_working_directory),
EXEC_PROPERTY_ABSOLUTE_PATH("RootDirectory", root_directory),
EXEC_PROPERTY_ABSOLUTE_PATH("RootImage", root_image),
EXEC_PROPERTY_ABSOLUTE_PATH("RootMStack", root_mstack),
EXEC_PROPERTY_TRISTATE_BOOL("RootEphemeral", root_ephemeral),
EXEC_PROPERTY ("RootHashPath", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(TransientExecContextParameters, root_hash_path), 0, apply_exec_root_hash_path),
EXEC_PROPERTY ("RootHashSignaturePath", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(TransientExecContextParameters, root_hash_sig_path), 0, apply_exec_root_hash_sig_path),
EXEC_PROPERTY_ABSOLUTE_PATH("RootVerity", root_verity),
EXEC_PROPERTY_STRING ("User", user, json_dispatch_const_user_group_name, SD_JSON_RELAX),
EXEC_PROPERTY_STRING ("Group", group, json_dispatch_const_user_group_name, SD_JSON_RELAX),
EXEC_PROPERTY_TRISTATE_BOOL("DynamicUser", dynamic_user),
@@ -1266,6 +1364,9 @@ static const TransientExecProperty exec_properties[] = {
EXEC_PROPERTY ("OOMScoreAdjust", SD_JSON_VARIANT_INTEGER, dispatch_transient_oom_score_adjust, 0, 0, apply_exec_oom_score_adjust),
EXEC_PROPERTY_TRISTATE_BOOL("IgnoreSIGPIPE", ignore_sigpipe),
EXEC_PROPERTY ("Nice", SD_JSON_VARIANT_INTEGER, dispatch_transient_nice, 0, 0, apply_exec_nice),
EXEC_PROPERTY_ABSOLUTE_PATH("NetworkNamespacePath", network_namespace_path),
EXEC_PROPERTY_ABSOLUTE_PATH("IPCNamespacePath", ipc_namespace_path),
EXEC_PROPERTY_ABSOLUTE_PATH("UserNamespacePath", user_namespace_path),
EXEC_PROPERTY_TRISTATE_BOOL("LockPersonality", lock_personality),
EXEC_PROPERTY_TRISTATE_BOOL("MemoryDenyWriteExecute", memory_deny_write_execute),
EXEC_PROPERTY_TRISTATE_BOOL("RestrictRealtime", restrict_realtime),
@@ -1278,6 +1379,7 @@ static const TransientExecProperty exec_properties[] = {
#undef EXEC_PROPERTY
#undef EXEC_PROPERTY_TRISTATE_BOOL
#undef EXEC_PROPERTY_STRING
#undef EXEC_PROPERTY_ABSOLUTE_PATH
/* Tristate-bool fields default to -1 (= absent). Default-zeroed slots would look "explicitly false"
* to the apply path. Initialize all tristate fields here so adding a new tristate property only
@@ -253,6 +253,17 @@ systemctl show -P UMask varlink-transient-procctl.service | grep '^0022$' >/dev/
systemctl show -P NoNewPrivileges varlink-transient-procctl.service | grep '^yes$' >/dev/null
systemctl show -P MemoryDenyWriteExecute varlink-transient-procctl.service | grep '^yes$' >/dev/null
# Exec.RootHashPath / Exec.RootHashSignaturePath: the unit-file directive must be
# "RootHash=" / "RootHashSignature=" (not the JSON name), otherwise the next daemon-reload
# would drop the setting as an unknown key.
defer_transient_cleanup varlink-transient-roothash.service
varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-roothash.service","Exec":{"RootHashPath":"/etc/hostname","RootHashSignaturePath":"/etc/machine-id"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' >/dev/null
fragment=$(systemctl show -P FragmentPath varlink-transient-roothash.service)
test -n "$fragment"
grep '^RootHash=/etc/hostname$' "$fragment" >/dev/null
grep '^RootHashSignature=/etc/machine-id$' "$fragment" >/dev/null
# Error cases: verify specific varlink error types
set +o pipefail
varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
@@ -303,6 +314,11 @@ defer_transient_cleanup varlink-transient-bad-oom.service
expect_invalid_parameter \
'{"context":{"ID":"varlink-transient-bad-oom.service","Exec":{"OOMScoreAdjust":9999},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
"Exec.OOMScoreAdjust"
# Relative RootDirectory path is rejected
defer_transient_cleanup varlink-transient-bad-rd.service
expect_invalid_parameter \
'{"context":{"ID":"varlink-transient-bad-rd.service","Exec":{"RootDirectory":"relative/path"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
"Exec.RootDirectory"
# Invalid credential ID
defer_transient_cleanup varlink-transient-bad-cred-id.service
expect_invalid_parameter \
@@ -319,9 +335,9 @@ varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
# Unknown field in Exec is rejected as PropertyNotSupported
defer_transient_cleanup varlink-transient-unknown-exec.service
unsupported_exec=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-unknown-exec.service","Exec":{"RootDirectory":"/tmp"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true)
'{"context":{"ID":"varlink-transient-unknown-exec.service","Exec":{"AmbientCapabilities":["cap_net_raw"]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true)
echo "$unsupported_exec" | grep "io.systemd.Unit.PropertyNotSupported"
echo "$unsupported_exec" | grep "Exec.RootDirectory"
echo "$unsupported_exec" | grep "Exec.AmbientCapabilities"
# Service field declared in the IDL but not yet settable at creation is rejected as PropertyNotSupported,
# and the offending sub-property is identified
defer_transient_cleanup varlink-transient-unknown-service.service