core: open more fields up to StartTransientUnit via Varlink (#43516)

This is split out of #43058 but makes a ton of sense on its own.
This commit is contained in:
Yu Watanabe
2026-09-23 08:50:36 +09:00
committed by GitHub
10 changed files with 414 additions and 72 deletions
@@ -211,6 +211,99 @@ varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
timeout 30 bash -c 'until systemctl is-active varlink-transient-wd-home.service; do sleep 0.5; done'
systemctl show -P WorkingDirectory varlink-transient-wd-home.service | grep '^~$' >/dev/null
# Exec.StandardInput/StandardOutput/StandardError enum properties and Exec.TTYPath
defer_transient_cleanup varlink-transient-stdio.service
result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-stdio.service","Exec":{"StandardInput":"null","StandardOutput":"null","StandardError":"journal","TTYPath":"/dev/tty2"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
echo "$result" | jq -e '.context.Exec.StandardInput == "null"'
echo "$result" | jq -e '.context.Exec.StandardOutput == "null"'
echo "$result" | jq -e '.context.Exec.StandardError == "journal"'
echo "$result" | jq -e '.context.Exec.TTYPath == "/dev/tty2"'
timeout 30 bash -c 'until systemctl is-active varlink-transient-stdio.service; do sleep 0.5; done'
systemctl show -P StandardInput varlink-transient-stdio.service | grep '^null$' >/dev/null
systemctl show -P StandardOutput varlink-transient-stdio.service | grep '^null$' >/dev/null
systemctl show -P StandardError varlink-transient-stdio.service | grep '^journal$' >/dev/null
systemctl show -P TTYPath varlink-transient-stdio.service | grep '^/dev/tty2$' >/dev/null
# "null" is also the default for StandardInput=, hence additionally verify it was written explicitly
fragment=$(systemctl show -P FragmentPath varlink-transient-stdio.service)
grep '^StandardInput=null$' "$fragment" >/dev/null
# 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
result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-stdio-combined.service","Exec":{"StandardOutput":"kmsg_console","StandardError":"journal_console"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
echo "$result" | jq -e '.context.Exec.StandardOutput == "kmsg_console"'
echo "$result" | jq -e '.context.Exec.StandardError == "journal_console"'
timeout 30 bash -c 'until systemctl is-active varlink-transient-stdio-combined.service; do sleep 0.5; done'
systemctl show -P StandardOutput varlink-transient-stdio-combined.service | grep '^kmsg+console$' >/dev/null
systemctl show -P StandardError varlink-transient-stdio-combined.service | grep '^journal+console$' >/dev/null
# Kill.KillMode/Kill.SendSIGHUP
defer_transient_cleanup varlink-transient-kill.service
result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-kill.service","Kill":{"KillMode":"process","SendSIGHUP":true},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
echo "$result" | jq -e '.context.Kill.KillMode == "process"'
echo "$result" | jq -e '.context.Kill.SendSIGHUP == true'
timeout 30 bash -c 'until systemctl is-active varlink-transient-kill.service; do sleep 0.5; done'
systemctl show -P KillMode varlink-transient-kill.service | grep '^process$' >/dev/null
systemctl show -P SendSIGHUP varlink-transient-kill.service | grep '^yes$' >/dev/null
# ExecStart execution flags, i.e. the Varlink counterparts of the "-", ":", "|", "+" and "!" prefixes of
# unit file ExecStart= lines. First the ones observable from an unprivileged unit:
# - ignoreFailure: a failing command does not fail the unit
# - noEnvExpand: "${VAR}" in the arguments is passed as-is rather than expanded
# - viaShell: the command line is handed to the user's shell via "-c" (argv[0] is normalized to "sh")
rm -f /tmp/varlink-transient-execflags-*
defer_transient_cleanup varlink-transient-execflags.service
# shellcheck disable=SC2016
result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-execflags.service","Exec":{"Environment":["FLAGTEST=expanded"]},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[
{"path":"/bin/false","ignoreFailure":true},
{"path":"/usr/bin/touch","arguments":["touch","/tmp/varlink-transient-execflags-${FLAGTEST}"]},
{"path":"/usr/bin/touch","arguments":["touch","/tmp/varlink-transient-execflags-${FLAGTEST}"],"noEnvExpand":true},
{"path":"/bin/sh","arguments":["sh","touch","/tmp/varlink-transient-execflags-shell"],"viaShell":true}]}}}')
echo "$result" | jq -e '.context.Service.ExecStart[0].ignoreFailure == true'
echo "$result" | jq -e '.context.Service.ExecStart[1].noEnvExpand == false'
echo "$result" | jq -e '.context.Service.ExecStart[2].noEnvExpand == true'
echo "$result" | jq -e '.context.Service.ExecStart[3].viaShell == true'
echo "$result" | jq -e '.context.Service.ExecStart[3].path == "/bin/sh"'
echo "$result" | jq -e '.context.Service.ExecStart[3].arguments[0] == "sh"'
# The unit only becomes active once all ExecStart= commands succeeded (or were allowed to fail)
timeout 30 bash -c 'until systemctl is-active varlink-transient-execflags.service; do sleep 0.5; done'
test -e /tmp/varlink-transient-execflags-expanded
# shellcheck disable=SC2016
test -e '/tmp/varlink-transient-execflags-${FLAGTEST}'
test -e /tmp/varlink-transient-execflags-shell
# The flags must show up as the matching prefix characters in the transient unit file
fragment=$(systemctl show -P FragmentPath varlink-transient-execflags.service)
grep -E '^ExecStart=-"/bin/false"$' "$fragment" >/dev/null
grep -E '^ExecStart=:' "$fragment" >/dev/null
grep -E '^ExecStart=\|' "$fragment" >/dev/null
rm -f /tmp/varlink-transient-execflags-*
# Now the privilege related ones, in a unit running as an unprivileged user:
# - privileged/noSetuid: the command runs with full privileges, i.e. can write where the user cannot
# - (neither): the command runs as the user, and fails to write there (tolerated via ignoreFailure)
rm -f /run/varlink-transient-execflags-*
defer_transient_cleanup varlink-transient-execflags-priv.service
result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-execflags-priv.service","Exec":{"User":"nobody"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[
{"path":"/usr/bin/touch","arguments":["touch","/run/varlink-transient-execflags-plain"],"ignoreFailure":true},
{"path":"/usr/bin/touch","arguments":["touch","/run/varlink-transient-execflags-privileged"],"privileged":true},
{"path":"/usr/bin/touch","arguments":["touch","/run/varlink-transient-execflags-nosetuid"],"noSetuid":true}]}}}')
echo "$result" | jq -e '.context.Service.ExecStart[0].privileged == false'
echo "$result" | jq -e '.context.Service.ExecStart[1].privileged == true'
echo "$result" | jq -e '.context.Service.ExecStart[2].noSetuid == true'
timeout 30 bash -c 'until systemctl is-active varlink-transient-execflags-priv.service; do sleep 0.5; done'
test ! -e /run/varlink-transient-execflags-plain
test -e /run/varlink-transient-execflags-privileged
test -e /run/varlink-transient-execflags-nosetuid
fragment=$(systemctl show -P FragmentPath varlink-transient-execflags-priv.service)
grep -E '^ExecStart=\+' "$fragment" >/dev/null
grep -E '^ExecStart=!' "$fragment" >/dev/null
rm -f /run/varlink-transient-execflags-*
# Exec.SetCredential: pass a credential and verify the running process can read it
defer_transient_cleanup varlink-transient-cred.service
CRED_VALUE_B64=$(printf 'secret-value' | base64 -w0)
@@ -310,6 +403,11 @@ defer_transient_cleanup varlink-transient-badpath.service
expect_invalid_parameter \
'{"context":{"ID":"varlink-transient-badpath.service","Service":{"Type":"simple","ExecStart":[{"path":""}]}}}' \
"Service.ExecStart"
# The 'privileged' and 'noSetuid' execution flags are mutually exclusive
defer_transient_cleanup varlink-transient-badflags.service
expect_invalid_parameter \
'{"context":{"ID":"varlink-transient-badflags.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true","privileged":true,"noSetuid":true}]}}}' \
"Service.ExecStart"
# Relative WorkingDirectory path is rejected
defer_transient_cleanup varlink-transient-bad-wd.service
expect_invalid_parameter \
@@ -340,6 +438,33 @@ 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"
# Note that an unparsable enum value (e.g. StandardOutput=bogus) is rejected by the Varlink IDL
# validation layer before the JSON dispatch even runs, hence there's no point in testing pid1's
# dispatch-level enum handling here — and doing so would trip post.sh's "didn't pass validation"
# journal check, since the IDL layer logs about the rejection.
# Relative TTYPath is rejected
defer_transient_cleanup varlink-transient-bad-ttypath.service
expect_invalid_parameter \
'{"context":{"ID":"varlink-transient-bad-ttypath.service","Exec":{"TTYPath":"relative/tty"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
"Exec.TTYPath"
# The file based stdio types need a companion path, and StandardInput=data a companion payload, neither of
# which can be passed yet, hence they are rejected
for t in file append truncate; do
defer_transient_cleanup "varlink-transient-bad-stdout-$t.service"
expect_invalid_parameter \
"{\"context\":{\"ID\":\"varlink-transient-bad-stdout-$t.service\",\"Exec\":{\"StandardOutput\":\"$t\"},\"Service\":{\"Type\":\"oneshot\",\"ExecStart\":[{\"path\":\"/bin/true\"}]}}}" \
"Exec.StandardOutput"
done
defer_transient_cleanup varlink-transient-bad-stderr.service
expect_invalid_parameter \
'{"context":{"ID":"varlink-transient-bad-stderr.service","Exec":{"StandardError":"file"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
"Exec.StandardError"
for t in file data; do
defer_transient_cleanup "varlink-transient-bad-stdin-$t.service"
expect_invalid_parameter \
"{\"context\":{\"ID\":\"varlink-transient-bad-stdin-$t.service\",\"Exec\":{\"StandardInput\":\"$t\"},\"Service\":{\"Type\":\"oneshot\",\"ExecStart\":[{\"path\":\"/bin/true\"}]}}}" \
"Exec.StandardInput"
done
# Invalid credential ID
defer_transient_cleanup varlink-transient-bad-cred-id.service
expect_invalid_parameter \
@@ -353,6 +478,9 @@ expect_invalid_parameter \
# Exec on a unit type without an exec context (.slice) is rejected
varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-exec.slice","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false}}}}' |& grep "io.systemd.Unit.UnitTypeNotSupported"
# Same for Kill on a unit type without a kill context
varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-kill.slice","Kill":{"KillMode":"process"}}}' |& grep "io.systemd.Unit.UnitTypeNotSupported"
# 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 \
@@ -366,6 +494,12 @@ unsupported_service=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTra
'{"context":{"ID":"varlink-transient-unknown-service.service","Service":{"Type":"oneshot","Restart":"always","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true)
echo "$unsupported_service" | grep "io.systemd.Unit.PropertyNotSupported"
echo "$unsupported_service" | grep "Service.Restart"
# Same for a Kill field
defer_transient_cleanup varlink-transient-unknown-kill.service
unsupported_kill=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-unknown-kill.service","Kill":{"KillSignal":"SIGINT"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true)
echo "$unsupported_kill" | grep "io.systemd.Unit.PropertyNotSupported"
echo "$unsupported_kill" | grep "Kill.KillSignal"
set -o pipefail
transient_cleanup