From: ZIHCO Date: Wed, 9 Jul 2025 11:56:32 +0000 (+0100) Subject: systemd-analyze: added the verb unit-gdb to spawn and attach gdb X-Git-Tag: v258-rc1~39 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ad6e02e7b42db35178305614e643be7a62568d87;p=thirdparty%2Fsystemd.git systemd-analyze: added the verb unit-gdb to spawn and attach gdb --- diff --git a/man/coredumpctl.xml b/man/coredumpctl.xml index 9ad7608b277..9315679f07d 100644 --- a/man/coredumpctl.xml +++ b/man/coredumpctl.xml @@ -251,7 +251,7 @@ Use the given debugger for the debug command. If not given and $SYSTEMD_DEBUGGER is unset, then gdb1 - will be used. + will be used. diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml index d59967efb44..dc4ab642a81 100644 --- a/man/systemd-analyze.xml +++ b/man/systemd-analyze.xml @@ -65,6 +65,12 @@ OPTIONS unit-files + + systemd-analyze + OPTIONS + unit-gdb + SERVICE + systemd-analyze OPTIONS @@ -1183,6 +1189,23 @@ boot efi exitrd init lib32 libx32 media opt root sbin sys usr vmlin + + + <command>systemd-analyze unit-gdb <replaceable>SERVICE</replaceable></command> + + Spawn and attach a debugger to the given service. By default, + gdb1 + will be used. This may be changed using the option or the + $SYSTEMD_DEBUGGER environment variable. Use the option + to pass extra command line arguments to the debugger and quote as appropriate + when ARGS contain whitespace (See Example). + + + $ systemd-analyze --debugger-arguments="-batch -ex 'info all-registers'" unit-gdb systemd-oomd.service + + + + @@ -1328,8 +1351,8 @@ boot efi exitrd init lib32 libx32 media opt root sbin sys usr vmlin With cat-config, verify, - condition and security when used with - , operate on files underneath the specified root path + condition, unit-gdb, and security when + used with , operate on files underneath the specified root path PATH. @@ -1784,6 +1807,26 @@ boot efi exitrd init lib32 libx32 media opt root sbin sys usr vmlin + + + + Use the given debugger for the unit-gdb command. If not given and + $SYSTEMD_DEBUGGER is unset, then + gdb1 + will be used. + + + + + + ARGS + + + Pass the given ARGS as extra command line arguments to the debugger. + + + + diff --git a/shell-completion/bash/systemd-analyze b/shell-completion/bash/systemd-analyze index 8329b1b495b..d3d3fe4c63e 100644 --- a/shell-completion/bash/systemd-analyze +++ b/shell-completion/bash/systemd-analyze @@ -63,7 +63,7 @@ _systemd_analyze() { local -A OPTS=( [STANDALONE]='-h --help --version --system --user --global --order --require --no-pager --man=no --generators=yes -q --quiet' - [ARG]='-H --host -M --machine --fuzz --from-pattern --to-pattern --root' + [ARG]='-H --host -M --machine --fuzz --from-pattern --to-pattern --root --debugger' ) local -A VERBS=( @@ -83,6 +83,7 @@ _systemd_analyze() { [CAPABILITY]='capability' [TRANSIENT_SETTINGS]='transient-settings' [UNIT_SHELL]='unit-shell' + [UNIT_GDB]='unit-gdb' ) local CONFIGS='locale.conf systemd/bootchart.conf systemd/coredump.conf systemd/journald.conf @@ -241,6 +242,13 @@ _systemd_analyze() { else comps=$( __get_services $mode ) fi + + elif __contains_word "$verb" ${VERBS[UNIT_GDB]}; then + if [[ $cur = -* ]]; then + comps='--help --version --debugger --debugger-arguments --root' + else + comps=$( __get_services $mode ) + fi fi COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) diff --git a/src/analyze/analyze-unit-gdb.c b/src/analyze/analyze-unit-gdb.c new file mode 100644 index 00000000000..92b8a5cb5bd --- /dev/null +++ b/src/analyze/analyze-unit-gdb.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-gdb.h" +#include "bus-error.h" +#include "bus-util.h" +#include "log.h" +#include "pidref.h" +#include "process-util.h" +#include "runtime-scope.h" +#include "signal-util.h" +#include "strv.h" +#include "unit-def.h" +#include "unit-name.h" + +int verb_unit_gdb(int argc, char *argv[], void *userdata) { + static const struct sigaction sa = { + .sa_sigaction = sigterm_process_group_handler, + .sa_flags = SA_SIGINFO, + }; + + _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 debugger 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); + + if (!arg_debugger) + arg_debugger = secure_getenv("SYSTEMD_DEBUGGER") ?: "gdb"; + + _cleanup_strv_free_ char **debugger_call = NULL; + r = strv_extend(&debugger_call, arg_debugger); + if (r < 0) + return log_oom(); + + if (!STR_IN_SET(arg_debugger, "gdb", "lldb")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The debugger must be either 'gdb' or 'lldb'"); + + if (streq(arg_debugger, "gdb")) { + r = strv_extendf(&debugger_call, "--pid=" PID_FMT, pid); + if (r < 0) + return log_oom(); + } + + if (streq(arg_debugger, "lldb")) { + r = strv_extendf(&debugger_call, "--attach-pid=" PID_FMT, pid); + if (r < 0) + return log_oom(); + } + + if (arg_root) { + if (streq(arg_debugger, "gdb")) { + _cleanup_free_ char *sysroot_cmd = strjoin("set sysroot ", arg_root); + r = strv_extend_many(&debugger_call, "-iex", sysroot_cmd); + if (r < 0) + return log_oom(); + } else if (streq(arg_debugger, "lldb")) { + _cleanup_free_ char *sysroot_cmd = strjoin("platform select --sysroot ", arg_root, " host"); + r = strv_extend_many(&debugger_call, "-O", sysroot_cmd); + if (r < 0) + return log_oom(); + } + } + + /* Don't interfere with debugger and its handling of SIGINT. */ + (void) ignore_signals(SIGINT); + (void) sigaction(SIGTERM, &sa, NULL); + + _cleanup_free_ char *fork_name = strjoin("(", debugger_call[0], ")"); + _cleanup_(pidref_done) PidRef pidref = PIDREF_NULL; + r = pidref_safe_fork(fork_name, FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL, &pidref); + if (r < 0) + return log_error_errno(r, "Fork failed: %m"); + + if (r == 0) { + (void) execvp(debugger_call[0], debugger_call); + log_error_errno(errno, "Failed to invoke '%s': %m", debugger_call[0]); + _exit(EXIT_FAILURE); + } + + return pidref_wait_for_terminate_and_check( + debugger_call[0], + &pidref, + WAIT_LOG_ABNORMAL|WAIT_LOG_NON_ZERO_EXIT_STATUS); +} diff --git a/src/analyze/analyze-unit-gdb.h b/src/analyze/analyze-unit-gdb.h new file mode 100644 index 00000000000..a3df6b128f1 --- /dev/null +++ b/src/analyze/analyze-unit-gdb.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_unit_gdb(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index cc50eb410ac..762317a6536 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -43,6 +43,7 @@ #include "analyze-timespan.h" #include "analyze-timestamp.h" #include "analyze-unit-files.h" +#include "analyze-unit-gdb.h" #include "analyze-unit-paths.h" #include "analyze-unit-shell.h" #include "analyze-verify.h" @@ -53,6 +54,7 @@ #include "bus-util.h" #include "calendarspec.h" #include "dissect-image.h" +#include "extract-word.h" #include "image-policy.h" #include "log.h" #include "loop-util.h" @@ -79,6 +81,8 @@ usec_t arg_fuzz = 0; PagerFlags arg_pager_flags = 0; CatFlags arg_cat_flags = 0; BusTransport arg_transport = BUS_TRANSPORT_LOCAL; +const char *arg_debugger = NULL; +static char **arg_debugger_args = NULL; const char *arg_host = NULL; RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; RecursiveErrors arg_recursive_errors = _RECURSIVE_ERRORS_INVALID; @@ -242,6 +246,7 @@ 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-gdb SERVICE Attach a debugger to the given running service\n" " unit-shell SERVICE [Command]\n" " Run command on the namespace of the service\n" "\n%3$sExecutable Analysis:%4$s\n" @@ -296,6 +301,9 @@ static int help(int argc, char *argv[], void *userdata) { " --image-policy=POLICY Specify disk image dissection policy\n" " -m --mask Parse parameter as numeric capability mask\n" " --drm-device=PATH Use this DRM device sysfs path to get EDID\n" + " --debugger=DEBUGGER Use the given debugger\n" + " -A --debugger-arguments=ARGS\n" + " Pass the given arguments to the debugger\n" "\nSee the %2$s for details.\n", program_invocation_short_name, @@ -344,45 +352,48 @@ static int parse_argv(int argc, char *argv[]) { ARG_SCALE_FACTOR_SVG, ARG_DETAILED_SVG, ARG_DRM_DEVICE_PATH, + ARG_DEBUGGER, }; static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "quiet", no_argument, NULL, 'q' }, - { "order", no_argument, NULL, ARG_ORDER }, - { "require", no_argument, NULL, ARG_REQUIRE }, - { "root", required_argument, NULL, ARG_ROOT }, - { "image", required_argument, NULL, ARG_IMAGE }, - { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, - { "recursive-errors", required_argument, NULL, ARG_RECURSIVE_ERRORS }, - { "offline", required_argument, NULL, ARG_OFFLINE }, - { "threshold", required_argument, NULL, ARG_THRESHOLD }, - { "security-policy", required_argument, NULL, ARG_SECURITY_POLICY }, - { "system", no_argument, NULL, ARG_SYSTEM }, - { "user", no_argument, NULL, ARG_USER }, - { "global", no_argument, NULL, ARG_GLOBAL }, - { "from-pattern", required_argument, NULL, ARG_DOT_FROM_PATTERN }, - { "to-pattern", required_argument, NULL, ARG_DOT_TO_PATTERN }, - { "fuzz", required_argument, NULL, ARG_FUZZ }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "man", optional_argument, NULL, ARG_MAN }, - { "generators", optional_argument, NULL, ARG_GENERATORS }, - { "instance", required_argument, NULL, ARG_INSTANCE }, - { "host", required_argument, NULL, 'H' }, - { "machine", required_argument, NULL, 'M' }, - { "iterations", required_argument, NULL, ARG_ITERATIONS }, - { "base-time", required_argument, NULL, ARG_BASE_TIME }, - { "unit", required_argument, NULL, 'U' }, - { "json", required_argument, NULL, ARG_JSON }, - { "profile", required_argument, NULL, ARG_PROFILE }, - { "table", optional_argument, NULL, ARG_TABLE }, - { "no-legend", optional_argument, NULL, ARG_NO_LEGEND }, - { "tldr", no_argument, NULL, ARG_TLDR }, - { "mask", no_argument, NULL, 'm' }, - { "scale-svg", required_argument, NULL, ARG_SCALE_FACTOR_SVG }, - { "detailed", no_argument, NULL, ARG_DETAILED_SVG }, - { "drm-device", required_argument, NULL, ARG_DRM_DEVICE_PATH }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "quiet", no_argument, NULL, 'q' }, + { "order", no_argument, NULL, ARG_ORDER }, + { "require", no_argument, NULL, ARG_REQUIRE }, + { "root", required_argument, NULL, ARG_ROOT }, + { "image", required_argument, NULL, ARG_IMAGE }, + { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, + { "recursive-errors" , required_argument, NULL, ARG_RECURSIVE_ERRORS }, + { "offline", required_argument, NULL, ARG_OFFLINE }, + { "threshold", required_argument, NULL, ARG_THRESHOLD }, + { "security-policy", required_argument, NULL, ARG_SECURITY_POLICY }, + { "system", no_argument, NULL, ARG_SYSTEM }, + { "user", no_argument, NULL, ARG_USER }, + { "global", no_argument, NULL, ARG_GLOBAL }, + { "from-pattern", required_argument, NULL, ARG_DOT_FROM_PATTERN }, + { "to-pattern", required_argument, NULL, ARG_DOT_TO_PATTERN }, + { "fuzz", required_argument, NULL, ARG_FUZZ }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "man", optional_argument, NULL, ARG_MAN }, + { "generators", optional_argument, NULL, ARG_GENERATORS }, + { "instance", required_argument, NULL, ARG_INSTANCE }, + { "host", required_argument, NULL, 'H' }, + { "machine", required_argument, NULL, 'M' }, + { "iterations", required_argument, NULL, ARG_ITERATIONS }, + { "base-time", required_argument, NULL, ARG_BASE_TIME }, + { "unit", required_argument, NULL, 'U' }, + { "json", required_argument, NULL, ARG_JSON }, + { "profile", required_argument, NULL, ARG_PROFILE }, + { "table", optional_argument, NULL, ARG_TABLE }, + { "no-legend", optional_argument, NULL, ARG_NO_LEGEND }, + { "tldr", no_argument, NULL, ARG_TLDR }, + { "mask", no_argument, NULL, 'm' }, + { "scale-svg", required_argument, NULL, ARG_SCALE_FACTOR_SVG }, + { "detailed", no_argument, NULL, ARG_DETAILED_SVG }, + { "drm-device", required_argument, NULL, ARG_DRM_DEVICE_PATH }, + { "debugger", required_argument, NULL, ARG_DEBUGGER }, + { "debugger-arguments", required_argument, NULL, 'A' }, {} }; @@ -397,7 +408,7 @@ static int parse_argv(int argc, char *argv[]) { optind = 0; for (;;) { - static const char option_string[] = "-hqH:M:U:m"; + static const char option_string[] = "-hqH:M:U:mA:"; c = getopt_long(argc, argv, option_string + reorder, options, NULL); if (c < 0) @@ -653,6 +664,19 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + case ARG_DEBUGGER: + arg_debugger = strdup(optarg); + break; + + case 'A': { + _cleanup_strv_free_ char **l = NULL; + r = strv_split_full(&l, optarg, WHITESPACE, EXTRACT_UNQUOTE); + if (r < 0) + return log_error_errno(r, "Failed to parse debugger arguments '%s': %m", optarg); + strv_free_and_replace(arg_debugger_args, l); + break; + } + case '?': return -EINVAL; @@ -700,10 +724,10 @@ done: return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --security-policy= is only supported for security."); - if ((arg_root || arg_image) && (!STRPTR_IN_SET(argv[optind], "cat-config", "verify", "condition", "inspect-elf")) && + if ((arg_root || arg_image) && (!STRPTR_IN_SET(argv[optind], "cat-config", "verify", "condition", "inspect-elf", "unit-gdb")) && (!(streq_ptr(argv[optind], "security") && arg_offline))) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Options --root= and --image= are only supported for cat-config, verify, condition and security when used with --offline= right now."); + "Options --root= and --image= are only supported for cat-config, verify, condition, unit-gdb, and security when used with --offline= right now."); /* Having both an image and a root is not supported by the code */ if (arg_root && arg_image) @@ -756,6 +780,7 @@ static int run(int argc, char *argv[]) { { "dump", VERB_ANY, VERB_ANY, 0, verb_dump }, { "cat-config", 2, VERB_ANY, 0, verb_cat_config }, { "unit-files", VERB_ANY, VERB_ANY, 0, verb_unit_files }, + { "unit-gdb", 2, VERB_ANY, 0, verb_unit_gdb }, { "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 }, diff --git a/src/analyze/analyze.h b/src/analyze/analyze.h index ac31dfce832..1d489b723e9 100644 --- a/src/analyze/analyze.h +++ b/src/analyze/analyze.h @@ -24,6 +24,7 @@ extern PagerFlags arg_pager_flags; extern CatFlags arg_cat_flags; extern BusTransport arg_transport; extern const char *arg_host; +extern const char *arg_debugger; extern RuntimeScope arg_runtime_scope; extern RecursiveErrors arg_recursive_errors; extern bool arg_man; diff --git a/src/analyze/meson.build b/src/analyze/meson.build index 283378a2f3c..b1a5b691ce5 100644 --- a/src/analyze/meson.build +++ b/src/analyze/meson.build @@ -32,6 +32,7 @@ systemd_analyze_sources = files( 'analyze-timespan.c', 'analyze-timestamp.c', 'analyze-unit-files.c', + 'analyze-unit-gdb.c', 'analyze-unit-paths.c', 'analyze-unit-shell.c', 'analyze-verify.c', diff --git a/src/basic/signal-util.c b/src/basic/signal-util.c index 459328a3c72..bd7d88f28fa 100644 --- a/src/basic/signal-util.c +++ b/src/basic/signal-util.c @@ -5,6 +5,7 @@ #include "errno-util.h" #include "parse-util.h" +#include "process-util.h" #include "signal-util.h" #include "stdio-util.h" #include "string-table.h" @@ -319,3 +320,15 @@ int parse_signo(const char *s, int *ret) { return 0; } + +void sigterm_process_group_handler(int signal, siginfo_t *info, void *ucontext) { + assert(signal == SIGTERM); + assert(info); + + /* If the sender is not us, propagate the signal to all processes in + * the same process group */ + if (si_code_from_process(info->si_code) && + pid_is_valid(info->si_pid) && + info->si_pid != getpid_cached()) + (void) kill(0, signal); +} diff --git a/src/basic/signal-util.h b/src/basic/signal-util.h index c45b9934d02..7d8a284c503 100644 --- a/src/basic/signal-util.h +++ b/src/basic/signal-util.h @@ -93,3 +93,5 @@ static inline bool si_code_from_process(int si_code) { return si_code < 0 || IN_SET(si_code, SI_USER, SI_QUEUE); } + +void sigterm_process_group_handler(int signal, siginfo_t *info, void *ucontext); diff --git a/src/coredump/coredumpctl.c b/src/coredump/coredumpctl.c index a8d81955c0a..d0755d9e1b4 100644 --- a/src/coredump/coredumpctl.c +++ b/src/coredump/coredumpctl.c @@ -1164,21 +1164,9 @@ static int dump_core(int argc, char **argv, void *userdata) { return 0; } -static void sigterm_handler(int signal, siginfo_t *info, void *ucontext) { - assert(signal == SIGTERM); - assert(info); - - /* If the sender is not us, propagate the signal to all processes in - * the same process group */ - if (si_code_from_process(info->si_code) && - pid_is_valid(info->si_pid) && - info->si_pid != getpid_cached()) - (void) kill(0, signal); -} - static int run_debug(int argc, char **argv, void *userdata) { static const struct sigaction sa = { - .sa_sigaction = sigterm_handler, + .sa_sigaction = sigterm_process_group_handler, .sa_flags = SA_SIGINFO, };