From: Matthew Garrett <matthewg@nvidia.com>
To: mjg59@srcf.ucam.org
Cc: keyrings@vger.kernel.org, James.Bottomley@HansenPartnership.com,
linux-integrity@vger.kernel.org, rafael@kernel.org,
linux-pm@vger.kernel.org, linux-efi@vger.kernel.org,
Matthew Garrett <matthewg@nvidia.com>
Subject: [PATCH 14/17] PM: hibernate: Add image digest and signature page infrastructure
Date: Thu, 8 Oct 2026 06:20:30 -0700 [thread overview]
Message-ID: <20261008132532.1155166-15-matthewg@nvidia.com> (raw)
In-Reply-To: <20261008132532.1155166-1-matthewg@nvidia.com>
Add support for signing hibernation images, so that a kernel can refuse
to restore an image that it did not write or that has been modified.
A signing backend registers a struct hib_sig_ops. When one is
registered, the SHA-256 digest of every page of the image stream, with
a domain separation prefix, is computed as the image is produced by
snapshot_read_next(). An extra page holding the backend's signature over
the digest is added to the end of the stream, and the image header
records its presence along with up to HIB_SIG_HEADER_SIZE bytes of
backend data needed for verification. As the signature page is part of
the image stream, it is carried by every backend that stores the
stream, including uswsusp.
When the image is loaded, snapshot_write_next() hashes each page once
the caller has filled it, skipping zero pages that are reconstructed
rather than read. The backend checks its header data as soon as the
header is loaded, before the rest of the image is read, and the
signature is verified at the start of hibernation_restore(), while
devices are still usable and before anything is quiesced. With a
backend registered, an image without a signature is rejected.
The uncompressed swap reader issues reads asynchronously, so when the
image is signed it now waits for each page before passing it on to be
hashed. The compressed reader and uswsusp already copy each page
synchronously.
With no backend registered, there is no change in behaviour.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
kernel/power/Kconfig | 1 +
kernel/power/hibernate.c | 6 ++
kernel/power/power.h | 34 +++++++++
kernel/power/snapshot.c | 161 ++++++++++++++++++++++++++++++++++++---
kernel/power/swap.c | 3 +-
5 files changed, 194 insertions(+), 11 deletions(-)
diff --git a/kernel/power/Kconfig b/kernel/power/Kconfig
index 71165e7f04f4..846f0f91dbc0 100644
--- a/kernel/power/Kconfig
+++ b/kernel/power/Kconfig
@@ -43,6 +43,7 @@ config HIBERNATION
select CRYPTO
select CRYPTO_LZO
select CRYPTO_LZ4
+ select CRYPTO_LIB_SHA256
help
Enable the suspend to disk (STD) functionality, which is usually
called "hibernation" in user interfaces. STD checkpoints the
diff --git a/kernel/power/hibernate.c b/kernel/power/hibernate.c
index c13f68ab7f6e..1efc2bd3a387 100644
--- a/kernel/power/hibernate.c
+++ b/kernel/power/hibernate.c
@@ -568,6 +568,12 @@ int hibernation_restore(int platform_mode)
{
int error;
+ error = snapshot_image_verify();
+ if (error) {
+ pr_err("Image signature verification failed: %d\n", error);
+ return error;
+ }
+
pm_prepare_console();
console_suspend_all();
error = dpm_suspend_start(PMSG_QUIESCE);
diff --git a/kernel/power/power.h b/kernel/power/power.h
index 75b63843886e..b4733d45e6cd 100644
--- a/kernel/power/power.h
+++ b/kernel/power/power.h
@@ -8,6 +8,9 @@
#include <linux/cpuidle.h>
#include <linux/crypto.h>
+#define HIB_SIG_HEADER_SIZE 2560
+#define HIB_SIG_TRAILER_SIZE 256
+
struct swsusp_info {
struct new_utsname uts;
u32 version_code;
@@ -16,8 +19,36 @@ struct swsusp_info {
unsigned long image_pages;
unsigned long pages;
unsigned long size;
+ /* number of signature pages at the end of the image (0 or 1) */
+ unsigned long sig_pages;
+ /* data needed to verify the signature, stored by hib_sig_ops.begin */
+ u8 sig_header[HIB_SIG_HEADER_SIZE];
} __aligned(PAGE_SIZE);
+/**
+ * struct hib_sig_ops - hibernation image signing
+ * @begin: Called when the image header is built. May store up to
+ * @size bytes needed to verify the signature in @data.
+ * @sign: Sign @digest, the SHA-256 digest of every page of the image
+ * before the signature page, storing the signature in @trailer.
+ * @end: Release any resources held for signing, after signing or when
+ * hibernation is aborted. May be called more than once.
+ * @check_header: Check the data stored by @begin when the image header is
+ * loaded, before the rest of the image is read.
+ * @verify: Verify the signature in @trailer over @digest before the
+ * image is restored.
+ *
+ * If signing is enabled, every image is signed and images without a valid
+ * signature are not restored.
+ */
+struct hib_sig_ops {
+ int (*begin)(void *data, size_t size);
+ int (*sign)(const u8 *digest, void *trailer, size_t size);
+ void (*end)(void);
+ int (*check_header)(const void *data, size_t size);
+ int (*verify)(const u8 *digest, const void *trailer, size_t size);
+};
+
#if defined(CONFIG_SUSPEND) || defined(CONFIG_HIBERNATION)
extern int pm_sleep_fs_sync(void);
extern bool filesystem_freeze_enabled;
@@ -164,6 +195,9 @@ extern int snapshot_read_next(struct snapshot_handle *handle);
extern int snapshot_write_next(struct snapshot_handle *handle);
int snapshot_write_finalize(struct snapshot_handle *handle);
extern int snapshot_image_loaded(struct snapshot_handle *handle);
+extern bool snapshot_image_signed(void);
+extern int snapshot_image_verify(void);
+extern void hibernate_set_sig_ops(const struct hib_sig_ops *ops);
extern bool hibernate_acquire(void);
extern void hibernate_release(void);
diff --git a/kernel/power/snapshot.c b/kernel/power/snapshot.c
index b209712cb2c3..bbf9de4b15ab 100644
--- a/kernel/power/snapshot.c
+++ b/kernel/power/snapshot.c
@@ -36,6 +36,7 @@
#include <asm/mmu_context.h>
#include <asm/tlbflush.h>
#include <asm/io.h>
+#include <crypto/sha2.h>
#include "power.h"
@@ -168,6 +169,37 @@ static struct linked_page *safe_pages_list;
/* Pointer to an auxiliary buffer (1 page) */
static void *buffer;
+/*
+ * Image signing. When signing is enabled, the SHA-256 digest of every page
+ * of the image is signed, and the signature is stored in an extra page at
+ * the end of the image.
+ */
+static const struct hib_sig_ops *hib_sig;
+static unsigned long nr_sig_pages;
+static struct sha256_ctx hib_sig_ctx;
+static u8 hib_sig_digest[SHA256_DIGEST_SIZE];
+/*
+ * restore: the signature page is read here, as the image buffer is freed
+ * by prepare_image() on systems without highmem
+ */
+static u8 hib_sig_page[PAGE_SIZE] __aligned(PAGE_SIZE);
+/* restore: the page handed out last must be hashed when it is filled */
+static bool hib_sig_hash_pending;
+/* the signature page has been handed out, or (restore) received */
+static bool hib_sig_trailer_out;
+static bool hib_sig_trailer_in;
+
+static const char hib_sig_domain[] = "Linux hibernation image v1";
+
+static void hib_sig_start(void)
+{
+ sha256_init(&hib_sig_ctx);
+ sha256_update(&hib_sig_ctx, hib_sig_domain, sizeof(hib_sig_domain));
+ hib_sig_hash_pending = false;
+ hib_sig_trailer_out = false;
+ hib_sig_trailer_in = false;
+}
+
#define PG_ANY 0
#define PG_SAFE 1
#define PG_UNSAFE_CLEAR 1
@@ -1591,6 +1623,9 @@ void swsusp_free(void)
{
unsigned long fb_pfn, fr_pfn;
+ if (hib_sig && hib_sig->end)
+ hib_sig->end();
+
if (!forbidden_pages_map || !free_pages_map)
goto out;
@@ -2141,6 +2176,7 @@ asmlinkage __visible int swsusp_save(void)
/* We don't actually copy the zero pages */
nr_zero_pages = nr_pages - nr_copy_pages;
nr_meta_pages = DIV_ROUND_UP(nr_pages * sizeof(long), PAGE_SIZE);
+ nr_sig_pages = hib_sig ? 1 : 0;
pm_deferred_pr_dbg("Image created (%d pages copied, %d zero pages)\n",
nr_copy_pages, nr_zero_pages);
@@ -2174,17 +2210,26 @@ static const char *check_image_kernel(struct swsusp_info *info)
unsigned long snapshot_get_image_size(void)
{
- return nr_copy_pages + nr_meta_pages + 1;
+ return nr_copy_pages + nr_meta_pages + nr_sig_pages + 1;
}
static int init_header(struct swsusp_info *info)
{
+ int error;
+
memset(info, 0, sizeof(struct swsusp_info));
info->num_physpages = get_num_physpages();
info->image_pages = nr_copy_pages;
info->pages = snapshot_get_image_size();
info->size = info->pages;
info->size <<= PAGE_SHIFT;
+ info->sig_pages = nr_sig_pages;
+ if (nr_sig_pages) {
+ error = hib_sig->begin(info->sig_header,
+ sizeof(info->sig_header));
+ if (error)
+ return error;
+ }
return init_header_complete(info);
}
@@ -2234,7 +2279,7 @@ static inline void pack_pfns(unsigned long *buf, struct memory_bitmap *bm,
*/
int snapshot_read_next(struct snapshot_handle *handle)
{
- if (handle->cur > nr_meta_pages + nr_copy_pages)
+ if (handle->cur > nr_meta_pages + nr_copy_pages + nr_sig_pages)
return 0;
if (!buffer) {
@@ -2246,6 +2291,7 @@ int snapshot_read_next(struct snapshot_handle *handle)
if (!handle->cur) {
int error;
+ hib_sig_start();
error = init_header((struct swsusp_info *)buffer);
if (error)
return error;
@@ -2255,6 +2301,19 @@ int snapshot_read_next(struct snapshot_handle *handle)
} else if (handle->cur <= nr_meta_pages) {
clear_page(buffer);
pack_pfns(buffer, &orig_bm, &zero_bm);
+ } else if (handle->cur > nr_meta_pages + nr_copy_pages) {
+ int error;
+
+ /* the signature page, covering every page before it */
+ sha256_final(&hib_sig_ctx, hib_sig_digest);
+ clear_page(buffer);
+ error = hib_sig->sign(hib_sig_digest, buffer,
+ HIB_SIG_TRAILER_SIZE);
+ if (error)
+ return error;
+ handle->buffer = buffer;
+ handle->cur++;
+ return PAGE_SIZE;
} else {
struct page *page;
@@ -2275,6 +2334,8 @@ int snapshot_read_next(struct snapshot_handle *handle)
handle->buffer = page_address(page);
}
}
+ if (nr_sig_pages)
+ sha256_update(&hib_sig_ctx, handle->buffer, PAGE_SIZE);
handle->cur++;
return PAGE_SIZE;
}
@@ -2339,11 +2400,32 @@ static int load_header(struct swsusp_info *info)
restore_pblist = NULL;
error = check_header(info);
- if (!error) {
- nr_copy_pages = info->image_pages;
- nr_meta_pages = info->pages - info->image_pages - 1;
+ if (error)
+ return error;
+
+ if (info->sig_pages > 1)
+ return -EINVAL;
+
+ nr_copy_pages = info->image_pages;
+ nr_sig_pages = info->sig_pages;
+ nr_meta_pages = info->pages - info->image_pages - nr_sig_pages - 1;
+
+ /* without signing enabled, a signature page is ignored */
+ if (!hib_sig)
+ return 0;
+
+ if (!nr_sig_pages) {
+ pr_err("Image is not signed\n");
+ return -EKEYREJECTED;
}
- return error;
+
+ error = hib_sig->check_header(info->sig_header,
+ sizeof(info->sig_header));
+ if (error)
+ return error;
+
+ sha256_update(&hib_sig_ctx, (u8 *)info, PAGE_SIZE);
+ return 0;
}
/**
@@ -2771,10 +2853,38 @@ int snapshot_write_next(struct snapshot_handle *handle)
static struct chain_allocator ca;
int error;
+ if (!handle->cur) {
+ /* A new load: discard any state left by an abandoned one. */
+ hib_sig_start();
+ nr_sig_pages = 0;
+ } else if (hib_sig_hash_pending) {
+ /* The caller has filled the page handed out last time. */
+ hib_sig_hash_pending = false;
+ if (hib_sig_trailer_out)
+ hib_sig_trailer_in = true;
+ else
+ sha256_update(&hib_sig_ctx, handle->buffer, PAGE_SIZE);
+ }
+
next:
/* Check if we have already loaded the entire image */
- if (handle->cur > 1 && handle->cur > nr_meta_pages + nr_copy_pages + nr_zero_pages)
- return 0;
+ if (handle->cur > 1 && handle->cur > nr_meta_pages + nr_copy_pages + nr_zero_pages) {
+ if (!nr_sig_pages || hib_sig_trailer_out)
+ return 0;
+
+ /* Hand out the signature page, which is not restored. */
+ copy_last_highmem_page();
+ error = hibernate_restore_protect_page(handle->buffer);
+ if (error)
+ return error;
+ sha256_final(&hib_sig_ctx, hib_sig_digest);
+ handle->buffer = hib_sig_page;
+ handle->sync_read = true;
+ hib_sig_trailer_out = true;
+ hib_sig_hash_pending = true;
+ handle->cur++;
+ return PAGE_SIZE;
+ }
if (!handle->cur) {
if (!buffer)
@@ -2841,6 +2951,8 @@ int snapshot_write_next(struct snapshot_handle *handle)
goto next;
}
+ /* The header is hashed by load_header() once it is known to be signed */
+ hib_sig_hash_pending = handle->cur > 1 && nr_sig_pages && hib_sig;
return PAGE_SIZE;
}
@@ -2868,7 +2980,9 @@ int snapshot_write_finalize(struct snapshot_handle *handle)
return error > 0 ? -ENODATA : error;
}
copy_last_highmem_page();
- error = hibernate_restore_protect_page(handle->buffer);
+ /* The last image page was protected when the signature page was handed out */
+ error = hib_sig_trailer_out ? 0 :
+ hibernate_restore_protect_page(handle->buffer);
/* Do that only if we have loaded the image entirely */
if (handle->cur > 1 && handle->cur > nr_meta_pages + nr_copy_pages + nr_zero_pages) {
memory_bm_recycle(&orig_bm);
@@ -2880,7 +2994,34 @@ int snapshot_write_finalize(struct snapshot_handle *handle)
int snapshot_image_loaded(struct snapshot_handle *handle)
{
return !(!nr_copy_pages || !last_highmem_page_copied() ||
- handle->cur <= nr_meta_pages + nr_copy_pages + nr_zero_pages);
+ handle->cur <= nr_meta_pages + nr_copy_pages + nr_zero_pages ||
+ (nr_sig_pages && !hib_sig_trailer_in));
+}
+
+/**
+ * snapshot_image_signed - Check if the image being loaded is signed.
+ */
+bool snapshot_image_signed(void)
+{
+ return nr_sig_pages && hib_sig;
+}
+
+/**
+ * snapshot_image_verify - Verify the signature of a loaded image.
+ *
+ * Return: 0 if signing is disabled or the image has a valid signature, or
+ * a negative error code otherwise.
+ */
+int snapshot_image_verify(void)
+{
+ if (!hib_sig)
+ return 0;
+
+ if (!nr_sig_pages || !hib_sig_trailer_in)
+ return -EKEYREJECTED;
+
+ return hib_sig->verify(hib_sig_digest, hib_sig_page,
+ HIB_SIG_TRAILER_SIZE);
}
#ifdef CONFIG_HIGHMEM
diff --git a/kernel/power/swap.c b/kernel/power/swap.c
index c78f1593600b..f8840cb1a2a6 100644
--- a/kernel/power/swap.c
+++ b/kernel/power/swap.c
@@ -1116,7 +1116,8 @@ static int load_image(struct swap_map_handle *handle,
ret = swap_read_page(handle, data_of(*snapshot), &hb);
if (ret)
break;
- if (snapshot->sync_read)
+ /* each page must be complete before it is hashed */
+ if (snapshot->sync_read || snapshot_image_signed())
ret = hib_wait_io(&hb);
if (ret)
break;
--
2.43.0
next prev parent reply other threads:[~2026-10-08 13:26 UTC|newest]
Thread overview: 39+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
2026-10-08 13:20 ` [PATCH 01/17] tpm: Define a kernel-owned TPM NV index that can't be modified by userland Matthew Garrett
2026-10-08 13:41 ` Matthew Garrett
2026-10-08 16:24 ` Jarkko Sakkinen
2026-10-08 16:23 ` Jarkko Sakkinen
2026-10-09 8:33 ` Matthew Garrett
2026-10-10 21:36 ` Jarkko Sakkinen
2026-10-08 17:06 ` Ilias Apalodimas
2026-10-10 9:22 ` James Bottomley
2026-10-10 21:26 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 02/17] efi: Add a mechanism to modify TPM state depending on kernel security features Matthew Garrett
2026-10-08 16:38 ` Jarkko Sakkinen
2026-10-08 13:20 ` [PATCH 03/17] tpm: Allow tpm2_start_auth_session() to start an audit session Matthew Garrett
2026-10-08 13:20 ` [PATCH 04/17] tpm: Log commands executed in " Matthew Garrett
2026-10-08 13:20 ` [PATCH 05/17] tpm: Add a kernel attestation key and signed audit digest retrieval Matthew Garrett
2026-10-08 16:45 ` James Bottomley
2026-10-09 8:29 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 06/17] tpm: Use TPM2_NV_ReadPublic to read NV index names Matthew Garrett
2026-10-08 13:20 ` [PATCH 07/17] tpm: Add in-kernel support for reading NV indices Matthew Garrett
2026-10-08 13:20 ` [PATCH 08/17] tpm: Add NV define, undefine and write helpers Matthew Garrett
2026-10-08 13:20 ` [PATCH 09/17] tpm: Provision the kernel NV index at registration Matthew Garrett
2026-10-08 13:20 ` [PATCH 10/17] tpm: Move the bounds-checked response reader to a header Matthew Garrett
2026-10-08 13:20 ` [PATCH 11/17] tpm: Add kernel signing key creation with audited provenance Matthew Garrett
2026-10-08 17:00 ` James Bottomley
2026-10-09 8:31 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 12/17] tpm: Add signing with the kernel signing key Matthew Garrett
2026-10-08 13:20 ` [PATCH 13/17] tpm: Add verification of kernel signing key provenance Matthew Garrett
2026-10-10 9:34 ` James Bottomley
2026-10-10 21:18 ` Matthew Garrett
2026-10-10 9:42 ` James Bottomley
2026-10-10 21:20 ` Matthew Garrett
2026-10-08 13:20 ` Matthew Garrett [this message]
2026-10-08 13:20 ` [PATCH 15/17] PM: hibernate: Sign and verify images with a kernel-generated TPM key Matthew Garrett
2026-10-08 13:20 ` [PATCH 16/17] PM: hibernate: Refuse to verify images with a virtual TPM Matthew Garrett
2026-10-08 13:20 ` [PATCH 17/17] PM: hibernate: Allow hibernation under lockdown with signed images Matthew Garrett
2026-10-08 16:53 ` Jarkko Sakkinen
2026-10-09 8:31 ` Matthew Garrett
2026-10-10 21:29 ` Jarkko Sakkinen
2026-10-08 15:56 ` [RFC] Make hibernation work with lockdown Jarkko Sakkinen
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261008132532.1155166-15-matthewg@nvidia.com \
--to=matthewg@nvidia.com \
--cc=James.Bottomley@HansenPartnership.com \
--cc=keyrings@vger.kernel.org \
--cc=linux-efi@vger.kernel.org \
--cc=linux-integrity@vger.kernel.org \
--cc=linux-pm@vger.kernel.org \
--cc=mjg59@srcf.ucam.org \
--cc=rafael@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox