From 9a08000d186396bc8bcb8fe057720417543c3bf0 Mon Sep 17 00:00:00 2001 From: ZIHCO Date: Fri, 13 Jun 2025 19:38:55 +0100 Subject: [PATCH] systemd-analyze: added the verb unit-shell to spawn and attach shell --- man/systemd-analyze.xml | 23 +++++ shell-completion/bash/systemd-analyze | 8 ++ src/analyze/analyze-unit-shell.c | 123 ++++++++++++++++++++++++++ src/analyze/analyze-unit-shell.h | 4 + src/analyze/analyze.c | 78 +++++++++++++++- src/analyze/meson.build | 1 + test/units/TEST-65-ANALYZE.sh | 23 +++++ 7 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 src/analyze/analyze-unit-shell.c create mode 100644 src/analyze/analyze-unit-shell.h diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml index e1c575757bd..38063f6d4c4 100644 --- a/man/systemd-analyze.xml +++ b/man/systemd-analyze.xml @@ -70,6 +70,13 @@ OPTIONS unit-paths + + systemd-analyze + OPTIONS + unit-shell + SERVICE + Command + systemd-analyze OPTIONS @@ -1160,6 +1167,22 @@ LEGEND: M → sys_vendor (LENOVO) ┄ F → product_family (ThinkPad X1 Carbon G + + + <command>systemd-analyze unit-shell <replaceable>SERVICE</replaceable> <optional><replaceable>command</replaceable>...</optional></command> + + The given command runs on the namespace of the specified running service. If no command is given, + spawn and attach a shell with the namespace to the service. + + + Example output + $ systemd-analyze unit-shell systemd-resolved.service ls +bin dev etc home lib lib64 lost+found mnt proc run srv tmp var vmlinuz.old +boot efi exitrd init lib32 libx32 media opt root sbin sys usr vmlinuz work + + + + diff --git a/shell-completion/bash/systemd-analyze b/shell-completion/bash/systemd-analyze index e4ecafeddca..b909a954e47 100644 --- a/shell-completion/bash/systemd-analyze +++ b/shell-completion/bash/systemd-analyze @@ -82,6 +82,7 @@ _systemd_analyze() { [FDSTORE]='fdstore' [CAPABILITY]='capability' [TRANSIENT_SETTINGS]='transient-settings' + [UNIT_SHELL]='unit-shell' ) local CONFIGS='locale.conf systemd/bootchart.conf systemd/coredump.conf systemd/journald.conf @@ -233,6 +234,13 @@ _systemd_analyze() { else comps="$(systemctl --no-legend --no-pager -t help)" fi + + elif __contains_word "$verb" ${VERBS[UNIT_SHELL]}; then + if [[ $cur = -* ]]; then + comps='--help --version' + else + comps=$( __get_services $mode ) + fi fi COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) diff --git a/src/analyze/analyze-unit-shell.c b/src/analyze/analyze-unit-shell.c new file mode 100644 index 00000000000..576a4200343 --- /dev/null +++ b/src/analyze/analyze-unit-shell.c @@ -0,0 +1,123 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "sd-bus.h" + +#include "alloc-util.h" +#include "analyze.h" +#include "analyze-unit-shell.h" +#include "bus-error.h" +#include "bus-util.h" +#include "fd-util.h" +#include "log.h" +#include "namespace-util.h" +#include "process-util.h" +#include "runtime-scope.h" +#include "strv.h" +#include "unit-def.h" +#include "unit-name.h" + +int verb_unit_shell(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *unit = NULL; + int r; + + if (arg_transport != BUS_TRANSPORT_LOCAL) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot spawn a unit shell for a remote service"); + + r = unit_name_mangle_with_suffix(argv[1], "as unit", UNIT_NAME_MANGLE_WARN, ".service", &unit); + if (r < 0) + return log_error_errno(r, "Failed to mangle name '%s': %m", argv[1]); + + r = acquire_bus(&bus, /* use_full_bus= */ NULL); + if (r < 0) + return bus_log_connect_error(r, arg_transport, arg_runtime_scope); + + _cleanup_free_ char *object = unit_dbus_path_from_name(unit); + if (!object) + return log_oom(); + + r = sd_bus_get_property( + bus, + "org.freedesktop.systemd1", + object, + "org.freedesktop.systemd1.Service", + "MainPID", + &error, + &reply, + "u"); + if (r < 0) + return log_error_errno(r, "Failed to get the main PID of %s: %s", unit, bus_error_message(&error, r)); + + pid_t pid; + r = sd_bus_message_read(reply, "u", &pid); + if (r < 0) + return log_error_errno(r, "Failed to read the main PID of %s from reply: %m", unit); + + _cleanup_close_ int mntns_fd = -EBADF, root_fd = -EBADF, pidns_fd = -EBADF, netns_fd = -EBADF, userns_fd = -EBADF; + r = namespace_open( + pid, + &pidns_fd, + &mntns_fd, + &netns_fd, + &userns_fd, + &root_fd); + if (r < 0) + return log_error_errno(r, "Failed to retrieve FDs of namespaces of %s: %m", unit); + + _cleanup_strv_free_ char **args = NULL; + if (argc > 2) { + args = strv_copy(strv_skip(argv, 2)); + if (!args) + return log_oom(); + } + + pid_t child; + r = namespace_fork( + "(unit-shell-ns)", + "(unit-shell)", + /* except_fds= */ NULL, + /* n_except_fds */ 0, + FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL, + pidns_fd, + mntns_fd, + netns_fd, + userns_fd, + root_fd, + &child); + if (r < 0) + return log_error_errno(r, "Failed to fork and enter the namespace of %s: %m", unit); + + if (r == 0) { + if (args) { + r = execvp(args[0], args); + if (r < 0) + log_error_errno(errno, "Failed to execute '%s': %m", *args); + } else { + r = execl(DEFAULT_USER_SHELL, "-" DEFAULT_USER_SHELL_NAME, NULL); + if (r < 0) + log_debug_errno(errno, "Failed to execute '" DEFAULT_USER_SHELL "', ignoring: %m"); + if (!streq(DEFAULT_USER_SHELL, "/bin/bash")) { + r = execl("/bin/bash", "-bash", NULL); + if (r < 0) + log_debug_errno(errno, "Failed to execute '/bin/bash', ignoring: %m"); + } + if (!streq(DEFAULT_USER_SHELL, "/bin/sh")) { + r = execl("/bin/sh", "-sh", NULL); + if (r < 0) + log_debug_errno(errno, "Failed to execute '/bin/sh', ignoring: %m"); + } + log_error_errno(errno, "Failed to execute '" DEFAULT_USER_SHELL "', '/bin/bash', and '/bin/sh': %m"); + } + _exit(EXIT_FAILURE); + } + + return wait_for_terminate_and_check( + "(unit-shell)", + child, + WAIT_LOG_ABNORMAL|WAIT_LOG_NON_ZERO_EXIT_STATUS); +} diff --git a/src/analyze/analyze-unit-shell.h b/src/analyze/analyze-unit-shell.h new file mode 100644 index 00000000000..7c15e083a8e --- /dev/null +++ b/src/analyze/analyze-unit-shell.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_unit_shell(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index 8745db63f5f..a43ae3bdb45 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -44,6 +44,7 @@ #include "analyze-timestamp.h" #include "analyze-unit-files.h" #include "analyze-unit-paths.h" +#include "analyze-unit-shell.h" #include "analyze-verify.h" #include "analyze-verify-util.h" #include "build.h" @@ -241,6 +242,8 @@ static int help(int argc, char *argv[], void *userdata) { " security [UNIT...] Analyze security of unit\n" " fdstore SERVICE... Show file descriptor store contents of service\n" " malloc [D-BUS SERVICE...] Dump malloc stats of a D-Bus service\n" + " unit-shell SERVICE [Command]\n" + " Run command on the namespace of the service\n" "\n%3$sExecutable Analysis:%4$s\n" " inspect-elf FILE... Parse and print ELF package metadata\n" "\n%3$sTPM Operations:%4$s\n" @@ -383,14 +386,68 @@ static int parse_argv(int argc, char *argv[]) { {} }; - int r, c; + bool reorder = false; + int r, c, unit_shell = -1; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hqH:M:U:m", options, NULL)) >= 0) + /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long() + * that checks for GNU extensions in optstring ('-' or '+; at the beginning). */ + optind = 0; + + for (;;) { + static const char option_string[] = "-hqH:M:U:m"; + + c = getopt_long(argc, argv, option_string + reorder, options, NULL); + if (c < 0) + break; + switch (c) { + case 1: /* getopt_long() returns 1 if "-" was the first character of the option string, and a + * non-option argument was discovered. */ + + assert(!reorder); + + /* We generally are fine with the fact that getopt_long() reorders the command line, and looks + * for switches after the main verb. However, for "unit-shell" we really don't want that, since we + * want that switches specified after the service name are passed to the program to execute, + * and not processed by us. To make this possible, we'll first invoke getopt_long() with + * reordering disabled (i.e. with the "-" prefix in the option string), looking for the first + * non-option parameter. If it's the verb "unit-shell" we remember its position and continue + * processing options. In this case, as soon as we hit the next non-option argument we found + * the service name, and stop further processing. If the first non-option argument is any other + * verb than "unit-shell" we switch to normal reordering mode and continue processing arguments + * normally. */ + + if (unit_shell >= 0) { + optind--; /* don't processs this argument, go one step back */ + goto done; + } + if (streq(optarg, "unit-shell")) + /* Remember the position of the "unit_shell" verb, and continue processing normally. */ + unit_shell = optind - 1; + else { + int saved_optind; + + /* Ok, this is some other verb. In this case, turn on reordering again, and continue + * processing normally. */ + reorder = true; + + /* We changed the option string. getopt_long() only looks at it again if we invoke it + * at least once with a reset option index. Hence, let's reset the option index here, + * then invoke getopt_long() again (ignoring what it has to say, after all we most + * likely already processed it), and the bump the option index so that we read the + * intended argument again. */ + saved_optind = optind; + optind = 0; + (void) getopt_long(argc, argv, option_string + reorder, options, NULL); + optind = saved_optind - 1; /* go one step back, process this argument again */ + } + + break; + case 'h': return help(0, NULL, NULL); @@ -602,6 +659,22 @@ static int parse_argv(int argc, char *argv[]) { default: assert_not_reached(); } + } + +done: + if (unit_shell >= 0) { + char *t; + + /* We found the "unit-shell" verb while processing the argument list. Since we turned off reordering of the + * argument list initially let's readjust it now, and move the "unit-shell" verb to the back. */ + + optind -= 1; /* place the option index where the "unit-shell" verb will be placed */ + + t = argv[unit_shell]; + for (int i = unit_shell; i < optind; i++) + argv[i] = argv[i+1]; + argv[optind] = t; + } if (arg_offline && !streq_ptr(argv[optind], "security")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), @@ -684,6 +757,7 @@ static int run(int argc, char *argv[]) { { "cat-config", 2, VERB_ANY, 0, verb_cat_config }, { "unit-files", VERB_ANY, VERB_ANY, 0, verb_unit_files }, { "unit-paths", 1, 1, 0, verb_unit_paths }, + { "unit-shell", 2, VERB_ANY, 0, verb_unit_shell }, { "exit-status", VERB_ANY, VERB_ANY, 0, verb_exit_status }, { "syscall-filter", VERB_ANY, VERB_ANY, 0, verb_syscall_filters }, { "capability", VERB_ANY, VERB_ANY, 0, verb_capabilities }, diff --git a/src/analyze/meson.build b/src/analyze/meson.build index 7d437953d9b..283378a2f3c 100644 --- a/src/analyze/meson.build +++ b/src/analyze/meson.build @@ -33,6 +33,7 @@ systemd_analyze_sources = files( 'analyze-timestamp.c', 'analyze-unit-files.c', 'analyze-unit-paths.c', + 'analyze-unit-shell.c', 'analyze-verify.c', 'analyze.c', ) diff --git a/test/units/TEST-65-ANALYZE.sh b/test/units/TEST-65-ANALYZE.sh index 468c9ed0939..e5a9aba4e72 100755 --- a/test/units/TEST-65-ANALYZE.sh +++ b/test/units/TEST-65-ANALYZE.sh @@ -1116,6 +1116,29 @@ systemd-analyze transient-settings mount | grep CPUQuotaPeriodSec (! systemd-analyze transient-settings service | grep ConditionKernelVersion ) (! systemd-analyze transient-settings service | grep AssertKernelVersion ) +# check systemd-analyze unit-shell with a namespaced unit +UNIT_NAME="test-unit-shell.service" +UNIT_FILE="/run/systemd/system/$UNIT_NAME" +cat >"$UNIT_FILE" </tmp/testfile; systemd-notify --ready; sleep infinity" +PrivateTmp=disconnected +EOF +# Start the service +systemctl start "$UNIT_NAME" +# Wait for the service to be active +systemctl is-active --quiet "$UNIT_NAME" +# Verify the service is active and has a MainPID +MAIN_PID=$(systemctl show -p MainPID --value "$UNIT_NAME") +[ "$MAIN_PID" -gt 0 ] +# Test systemd-analyze unit-shell with a command (cat /tmp/testfile) +OUTPUT=$(systemd-analyze unit-shell "$UNIT_NAME" cat /tmp/testfile) +assert_in "Hello from test unit" "$OUTPUT" + systemd-analyze log-level info touch /testok