a0fa268337
Fixup for 484f4e5b2d
. Should fix #15730.
346 lines
9.9 KiB
C
346 lines
9.9 KiB
C
/* SPDX-License-Identifier: LGPL-2.1+ */
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <linux/fs.h>
|
|
#include <stdlib.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include "sd-id128.h"
|
|
|
|
#include "alloc-util.h"
|
|
#include "chattr-util.h"
|
|
#include "efivars.h"
|
|
#include "fd-util.h"
|
|
#include "io-util.h"
|
|
#include "macro.h"
|
|
#include "stdio-util.h"
|
|
#include "strv.h"
|
|
#include "time-util.h"
|
|
#include "utf8.h"
|
|
#include "virt.h"
|
|
|
|
#if ENABLE_EFI
|
|
|
|
/* Reads from efivarfs sometimes fail with EINTR. Retry that many times. */
|
|
#define EFI_N_RETRIES 5
|
|
#define EFI_RETRY_DELAY (50 * USEC_PER_MSEC)
|
|
|
|
char* efi_variable_path(sd_id128_t vendor, const char *name) {
|
|
char *p;
|
|
|
|
if (asprintf(&p,
|
|
"/sys/firmware/efi/efivars/%s-" SD_ID128_UUID_FORMAT_STR,
|
|
name, SD_ID128_FORMAT_VAL(vendor)) < 0)
|
|
return NULL;
|
|
|
|
return p;
|
|
}
|
|
|
|
int efi_get_variable(
|
|
sd_id128_t vendor,
|
|
const char *name,
|
|
uint32_t *ret_attribute,
|
|
void **ret_value,
|
|
size_t *ret_size) {
|
|
|
|
_cleanup_close_ int fd = -1;
|
|
_cleanup_free_ char *p = NULL;
|
|
_cleanup_free_ void *buf = NULL;
|
|
struct stat st;
|
|
uint32_t a;
|
|
ssize_t n;
|
|
|
|
assert(name);
|
|
|
|
p = efi_variable_path(vendor, name);
|
|
if (!p)
|
|
return -ENOMEM;
|
|
|
|
if (!ret_value && !ret_size && !ret_attribute) {
|
|
/* If caller is not interested in anything, just check if the variable exists and is
|
|
* readable. */
|
|
if (access(p, R_OK) < 0)
|
|
return -errno;
|
|
|
|
return 0;
|
|
}
|
|
|
|
fd = open(p, O_RDONLY|O_NOCTTY|O_CLOEXEC);
|
|
if (fd < 0)
|
|
return log_debug_errno(errno, "open(\"%s\") failed: %m", p);
|
|
|
|
if (fstat(fd, &st) < 0)
|
|
return -errno;
|
|
if (st.st_size < 4)
|
|
return -ENODATA;
|
|
if (st.st_size > 4*1024*1024 + 4)
|
|
return -E2BIG;
|
|
|
|
if (ret_value || ret_attribute) {
|
|
/* The kernel ratelimits reads from the efivarfs because EFI is inefficient, and we'll
|
|
* occasionally fail with EINTR here. A slowdown is better than a failure for us, so
|
|
* retry a few times and eventually fail with -EBUSY.
|
|
*
|
|
* See https://github.com/torvalds/linux/blob/master/fs/efivarfs/file.c#L75
|
|
* and
|
|
* https://github.com/torvalds/linux/commit/bef3efbeb897b56867e271cdbc5f8adaacaeb9cd.
|
|
*/
|
|
for (unsigned try = 0;; try++) {
|
|
n = read(fd, &a, sizeof(a));
|
|
if (n >= 0)
|
|
break;
|
|
log_debug_errno(errno, "read from \"%s\" failed: %m", p);
|
|
if (errno != EINTR)
|
|
return -errno;
|
|
if (try >= EFI_N_RETRIES)
|
|
return -EBUSY;
|
|
usleep(EFI_RETRY_DELAY);
|
|
}
|
|
|
|
if (n != sizeof(a))
|
|
return -EIO;
|
|
}
|
|
|
|
if (ret_value) {
|
|
buf = malloc(st.st_size - 4 + 2);
|
|
if (!buf)
|
|
return -ENOMEM;
|
|
|
|
n = read(fd, buf, (size_t) st.st_size - 4);
|
|
if (n < 0)
|
|
return -errno;
|
|
assert(n <= st.st_size - 4);
|
|
|
|
/* Always NUL terminate (2 bytes, to protect UTF-16) */
|
|
((char*) buf)[n] = 0;
|
|
((char*) buf)[n + 1] = 0;
|
|
} else
|
|
/* Assume that the reported size is accurate */
|
|
n = st.st_size - 4;
|
|
|
|
/* Note that efivarfs interestingly doesn't require ftruncate() to update an existing EFI variable
|
|
* with a smaller value. */
|
|
|
|
if (ret_attribute)
|
|
*ret_attribute = a;
|
|
|
|
if (ret_value)
|
|
*ret_value = TAKE_PTR(buf);
|
|
|
|
if (ret_size)
|
|
*ret_size = n;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int efi_get_variable_string(sd_id128_t vendor, const char *name, char **p) {
|
|
_cleanup_free_ void *s = NULL;
|
|
size_t ss = 0;
|
|
int r;
|
|
char *x;
|
|
|
|
r = efi_get_variable(vendor, name, NULL, &s, &ss);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
x = utf16_to_utf8(s, ss);
|
|
if (!x)
|
|
return -ENOMEM;
|
|
|
|
*p = x;
|
|
return 0;
|
|
}
|
|
|
|
int efi_set_variable(
|
|
sd_id128_t vendor,
|
|
const char *name,
|
|
const void *value,
|
|
size_t size) {
|
|
|
|
struct var {
|
|
uint32_t attr;
|
|
char buf[];
|
|
} _packed_ * _cleanup_free_ buf = NULL;
|
|
_cleanup_free_ char *p = NULL;
|
|
_cleanup_close_ int fd = -1;
|
|
bool saved_flags_valid = false;
|
|
unsigned saved_flags;
|
|
int r;
|
|
|
|
assert(name);
|
|
assert(value || size == 0);
|
|
|
|
p = efi_variable_path(vendor, name);
|
|
if (!p)
|
|
return -ENOMEM;
|
|
|
|
/* Newer efivarfs protects variables that are not in a whitelist with FS_IMMUTABLE_FL by default, to protect
|
|
* them for accidental removal and modification. We are not changing these variables accidentally however,
|
|
* hence let's unset the bit first. */
|
|
|
|
r = chattr_path(p, 0, FS_IMMUTABLE_FL, &saved_flags);
|
|
if (r < 0 && r != -ENOENT)
|
|
log_debug_errno(r, "Failed to drop FS_IMMUTABLE_FL flag from '%s', ignoring: %m", p);
|
|
|
|
saved_flags_valid = r >= 0;
|
|
|
|
if (size == 0) {
|
|
if (unlink(p) < 0) {
|
|
r = -errno;
|
|
goto finish;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
fd = open(p, O_WRONLY|O_CREAT|O_NOCTTY|O_CLOEXEC, 0644);
|
|
if (fd < 0) {
|
|
r = -errno;
|
|
goto finish;
|
|
}
|
|
|
|
buf = malloc(sizeof(uint32_t) + size);
|
|
if (!buf) {
|
|
r = -ENOMEM;
|
|
goto finish;
|
|
}
|
|
|
|
buf->attr = EFI_VARIABLE_NON_VOLATILE|EFI_VARIABLE_BOOTSERVICE_ACCESS|EFI_VARIABLE_RUNTIME_ACCESS;
|
|
memcpy(buf->buf, value, size);
|
|
|
|
r = loop_write(fd, buf, sizeof(uint32_t) + size, false);
|
|
if (r < 0)
|
|
goto finish;
|
|
|
|
r = 0;
|
|
|
|
finish:
|
|
if (saved_flags_valid) {
|
|
int q;
|
|
|
|
/* Restore the original flags field, just in case */
|
|
if (fd < 0)
|
|
q = chattr_path(p, saved_flags, FS_IMMUTABLE_FL, NULL);
|
|
else
|
|
q = chattr_fd(fd, saved_flags, FS_IMMUTABLE_FL, NULL);
|
|
if (q < 0)
|
|
log_debug_errno(q, "Failed to restore FS_IMMUTABLE_FL on '%s', ignoring: %m", p);
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
int efi_set_variable_string(sd_id128_t vendor, const char *name, const char *v) {
|
|
_cleanup_free_ char16_t *u16 = NULL;
|
|
|
|
u16 = utf8_to_utf16(v, strlen(v));
|
|
if (!u16)
|
|
return -ENOMEM;
|
|
|
|
return efi_set_variable(vendor, name, u16, (char16_strlen(u16) + 1) * sizeof(char16_t));
|
|
}
|
|
|
|
bool is_efi_boot(void) {
|
|
static int cache = -1;
|
|
|
|
if (cache < 0) {
|
|
if (detect_container() > 0)
|
|
cache = false;
|
|
else
|
|
cache = access("/sys/firmware/efi/", F_OK) >= 0;
|
|
}
|
|
|
|
return cache;
|
|
}
|
|
|
|
static int read_flag(const char *varname) {
|
|
_cleanup_free_ void *v = NULL;
|
|
uint8_t b;
|
|
size_t s;
|
|
int r;
|
|
|
|
if (!is_efi_boot()) /* If this is not an EFI boot, assume the queried flags are zero */
|
|
return 0;
|
|
|
|
r = efi_get_variable(EFI_VENDOR_GLOBAL, varname, NULL, &v, &s);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
if (s != 1)
|
|
return -EINVAL;
|
|
|
|
b = *(uint8_t *)v;
|
|
return !!b;
|
|
}
|
|
|
|
bool is_efi_secure_boot(void) {
|
|
static int cache = -1;
|
|
|
|
if (cache < 0)
|
|
cache = read_flag("SecureBoot");
|
|
|
|
return cache > 0;
|
|
}
|
|
|
|
bool is_efi_secure_boot_setup_mode(void) {
|
|
static int cache = -1;
|
|
|
|
if (cache < 0)
|
|
cache = read_flag("SetupMode");
|
|
|
|
return cache > 0;
|
|
}
|
|
|
|
int systemd_efi_options_variable(char **line) {
|
|
const char *e;
|
|
int r;
|
|
|
|
assert(line);
|
|
|
|
/* For testing purposes it is sometimes useful to be able to override this */
|
|
e = secure_getenv("SYSTEMD_EFI_OPTIONS");
|
|
if (e) {
|
|
char *m;
|
|
|
|
m = strdup(e);
|
|
if (!m)
|
|
return -ENOMEM;
|
|
|
|
*line = m;
|
|
return 0;
|
|
}
|
|
|
|
/* In SecureBoot mode this is probably not what you want. As your cmdline is cryptographically signed
|
|
* like when using Type #2 EFI Unified Kernel Images (https://systemd.io/BOOT_LOADER_SPECIFICATION/)
|
|
* The user's intention is then that the cmdline should not be modified. You want to make sure that
|
|
* the system starts up as exactly specified in the signed artifact.
|
|
*
|
|
* (NB: to make testing purposes we still check the $SYSTEMD_EFI_OPTIONS env var above, even when in
|
|
* SecureBoot mode.) */
|
|
if (is_efi_secure_boot()) {
|
|
_cleanup_free_ char *k;
|
|
|
|
k = efi_variable_path(EFI_VENDOR_SYSTEMD, "SystemdOptions");
|
|
if (!k)
|
|
return -ENOMEM;
|
|
|
|
/* Let's be helpful with the returned error and check if the variable exists at all. If it
|
|
* does, let's return a recognizable error (EPERM), and if not ENODATA. */
|
|
|
|
if (access(k, F_OK) < 0)
|
|
return errno == ENOENT ? -ENODATA : -errno;
|
|
|
|
return -EPERM;
|
|
}
|
|
|
|
r = efi_get_variable_string(EFI_VENDOR_SYSTEMD, "SystemdOptions", line);
|
|
if (r == -ENOENT)
|
|
return -ENODATA;
|
|
|
|
return r;
|
|
}
|
|
#endif
|