systemctl: fix edit and cat verbs with --global flag (#39606)

The --global flag has been broken since commit 
d77d42ed3a, which added a
blanket restriction on acquiring D-Bus connections when
arg_runtime_scope is RUNTIME_SCOPE_GLOBAL. This was done to prevent
crashes, but inadvertently broke legitimate use cases like 'systemctl
edit --global' and 'systemctl cat --global'.

The issue is that verb_edit() and verb_cat() were unconditionally
calling acquire_bus(), which triggers the restriction and fails with
"--global is not supported for this operation."

This commit fixes the issue by making bus acquisition conditional,
following the same pattern used in verb_enable():

- Only acquire the bus when install_client_side() returns NO (i.e., for
system and user scopes)
- For client-side operations (--global, --root, etc.), skip bus
acquisition and use mangle_names() instead of expand_unit_names()
- Update find_paths_to_edit() and verb_cat() to handle NULL bus by
forcing client-side path lookups
- Skip bus-dependent checks (unit_is_masked, need_daemon_reload) when
bus is NULL

This allows both 'systemctl edit --global' and 'systemctl cat --global'
to work correctly by performing all operations client-side without
requiring a connection to the system or user manager.

Fixes #31272
This commit is contained in:
Yu Watanabe
2025-11-12 08:59:06 +09:00
committed by GitHub
3 changed files with 87 additions and 30 deletions

View File

@@ -24,7 +24,7 @@ int verb_cat(int argc, char *argv[], void *userdata) {
_cleanup_hashmap_free_ Hashmap *cached_id_map = NULL, *cached_name_map = NULL;
_cleanup_(lookup_paths_done) LookupPaths lp = {};
_cleanup_strv_free_ char **names = NULL;
sd_bus *bus;
sd_bus *bus = NULL;
bool first = true;
int r, rc = 0;
@@ -39,17 +39,24 @@ int verb_cat(int argc, char *argv[], void *userdata) {
if (r < 0)
return r;
r = acquire_bus(BUS_MANAGER, &bus);
if (r < 0)
return r;
if (install_client_side() == INSTALL_CLIENT_SIDE_NO) {
r = acquire_bus(BUS_MANAGER, &bus);
if (r < 0)
return r;
r = expand_unit_names(bus, strv_skip(argv, 1), NULL, &names, NULL);
if (r < 0)
return log_error_errno(r, "Failed to expand names: %m");
r = expand_unit_names(bus, strv_skip(argv, 1), NULL, &names, NULL);
if (r < 0)
return log_error_errno(r, "Failed to expand names: %m");
r = maybe_extend_with_unit_dependencies(bus, &names);
if (r < 0)
return r;
r = maybe_extend_with_unit_dependencies(bus, &names);
if (r < 0)
return r;
} else {
/* In client-side mode (--global, --root, etc.), just mangle names without bus interaction */
r = mangle_names("to cat", strv_skip(argv, 1), &names);
if (r < 0)
return r;
}
pager_open(arg_pager_flags);
@@ -57,7 +64,7 @@ int verb_cat(int argc, char *argv[], void *userdata) {
_cleanup_free_ char *fragment_path = NULL;
_cleanup_strv_free_ char **dropin_paths = NULL;
r = unit_find_paths(bus, *name, &lp, false, &cached_id_map, &cached_name_map, &fragment_path, &dropin_paths);
r = unit_find_paths(bus, *name, &lp, /* force_client_side= */ !bus, &cached_id_map, &cached_name_map, &fragment_path, &dropin_paths);
if (r == -ERFKILL) {
printf("%s# Unit %s is masked%s.\n",
ansi_highlight_magenta(),
@@ -87,7 +94,7 @@ int verb_cat(int argc, char *argv[], void *userdata) {
else
puts("");
if (need_daemon_reload(bus, *name) > 0) /* ignore errors (<0), this is informational output */
if (bus && need_daemon_reload(bus, *name) > 0) /* ignore errors (<0), this is informational output */
fprintf(stderr,
"%s# Warning: %s changed on disk, the version systemd has loaded is outdated.\n"
"%s# This output shows the current version of the unit's original fragment and drop-in files.\n"
@@ -211,7 +218,6 @@ static int find_paths_to_edit(
const char *drop_in;
int r;
assert(bus);
assert(context);
assert(names);
@@ -241,8 +247,8 @@ static int find_paths_to_edit(
_cleanup_free_ char *path = NULL;
_cleanup_strv_free_ char **unit_paths = NULL;
r = unit_find_paths(bus, *name, &lp, /* force_client_side= */ false, &cached_id_map, &cached_name_map, &path, &unit_paths);
if (r == -EKEYREJECTED) {
r = unit_find_paths(bus, *name, &lp, /* force_client_side= */ !bus, &cached_id_map, &cached_name_map, &path, &unit_paths);
if (r == -EKEYREJECTED && bus) {
/* If loading of the unit failed server side complete, then the server won't tell us
* the unit file path. In that case, find the file client side. */
@@ -327,7 +333,7 @@ int verb_edit(int argc, char *argv[], void *userdata) {
.read_from_stdin = arg_stdin,
};
_cleanup_strv_free_ char **names = NULL;
sd_bus *bus;
sd_bus *bus = NULL;
int r;
if (!on_tty() && !arg_stdin)
@@ -340,13 +346,20 @@ int verb_edit(int argc, char *argv[], void *userdata) {
if (r < 0)
return r;
r = acquire_bus(BUS_MANAGER, &bus);
if (r < 0)
return r;
if (install_client_side() == INSTALL_CLIENT_SIDE_NO) {
r = acquire_bus(BUS_MANAGER, &bus);
if (r < 0)
return r;
r = expand_unit_names(bus, strv_skip(argv, 1), NULL, &names, NULL);
if (r < 0)
return log_error_errno(r, "Failed to expand names: %m");
r = expand_unit_names(bus, strv_skip(argv, 1), NULL, &names, NULL);
if (r < 0)
return log_error_errno(r, "Failed to expand names: %m");
} else {
/* In client-side mode (--global, --root, etc.), just mangle names without bus interaction */
r = mangle_names("to edit", strv_skip(argv, 1), &names);
if (r < 0)
return r;
}
if (strv_isempty(names))
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No units matched the specified patterns.");
@@ -354,14 +367,6 @@ int verb_edit(int argc, char *argv[], void *userdata) {
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"With 'edit --stdin --full', exactly one unit for editing must be specified.");
STRV_FOREACH(tmp, names) {
r = unit_is_masked(bus, *tmp);
if (r < 0 && r != -ENOENT)
return log_error_errno(r, "Failed to check if unit %s is masked: %m", *tmp);
if (r > 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit %s: unit is masked.", *tmp);
}
r = find_paths_to_edit(bus, &context, names);
if (r < 0)
return r;

View File

@@ -29,6 +29,7 @@
#include "reboot-util.h"
#include "runtime-scope.h"
#include "set.h"
#include "stat-util.h"
#include "string-util.h"
#include "strv.h"
#include "systemctl.h"
@@ -589,6 +590,13 @@ int unit_find_paths(
return log_error_errno(r, "Failed to find fragment for '%s': %m", unit_name);
if (_path) {
/* Check if unit is masked (symlinked to /dev/null or empty) */
r = null_or_empty_path(_path);
if (r < 0)
return log_error_errno(r, "Failed to check if '%s' is masked: %m", unit_name);
if (r > 0)
return -ERFKILL; /* special case: no logging */
path = strdup(_path);
if (!path)
return log_oom();

View File

@@ -573,4 +573,48 @@ systemctl daemon-reload
systemctl enable --now test-WantedBy.service || :
systemctl daemon-reload
# Test systemctl edit --global and systemctl cat --global (issue #31272)
GLOBAL_UNIT_NAME="systemctl-test-$RANDOM.service"
GLOBAL_MASKED_UNIT="systemctl-test-masked-$RANDOM.service"
# Test 1: Create a new global user unit with --force and --runtime
systemctl edit --global --runtime --stdin --full --force "$GLOBAL_UNIT_NAME" <<EOF
[Unit]
Description=Test global unit
[Service]
ExecStart=/bin/true
EOF
# Verify the unit file was created in /run/systemd/user/
test -f "/run/systemd/user/$GLOBAL_UNIT_NAME"
# Test 2: Read the global unit with systemctl cat --global
systemctl cat --global "$GLOBAL_UNIT_NAME" | grep -q "ExecStart=/bin/true"
# Test 3: Edit existing global unit (add a drop-in)
systemctl edit --global --runtime --stdin "$GLOBAL_UNIT_NAME" <<EOF
[Service]
Environment=TEST=value
EOF
# Verify drop-in was created
test -f "/run/systemd/user/$GLOBAL_UNIT_NAME.d/override.conf"
systemctl cat --global "$GLOBAL_UNIT_NAME" | grep -q "Environment=TEST=value"
# Test 4: Create a masked global unit in /run/
mkdir -p /run/systemd/user
ln -sf /dev/null "/run/systemd/user/$GLOBAL_MASKED_UNIT"
# Test 5: Verify cat shows it's masked
systemctl cat --global "$GLOBAL_MASKED_UNIT" 2>&1 | grep -q "masked"
# Test 6: Verify edit refuses to edit masked unit
(! systemctl edit --global --runtime --stdin --full "$GLOBAL_MASKED_UNIT" </dev/null 2>&1) | grep -q "masked"
# Cleanup global test units
rm -f "/run/systemd/user/$GLOBAL_UNIT_NAME"
rm -rf "/run/systemd/user/$GLOBAL_UNIT_NAME.d"
rm -f "/run/systemd/user/$GLOBAL_MASKED_UNIT"
touch /testok