From 36695e880aff9c1525ed4efd5e42df1c62932a80 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 25 Jun 2018 17:24:09 +0200 Subject: [PATCH] add new systemd-bless-boot.service that marks boots as successful This is the counterpiece to the boot counting implemented in systemd-boot: if a boot is detected as successful we mark drop the counter again from the booted snippet or kernel image. --- meson.build | 9 + src/basic/fs-util.c | 28 ++ src/basic/fs-util.h | 1 + src/boot/bless-boot.c | 476 ++++++++++++++++++++++++++++ units/meson.build | 1 + units/systemd-bless-boot.service.in | 22 ++ 6 files changed, 537 insertions(+) create mode 100644 src/boot/bless-boot.c create mode 100644 units/systemd-bless-boot.service.in diff --git a/meson.build b/meson.build index a47d7f9370..35d0968b7e 100644 --- a/meson.build +++ b/meson.build @@ -1796,6 +1796,15 @@ if conf.get('ENABLE_EFI') == 1 and conf.get('HAVE_BLKID') == 1 install_rpath : rootlibexecdir, install : true) public_programs += exe + + executable('systemd-bless-boot', + 'src/boot/bless-boot.c', + include_directories : includes, + link_with : [libshared], + dependencies : [libblkid], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootlibexecdir) endif exe = executable('systemd-socket-activate', 'src/activate/activate.c', diff --git a/src/basic/fs-util.c b/src/basic/fs-util.c index 3d83fc9b10..e1628ddba0 100644 --- a/src/basic/fs-util.c +++ b/src/basic/fs-util.c @@ -1235,6 +1235,34 @@ int fsync_directory_of_file(int fd) { return 0; } +int fsync_path_at(int at_fd, const char *path) { + _cleanup_close_ int opened_fd = -1; + int fd; + + if (isempty(path)) { + if (at_fd == AT_FDCWD) { + opened_fd = open(".", O_RDONLY|O_DIRECTORY|O_CLOEXEC); + if (opened_fd < 0) + return -errno; + + fd = opened_fd; + } else + fd = at_fd; + } else { + + opened_fd = openat(at_fd, path, O_RDONLY|O_CLOEXEC); + if (opened_fd < 0) + return -errno; + + fd = opened_fd; + } + + if (fsync(fd) < 0) + return -errno; + + return 0; +} + int open_parent(const char *path, int flags, mode_t mode) { _cleanup_free_ char *parent = NULL; int fd; diff --git a/src/basic/fs-util.h b/src/basic/fs-util.h index bc753d5920..955b146a6a 100644 --- a/src/basic/fs-util.h +++ b/src/basic/fs-util.h @@ -105,5 +105,6 @@ void unlink_tempfilep(char (*p)[]); int unlinkat_deallocate(int fd, const char *name, int flags); int fsync_directory_of_file(int fd); +int fsync_path_at(int at_fd, const char *path); int open_parent(const char *path, int flags, mode_t mode); diff --git a/src/boot/bless-boot.c b/src/boot/bless-boot.c new file mode 100644 index 0000000000..84ac9e39e4 --- /dev/null +++ b/src/boot/bless-boot.c @@ -0,0 +1,476 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "alloc-util.h" +#include "bootspec.h" +#include "efivars.h" +#include "fd-util.h" +#include "fs-util.h" +#include "log.h" +#include "parse-util.h" +#include "path-util.h" +#include "util.h" +#include "verbs.h" +#include "virt.h" + +static char *arg_path = NULL; + +static int help(int argc, char *argv[], void *userdata) { + + printf("%s [COMMAND] [OPTIONS...]\n" + "\n" + "Mark the boot process as good or bad.\n\n" + " -h --help Show this help\n" + " --version Print version\n" + " --path=PATH Path to the EFI System Partition (ESP)\n" + "\n" + "Commands:\n" + " good Mark this boot as good\n" + " bad Mark this boot as bad\n" + " indeterminate Undo any marking as good or bad\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_PATH = 0x100, + ARG_VERSION, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "path", required_argument, NULL, ARG_PATH }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + switch (c) { + + case 'h': + help(0, NULL, NULL); + return 0; + + case ARG_VERSION: + return version(); + + case ARG_PATH: + r = free_and_strdup(&arg_path, optarg); + if (r < 0) + return log_oom(); + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached("Unknown option"); + } + + return 1; +} + +static int acquire_esp(void) { + _cleanup_free_ char *np = NULL; + int r; + + r = find_esp_and_warn(arg_path, false, &np, NULL, NULL, NULL, NULL); + if (r == -ENOKEY) /* find_esp_and_warn() doesn't warn in this one error case, but in all others */ + return log_error_errno(r, + "Couldn't find EFI system partition. It is recommended to mount it to /boot or /efi.\n" + "Alternatively, use --path= to specify path to mount point."); + if (r < 0) + return r; + + free_and_replace(arg_path, np); + log_debug("Using EFI System Partition at %s.", arg_path); + + return 0; +} + +static int parse_counter( + const char *path, + const char **p, + uint64_t *ret_left, + uint64_t *ret_done) { + + uint64_t left, done; + const char *z, *e; + size_t k; + int r; + + assert(path); + assert(p); + + e = *p; + assert(e); + assert(*e == '+'); + + e++; + + k = strspn(e, DIGITS); + if (k == 0) { + log_error("Can't parse empty 'tries left' counter from LoaderBootCountPath: %s", path); + return -EINVAL; + } + + z = strndupa(e, k); + r = safe_atou64(z, &left); + if (r < 0) + return log_error_errno(r, "Failed to parse 'tries left' counter from LoaderBootCountPath: %s", path); + + e += k; + + if (*e == '-') { + e++; + + k = strspn(e, DIGITS); + if (k == 0) { /* If there's a "-" there also needs to be at least one digit */ + log_error("Can't parse empty 'tries done' counter from LoaderBootCountPath: %s", path); + return -EINVAL; + } + + z = strndupa(e, k); + r = safe_atou64(z, &done); + if (r < 0) + return log_error_errno(r, "Failed to parse 'tries done' counter from LoaderBootCountPath: %s", path); + + e += k; + } else + done = 0; + + if (done == 0) + log_warning("The 'tries done' counter is currently at zero. This can't really be, after all we are running, and this boot must hence count as one. Proceeding anyway."); + + *p = e; + + if (ret_left) + *ret_left = left; + + if (ret_done) + *ret_done = done; + + return 0; +} + +static int acquire_boot_count_path( + char **ret_path, + char **ret_prefix, + uint64_t *ret_left, + uint64_t *ret_done, + char **ret_suffix) { + + _cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL; + const char *last, *e; + uint64_t left, done; + int r; + + r = efi_get_variable_string(EFI_VENDOR_LOADER, "LoaderBootCountPath", &path); + if (r == -ENOENT) + return -EUNATCH; /* in this case, let the caller print a message */ + if (r < 0) + return log_error_errno(r, "Failed to read LoaderBootCountPath EFI variable: %m"); + + efi_tilt_backslashes(path); + + if (!path_is_normalized(path)) { + log_error("Path read from LoaderBootCountPath is not normalized, refusing: %s", path); + return -EINVAL; + } + + if (!path_is_absolute(path)) { + log_error("Path read from LoaderBootCountPath is not absolute, refusing: %s", path); + return -EINVAL; + } + + last = last_path_component(path); + e = strrchr(last, '+'); + if (!e) { + log_error("Path read from LoaderBootCountPath does not contain a counter, refusing: %s", path); + return -EINVAL; + } + + if (ret_prefix) { + prefix = strndup(path, e - path); + if (!prefix) + return log_oom(); + } + + r = parse_counter(path, &e, &left, &done); + if (r < 0) + return r; + + if (ret_suffix) { + suffix = strdup(e); + if (!suffix) + return log_oom(); + + *ret_suffix = TAKE_PTR(suffix); + } + + if (ret_path) + *ret_path = TAKE_PTR(path); + if (ret_prefix) + *ret_prefix = TAKE_PTR(prefix); + if (ret_left) + *ret_left = left; + if (ret_done) + *ret_done = done; + + return 0; +} + +static int make_good(const char *prefix, const char *suffix, char **ret) { + _cleanup_free_ char *good = NULL; + + assert(prefix); + assert(suffix); + assert(ret); + + /* Generate the path we'd use on good boots. This one is easy. If we are successful, we simple drop the counter + * pair entirely from the name. After all, we know all is good, and the logs will contain information about the + * tries we needed to come here, hence it's safe to drop the counters from the name. */ + + good = strjoin(prefix, suffix); + if (!good) + return -ENOMEM; + + *ret = TAKE_PTR(good); + return 0; +} + +static int make_bad(const char *prefix, uint64_t done, const char *suffix, char **ret) { + _cleanup_free_ char *bad = NULL; + + assert(prefix); + assert(suffix); + assert(ret); + + /* Generate the path we'd use on bad boots. Let's simply set the 'left' counter to zero, and keep the 'done' + * counter. The information might be interesting to boot loaders, after all. */ + + if (done == 0) { + bad = strjoin(prefix, "+0", suffix); + if (!bad) + return -ENOMEM; + } else { + if (asprintf(&bad, "%s+0-%" PRIu64 "%s", prefix, done, suffix) < 0) + return -ENOMEM; + } + + *ret = TAKE_PTR(bad); + return 0; +} + +static const char *skip_slash(const char *path) { + assert(path); + assert(path[0] == '/'); + + return path + 1; +} + +static int verb_status(int argc, char *argv[], void *userdata) { + + _cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL, *good = NULL, *bad = NULL; + _cleanup_close_ int fd = -1; + uint64_t left, done; + int r; + + r = acquire_boot_count_path(&path, &prefix, &left, &done, &suffix); + if (r == -EUNATCH) { /* No boot count in place, then let's consider this a "clean" boot, as "good", "bad" or "indeterminate" don't apply. */ + puts("clean"); + return 0; + } + if (r < 0) + return r; + + r = acquire_esp(); + if (r < 0) + return r; + + r = make_good(prefix, suffix, &good); + if (r < 0) + return log_oom(); + + r = make_bad(prefix, done, suffix, &bad); + if (r < 0) + return log_oom(); + + log_debug("Booted file: %s%s\n" + "The same modified for 'good': %s%s\n" + "The same modified for 'bad': %s%s\n", + arg_path, path, + arg_path, good, + arg_path, bad); + + log_debug("Tries left: %" PRIu64"\n" + "Tries done: %" PRIu64"\n", + left, done); + + fd = open(arg_path, O_DIRECTORY|O_CLOEXEC|O_RDONLY); + if (fd < 0) + return log_error_errno(errno, "Failed to open ESP '%s': %m", arg_path); + + if (faccessat(fd, skip_slash(path), F_OK, 0) >= 0) { + puts("indeterminate"); + return 0; + } + if (errno != ENOENT) + return log_error_errno(errno, "Failed to check if '%s' exists: %m", path); + + if (faccessat(fd, skip_slash(good), F_OK, 0) >= 0) { + puts("good"); + return 0; + } + if (errno != ENOENT) + return log_error_errno(errno, "Failed to check if '%s' exists: %m", good); + + if (faccessat(fd, skip_slash(bad), F_OK, 0) >= 0) { + puts("bad"); + return 0; + } + if (errno != ENOENT) + return log_error_errno(errno, "Failed to check if '%s' exists: %m", bad); + + return log_error_errno(errno, "Couldn't determine boot state: %m"); +} + +static int verb_set(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL, *good = NULL, *bad = NULL, *parent = NULL; + const char *target, *source1, *source2; + _cleanup_close_ int fd = -1; + uint64_t done; + int r; + + r = acquire_boot_count_path(&path, &prefix, NULL, &done, &suffix); + if (r == -EUNATCH) /* acquire_boot_count_path() won't log on its own for this specific error */ + return log_error_errno(r, "Not booted with boot counting in effect."); + if (r < 0) + return r; + + r = acquire_esp(); + if (r < 0) + return r; + + r = make_good(prefix, suffix, &good); + if (r < 0) + return log_oom(); + + r = make_bad(prefix, done, suffix, &bad); + if (r < 0) + return log_oom(); + + fd = open(arg_path, O_DIRECTORY|O_CLOEXEC|O_RDONLY); + if (fd < 0) + return log_error_errno(errno, "Failed to open ESP '%s': %m", arg_path); + + /* Figure out what rename to what */ + if (streq(argv[0], "good")) { + target = good; + source1 = path; + source2 = bad; /* Maybe this boot was previously marked as 'bad'? */ + } else if (streq(argv[0], "bad")) { + target = bad; + source1 = path; + source2 = good; /* Maybe this boot was previously marked as 'good'? */ + } else { + assert(streq(argv[0], "indeterminate")); + target = path; + source1 = good; + source2 = bad; + } + + r = rename_noreplace(fd, skip_slash(source1), fd, skip_slash(target)); + if (r == -EEXIST) + goto exists; + else if (r == -ENOENT) { + + r = rename_noreplace(fd, skip_slash(source2), fd, skip_slash(target)); + if (r == -EEXIST) + goto exists; + else if (r == -ENOENT) { + + if (access(target, F_OK) >= 0) /* Hmm, if we can't find either source file, maybe the destination already exists? */ + goto exists; + + return log_error_errno(r, "Can't find boot counter source file for '%s': %m", target); + } else if (r < 0) + return log_error_errno(r, "Failed to rename '%s' to '%s': %m", source2, target); + else + log_debug("Successfully renamed '%s' to '%s'.", source2, target); + + } else if (r < 0) + return log_error_errno(r, "Failed to rename '%s' to '%s': %m", source1, target); + else + log_debug("Successfully renamed '%s' to '%s'.", source1, target); + + /* First, fsync() the directory these files are located in */ + parent = dirname_malloc(path); + if (!parent) + return log_oom(); + + r = fsync_path_at(fd, skip_slash(parent)); + if (r < 0) + log_debug_errno(errno, "Failed to synchronize image directory, ignoring: %m"); + + /* Secondly, syncfs() the whole file system these files are located in */ + if (syncfs(fd) < 0) + log_debug_errno(errno, "Failed to synchronize ESP, ignoring: %m"); + + log_info("Marked boot as '%s'. (Boot attempt counter is at %" PRIu64".)", argv[0], done); + + return 1; + +exists: + log_debug("Operation already executed before, not doing anything."); + return 0; +} + +int main(int argc, char *argv[]) { + + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status }, + { "good", VERB_ANY, 1, VERB_MUST_BE_ROOT, verb_set }, + { "bad", VERB_ANY, 1, VERB_MUST_BE_ROOT, verb_set }, + { "indeterminate", VERB_ANY, 1, VERB_MUST_BE_ROOT, verb_set }, + {} + }; + + int r; + + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + goto finish; + + if (detect_container() > 0) { + log_error("Marking a boot is not supported in containers."); + r = -EOPNOTSUPP; + goto finish; + } + + if (!is_efi_boot()) { + log_error("Marking a boot is only supported on EFI systems."); + r = -EOPNOTSUPP; + goto finish; + } + + r = dispatch_verb(argc, argv, verbs, NULL); + +finish: + free(arg_path); + + return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/units/meson.build b/units/meson.build index 70eabe5227..26a725b575 100644 --- a/units/meson.build +++ b/units/meson.build @@ -136,6 +136,7 @@ in_units = [ ['systemd-backlight@.service', 'ENABLE_BACKLIGHT'], ['systemd-binfmt.service', 'ENABLE_BINFMT', 'sysinit.target.wants/'], + ['systemd-bless-boot.service', 'ENABLE_EFI HAVE_BLKID'], ['systemd-coredump@.service', 'ENABLE_COREDUMP'], ['systemd-firstboot.service', 'ENABLE_FIRSTBOOT', 'sysinit.target.wants/'], diff --git a/units/systemd-bless-boot.service.in b/units/systemd-bless-boot.service.in new file mode 100644 index 0000000000..511d991d3b --- /dev/null +++ b/units/systemd-bless-boot.service.in @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1+ +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Mark the Current Boot Loader Entry as Good +Documentation=man:systemd-bless-boot.service(8) +DefaultDependencies=no +Requires=boot-complete.target +After=local-fs.target boot-complete.target +Conflicts=shutdown.target +Before=shutdown.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=@rootlibexecdir@/systemd-bless-boot good