diff options
Diffstat (limited to 'src/boot/bless-boot.c')
-rw-r--r-- | src/boot/bless-boot.c | 476 |
1 files changed, 476 insertions, 0 deletions
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 <getopt.h> +#include <stdlib.h> + +#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; +} |