If KExecsCount > 0 then do not attempt to mark a UKI, as the stub (and also any addon and the bootloader) will not have been exercised, so there's no guaranteed a full reboot with the same image will actually work. Fixes https://github.com/systemd/systemd/issues/43738
606 lines
21 KiB
C
606 lines
21 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
|
|
#include <unistd.h>
|
|
|
|
#include "sd-json.h"
|
|
#include "sd-varlink.h"
|
|
|
|
#include "alloc-util.h"
|
|
#include "build.h"
|
|
#include "devnum-util.h"
|
|
#include "dlopen-note.h"
|
|
#include "efivars.h"
|
|
#include "fd-util.h"
|
|
#include "find-esp.h"
|
|
#include "fs-util.h"
|
|
#include "log.h"
|
|
#include "main-func.h"
|
|
#include "parse-util.h"
|
|
#include "path-util.h"
|
|
#include "stdio-util.h"
|
|
#include "string-util.h"
|
|
#include "strv.h"
|
|
#include "sync-util.h"
|
|
#include "varlink-util.h"
|
|
#include "verbs.h"
|
|
#include "virt.h"
|
|
|
|
static char **arg_path = NULL;
|
|
|
|
STATIC_DESTRUCTOR_REGISTER(arg_path, strv_freep);
|
|
|
|
COMMAND(
|
|
"systemd-bless-boot\0",
|
|
"Mark the boot process as good or bad.",
|
|
.man_pages = "systemd-bless-boot.service(8)\0",
|
|
);
|
|
|
|
typedef enum Status {
|
|
STATUS_GOOD,
|
|
STATUS_BAD,
|
|
STATUS_INDETERMINATE,
|
|
} Status;
|
|
|
|
VERB_COMMON_HELP_AUTO_HIDDEN();
|
|
|
|
static int check_support(void) {
|
|
if (detect_container() > 0)
|
|
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
|
|
"Marking a boot is not supported in containers.");
|
|
|
|
if (!is_efi_boot())
|
|
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
|
|
"Marking a boot is only supported on EFI systems.");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int parse_argv(int argc, char *argv[], char ***ret_args) {
|
|
int r;
|
|
|
|
assert(argc >= 0);
|
|
assert(argv);
|
|
assert(ret_args);
|
|
|
|
OptionParser opts = { argc, argv };
|
|
|
|
FOREACH_OPTION_OR_RETURN(c, &opts)
|
|
switch (c) {
|
|
OPTION_COMMON_HELP:
|
|
return command_print_help();
|
|
|
|
OPTION_COMMON_VERSION:
|
|
return version();
|
|
|
|
OPTION_LONG("path", "PATH", "Path to the $BOOT partition (may be used multiple times)"):
|
|
r = strv_extend(&arg_path, opts.arg);
|
|
if (r < 0)
|
|
return log_oom();
|
|
break;
|
|
|
|
OPTION_COMMON_INTROSPECT_CLI:
|
|
return introspect_cli(SD_JSON_FORMAT_OFF);
|
|
}
|
|
|
|
*ret_args = option_parser_get_args(&opts);
|
|
return 1;
|
|
}
|
|
|
|
static int acquire_path(void) {
|
|
_cleanup_free_ char *esp_path = NULL, *xbootldr_path = NULL;
|
|
dev_t esp_devid = 0, xbootldr_devid = 0;
|
|
char **a;
|
|
int r;
|
|
|
|
if (!strv_isempty(arg_path))
|
|
return 0;
|
|
|
|
r = find_esp_and_warn_full(
|
|
/* root= */ NULL,
|
|
/* path= */ NULL,
|
|
/* unprivileged_mode= */ false,
|
|
&esp_path,
|
|
/* ret_fd= */ NULL,
|
|
/* ret_part= */ NULL,
|
|
/* ret_pstart= */ NULL,
|
|
/* ret_psize= */ NULL,
|
|
/* ret_uuid= */ NULL,
|
|
&esp_devid);
|
|
if (r < 0 && r != -ENOKEY) /* ENOKEY means not found, and is the only error the function won't log about on its own */
|
|
return r;
|
|
|
|
r = find_xbootldr_and_warn_full(
|
|
/* root= */ NULL,
|
|
/* path= */ NULL,
|
|
/* unprivileged_mode= */ false,
|
|
&xbootldr_path,
|
|
/* ret_fd= */ NULL,
|
|
/* ret_uuid= */ NULL,
|
|
&xbootldr_devid);
|
|
if (r < 0 && r != -ENOKEY)
|
|
return r;
|
|
|
|
if (!esp_path && !xbootldr_path)
|
|
return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
|
|
"Couldn't find $BOOT partition. It is recommended to mount it to /boot.\n"
|
|
"Alternatively, use --path= to specify path to mount point.");
|
|
|
|
if (esp_path && xbootldr_path && !devnum_set_and_equal(esp_devid, xbootldr_devid)) /* in case the two paths refer to the same inode, suppress one */
|
|
a = strv_new(esp_path, xbootldr_path);
|
|
else if (esp_path)
|
|
a = strv_new(esp_path);
|
|
else
|
|
a = strv_new(xbootldr_path);
|
|
if (!a)
|
|
return log_oom();
|
|
|
|
strv_free_and_replace(arg_path, a);
|
|
|
|
if (DEBUG_LOGGING) {
|
|
_cleanup_free_ char *j = NULL;
|
|
|
|
j = strv_join(arg_path, ":");
|
|
log_debug("Using %s as boot loader drop-in search path.", strna(j));
|
|
}
|
|
|
|
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)
|
|
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
|
"Can't parse empty 'tries left' counter from LoaderBootCountPath: %s",
|
|
path);
|
|
|
|
z = strndupa_safe(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 */
|
|
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
|
"Can't parse empty 'tries done' counter from LoaderBootCountPath: %s",
|
|
path);
|
|
|
|
z = strndupa_safe(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) {
|
|
|
|
int r;
|
|
|
|
_cleanup_free_ char *path = NULL;
|
|
r = efi_get_variable_path(EFI_LOADER_VARIABLE_STR("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");
|
|
|
|
if (!path_is_normalized(path))
|
|
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
|
"Path read from LoaderBootCountPath is not normalized, refusing: %s",
|
|
path);
|
|
|
|
if (!path_is_absolute(path))
|
|
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
|
"Path read from LoaderBootCountPath is not absolute, refusing: %s",
|
|
path);
|
|
|
|
const char *last = NULL;
|
|
r = path_find_last_component(path, /* accept_dot_dot= */ false, /* next= */ NULL, &last);
|
|
if (r < 0)
|
|
return log_error_errno(r, "Failed to extract filename from LoaderBootCountPath '%s': %m", path);
|
|
if (r == 0)
|
|
return log_error_errno(SYNTHETIC_ERRNO(EADDRNOTAVAIL), "LoaderBootCountPath '%s' refers to the root directory.", path);
|
|
if (strlen(last) > (size_t) r)
|
|
return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "LoaderBootCountPath '%s' refers to directory path, refusing.", path);
|
|
|
|
const char *e = strrchr(last, '+');
|
|
if (!e)
|
|
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
|
"Path read from LoaderBootCountPath does not contain a counter, refusing: %s",
|
|
path);
|
|
|
|
_cleanup_free_ char *prefix = NULL;
|
|
if (ret_prefix) {
|
|
prefix = strndup(path, e - path);
|
|
if (!prefix)
|
|
return log_oom();
|
|
}
|
|
|
|
uint64_t left, done;
|
|
r = parse_counter(path, &e, &left, &done);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
_cleanup_free_ char *suffix = NULL;
|
|
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) {
|
|
char *bad;
|
|
|
|
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);
|
|
else
|
|
bad = asprintf_safe("%s+0-%" PRIu64 "%s", prefix, done, suffix);
|
|
if (!bad)
|
|
return -ENOMEM;
|
|
|
|
*ret = bad;
|
|
return 0;
|
|
}
|
|
|
|
VERB_DEFAULT_NOARG(verb_status, "status", "Show status of current boot loader entry");
|
|
static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) {
|
|
_cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL, *good = NULL, *bad = NULL;
|
|
uint64_t left, done;
|
|
int r;
|
|
|
|
r = check_support();
|
|
if (r < 0)
|
|
return 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,
|
|
* since "good", "bad", or "indeterminate" don't apply. */
|
|
puts("clean");
|
|
return 0;
|
|
}
|
|
if (r < 0)
|
|
return r;
|
|
|
|
r = acquire_path();
|
|
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\n"
|
|
"The same modified for 'good': %s\n"
|
|
"The same modified for 'bad': %s\n",
|
|
path,
|
|
good,
|
|
bad);
|
|
|
|
log_debug("Tries left: %" PRIu64"\n"
|
|
"Tries done: %" PRIu64"\n",
|
|
left, done);
|
|
|
|
STRV_FOREACH(p, arg_path) {
|
|
_cleanup_close_ int fd = -EBADF;
|
|
|
|
fd = open(*p, O_DIRECTORY|O_CLOEXEC|O_RDONLY);
|
|
if (fd < 0) {
|
|
if (errno == ENOENT)
|
|
continue;
|
|
|
|
return log_error_errno(errno, "Failed to open $BOOT partition '%s': %m", *p);
|
|
}
|
|
|
|
if (faccessat(fd, skip_leading_slash(path), F_OK, 0) >= 0) {
|
|
/* If the item we booted with still exists under its name, it means we have not
|
|
* change the current boot's marking so far. This may have two reasons: because we
|
|
* simply didn't do that yet but still plan to, or because the left tries counter is
|
|
* already at zero, hence we cannot further decrease it to mark it even
|
|
* "worse"... Here we check the current counter to detect the latter case and return
|
|
* "dirty", since the item is already marked bad from a previous boot, but otherwise
|
|
* report "indeterminate" since we just didn't make a decision yet. */
|
|
puts(left == 0 ? "dirty" : "indeterminate");
|
|
return 0;
|
|
}
|
|
if (errno != ENOENT)
|
|
return log_error_errno(errno, "Failed to check if '%s' exists: %m", path);
|
|
|
|
if (faccessat(fd, skip_leading_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_leading_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);
|
|
|
|
/* We didn't find any of the three? If so, let's try the next directory, before we give up. */
|
|
}
|
|
|
|
return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Couldn't determine boot state.");
|
|
}
|
|
|
|
static int rename_in_dir_idempotent(int fd, const char *from, const char *to) {
|
|
int r;
|
|
|
|
assert(fd >= 0);
|
|
assert(from);
|
|
assert(to);
|
|
|
|
/* A wrapper around rename_noreplace() which executes no operation if the source and target are the
|
|
* same. */
|
|
|
|
if (streq(from, to)) {
|
|
if (faccessat(fd, from, F_OK, AT_SYMLINK_NOFOLLOW) < 0)
|
|
return -errno;
|
|
|
|
return 0;
|
|
}
|
|
|
|
r = rename_noreplace(fd, from, fd, to);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
return 1;
|
|
}
|
|
|
|
VERB_FULL(verb_set, "good", NULL, VERB_ANY, 1, 0, STATUS_GOOD,
|
|
"Mark this boot as good");
|
|
VERB_FULL(verb_set, "bad", NULL, VERB_ANY, 1, 0, STATUS_BAD,
|
|
"Mark this boot as bad");
|
|
VERB_FULL(verb_set, "indeterminate", NULL, VERB_ANY, 1, 0, STATUS_INDETERMINATE,
|
|
"Undo any marking as good or bad");
|
|
static int verb_set(int argc, char *argv[], uintptr_t data, void *userdata) {
|
|
_cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL, *good = NULL, *bad = NULL;
|
|
_cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL;
|
|
sd_json_variant *reply = NULL;
|
|
const char *target = NULL, *source1 = NULL, *source2 = NULL; /* avoid false maybe-uninitialized warning */
|
|
uint64_t left, done;
|
|
Status status = data;
|
|
int r;
|
|
|
|
assert(IN_SET(status, STATUS_GOOD, STATUS_BAD, STATUS_INDETERMINATE));
|
|
|
|
r = check_support();
|
|
if (r < 0)
|
|
return r;
|
|
|
|
/* kexec doesn't go through the EFI loaders, so we shouldn't try to mark an image as good/bad.
|
|
* E.g.: booting the kernel+initrd might work, but the EFI stub might not, and we wouldn't notice */
|
|
r = sd_varlink_connect_address(&link, "/run/systemd/io.systemd.Manager");
|
|
if (r < 0)
|
|
return log_error_errno(r, "Failed to connect to service manager: %m");
|
|
|
|
r = varlink_call_and_log(link, "io.systemd.Manager.Describe", /* parameters= */ NULL, &reply);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
static const sd_json_dispatch_field dispatch_table[] = {
|
|
{ "KExecCount", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, 0, 0 },
|
|
{},
|
|
};
|
|
uint64_t kexec_count = 0;
|
|
|
|
r = sd_json_dispatch(
|
|
sd_json_variant_by_key(reply, "runtime"),
|
|
dispatch_table,
|
|
SD_JSON_LOG|SD_JSON_ALLOW_EXTENSIONS,
|
|
&kexec_count);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
if (kexec_count > 0) {
|
|
log_info("System was booted via kexec, skipping boot entry marking.");
|
|
return 0;
|
|
}
|
|
|
|
r = acquire_boot_count_path(&path, &prefix, &left, &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_path();
|
|
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();
|
|
|
|
/* Figure out what rename to what */
|
|
switch (status) {
|
|
case STATUS_GOOD:
|
|
target = good;
|
|
source1 = path;
|
|
source2 = bad; /* Maybe this boot was previously marked as 'bad'? */
|
|
break;
|
|
case STATUS_BAD:
|
|
target = bad;
|
|
source1 = path;
|
|
source2 = good; /* Maybe this boot was previously marked as 'good'? */
|
|
break;
|
|
case STATUS_INDETERMINATE:
|
|
if (left == 0)
|
|
return log_error_errno(r, "Current boot entry was already marked bad in a previous boot, cannot reset to indeterminate.");
|
|
|
|
target = path;
|
|
source1 = good;
|
|
source2 = bad;
|
|
break;
|
|
}
|
|
|
|
STRV_FOREACH(p, arg_path) {
|
|
_cleanup_close_ int fd = -EBADF;
|
|
|
|
fd = open(*p, O_DIRECTORY|O_CLOEXEC|O_RDONLY);
|
|
if (fd < 0)
|
|
return log_error_errno(errno, "Failed to open $BOOT partition '%s': %m", *p);
|
|
|
|
r = rename_in_dir_idempotent(fd, skip_leading_slash(source1), skip_leading_slash(target));
|
|
if (r == -EEXIST)
|
|
goto exists;
|
|
if (r == -ENOENT) {
|
|
|
|
r = rename_in_dir_idempotent(fd, skip_leading_slash(source2), skip_leading_slash(target));
|
|
if (r == -EEXIST)
|
|
goto exists;
|
|
if (r == -ENOENT) {
|
|
|
|
if (faccessat(fd, skip_leading_slash(target), F_OK, 0) >= 0) /* Hmm, if we can't find either source file, maybe the destination already exists? */
|
|
goto exists;
|
|
|
|
if (errno != ENOENT)
|
|
return log_error_errno(errno, "Failed to determine if %s already exists: %m", target);
|
|
|
|
/* We found none of the snippets here, try the next directory */
|
|
continue;
|
|
}
|
|
if (r < 0)
|
|
return log_error_errno(r, "Failed to rename '%s' to '%s': %m", source2, target);
|
|
|
|
if (r > 0)
|
|
log_debug("Successfully renamed '%s' to '%s'.", source2, target);
|
|
else
|
|
log_debug("Not renaming, as '%s' already matches target name.", source2);
|
|
} else if (r < 0)
|
|
return log_error_errno(r, "Failed to rename '%s' to '%s': %m", source1, target);
|
|
else if (r > 0)
|
|
log_debug("Successfully renamed '%s' to '%s'.", source1, target);
|
|
else
|
|
log_debug("Not renaming, as '%s' already matches target name.", source1);
|
|
|
|
/* First, fsync() the directory these files are located in */
|
|
r = fsync_parent_at(fd, skip_leading_slash(target));
|
|
if (r < 0)
|
|
log_debug_errno(r, "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 $BOOT partition, ignoring: %m");
|
|
|
|
log_info("Marked boot as '%s'. (Boot attempt counter is at %" PRIu64".)", argv[0], done);
|
|
return 0;
|
|
}
|
|
|
|
return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Can't find boot counter source file for '%s'.", target);
|
|
|
|
exists:
|
|
log_debug("Operation already executed before, not doing anything.");
|
|
return 0;
|
|
}
|
|
|
|
static int run(int argc, char *argv[]) {
|
|
char **args = NULL;
|
|
int r;
|
|
|
|
LIBBLKID_NOTE(recommended);
|
|
|
|
log_setup();
|
|
|
|
r = parse_argv(argc, argv, &args);
|
|
if (r <= 0)
|
|
return r;
|
|
|
|
return dispatch_verb(args, NULL);
|
|
}
|
|
|
|
DEFINE_MAIN_FUNCTION(run);
|