diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
commit | 55944e5e40b1be2afc4855d8d2baf4b73d1876b5 (patch) | |
tree | 33f869f55a1b149e9b7c2b7e201867ca5dd52992 /src/analyze | |
parent | Initial commit. (diff) | |
download | systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.tar.xz systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.zip |
Adding upstream version 255.4.upstream/255.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/analyze')
64 files changed, 7725 insertions, 0 deletions
diff --git a/src/analyze/analyze-blame.c b/src/analyze/analyze-blame.c new file mode 100644 index 0000000..81e5c59 --- /dev/null +++ b/src/analyze/analyze-blame.c @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-blame.h" +#include "analyze-time-data.h" +#include "format-table.h" + +int verb_blame(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(unit_times_free_arrayp) UnitTimes *times = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + TableCell *cell; + int n, r; + + r = acquire_bus(&bus, NULL); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + + n = acquire_time_data(bus, /* require_finished = */ false, ×); + if (n <= 0) + return n; + + table = table_new("time", "unit"); + if (!table) + return log_oom(); + + table_set_header(table, false); + + assert_se(cell = table_get_cell(table, 0, 0)); + r = table_set_ellipsize_percent(table, cell, 100); + if (r < 0) + return r; + + r = table_set_align_percent(table, cell, 100); + if (r < 0) + return r; + + assert_se(cell = table_get_cell(table, 0, 1)); + r = table_set_ellipsize_percent(table, cell, 100); + if (r < 0) + return r; + + r = table_set_sort(table, (size_t) 0); + if (r < 0) + return r; + + r = table_set_reverse(table, 0, true); + if (r < 0) + return r; + + for (UnitTimes *u = times; u->has_data; u++) { + if (u->time <= 0) + continue; + + r = table_add_many(table, + TABLE_TIMESPAN_MSEC, u->time, + TABLE_STRING, u->name); + if (r < 0) + return table_log_add_error(r); + } + + pager_open(arg_pager_flags); + + r = table_print(table, NULL); + if (r < 0) + return r; + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-blame.h b/src/analyze/analyze-blame.h new file mode 100644 index 0000000..d9aa985 --- /dev/null +++ b/src/analyze/analyze-blame.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_blame(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-calendar.c b/src/analyze/analyze-calendar.c new file mode 100644 index 0000000..6daab08 --- /dev/null +++ b/src/analyze/analyze-calendar.c @@ -0,0 +1,140 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-calendar.h" +#include "calendarspec.h" +#include "format-table.h" +#include "terminal-util.h" + +static int test_calendar_one(usec_t n, const char *p) { + _cleanup_(calendar_spec_freep) CalendarSpec *spec = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + _cleanup_free_ char *t = NULL; + TableCell *cell; + int r; + + r = calendar_spec_from_string(p, &spec); + if (r < 0) { + log_error_errno(r, "Failed to parse calendar specification '%s': %m", p); + time_parsing_hint(p, /* calendar= */ false, /* timestamp= */ true, /* timespan= */ true); + return r; + } + + r = calendar_spec_to_string(spec, &t); + if (r < 0) + return log_error_errno(r, "Failed to format calendar specification '%s': %m", p); + + table = table_new_vertical(); + if (!table) + return log_oom(); + + assert_se(cell = table_get_cell(table, 0, 0)); + r = table_set_ellipsize_percent(table, cell, 100); + if (r < 0) + return r; + + assert_se(cell = table_get_cell(table, 0, 1)); + r = table_set_ellipsize_percent(table, cell, 100); + if (r < 0) + return r; + + if (!streq(t, p)) { + r = table_add_many(table, + TABLE_FIELD, "Original form", + TABLE_STRING, p); + if (r < 0) + return table_log_add_error(r); + } + + r = table_add_many(table, + TABLE_FIELD, "Normalized form", + TABLE_STRING, t); + if (r < 0) + return table_log_add_error(r); + + for (unsigned i = 0; i < arg_iterations; i++) { + usec_t next; + + r = calendar_spec_next_usec(spec, n, &next); + if (r == -ENOENT) { + if (i == 0) { + r = table_add_many(table, + TABLE_FIELD, "Next elapse", + TABLE_STRING, "never", + TABLE_SET_COLOR, ansi_highlight_yellow()); + if (r < 0) + return table_log_add_error(r); + } + break; + } + if (r < 0) + return log_error_errno(r, "Failed to determine next elapse for '%s': %m", p); + + if (i == 0) { + r = table_add_many(table, + TABLE_FIELD, "Next elapse", + TABLE_TIMESTAMP, next, + TABLE_SET_COLOR, ansi_highlight_blue()); + if (r < 0) + return table_log_add_error(r); + } else { + int k = DECIMAL_STR_WIDTH(i + 1); + + if (k < 8) + k = 8 - k; + else + k = 0; + + r = table_add_cell_stringf_full(table, NULL, TABLE_FIELD, "Iteration #%u", i+1); + if (r < 0) + return table_log_add_error(r); + + r = table_add_many(table, + TABLE_TIMESTAMP, next, + TABLE_SET_COLOR, ansi_highlight_blue()); + if (r < 0) + return table_log_add_error(r); + } + + if (!in_utc_timezone()) { + r = table_add_many(table, + TABLE_FIELD, "(in UTC)", + TABLE_TIMESTAMP_UTC, next); + if (r < 0) + return table_log_add_error(r); + } + + r = table_add_many(table, + TABLE_FIELD, "From now", + TABLE_TIMESTAMP_RELATIVE, next); + if (r < 0) + return table_log_add_error(r); + + n = next; + } + + return table_print(table, NULL); +} + +int verb_calendar(int argc, char *argv[], void *userdata) { + int r = 0; + usec_t n; + + if (arg_base_time != USEC_INFINITY) + n = arg_base_time; + else + n = now(CLOCK_REALTIME); /* We want to use the same "base" for all expressions */ + + STRV_FOREACH(p, strv_skip(argv, 1)) { + int k; + + k = test_calendar_one(n, *p); + if (r == 0 && k < 0) + r = k; + + if (p[1]) + putchar('\n'); + } + + return r; +} diff --git a/src/analyze/analyze-calendar.h b/src/analyze/analyze-calendar.h new file mode 100644 index 0000000..3d6eac2 --- /dev/null +++ b/src/analyze/analyze-calendar.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_calendar(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-capability.c b/src/analyze/analyze-capability.c new file mode 100644 index 0000000..8072175 --- /dev/null +++ b/src/analyze/analyze-capability.c @@ -0,0 +1,56 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-capability.h" +#include "cap-list.h" +#include "capability-util.h" +#include "format-table.h" + +int verb_capabilities(int argc, char *argv[], void *userdata) { + _cleanup_(table_unrefp) Table *table = NULL; + unsigned last_cap; + int r; + + table = table_new("name", "number"); + if (!table) + return log_oom(); + + (void) table_set_align_percent(table, table_get_cell(table, 0, 1), 100); + + /* Determine the maximum of the last cap known by the kernel and by us */ + last_cap = MAX((unsigned) CAP_LAST_CAP, cap_last_cap()); + + if (strv_isempty(strv_skip(argv, 1))) + for (unsigned c = 0; c <= last_cap; c++) { + r = table_add_many(table, + TABLE_STRING, capability_to_name(c) ?: "cap_???", + TABLE_UINT, c); + if (r < 0) + return table_log_add_error(r); + } + else { + for (int i = 1; i < argc; i++) { + int c; + + c = capability_from_name(argv[i]); + if (c < 0 || (unsigned) c > last_cap) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Capability \"%s\" not known.", argv[i]); + + r = table_add_many(table, + TABLE_STRING, capability_to_name(c) ?: "cap_???", + TABLE_UINT, (unsigned) c); + if (r < 0) + return table_log_add_error(r); + } + + (void) table_set_sort(table, (size_t) 1); + } + + pager_open(arg_pager_flags); + + r = table_print(table, NULL); + if (r < 0) + return r; + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-capability.h b/src/analyze/analyze-capability.h new file mode 100644 index 0000000..07ff088 --- /dev/null +++ b/src/analyze/analyze-capability.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_capabilities(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-cat-config.c b/src/analyze/analyze-cat-config.c new file mode 100644 index 0000000..66bbbc1 --- /dev/null +++ b/src/analyze/analyze-cat-config.c @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-cat-config.h" +#include "conf-files.h" +#include "constants.h" +#include "nulstr-util.h" +#include "path-util.h" +#include "pretty-print.h" +#include "strv.h" + +int verb_cat_config(int argc, char *argv[], void *userdata) { + char **list; + int r; + + pager_open(arg_pager_flags); + + list = strv_skip(argv, 1); + STRV_FOREACH(arg, list) { + const char *t = NULL; + + if (arg != list) + print_separator(); + + if (path_is_absolute(*arg)) { + NULSTR_FOREACH(dir, CONF_PATHS_NULSTR("")) { + t = path_startswith(*arg, dir); + if (t) + break; + } + + if (!t) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Path %s does not start with any known prefix.", *arg); + } else + t = *arg; + + r = conf_files_cat(arg_root, t, arg_cat_flags); + if (r < 0) + return r; + } + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-cat-config.h b/src/analyze/analyze-cat-config.h new file mode 100644 index 0000000..64e87a3 --- /dev/null +++ b/src/analyze/analyze-cat-config.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_cat_config(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-compare-versions.c b/src/analyze/analyze-compare-versions.c new file mode 100644 index 0000000..94cff18 --- /dev/null +++ b/src/analyze/analyze-compare-versions.c @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <stdio.h> + +#include "analyze-compare-versions.h" +#include "compare-operator.h" +#include "macro.h" +#include "string-util.h" +#include "strv.h" + +int verb_compare_versions(int argc, char *argv[], void *userdata) { + const char *v1 = ASSERT_PTR(argv[1]), *v2 = ASSERT_PTR(argv[argc-1]); + int r; + + assert(IN_SET(argc, 3, 4)); + assert(argv); + + /* We only output a warning on invalid version strings (instead of failing), since the comparison + * functions try to handle invalid strings gracefully and it's still interesting to see what the + * comparison result will be. */ + if (!version_is_valid_versionspec(v1)) + log_warning("Version string 1 contains disallowed characters, they will be treated as separators: %s", v1); + if (!version_is_valid_versionspec(v2)) + log_warning("Version string 2 contains disallowed characters, they will be treated as separators: %s", v2); + + if (argc == 3) { + r = strverscmp_improved(v1, v2); + printf("%s %s %s\n", + isempty(v1) ? "''" : v1, + comparison_operator(r), + isempty(v2) ? "''" : v2); + + /* This matches the exit convention used by rpmdev-vercmp. + * We don't use named values because 11 and 12 don't have names. */ + return r < 0 ? 12 : r > 0 ? 11 : 0; + + } else { + const char *op = ASSERT_PTR(argv[2]); + CompareOperator operator; + assert(argc == 4); + + operator = parse_compare_operator(&op, COMPARE_ALLOW_TEXTUAL); + if (operator < 0 || !isempty(op)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown operator \"%s\".", op); + + r = version_or_fnmatch_compare(operator, v1, v2); + if (r < 0) + return log_error_errno(r, "Failed to compare versions: %m"); + + return r ? EXIT_SUCCESS : EXIT_FAILURE; + } +} diff --git a/src/analyze/analyze-compare-versions.h b/src/analyze/analyze-compare-versions.h new file mode 100644 index 0000000..ac90ede --- /dev/null +++ b/src/analyze/analyze-compare-versions.h @@ -0,0 +1,3 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +int verb_compare_versions(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-condition.c b/src/analyze/analyze-condition.c new file mode 100644 index 0000000..1e9136d --- /dev/null +++ b/src/analyze/analyze-condition.c @@ -0,0 +1,137 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <stdlib.h> + +#include "analyze.h" +#include "analyze-condition.h" +#include "analyze-verify-util.h" +#include "condition.h" +#include "conf-parser.h" +#include "load-fragment.h" +#include "service.h" + +static int parse_condition(Unit *u, const char *line) { + assert(u); + assert(line); + + for (ConditionType t = 0; t < _CONDITION_TYPE_MAX; t++) { + ConfigParserCallback callback; + Condition **target; + const char *p, *name; + + name = condition_type_to_string(t); + p = startswith(line, name); + if (p) + target = &u->conditions; + else { + name = assert_type_to_string(t); + p = startswith(line, name); + if (!p) + continue; + + target = &u->asserts; + } + + p += strspn(p, WHITESPACE); + + if (*p != '=') + continue; + p++; + + p += strspn(p, WHITESPACE); + + if (condition_takes_path(t)) + callback = config_parse_unit_condition_path; + else + callback = config_parse_unit_condition_string; + + return callback(NULL, "(cmdline)", 0, NULL, 0, name, t, p, target, u); + } + + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot parse \"%s\".", line); +} + +_printf_(7, 8) +static int log_helper(void *userdata, int level, int error, const char *file, int line, const char *func, const char *format, ...) { + Unit *u = ASSERT_PTR(userdata); + va_list ap; + int r; + + /* "upgrade" debug messages */ + level = MIN(LOG_INFO, level); + + va_start(ap, format); + r = log_object_internalv(level, error, file, line, func, + NULL, + u->id, + NULL, + NULL, + format, ap); + va_end(ap); + + return r; +} + +static int verify_conditions(char **lines, RuntimeScope scope, const char *unit, const char *root) { + _cleanup_(manager_freep) Manager *m = NULL; + Unit *u; + int r, q = 1; + + if (unit) { + r = verify_set_unit_path(STRV_MAKE(unit)); + if (r < 0) + return log_error_errno(r, "Failed to set unit load path: %m"); + } + + r = manager_new(scope, MANAGER_TEST_RUN_MINIMAL|MANAGER_TEST_DONT_OPEN_EXECUTOR, &m); + if (r < 0) + return log_error_errno(r, "Failed to initialize manager: %m"); + + log_debug("Starting manager..."); + r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, root); + if (r < 0) + return r; + + if (unit) { + _cleanup_free_ char *prepared = NULL; + + r = verify_prepare_filename(unit, &prepared); + if (r < 0) + return log_error_errno(r, "Failed to prepare filename %s: %m", unit); + + r = manager_load_startable_unit_or_warn(m, NULL, prepared, &u); + if (r < 0) + return r; + } else { + r = unit_new_for_name(m, sizeof(Service), "test.service", &u); + if (r < 0) + return log_error_errno(r, "Failed to create test.service: %m"); + + STRV_FOREACH(line, lines) { + r = parse_condition(u, *line); + if (r < 0) + return r; + } + } + + condition_test_logger_t logger = arg_quiet ? NULL : log_helper; + r = condition_test_list(u->asserts, environ, assert_type_to_string, logger, u); + if (u->asserts) + log_full(arg_quiet ? LOG_DEBUG : LOG_NOTICE, "Asserts %s.", r > 0 ? "succeeded" : "failed"); + + q = condition_test_list(u->conditions, environ, condition_type_to_string, logger, u); + if (u->conditions) + log_full(arg_quiet ? LOG_DEBUG : LOG_NOTICE, "Conditions %s.", q > 0 ? "succeeded" : "failed"); + + return r > 0 && q > 0 ? 0 : -EIO; +} + +int verb_condition(int argc, char *argv[], void *userdata) { + int r; + + r = verify_conditions(strv_skip(argv, 1), arg_runtime_scope, arg_unit, arg_root); + if (r < 0) + return r; + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-condition.h b/src/analyze/analyze-condition.h new file mode 100644 index 0000000..28ef51a --- /dev/null +++ b/src/analyze/analyze-condition.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_condition(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-critical-chain.c b/src/analyze/analyze-critical-chain.c new file mode 100644 index 0000000..4a7f452 --- /dev/null +++ b/src/analyze/analyze-critical-chain.c @@ -0,0 +1,230 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze-critical-chain.h" +#include "analyze-time-data.h" +#include "analyze.h" +#include "bus-error.h" +#include "copy.h" +#include "path-util.h" +#include "sort-util.h" +#include "special.h" +#include "static-destruct.h" +#include "strv.h" +#include "terminal-util.h" + +static Hashmap *unit_times_hashmap = NULL; +STATIC_DESTRUCTOR_REGISTER(unit_times_hashmap, hashmap_freep); + +static int list_dependencies_print( + const char *name, + unsigned level, + unsigned branches, + bool last, + UnitTimes *times, + BootTimes *boot) { + + for (unsigned i = level; i != 0; i--) + printf("%s", special_glyph(branches & (1 << (i-1)) ? SPECIAL_GLYPH_TREE_VERTICAL : SPECIAL_GLYPH_TREE_SPACE)); + + printf("%s", special_glyph(last ? SPECIAL_GLYPH_TREE_RIGHT : SPECIAL_GLYPH_TREE_BRANCH)); + + if (times) { + if (timestamp_is_set(times->time)) + printf("%s%s @%s +%s%s", ansi_highlight_red(), name, + FORMAT_TIMESPAN(times->activating - boot->userspace_time, USEC_PER_MSEC), + FORMAT_TIMESPAN(times->time, USEC_PER_MSEC), ansi_normal()); + else if (times->activated > boot->userspace_time) + printf("%s @%s", name, FORMAT_TIMESPAN(times->activated - boot->userspace_time, USEC_PER_MSEC)); + else + printf("%s", name); + } else + printf("%s", name); + printf("\n"); + + return 0; +} + +static int list_dependencies_get_dependencies(sd_bus *bus, const char *name, char ***deps) { + _cleanup_free_ char *path = NULL; + + assert(bus); + assert(name); + assert(deps); + + path = unit_dbus_path_from_name(name); + if (!path) + return -ENOMEM; + + return bus_get_unit_property_strv(bus, path, "After", deps); +} + +static int list_dependencies_compare(char *const *a, char *const *b) { + usec_t usa = 0, usb = 0; + UnitTimes *times; + + times = hashmap_get(unit_times_hashmap, *a); + if (times) + usa = times->activated; + times = hashmap_get(unit_times_hashmap, *b); + if (times) + usb = times->activated; + + return CMP(usb, usa); +} + +static bool times_in_range(const UnitTimes *times, const BootTimes *boot) { + return times && times->activated > 0 && times->activated <= boot->finish_time; +} + +static int list_dependencies_one(sd_bus *bus, const char *name, unsigned level, char ***units, unsigned branches) { + _cleanup_strv_free_ char **deps = NULL; + int r; + usec_t service_longest = 0; + int to_print = 0; + UnitTimes *times; + BootTimes *boot; + + if (strv_extend(units, name)) + return log_oom(); + + r = list_dependencies_get_dependencies(bus, name, &deps); + if (r < 0) + return r; + + typesafe_qsort(deps, strv_length(deps), list_dependencies_compare); + + r = acquire_boot_times(bus, /* require_finished = */ true, &boot); + if (r < 0) + return r; + + STRV_FOREACH(c, deps) { + times = hashmap_get(unit_times_hashmap, *c); + if (times_in_range(times, boot) && times->activated >= service_longest) + service_longest = times->activated; + } + + if (service_longest == 0) + return r; + + STRV_FOREACH(c, deps) { + times = hashmap_get(unit_times_hashmap, *c); + if (times_in_range(times, boot) && service_longest - times->activated <= arg_fuzz) + to_print++; + } + + if (!to_print) + return r; + + STRV_FOREACH(c, deps) { + times = hashmap_get(unit_times_hashmap, *c); + if (!times_in_range(times, boot) || service_longest - times->activated > arg_fuzz) + continue; + + to_print--; + + r = list_dependencies_print(*c, level, branches, to_print == 0, times, boot); + if (r < 0) + return r; + + if (strv_contains(*units, *c)) { + r = list_dependencies_print("...", level + 1, (branches << 1) | (to_print ? 1 : 0), + true, NULL, boot); + if (r < 0) + return r; + continue; + } + + r = list_dependencies_one(bus, *c, level + 1, units, (branches << 1) | (to_print ? 1 : 0)); + if (r < 0) + return r; + + if (to_print == 0) + break; + } + return 0; +} + +static int list_dependencies(sd_bus *bus, const char *name) { + _cleanup_strv_free_ char **units = NULL; + UnitTimes *times; + int r; + const char *id; + _cleanup_free_ char *path = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + BootTimes *boot; + + assert(bus); + + path = unit_dbus_path_from_name(name); + if (!path) + return -ENOMEM; + + r = sd_bus_get_property( + bus, + "org.freedesktop.systemd1", + path, + "org.freedesktop.systemd1.Unit", + "Id", + &error, + &reply, + "s"); + if (r < 0) + return log_error_errno(r, "Failed to get ID: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "s", &id); + if (r < 0) + return bus_log_parse_error(r); + + times = hashmap_get(unit_times_hashmap, id); + + r = acquire_boot_times(bus, /* require_finished = */ true, &boot); + if (r < 0) + return r; + + if (times) { + if (times->time) + printf("%s%s +%s%s\n", ansi_highlight_red(), id, + FORMAT_TIMESPAN(times->time, USEC_PER_MSEC), ansi_normal()); + else if (times->activated > boot->userspace_time) + printf("%s @%s\n", id, + FORMAT_TIMESPAN(times->activated - boot->userspace_time, USEC_PER_MSEC)); + else + printf("%s\n", id); + } + + return list_dependencies_one(bus, name, 0, &units, 0); +} + +int verb_critical_chain(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(unit_times_free_arrayp) UnitTimes *times = NULL; + int n, r; + + r = acquire_bus(&bus, NULL); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + + n = acquire_time_data(bus, /* require_finished = */ true, ×); + if (n <= 0) + return n; + + for (UnitTimes *u = times; u->has_data; u++) { + r = hashmap_ensure_put(&unit_times_hashmap, &string_hash_ops, u->name, u); + if (r < 0) + return log_error_errno(r, "Failed to add entry to hashmap: %m"); + } + + pager_open(arg_pager_flags); + + puts("The time when unit became active or started is printed after the \"@\" character.\n" + "The time the unit took to start is printed after the \"+\" character.\n"); + + if (argc > 1) + STRV_FOREACH(name, strv_skip(argv, 1)) + list_dependencies(bus, *name); + else + list_dependencies(bus, SPECIAL_DEFAULT_TARGET); + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-critical-chain.h b/src/analyze/analyze-critical-chain.h new file mode 100644 index 0000000..844249c --- /dev/null +++ b/src/analyze/analyze-critical-chain.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_critical_chain(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-dot.c b/src/analyze/analyze-dot.c new file mode 100644 index 0000000..bf8aa81 --- /dev/null +++ b/src/analyze/analyze-dot.c @@ -0,0 +1,182 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-dot.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "bus-unit-util.h" +#include "glob-util.h" +#include "terminal-util.h" + +static int graph_one_property( + sd_bus *bus, + const UnitInfo *u, + const char *prop, + const char *color, + char *patterns[], + char *from_patterns[], + char *to_patterns[]) { + + _cleanup_strv_free_ char **units = NULL; + bool match_patterns; + int r; + + assert(u); + assert(prop); + assert(color); + + match_patterns = strv_fnmatch(patterns, u->id); + + if (!strv_isempty(from_patterns) && !match_patterns && !strv_fnmatch(from_patterns, u->id)) + return 0; + + r = bus_get_unit_property_strv(bus, u->unit_path, prop, &units); + if (r < 0) + return r; + + STRV_FOREACH(unit, units) { + bool match_patterns2; + + match_patterns2 = strv_fnmatch(patterns, *unit); + + if (!strv_isempty(to_patterns) && !match_patterns2 && !strv_fnmatch(to_patterns, *unit)) + continue; + + if (!strv_isempty(patterns) && !match_patterns && !match_patterns2) + continue; + + printf("\t\"%s\"->\"%s\" [color=\"%s\"];\n", u->id, *unit, color); + } + + return 0; +} + +static int graph_one(sd_bus *bus, const UnitInfo *u, char *patterns[], char *from_patterns[], char *to_patterns[]) { + int r; + + assert(bus); + assert(u); + + if (IN_SET(arg_dot, DEP_ORDER, DEP_ALL)) { + r = graph_one_property(bus, u, "After", "green", patterns, from_patterns, to_patterns); + if (r < 0) + return r; + } + + if (IN_SET(arg_dot, DEP_REQUIRE, DEP_ALL)) { + r = graph_one_property(bus, u, "Requires", "black", patterns, from_patterns, to_patterns); + if (r < 0) + return r; + r = graph_one_property(bus, u, "Requisite", "darkblue", patterns, from_patterns, to_patterns); + if (r < 0) + return r; + r = graph_one_property(bus, u, "Wants", "grey66", patterns, from_patterns, to_patterns); + if (r < 0) + return r; + r = graph_one_property(bus, u, "Conflicts", "red", patterns, from_patterns, to_patterns); + if (r < 0) + return r; + } + + return 0; +} + +static int expand_patterns(sd_bus *bus, char **patterns, char ***ret) { + _cleanup_strv_free_ char **expanded_patterns = NULL; + int r; + + STRV_FOREACH(pattern, patterns) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *unit = NULL, *unit_id = NULL; + + if (strv_extend(&expanded_patterns, *pattern) < 0) + return log_oom(); + + if (string_is_glob(*pattern)) + continue; + + unit = unit_dbus_path_from_name(*pattern); + if (!unit) + return log_oom(); + + r = sd_bus_get_property_string( + bus, + "org.freedesktop.systemd1", + unit, + "org.freedesktop.systemd1.Unit", + "Id", + &error, + &unit_id); + if (r < 0) + return log_error_errno(r, "Failed to get ID: %s", bus_error_message(&error, r)); + + if (!streq(*pattern, unit_id)) { + if (strv_extend(&expanded_patterns, unit_id) < 0) + return log_oom(); + } + } + + *ret = TAKE_PTR(expanded_patterns); /* do not free */ + + return 0; +} + +int verb_dot(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_strv_free_ char **expanded_patterns = NULL; + _cleanup_strv_free_ char **expanded_from_patterns = NULL; + _cleanup_strv_free_ char **expanded_to_patterns = NULL; + int r; + UnitInfo u; + + r = acquire_bus(&bus, NULL); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + + r = expand_patterns(bus, strv_skip(argv, 1), &expanded_patterns); + if (r < 0) + return r; + + r = expand_patterns(bus, arg_dot_from_patterns, &expanded_from_patterns); + if (r < 0) + return r; + + r = expand_patterns(bus, arg_dot_to_patterns, &expanded_to_patterns); + if (r < 0) + return r; + + r = bus_call_method(bus, bus_systemd_mgr, "ListUnits", &error, &reply, NULL); + if (r < 0) + return log_error_errno(r, "Failed to list units: %s", bus_error_message(&error, r)); + + r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ssssssouso)"); + if (r < 0) + return bus_log_parse_error(r); + + printf("digraph systemd {\n"); + + while ((r = bus_parse_unit_info(reply, &u)) > 0) { + + r = graph_one(bus, &u, expanded_patterns, expanded_from_patterns, expanded_to_patterns); + if (r < 0) + return r; + } + if (r < 0) + return bus_log_parse_error(r); + + printf("}\n"); + + log_info(" Color legend: black = Requires\n" + " dark blue = Requisite\n" + " dark grey = Wants\n" + " red = Conflicts\n" + " green = After\n"); + + if (on_tty() && !arg_quiet) + log_notice("-- You probably want to process this output with graphviz' dot tool.\n" + "-- Try a shell pipeline like 'systemd-analyze dot | dot -Tsvg > systemd.svg'!\n"); + + return 0; +} diff --git a/src/analyze/analyze-dot.h b/src/analyze/analyze-dot.h new file mode 100644 index 0000000..144b43d --- /dev/null +++ b/src/analyze/analyze-dot.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_dot(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-dump.c b/src/analyze/analyze-dump.c new file mode 100644 index 0000000..2642582 --- /dev/null +++ b/src/analyze/analyze-dump.c @@ -0,0 +1,145 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-bus.h" + +#include "analyze-dump.h" +#include "analyze.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "bus-util.h" +#include "copy.h" + +static int dump_fallback(sd_bus *bus) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + const char *text; + int r; + + assert(bus); + + r = bus_call_method(bus, bus_systemd_mgr, "Dump", &error, &reply, NULL); + if (r < 0) + return log_error_errno(r, "Failed to call Dump: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "s", &text); + if (r < 0) + return bus_log_parse_error(r); + + fputs(text, stdout); + return 0; +} + +static int dump(sd_bus *bus) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + int r; + + r = bus_call_method(bus, bus_systemd_mgr, "DumpByFileDescriptor", &error, &reply, NULL); + if (IN_SET(r, -EACCES, -EBADR)) + return 0; /* Fall back to non-fd method. We need to do this even if the bus supports sending + * fds to cater to very old managers which didn't have the fd-based method. */ + if (r < 0) + return log_error_errno(r, "Failed to call DumpByFileDescriptor: %s", + bus_error_message(&error, r)); + + return dump_fd_reply(reply); +} + +static int dump_patterns_fallback(sd_bus *bus, char **patterns) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL, *m = NULL; + const char *text; + int r; + + r = bus_message_new_method_call(bus, &m, bus_systemd_mgr, "DumpUnitsMatchingPatterns"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_strv(m, patterns); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, 0, &error, &reply); + if (r < 0) + return log_error_errno(r, "Failed to call DumpUnitsMatchingPatterns: %s", + bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "s", &text); + if (r < 0) + return bus_log_parse_error(r); + + fputs(text, stdout); + return 0; +} + +static int dump_patterns(sd_bus *bus, char **patterns) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL, *m = NULL; + int r; + + r = bus_message_new_method_call(bus, &m, bus_systemd_mgr, "DumpUnitsMatchingPatternsByFileDescriptor"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_strv(m, patterns); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, 0, &error, &reply); + if (r < 0) + return log_error_errno(r, "Failed to call DumpUnitsMatchingPatternsByFileDescriptor: %s", + bus_error_message(&error, r)); + + return dump_fd_reply(reply); +} + +static int mangle_patterns(char **args, char ***ret) { + _cleanup_strv_free_ char **mangled = NULL; + int r; + + STRV_FOREACH(arg, args) { + char *t; + + r = unit_name_mangle_with_suffix(*arg, NULL, UNIT_NAME_MANGLE_GLOB, ".service", &t); + if (r < 0) + return log_error_errno(r, "Failed to mangle name '%s': %m", *arg); + + r = strv_consume(&mangled, t); + if (r < 0) + return log_oom(); + } + + if (strv_isempty(mangled)) + mangled = strv_free(mangled); + + *ret = TAKE_PTR(mangled); + return 0; +} + +int verb_dump(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_strv_free_ char **patterns = NULL; + int r; + + r = acquire_bus(&bus, NULL); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + + pager_open(arg_pager_flags); + + r = mangle_patterns(strv_skip(argv, 1), &patterns); + if (r < 0) + return r; + + r = sd_bus_can_send(bus, SD_BUS_TYPE_UNIX_FD); + if (r < 0) + return log_error_errno(r, "Unable to determine if bus connection supports fd passing: %m"); + if (r > 0) + r = patterns ? dump_patterns(bus, patterns) : dump(bus); + if (r == 0) /* wasn't supported */ + r = patterns ? dump_patterns_fallback(bus, patterns) : dump_fallback(bus); + if (r < 0) + return r; + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-dump.h b/src/analyze/analyze-dump.h new file mode 100644 index 0000000..5d6107c --- /dev/null +++ b/src/analyze/analyze-dump.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_dump(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-exit-status.c b/src/analyze/analyze-exit-status.c new file mode 100644 index 0000000..3a8d3f4 --- /dev/null +++ b/src/analyze/analyze-exit-status.c @@ -0,0 +1,56 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-exit-status.h" +#include "exit-status.h" +#include "format-table.h" + +int verb_exit_status(int argc, char *argv[], void *userdata) { + _cleanup_(table_unrefp) Table *table = NULL; + int r; + + table = table_new("name", "status", "class"); + if (!table) + return log_oom(); + + r = table_set_align_percent(table, table_get_cell(table, 0, 1), 100); + if (r < 0) + return log_error_errno(r, "Failed to right-align status: %m"); + + if (strv_isempty(strv_skip(argv, 1))) + for (size_t i = 0; i < ELEMENTSOF(exit_status_mappings); i++) { + if (!exit_status_mappings[i].name) + continue; + + r = table_add_many(table, + TABLE_STRING, exit_status_mappings[i].name, + TABLE_INT, (int) i, + TABLE_STRING, exit_status_class(i)); + if (r < 0) + return table_log_add_error(r); + } + else + for (int i = 1; i < argc; i++) { + int status; + + status = exit_status_from_string(argv[i]); + if (status < 0) + return log_error_errno(status, "Invalid exit status \"%s\".", argv[i]); + + assert(status >= 0 && (size_t) status < ELEMENTSOF(exit_status_mappings)); + r = table_add_many(table, + TABLE_STRING, exit_status_mappings[status].name ?: "-", + TABLE_INT, status, + TABLE_STRING, exit_status_class(status) ?: "-"); + if (r < 0) + return table_log_add_error(r); + } + + pager_open(arg_pager_flags); + + r = table_print(table, NULL); + if (r < 0) + return r; + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-exit-status.h b/src/analyze/analyze-exit-status.h new file mode 100644 index 0000000..ce14cdb --- /dev/null +++ b/src/analyze/analyze-exit-status.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_exit_status(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-fdstore.c b/src/analyze/analyze-fdstore.c new file mode 100644 index 0000000..13db7f5 --- /dev/null +++ b/src/analyze/analyze-fdstore.c @@ -0,0 +1,110 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze-fdstore.h" +#include "analyze.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "fd-util.h" +#include "format-table.h" + +static int dump_fdstore(sd_bus *bus, const char *arg) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + _cleanup_free_ char *unit = NULL; + int r; + + assert(bus); + assert(arg); + + r = unit_name_mangle_with_suffix(arg, NULL, UNIT_NAME_MANGLE_GLOB, ".service", &unit); + if (r < 0) + return log_error_errno(r, "Failed to mangle name '%s': %m", arg); + + r = bus_call_method( + bus, + bus_systemd_mgr, + "DumpUnitFileDescriptorStore", + &error, + &reply, + "s", unit); + if (r < 0) + return log_error_errno(r, "Failed to call DumpUnitFileDescriptorStore: %s", + bus_error_message(&error, r)); + + r = sd_bus_message_enter_container(reply, 'a', "(suuutuusu)"); + if (r < 0) + return bus_log_parse_error(r); + + table = table_new("fdname", "type", "devno", "inode", "rdevno", "path", "flags"); + if (!table) + return log_oom(); + + table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + + (void) table_set_align_percent(table, TABLE_HEADER_CELL(3), 100); + + for (;;) { + uint32_t mode, major, minor, rmajor, rminor, flags; + const char *fdname, *path; + uint64_t inode; + + r = sd_bus_message_read( + reply, + "(suuutuusu)", + &fdname, + &mode, + &major, &minor, + &inode, + &rmajor, &rminor, + &path, + &flags); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + r = table_add_many( + table, + TABLE_STRING, fdname, + TABLE_MODE_INODE_TYPE, mode, + TABLE_DEVNUM, makedev(major, minor), + TABLE_UINT64, inode, + TABLE_DEVNUM, makedev(rmajor, rminor), + TABLE_PATH, path, + TABLE_STRING, accmode_to_string(flags)); + if (r < 0) + return table_log_add_error(r); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return r; + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF) && table_get_rows(table) <= 0) + log_info("No file descriptors in fdstore of '%s'.", unit); + else { + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */true); + if (r < 0) + return log_error_errno(r, "Failed to output table: %m"); + } + + return EXIT_SUCCESS; +} + +int verb_fdstore(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r; + + r = acquire_bus(&bus, NULL); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + + STRV_FOREACH(arg, strv_skip(argv, 1)) { + r = dump_fdstore(bus, *arg); + if (r < 0) + return r; + } + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-fdstore.h b/src/analyze/analyze-fdstore.h new file mode 100644 index 0000000..0b990db --- /dev/null +++ b/src/analyze/analyze-fdstore.h @@ -0,0 +1,5 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-bus.h" + +int verb_fdstore(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-filesystems.c b/src/analyze/analyze-filesystems.c new file mode 100644 index 0000000..582e04e --- /dev/null +++ b/src/analyze/analyze-filesystems.c @@ -0,0 +1,221 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-filesystems.h" +#include "fileio.h" +#include "filesystems.h" +#include "set.h" +#include "strv.h" +#include "terminal-util.h" + +static int load_available_kernel_filesystems(Set **ret) { + _cleanup_set_free_ Set *filesystems = NULL; + _cleanup_free_ char *t = NULL; + int r; + + assert(ret); + + /* Let's read the available filesystems */ + + r = read_virtual_file("/proc/filesystems", SIZE_MAX, &t, NULL); + if (r < 0) + return r; + + for (int i = 0;;) { + _cleanup_free_ char *line = NULL; + const char *p; + + r = string_extract_line(t, i++, &line); + if (r < 0) + return log_oom(); + if (r == 0) + break; + + if (!line) + line = t; + + p = strchr(line, '\t'); + if (!p) + continue; + + p += strspn(p, WHITESPACE); + + r = set_put_strdup(&filesystems, p); + if (r < 0) + return log_error_errno(r, "Failed to add filesystem to list: %m"); + } + + *ret = TAKE_PTR(filesystems); + return 0; +} + +static void filesystem_set_remove(Set *s, const FilesystemSet *set) { + NULSTR_FOREACH(filesystem, set->value) { + if (filesystem[0] == '@') + continue; + + free(set_remove(s, filesystem)); + } +} + +static void dump_filesystem_set(const FilesystemSet *set) { + int r; + + if (!set) + return; + + printf("%s%s%s\n" + " # %s\n", + ansi_highlight(), + set->name, + ansi_normal(), + set->help); + + NULSTR_FOREACH(filesystem, set->value) { + const statfs_f_type_t *magic; + + if (filesystem[0] == '@') { + printf(" %s%s%s\n", ansi_underline(), filesystem, ansi_normal()); + continue; + } + + r = fs_type_from_string(filesystem, &magic); + assert_se(r >= 0); + + printf(" %s", filesystem); + + for (size_t i = 0; magic[i] != 0; i++) { + const char *primary; + if (i == 0) + printf(" %s(magic: ", ansi_grey()); + else + printf(", "); + + printf("0x%llx", (unsigned long long) magic[i]); + + primary = fs_type_to_string(magic[i]); + if (primary && !streq(primary, filesystem)) + printf("[%s]", primary); + + if (magic[i+1] == 0) + printf(")%s", ansi_normal()); + } + + printf("\n"); + } +} + +int verb_filesystems(int argc, char *argv[], void *userdata) { + bool first = true; + +#if ! HAVE_LIBBPF + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Not compiled with libbpf support, sorry."); +#endif + + pager_open(arg_pager_flags); + + if (strv_isempty(strv_skip(argv, 1))) { + _cleanup_set_free_ Set *kernel = NULL, *known = NULL; + int k; + + NULSTR_FOREACH(fs, filesystem_sets[FILESYSTEM_SET_KNOWN].value) + if (set_put_strdup(&known, fs) < 0) + return log_oom(); + + k = load_available_kernel_filesystems(&kernel); + + for (FilesystemGroups i = 0; i < _FILESYSTEM_SET_MAX; i++) { + const FilesystemSet *set = filesystem_sets + i; + if (!first) + puts(""); + + dump_filesystem_set(set); + filesystem_set_remove(kernel, set); + if (i != FILESYSTEM_SET_KNOWN) + filesystem_set_remove(known, set); + first = false; + } + + if (arg_quiet) /* Let's not show the extra stuff in quiet mode */ + return 0; + + if (!set_isempty(known)) { + _cleanup_free_ char **l = NULL; + + printf("\n" + "# %sUngrouped filesystems%s (known but not included in any of the groups except @known):\n", + ansi_highlight(), ansi_normal()); + + l = set_get_strv(known); + if (!l) + return log_oom(); + + strv_sort(l); + + STRV_FOREACH(filesystem, l) { + const statfs_f_type_t *magic; + bool is_primary = false; + + assert_se(fs_type_from_string(*filesystem, &magic) >= 0); + + for (size_t i = 0; magic[i] != 0; i++) { + const char *primary; + + primary = fs_type_to_string(magic[i]); + assert(primary); + + if (streq(primary, *filesystem)) + is_primary = true; + } + + if (!is_primary) { + log_debug("Skipping ungrouped file system '%s', because it's an alias for another one.", *filesystem); + continue; + } + + printf("# %s\n", *filesystem); + } + } + + if (k < 0) { + fputc('\n', stdout); + fflush(stdout); + log_notice_errno(k, "# Not showing unlisted filesystems, couldn't retrieve kernel filesystem list: %m"); + } else if (!set_isempty(kernel)) { + _cleanup_free_ char **l = NULL; + + printf("\n" + "# %sUnlisted filesystems%s (available to the local kernel, but not included in any of the groups listed above):\n", + ansi_highlight(), ansi_normal()); + + l = set_get_strv(kernel); + if (!l) + return log_oom(); + + strv_sort(l); + + STRV_FOREACH(filesystem, l) + printf("# %s\n", *filesystem); + } + } else + STRV_FOREACH(name, strv_skip(argv, 1)) { + const FilesystemSet *set; + + if (!first) + puts(""); + + set = filesystem_set_find(*name); + if (!set) { + /* make sure the error appears below normal output */ + fflush(stdout); + + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "Filesystem set \"%s\" not found.", *name); + } + + dump_filesystem_set(set); + first = false; + } + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-filesystems.h b/src/analyze/analyze-filesystems.h new file mode 100644 index 0000000..0904571 --- /dev/null +++ b/src/analyze/analyze-filesystems.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_filesystems(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-image-policy.c b/src/analyze/analyze-image-policy.c new file mode 100644 index 0000000..0146b50 --- /dev/null +++ b/src/analyze/analyze-image-policy.c @@ -0,0 +1,156 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze-image-policy.h" +#include "analyze.h" +#include "format-table.h" +#include "terminal-util.h" + +static int table_add_designator_line(Table *table, PartitionDesignator d, PartitionPolicyFlags f) { + _cleanup_free_ char *q = NULL; + const char *color; + int r; + + assert(table); + assert(f >= 0); + + if (partition_policy_flags_to_string(f & _PARTITION_POLICY_USE_MASK, /* simplify= */ true, &q) < 0) + return log_oom(); + + color = (f & _PARTITION_POLICY_USE_MASK) == PARTITION_POLICY_IGNORE ? ansi_grey() : + ((f & (PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ABSENT)) == + (PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ABSENT)) ? ansi_highlight_yellow() : + (f & _PARTITION_POLICY_USE_MASK) == PARTITION_POLICY_ABSENT ? ansi_highlight_red() : + !(f & PARTITION_POLICY_UNPROTECTED) ? ansi_highlight_green() : NULL; + + if (d < 0) + r = table_add_many(table, + TABLE_STRING, "default", + TABLE_SET_COLOR, ansi_highlight_green(), + TABLE_STRING, q, + TABLE_SET_COLOR, color); + else + r = table_add_many(table, + TABLE_STRING, partition_designator_to_string(d), + TABLE_SET_COLOR, ansi_normal(), + TABLE_STRING, q, + TABLE_SET_COLOR, color); + if (r < 0) + return table_log_add_error(r); + + switch (f & _PARTITION_POLICY_READ_ONLY_MASK) { + + case PARTITION_POLICY_READ_ONLY_ON: + r = table_add_many(table, TABLE_BOOLEAN, true); + break; + + case PARTITION_POLICY_READ_ONLY_OFF: + r = table_add_many(table, TABLE_BOOLEAN, false); + break; + + default: + r = table_add_many(table, TABLE_EMPTY); + break; + } + if (r < 0) + return table_log_add_error(r); + + switch (f & _PARTITION_POLICY_GROWFS_MASK) { + + case PARTITION_POLICY_GROWFS_ON: + r = table_add_many(table, TABLE_BOOLEAN, true); + break; + + case PARTITION_POLICY_GROWFS_OFF: + r = table_add_many(table, TABLE_BOOLEAN, false); + break; + + default: + r = table_add_many(table, TABLE_EMPTY); + break; + } + + if (r < 0) + return table_log_add_error(r); + + return 0; +} + +int verb_image_policy(int argc, char *argv[], void *userdata) { + int r; + + for (int i = 1; i < argc; i++) { + _cleanup_(table_unrefp) Table *table = NULL; + _cleanup_(image_policy_freep) ImagePolicy *pbuf = NULL; + _cleanup_free_ char *as_string = NULL, *as_string_simplified = NULL; + const ImagePolicy *p; + + /* NB: The magic '@' strings are not officially documented for now, since we might change + * around defaults (and in particular where precisely to reuse policy). We should document + * them once the dust has settled a bit. For now it's just useful for debugging and + * introspect our own defaults without guaranteeing API safety. */ + if (streq(argv[i], "@sysext")) + p = &image_policy_sysext; + else if (streq(argv[i], "@sysext-strict")) + p = &image_policy_sysext_strict; + else if (streq(argv[i], "@confext")) + p = &image_policy_confext; + else if (streq(argv[i], "@container")) + p = &image_policy_container; + else if (streq(argv[i], "@service")) + p = &image_policy_service; + else if (streq(argv[i], "@host")) + p = &image_policy_host; + else { + r = image_policy_from_string(argv[i], &pbuf); + if (r < 0) + return log_error_errno(r, "Failed to parse image policy '%s': %m", argv[i]); + + p = pbuf; + } + + r = image_policy_to_string(p, /* simplify= */ false, &as_string); + if (r < 0) + return log_error_errno(r, "Failed to format policy '%s' as string: %m", argv[i]); + + r = image_policy_to_string(p, /* simplify= */ true, &as_string_simplified); + if (r < 0) + return log_error_errno(r, "Failed to format policy '%s' as string: %m", argv[i]); + + pager_open(arg_pager_flags); + + if (streq(as_string, as_string_simplified)) + printf("Analyzing policy: %s%s%s\n", ansi_highlight_magenta_underline(), as_string, ansi_normal()); + else + printf("Analyzing policy: %s%s%s\n" + " Long form: %s%s%s\n", + ansi_highlight(), as_string_simplified, ansi_normal(), + ansi_grey(), as_string, ansi_normal()); + + table = table_new("partition", "mode", "read-only", "growfs"); + if (!table) + return log_oom(); + + (void) table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + + for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) { + PartitionPolicyFlags f = image_policy_get_exhaustively(p, d); + assert(f >= 0); + + r = table_add_designator_line(table, d, f); + if (r < 0) + return r; + } + + r = table_add_designator_line(table, _PARTITION_DESIGNATOR_INVALID, image_policy_default(p)); + if (r < 0) + return r; + + putc('\n', stdout); + + r = table_print(table, NULL); + if (r < 0) + return r; + } + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-image-policy.h b/src/analyze/analyze-image-policy.h new file mode 100644 index 0000000..fa08447 --- /dev/null +++ b/src/analyze/analyze-image-policy.h @@ -0,0 +1,3 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +int verb_image_policy(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-inspect-elf.c b/src/analyze/analyze-inspect-elf.c new file mode 100644 index 0000000..70226a8 --- /dev/null +++ b/src/analyze/analyze-inspect-elf.c @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-inspect-elf.h" +#include "elf-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "format-table.h" +#include "format-util.h" +#include "json.h" +#include "path-util.h" +#include "strv.h" + +static int analyze_elf(char **filenames, JsonFormatFlags json_flags) { + int r; + + STRV_FOREACH(filename, filenames) { + _cleanup_(json_variant_unrefp) JsonVariant *package_metadata = NULL; + _cleanup_(table_unrefp) Table *t = NULL; + _cleanup_free_ char *abspath = NULL; + _cleanup_close_ int fd = -EBADF; + + r = path_make_absolute_cwd(*filename, &abspath); + if (r < 0) + return log_error_errno(r, "Could not make an absolute path out of \"%s\": %m", *filename); + + path_simplify(abspath); + + fd = RET_NERRNO(open(abspath, O_RDONLY|O_CLOEXEC)); + if (fd < 0) + return log_error_errno(fd, "Could not open \"%s\": %m", abspath); + + r = parse_elf_object(fd, abspath, /* fork_disable_dump= */false, NULL, &package_metadata); + if (r < 0) + return log_error_errno(r, "Parsing \"%s\" as ELF object failed: %m", abspath); + + t = table_new_vertical(); + if (!t) + return log_oom(); + + r = table_add_many( + t, + TABLE_FIELD, "path", + TABLE_STRING, abspath); + if (r < 0) + return table_log_add_error(r); + + if (package_metadata) { + JsonVariant *module_json; + const char *module_name; + + JSON_VARIANT_OBJECT_FOREACH(module_name, module_json, package_metadata) { + const char *field_name; + JsonVariant *field; + + /* The ELF type and architecture are added as top-level objects, + * since they are only parsed for the file itself, but the packaging + * metadata is parsed recursively in core files, so there might be + * multiple modules. */ + if (STR_IN_SET(module_name, "elfType", "elfArchitecture")) { + r = table_add_many( + t, + TABLE_FIELD, module_name, + TABLE_STRING, json_variant_string(module_json)); + if (r < 0) + return table_log_add_error(r); + + continue; + } + + /* path/elfType/elfArchitecture come first just once per file, + * then we might have multiple modules, so add a separator between + * them to make the output more readable. */ + r = table_add_many(t, TABLE_EMPTY, TABLE_EMPTY); + if (r < 0) + return table_log_add_error(r); + + /* In case of core files the module name will be the executable, + * but for binaries/libraries it's just the path, so don't print it + * twice. */ + if (!streq(abspath, module_name)) { + r = table_add_many( + t, + TABLE_FIELD, "module name", + TABLE_STRING, module_name); + if (r < 0) + return table_log_add_error(r); + } + + JSON_VARIANT_OBJECT_FOREACH(field_name, field, module_json) + if (json_variant_is_string(field)) { + r = table_add_many( + t, + TABLE_FIELD, field_name, + TABLE_STRING, json_variant_string(field)); + if (r < 0) + return table_log_add_error(r); + } + } + } + if (json_flags & JSON_FORMAT_OFF) { + r = table_print(t, NULL); + if (r < 0) + return table_log_print_error(r); + } else + json_variant_dump(package_metadata, json_flags, stdout, NULL); + } + + return 0; +} + +int verb_elf_inspection(int argc, char *argv[], void *userdata) { + pager_open(arg_pager_flags); + + return analyze_elf(strv_skip(argv, 1), arg_json_format_flags); +} diff --git a/src/analyze/analyze-inspect-elf.h b/src/analyze/analyze-inspect-elf.h new file mode 100644 index 0000000..a790eae --- /dev/null +++ b/src/analyze/analyze-inspect-elf.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_elf_inspection(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-log-control.c b/src/analyze/analyze-log-control.c new file mode 100644 index 0000000..cead0e8 --- /dev/null +++ b/src/analyze/analyze-log-control.c @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-log-control.h" +#include "verb-log-control.h" + +int verb_log_control(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r; + + assert(IN_SET(argc, 1, 2)); + + r = acquire_bus(&bus, NULL); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + + r = verb_log_control_common(bus, "org.freedesktop.systemd1", argv[0], argc == 2 ? argv[1] : NULL); + if (r < 0) + return r; + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-log-control.h b/src/analyze/analyze-log-control.h new file mode 100644 index 0000000..350c228 --- /dev/null +++ b/src/analyze/analyze-log-control.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_log_control(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-malloc.c b/src/analyze/analyze-malloc.c new file mode 100644 index 0000000..5e6ff5b --- /dev/null +++ b/src/analyze/analyze-malloc.c @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-bus.h" + +#include "analyze-malloc.h" +#include "analyze.h" +#include "bus-error.h" +#include "bus-internal.h" + +static int dump_malloc_info(sd_bus *bus, char *service) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + int r; + + assert(bus); + assert(service); + + r = sd_bus_call_method(bus, + service, + "/org/freedesktop/MemoryAllocation1", + "org.freedesktop.MemoryAllocation1", + "GetMallocInfo", + &error, + &reply, + NULL); + if (r < 0) + return log_error_errno(r, "Failed to call GetMallocInfo on '%s': %s", service, bus_error_message(&error, r)); + + return dump_fd_reply(reply); +} + +int verb_malloc(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + char **services = STRV_MAKE("org.freedesktop.systemd1"); + int r; + + if (!strv_isempty(strv_skip(argv, 1))) { + services = strv_skip(argv, 1); + STRV_FOREACH(service, services) + if (!service_name_is_valid(*service)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "D-Bus service name '%s' is not valid.", *service); + } + + r = acquire_bus(&bus, NULL); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + + r = sd_bus_can_send(bus, SD_BUS_TYPE_UNIX_FD); + if (r < 0) + return log_error_errno(r, "Unable to determine if bus connection supports fd passing: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unable to receive FDs over D-Bus."); + + pager_open(arg_pager_flags); + + STRV_FOREACH(service, services) { + r = dump_malloc_info(bus, *service); + if (r < 0) + return r; + } + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-malloc.h b/src/analyze/analyze-malloc.h new file mode 100644 index 0000000..d3feabd --- /dev/null +++ b/src/analyze/analyze-malloc.h @@ -0,0 +1,5 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#pragma once + +int verb_malloc(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-pcrs.c b/src/analyze/analyze-pcrs.c new file mode 100644 index 0000000..ed907f7 --- /dev/null +++ b/src/analyze/analyze-pcrs.c @@ -0,0 +1,144 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-pcrs.h" +#include "fileio.h" +#include "format-table.h" +#include "hexdecoct.h" +#include "terminal-util.h" +#include "tpm2-util.h" + +static int get_pcr_alg(const char **ret) { + assert(ret); + + FOREACH_STRING(alg, "sha256", "sha1") { + _cleanup_free_ char *p = NULL; + + if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/0", alg) < 0) + return log_oom(); + + if (access(p, F_OK) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", p); + } else { + *ret = alg; + return 1; + } + } + + log_notice("Kernel does not support reading PCR values."); + *ret = NULL; + return 0; +} + +static int get_current_pcr(const char *alg, uint32_t pcr, void **ret, size_t *ret_size) { + _cleanup_free_ char *p = NULL, *s = NULL; + _cleanup_free_ void *buf = NULL; + size_t ss = 0, bufsize = 0; + int r; + + assert(alg); + assert(ret); + assert(ret_size); + + if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/%" PRIu32, alg, pcr) < 0) + return log_oom(); + + r = read_virtual_file(p, 4096, &s, &ss); + if (r < 0) + return log_error_errno(r, "Failed to read '%s': %m", p); + + r = unhexmem(s, ss, &buf, &bufsize); + if (r < 0) + return log_error_errno(r, "Failed to decode hex PCR data '%s': %m", s); + + *ret = TAKE_PTR(buf); + *ret_size = bufsize; + return 0; +} + +static int add_pcr_to_table(Table *table, const char *alg, uint32_t pcr) { + _cleanup_free_ char *h = NULL; + const char *color = NULL; + int r; + + if (alg) { + _cleanup_free_ void *buf = NULL; + size_t bufsize = 0; + + r = get_current_pcr(alg, pcr, &buf, &bufsize); + if (r < 0) + return r; + + h = hexmem(buf, bufsize); + if (!h) + return log_oom(); + + /* Grey out PCRs that are not sensibly initialized */ + if (memeqbyte(0, buf, bufsize) || + memeqbyte(0xFFU, buf, bufsize)) + color = ANSI_GREY; + } + + r = table_add_many(table, + TABLE_UINT32, pcr, + TABLE_STRING, tpm2_pcr_index_to_string(pcr), + TABLE_STRING, h, + TABLE_SET_COLOR, color); + if (r < 0) + return table_log_add_error(r); + + return 0; +} + +int verb_pcrs(int argc, char *argv[], void *userdata) { + _cleanup_(table_unrefp) Table *table = NULL; + const char *alg = NULL; + int r; + + if (tpm2_support() != TPM2_SUPPORT_FULL) + log_notice("System lacks full TPM2 support, not showing PCR state."); + else { + r = get_pcr_alg(&alg); + if (r < 0) + return r; + } + + table = table_new("nr", "name", alg ?: "-"); + if (!table) + return log_oom(); + + (void) table_set_align_percent(table, table_get_cell(table, 0, 0), 100); + (void) table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + + if (!alg) /* hide hash column if we couldn't acquire it */ + (void) table_set_display(table, 0, 1); + + if (strv_isempty(strv_skip(argv, 1))) + for (uint32_t pi = 0; pi < _TPM2_PCR_INDEX_MAX_DEFINED; pi++) { + r = add_pcr_to_table(table, alg, pi); + if (r < 0) + return r; + } + else { + for (int i = 1; i < argc; i++) { + int pi; + + pi = tpm2_pcr_index_from_string(argv[i]); + if (pi < 0) + return log_error_errno(pi, "PCR index \"%s\" not known.", argv[i]); + + r = add_pcr_to_table(table, alg, pi); + if (r < 0) + return r; + } + + (void) table_set_sort(table, (size_t) 0); + } + + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */true); + if (r < 0) + return log_error_errno(r, "Failed to output table: %m"); + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-pcrs.h b/src/analyze/analyze-pcrs.h new file mode 100644 index 0000000..2a59511 --- /dev/null +++ b/src/analyze/analyze-pcrs.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_pcrs(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-plot.c b/src/analyze/analyze-plot.c new file mode 100644 index 0000000..81fc25b --- /dev/null +++ b/src/analyze/analyze-plot.c @@ -0,0 +1,493 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze-plot.h" +#include "analyze-time-data.h" +#include "analyze.h" +#include "bus-error.h" +#include "bus-map-properties.h" +#include "format-table.h" +#include "os-util.h" +#include "sort-util.h" +#include "strv.h" +#include "unit-def.h" +#include "version.h" + +#define SCALE_X (0.1 / 1000.0) /* pixels per us */ +#define SCALE_Y (20.0) + +#define svg(...) printf(__VA_ARGS__) + +#define svg_bar(class, x1, x2, y) \ + svg(" <rect class=\"%s\" x=\"%.03f\" y=\"%.03f\" width=\"%.03f\" height=\"%.03f\" />\n", \ + (class), \ + SCALE_X * (x1), SCALE_Y * (y), \ + SCALE_X * ((x2) - (x1)), SCALE_Y - 1.0) + +#define svg_text(b, x, y, format, ...) \ + do { \ + svg(" <text class=\"%s\" x=\"%.03f\" y=\"%.03f\">", (b) ? "left" : "right", SCALE_X * (x) + (b ? 5.0 : -5.0), SCALE_Y * (y) + 14.0); \ + svg(format, ## __VA_ARGS__); \ + svg("</text>\n"); \ + } while (false) + + +typedef struct HostInfo { + char *hostname; + char *kernel_name; + char *kernel_release; + char *kernel_version; + char *os_pretty_name; + char *virtualization; + char *architecture; +} HostInfo; + +static HostInfo *free_host_info(HostInfo *hi) { + if (!hi) + return NULL; + + free(hi->hostname); + free(hi->kernel_name); + free(hi->kernel_release); + free(hi->kernel_version); + free(hi->os_pretty_name); + free(hi->virtualization); + free(hi->architecture); + return mfree(hi); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(HostInfo *, free_host_info); + +static int acquire_host_info(sd_bus *bus, HostInfo **hi) { + static const struct bus_properties_map hostname_map[] = { + { "Hostname", "s", NULL, offsetof(HostInfo, hostname) }, + { "KernelName", "s", NULL, offsetof(HostInfo, kernel_name) }, + { "KernelRelease", "s", NULL, offsetof(HostInfo, kernel_release) }, + { "KernelVersion", "s", NULL, offsetof(HostInfo, kernel_version) }, + { "OperatingSystemPrettyName", "s", NULL, offsetof(HostInfo, os_pretty_name) }, + {} + }; + + static const struct bus_properties_map manager_map[] = { + { "Virtualization", "s", NULL, offsetof(HostInfo, virtualization) }, + { "Architecture", "s", NULL, offsetof(HostInfo, architecture) }, + {} + }; + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *system_bus = NULL; + _cleanup_(free_host_infop) HostInfo *host = NULL; + int r; + + host = new0(HostInfo, 1); + if (!host) + return log_oom(); + + if (arg_runtime_scope != RUNTIME_SCOPE_SYSTEM) { + r = bus_connect_transport(arg_transport, arg_host, RUNTIME_SCOPE_SYSTEM, &system_bus); + if (r < 0) { + log_debug_errno(r, "Failed to connect to system bus, ignoring: %m"); + goto manager; + } + } + + r = bus_map_all_properties( + system_bus ?: bus, + "org.freedesktop.hostname1", + "/org/freedesktop/hostname1", + hostname_map, + BUS_MAP_STRDUP, + &error, + NULL, + host); + if (r < 0) { + log_debug_errno(r, "Failed to get host information from systemd-hostnamed, ignoring: %s", + bus_error_message(&error, r)); + sd_bus_error_free(&error); + } + +manager: + r = bus_map_all_properties( + bus, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + manager_map, + BUS_MAP_STRDUP, + &error, + NULL, + host); + if (r < 0) + return log_error_errno(r, "Failed to get host information from systemd: %s", + bus_error_message(&error, r)); + + *hi = TAKE_PTR(host); + return 0; +} + +static int compare_unit_start(const UnitTimes *a, const UnitTimes *b) { + return CMP(a->activating, b->activating); +} + +static void svg_graph_box(double height, double begin, double end) { + /* outside box, fill */ + svg("<rect class=\"box\" x=\"0\" y=\"0\" width=\"%.03f\" height=\"%.03f\" />\n", + SCALE_X * (end - begin), + SCALE_Y * height); + + for (long long i = ((long long) (begin / 100000)) * 100000; i <= end; i += 100000) { + /* lines for each second */ + if (i % 5000000 == 0) + svg(" <line class=\"sec5\" x1=\"%.03f\" y1=\"0\" x2=\"%.03f\" y2=\"%.03f\" />\n" + " <text class=\"sec\" x=\"%.03f\" y=\"%.03f\" >%.01fs</text>\n", + SCALE_X * i, + SCALE_X * i, + SCALE_Y * height, + SCALE_X * i, + -5.0, + 0.000001 * i); + else if (i % 1000000 == 0) + svg(" <line class=\"sec1\" x1=\"%.03f\" y1=\"0\" x2=\"%.03f\" y2=\"%.03f\" />\n" + " <text class=\"sec\" x=\"%.03f\" y=\"%.03f\" >%.01fs</text>\n", + SCALE_X * i, + SCALE_X * i, + SCALE_Y * height, + SCALE_X * i, + -5.0, + 0.000001 * i); + else + svg(" <line class=\"sec01\" x1=\"%.03f\" y1=\"0\" x2=\"%.03f\" y2=\"%.03f\" />\n", + SCALE_X * i, + SCALE_X * i, + SCALE_Y * height); + } +} + +static void plot_tooltip(const UnitTimes *ut) { + assert(ut); + assert(ut->name); + + svg("%s:\n", ut->name); + + UnitDependency i; + VA_ARGS_FOREACH(i, UNIT_AFTER, UNIT_BEFORE, UNIT_REQUIRES, UNIT_REQUISITE, UNIT_WANTS, UNIT_CONFLICTS, UNIT_UPHOLDS) + if (!strv_isempty(ut->deps[i])) { + svg("\n%s:\n", unit_dependency_to_string(i)); + STRV_FOREACH(s, ut->deps[i]) + svg(" %s\n", *s); + } +} + +static int plot_unit_times(UnitTimes *u, double width, int y) { + bool b; + + if (!u->name) + return 0; + + svg("<g>\n"); + svg("<title>"); + plot_tooltip(u); + svg("</title>\n"); + svg_bar("activating", u->activating, u->activated, y); + svg_bar("active", u->activated, u->deactivating, y); + svg_bar("deactivating", u->deactivating, u->deactivated, y); + + /* place the text on the left if we have passed the half of the svg width */ + b = u->activating * SCALE_X < width / 2; + if (u->time) + svg_text(b, u->activating, y, "%s (%s)", + u->name, FORMAT_TIMESPAN(u->time, USEC_PER_MSEC)); + else + svg_text(b, u->activating, y, "%s", u->name); + svg("</g>\n"); + + return 1; +} + +static void limit_times_to_boot(const BootTimes *boot, UnitTimes *u) { + if (u->deactivated > u->activating && u->deactivated <= boot->finish_time && u->activated == 0 + && u->deactivating == 0) + u->activated = u->deactivating = u->deactivated; + if (u->activated < u->activating || u->activated > boot->finish_time) + u->activated = boot->finish_time; + if (u->deactivating < u->activated || u->deactivating > boot->finish_time) + u->deactivating = boot->finish_time; + if (u->deactivated < u->deactivating || u->deactivated > boot->finish_time) + u->deactivated = boot->finish_time; +} + +static int produce_plot_as_svg( + UnitTimes *times, + const HostInfo *host, + const BootTimes *boot, + const char *pretty_times) { + int m = 1, y = 0; + UnitTimes *u; + double width; + + width = SCALE_X * (boot->firmware_time + boot->finish_time); + if (width < 800.0) + width = 800.0; + + if (boot->firmware_time > boot->loader_time) + m++; + if (timestamp_is_set(boot->loader_time)) { + m++; + if (width < 1000.0) + width = 1000.0; + } + if (timestamp_is_set(boot->initrd_time)) + m++; + if (timestamp_is_set(boot->kernel_done_time)) + m++; + + for (u = times; u->has_data; u++) { + double text_start, text_width; + + if (u->activating > boot->finish_time) { + unit_times_clear(u); + continue; + } + + /* If the text cannot fit on the left side then + * increase the svg width so it fits on the right. + * TODO: calculate the text width more accurately */ + text_width = 8.0 * strlen(u->name); + text_start = (boot->firmware_time + u->activating) * SCALE_X; + if (text_width > text_start && text_width + text_start > width) + width = text_width + text_start; + + limit_times_to_boot(boot, u); + + m++; + } + + svg("<?xml version=\"1.0\" standalone=\"no\"?>\n" + "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" " + "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n"); + + svg("<svg width=\"%.0fpx\" height=\"%.0fpx\" version=\"1.1\" " + "xmlns=\"http://www.w3.org/2000/svg\">\n\n", + 80.0 + width, 150.0 + (m * SCALE_Y) + + 5 * SCALE_Y /* legend */); + + /* write some basic info as a comment, including some help */ + svg("<!-- This file is a systemd-analyze SVG file. It is best rendered in a -->\n" + "<!-- browser such as Chrome, Chromium or Firefox. Other applications -->\n" + "<!-- that render these files properly but much slower are ImageMagick, -->\n" + "<!-- gimp, inkscape, etc. To display the files on your system, just -->\n" + "<!-- point your browser to this file. -->\n\n" + "<!-- This plot was generated by systemd-analyze version %-16.16s -->\n\n", GIT_VERSION); + + /* style sheet */ + svg("<defs>\n <style type=\"text/css\">\n <![CDATA[\n" + " rect { stroke-width: 1; stroke-opacity: 0; }\n" + " rect.background { fill: rgb(255,255,255); }\n" + " rect.activating { fill: rgb(255,0,0); fill-opacity: 0.7; }\n" + " rect.active { fill: rgb(200,150,150); fill-opacity: 0.7; }\n" + " rect.deactivating { fill: rgb(150,100,100); fill-opacity: 0.7; }\n" + " rect.kernel { fill: rgb(150,150,150); fill-opacity: 0.7; }\n" + " rect.initrd { fill: rgb(150,150,150); fill-opacity: 0.7; }\n" + " rect.firmware { fill: rgb(150,150,150); fill-opacity: 0.7; }\n" + " rect.loader { fill: rgb(150,150,150); fill-opacity: 0.7; }\n" + " rect.userspace { fill: rgb(150,150,150); fill-opacity: 0.7; }\n" + " rect.security { fill: rgb(144,238,144); fill-opacity: 0.7; }\n" + " rect.generators { fill: rgb(102,204,255); fill-opacity: 0.7; }\n" + " rect.unitsload { fill: rgb( 82,184,255); fill-opacity: 0.7; }\n" + " rect.box { fill: rgb(240,240,240); stroke: rgb(192,192,192); }\n" + " line { stroke: rgb(64,64,64); stroke-width: 1; }\n" + "// line.sec1 { }\n" + " line.sec5 { stroke-width: 2; }\n" + " line.sec01 { stroke: rgb(224,224,224); stroke-width: 1; }\n" + " text { font-family: Verdana, Helvetica; font-size: 14px; }\n" + " text.left { font-family: Verdana, Helvetica; font-size: 14px; text-anchor: start; }\n" + " text.right { font-family: Verdana, Helvetica; font-size: 14px; text-anchor: end; }\n" + " text.sec { font-size: 10px; }\n" + " ]]>\n </style>\n</defs>\n\n"); + + svg("<rect class=\"background\" width=\"100%%\" height=\"100%%\" />\n"); + svg("<text x=\"20\" y=\"50\">%s</text>", pretty_times); + if (host) + svg("<text x=\"20\" y=\"30\">%s %s (%s %s %s) %s %s</text>", + os_release_pretty_name(host->os_pretty_name, NULL), + strempty(host->hostname), + strempty(host->kernel_name), + strempty(host->kernel_release), + strempty(host->kernel_version), + strempty(host->architecture), + strempty(host->virtualization)); + + svg("<g transform=\"translate(%.3f,100)\">\n", 20.0 + (SCALE_X * boot->firmware_time)); + svg_graph_box(m, -(double) boot->firmware_time, boot->finish_time); + + if (timestamp_is_set(boot->firmware_time)) { + svg_bar("firmware", -(double) boot->firmware_time, -(double) boot->loader_time, y); + svg_text(true, -(double) boot->firmware_time, y, "firmware"); + y++; + } + if (timestamp_is_set(boot->loader_time)) { + svg_bar("loader", -(double) boot->loader_time, 0, y); + svg_text(true, -(double) boot->loader_time, y, "loader"); + y++; + } + if (timestamp_is_set(boot->kernel_done_time)) { + svg_bar("kernel", 0, boot->kernel_done_time, y); + svg_text(true, 0, y, "kernel"); + y++; + } + if (timestamp_is_set(boot->initrd_time)) { + svg_bar("initrd", boot->initrd_time, boot->userspace_time, y); + if (boot->initrd_security_start_time < boot->initrd_security_finish_time) + svg_bar("security", boot->initrd_security_start_time, boot->initrd_security_finish_time, y); + if (boot->initrd_generators_start_time < boot->initrd_generators_finish_time) + svg_bar("generators", boot->initrd_generators_start_time, boot->initrd_generators_finish_time, y); + if (boot->initrd_unitsload_start_time < boot->initrd_unitsload_finish_time) + svg_bar("unitsload", boot->initrd_unitsload_start_time, boot->initrd_unitsload_finish_time, y); + svg_text(true, boot->initrd_time, y, "initrd"); + y++; + } + + for (u = times; u->has_data; u++) { + if (u->activating >= boot->userspace_time) + break; + + y += plot_unit_times(u, width, y); + } + + svg_bar("active", boot->userspace_time, boot->finish_time, y); + if (timestamp_is_set(boot->security_start_time)) + svg_bar("security", boot->security_start_time, boot->security_finish_time, y); + svg_bar("generators", boot->generators_start_time, boot->generators_finish_time, y); + svg_bar("unitsload", boot->unitsload_start_time, boot->unitsload_finish_time, y); + svg_text(true, boot->userspace_time, y, "systemd"); + y++; + + for (; u->has_data; u++) + y += plot_unit_times(u, width, y); + + svg("</g>\n"); + + /* Legend */ + svg("<g transform=\"translate(20,100)\">\n"); + y++; + svg_bar("activating", 0, 300000, y); + svg_text(true, 400000, y, "Activating"); + y++; + svg_bar("active", 0, 300000, y); + svg_text(true, 400000, y, "Active"); + y++; + svg_bar("deactivating", 0, 300000, y); + svg_text(true, 400000, y, "Deactivating"); + y++; + if (timestamp_is_set(boot->security_start_time)) { + svg_bar("security", 0, 300000, y); + svg_text(true, 400000, y, "Setting up security module"); + y++; + } + svg_bar("generators", 0, 300000, y); + svg_text(true, 400000, y, "Generators"); + y++; + svg_bar("unitsload", 0, 300000, y); + svg_text(true, 400000, y, "Loading unit files"); + y++; + + svg("</g>\n\n"); + + svg("</svg>\n"); + + return 0; +} + +static int show_table(Table *table, const char *word) { + int r; + + assert(table); + assert(word); + + if (table_get_rows(table) > 1) { + table_set_header(table, arg_legend); + + if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) + r = table_print_json(table, NULL, arg_json_format_flags | JSON_FORMAT_COLOR_AUTO); + else + r = table_print(table, NULL); + if (r < 0) + return table_log_print_error(r); + } + + if (arg_legend) { + if (table_get_rows(table) > 1) + printf("\n%zu %s listed.\n", table_get_rows(table) - 1, word); + else + printf("No %s.\n", word); + } + + return 0; +} + +static int produce_plot_as_text(UnitTimes *times, const BootTimes *boot) { + _cleanup_(table_unrefp) Table *table = NULL; + int r; + + table = table_new("name", "activated", "activating", "time", "deactivated", "deactivating"); + if (!table) + return log_oom(); + + for (; times->has_data; times++) { + limit_times_to_boot(boot, times); + + r = table_add_many( + table, + TABLE_STRING, times->name, + TABLE_TIMESPAN_MSEC, times->activated, + TABLE_TIMESPAN_MSEC, times->activating, + TABLE_TIMESPAN_MSEC, times->time, + TABLE_TIMESPAN_MSEC, times->deactivated, + TABLE_TIMESPAN_MSEC, times->deactivating); + if (r < 0) + return table_log_add_error(r); + } + + return show_table(table, "Units"); +} + +int verb_plot(int argc, char *argv[], void *userdata) { + _cleanup_(free_host_infop) HostInfo *host = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(unit_times_free_arrayp) UnitTimes *times = NULL; + _cleanup_free_ char *pretty_times = NULL; + bool use_full_bus = arg_runtime_scope == RUNTIME_SCOPE_SYSTEM; + BootTimes *boot; + int n, r; + + r = acquire_bus(&bus, &use_full_bus); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + + n = acquire_boot_times(bus, /* require_finished = */ true, &boot); + if (n < 0) + return n; + + n = pretty_boot_time(bus, &pretty_times); + if (n < 0) + return n; + + if (use_full_bus || arg_runtime_scope != RUNTIME_SCOPE_SYSTEM) { + n = acquire_host_info(bus, &host); + if (n < 0) + return n; + } + + n = acquire_time_data(bus, /* require_finished = */ true, ×); + if (n <= 0) + return n; + + typesafe_qsort(times, n, compare_unit_start); + + if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF) || arg_table) + r = produce_plot_as_text(times, boot); + else + r = produce_plot_as_svg(times, host, boot, pretty_times); + if (r < 0) + return r; + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-plot.h b/src/analyze/analyze-plot.h new file mode 100644 index 0000000..eb2e398 --- /dev/null +++ b/src/analyze/analyze-plot.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_plot(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-security.c b/src/analyze/analyze-security.c new file mode 100644 index 0000000..5f1b5e6 --- /dev/null +++ b/src/analyze/analyze-security.c @@ -0,0 +1,2956 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <sys/utsname.h> + +#include "af-list.h" +#include "analyze.h" +#include "analyze-security.h" +#include "analyze-verify.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "bus-map-properties.h" +#include "bus-unit-util.h" +#include "bus-util.h" +#include "copy.h" +#include "env-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-table.h" +#include "in-addr-prefix-util.h" +#include "locale-util.h" +#include "macro.h" +#include "manager.h" +#include "missing_capability.h" +#include "missing_sched.h" +#include "mkdir.h" +#include "nulstr-util.h" +#include "parse-util.h" +#include "path-util.h" +#include "pretty-print.h" +#include "seccomp-util.h" +#include "service.h" +#include "set.h" +#include "stdio-util.h" +#include "strv.h" +#include "terminal-util.h" +#include "unit-def.h" +#include "unit-name.h" +#include "unit-serialize.h" + +typedef struct SecurityInfo { + char *id; + char *type; + char *load_state; + char *fragment_path; + bool default_dependencies; + + uint64_t ambient_capabilities; + uint64_t capability_bounding_set; + + char *user; + char **supplementary_groups; + bool dynamic_user; + + bool ip_address_deny_all; + bool ip_address_allow_localhost; + bool ip_address_allow_other; + + bool ip_filters_custom_ingress; + bool ip_filters_custom_egress; + + char *keyring_mode; + char *protect_proc; + char *proc_subset; + bool lock_personality; + bool memory_deny_write_execute; + bool no_new_privileges; + char *notify_access; + bool protect_hostname; + + bool private_devices; + bool private_mounts; + bool private_network; + bool private_tmp; + bool private_users; + + bool protect_control_groups; + bool protect_kernel_modules; + bool protect_kernel_tunables; + bool protect_kernel_logs; + bool protect_clock; + + char *protect_home; + char *protect_system; + + bool remove_ipc; + + bool restrict_address_family_inet; + bool restrict_address_family_unix; + bool restrict_address_family_netlink; + bool restrict_address_family_packet; + bool restrict_address_family_other; + + unsigned long long restrict_namespaces; + bool restrict_realtime; + bool restrict_suid_sgid; + + char *root_directory; + char *root_image; + + bool delegate; + char *device_policy; + char **device_allow; + + Set *system_call_architectures; + + bool system_call_filter_allow_list; + Set *system_call_filter; + + mode_t _umask; +} SecurityInfo; + +struct security_assessor { + const char *id; + const char *json_field; + const char *description_good; + const char *description_bad; + const char *description_na; + const char *url; + uint64_t weight; + uint64_t range; + int (*assess)( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description); + size_t offset; + uint64_t parameter; + bool default_dependencies_only; +}; + +static SecurityInfo *security_info_new(void) { + SecurityInfo *info = new(SecurityInfo, 1); + if (!info) + return NULL; + + *info = (SecurityInfo) { + .default_dependencies = true, + .capability_bounding_set = UINT64_MAX, + .restrict_namespaces = UINT64_MAX, + ._umask = 0002, + }; + + return info; +} + +static SecurityInfo *security_info_free(SecurityInfo *i) { + if (!i) + return NULL; + + free(i->id); + free(i->type); + free(i->load_state); + free(i->fragment_path); + + free(i->user); + + free(i->protect_home); + free(i->protect_system); + + free(i->root_directory); + free(i->root_image); + + free(i->keyring_mode); + free(i->protect_proc); + free(i->proc_subset); + free(i->notify_access); + + free(i->device_policy); + strv_free(i->device_allow); + + strv_free(i->supplementary_groups); + set_free(i->system_call_architectures); + set_free(i->system_call_filter); + + return mfree(i); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(SecurityInfo*, security_info_free); + +static bool security_info_runs_privileged(const SecurityInfo *i) { + assert(i); + + if (STRPTR_IN_SET(i->user, "0", "root")) + return true; + + if (i->dynamic_user) + return false; + + return isempty(i->user); +} + +static int assess_bool( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + const bool *b = ASSERT_PTR(data); + + assert(ret_badness); + assert(ret_description); + + *ret_badness = a->parameter ? *b : !*b; + *ret_description = NULL; + + return 0; +} + +static int assess_user( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + _cleanup_free_ char *d = NULL; + uint64_t b; + + assert(ret_badness); + assert(ret_description); + + if (streq_ptr(info->user, NOBODY_USER_NAME)) { + d = strdup("Service runs under as '" NOBODY_USER_NAME "' user, which should not be used for services"); + b = 9; + } else if (info->dynamic_user && !STR_IN_SET(info->user, "0", "root")) { + d = strdup("Service runs under a transient non-root user identity"); + b = 0; + } else if (info->user && !STR_IN_SET(info->user, "0", "root", "")) { + d = strdup("Service runs under a static non-root user identity"); + b = 0; + } else { + *ret_badness = 10; + *ret_description = NULL; + return 0; + } + + if (!d) + return log_oom(); + + *ret_badness = b; + *ret_description = TAKE_PTR(d); + + return 0; +} + +static int assess_protect_home( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + const char *description; + uint64_t badness; + char *copy; + int r; + + assert(ret_badness); + assert(ret_description); + + badness = 10; + description = "Service has full access to home directories"; + + r = parse_boolean(info->protect_home); + if (r < 0) { + if (streq_ptr(info->protect_home, "read-only")) { + badness = 5; + description = "Service has read-only access to home directories"; + } else if (streq_ptr(info->protect_home, "tmpfs")) { + badness = 1; + description = "Service has access to fake empty home directories"; + } + } else if (r > 0) { + badness = 0; + description = "Service has no access to home directories"; + } + + copy = strdup(description); + if (!copy) + return log_oom(); + + *ret_badness = badness; + *ret_description = copy; + + return 0; +} + +static int assess_protect_system( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + const char *description; + uint64_t badness; + char *copy; + int r; + + assert(ret_badness); + assert(ret_description); + + badness = 10; + description = "Service has full access to the OS file hierarchy"; + + r = parse_boolean(info->protect_system); + if (r < 0) { + if (streq_ptr(info->protect_system, "full")) { + badness = 3; + description = "Service has very limited write access to the OS file hierarchy"; + } else if (streq_ptr(info->protect_system, "strict")) { + badness = 0; + description = "Service has strict read-only access to the OS file hierarchy"; + } + } else if (r > 0) { + badness = 5; + description = "Service has limited write access to the OS file hierarchy"; + } + + copy = strdup(description); + if (!copy) + return log_oom(); + + *ret_badness = badness; + *ret_description = copy; + + return 0; +} + +static int assess_root_directory( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = + empty_or_root(info->root_directory) && + empty_or_root(info->root_image); + *ret_description = NULL; + + return 0; +} + +static int assess_capability_bounding_set( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = !!(info->capability_bounding_set & a->parameter); + *ret_description = NULL; + + return 0; +} + +static int assess_umask( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + char *copy = NULL; + const char *d; + uint64_t b; + + assert(ret_badness); + assert(ret_description); + + if (!FLAGS_SET(info->_umask, 0002)) { + d = "Files created by service are world-writable by default"; + b = 10; + } else if (!FLAGS_SET(info->_umask, 0004)) { + d = "Files created by service are world-readable by default"; + b = 5; + } else if (!FLAGS_SET(info->_umask, 0020)) { + d = "Files created by service are group-writable by default"; + b = 2; + } else if (!FLAGS_SET(info->_umask, 0040)) { + d = "Files created by service are group-readable by default"; + b = 1; + } else { + d = "Files created by service are accessible only by service's own user by default"; + b = 0; + } + + copy = strdup(d); + if (!copy) + return log_oom(); + + *ret_badness = b; + *ret_description = copy; + + return 0; +} + +static int assess_keyring_mode( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = !streq_ptr(info->keyring_mode, "private"); + *ret_description = NULL; + + return 0; +} + +static int assess_protect_proc( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + if (streq_ptr(info->protect_proc, "noaccess")) + *ret_badness = 1; + else if (STRPTR_IN_SET(info->protect_proc, "invisible", "ptraceable")) + *ret_badness = 0; + else + *ret_badness = 3; + + *ret_description = NULL; + + return 0; +} + +static int assess_proc_subset( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = !streq_ptr(info->proc_subset, "pid"); + *ret_description = NULL; + + return 0; +} + +static int assess_notify_access( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = streq_ptr(info->notify_access, "all"); + *ret_description = NULL; + + return 0; +} + +static int assess_remove_ipc( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + if (security_info_runs_privileged(info)) + *ret_badness = UINT64_MAX; + else + *ret_badness = !info->remove_ipc; + + *ret_description = NULL; + return 0; +} + +static int assess_supplementary_groups( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + if (security_info_runs_privileged(info)) + *ret_badness = UINT64_MAX; + else + *ret_badness = !strv_isempty(info->supplementary_groups); + + *ret_description = NULL; + return 0; +} + +static int assess_restrict_namespaces( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = !!(info->restrict_namespaces & a->parameter); + *ret_description = NULL; + + return 0; +} + +#if HAVE_SECCOMP + +static int assess_system_call_architectures( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + char *d; + uint64_t b; + + assert(ret_badness); + assert(ret_description); + + if (set_isempty(info->system_call_architectures)) { + b = 10; + d = strdup("Service may execute system calls with all ABIs"); + } else if (set_contains(info->system_call_architectures, "native") && + set_size(info->system_call_architectures) == 1) { + b = 0; + d = strdup("Service may execute system calls only with native ABI"); + } else { + b = 8; + d = strdup("Service may execute system calls with multiple ABIs"); + } + + if (!d) + return log_oom(); + + *ret_badness = b; + *ret_description = d; + + return 0; +} + +static bool syscall_names_in_filter(Set *s, bool allow_list, const SyscallFilterSet *f, const char **ret_offending_syscall) { + NULSTR_FOREACH(syscall, f->value) { + if (syscall[0] == '@') { + const SyscallFilterSet *g; + + assert_se(g = syscall_filter_set_find(syscall)); + if (syscall_names_in_filter(s, allow_list, g, ret_offending_syscall)) + return true; /* bad! */ + + continue; + } + + /* Let's see if the system call actually exists on this platform, before complaining */ + if (seccomp_syscall_resolve_name(syscall) < 0) + continue; + + if (set_contains(s, syscall) == allow_list) { + log_debug("Offending syscall filter item: %s", syscall); + if (ret_offending_syscall) + *ret_offending_syscall = syscall; + return true; /* bad! */ + } + } + + *ret_offending_syscall = NULL; + return false; +} + +static int assess_system_call_filter( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(a); + assert(info); + assert(ret_badness); + assert(ret_description); + + assert(a->parameter < _SYSCALL_FILTER_SET_MAX); + const SyscallFilterSet *f = syscall_filter_sets + a->parameter; + + _cleanup_free_ char *d = NULL; + uint64_t b; + int r; + + if (!info->system_call_filter_allow_list && set_isempty(info->system_call_filter)) { + r = free_and_strdup(&d, "Service does not filter system calls"); + b = 10; + } else { + bool bad; + const char *offender = NULL; + + log_debug("Analyzing system call filter, checking against: %s", f->name); + bad = syscall_names_in_filter(info->system_call_filter, info->system_call_filter_allow_list, f, &offender); + log_debug("Result: %s", bad ? "bad" : "good"); + + if (info->system_call_filter_allow_list) { + if (bad) { + r = asprintf(&d, "System call allow list defined for service, and %s is included " + "(e.g. %s is allowed)", + f->name, offender); + b = 9; + } else { + r = asprintf(&d, "System call allow list defined for service, and %s is not included", + f->name); + b = 0; + } + } else { + if (bad) { + r = asprintf(&d, "System call deny list defined for service, and %s is not included " + "(e.g. %s is allowed)", + f->name, offender); + b = 10; + } else { + r = asprintf(&d, "System call deny list defined for service, and %s is included", + f->name); + b = 0; + } + } + } + if (r < 0) + return log_oom(); + + *ret_badness = b; + *ret_description = TAKE_PTR(d); + + return 0; +} + +#endif + +static int assess_ip_address_allow( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + char *d = NULL; + uint64_t b; + + assert(info); + assert(ret_badness); + assert(ret_description); + + if (info->ip_filters_custom_ingress || info->ip_filters_custom_egress) { + d = strdup("Service defines custom ingress/egress IP filters with BPF programs"); + b = 0; + } else if (!info->ip_address_deny_all) { + d = strdup("Service does not define an IP address allow list"); + b = 10; + } else if (info->ip_address_allow_other) { + d = strdup("Service defines IP address allow list with non-localhost entries"); + b = 5; + } else if (info->ip_address_allow_localhost) { + d = strdup("Service defines IP address allow list with only localhost entries"); + b = 2; + } else { + d = strdup("Service blocks all IP address ranges"); + b = 0; + } + + if (!d) + return log_oom(); + + *ret_badness = b; + *ret_description = d; + + return 0; +} + +static int assess_device_allow( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + char *d = NULL; + uint64_t b; + + assert(info); + assert(ret_badness); + assert(ret_description); + + if (STRPTR_IN_SET(info->device_policy, "strict", "closed")) { + + if (!strv_isempty(info->device_allow)) { + _cleanup_free_ char *join = NULL; + + join = strv_join(info->device_allow, " "); + if (!join) + return log_oom(); + + d = strjoin("Service has a device ACL with some special devices: ", join); + b = 5; + } else { + d = strdup("Service has a minimal device ACL"); + b = 0; + } + } else { + d = strdup("Service has no device ACL"); + b = 10; + } + + if (!d) + return log_oom(); + + *ret_badness = b; + *ret_description = d; + + return 0; +} + +static int assess_ambient_capabilities( + const struct security_assessor *a, + const SecurityInfo *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = info->ambient_capabilities != 0; + *ret_description = NULL; + + return 0; +} + +static const struct security_assessor security_assessor_table[] = { + { + .id = "User=/DynamicUser=", + .json_field = "UserOrDynamicUser", + .description_bad = "Service runs as root user", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#User=", + .weight = 2000, + .range = 10, + .assess = assess_user, + }, + { + .id = "SupplementaryGroups=", + .json_field = "SupplementaryGroups", + .description_good = "Service has no supplementary groups", + .description_bad = "Service runs with supplementary groups", + .description_na = "Service runs as root, option does not matter", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SupplementaryGroups=", + .weight = 200, + .range = 1, + .assess = assess_supplementary_groups, + }, + { + .id = "PrivateDevices=", + .json_field = "PrivateDevices", + .description_good = "Service has no access to hardware devices", + .description_bad = "Service potentially has access to hardware devices", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateDevices=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, private_devices), + }, + { + .id = "PrivateMounts=", + .json_field = "PrivateMounts", + .description_good = "Service cannot install system mounts", + .description_bad = "Service may install system mounts", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateMounts=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, private_mounts), + }, + { + .id = "PrivateNetwork=", + .json_field = "PrivateNetwork", + .description_good = "Service has no access to the host's network", + .description_bad = "Service has access to the host's network", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateNetwork=", + .weight = 2500, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, private_network), + }, + { + .id = "PrivateTmp=", + .json_field = "PrivateTmp", + .description_good = "Service has no access to other software's temporary files", + .description_bad = "Service has access to other software's temporary files", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateTmp=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, private_tmp), + .default_dependencies_only = true, + }, + { + .id = "PrivateUsers=", + .json_field = "PrivateUsers", + .description_good = "Service does not have access to other users", + .description_bad = "Service has access to other users", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateUsers=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, private_users), + }, + { + .id = "ProtectControlGroups=", + .json_field = "ProtectControlGroups", + .description_good = "Service cannot modify the control group file system", + .description_bad = "Service may modify the control group file system", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectControlGroups=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, protect_control_groups), + }, + { + .id = "ProtectKernelModules=", + .json_field = "ProtectKernelModules", + .description_good = "Service cannot load or read kernel modules", + .description_bad = "Service may load or read kernel modules", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelModules=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, protect_kernel_modules), + }, + { + .id = "ProtectKernelTunables=", + .json_field = "ProtectKernelTunables", + .description_good = "Service cannot alter kernel tunables (/proc/sys, …)", + .description_bad = "Service may alter kernel tunables", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelTunables=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, protect_kernel_tunables), + }, + { + .id = "ProtectKernelLogs=", + .json_field = "ProtectKernelLogs", + .description_good = "Service cannot read from or write to the kernel log ring buffer", + .description_bad = "Service may read from or write to the kernel log ring buffer", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelLogs=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, protect_kernel_logs), + }, + { + .id = "ProtectClock=", + .json_field = "ProtectClock", + .description_good = "Service cannot write to the hardware clock or system clock", + .description_bad = "Service may write to the hardware clock or system clock", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectClock=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, protect_clock), + }, + { + .id = "ProtectHome=", + .json_field = "ProtectHome", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome=", + .weight = 1000, + .range = 10, + .assess = assess_protect_home, + .default_dependencies_only = true, + }, + { + .id = "ProtectHostname=", + .json_field = "ProtectHostname", + .description_good = "Service cannot change system host/domainname", + .description_bad = "Service may change system host/domainname", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHostname=", + .weight = 50, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, protect_hostname), + }, + { + .id = "ProtectSystem=", + .json_field = "ProtectSystem", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectSystem=", + .weight = 1000, + .range = 10, + .assess = assess_protect_system, + .default_dependencies_only = true, + }, + { + .id = "RootDirectory=/RootImage=", + .json_field = "RootDirectoryOrRootImage", + .description_good = "Service has its own root directory/image", + .description_bad = "Service runs within the host's root directory", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RootDirectory=", + .weight = 200, + .range = 1, + .assess = assess_root_directory, + .default_dependencies_only = true, + }, + { + .id = "LockPersonality=", + .json_field = "LockPersonality", + .description_good = "Service cannot change ABI personality", + .description_bad = "Service may change ABI personality", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#LockPersonality=", + .weight = 100, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, lock_personality), + }, + { + .id = "MemoryDenyWriteExecute=", + .json_field = "MemoryDenyWriteExecute", + .description_good = "Service cannot create writable executable memory mappings", + .description_bad = "Service may create writable executable memory mappings", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#MemoryDenyWriteExecute=", + .weight = 100, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, memory_deny_write_execute), + }, + { + .id = "NoNewPrivileges=", + .json_field = "NoNewPrivileges", + .description_good = "Service processes cannot acquire new privileges", + .description_bad = "Service processes may acquire new privileges", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#NoNewPrivileges=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, no_new_privileges), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_ADMIN", + .json_field = "CapabilityBoundingSet_CAP_SYS_ADMIN", + .description_good = "Service has no administrator privileges", + .description_bad = "Service has administrator privileges", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = UINT64_C(1) << CAP_SYS_ADMIN, + }, + { + .id = "CapabilityBoundingSet=~CAP_SET(UID|GID|PCAP)", + .json_field = "CapabilityBoundingSet_CAP_SET_UID_GID_PCAP", + .description_good = "Service cannot change UID/GID identities/capabilities", + .description_bad = "Service may change UID/GID identities/capabilities", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SETUID)| + (UINT64_C(1) << CAP_SETGID)| + (UINT64_C(1) << CAP_SETPCAP), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_PTRACE", + .json_field = "CapabilityBoundingSet_CAP_SYS_PTRACE", + .description_good = "Service has no ptrace() debugging abilities", + .description_bad = "Service has ptrace() debugging abilities", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_PTRACE), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_TIME", + .json_field = "CapabilityBoundingSet_CAP_SYS_TIME", + .description_good = "Service processes cannot change the system clock", + .description_bad = "Service processes may change the system clock", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1000, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = UINT64_C(1) << CAP_SYS_TIME, + }, + { + .id = "CapabilityBoundingSet=~CAP_NET_ADMIN", + .json_field = "CapabilityBoundingSet_CAP_NET_ADMIN", + .description_good = "Service has no network configuration privileges", + .description_bad = "Service has network configuration privileges", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1000, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_NET_ADMIN), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_RAWIO", + .json_field = "CapabilityBoundingSet_CAP_SYS_RAWIO", + .description_good = "Service has no raw I/O access", + .description_bad = "Service has raw I/O access", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1000, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_RAWIO), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_MODULE", + .json_field = "CapabilityBoundingSet_CAP_SYS_MODULE", + .description_good = "Service cannot load kernel modules", + .description_bad = "Service may load kernel modules", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1000, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_MODULE), + }, + { + .id = "CapabilityBoundingSet=~CAP_AUDIT_*", + .json_field = "CapabilityBoundingSet_CAP_AUDIT", + .description_good = "Service has no audit subsystem access", + .description_bad = "Service has audit subsystem access", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_AUDIT_CONTROL) | + (UINT64_C(1) << CAP_AUDIT_READ) | + (UINT64_C(1) << CAP_AUDIT_WRITE), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYSLOG", + .json_field = "CapabilityBoundingSet_CAP_SYSLOG", + .description_good = "Service has no access to kernel logging", + .description_bad = "Service has access to kernel logging", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYSLOG), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_(NICE|RESOURCE)", + .json_field = "CapabilityBoundingSet_CAP_SYS_NICE_RESOURCE", + .description_good = "Service has no privileges to change resource use parameters", + .description_bad = "Service has privileges to change resource use parameters", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_NICE) | + (UINT64_C(1) << CAP_SYS_RESOURCE), + }, + { + .id = "CapabilityBoundingSet=~CAP_MKNOD", + .json_field = "CapabilityBoundingSet_CAP_MKNOD", + .description_good = "Service cannot create device nodes", + .description_bad = "Service may create device nodes", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_MKNOD), + }, + { + .id = "CapabilityBoundingSet=~CAP_(CHOWN|FSETID|SETFCAP)", + .json_field = "CapabilityBoundingSet_CAP_CHOWN_FSETID_SETFCAP", + .description_good = "Service cannot change file ownership/access mode/capabilities", + .description_bad = "Service may change file ownership/access mode/capabilities unrestricted", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1000, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_CHOWN) | + (UINT64_C(1) << CAP_FSETID) | + (UINT64_C(1) << CAP_SETFCAP), + }, + { + .id = "CapabilityBoundingSet=~CAP_(DAC_*|FOWNER|IPC_OWNER)", + .json_field = "CapabilityBoundingSet_CAP_DAC_FOWNER_IPC_OWNER", + .description_good = "Service cannot override UNIX file/IPC permission checks", + .description_bad = "Service may override UNIX file/IPC permission checks", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1000, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_DAC_OVERRIDE) | + (UINT64_C(1) << CAP_DAC_READ_SEARCH) | + (UINT64_C(1) << CAP_FOWNER) | + (UINT64_C(1) << CAP_IPC_OWNER), + }, + { + .id = "CapabilityBoundingSet=~CAP_KILL", + .json_field = "CapabilityBoundingSet_CAP_KILL", + .description_good = "Service cannot send UNIX signals to arbitrary processes", + .description_bad = "Service may send UNIX signals to arbitrary processes", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_KILL), + }, + { + .id = "CapabilityBoundingSet=~CAP_NET_(BIND_SERVICE|BROADCAST|RAW)", + .json_field = "CapabilityBoundingSet_CAP_NET_BIND_SERVICE_BROADCAST_RAW)", + .description_good = "Service has no elevated networking privileges", + .description_bad = "Service has elevated networking privileges", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_NET_BIND_SERVICE) | + (UINT64_C(1) << CAP_NET_BROADCAST) | + (UINT64_C(1) << CAP_NET_RAW), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_BOOT", + .json_field = "CapabilityBoundingSet_CAP_SYS_BOOT", + .description_good = "Service cannot issue reboot()", + .description_bad = "Service may issue reboot()", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 100, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_BOOT), + }, + { + .id = "CapabilityBoundingSet=~CAP_MAC_*", + .json_field = "CapabilityBoundingSet_CAP_MAC", + .description_good = "Service cannot adjust SMACK MAC", + .description_bad = "Service may adjust SMACK MAC", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 100, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_MAC_ADMIN)| + (UINT64_C(1) << CAP_MAC_OVERRIDE), + }, + { + .id = "CapabilityBoundingSet=~CAP_LINUX_IMMUTABLE", + .json_field = "CapabilityBoundingSet_CAP_LINUX_IMMUTABLE", + .description_good = "Service cannot mark files immutable", + .description_bad = "Service may mark files immutable", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 75, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_LINUX_IMMUTABLE), + }, + { + .id = "CapabilityBoundingSet=~CAP_IPC_LOCK", + .json_field = "CapabilityBoundingSet_CAP_IPC_LOCK", + .description_good = "Service cannot lock memory into RAM", + .description_bad = "Service may lock memory into RAM", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 50, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_IPC_LOCK), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_CHROOT", + .json_field = "CapabilityBoundingSet_CAP_SYS_CHROOT", + .description_good = "Service cannot issue chroot()", + .description_bad = "Service may issue chroot()", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 50, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_CHROOT), + }, + { + .id = "CapabilityBoundingSet=~CAP_BLOCK_SUSPEND", + .json_field = "CapabilityBoundingSet_CAP_BLOCK_SUSPEND", + .description_good = "Service cannot establish wake locks", + .description_bad = "Service may establish wake locks", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 25, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_BLOCK_SUSPEND), + }, + { + .id = "CapabilityBoundingSet=~CAP_WAKE_ALARM", + .json_field = "CapabilityBoundingSet_CAP_WAKE_ALARM", + .description_good = "Service cannot program timers that wake up the system", + .description_bad = "Service may program timers that wake up the system", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 25, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_WAKE_ALARM), + }, + { + .id = "CapabilityBoundingSet=~CAP_LEASE", + .json_field = "CapabilityBoundingSet_CAP_LEASE", + .description_good = "Service cannot create file leases", + .description_bad = "Service may create file leases", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 25, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_LEASE), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG", + .json_field = "CapabilityBoundingSet_CAP_SYS_TTY_CONFIG", + .description_good = "Service cannot issue vhangup()", + .description_bad = "Service may issue vhangup()", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 25, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_TTY_CONFIG), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_PACCT", + .json_field = "CapabilityBoundingSet_CAP_SYS_PACCT", + .description_good = "Service cannot use acct()", + .description_bad = "Service may use acct()", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 25, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_PACCT), + }, + { + .id = "CapabilityBoundingSet=~CAP_BPF", + .json_field = "CapabilityBoundingSet_CAP_BPF", + .description_good = "Service may load BPF programs", + .description_bad = "Service may not load BPF programs", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 25, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_BPF), + }, + { + .id = "UMask=", + .json_field = "UMask", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#UMask=", + .weight = 100, + .range = 10, + .assess = assess_umask, + }, + { + .id = "KeyringMode=", + .json_field = "KeyringMode", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#KeyringMode=", + .description_good = "Service doesn't share key material with other services", + .description_bad = "Service shares key material with other service", + .weight = 1000, + .range = 1, + .assess = assess_keyring_mode, + }, + { + .id = "ProtectProc=", + .json_field = "ProtectProc", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectProc=", + .description_good = "Service has restricted access to process tree (/proc hidepid=)", + .description_bad = "Service has full access to process tree (/proc hidepid=)", + .weight = 1000, + .range = 3, + .assess = assess_protect_proc, + }, + { + .id = "ProcSubset=", + .json_field = "ProcSubset", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProcSubset=", + .description_good = "Service has no access to non-process /proc files (/proc subset=)", + .description_bad = "Service has full access to non-process /proc files (/proc subset=)", + .weight = 10, + .range = 1, + .assess = assess_proc_subset, + }, + { + .id = "NotifyAccess=", + .json_field = "NotifyAccess", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#NotifyAccess=", + .description_good = "Service child processes cannot alter service state", + .description_bad = "Service child processes may alter service state", + .weight = 1000, + .range = 1, + .assess = assess_notify_access, + }, + { + .id = "RemoveIPC=", + .json_field = "RemoveIPC", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RemoveIPC=", + .description_good = "Service user cannot leave SysV IPC objects around", + .description_bad = "Service user may leave SysV IPC objects around", + .description_na = "Service runs as root, option does not apply", + .weight = 100, + .range = 1, + .assess = assess_remove_ipc, + .offset = offsetof(SecurityInfo, remove_ipc), + }, + { + .id = "Delegate=", + .json_field = "Delegate", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Delegate=", + .description_good = "Service does not maintain its own delegated control group subtree", + .description_bad = "Service maintains its own delegated control group subtree", + .weight = 100, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, delegate), + .parameter = true, /* invert! */ + }, + { + .id = "RestrictRealtime=", + .json_field = "RestrictRealtime", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictRealtime=", + .description_good = "Service realtime scheduling access is restricted", + .description_bad = "Service may acquire realtime scheduling", + .weight = 500, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, restrict_realtime), + }, + { + .id = "RestrictSUIDSGID=", + .json_field = "RestrictSUIDSGID", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictSUIDSGID=", + .description_good = "SUID/SGID file creation by service is restricted", + .description_bad = "Service may create SUID/SGID files", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, restrict_suid_sgid), + }, + { + .id = "RestrictNamespaces=~user", + .json_field = "RestrictNamespaces_user", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create user namespaces", + .description_bad = "Service may create user namespaces", + .weight = 1500, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWUSER, + }, + { + .id = "RestrictNamespaces=~mnt", + .json_field = "RestrictNamespaces_mnt", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create file system namespaces", + .description_bad = "Service may create file system namespaces", + .weight = 500, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWNS, + }, + { + .id = "RestrictNamespaces=~ipc", + .json_field = "RestrictNamespaces_ipc", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create IPC namespaces", + .description_bad = "Service may create IPC namespaces", + .weight = 500, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWIPC, + }, + { + .id = "RestrictNamespaces=~pid", + .json_field = "RestrictNamespaces_pid", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create process namespaces", + .description_bad = "Service may create process namespaces", + .weight = 500, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWPID, + }, + { + .id = "RestrictNamespaces=~cgroup", + .json_field = "RestrictNamespaces_cgroup", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create cgroup namespaces", + .description_bad = "Service may create cgroup namespaces", + .weight = 500, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWCGROUP, + }, + { + .id = "RestrictNamespaces=~net", + .json_field = "RestrictNamespaces_net", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create network namespaces", + .description_bad = "Service may create network namespaces", + .weight = 500, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWNET, + }, + { + .id = "RestrictNamespaces=~uts", + .json_field = "RestrictNamespaces_uts", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create hostname namespaces", + .description_bad = "Service may create hostname namespaces", + .weight = 100, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWUTS, + }, + { + .id = "RestrictAddressFamilies=~AF_(INET|INET6)", + .json_field = "RestrictAddressFamilies_AF_INET_INET6", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", + .description_good = "Service cannot allocate Internet sockets", + .description_bad = "Service may allocate Internet sockets", + .weight = 1500, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, restrict_address_family_inet), + }, + { + .id = "RestrictAddressFamilies=~AF_UNIX", + .json_field = "RestrictAddressFamilies_AF_UNIX", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", + .description_good = "Service cannot allocate local sockets", + .description_bad = "Service may allocate local sockets", + .weight = 25, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, restrict_address_family_unix), + }, + { + .id = "RestrictAddressFamilies=~AF_NETLINK", + .json_field = "RestrictAddressFamilies_AF_NETLINK", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", + .description_good = "Service cannot allocate netlink sockets", + .description_bad = "Service may allocate netlink sockets", + .weight = 200, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, restrict_address_family_netlink), + }, + { + .id = "RestrictAddressFamilies=~AF_PACKET", + .json_field = "RestrictAddressFamilies_AF_PACKET", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", + .description_good = "Service cannot allocate packet sockets", + .description_bad = "Service may allocate packet sockets", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, restrict_address_family_packet), + }, + { + .id = "RestrictAddressFamilies=~…", + .json_field = "RestrictAddressFamilies_OTHER", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", + .description_good = "Service cannot allocate exotic sockets", + .description_bad = "Service may allocate exotic sockets", + .weight = 1250, + .range = 1, + .assess = assess_bool, + .offset = offsetof(SecurityInfo, restrict_address_family_other), + }, +#if HAVE_SECCOMP + { + .id = "SystemCallArchitectures=", + .json_field = "SystemCallArchitectures", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallArchitectures=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_architectures, + }, + { + .id = "SystemCallFilter=~@swap", + .json_field = "SystemCallFilter_swap", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_SWAP, + }, + { + .id = "SystemCallFilter=~@obsolete", + .json_field = "SystemCallFilter_obsolete", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 250, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_OBSOLETE, + }, + { + .id = "SystemCallFilter=~@clock", + .json_field = "SystemCallFilter_clock", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_CLOCK, + }, + { + .id = "SystemCallFilter=~@cpu-emulation", + .json_field = "SystemCallFilter_cpu_emulation", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 250, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_CPU_EMULATION, + }, + { + .id = "SystemCallFilter=~@debug", + .json_field = "SystemCallFilter_debug", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_DEBUG, + }, + { + .id = "SystemCallFilter=~@mount", + .json_field = "SystemCallFilter_mount", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_MOUNT, + }, + { + .id = "SystemCallFilter=~@module", + .json_field = "SystemCallFilter_module", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_MODULE, + }, + { + .id = "SystemCallFilter=~@raw-io", + .json_field = "SystemCallFilter_raw_io", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_RAW_IO, + }, + { + .id = "SystemCallFilter=~@reboot", + .json_field = "SystemCallFilter_reboot", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_REBOOT, + }, + { + .id = "SystemCallFilter=~@privileged", + .json_field = "SystemCallFilter_privileged", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 700, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_PRIVILEGED, + }, + { + .id = "SystemCallFilter=~@resources", + .json_field = "SystemCallFilter_resources", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 700, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_RESOURCES, + }, +#endif + { + .id = "IPAddressDeny=", + .json_field = "IPAddressDeny", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#IPAddressDeny=", + .weight = 1000, + .range = 10, + .assess = assess_ip_address_allow, + }, + { + .id = "DeviceAllow=", + .json_field = "DeviceAllow", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#DeviceAllow=", + .weight = 1000, + .range = 10, + .assess = assess_device_allow, + }, + { + .id = "AmbientCapabilities=", + .json_field = "AmbientCapabilities", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#AmbientCapabilities=", + .description_good = "Service process does not receive ambient capabilities", + .description_bad = "Service process receives ambient capabilities", + .weight = 500, + .range = 1, + .assess = assess_ambient_capabilities, + }, +}; + +static JsonVariant* security_assessor_find_in_policy(const struct security_assessor *a, JsonVariant *policy, const char *name) { + JsonVariant *item; + assert(a); + + if (!policy) + return NULL; + if (!json_variant_is_object(policy)) { + log_debug("Specified policy is not a JSON object, ignoring."); + return NULL; + } + + item = json_variant_by_key(policy, a->json_field); + if (!item) + return NULL; + if (!json_variant_is_object(item)) { + log_debug("Item for '%s' in policy JSON object is not an object, ignoring.", a->id); + return NULL; + } + + return name ? json_variant_by_key(item, name) : item; +} + +static uint64_t access_weight(const struct security_assessor *a, JsonVariant *policy) { + JsonVariant *val; + + assert(a); + + val = security_assessor_find_in_policy(a, policy, "weight"); + if (val) { + if (json_variant_is_unsigned(val)) + return json_variant_unsigned(val); + log_debug("JSON field 'weight' of policy for %s is not an unsigned integer, ignoring.", a->id); + } + + return a->weight; +} + +static uint64_t access_range(const struct security_assessor *a, JsonVariant *policy) { + JsonVariant *val; + + assert(a); + + val = security_assessor_find_in_policy(a, policy, "range"); + if (val) { + if (json_variant_is_unsigned(val)) + return json_variant_unsigned(val); + log_debug("JSON field 'range' of policy for %s is not an unsigned integer, ignoring.", a->id); + } + + return a->range; +} + +static const char *access_description_na(const struct security_assessor *a, JsonVariant *policy) { + JsonVariant *val; + + assert(a); + + val = security_assessor_find_in_policy(a, policy, "description_na"); + if (val) { + if (json_variant_is_string(val)) + return json_variant_string(val); + log_debug("JSON field 'description_na' of policy for %s is not a string, ignoring.", a->id); + } + + return a->description_na; +} + +static const char *access_description_good(const struct security_assessor *a, JsonVariant *policy) { + JsonVariant *val; + + assert(a); + + val = security_assessor_find_in_policy(a, policy, "description_good"); + if (val) { + if (json_variant_is_string(val)) + return json_variant_string(val); + log_debug("JSON field 'description_good' of policy for %s is not a string, ignoring.", a->id); + } + + return a->description_good; +} + +static const char *access_description_bad(const struct security_assessor *a, JsonVariant *policy) { + JsonVariant *val; + + assert(a); + + val = security_assessor_find_in_policy(a, policy, "description_bad"); + if (val) { + if (json_variant_is_string(val)) + return json_variant_string(val); + log_debug("JSON field 'description_bad' of policy for %s is not a string, ignoring.", a->id); + } + + return a->description_bad; +} + +static int assess(const SecurityInfo *info, + Table *overview_table, + AnalyzeSecurityFlags flags, + unsigned threshold, + JsonVariant *policy, + PagerFlags pager_flags, + JsonFormatFlags json_format_flags) { + + static const struct { + uint64_t exposure; + const char *name; + const char *color; + SpecialGlyph smiley; + } badness_table[] = { + { 100, "DANGEROUS", ANSI_HIGHLIGHT_RED, SPECIAL_GLYPH_DEPRESSED_SMILEY }, + { 90, "UNSAFE", ANSI_HIGHLIGHT_RED, SPECIAL_GLYPH_UNHAPPY_SMILEY }, + { 75, "EXPOSED", ANSI_HIGHLIGHT_YELLOW, SPECIAL_GLYPH_SLIGHTLY_UNHAPPY_SMILEY }, + { 50, "MEDIUM", NULL, SPECIAL_GLYPH_NEUTRAL_SMILEY }, + { 10, "OK", ANSI_HIGHLIGHT_GREEN, SPECIAL_GLYPH_SLIGHTLY_HAPPY_SMILEY }, + { 1, "SAFE", ANSI_HIGHLIGHT_GREEN, SPECIAL_GLYPH_HAPPY_SMILEY }, + { 0, "PERFECT", ANSI_HIGHLIGHT_GREEN, SPECIAL_GLYPH_ECSTATIC_SMILEY }, + }; + + uint64_t badness_sum = 0, weight_sum = 0, exposure; + _cleanup_(table_unrefp) Table *details_table = NULL; + size_t i; + int r; + + if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT)) { + details_table = table_new(" ", "name", "json_field", "description", "weight", "badness", "range", "exposure"); + if (!details_table) + return log_oom(); + + r = table_set_json_field_name(details_table, 0, "set"); + if (r < 0) + return log_error_errno(r, "Failed to set JSON field name of column 0: %m"); + + (void) table_set_sort(details_table, (size_t) 3, (size_t) 1); + (void) table_set_reverse(details_table, 3, true); + + if (getenv_bool("SYSTEMD_ANALYZE_DEBUG") <= 0) + (void) table_set_display(details_table, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 7); + } + + for (i = 0; i < ELEMENTSOF(security_assessor_table); i++) { + const struct security_assessor *a = security_assessor_table + i; + _cleanup_free_ char *d = NULL; + uint64_t badness; + void *data; + uint64_t weight = access_weight(a, policy); + uint64_t range = access_range(a, policy); + + data = (uint8_t *) info + a->offset; + + if (a->default_dependencies_only && !info->default_dependencies) { + badness = UINT64_MAX; + d = strdup("Service runs in special boot phase, option is not appropriate"); + if (!d) + return log_oom(); + } else if (weight == 0) { + badness = UINT64_MAX; + d = strdup("Option excluded by policy, skipping"); + if (!d) + return log_oom(); + } else { + r = a->assess(a, info, data, &badness, &d); + if (r < 0) + return r; + } + + assert(range > 0); + + if (badness != UINT64_MAX) { + assert(badness <= range); + + badness_sum += DIV_ROUND_UP(badness * weight, range); + weight_sum += weight; + } + + if (details_table) { + const char *description, *color = NULL; + int checkmark; + + if (badness == UINT64_MAX) { + checkmark = -1; + description = access_description_na(a, policy); + color = NULL; + } else if (badness == a->range) { + checkmark = 0; + description = access_description_bad(a, policy); + color = ansi_highlight_red(); + } else if (badness == 0) { + checkmark = 1; + description = access_description_good(a, policy); + color = ansi_highlight_green(); + } else { + checkmark = 0; + description = NULL; + color = ansi_highlight_red(); + } + + if (d) + description = d; + + if (checkmark < 0) { + r = table_add_many(details_table, TABLE_EMPTY); + if (r < 0) + return table_log_add_error(r); + } else { + r = table_add_many(details_table, + TABLE_BOOLEAN_CHECKMARK, checkmark > 0, + TABLE_SET_MINIMUM_WIDTH, 1, + TABLE_SET_MAXIMUM_WIDTH, 1, + TABLE_SET_ELLIPSIZE_PERCENT, 0, + TABLE_SET_COLOR, color); + if (r < 0) + return table_log_add_error(r); + } + + r = table_add_many(details_table, + TABLE_STRING, a->id, TABLE_SET_URL, a->url, + TABLE_STRING, a->json_field, + TABLE_STRING, description, + TABLE_UINT64, weight, TABLE_SET_ALIGN_PERCENT, 100, + TABLE_UINT64, badness, TABLE_SET_ALIGN_PERCENT, 100, + TABLE_UINT64, range, TABLE_SET_ALIGN_PERCENT, 100, + TABLE_EMPTY, TABLE_SET_ALIGN_PERCENT, 100); + if (r < 0) + return table_log_add_error(r); + } + } + + assert(weight_sum > 0); + + if (details_table) { + size_t row; + + for (row = 1; row < table_get_rows(details_table); row++) { + char buf[DECIMAL_STR_MAX(uint64_t) + 1 + DECIMAL_STR_MAX(uint64_t) + 1]; + const uint64_t *weight, *badness, *range; + TableCell *cell; + uint64_t x; + + assert_se(weight = table_get_at(details_table, row, 4)); + assert_se(badness = table_get_at(details_table, row, 5)); + assert_se(range = table_get_at(details_table, row, 6)); + + if (*badness == UINT64_MAX || *badness == 0) + continue; + + assert_se(cell = table_get_cell(details_table, row, 7)); + + x = DIV_ROUND_UP(DIV_ROUND_UP(*badness * *weight * 100U, *range), weight_sum); + xsprintf(buf, "%" PRIu64 ".%" PRIu64, x / 10, x % 10); + + r = table_update(details_table, cell, TABLE_STRING, buf); + if (r < 0) + return log_error_errno(r, "Failed to update cell in table: %m"); + } + + if (json_format_flags & JSON_FORMAT_OFF) { + r = table_hide_column_from_display(details_table, (size_t) 2); + if (r < 0) + return log_error_errno(r, "Failed to set columns to display: %m"); + } + + r = table_print_with_pager(details_table, json_format_flags, pager_flags, /* show_header= */true); + if (r < 0) + return log_error_errno(r, "Failed to output table: %m"); + } + + exposure = DIV_ROUND_UP(badness_sum * 100U, weight_sum); + + for (i = 0; i < ELEMENTSOF(badness_table); i++) + if (exposure >= badness_table[i].exposure) + break; + + assert(i < ELEMENTSOF(badness_table)); + + if (details_table && (json_format_flags & JSON_FORMAT_OFF)) { + _cleanup_free_ char *clickable = NULL; + const char *name; + + /* If we shall output the details table, also print the brief summary underneath */ + + if (info->fragment_path) { + r = terminal_urlify_path(info->fragment_path, info->id, &clickable); + if (r < 0) + return log_oom(); + + name = clickable; + } else + name = info->id; + + printf("\n%s %sOverall exposure level for %s%s: %s%" PRIu64 ".%" PRIu64 " %s%s %s\n", + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), + ansi_highlight(), + name, + ansi_normal(), + colors_enabled() ? strempty(badness_table[i].color) : "", + exposure / 10, exposure % 10, + badness_table[i].name, + ansi_normal(), + special_glyph(badness_table[i].smiley)); + } + + fflush(stdout); + + if (overview_table) { + char buf[DECIMAL_STR_MAX(uint64_t) + 1 + DECIMAL_STR_MAX(uint64_t) + 1]; + _cleanup_free_ char *url = NULL; + + if (info->fragment_path) { + r = file_url_from_path(info->fragment_path, &url); + if (r < 0) + return log_error_errno(r, "Failed to generate URL from path: %m"); + } + + xsprintf(buf, "%" PRIu64 ".%" PRIu64, exposure / 10, exposure % 10); + + r = table_add_many(overview_table, + TABLE_STRING, info->id, + TABLE_SET_URL, url, + TABLE_STRING, buf, + TABLE_SET_ALIGN_PERCENT, 100, + TABLE_STRING, badness_table[i].name, + TABLE_SET_COLOR, strempty(badness_table[i].color), + TABLE_STRING, special_glyph(badness_table[i].smiley)); + if (r < 0) + return table_log_add_error(r); + } + + /* Return error when overall exposure level is over threshold */ + if (exposure > threshold) + return -EINVAL; + + return 0; +} + +static int property_read_restrict_namespaces( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + SecurityInfo *info = ASSERT_PTR(userdata); + int r; + uint64_t namespaces; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_read(m, "t", &namespaces); + if (r < 0) + return r; + + info->restrict_namespaces = (unsigned long long) namespaces; + + return 0; +} + +static int property_read_umask( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + SecurityInfo *info = ASSERT_PTR(userdata); + int r; + uint32_t umask; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_read(m, "u", &umask); + if (r < 0) + return r; + + info->_umask = (mode_t) umask; + + return 0; +} + +static int property_read_restrict_address_families( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + SecurityInfo *info = userdata; + int allow_list, r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'r', "bas"); + if (r < 0) + return r; + + r = sd_bus_message_read(m, "b", &allow_list); + if (r < 0) + return r; + + info->restrict_address_family_inet = + info->restrict_address_family_unix = + info->restrict_address_family_netlink = + info->restrict_address_family_packet = + info->restrict_address_family_other = allow_list; + + r = sd_bus_message_enter_container(m, 'a', "s"); + if (r < 0) + return r; + + for (;;) { + const char *name; + + r = sd_bus_message_read(m, "s", &name); + if (r < 0) + return r; + if (r == 0) + break; + + if (STR_IN_SET(name, "AF_INET", "AF_INET6")) + info->restrict_address_family_inet = !allow_list; + else if (streq(name, "AF_UNIX")) + info->restrict_address_family_unix = !allow_list; + else if (streq(name, "AF_NETLINK")) + info->restrict_address_family_netlink = !allow_list; + else if (streq(name, "AF_PACKET")) + info->restrict_address_family_packet = !allow_list; + else + info->restrict_address_family_other = !allow_list; + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + return sd_bus_message_exit_container(m); +} + +static int property_read_syscall_archs( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + SecurityInfo *info = ASSERT_PTR(userdata); + int r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'a', "s"); + if (r < 0) + return r; + + for (;;) { + const char *name; + + r = sd_bus_message_read(m, "s", &name); + if (r < 0) + return r; + if (r == 0) + break; + + r = set_put_strdup(&info->system_call_architectures, name); + if (r < 0) + return r; + } + + return sd_bus_message_exit_container(m); +} + +static int property_read_system_call_filter( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + SecurityInfo *info = userdata; + int allow_list, r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'r', "bas"); + if (r < 0) + return r; + + r = sd_bus_message_read(m, "b", &allow_list); + if (r < 0) + return r; + + info->system_call_filter_allow_list = allow_list; + + r = sd_bus_message_enter_container(m, 'a', "s"); + if (r < 0) + return r; + + for (;;) { + const char *name; + + r = sd_bus_message_read(m, "s", &name); + if (r < 0) + return r; + if (r == 0) + break; + + /* ignore errno or action after colon */ + r = set_put_strndup(&info->system_call_filter, name, strchrnul(name, ':') - name); + if (r < 0) + return r; + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + return sd_bus_message_exit_container(m); +} + +static int property_read_ip_address_allow( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + SecurityInfo *info = userdata; + bool deny_ipv4 = false, deny_ipv6 = false; + int r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'a', "(iayu)"); + if (r < 0) + return r; + + for (;;) { + const void *data; + size_t size; + int32_t family; + uint32_t prefixlen; + + r = sd_bus_message_enter_container(m, 'r', "iayu"); + if (r < 0) + return r; + if (r == 0) + break; + + r = sd_bus_message_read(m, "i", &family); + if (r < 0) + return r; + + r = sd_bus_message_read_array(m, 'y', &data, &size); + if (r < 0) + return r; + + r = sd_bus_message_read(m, "u", &prefixlen); + if (r < 0) + return r; + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + if (streq(member, "IPAddressAllow")) { + union in_addr_union u; + + if (family == AF_INET && size == 4 && prefixlen == 8) + memcpy(&u.in, data, size); + else if (family == AF_INET6 && size == 16 && prefixlen == 128) + memcpy(&u.in6, data, size); + else { + info->ip_address_allow_other = true; + continue; + } + + if (in_addr_is_localhost(family, &u)) + info->ip_address_allow_localhost = true; + else + info->ip_address_allow_other = true; + } else { + assert(streq(member, "IPAddressDeny")); + + if (family == AF_INET && size == 4 && prefixlen == 0) + deny_ipv4 = true; + else if (family == AF_INET6 && size == 16 && prefixlen == 0) + deny_ipv6 = true; + } + } + + info->ip_address_deny_all = deny_ipv4 && deny_ipv6; + + return sd_bus_message_exit_container(m); +} + +static int property_read_ip_filters( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + SecurityInfo *info = userdata; + _cleanup_strv_free_ char **l = NULL; + int r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_read_strv(m, &l); + if (r < 0) + return r; + + if (streq(member, "IPIngressFilterPath")) + info->ip_filters_custom_ingress = !strv_isempty(l); + else if (streq(member, "IPEgressFilterPath")) + info->ip_filters_custom_egress = !strv_isempty(l); + + return 0; +} + +static int property_read_device_allow( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + SecurityInfo *info = userdata; + int r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'a', "(ss)"); + if (r < 0) + return r; + + for (;;) { + const char *name, *policy; + + r = sd_bus_message_read(m, "(ss)", &name, &policy); + if (r < 0) + return r; + if (r == 0) + break; + + r = strv_extendf(&info->device_allow, "%s:%s", name, policy); + if (r < 0) + return r; + } + + return sd_bus_message_exit_container(m); +} + +static int acquire_security_info(sd_bus *bus, const char *name, SecurityInfo *info, AnalyzeSecurityFlags flags) { + + static const struct bus_properties_map security_map[] = { + { "AmbientCapabilities", "t", NULL, offsetof(SecurityInfo, ambient_capabilities) }, + { "CapabilityBoundingSet", "t", NULL, offsetof(SecurityInfo, capability_bounding_set) }, + { "DefaultDependencies", "b", NULL, offsetof(SecurityInfo, default_dependencies) }, + { "Delegate", "b", NULL, offsetof(SecurityInfo, delegate) }, + { "DeviceAllow", "a(ss)", property_read_device_allow, 0 }, + { "DevicePolicy", "s", NULL, offsetof(SecurityInfo, device_policy) }, + { "DynamicUser", "b", NULL, offsetof(SecurityInfo, dynamic_user) }, + { "FragmentPath", "s", NULL, offsetof(SecurityInfo, fragment_path) }, + { "IPAddressAllow", "a(iayu)", property_read_ip_address_allow, 0 }, + { "IPAddressDeny", "a(iayu)", property_read_ip_address_allow, 0 }, + { "IPIngressFilterPath", "as", property_read_ip_filters, 0 }, + { "IPEgressFilterPath", "as", property_read_ip_filters, 0 }, + { "Id", "s", NULL, offsetof(SecurityInfo, id) }, + { "KeyringMode", "s", NULL, offsetof(SecurityInfo, keyring_mode) }, + { "ProtectProc", "s", NULL, offsetof(SecurityInfo, protect_proc) }, + { "ProcSubset", "s", NULL, offsetof(SecurityInfo, proc_subset) }, + { "LoadState", "s", NULL, offsetof(SecurityInfo, load_state) }, + { "LockPersonality", "b", NULL, offsetof(SecurityInfo, lock_personality) }, + { "MemoryDenyWriteExecute", "b", NULL, offsetof(SecurityInfo, memory_deny_write_execute) }, + { "NoNewPrivileges", "b", NULL, offsetof(SecurityInfo, no_new_privileges) }, + { "NotifyAccess", "s", NULL, offsetof(SecurityInfo, notify_access) }, + { "PrivateDevices", "b", NULL, offsetof(SecurityInfo, private_devices) }, + { "PrivateMounts", "b", NULL, offsetof(SecurityInfo, private_mounts) }, + { "PrivateNetwork", "b", NULL, offsetof(SecurityInfo, private_network) }, + { "PrivateTmp", "b", NULL, offsetof(SecurityInfo, private_tmp) }, + { "PrivateUsers", "b", NULL, offsetof(SecurityInfo, private_users) }, + { "ProtectControlGroups", "b", NULL, offsetof(SecurityInfo, protect_control_groups) }, + { "ProtectHome", "s", NULL, offsetof(SecurityInfo, protect_home) }, + { "ProtectHostname", "b", NULL, offsetof(SecurityInfo, protect_hostname) }, + { "ProtectKernelModules", "b", NULL, offsetof(SecurityInfo, protect_kernel_modules) }, + { "ProtectKernelTunables", "b", NULL, offsetof(SecurityInfo, protect_kernel_tunables) }, + { "ProtectKernelLogs", "b", NULL, offsetof(SecurityInfo, protect_kernel_logs) }, + { "ProtectClock", "b", NULL, offsetof(SecurityInfo, protect_clock) }, + { "ProtectSystem", "s", NULL, offsetof(SecurityInfo, protect_system) }, + { "RemoveIPC", "b", NULL, offsetof(SecurityInfo, remove_ipc) }, + { "RestrictAddressFamilies", "(bas)", property_read_restrict_address_families, 0 }, + { "RestrictNamespaces", "t", property_read_restrict_namespaces, 0 }, + { "RestrictRealtime", "b", NULL, offsetof(SecurityInfo, restrict_realtime) }, + { "RestrictSUIDSGID", "b", NULL, offsetof(SecurityInfo, restrict_suid_sgid) }, + { "RootDirectory", "s", NULL, offsetof(SecurityInfo, root_directory) }, + { "RootImage", "s", NULL, offsetof(SecurityInfo, root_image) }, + { "SupplementaryGroups", "as", NULL, offsetof(SecurityInfo, supplementary_groups) }, + { "SystemCallArchitectures", "as", property_read_syscall_archs, 0 }, + { "SystemCallFilter", "(as)", property_read_system_call_filter, 0 }, + { "Type", "s", NULL, offsetof(SecurityInfo, type) }, + { "UMask", "u", property_read_umask, 0 }, + { "User", "s", NULL, offsetof(SecurityInfo, user) }, + {} + }; + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *path = NULL; + int r; + + /* Note: this mangles *info on failure! */ + + assert(bus); + assert(name); + assert(info); + + path = unit_dbus_path_from_name(name); + if (!path) + return log_oom(); + + r = bus_map_all_properties( + bus, + "org.freedesktop.systemd1", + path, + security_map, + BUS_MAP_STRDUP | BUS_MAP_BOOLEAN_AS_BOOL, + &error, + NULL, + info); + if (r < 0) + return log_error_errno(r, "Failed to get unit properties: %s", bus_error_message(&error, r)); + + if (!streq_ptr(info->load_state, "loaded")) { + + if (FLAGS_SET(flags, ANALYZE_SECURITY_ONLY_LOADED)) + return -EMEDIUMTYPE; + + if (streq_ptr(info->load_state, "not-found")) + log_error("Unit %s not found, cannot analyze.", name); + else if (streq_ptr(info->load_state, "masked")) + log_error("Unit %s is masked, cannot analyze.", name); + else + log_error("Unit %s not loaded properly, cannot analyze.", name); + + return -EINVAL; + } + + if (FLAGS_SET(flags, ANALYZE_SECURITY_ONLY_LONG_RUNNING) && streq_ptr(info->type, "oneshot")) + return -EMEDIUMTYPE; + + if (info->private_devices || + info->private_tmp || + info->protect_control_groups || + info->protect_kernel_tunables || + info->protect_kernel_modules || + !streq_ptr(info->protect_home, "no") || + !streq_ptr(info->protect_system, "no") || + info->root_image) + info->private_mounts = true; + + if (info->protect_kernel_modules) + info->capability_bounding_set &= ~(UINT64_C(1) << CAP_SYS_MODULE); + + if (info->protect_kernel_logs) + info->capability_bounding_set &= ~(UINT64_C(1) << CAP_SYSLOG); + + if (info->protect_clock) + info->capability_bounding_set &= ~((UINT64_C(1) << CAP_SYS_TIME) | + (UINT64_C(1) << CAP_WAKE_ALARM)); + + if (info->private_devices) + info->capability_bounding_set &= ~((UINT64_C(1) << CAP_MKNOD) | + (UINT64_C(1) << CAP_SYS_RAWIO)); + + return 0; +} + +static int analyze_security_one(sd_bus *bus, + const char *name, + Table *overview_table, + AnalyzeSecurityFlags flags, + unsigned threshold, + JsonVariant *policy, + PagerFlags pager_flags, + JsonFormatFlags json_format_flags) { + + _cleanup_(security_info_freep) SecurityInfo *info = security_info_new(); + if (!info) + return log_oom(); + + int r; + + assert(bus); + assert(name); + + r = acquire_security_info(bus, name, info, flags); + if (r == -EMEDIUMTYPE) /* Ignore this one because not loaded or Type is oneshot */ + return 0; + if (r < 0) + return r; + + r = assess(info, overview_table, flags, threshold, policy, pager_flags, json_format_flags); + if (r < 0) + return r; + + return 0; +} + +/* Refactoring SecurityInfo so that it can make use of existing struct variables instead of reading from dbus */ +static int get_security_info(Unit *u, ExecContext *c, CGroupContext *g, SecurityInfo **ret_info) { + assert(ret_info); + + _cleanup_(security_info_freep) SecurityInfo *info = security_info_new(); + if (!info) + return log_oom(); + + if (u) { + if (u->id) { + info->id = strdup(u->id); + if (!info->id) + return log_oom(); + } + if (unit_type_to_string(u->type)) { + info->type = strdup(unit_type_to_string(u->type)); + if (!info->type) + return log_oom(); + } + if (unit_load_state_to_string(u->load_state)) { + info->load_state = strdup(unit_load_state_to_string(u->load_state)); + if (!info->load_state) + return log_oom(); + } + if (u->fragment_path) { + info->fragment_path = strdup(u->fragment_path); + if (!info->fragment_path) + return log_oom(); + } + info->default_dependencies = u->default_dependencies; + if (u->type == UNIT_SERVICE && notify_access_to_string(SERVICE(u)->notify_access)) { + info->notify_access = strdup(notify_access_to_string(SERVICE(u)->notify_access)); + if (!info->notify_access) + return log_oom(); + } + } + + if (c) { + info->ambient_capabilities = c->capability_ambient_set; + info->capability_bounding_set = c->capability_bounding_set; + if (c->user) { + info->user = strdup(c->user); + if (!info->user) + return log_oom(); + } + if (c->supplementary_groups) { + info->supplementary_groups = strv_copy(c->supplementary_groups); + if (!info->supplementary_groups) + return log_oom(); + } + info->dynamic_user = c->dynamic_user; + if (exec_keyring_mode_to_string(c->keyring_mode)) { + info->keyring_mode = strdup(exec_keyring_mode_to_string(c->keyring_mode)); + if (!info->keyring_mode) + return log_oom(); + } + if (protect_proc_to_string(c->protect_proc)) { + info->protect_proc = strdup(protect_proc_to_string(c->protect_proc)); + if (!info->protect_proc) + return log_oom(); + } + if (proc_subset_to_string(c->proc_subset)) { + info->proc_subset = strdup(proc_subset_to_string(c->proc_subset)); + if (!info->proc_subset) + return log_oom(); + } + info->lock_personality = c->lock_personality; + info->memory_deny_write_execute = c->memory_deny_write_execute; + info->no_new_privileges = c->no_new_privileges; + info->protect_hostname = c->protect_hostname; + info->private_devices = c->private_devices; + info->private_mounts = c->private_mounts; + info->private_network = c->private_network; + info->private_tmp = c->private_tmp; + info->private_users = c->private_users; + info->protect_control_groups = c->protect_control_groups; + info->protect_kernel_modules = c->protect_kernel_modules; + info->protect_kernel_tunables = c->protect_kernel_tunables; + info->protect_kernel_logs = c->protect_kernel_logs; + info->protect_clock = c->protect_clock; + if (protect_home_to_string(c->protect_home)) { + info->protect_home = strdup(protect_home_to_string(c->protect_home)); + if (!info->protect_home) + return log_oom(); + } + if (protect_system_to_string(c->protect_system)) { + info->protect_system = strdup(protect_system_to_string(c->protect_system)); + if (!info->protect_system) + return log_oom(); + } + info->remove_ipc = c->remove_ipc; + info->restrict_address_family_inet = + info->restrict_address_family_unix = + info->restrict_address_family_netlink = + info->restrict_address_family_packet = + info->restrict_address_family_other = + c->address_families_allow_list; + + void *key; + SET_FOREACH(key, c->address_families) { + int family = PTR_TO_INT(key); + if (family == 0) + continue; + if (IN_SET(family, AF_INET, AF_INET6)) + info->restrict_address_family_inet = !c->address_families_allow_list; + else if (family == AF_UNIX) + info->restrict_address_family_unix = !c->address_families_allow_list; + else if (family == AF_NETLINK) + info->restrict_address_family_netlink = !c->address_families_allow_list; + else if (family == AF_PACKET) + info->restrict_address_family_packet = !c->address_families_allow_list; + else + info->restrict_address_family_other = !c->address_families_allow_list; + } + + info->restrict_namespaces = c->restrict_namespaces; + info->restrict_realtime = c->restrict_realtime; + info->restrict_suid_sgid = c->restrict_suid_sgid; + if (c->root_directory) { + info->root_directory = strdup(c->root_directory); + if (!info->root_directory) + return log_oom(); + } + if (c->root_image) { + info->root_image = strdup(c->root_image); + if (!info->root_image) + return log_oom(); + } + info->_umask = c->umask; + +#if HAVE_SECCOMP + SET_FOREACH(key, c->syscall_archs) { + const char *name; + + name = seccomp_arch_to_string(PTR_TO_UINT32(key) - 1); + if (!name) + continue; + + if (set_put_strdup(&info->system_call_architectures, name) < 0) + return log_oom(); + } + + info->system_call_filter_allow_list = c->syscall_allow_list; + + void *id, *num; + HASHMAP_FOREACH_KEY(num, id, c->syscall_filter) { + _cleanup_free_ char *name = NULL; + + if (info->system_call_filter_allow_list && PTR_TO_INT(num) >= 0) + continue; + + name = seccomp_syscall_resolve_num_arch(SCMP_ARCH_NATIVE, PTR_TO_INT(id) - 1); + if (!name) + continue; + + if (set_ensure_consume(&info->system_call_filter, &string_hash_ops_free, TAKE_PTR(name)) < 0) + return log_oom(); + } +#endif + } + + if (g) { + info->delegate = g->delegate; + if (cgroup_device_policy_to_string(g->device_policy)) { + info->device_policy = strdup(cgroup_device_policy_to_string(g->device_policy)); + if (!info->device_policy) + return log_oom(); + } + + struct in_addr_prefix *i; + bool deny_ipv4 = false, deny_ipv6 = false; + + SET_FOREACH(i, g->ip_address_deny) { + if (i->family == AF_INET && i->prefixlen == 0) + deny_ipv4 = true; + else if (i->family == AF_INET6 && i->prefixlen == 0) + deny_ipv6 = true; + } + info->ip_address_deny_all = deny_ipv4 && deny_ipv6; + + info->ip_address_allow_localhost = info->ip_address_allow_other = false; + SET_FOREACH(i, g->ip_address_allow) { + if (in_addr_is_localhost(i->family, &i->address)) + info->ip_address_allow_localhost = true; + else + info->ip_address_allow_other = true; + } + + info->ip_filters_custom_ingress = !strv_isempty(g->ip_filters_ingress); + info->ip_filters_custom_egress = !strv_isempty(g->ip_filters_egress); + + LIST_FOREACH(device_allow, a, g->device_allow) + if (strv_extendf(&info->device_allow, + "%s:%s", + a->path, + cgroup_device_permissions_to_string(a->permissions)) < 0) + return log_oom(); + } + + *ret_info = TAKE_PTR(info); + + return 0; +} + +static int offline_security_check(Unit *u, + unsigned threshold, + JsonVariant *policy, + PagerFlags pager_flags, + JsonFormatFlags json_format_flags) { + + _cleanup_(table_unrefp) Table *overview_table = NULL; + AnalyzeSecurityFlags flags = 0; + _cleanup_(security_info_freep) SecurityInfo *info = NULL; + int r; + + assert(u); + + if (DEBUG_LOGGING) + unit_dump(u, stdout, "\t"); + + r = get_security_info(u, unit_get_exec_context(u), unit_get_cgroup_context(u), &info); + if (r < 0) + return r; + + return assess(info, overview_table, flags, threshold, policy, pager_flags, json_format_flags); +} + +static int offline_security_checks( + char **filenames, + JsonVariant *policy, + RuntimeScope scope, + bool check_man, + bool run_generators, + unsigned threshold, + const char *root, + const char *profile, + PagerFlags pager_flags, + JsonFormatFlags json_format_flags) { + + const ManagerTestRunFlags flags = + MANAGER_TEST_RUN_MINIMAL | + MANAGER_TEST_RUN_ENV_GENERATORS | + MANAGER_TEST_RUN_IGNORE_DEPENDENCIES | + MANAGER_TEST_DONT_OPEN_EXECUTOR | + run_generators * MANAGER_TEST_RUN_GENERATORS; + + _cleanup_(manager_freep) Manager *m = NULL; + Unit *units[strv_length(filenames)]; + int r, k; + size_t count = 0; + + if (strv_isempty(filenames)) + return 0; + + r = verify_set_unit_path(filenames); + if (r < 0) + return log_error_errno(r, "Failed to set unit load path: %m"); + + r = manager_new(scope, flags, &m); + if (r < 0) + return log_error_errno(r, "Failed to initialize manager: %m"); + + log_debug("Starting manager..."); + + r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, root); + if (r < 0) + return r; + + if (profile) { + /* Ensure the temporary directory is in the search path, so that we can add drop-ins. */ + r = strv_extend(&m->lookup_paths.search_path, m->lookup_paths.temporary_dir); + if (r < 0) + return log_oom(); + } + + log_debug("Loading remaining units from the command line..."); + + STRV_FOREACH(filename, filenames) { + _cleanup_free_ char *prepared = NULL; + + log_debug("Handling %s...", *filename); + + k = verify_prepare_filename(*filename, &prepared); + if (k < 0) { + log_warning_errno(k, "Failed to prepare filename %s: %m", *filename); + RET_GATHER(r, k); + continue; + } + + /* When a portable image is analyzed, the profile is what provides a good chunk of + * the security-related settings, but they are obviously not shipped with the image. + * This allows to take them in consideration. */ + if (profile) { + _cleanup_free_ char *unit_name = NULL, *dropin = NULL, *profile_path = NULL; + + r = path_extract_filename(prepared, &unit_name); + if (r < 0) + return log_oom(); + + dropin = strjoin(m->lookup_paths.temporary_dir, "/", unit_name, ".d/profile.conf"); + if (!dropin) + return log_oom(); + (void) mkdir_parents(dropin, 0755); + + if (!is_path(profile)) { + r = find_portable_profile(profile, unit_name, &profile_path); + if (r < 0) + return log_error_errno(r, "Failed to find portable profile %s: %m", profile); + profile = profile_path; + } + + r = copy_file(profile, dropin, 0, 0644, 0); + if (r < 0) + return log_error_errno(r, "Failed to copy: %m"); + } + + k = manager_load_startable_unit_or_warn(m, NULL, prepared, &units[count]); + if (k < 0) { + RET_GATHER(r, k); + continue; + } + + count++; + } + + for (size_t i = 0; i < count; i++) + RET_GATHER(r, offline_security_check(units[i], threshold, policy, pager_flags, json_format_flags)); + + return r; +} + +static int analyze_security(sd_bus *bus, + char **units, + JsonVariant *policy, + RuntimeScope scope, + bool check_man, + bool run_generators, + bool offline, + unsigned threshold, + const char *root, + const char *profile, + JsonFormatFlags json_format_flags, + PagerFlags pager_flags, + AnalyzeSecurityFlags flags) { + + _cleanup_(table_unrefp) Table *overview_table = NULL; + int ret = 0, r; + + assert(!!bus != offline); + + if (offline) + return offline_security_checks(units, policy, scope, check_man, run_generators, threshold, root, profile, pager_flags, json_format_flags); + + if (strv_length(units) != 1) { + overview_table = table_new("unit", "exposure", "predicate", "happy"); + if (!overview_table) + return log_oom(); + } + + if (strv_isempty(units)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_strv_free_ char **list = NULL; + size_t n = 0; + + r = bus_call_method( + bus, + bus_systemd_mgr, + "ListUnits", + &error, + &reply, + NULL); + if (r < 0) + return log_error_errno(r, "Failed to list units: %s", bus_error_message(&error, r)); + + r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ssssssouso)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + UnitInfo info; + char *copy = NULL; + + r = bus_parse_unit_info(reply, &info); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + if (!endswith(info.id, ".service")) + continue; + + if (!GREEDY_REALLOC(list, n + 2)) + return log_oom(); + + copy = strdup(info.id); + if (!copy) + return log_oom(); + + list[n++] = copy; + list[n] = NULL; + } + + strv_sort(list); + + flags |= ANALYZE_SECURITY_SHORT|ANALYZE_SECURITY_ONLY_LOADED|ANALYZE_SECURITY_ONLY_LONG_RUNNING; + + STRV_FOREACH(i, list) { + r = analyze_security_one(bus, *i, overview_table, flags, threshold, policy, pager_flags, json_format_flags); + if (r < 0 && ret >= 0) + ret = r; + } + + } else + STRV_FOREACH(i, units) { + _cleanup_free_ char *mangled = NULL, *instance = NULL; + const char *name; + + if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT) && i != units) { + putc('\n', stdout); + fflush(stdout); + } + + r = unit_name_mangle(*i, 0, &mangled); + if (r < 0) + return log_error_errno(r, "Failed to mangle unit name '%s': %m", *i); + + if (!endswith(mangled, ".service")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unit %s is not a service unit, refusing.", + *i); + + if (unit_name_is_valid(mangled, UNIT_NAME_TEMPLATE)) { + r = unit_name_replace_instance(mangled, "test-instance", &instance); + if (r < 0) + return log_oom(); + + name = instance; + } else + name = mangled; + + r = analyze_security_one(bus, name, overview_table, flags, threshold, policy, pager_flags, json_format_flags); + if (r < 0 && ret >= 0) + ret = r; + } + + if (overview_table) { + if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT)) { + putc('\n', stdout); + fflush(stdout); + } + + r = table_print_with_pager(overview_table, json_format_flags, pager_flags, /* show_header= */true); + if (r < 0) + return log_error_errno(r, "Failed to output table: %m"); + } + return ret; +} + +int verb_security(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *policy = NULL; + int r; + unsigned line, column; + + if (!arg_offline) { + r = acquire_bus(&bus, NULL); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + } + + pager_open(arg_pager_flags); + + if (arg_security_policy) { + r = json_parse_file(/*f=*/ NULL, arg_security_policy, /*flags=*/ 0, &policy, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse '%s' at %u:%u: %m", arg_security_policy, line, column); + } else { + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *pp = NULL; + + r = search_and_fopen_nulstr("systemd-analyze-security.policy", "re", /*root=*/ NULL, CONF_PATHS_NULSTR("systemd"), &f, &pp); + if (r < 0 && r != -ENOENT) + return r; + + if (f) { + r = json_parse_file(f, pp, /*flags=*/ 0, &policy, &line, &column); + if (r < 0) + return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON policy: %m", pp, line, column); + } + } + + return analyze_security( + bus, + strv_skip(argv, 1), + policy, + arg_runtime_scope, + arg_man, + arg_generators, + arg_offline, + arg_threshold, + arg_root, + arg_profile, + arg_json_format_flags, + arg_pager_flags, + /*flags=*/ 0); +} diff --git a/src/analyze/analyze-security.h b/src/analyze/analyze-security.h new file mode 100644 index 0000000..82f4c7f --- /dev/null +++ b/src/analyze/analyze-security.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef enum AnalyzeSecurityFlags { + ANALYZE_SECURITY_SHORT = 1 << 0, + ANALYZE_SECURITY_ONLY_LOADED = 1 << 1, + ANALYZE_SECURITY_ONLY_LONG_RUNNING = 1 << 2, +} AnalyzeSecurityFlags; + +int verb_security(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-service-watchdogs.c b/src/analyze/analyze-service-watchdogs.c new file mode 100644 index 0000000..6535eb1 --- /dev/null +++ b/src/analyze/analyze-service-watchdogs.c @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-service-watchdogs.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "parse-util.h" + +int verb_service_watchdogs(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int b, r; + + assert(IN_SET(argc, 1, 2)); + assert(argv); + + r = acquire_bus(&bus, NULL); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + + if (argc == 1) { + /* get ServiceWatchdogs */ + r = bus_get_property_trivial(bus, bus_systemd_mgr, "ServiceWatchdogs", &error, 'b', &b); + if (r < 0) + return log_error_errno(r, "Failed to get service-watchdog state: %s", bus_error_message(&error, r)); + + printf("%s\n", yes_no(!!b)); + + } else { + /* set ServiceWatchdogs */ + b = parse_boolean(argv[1]); + if (b < 0) + return log_error_errno(b, "Failed to parse service-watchdogs argument: %m"); + + r = bus_set_property(bus, bus_systemd_mgr, "ServiceWatchdogs", &error, "b", b); + if (r < 0) + return log_error_errno(r, "Failed to set service-watchdog state: %s", bus_error_message(&error, r)); + } + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-service-watchdogs.h b/src/analyze/analyze-service-watchdogs.h new file mode 100644 index 0000000..2f59f5a --- /dev/null +++ b/src/analyze/analyze-service-watchdogs.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_service_watchdogs(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-srk.c b/src/analyze/analyze-srk.c new file mode 100644 index 0000000..0e24b41 --- /dev/null +++ b/src/analyze/analyze-srk.c @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-srk.h" +#include "fileio.h" +#include "tpm2-util.h" + +int verb_srk(int argc, char *argv[], void *userdata) { +#if HAVE_TPM2 + _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; + _cleanup_(Esys_Freep) TPM2B_PUBLIC *public = NULL; + int r; + + r = tpm2_context_new(/* device= */ NULL, &c); + if (r < 0) + return log_error_errno(r, "Failed to create TPM2 context: %m"); + + r = tpm2_get_srk( + c, + /* session= */ NULL, + &public, + /* ret_name= */ NULL, + /* ret_qname= */ NULL, + /* ret_handle= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to get SRK: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No SRK stored so far."); + + _cleanup_free_ void *marshalled = NULL; + size_t marshalled_size = 0; + r = tpm2_marshal_public(public, &marshalled, &marshalled_size); + if (r < 0) + return log_error_errno(r, "Failed to marshal SRK: %m"); + + if (isatty(STDOUT_FILENO)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Refusing to write binary data to TTY, please redirect output to file."); + + if (fwrite(marshalled, 1, marshalled_size, stdout) != marshalled_size) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write SRK to stdout: %m"); + + r = fflush_and_check(stdout); + if (r < 0) + return log_error_errno(r, "Failed to write SRK to stdout: %m"); + + return EXIT_SUCCESS; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support not available."); +#endif +} diff --git a/src/analyze/analyze-srk.h b/src/analyze/analyze-srk.h new file mode 100644 index 0000000..2602835 --- /dev/null +++ b/src/analyze/analyze-srk.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_srk(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-syscall-filter.c b/src/analyze/analyze-syscall-filter.c new file mode 100644 index 0000000..66a52da --- /dev/null +++ b/src/analyze/analyze-syscall-filter.c @@ -0,0 +1,202 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze-syscall-filter.h" +#include "analyze.h" +#include "fd-util.h" +#include "fileio.h" +#include "nulstr-util.h" +#include "seccomp-util.h" +#include "set.h" +#include "strv.h" +#include "terminal-util.h" + +#if HAVE_SECCOMP + +static int load_kernel_syscalls(Set **ret) { + _cleanup_set_free_ Set *syscalls = NULL; + _cleanup_fclose_ FILE *f = NULL; + int r; + + /* Let's read the available system calls from the list of available tracing events. Slightly dirty, + * but good enough for analysis purposes. */ + + f = fopen("/sys/kernel/tracing/available_events", "re"); + if (!f) { + /* We tried the non-debugfs mount point and that didn't work. If it wasn't mounted, maybe the + * old debugfs mount point works? */ + f = fopen("/sys/kernel/debug/tracing/available_events", "re"); + if (!f) + return log_full_errno(IN_SET(errno, EPERM, EACCES, ENOENT) ? LOG_DEBUG : LOG_WARNING, errno, + "Can't read open tracefs' available_events file: %m"); + } + + for (;;) { + _cleanup_free_ char *line = NULL; + const char *e; + + r = read_line(f, LONG_LINE_MAX, &line); + if (r < 0) + return log_error_errno(r, "Failed to read system call list: %m"); + if (r == 0) + break; + + e = startswith(line, "syscalls:sys_enter_"); + if (!e) + continue; + + /* These are named differently inside the kernel than their external name for historical + * reasons. Let's hide them here. */ + if (STR_IN_SET(e, "newuname", "newfstat", "newstat", "newlstat", "sysctl")) + continue; + + r = set_put_strdup(&syscalls, e); + if (r < 0) + return log_error_errno(r, "Failed to add system call to list: %m"); + } + + *ret = TAKE_PTR(syscalls); + return 0; +} + +static int syscall_set_add(Set **s, const SyscallFilterSet *set) { + int r; + + assert(s); + + if (!set) + return 0; + + NULSTR_FOREACH(sc, set->value) { + if (sc[0] == '@') + continue; + + r = set_put_strdup(s, sc); + if (r < 0) + return r; + } + + return 0; +} + +static void syscall_set_remove(Set *s, const SyscallFilterSet *set) { + if (!set) + return; + + NULSTR_FOREACH(sc, set->value) { + if (sc[0] == '@') + continue; + + free(set_remove(s, sc)); + } +} + +static void dump_syscall_filter(const SyscallFilterSet *set) { + printf("%s%s%s\n" + " # %s\n", + ansi_highlight(), + set->name, + ansi_normal(), + set->help); + + NULSTR_FOREACH(syscall, set->value) + printf(" %s%s%s\n", syscall[0] == '@' ? ansi_underline() : "", syscall, ansi_normal()); +} + +int verb_syscall_filters(int argc, char *argv[], void *userdata) { + bool first = true; + int r; + + pager_open(arg_pager_flags); + + if (strv_isempty(strv_skip(argv, 1))) { + _cleanup_set_free_ Set *kernel = NULL, *known = NULL; + int k = 0; /* explicit initialization to appease gcc */ + + r = syscall_set_add(&known, syscall_filter_sets + SYSCALL_FILTER_SET_KNOWN); + if (r < 0) + return log_error_errno(r, "Failed to prepare set of known system calls: %m"); + + if (!arg_quiet) + k = load_kernel_syscalls(&kernel); + + for (int i = 0; i < _SYSCALL_FILTER_SET_MAX; i++) { + const SyscallFilterSet *set = syscall_filter_sets + i; + if (!first) + puts(""); + + dump_syscall_filter(set); + syscall_set_remove(kernel, set); + if (i != SYSCALL_FILTER_SET_KNOWN) + syscall_set_remove(known, set); + first = false; + } + + if (arg_quiet) /* Let's not show the extra stuff in quiet mode */ + return 0; + + if (!set_isempty(known)) { + _cleanup_free_ char **l = NULL; + + printf("\n" + "# %sUngrouped System Calls%s (known but not included in any of the groups except @known):\n", + ansi_highlight(), ansi_normal()); + + l = set_get_strv(known); + if (!l) + return log_oom(); + + strv_sort(l); + + STRV_FOREACH(syscall, l) + printf("# %s\n", *syscall); + } + + if (k < 0) { + fputc('\n', stdout); + fflush(stdout); + if (!arg_quiet) + log_notice_errno(k, "# Not showing unlisted system calls, couldn't retrieve kernel system call list: %m"); + } else if (!set_isempty(kernel)) { + _cleanup_free_ char **l = NULL; + + printf("\n" + "# %sUnlisted System Calls%s (supported by the local kernel, but not included in any of the groups listed above):\n", + ansi_highlight(), ansi_normal()); + + l = set_get_strv(kernel); + if (!l) + return log_oom(); + + strv_sort(l); + + STRV_FOREACH(syscall, l) + printf("# %s\n", *syscall); + } + } else + STRV_FOREACH(name, strv_skip(argv, 1)) { + const SyscallFilterSet *set; + + if (!first) + puts(""); + + set = syscall_filter_set_find(*name); + if (!set) { + /* make sure the error appears below normal output */ + fflush(stdout); + + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "Filter set \"%s\" not found.", *name); + } + + dump_syscall_filter(set); + first = false; + } + + return EXIT_SUCCESS; +} + +#else +int verb_syscall_filters(int argc, char *argv[], void *userdata) { + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Not compiled with syscall filters, sorry."); +} +#endif diff --git a/src/analyze/analyze-syscall-filter.h b/src/analyze/analyze-syscall-filter.h new file mode 100644 index 0000000..3a1af85 --- /dev/null +++ b/src/analyze/analyze-syscall-filter.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_syscall_filters(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-time-data.c b/src/analyze/analyze-time-data.c new file mode 100644 index 0000000..741cab3 --- /dev/null +++ b/src/analyze/analyze-time-data.c @@ -0,0 +1,331 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze-time-data.h" +#include "analyze.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "bus-map-properties.h" +#include "bus-unit-util.h" +#include "memory-util.h" +#include "special.h" +#include "strv.h" + +static void subtract_timestamp(usec_t *a, usec_t b) { + assert(a); + + if (*a > 0) { + assert(*a >= b); + *a -= b; + } +} + +static int log_not_finished(usec_t finish_time) { + return log_error_errno(SYNTHETIC_ERRNO(EINPROGRESS), + "Bootup is not yet finished (org.freedesktop.systemd1.Manager.FinishTimestampMonotonic=%"PRIu64").\n" + "Please try again later.\n" + "Hint: Use 'systemctl%s list-jobs' to see active jobs", + finish_time, + arg_runtime_scope == RUNTIME_SCOPE_SYSTEM ? "" : " --user"); +} + +int acquire_boot_times(sd_bus *bus, bool require_finished, BootTimes **ret) { + static const struct bus_properties_map property_map[] = { + { "FirmwareTimestampMonotonic", "t", NULL, offsetof(BootTimes, firmware_time) }, + { "LoaderTimestampMonotonic", "t", NULL, offsetof(BootTimes, loader_time) }, + { "KernelTimestamp", "t", NULL, offsetof(BootTimes, kernel_time) }, + { "InitRDTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_time) }, + { "UserspaceTimestampMonotonic", "t", NULL, offsetof(BootTimes, userspace_time) }, + { "FinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, finish_time) }, + { "SecurityStartTimestampMonotonic", "t", NULL, offsetof(BootTimes, security_start_time) }, + { "SecurityFinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, security_finish_time) }, + { "GeneratorsStartTimestampMonotonic", "t", NULL, offsetof(BootTimes, generators_start_time) }, + { "GeneratorsFinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, generators_finish_time) }, + { "UnitsLoadStartTimestampMonotonic", "t", NULL, offsetof(BootTimes, unitsload_start_time) }, + { "UnitsLoadFinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, unitsload_finish_time) }, + { "InitRDSecurityStartTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_security_start_time) }, + { "InitRDSecurityFinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_security_finish_time) }, + { "InitRDGeneratorsStartTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_generators_start_time) }, + { "InitRDGeneratorsFinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_generators_finish_time) }, + { "InitRDUnitsLoadStartTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_unitsload_start_time) }, + { "InitRDUnitsLoadFinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_unitsload_finish_time) }, + {}, + }; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + static BootTimes times; + static bool cached = false; + int r; + + if (cached) { + if (require_finished && times.finish_time <= 0) + return log_not_finished(times.finish_time); + + if (ret) + *ret = × + return 0; + } + + assert_cc(sizeof(usec_t) == sizeof(uint64_t)); + + r = bus_map_all_properties( + bus, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + property_map, + BUS_MAP_STRDUP, + &error, + NULL, + ×); + if (r < 0) + return log_error_errno(r, "Failed to get timestamp properties: %s", bus_error_message(&error, r)); + + if (require_finished && times.finish_time <= 0) + return log_not_finished(times.finish_time); + + if (arg_runtime_scope == RUNTIME_SCOPE_SYSTEM && times.security_start_time > 0) { + /* security_start_time is set when systemd is not running under container environment. */ + if (times.initrd_time > 0) + times.kernel_done_time = times.initrd_time; + else + times.kernel_done_time = times.userspace_time; + } else { + /* + * User-instance-specific or container-system-specific timestamps processing + * (see comment to reverse_offset in BootTimes). + */ + times.reverse_offset = times.userspace_time; + + times.firmware_time = times.loader_time = times.kernel_time = times.initrd_time = + times.userspace_time = times.security_start_time = times.security_finish_time = 0; + + if (times.finish_time > 0) + subtract_timestamp(×.finish_time, times.reverse_offset); + + subtract_timestamp(×.generators_start_time, times.reverse_offset); + subtract_timestamp(×.generators_finish_time, times.reverse_offset); + + subtract_timestamp(×.unitsload_start_time, times.reverse_offset); + subtract_timestamp(×.unitsload_finish_time, times.reverse_offset); + } + + cached = true; + + if (ret) + *ret = × + return 0; +} + +static int bus_get_uint64_property(sd_bus *bus, const char *path, const char *interface, const char *property, uint64_t *val) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(bus); + assert(path); + assert(interface); + assert(property); + assert(val); + + r = sd_bus_get_property_trivial( + bus, + "org.freedesktop.systemd1", + path, + interface, + property, + &error, + 't', val); + if (r < 0) + return log_error_errno(r, "Failed to parse reply: %s", bus_error_message(&error, r)); + + return 0; +} + +int pretty_boot_time(sd_bus *bus, char **ret) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *path = NULL, *unit_id = NULL, *text = NULL; + usec_t activated_time = USEC_INFINITY; + BootTimes *t; + int r; + + r = acquire_boot_times(bus, /* require_finished = */ true, &t); + if (r < 0) + return r; + + path = unit_dbus_path_from_name(SPECIAL_DEFAULT_TARGET); + if (!path) + return log_oom(); + + r = sd_bus_get_property_string( + bus, + "org.freedesktop.systemd1", + path, + "org.freedesktop.systemd1.Unit", + "Id", + &error, + &unit_id); + if (r < 0) + log_warning_errno(r, "default.target doesn't seem to exist, ignoring: %s", bus_error_message(&error, r)); + + r = bus_get_uint64_property(bus, path, + "org.freedesktop.systemd1.Unit", + "ActiveEnterTimestampMonotonic", + &activated_time); + if (r < 0) + log_warning_errno(r, "Could not get time to reach default.target, ignoring: %m"); + + text = strdup("Startup finished in "); + if (!text) + return log_oom(); + + if (timestamp_is_set(t->firmware_time) && !strextend(&text, FORMAT_TIMESPAN(t->firmware_time - t->loader_time, USEC_PER_MSEC), " (firmware) + ")) + return log_oom(); + if (timestamp_is_set(t->loader_time) && !strextend(&text, FORMAT_TIMESPAN(t->loader_time, USEC_PER_MSEC), " (loader) + ")) + return log_oom(); + if (timestamp_is_set(t->kernel_done_time) && !strextend(&text, FORMAT_TIMESPAN(t->kernel_done_time, USEC_PER_MSEC), " (kernel) + ")) + return log_oom(); + if (timestamp_is_set(t->initrd_time) && !strextend(&text, FORMAT_TIMESPAN(t->userspace_time - t->initrd_time, USEC_PER_MSEC), " (initrd) + ")) + return log_oom(); + + if (!strextend(&text, FORMAT_TIMESPAN(t->finish_time - t->userspace_time, USEC_PER_MSEC), " (userspace) ")) + return log_oom(); + + if (timestamp_is_set(t->kernel_done_time)) + if (!strextend(&text, "= ", FORMAT_TIMESPAN(t->firmware_time + t->finish_time, USEC_PER_MSEC), " ")) + return log_oom(); + + if (unit_id && timestamp_is_set(activated_time)) { + usec_t base = timestamp_is_set(t->userspace_time) ? t->userspace_time : t->reverse_offset; + + if (!strextend(&text, "\n", unit_id, " reached after ", FORMAT_TIMESPAN(activated_time - base, USEC_PER_MSEC), " in userspace.")) + return log_oom(); + + } else if (unit_id && activated_time == 0) { + + if (!strextend(&text, "\n", unit_id, " was never reached.")) + return log_oom(); + + } else if (unit_id && activated_time == USEC_INFINITY) { + + if (!strextend(&text, "\nCould not get time to reach ", unit_id, ".")) + return log_oom(); + + } else if (!unit_id) { + + if (!strextend(&text, "\ncould not find default.target.")) + return log_oom(); + } + + *ret = TAKE_PTR(text); + return 0; +} + +void unit_times_clear(UnitTimes *t) { + if (!t) + return; + + FOREACH_ARRAY(d, t->deps, ELEMENTSOF(t->deps)) + *d = strv_free(*d); + + t->name = mfree(t->name); +} + +UnitTimes* unit_times_free_array(UnitTimes *t) { + if (!t) + return NULL; + + for (UnitTimes *p = t; p->has_data; p++) + unit_times_clear(p); + + return mfree(t); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(UnitTimes*, unit_times_clear, NULL); + +int acquire_time_data(sd_bus *bus, bool require_finished, UnitTimes **out) { + static const struct bus_properties_map property_map[] = { + { "InactiveExitTimestampMonotonic", "t", NULL, offsetof(UnitTimes, activating) }, + { "ActiveEnterTimestampMonotonic", "t", NULL, offsetof(UnitTimes, activated) }, + { "ActiveExitTimestampMonotonic", "t", NULL, offsetof(UnitTimes, deactivating) }, + { "InactiveEnterTimestampMonotonic", "t", NULL, offsetof(UnitTimes, deactivated) }, + { "After", "as", NULL, offsetof(UnitTimes, deps[UNIT_AFTER]) }, + { "Before", "as", NULL, offsetof(UnitTimes, deps[UNIT_BEFORE]) }, + { "Requires", "as", NULL, offsetof(UnitTimes, deps[UNIT_REQUIRES]) }, + { "Requisite", "as", NULL, offsetof(UnitTimes, deps[UNIT_REQUISITE]) }, + { "Wants", "as", NULL, offsetof(UnitTimes, deps[UNIT_WANTS]) }, + { "Conflicts", "as", NULL, offsetof(UnitTimes, deps[UNIT_CONFLICTS]) }, + { "Upholds", "as", NULL, offsetof(UnitTimes, deps[UNIT_UPHOLDS]) }, + {}, + }; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(unit_times_free_arrayp) UnitTimes *unit_times = NULL; + BootTimes *boot_times; + size_t c = 0; + UnitInfo u; + int r; + + r = acquire_boot_times(bus, require_finished, &boot_times); + if (r < 0) + return r; + + r = bus_call_method(bus, bus_systemd_mgr, "ListUnits", &error, &reply, NULL); + if (r < 0) + return log_error_errno(r, "Failed to list units: %s", bus_error_message(&error, r)); + + r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ssssssouso)"); + if (r < 0) + return bus_log_parse_error(r); + + while ((r = bus_parse_unit_info(reply, &u)) > 0) { + _cleanup_(unit_times_clearp) UnitTimes *t = NULL; + + if (!GREEDY_REALLOC0(unit_times, c + 2)) + return log_oom(); + + /* t initially has pointers zeroed by the allocation, and unit_times_clearp will have zeroed + * them if the entry is being reused. */ + t = &unit_times[c]; + + assert_cc(sizeof(usec_t) == sizeof(uint64_t)); + + r = bus_map_all_properties( + bus, + "org.freedesktop.systemd1", + u.unit_path, + property_map, + BUS_MAP_STRDUP, + &error, + NULL, + t); + if (r < 0) + return log_error_errno(r, "Failed to get timestamp properties of unit %s: %s", + u.id, bus_error_message(&error, r)); + + subtract_timestamp(&t->activating, boot_times->reverse_offset); + subtract_timestamp(&t->activated, boot_times->reverse_offset); + subtract_timestamp(&t->deactivating, boot_times->reverse_offset); + subtract_timestamp(&t->deactivated, boot_times->reverse_offset); + + if (t->activated >= t->activating) + t->time = t->activated - t->activating; + else if (t->deactivated >= t->activating) + t->time = t->deactivated - t->activating; + else + t->time = 0; + + if (t->activating == 0) + continue; + + t->name = strdup(u.id); + if (!t->name) + return log_oom(); + + t->has_data = true; + /* Prevent destructor from running on t reference. */ + TAKE_PTR(t); + c++; + } + if (r < 0) + return bus_log_parse_error(r); + + *out = TAKE_PTR(unit_times); + return c; +} diff --git a/src/analyze/analyze-time-data.h b/src/analyze/analyze-time-data.h new file mode 100644 index 0000000..9049d87 --- /dev/null +++ b/src/analyze/analyze-time-data.h @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <sd-bus.h> + +#include "time-util.h" +#include "unit-def.h" + +typedef struct BootTimes { + usec_t firmware_time; + usec_t loader_time; + usec_t kernel_time; + usec_t kernel_done_time; + usec_t initrd_time; + usec_t userspace_time; + usec_t finish_time; + usec_t security_start_time; + usec_t security_finish_time; + usec_t generators_start_time; + usec_t generators_finish_time; + usec_t unitsload_start_time; + usec_t unitsload_finish_time; + usec_t initrd_security_start_time; + usec_t initrd_security_finish_time; + usec_t initrd_generators_start_time; + usec_t initrd_generators_finish_time; + usec_t initrd_unitsload_start_time; + usec_t initrd_unitsload_finish_time; + + /* + * If we're analyzing the user instance, all timestamps will be offset by its own start-up timestamp, + * which may be arbitrarily big. With "plot", this causes arbitrarily wide output SVG files which + * almost completely consist of empty space. Thus we cancel out this offset. + * + * This offset is subtracted from times above by acquire_boot_times(), but it still needs to be + * subtracted from unit-specific timestamps (so it is stored here for reference). + */ + usec_t reverse_offset; +} BootTimes; + +typedef struct UnitTimes { + bool has_data; + char *name; + usec_t activating; + usec_t activated; + usec_t deactivated; + usec_t deactivating; + usec_t time; + char **deps[_UNIT_DEPENDENCY_MAX]; +} UnitTimes; + +int acquire_boot_times(sd_bus *bus, bool require_finished, BootTimes **ret); +int pretty_boot_time(sd_bus *bus, char **ret); + +void unit_times_clear(UnitTimes *t); +UnitTimes* unit_times_free_array(UnitTimes *t); +DEFINE_TRIVIAL_CLEANUP_FUNC(UnitTimes*, unit_times_free_array); + +int acquire_time_data(sd_bus *bus, bool require_finished, UnitTimes **out); diff --git a/src/analyze/analyze-time.c b/src/analyze/analyze-time.c new file mode 100644 index 0000000..c233b1f --- /dev/null +++ b/src/analyze/analyze-time.c @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-time.h" +#include "analyze-time-data.h" + +int verb_time(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_free_ char *buf = NULL; + int r; + + r = acquire_bus(&bus, NULL); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + + r = pretty_boot_time(bus, &buf); + if (r < 0) + return r; + + puts(buf); + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-time.h b/src/analyze/analyze-time.h new file mode 100644 index 0000000..a8f8575 --- /dev/null +++ b/src/analyze/analyze-time.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_time(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-timespan.c b/src/analyze/analyze-timespan.c new file mode 100644 index 0000000..3fdf0f9 --- /dev/null +++ b/src/analyze/analyze-timespan.c @@ -0,0 +1,66 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-timespan.h" +#include "calendarspec.h" +#include "format-table.h" +#include "glyph-util.h" +#include "strv.h" +#include "terminal-util.h" + +int verb_timespan(int argc, char *argv[], void *userdata) { + STRV_FOREACH(input_timespan, strv_skip(argv, 1)) { + _cleanup_(table_unrefp) Table *table = NULL; + usec_t output_usecs; + TableCell *cell; + int r; + + r = parse_time(*input_timespan, &output_usecs, USEC_PER_SEC); + if (r < 0) { + log_error_errno(r, "Failed to parse time span '%s': %m", *input_timespan); + time_parsing_hint(*input_timespan, /* calendar= */ true, /* timestamp= */ true, /* timespan= */ false); + return r; + } + + table = table_new_vertical(); + if (!table) + return log_oom(); + + assert_se(cell = table_get_cell(table, 0, 0)); + r = table_set_ellipsize_percent(table, cell, 100); + if (r < 0) + return r; + + assert_se(cell = table_get_cell(table, 0, 1)); + r = table_set_ellipsize_percent(table, cell, 100); + if (r < 0) + return r; + + r = table_add_many(table, + TABLE_FIELD, "Original", + TABLE_STRING, *input_timespan); + if (r < 0) + return table_log_add_error(r); + + r = table_add_cell_stringf_full(table, NULL, TABLE_FIELD, "%ss", special_glyph(SPECIAL_GLYPH_MU)); + if (r < 0) + return table_log_add_error(r); + + r = table_add_many(table, + TABLE_UINT64, output_usecs, + TABLE_FIELD, "Human", + TABLE_TIMESPAN, output_usecs, + TABLE_SET_COLOR, ansi_highlight()); + if (r < 0) + return table_log_add_error(r); + + r = table_print(table, NULL); + if (r < 0) + return r; + + if (input_timespan[1]) + putchar('\n'); + } + + return 0; +} diff --git a/src/analyze/analyze-timespan.h b/src/analyze/analyze-timespan.h new file mode 100644 index 0000000..46d2295 --- /dev/null +++ b/src/analyze/analyze-timespan.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_timespan(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-timestamp.c b/src/analyze/analyze-timestamp.c new file mode 100644 index 0000000..97de438 --- /dev/null +++ b/src/analyze/analyze-timestamp.c @@ -0,0 +1,90 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-timestamp.h" +#include "format-table.h" +#include "terminal-util.h" + +static int test_timestamp_one(const char *p) { + _cleanup_(table_unrefp) Table *table = NULL; + TableCell *cell; + usec_t usec; + int r; + + r = parse_timestamp(p, &usec); + if (r < 0) { + log_error_errno(r, "Failed to parse \"%s\": %m", p); + time_parsing_hint(p, /* calendar= */ true, /* timestamp= */ false, /* timespan= */ true); + return r; + } + + table = table_new_vertical(); + if (!table) + return log_oom(); + + assert_se(cell = table_get_cell(table, 0, 0)); + r = table_set_ellipsize_percent(table, cell, 100); + if (r < 0) + return r; + + assert_se(cell = table_get_cell(table, 0, 1)); + r = table_set_ellipsize_percent(table, cell, 100); + if (r < 0) + return r; + + r = table_add_many(table, + TABLE_FIELD, "Original form", + TABLE_STRING, p, + TABLE_FIELD, "Normalized form", + TABLE_TIMESTAMP, usec, + TABLE_SET_COLOR, ansi_highlight_blue()); + if (r < 0) + return table_log_add_error(r); + + if (!in_utc_timezone()) { + r = table_add_many(table, + TABLE_FIELD, "(in UTC)", + TABLE_TIMESTAMP_UTC, usec); + if (r < 0) + return table_log_add_error(r); + } + + r = table_add_cell(table, NULL, TABLE_FIELD, "UNIX seconds"); + if (r < 0) + return table_log_add_error(r); + + if (usec % USEC_PER_SEC == 0) + r = table_add_cell_stringf(table, NULL, "@%"PRI_USEC, + usec / USEC_PER_SEC); + else + r = table_add_cell_stringf(table, NULL, "@%"PRI_USEC".%06"PRI_USEC"", + usec / USEC_PER_SEC, + usec % USEC_PER_SEC); + if (r < 0) + return r; + + r = table_add_many(table, + TABLE_FIELD, "From now", + TABLE_TIMESTAMP_RELATIVE, usec); + if (r < 0) + return table_log_add_error(r); + + return table_print(table, NULL); +} + +int verb_timestamp(int argc, char *argv[], void *userdata) { + int r = 0; + + STRV_FOREACH(p, strv_skip(argv, 1)) { + int k; + + k = test_timestamp_one(*p); + if (r == 0 && k < 0) + r = k; + + if (p[1]) + putchar('\n'); + } + + return r; +} diff --git a/src/analyze/analyze-timestamp.h b/src/analyze/analyze-timestamp.h new file mode 100644 index 0000000..43e4b57 --- /dev/null +++ b/src/analyze/analyze-timestamp.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_timestamp(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-unit-files.c b/src/analyze/analyze-unit-files.c new file mode 100644 index 0000000..d9b3313 --- /dev/null +++ b/src/analyze/analyze-unit-files.c @@ -0,0 +1,50 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-unit-files.h" +#include "path-lookup.h" +#include "strv.h" + +static bool strv_fnmatch_strv_or_empty(char* const* patterns, char **strv, int flags) { + STRV_FOREACH(s, strv) + if (strv_fnmatch_or_empty(patterns, *s, flags)) + return true; + + return false; +} + +int verb_unit_files(int argc, char *argv[], void *userdata) { + _cleanup_hashmap_free_ Hashmap *unit_ids = NULL, *unit_names = NULL; + _cleanup_(lookup_paths_free) LookupPaths lp = {}; + char **patterns = strv_skip(argv, 1); + const char *k, *dst; + char **v; + int r; + + r = lookup_paths_init_or_warn(&lp, arg_runtime_scope, 0, NULL); + if (r < 0) + return r; + + r = unit_file_build_name_map(&lp, NULL, &unit_ids, &unit_names, NULL); + if (r < 0) + return log_error_errno(r, "unit_file_build_name_map() failed: %m"); + + HASHMAP_FOREACH_KEY(dst, k, unit_ids) { + if (!strv_fnmatch_or_empty(patterns, k, FNM_NOESCAPE) && + !strv_fnmatch_or_empty(patterns, dst, FNM_NOESCAPE)) + continue; + + printf("ids: %s → %s\n", k, dst); + } + + HASHMAP_FOREACH_KEY(v, k, unit_names) { + if (!strv_fnmatch_or_empty(patterns, k, FNM_NOESCAPE) && + !strv_fnmatch_strv_or_empty(patterns, v, FNM_NOESCAPE)) + continue; + + _cleanup_free_ char *j = strv_join(v, ", "); + printf("aliases: %s ← %s\n", k, j); + } + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-unit-files.h b/src/analyze/analyze-unit-files.h new file mode 100644 index 0000000..c193fd8 --- /dev/null +++ b/src/analyze/analyze-unit-files.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_unit_files(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-unit-paths.c b/src/analyze/analyze-unit-paths.c new file mode 100644 index 0000000..bb00a4f --- /dev/null +++ b/src/analyze/analyze-unit-paths.c @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-unit-paths.h" +#include "path-lookup.h" +#include "strv.h" + +int verb_unit_paths(int argc, char *argv[], void *userdata) { + _cleanup_(lookup_paths_free) LookupPaths paths = {}; + int r; + + r = lookup_paths_init_or_warn(&paths, arg_runtime_scope, 0, NULL); + if (r < 0) + return r; + + STRV_FOREACH(p, paths.search_path) + puts(*p); + + return EXIT_SUCCESS; +} diff --git a/src/analyze/analyze-unit-paths.h b/src/analyze/analyze-unit-paths.h new file mode 100644 index 0000000..b8d46e8 --- /dev/null +++ b/src/analyze/analyze-unit-paths.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_unit_paths(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze-verify-util.c b/src/analyze/analyze-verify-util.c new file mode 100644 index 0000000..6fbd6fa --- /dev/null +++ b/src/analyze/analyze-verify-util.c @@ -0,0 +1,376 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <stdlib.h> + +#include "all-units.h" +#include "alloc-util.h" +#include "analyze-verify-util.h" +#include "bus-error.h" +#include "bus-util.h" +#include "log.h" +#include "manager.h" +#include "pager.h" +#include "path-util.h" +#include "string-table.h" +#include "strv.h" +#include "unit-name.h" +#include "unit-serialize.h" + +static void log_syntax_callback(const char *unit, int level, void *userdata) { + Set **s = ASSERT_PTR(userdata); + int r; + + assert(unit); + + if (level > LOG_WARNING) + return; + + if (*s == POINTER_MAX) + return; + + r = set_put_strdup(s, unit); + if (r < 0) { + set_free_free(*s); + *s = POINTER_MAX; + } +} + +int verify_prepare_filename(const char *filename, char **ret) { + _cleanup_free_ char *abspath = NULL, *name = NULL, *dir = NULL, *with_instance = NULL; + char *c; + int r; + + assert(filename); + assert(ret); + + r = path_make_absolute_cwd(filename, &abspath); + if (r < 0) + return r; + + r = path_extract_filename(abspath, &name); + if (r < 0) + return r; + + if (!unit_name_is_valid(name, UNIT_NAME_ANY)) + return -EINVAL; + + if (unit_name_is_valid(name, UNIT_NAME_TEMPLATE)) { + r = unit_name_replace_instance(name, "i", &with_instance); + if (r < 0) + return r; + } + + r = path_extract_directory(abspath, &dir); + if (r < 0) + return r; + + c = path_join(dir, with_instance ?: name); + if (!c) + return -ENOMEM; + + *ret = c; + return 0; +} + +static int find_unit_directory(const char *p, char **ret) { + _cleanup_free_ char *a = NULL, *u = NULL, *t = NULL, *d = NULL; + int r; + + assert(p); + assert(ret); + + r = path_make_absolute_cwd(p, &a); + if (r < 0) + return r; + + if (access(a, F_OK) >= 0) { + r = path_extract_directory(a, &d); + if (r < 0) + return r; + + *ret = TAKE_PTR(d); + return 0; + } + + r = path_extract_filename(a, &u); + if (r < 0) + return r; + + if (!unit_name_is_valid(u, UNIT_NAME_INSTANCE)) + return -ENOENT; + + /* If the specified unit is an instance of a template unit, then let's try to find the template unit. */ + r = unit_name_template(u, &t); + if (r < 0) + return r; + + r = path_extract_directory(a, &d); + if (r < 0) + return r; + + free(a); + a = path_join(d, t); + if (!a) + return -ENOMEM; + + if (access(a, F_OK) < 0) + return -errno; + + *ret = TAKE_PTR(d); + return 0; +} + +int verify_set_unit_path(char **filenames) { + _cleanup_strv_free_ char **ans = NULL; + _cleanup_free_ char *joined = NULL; + const char *old; + int r; + + STRV_FOREACH(filename, filenames) { + _cleanup_free_ char *t = NULL; + + r = find_unit_directory(*filename, &t); + if (r == -ENOMEM) + return r; + if (r < 0) + continue; + + r = strv_consume(&ans, TAKE_PTR(t)); + if (r < 0) + return r; + } + + if (strv_isempty(ans)) + return 0; + + joined = strv_join(strv_uniq(ans), ":"); + if (!joined) + return -ENOMEM; + + /* First, prepend our directories. Second, if some path was specified, use that, and + * otherwise use the defaults. Any duplicates will be filtered out in path-lookup.c. + * Treat explicit empty path to mean that nothing should be appended. */ + old = getenv("SYSTEMD_UNIT_PATH"); + if (!streq_ptr(old, "") && + !strextend_with_separator(&joined, ":", old ?: "")) + return -ENOMEM; + + assert_se(set_unit_path(joined) >= 0); + return 0; +} + +static int verify_socket(Unit *u) { + Unit *service; + int r; + + assert(u); + + if (u->type != UNIT_SOCKET) + return 0; + + r = socket_load_service_unit(SOCKET(u), -1, &service); + if (r < 0) + return log_unit_error_errno(u, r, "service unit for the socket cannot be loaded: %m"); + + if (service->load_state != UNIT_LOADED) + return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOENT), + "service %s not loaded, socket cannot be started.", service->id); + + log_unit_debug(u, "using service unit %s.", service->id); + return 0; +} + +int verify_executable(Unit *u, const ExecCommand *exec, const char *root) { + int r; + + if (!exec) + return 0; + + if (exec->flags & EXEC_COMMAND_IGNORE_FAILURE) + return 0; + + r = find_executable_full(exec->path, root, NULL, false, NULL, NULL); + if (r < 0) + return log_unit_error_errno(u, r, "Command %s is not executable: %m", exec->path); + + return 0; +} + +static int verify_executables(Unit *u, const char *root) { + int r = 0; + + assert(u); + + ExecCommand *exec = + u->type == UNIT_SOCKET ? SOCKET(u)->control_command : + u->type == UNIT_MOUNT ? MOUNT(u)->control_command : + u->type == UNIT_SWAP ? SWAP(u)->control_command : NULL; + RET_GATHER(r, verify_executable(u, exec, root)); + + if (u->type == UNIT_SERVICE) + FOREACH_ARRAY(i, SERVICE(u)->exec_command, ELEMENTSOF(SERVICE(u)->exec_command)) + RET_GATHER(r, verify_executable(u, *i, root)); + + if (u->type == UNIT_SOCKET) + FOREACH_ARRAY(i, SOCKET(u)->exec_command, ELEMENTSOF(SOCKET(u)->exec_command)) + RET_GATHER(r, verify_executable(u, *i, root)); + + return r; +} + +static int verify_documentation(Unit *u, bool check_man) { + int r = 0, k; + + STRV_FOREACH(p, u->documentation) { + log_unit_debug(u, "Found documentation item: %s", *p); + + if (check_man && startswith(*p, "man:")) { + k = show_man_page(*p + 4, true); + if (k != 0) { + if (k < 0) + log_unit_error_errno(u, k, "Can't show %s: %m", *p + 4); + else { + log_unit_error(u, "Command 'man %s' failed with code %d", *p + 4, k); + k = -ENOEXEC; + } + if (r == 0) + r = k; + } + } + } + + /* Check remote URLs? */ + + return r; +} + +static int verify_unit(Unit *u, bool check_man, const char *root) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(u); + + if (DEBUG_LOGGING) + unit_dump(u, stdout, "\t"); + + log_unit_debug(u, "Creating %s/start job", u->id); + r = manager_add_job(u->manager, JOB_START, u, JOB_REPLACE, NULL, &error, NULL); + if (r < 0) + log_unit_error_errno(u, r, "Failed to create %s/start: %s", u->id, bus_error_message(&error, r)); + + RET_GATHER(r, verify_socket(u)); + RET_GATHER(r, verify_executables(u, root)); + RET_GATHER(r, verify_documentation(u, check_man)); + + return r; +} + +static void set_destroy_ignore_pointer_max(Set **s) { + if (*s == POINTER_MAX) + return; + set_free_free(*s); +} + +int verify_units( + char **filenames, + RuntimeScope scope, + bool check_man, + bool run_generators, + RecursiveErrors recursive_errors, + const char *root) { + + const ManagerTestRunFlags flags = + MANAGER_TEST_RUN_MINIMAL | + MANAGER_TEST_RUN_ENV_GENERATORS | + MANAGER_TEST_DONT_OPEN_EXECUTOR | + (recursive_errors == RECURSIVE_ERRORS_NO) * MANAGER_TEST_RUN_IGNORE_DEPENDENCIES | + run_generators * MANAGER_TEST_RUN_GENERATORS; + + _cleanup_(manager_freep) Manager *m = NULL; + _cleanup_(set_destroy_ignore_pointer_max) Set *s = NULL; + _unused_ _cleanup_(clear_log_syntax_callback) dummy_t dummy; + Unit *units[strv_length(filenames)]; + int r, k, count = 0; + + if (strv_isempty(filenames)) + return 0; + + /* Allow systemd-analyze to hook in a callback function so that it can get + * all the required log data from the function itself without having to rely + * on a global set variable for the same */ + set_log_syntax_callback(log_syntax_callback, &s); + + /* set the path */ + r = verify_set_unit_path(filenames); + if (r < 0) + return log_error_errno(r, "Failed to set unit load path: %m"); + + r = manager_new(scope, flags, &m); + if (r < 0) + return log_error_errno(r, "Failed to initialize manager: %m"); + + log_debug("Starting manager..."); + + r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, root); + if (r < 0) + return r; + + manager_clear_jobs(m); + + log_debug("Loading remaining units from the command line..."); + + STRV_FOREACH(filename, filenames) { + _cleanup_free_ char *prepared = NULL; + + log_debug("Handling %s...", *filename); + + k = verify_prepare_filename(*filename, &prepared); + if (k < 0) { + log_error_errno(k, "Failed to prepare filename %s: %m", *filename); + RET_GATHER(r, k); + continue; + } + + k = manager_load_startable_unit_or_warn(m, NULL, prepared, &units[count]); + if (k < 0) { + RET_GATHER(r, k); + continue; + } + + count++; + } + + FOREACH_ARRAY(i, units, count) + RET_GATHER(r, verify_unit(*i, check_man, root)); + + if (s == POINTER_MAX) + return log_oom(); + + if (set_isempty(s) || r != 0) + return r; + + /* If all previous verifications succeeded, then either the recursive parsing of all the + * associated dependencies with RECURSIVE_ERRORS_YES or the parsing of the specified unit file + * with RECURSIVE_ERRORS_NO must have yielded a syntax warning and hence, a non-empty set. */ + if (IN_SET(recursive_errors, RECURSIVE_ERRORS_YES, RECURSIVE_ERRORS_NO)) + return -ENOTRECOVERABLE; + + /* If all previous verifications succeeded, then the non-empty set could have resulted from + * a syntax warning encountered during the recursive parsing of the specified unit file and + * its direct dependencies. Hence, search for any of the filenames in the set and if found, + * return a non-zero process exit status. */ + if (recursive_errors == RECURSIVE_ERRORS_ONE) + STRV_FOREACH(filename, filenames) + if (set_contains(s, basename(*filename))) + return -ENOTRECOVERABLE; + + return 0; +} + +static const char* const recursive_errors_table[_RECURSIVE_ERRORS_MAX] = { + [RECURSIVE_ERRORS_NO] = "no", + [RECURSIVE_ERRORS_YES] = "yes", + [RECURSIVE_ERRORS_ONE] = "one", +}; + +DEFINE_STRING_TABLE_LOOKUP(recursive_errors, RecursiveErrors); diff --git a/src/analyze/analyze-verify-util.h b/src/analyze/analyze-verify-util.h new file mode 100644 index 0000000..0834c59 --- /dev/null +++ b/src/analyze/analyze-verify-util.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdbool.h> + +#include "execute.h" +#include "path-lookup.h" + +typedef enum RecursiveErrors { + RECURSIVE_ERRORS_YES, /* Look for errors in all associated units */ + RECURSIVE_ERRORS_NO, /* Don't look for errors in any but the selected unit */ + RECURSIVE_ERRORS_ONE, /* Look for errors in the selected unit and its direct dependencies */ + _RECURSIVE_ERRORS_MAX, + _RECURSIVE_ERRORS_INVALID = -EINVAL, +} RecursiveErrors; + +int verify_set_unit_path(char **filenames); +int verify_prepare_filename(const char *filename, char **ret); +int verify_executable(Unit *u, const ExecCommand *exec, const char *root); +int verify_units(char **filenames, RuntimeScope scope, bool check_man, bool run_generators, RecursiveErrors recursive_errors, const char *root); + +const char* recursive_errors_to_string(RecursiveErrors i) _const_; +RecursiveErrors recursive_errors_from_string(const char *s) _pure_; diff --git a/src/analyze/analyze-verify.c b/src/analyze/analyze-verify.c new file mode 100644 index 0000000..3b463f2 --- /dev/null +++ b/src/analyze/analyze-verify.c @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze.h" +#include "analyze-verify.h" +#include "analyze-verify-util.h" +#include "copy.h" +#include "rm-rf.h" +#include "tmpfile-util.h" + +static int process_aliases(char *argv[], char *tempdir, char ***ret) { + _cleanup_strv_free_ char **filenames = NULL; + int r; + + assert(argv); + assert(tempdir); + assert(ret); + + STRV_FOREACH(filename, strv_skip(argv, 1)) { + _cleanup_free_ char *src = NULL, *dst = NULL, *base = NULL; + const char *parse_arg; + + parse_arg = *filename; + r = extract_first_word(&parse_arg, &src, ":", EXTRACT_DONT_COALESCE_SEPARATORS|EXTRACT_RETAIN_ESCAPE); + if (r < 0) + return r; + + if (!parse_arg) { + r = strv_consume(&filenames, TAKE_PTR(src)); + if (r < 0) + return r; + + continue; + } + + r = path_extract_filename(parse_arg, &base); + if (r < 0) + return r; + + dst = path_join(tempdir, base); + if (!dst) + return -ENOMEM; + + r = copy_file(src, dst, 0, 0644, COPY_REFLINK); + if (r < 0) + return r; + + r = strv_consume(&filenames, TAKE_PTR(dst)); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(filenames); + return 0; +} + +int verb_verify(int argc, char *argv[], void *userdata) { + _cleanup_strv_free_ char **filenames = NULL; + _cleanup_(rm_rf_physical_and_freep) char *tempdir = NULL; + int r; + + r = mkdtemp_malloc("/tmp/systemd-analyze-XXXXXX", &tempdir); + if (r < 0) + return log_error_errno(r, "Failed to setup working directory: %m"); + + r = process_aliases(argv, tempdir, &filenames); + if (r < 0) + return log_error_errno(r, "Couldn't process aliases: %m"); + + return verify_units(filenames, arg_runtime_scope, arg_man, arg_generators, arg_recursive_errors, arg_root); +} diff --git a/src/analyze/analyze-verify.h b/src/analyze/analyze-verify.h new file mode 100644 index 0000000..4892c9a --- /dev/null +++ b/src/analyze/analyze-verify.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_verify(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c new file mode 100644 index 0000000..021de65 --- /dev/null +++ b/src/analyze/analyze.c @@ -0,0 +1,691 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2013 Simon Peeters +***/ + +#include <getopt.h> +#include <inttypes.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#include "sd-bus.h" + +#include "alloc-util.h" +#include "analyze.h" +#include "analyze-blame.h" +#include "analyze-calendar.h" +#include "analyze-capability.h" +#include "analyze-cat-config.h" +#include "analyze-compare-versions.h" +#include "analyze-condition.h" +#include "analyze-critical-chain.h" +#include "analyze-dot.h" +#include "analyze-dump.h" +#include "analyze-exit-status.h" +#include "analyze-fdstore.h" +#include "analyze-filesystems.h" +#include "analyze-inspect-elf.h" +#include "analyze-log-control.h" +#include "analyze-malloc.h" +#include "analyze-pcrs.h" +#include "analyze-plot.h" +#include "analyze-security.h" +#include "analyze-service-watchdogs.h" +#include "analyze-srk.h" +#include "analyze-syscall-filter.h" +#include "analyze-time.h" +#include "analyze-time-data.h" +#include "analyze-timespan.h" +#include "analyze-timestamp.h" +#include "analyze-unit-files.h" +#include "analyze-unit-paths.h" +#include "analyze-verify.h" +#include "analyze-image-policy.h" +#include "build.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "bus-map-properties.h" +#include "bus-unit-util.h" +#include "calendarspec.h" +#include "cap-list.h" +#include "capability-util.h" +#include "conf-files.h" +#include "copy.h" +#include "constants.h" +#include "exit-status.h" +#include "extract-word.h" +#include "fd-util.h" +#include "fileio.h" +#include "filesystems.h" +#include "format-table.h" +#include "glob-util.h" +#include "hashmap.h" +#include "locale-util.h" +#include "log.h" +#include "main-func.h" +#include "mount-util.h" +#include "nulstr-util.h" +#include "pager.h" +#include "parse-argument.h" +#include "parse-util.h" +#include "path-util.h" +#include "pretty-print.h" +#include "rm-rf.h" +#if HAVE_SECCOMP +# include "seccomp-util.h" +#endif +#include "sort-util.h" +#include "special.h" +#include "stat-util.h" +#include "string-table.h" +#include "strv.h" +#include "strxcpyx.h" +#include "terminal-util.h" +#include "time-util.h" +#include "tmpfile-util.h" +#include "unit-name.h" +#include "verb-log-control.h" +#include "verbs.h" + +DotMode arg_dot = DEP_ALL; +char **arg_dot_from_patterns = NULL, **arg_dot_to_patterns = NULL; +usec_t arg_fuzz = 0; +PagerFlags arg_pager_flags = 0; +CatFlags arg_cat_flags = 0; +BusTransport arg_transport = BUS_TRANSPORT_LOCAL; +const char *arg_host = NULL; +RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; +RecursiveErrors arg_recursive_errors = _RECURSIVE_ERRORS_INVALID; +bool arg_man = true; +bool arg_generators = false; +char *arg_root = NULL; +static char *arg_image = NULL; +char *arg_security_policy = NULL; +bool arg_offline = false; +unsigned arg_threshold = 100; +unsigned arg_iterations = 1; +usec_t arg_base_time = USEC_INFINITY; +char *arg_unit = NULL; +JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; +bool arg_quiet = false; +char *arg_profile = NULL; +bool arg_legend = true; +bool arg_table = false; +ImagePolicy *arg_image_policy = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_dot_from_patterns, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_dot_to_patterns, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_root, freep); +STATIC_DESTRUCTOR_REGISTER(arg_image, freep); +STATIC_DESTRUCTOR_REGISTER(arg_security_policy, freep); +STATIC_DESTRUCTOR_REGISTER(arg_unit, freep); +STATIC_DESTRUCTOR_REGISTER(arg_profile, freep); +STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); + +int acquire_bus(sd_bus **bus, bool *use_full_bus) { + int r; + + if (use_full_bus && *use_full_bus) { + r = bus_connect_transport(arg_transport, arg_host, arg_runtime_scope, bus); + if (IN_SET(r, 0, -EHOSTDOWN)) + return r; + + *use_full_bus = false; + } + + return bus_connect_transport_systemd(arg_transport, arg_host, arg_runtime_scope, bus); +} + +int bus_get_unit_property_strv(sd_bus *bus, const char *path, const char *property, char ***strv) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(bus); + assert(path); + assert(property); + assert(strv); + + r = sd_bus_get_property_strv( + bus, + "org.freedesktop.systemd1", + path, + "org.freedesktop.systemd1.Unit", + property, + &error, + strv); + if (r < 0) + return log_error_errno(r, "Failed to get unit property %s: %s", property, bus_error_message(&error, r)); + + return 0; +} + +void time_parsing_hint(const char *p, bool calendar, bool timestamp, bool timespan) { + if (calendar && calendar_spec_from_string(p, NULL) >= 0) + log_notice("Hint: this expression is a valid calendar specification. " + "Use 'systemd-analyze calendar \"%s\"' instead?", p); + if (timestamp && parse_timestamp(p, NULL) >= 0) + log_notice("Hint: this expression is a valid timestamp. " + "Use 'systemd-analyze timestamp \"%s\"' instead?", p); + if (timespan && parse_time(p, NULL, USEC_PER_SEC) >= 0) + log_notice("Hint: this expression is a valid timespan. " + "Use 'systemd-analyze timespan \"%s\"' instead?", p); +} + +int dump_fd_reply(sd_bus_message *message) { + int fd, r; + + assert(message); + + r = sd_bus_message_read(message, "h", &fd); + if (r < 0) + return bus_log_parse_error(r); + + fflush(stdout); + r = copy_bytes(fd, STDOUT_FILENO, UINT64_MAX, 0); + if (r < 0) + return r; + + return 1; /* Success */ +} + +static int help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL, *dot_link = NULL; + int r; + + pager_open(arg_pager_flags); + + r = terminal_urlify_man("systemd-analyze", "1", &link); + if (r < 0) + return log_oom(); + + /* Not using terminal_urlify_man() for this, since we don't want the "man page" text suffix in this case. */ + r = terminal_urlify("man:dot(1)", "dot(1)", &dot_link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...] COMMAND ...\n\n" + "%sProfile systemd, show unit dependencies, check unit files.%s\n" + "\nCommands:\n" + " [time] Print time required to boot the machine\n" + " blame Print list of running units ordered by\n" + " time to init\n" + " critical-chain [UNIT...] Print a tree of the time critical chain\n" + " of units\n" + " plot Output SVG graphic showing service\n" + " initialization\n" + " dot [UNIT...] Output dependency graph in %s format\n" + " dump [PATTERN...] Output state serialization of service\n" + " manager\n" + " cat-config NAME|PATH... Show configuration file and drop-ins\n" + " unit-files List files and symlinks for units\n" + " unit-paths List load directories for units\n" + " exit-status [STATUS...] List exit status definitions\n" + " capability [CAP...] List capability definitions\n" + " syscall-filter [NAME...] List syscalls in seccomp filters\n" + " filesystems [NAME...] List known filesystems\n" + " condition CONDITION... Evaluate conditions and asserts\n" + " compare-versions VERSION1 [OP] VERSION2\n" + " Compare two version strings\n" + " verify FILE... Check unit files for correctness\n" + " calendar SPEC... Validate repetitive calendar time\n" + " events\n" + " timestamp TIMESTAMP... Validate a timestamp\n" + " timespan SPAN... Validate a time span\n" + " security [UNIT...] Analyze security of unit\n" + " inspect-elf FILE... Parse and print ELF package metadata\n" + " malloc [D-BUS SERVICE...] Dump malloc stats of a D-Bus service\n" + " fdstore SERVICE... Show file descriptor store contents of service\n" + " image-policy POLICY... Analyze image policy string\n" + " pcrs [PCR...] Show TPM2 PCRs and their names\n" + " srk > FILE Write TPM2 SRK to stdout\n" + "\nOptions:\n" + " --recursive-errors=MODE Control which units are verified\n" + " --offline=BOOL Perform a security review on unit file(s)\n" + " --threshold=N Exit with a non-zero status when overall\n" + " exposure level is over threshold value\n" + " --security-policy=PATH Use custom JSON security policy instead\n" + " of built-in one\n" + " --json=pretty|short|off Generate JSON output of the security\n" + " analysis table, or plot's raw time data\n" + " --no-pager Do not pipe output into a pager\n" + " --no-legend Disable column headers and hints in plot\n" + " with either --table or --json=\n" + " --system Operate on system systemd instance\n" + " --user Operate on user systemd instance\n" + " --global Operate on global user configuration\n" + " -H --host=[USER@]HOST Operate on remote host\n" + " -M --machine=CONTAINER Operate on local container\n" + " --order Show only order in the graph\n" + " --require Show only requirement in the graph\n" + " --from-pattern=GLOB Show only origins in the graph\n" + " --to-pattern=GLOB Show only destinations in the graph\n" + " --fuzz=SECONDS Also print services which finished SECONDS\n" + " earlier than the latest in the branch\n" + " --man[=BOOL] Do [not] check for existence of man pages\n" + " --generators[=BOOL] Do [not] run unit generators\n" + " (requires privileges)\n" + " --iterations=N Show the specified number of iterations\n" + " --base-time=TIMESTAMP Calculate calendar times relative to\n" + " specified time\n" + " --profile=name|PATH Include the specified profile in the\n" + " security review of the unit(s)\n" + " --table Output plot's raw time data as a table\n" + " -h --help Show this help\n" + " --version Show package version\n" + " -q --quiet Do not emit hints\n" + " --tldr Skip comments and empty lines\n" + " --root=PATH Operate on an alternate filesystem root\n" + " --image=PATH Operate on disk image as filesystem root\n" + " --image-policy=POLICY Specify disk image dissection policy\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + dot_link, + link); + + /* When updating this list, including descriptions, apply changes to + * shell-completion/bash/systemd-analyze and shell-completion/zsh/_systemd-analyze too. */ + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + ARG_ORDER, + ARG_REQUIRE, + ARG_ROOT, + ARG_IMAGE, + ARG_IMAGE_POLICY, + ARG_SYSTEM, + ARG_USER, + ARG_GLOBAL, + ARG_DOT_FROM_PATTERN, + ARG_DOT_TO_PATTERN, + ARG_FUZZ, + ARG_NO_PAGER, + ARG_MAN, + ARG_GENERATORS, + ARG_ITERATIONS, + ARG_BASE_TIME, + ARG_RECURSIVE_ERRORS, + ARG_OFFLINE, + ARG_THRESHOLD, + ARG_SECURITY_POLICY, + ARG_JSON, + ARG_PROFILE, + ARG_TABLE, + ARG_NO_LEGEND, + ARG_TLDR, + }; + + 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 }, + { "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 }, + {} + }; + + int r, c; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hH:M:U:q", options, NULL)) >= 0) + switch (c) { + + case 'h': + return help(0, NULL, NULL); + + case ARG_VERSION: + return version(); + + case 'q': + arg_quiet = true; + break; + + case ARG_RECURSIVE_ERRORS: + if (streq(optarg, "help")) { + DUMP_STRING_TABLE(recursive_errors, RecursiveErrors, _RECURSIVE_ERRORS_MAX); + return 0; + } + r = recursive_errors_from_string(optarg); + if (r < 0) + return log_error_errno(r, "Unknown mode passed to --recursive-errors='%s'.", optarg); + + arg_recursive_errors = r; + break; + + case ARG_ROOT: + r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_root); + if (r < 0) + return r; + break; + + case ARG_IMAGE: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image); + if (r < 0) + return r; + break; + + case ARG_IMAGE_POLICY: + r = parse_image_policy_argument(optarg, &arg_image_policy); + if (r < 0) + return r; + break; + + case ARG_SYSTEM: + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; + + case ARG_USER: + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + + case ARG_GLOBAL: + arg_runtime_scope = RUNTIME_SCOPE_GLOBAL; + break; + + case ARG_ORDER: + arg_dot = DEP_ORDER; + break; + + case ARG_REQUIRE: + arg_dot = DEP_REQUIRE; + break; + + case ARG_DOT_FROM_PATTERN: + if (strv_extend(&arg_dot_from_patterns, optarg) < 0) + return log_oom(); + + break; + + case ARG_DOT_TO_PATTERN: + if (strv_extend(&arg_dot_to_patterns, optarg) < 0) + return log_oom(); + + break; + + case ARG_FUZZ: + r = parse_sec(optarg, &arg_fuzz); + if (r < 0) + return r; + break; + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case 'H': + arg_transport = BUS_TRANSPORT_REMOTE; + arg_host = optarg; + break; + + case 'M': + arg_transport = BUS_TRANSPORT_MACHINE; + arg_host = optarg; + break; + + case ARG_MAN: + r = parse_boolean_argument("--man", optarg, &arg_man); + if (r < 0) + return r; + break; + + case ARG_GENERATORS: + r = parse_boolean_argument("--generators", optarg, &arg_generators); + if (r < 0) + return r; + break; + + case ARG_OFFLINE: + r = parse_boolean_argument("--offline", optarg, &arg_offline); + if (r < 0) + return r; + break; + + case ARG_THRESHOLD: + r = safe_atou(optarg, &arg_threshold); + if (r < 0 || arg_threshold > 100) + return log_error_errno(r < 0 ? r : SYNTHETIC_ERRNO(EINVAL), "Failed to parse threshold: %s", optarg); + + break; + + case ARG_SECURITY_POLICY: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_security_policy); + if (r < 0) + return r; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + break; + + case ARG_ITERATIONS: + r = safe_atou(optarg, &arg_iterations); + if (r < 0) + return log_error_errno(r, "Failed to parse iterations: %s", optarg); + break; + + case ARG_BASE_TIME: + r = parse_timestamp(optarg, &arg_base_time); + if (r < 0) + return log_error_errno(r, "Failed to parse --base-time= parameter: %s", optarg); + break; + + case ARG_PROFILE: + if (isempty(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Profile file name is empty"); + + if (is_path(optarg)) { + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_profile); + if (r < 0) + return r; + if (!endswith(arg_profile, ".conf")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Profile file name must end with .conf: %s", arg_profile); + } else { + r = free_and_strdup(&arg_profile, optarg); + if (r < 0) + return log_oom(); + } + + break; + + case 'U': { + _cleanup_free_ char *mangled = NULL; + + r = unit_name_mangle(optarg, UNIT_NAME_MANGLE_WARN, &mangled); + if (r < 0) + return log_error_errno(r, "Failed to mangle unit name %s: %m", optarg); + + free_and_replace(arg_unit, mangled); + break; + } + + case ARG_TABLE: + arg_table = true; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + break; + + case ARG_TLDR: + arg_cat_flags = CAT_TLDR; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (arg_offline && !streq_ptr(argv[optind], "security")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Option --offline= is only supported for security right now."); + + if (arg_json_format_flags != JSON_FORMAT_OFF && !STRPTR_IN_SET(argv[optind], "security", "inspect-elf", "plot", "fdstore", "pcrs")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Option --json= is only supported for security, inspect-elf, plot, fdstore, pcrs right now."); + + if (arg_threshold != 100 && !streq_ptr(argv[optind], "security")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Option --threshold= is only supported for security right now."); + + if (arg_runtime_scope == RUNTIME_SCOPE_GLOBAL && + !STR_IN_SET(argv[optind] ?: "time", "dot", "unit-paths", "verify")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Option --global only makes sense with verbs dot, unit-paths, verify."); + + if (streq_ptr(argv[optind], "cat-config") && arg_runtime_scope == RUNTIME_SCOPE_USER) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Option --user is not supported for cat-config right now."); + + if (arg_security_policy && !streq_ptr(argv[optind], "security")) + 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")) && + (!(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."); + + /* Having both an image and a root is not supported by the code */ + if (arg_root && arg_image) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported."); + + if (arg_unit && !streq_ptr(argv[optind], "condition")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --unit= is only supported for condition"); + + if (streq_ptr(argv[optind], "condition") && !arg_unit && optind >= argc - 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too few arguments for condition"); + + if (streq_ptr(argv[optind], "condition") && arg_unit && optind < argc - 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No conditions can be passed if --unit= is used."); + + if ((!arg_legend && !streq_ptr(argv[optind], "plot")) || + (streq_ptr(argv[optind], "plot") && !arg_legend && !arg_table && FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF))) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --no-legend is only supported for plot with either --table or --json=."); + + if (arg_table && !streq_ptr(argv[optind], "plot")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --table is only supported for plot right now."); + + if (arg_table && !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--table and --json= are mutually exclusive."); + + return 1; /* work to do */ +} + +static int run(int argc, char *argv[]) { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(umount_and_freep) char *mounted_dir = NULL; + + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "time", VERB_ANY, 1, VERB_DEFAULT, verb_time }, + { "blame", VERB_ANY, 1, 0, verb_blame }, + { "critical-chain", VERB_ANY, VERB_ANY, 0, verb_critical_chain }, + { "plot", VERB_ANY, 1, 0, verb_plot }, + { "dot", VERB_ANY, VERB_ANY, 0, verb_dot }, + /* ↓ The following seven verbs are deprecated, from here … ↓ */ + { "log-level", VERB_ANY, 2, 0, verb_log_control }, + { "log-target", VERB_ANY, 2, 0, verb_log_control }, + { "set-log-level", 2, 2, 0, verb_log_control }, + { "get-log-level", VERB_ANY, 1, 0, verb_log_control }, + { "set-log-target", 2, 2, 0, verb_log_control }, + { "get-log-target", VERB_ANY, 1, 0, verb_log_control }, + { "service-watchdogs", VERB_ANY, 2, 0, verb_service_watchdogs }, + /* ↑ … until here ↑ */ + { "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-paths", 1, 1, 0, verb_unit_paths }, + { "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 }, + { "filesystems", VERB_ANY, VERB_ANY, 0, verb_filesystems }, + { "condition", VERB_ANY, VERB_ANY, 0, verb_condition }, + { "compare-versions", 3, 4, 0, verb_compare_versions }, + { "verify", 2, VERB_ANY, 0, verb_verify }, + { "calendar", 2, VERB_ANY, 0, verb_calendar }, + { "timestamp", 2, VERB_ANY, 0, verb_timestamp }, + { "timespan", 2, VERB_ANY, 0, verb_timespan }, + { "security", VERB_ANY, VERB_ANY, 0, verb_security }, + { "inspect-elf", 2, VERB_ANY, 0, verb_elf_inspection }, + { "malloc", VERB_ANY, VERB_ANY, 0, verb_malloc }, + { "fdstore", 2, VERB_ANY, 0, verb_fdstore }, + { "image-policy", 2, 2, 0, verb_image_policy }, + { "pcrs", VERB_ANY, VERB_ANY, 0, verb_pcrs }, + { "srk", VERB_ANY, 1, 0, verb_srk }, + {} + }; + + int r; + + setlocale(LC_ALL, ""); + setlocale(LC_NUMERIC, "C"); /* we want to format/parse floats in C style */ + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + /* Open up and mount the image */ + if (arg_image) { + assert(!arg_root); + + r = mount_image_privately_interactively( + arg_image, + arg_image_policy, + DISSECT_IMAGE_GENERIC_ROOT | + DISSECT_IMAGE_RELAX_VAR_CHECK | + DISSECT_IMAGE_READ_ONLY, + &mounted_dir, + /* ret_dir_fd= */ NULL, + &loop_device); + if (r < 0) + return r; + + arg_root = strdup(mounted_dir); + if (!arg_root) + return log_oom(); + } + + return dispatch_verb(argc, argv, verbs, NULL); +} + +DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/src/analyze/analyze.h b/src/analyze/analyze.h new file mode 100644 index 0000000..8a9528c --- /dev/null +++ b/src/analyze/analyze.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdbool.h> + +#include "analyze-verify-util.h" +#include "bus-util.h" +#include "json.h" +#include "pager.h" +#include "pretty-print.h" +#include "time-util.h" +#include "unit-file.h" + +typedef enum DotMode { + DEP_ALL, + DEP_ORDER, + DEP_REQUIRE, +} DotMode; + +extern DotMode arg_dot; +extern char **arg_dot_from_patterns, **arg_dot_to_patterns; +extern usec_t arg_fuzz; +extern PagerFlags arg_pager_flags; +extern CatFlags arg_cat_flags; +extern BusTransport arg_transport; +extern const char *arg_host; +extern RuntimeScope arg_runtime_scope; +extern RecursiveErrors arg_recursive_errors; +extern bool arg_man; +extern bool arg_generators; +extern char *arg_root; +extern char *arg_security_policy; +extern bool arg_offline; +extern unsigned arg_threshold; +extern unsigned arg_iterations; +extern usec_t arg_base_time; +extern char *arg_unit; +extern JsonFormatFlags arg_json_format_flags; +extern bool arg_quiet; +extern char *arg_profile; +extern bool arg_legend; +extern bool arg_table; +extern ImagePolicy *arg_image_policy; + +int acquire_bus(sd_bus **bus, bool *use_full_bus); + +int bus_get_unit_property_strv(sd_bus *bus, const char *path, const char *property, char ***strv); + +void time_parsing_hint(const char *p, bool calendar, bool timestamp, bool timespan); + +int dump_fd_reply(sd_bus_message *message); diff --git a/src/analyze/meson.build b/src/analyze/meson.build new file mode 100644 index 0000000..a505447 --- /dev/null +++ b/src/analyze/meson.build @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +systemd_analyze_sources = files( + 'analyze-blame.c', + 'analyze-calendar.c', + 'analyze-capability.c', + 'analyze-cat-config.c', + 'analyze-compare-versions.c', + 'analyze-condition.c', + 'analyze-critical-chain.c', + 'analyze-dot.c', + 'analyze-dump.c', + 'analyze-exit-status.c', + 'analyze-fdstore.c', + 'analyze-filesystems.c', + 'analyze-image-policy.c', + 'analyze-inspect-elf.c', + 'analyze-log-control.c', + 'analyze-malloc.c', + 'analyze-pcrs.c', + 'analyze-plot.c', + 'analyze-security.c', + 'analyze-service-watchdogs.c', + 'analyze-srk.c', + 'analyze-syscall-filter.c', + 'analyze-time.c', + 'analyze-time-data.c', + 'analyze-timespan.c', + 'analyze-timestamp.c', + 'analyze-unit-files.c', + 'analyze-unit-paths.c', + 'analyze-verify.c', + 'analyze-verify-util.c', + 'analyze.c', +) + +executables += [ + executable_template + { + 'name' : 'systemd-analyze', + 'public' : conf.get('ENABLE_ANALYZE') == 1, + 'sources' : systemd_analyze_sources, + 'include_directories' : core_includes, + 'link_with' : [ + libcore, + libshared, + ], + 'dependencies' : libseccomp, + 'install' : conf.get('ENABLE_ANALYZE') == 1, + }, + core_test_template + { + 'sources' : files( + 'test-verify.c', + 'analyze-verify-util.c', + ), + }, +] diff --git a/src/analyze/test-verify.c b/src/analyze/test-verify.c new file mode 100644 index 0000000..d37e54b --- /dev/null +++ b/src/analyze/test-verify.c @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "analyze-verify-util.h" +#include "tests.h" + +TEST(verify_nonexistent) { + /* Negative cases */ + assert_se(verify_executable(NULL, &(ExecCommand) {.flags = EXEC_COMMAND_IGNORE_FAILURE, .path = (char*) "/non/existent"}, NULL) == 0); + assert_se(verify_executable(NULL, &(ExecCommand) {.path = (char*) "/non/existent"}, NULL) < 0); + + /* Ordinary cases */ + assert_se(verify_executable(NULL, &(ExecCommand) {.path = (char*) "/bin/echo"}, NULL) == 0); + assert_se(verify_executable(NULL, &(ExecCommand) {.flags = EXEC_COMMAND_IGNORE_FAILURE, .path = (char*) "/bin/echo"}, NULL) == 0); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); |