]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
add new systemd-bless-boot.service that marks boots as successful
authorLennart Poettering <lennart@poettering.net>
Mon, 25 Jun 2018 15:24:09 +0000 (17:24 +0200)
committerLennart Poettering <lennart@poettering.net>
Fri, 19 Oct 2018 20:34:50 +0000 (22:34 +0200)
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
src/basic/fs-util.c
src/basic/fs-util.h
src/boot/bless-boot.c [new file with mode: 0644]
units/meson.build
units/systemd-bless-boot.service.in [new file with mode: 0644]

index a47d7f9370a3c00c906bf1b6833d09b6668b7fe3..35d0968b7e11133ab7883866b7eb96f69c49b9aa 100644 (file)
@@ -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',
index 3d83fc9b1006a84c2e51053ca4486454caf3199d..e1628ddba0852e0b915b4ceb5e0979304b138432 100644 (file)
@@ -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;
index bc753d5920699b84558e24aa526e4f2e897a51a9..955b146a6a4c62fd67c8312763b656fa2b1e3a36 100644 (file)
@@ -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 (file)
index 0000000..84ac9e3
--- /dev/null
@@ -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;
+}
index 70eabe522725337955a6cbb9687bbe6b8157c766..26a725b57561f70d0cd1bdf42db577cea3727eec 100644 (file)
@@ -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 (file)
index 0000000..511d991
--- /dev/null
@@ -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