summaryrefslogtreecommitdiffstats
path: root/src/hibernate-resume
diff options
context:
space:
mode:
Diffstat (limited to 'src/hibernate-resume')
-rw-r--r--src/hibernate-resume/hibernate-resume-config.c266
-rw-r--r--src/hibernate-resume/hibernate-resume-config.h55
-rw-r--r--src/hibernate-resume/hibernate-resume-generator.c130
-rw-r--r--src/hibernate-resume/hibernate-resume.c83
-rw-r--r--src/hibernate-resume/meson.build20
5 files changed, 554 insertions, 0 deletions
diff --git a/src/hibernate-resume/hibernate-resume-config.c b/src/hibernate-resume/hibernate-resume-config.c
new file mode 100644
index 0000000..e4be7ca
--- /dev/null
+++ b/src/hibernate-resume/hibernate-resume-config.c
@@ -0,0 +1,266 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "device-nodes.h"
+#include "fstab-util.h"
+#include "hibernate-resume-config.h"
+#include "json.h"
+#include "os-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "proc-cmdline.h"
+#include "efivars.h"
+
+static KernelHibernateLocation* kernel_hibernate_location_free(KernelHibernateLocation *k) {
+ if (!k)
+ return NULL;
+
+ free(k->device);
+
+ return mfree(k);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(KernelHibernateLocation*, kernel_hibernate_location_free);
+
+static EFIHibernateLocation* efi_hibernate_location_free(EFIHibernateLocation *e) {
+ if (!e)
+ return NULL;
+
+ free(e->device);
+
+ free(e->kernel_version);
+ free(e->id);
+ free(e->image_id);
+ free(e->image_version);
+
+ return mfree(e);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(EFIHibernateLocation*, efi_hibernate_location_free);
+
+void hibernate_info_done(HibernateInfo *info) {
+ assert(info);
+
+ kernel_hibernate_location_free(info->cmdline);
+ efi_hibernate_location_free(info->efi);
+}
+
+static int parse_proc_cmdline_item(const char *key, const char *value, void *data) {
+ KernelHibernateLocation *k = ASSERT_PTR(data);
+ int r;
+
+ assert(key);
+
+ if (streq(key, "resume")) {
+ _cleanup_free_ char *d = NULL;
+
+ if (proc_cmdline_value_missing(key, value))
+ return 0;
+
+ d = fstab_node_to_udev_node(value);
+ if (!d)
+ return log_oom();
+
+ free_and_replace(k->device, d);
+
+ } else if (proc_cmdline_key_streq(key, "resume_offset")) {
+
+ if (proc_cmdline_value_missing(key, value))
+ return 0;
+
+ r = safe_atou64(value, &k->offset);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse resume_offset=%s: %m", value);
+
+ k->offset_set = true;
+ }
+
+ return 0;
+}
+
+static int get_kernel_hibernate_location(KernelHibernateLocation **ret) {
+ _cleanup_(kernel_hibernate_location_freep) KernelHibernateLocation *k = NULL;
+ int r;
+
+ assert(ret);
+
+ k = new0(KernelHibernateLocation, 1);
+ if (!k)
+ return log_oom();
+
+ r = proc_cmdline_parse(parse_proc_cmdline_item, k, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse kernel command line: %m");
+
+ if (!k->device) {
+ if (k->offset_set)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Found resume_offset=%" PRIu64 " but resume= is unset, refusing.",
+ k->offset);
+
+ *ret = NULL;
+ return 0;
+ }
+
+ *ret = TAKE_PTR(k);
+ return 1;
+}
+
+#if ENABLE_EFI
+static bool validate_efi_hibernate_location(EFIHibernateLocation *e) {
+ _cleanup_free_ char *id = NULL, *image_id = NULL;
+ int r;
+
+ assert(e);
+
+ r = parse_os_release(NULL,
+ "ID", &id,
+ "IMAGE_ID", &image_id);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse os-release: %m");
+
+ if (!streq_ptr(id, e->id) ||
+ !streq_ptr(image_id, e->image_id)) {
+ log_notice("HibernateLocation system identifier doesn't match currently running system, not resuming from it.");
+ return false;
+ }
+
+ /*
+ * Note that we accept kernel version mismatches. Linux writes the old kernel to disk as part of the
+ * hibernation image, and thus resuming means the short-lived kernel that reads the image from the
+ * disk will be replaced by the original kernel and effectively removed from memory as part of that.
+ */
+
+ return true;
+}
+
+static int get_efi_hibernate_location(EFIHibernateLocation **ret) {
+
+ static const JsonDispatch dispatch_table[] = {
+ { "uuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(EFIHibernateLocation, uuid), JSON_MANDATORY },
+ { "offset", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(EFIHibernateLocation, offset), JSON_MANDATORY },
+ { "kernelVersion", JSON_VARIANT_STRING, json_dispatch_string, offsetof(EFIHibernateLocation, kernel_version), JSON_PERMISSIVE|JSON_DEBUG },
+ { "osReleaseId", JSON_VARIANT_STRING, json_dispatch_string, offsetof(EFIHibernateLocation, id), JSON_PERMISSIVE|JSON_DEBUG },
+ { "osReleaseImageId", JSON_VARIANT_STRING, json_dispatch_string, offsetof(EFIHibernateLocation, image_id), JSON_PERMISSIVE|JSON_DEBUG },
+ { "osReleaseVersionId", JSON_VARIANT_STRING, json_dispatch_string, offsetof(EFIHibernateLocation, version_id), JSON_PERMISSIVE|JSON_DEBUG },
+ { "osReleaseImageVersion", JSON_VARIANT_STRING, json_dispatch_string, offsetof(EFIHibernateLocation, image_version), JSON_PERMISSIVE|JSON_DEBUG },
+ {},
+ };
+
+ _cleanup_(efi_hibernate_location_freep) EFIHibernateLocation *e = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ char *location_str = NULL;
+ int r;
+
+ assert(ret);
+
+ if (!is_efi_boot())
+ goto skip;
+
+ r = efi_get_variable_string(EFI_SYSTEMD_VARIABLE(HibernateLocation), &location_str);
+ if (r == -ENOENT) {
+ log_debug_errno(r, "EFI variable HibernateLocation is not set, skipping.");
+ goto skip;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to get EFI variable HibernateLocation: %m");
+
+ r = json_parse(location_str, 0, &v, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse HibernateLocation JSON object: %m");
+
+ e = new0(EFIHibernateLocation, 1);
+ if (!e)
+ return log_oom();
+
+ r = json_dispatch(v, dispatch_table, JSON_LOG, e);
+ if (r < 0)
+ return r;
+
+ log_info("Reported hibernation image:%s%s%s%s%s%s%s%s%s%s UUID="SD_ID128_UUID_FORMAT_STR" offset=%"PRIu64,
+ e->id ? " ID=" : "", strempty(e->id),
+ e->image_id ? " IMAGE_ID=" : "", strempty(e->image_id),
+ e->version_id ? " VERSION_ID=" : "", strempty(e->version_id),
+ e->image_version ? " IMAGE_VERSION=" : "", strempty(e->image_version),
+ e->kernel_version ? " kernel=" : "", strempty(e->kernel_version),
+ SD_ID128_FORMAT_VAL(e->uuid),
+ e->offset);
+
+ if (!validate_efi_hibernate_location(e))
+ goto skip;
+
+ if (asprintf(&e->device, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(e->uuid)) < 0)
+ return log_oom();
+
+ *ret = TAKE_PTR(e);
+ return 1;
+
+skip:
+ *ret = NULL;
+ return 0;
+}
+
+void compare_hibernate_location_and_warn(const HibernateInfo *info) {
+ int r;
+
+ assert(info);
+
+ if (!info->cmdline || !info->efi)
+ return;
+
+ assert(info->device == info->cmdline->device);
+
+ if (!path_equal(info->cmdline->device, info->efi->device)) {
+ r = devnode_same(info->cmdline->device, info->efi->device);
+ if (r < 0)
+ log_warning_errno(r,
+ "Failed to check if resume=%s is the same device as EFI HibernateLocation device '%s', ignoring: %m",
+ info->cmdline->device, info->efi->device);
+ if (r == 0)
+ log_warning("resume=%s doesn't match with EFI HibernateLocation device '%s', proceeding anyway with resume=.",
+ info->cmdline->device, info->efi->device);
+ }
+
+ if (info->cmdline->offset != info->efi->offset)
+ log_warning("resume_offset=%" PRIu64 " doesn't match with EFI HibernateLocation offset %" PRIu64 ", proceeding anyway with resume_offset=.",
+ info->cmdline->offset, info->efi->offset);
+}
+
+void clear_efi_hibernate_location(void) {
+ int r;
+
+ if (!is_efi_boot())
+ return;
+
+ r = efi_set_variable(EFI_SYSTEMD_VARIABLE(HibernateLocation), NULL, 0);
+ if (r < 0)
+ log_warning_errno(r, "Failed to clear EFI variable HibernateLocation, ignoring: %m");
+}
+#endif
+
+int acquire_hibernate_info(HibernateInfo *ret) {
+ _cleanup_(hibernate_info_done) HibernateInfo i = {};
+ int r;
+
+ r = get_kernel_hibernate_location(&i.cmdline);
+ if (r < 0)
+ return r;
+
+#if ENABLE_EFI
+ r = get_efi_hibernate_location(&i.efi);
+ if (r < 0)
+ return r;
+#endif
+
+ if (i.cmdline) {
+ i.device = i.cmdline->device;
+ i.offset = i.cmdline->offset;
+ } else if (i.efi) {
+ i.device = i.efi->device;
+ i.offset = i.efi->offset;
+ } else
+ return -ENODEV;
+
+ *ret = TAKE_STRUCT(i);
+ return 0;
+}
diff --git a/src/hibernate-resume/hibernate-resume-config.h b/src/hibernate-resume/hibernate-resume-config.h
new file mode 100644
index 0000000..365d9cc
--- /dev/null
+++ b/src/hibernate-resume/hibernate-resume-config.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "sd-id128.h"
+
+typedef struct KernelHibernateLocation {
+ char *device;
+ uint64_t offset;
+ bool offset_set;
+} KernelHibernateLocation;
+
+typedef struct EFIHibernateLocation {
+ char *device;
+
+ sd_id128_t uuid;
+ uint64_t offset;
+
+ char *kernel_version;
+ char *id;
+ char *image_id;
+ char *version_id;
+ char *image_version;
+} EFIHibernateLocation;
+
+typedef struct HibernateInfo {
+ const char *device;
+ uint64_t offset; /* in memory pages */
+
+ KernelHibernateLocation *cmdline;
+ EFIHibernateLocation *efi;
+} HibernateInfo;
+
+void hibernate_info_done(HibernateInfo *info);
+
+int acquire_hibernate_info(HibernateInfo *ret);
+
+#if ENABLE_EFI
+
+void compare_hibernate_location_and_warn(const HibernateInfo *info);
+
+void clear_efi_hibernate_location(void);
+
+#else
+
+static inline void compare_hibernate_location_and_warn(const HibernateInfo *info) {
+ return;
+}
+
+static inline void clear_efi_hibernate_location(void) {
+ return;
+}
+
+#endif
diff --git a/src/hibernate-resume/hibernate-resume-generator.c b/src/hibernate-resume/hibernate-resume-generator.c
new file mode 100644
index 0000000..0168428
--- /dev/null
+++ b/src/hibernate-resume/hibernate-resume-generator.c
@@ -0,0 +1,130 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "dropin.h"
+#include "generator.h"
+#include "hibernate-resume-config.h"
+#include "initrd-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "parse-util.h"
+#include "proc-cmdline.h"
+#include "special.h"
+#include "static-destruct.h"
+#include "string-util.h"
+#include "unit-name.h"
+
+static const char *arg_dest = NULL;
+static char *arg_resume_options = NULL;
+static char *arg_root_options = NULL;
+static bool arg_noresume = false;
+
+STATIC_DESTRUCTOR_REGISTER(arg_resume_options, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_root_options, freep);
+
+static int parse_proc_cmdline_item(const char *key, const char *value, void *data) {
+ assert(key);
+
+ if (streq(key, "resumeflags")) {
+
+ if (proc_cmdline_value_missing(key, value))
+ return 0;
+
+ if (!strextend_with_separator(&arg_resume_options, ",", value))
+ return log_oom();
+
+ } else if (streq(key, "rootflags")) {
+
+ if (proc_cmdline_value_missing(key, value))
+ return 0;
+
+ if (!strextend_with_separator(&arg_root_options, ",", value))
+ return log_oom();
+
+ } else if (streq(key, "noresume")) {
+
+ if (value) {
+ log_warning("'noresume' kernel command line option specified with an argument, ignoring.");
+ return 0;
+ }
+
+ arg_noresume = true;
+ }
+
+ return 0;
+}
+
+static int process_resume(const HibernateInfo *info) {
+ _cleanup_free_ char *device_unit = NULL;
+ int r;
+
+ assert(info);
+
+ r = unit_name_from_path(info->device, ".device", &device_unit);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate device unit name from path '%s': %m", info->device);
+
+ /* If hibernate info is acquired from EFI variable, don't wait forever by default. Otherwise, if
+ * swap device is not present and HibernateLocation was not correctly cleared, we end up blocking
+ * the boot process infinitely. */
+ r = write_drop_in_format(arg_dest, device_unit, 40, "device-timeout",
+ "# Automatically generated by systemd-hibernate-resume-generator\n\n"
+ "[Unit]\n"
+ "JobTimeoutSec=%s\n",
+ info->cmdline ? "infinity" : "2min");
+ if (r < 0)
+ log_warning_errno(r, "Failed to write device timeout drop-in, ignoring: %m");
+
+ r = generator_write_timeouts(arg_dest,
+ info->device,
+ info->device,
+ arg_resume_options ?: arg_root_options,
+ NULL);
+ if (r < 0)
+ log_warning_errno(r, "Failed to write device timeout drop-in, ignoring: %m");
+
+ r = write_drop_in_format(arg_dest, SPECIAL_HIBERNATE_RESUME_SERVICE, 90, "device-dependency",
+ "# Automatically generated by systemd-hibernate-resume-generator\n\n"
+ "[Unit]\n"
+ "BindsTo=%1$s\n"
+ "After=%1$s\n",
+ device_unit);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write device dependency drop-in: %m");
+
+ return generator_add_symlink(arg_dest, SPECIAL_SYSINIT_TARGET, "wants", SPECIAL_HIBERNATE_RESUME_SERVICE);
+}
+
+static int run(const char *dest, const char *dest_early, const char *dest_late) {
+ _cleanup_(hibernate_info_done) HibernateInfo info = {};
+ int r;
+
+ arg_dest = ASSERT_PTR(dest);
+
+ /* Don't even consider resuming outside of initrd. */
+ if (!in_initrd()) {
+ log_debug("Not running in initrd, exiting.");
+ return 0;
+ }
+
+ r = proc_cmdline_parse(parse_proc_cmdline_item, NULL, 0);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse kernel command line, ignoring: %m");
+
+ if (arg_noresume) {
+ log_info("Found 'noresume' on the kernel command line, exiting.");
+ return 0;
+ }
+
+ r = acquire_hibernate_info(&info);
+ if (r == -ENODEV) {
+ log_debug_errno(r, "No resume device found, exiting.");
+ return 0;
+ }
+ if (r < 0)
+ return r;
+
+ return process_resume(&info);
+}
+
+DEFINE_MAIN_GENERATOR_FUNCTION(run);
diff --git a/src/hibernate-resume/hibernate-resume.c b/src/hibernate-resume/hibernate-resume.c
new file mode 100644
index 0000000..175a0bd
--- /dev/null
+++ b/src/hibernate-resume/hibernate-resume.c
@@ -0,0 +1,83 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <sys/stat.h>
+
+#include "devnum-util.h"
+#include "hibernate-resume-config.h"
+#include "hibernate-util.h"
+#include "initrd-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "parse-util.h"
+#include "static-destruct.h"
+
+static HibernateInfo arg_info = {};
+
+STATIC_DESTRUCTOR_REGISTER(arg_info, hibernate_info_done);
+
+static int setup_hibernate_info_and_warn(void) {
+ int r;
+
+ r = acquire_hibernate_info(&arg_info);
+ if (r == -ENODEV) {
+ log_info_errno(r, "No resume device found, exiting.");
+ return 0;
+ }
+ if (r < 0)
+ return r;
+
+ compare_hibernate_location_and_warn(&arg_info);
+
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ struct stat st;
+ int r;
+
+ log_setup();
+
+ if (argc < 1 || argc > 3)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program expects zero, one, or two arguments.");
+
+ umask(0022);
+
+ if (!in_initrd())
+ return 0;
+
+ if (argc > 1) {
+ arg_info.device = argv[1];
+
+ if (argc == 3) {
+ r = safe_atou64(argv[2], &arg_info.offset);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse resume offset %s: %m", argv[2]);
+ }
+ } else {
+ r = setup_hibernate_info_and_warn();
+ if (r <= 0)
+ return r;
+
+ if (arg_info.efi)
+ clear_efi_hibernate_location();
+ }
+
+ if (stat(arg_info.device, &st) < 0)
+ return log_error_errno(errno, "Failed to stat resume device '%s': %m", arg_info.device);
+
+ if (!S_ISBLK(st.st_mode))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Resume device '%s' is not a block device.", arg_info.device);
+
+ /* The write shall not return if a resume takes place. */
+ r = write_resume_config(st.st_rdev, arg_info.offset, arg_info.device);
+ log_full_errno(r < 0 ? LOG_ERR : LOG_DEBUG,
+ r < 0 ? r : SYNTHETIC_ERRNO(ENOENT),
+ "Unable to resume from device '%s' (" DEVNUM_FORMAT_STR ") offset %" PRIu64 ", continuing boot process.",
+ arg_info.device, DEVNUM_FORMAT_VAL(st.st_rdev), arg_info.offset);
+
+ return r;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/hibernate-resume/meson.build b/src/hibernate-resume/meson.build
new file mode 100644
index 0000000..5bcd8e0
--- /dev/null
+++ b/src/hibernate-resume/meson.build
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+executables += [
+ generator_template + {
+ 'name' : 'systemd-hibernate-resume-generator',
+ 'conditions' : ['ENABLE_HIBERNATE'],
+ 'sources' : files(
+ 'hibernate-resume-generator.c',
+ 'hibernate-resume-config.c',
+ ),
+ },
+ libexec_template + {
+ 'name' : 'systemd-hibernate-resume',
+ 'conditions' : ['ENABLE_HIBERNATE'],
+ 'sources' : files(
+ 'hibernate-resume.c',
+ 'hibernate-resume-config.c',
+ ),
+ },
+]