From nobody Fri Sep 25 02:08:28 2026 Received: from mail-wm2-f12.google.com (mail-wm2-f12.google.com [74.125.225.140]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id BF09251477F for ; Thu, 17 Sep 2026 13:29:57 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.225.140 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789651807; cv=none; b=f/9W200pYziayU8kMDP/C+3w25Vl50EI8u+P1TCocTuXTagP/WM2A0MYZCjeo1lec7puX7INUjti7Kr7z2grFq1S8eMKzgOqGA0M0vq3V7L8nmPsQRRUMwkEjjp8aZys2ckn5j8nOECfXdTWxUx8hTpgubq9Jl6UI5fYfCqNne0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789651807; c=relaxed/simple; bh=U3N+qyTHKCP+RHLw71C9Y4QQgabFkloF7H/PSTJr2sA=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=FgFs6oXiN04J5MIvbHue/j1XAIK8dYbx+FSZf2kD6qhAU4BNfqAozaHeWXwRQpoD2qjL0vXZysJWy76w/NBiWop8QJJvKVuhBDpUzUwDNHTbGmjAOWLJAeQMJqxM6BWk5mTAbINRYpUIsG8egvHMTutdB1rH5LYD0wQ8Woet55U= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=starlabs.systems; spf=pass smtp.mailfrom=starlabs.systems; dkim=pass (2048-bit key) header.d=starlabs-systems.20251104.gappssmtp.com header.i=@starlabs-systems.20251104.gappssmtp.com header.b=0L+ARRVi; arc=none smtp.client-ip=74.125.225.140 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=starlabs.systems Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=starlabs.systems Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=starlabs-systems.20251104.gappssmtp.com header.i=@starlabs-systems.20251104.gappssmtp.com header.b="0L+ARRVi" Received: by mail-wm2-f12.google.com with SMTP id 5b1f17b1804b1-49b912e4ad9so5006535e9.2 for ; Thu, 17 Sep 2026 06:29:57 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=starlabs-systems.20251104.gappssmtp.com; s=20251104; t=1789651795; x=1790256595; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=te9pOkBoV6jMkByYyKZgWN4wOydqVtJW9G1kH7jQqsc=; b=0L+ARRVio+MHzf3A1hmkZECoVO/Z+Y4DTv/L03A8gcySuJ6cUpINtO0QPmzxkN3oIC ur6mnbxgsv3bexWiyNUOjvNst4fmoR5+mt21n0mXtYWEzJwDZK/V5vU9Qb7NsYx1ipGb SuHMfmFvfN3+4mSsMGe5sH31qLvYOeJWP0yw6W9O1XnmXi7SzKxf4GKPO4YwygA1ShxD haE7r3X566H32bTM9RXxCVFy4caWXoTxO3HfYU2gMLbBcykn+KUmc1vX7mldc63H+YfI Oeu0BLQN71jQTc+t3FDv/SPK5QZ7+tZSi8JYZIM1t2mlec9rkmFJ5aS59LE5xWjo8ll9 v4cg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1789651795; x=1790256595; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=te9pOkBoV6jMkByYyKZgWN4wOydqVtJW9G1kH7jQqsc=; b=MCcXQjvbyfkFM0BClWbicZuBLIXfIJiGF+89j1GwMu+ff6QkJ7bVjgQFHI1SJvyQ3B oUb7JLkz+Ynhc7EBAA5S03cndZ1fTcx0psNdUcFjIgfvOqLj/ROPAOROjDa4pqmBLmzx gsdq6l+chX4gHnN7bKCcRwedawRsiGibBaVGqn3NGI0QhDynGaR4LB40R0NtHsT67xiR ofpfY3FgTp7flwUkUX5sAVRwkcIaEsDGVk06migtAkd91hu2GeLANY57Y4VNy3AwGdR5 zZmAc0PIHuz81WK1t+4fr56qV2Eiie1zEhgphZFgEQX/2aIFWSoFPyvcR6TwmtJdeyiG gk2Q== X-Forwarded-Encrypted: i=1; AKwUvBwJHTEApD9Y/bofCamJlTwOhonqI56jxj4ShK7EgP874kgnfGNUvf0NBadUAdYfWepsQ11yZrfXeND7BWI=@vger.kernel.org X-Gm-Message-State: AFuF++l8nHuYgd/GmSdSqxcZPqh1oQiIj+b1ta1gFQyi6hqp73ffSgCB +noWA/HtcTRW+o+CEvxbk3KS7cqBF9whNmubzDCo6pGd43AsaiLVInfuBo5RBIfX0g== X-Gm-Gg: AYBFou0WE4A4VjWLH2/V+ifOnpEB4c3TL0SucMc/pn96CVZBXPEucgYrXIFsjUIVDQZ IEyFpuLUyoktUA+8mJGqorzYNk7QHgeMLUOUWUu3iBOqtmhmsICajQPkiW2s4w0FvIWqR91E4vN SC3P1mlazkbPm/XiOxBtPNgLEmzICpoBH8ak50yHVAD2zDwNcLhMzxAcLfR4YNi1EkKeNX3/6UP Nm6i67DEKIVKIz7uMotloJVoVmRlZRXVi8iCZ+XCb1/xn/Aa8lBBxPDbfrad50T6CZT6mbkNitB nkzDY50cz6vmej07zERFSpjr0FiLvEfboal18INXfeYkcIImbCXXYK0GS5qIbjfl5ShS5y63ipQ T4KHIpKheal1DOn9sOF20l0eHrr2mtE3+otOlHZkMvRc8KXMtUrcNQyCeNoFCHPgtUeCKHVBKNH kMBhIw74l7N3SgckY9lj1hwafD1XvhJ6bSS1zkDb86LTY/Qf1y5gReiunw6pOMPE7sz5quZHhB+ MD86FX9tzu1tEhsz1XiWpNcBNc= X-Received: by 2002:a05:600c:8717:b0:49e:74c6:d973 with SMTP id 5b1f17b1804b1-49eb734137amr80252775e9.31.1789651794829; Thu, 17 Sep 2026 06:29:54 -0700 (PDT) Received: from fighter ([216.128.28.240]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-49fbd2356d1sm97548275e9.6.2026.09.17.06.29.53 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 17 Sep 2026 06:29:54 -0700 (PDT) From: Sean Rhodes To: linux-pm@vger.kernel.org Cc: "Rafael J. Wysocki" , Len Brown , Pavel Machek , Evan Green , Sean Rhodes , linux-kernel@vger.kernel.org, Jens Axboe , Andrew Morton , Chris Li , Kairui Song , Kemeng Shi , Nhat Pham , Baoquan He , Barry Song , Youngjun Park , linux-block@vger.kernel.org, linux-mm@kvack.org, Xueqin Luo , Nicolas Bouchinet Subject: [PATCH v3 RESEND 1/4] PM: hibernate: add seed-wrapped encrypted snapshots Date: Thu, 17 Sep 2026 14:29:47 +0100 Message-ID: X-Mailer: git-send-email 2.53.0 In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" From: Evan Green Encrypt and authenticate uswsusp hibernation images in the kernel so userspace cannot tamper with the image that will be restored as kernel memory. The image data is protected with gcm(aes) in 16-page chunks. SNAPSHOT_ENABLE_ENCRYPTION enables the encrypted read/write paths and returns an opaque wrapped image-key blob with the starting nonce on suspend. On resume, userspace provides the same blob and nonce after trusted early userspace has written the same 32-byte seed to /sys/power/snapshot_seed. The plaintext image key is generated and unwrapped inside the kernel. The wrapping key is derived from the locked seed, leaving TPM PCR policy and seed unsealing to early userspace rather than adding TPM or trusted-key state to PM code. SNAPSHOT_SET_USER_KEY lets userspace fold extra key material into the data encryption key after the metadata area has been read or written. Tested on StarFighter MTL with a TPM-sealed seed: complete keyless and user-key encrypted uswsusp cycles resumed the same process and boot ID. Corrupted wrapped data, invalid blob magic, a corrupted complete-chunk tag and a wrong user key were rejected. Signed-off-by: Evan Green Co-developed-by: Sean Rhodes Signed-off-by: Sean Rhodes --- Documentation/power/userland-swsusp.rst | 16 + include/uapi/linux/suspend_ioctls.h | 31 +- kernel/power/Kconfig | 15 + kernel/power/Makefile | 1 + kernel/power/hibernate.c | 24 + kernel/power/power.h | 1 + kernel/power/snapenc.c | 952 ++++++++++++++++++++++++ kernel/power/snapshot.c | 5 + kernel/power/user.c | 59 +- kernel/power/user.h | 139 ++++ 10 files changed, 1225 insertions(+), 18 deletions(-) create mode 100644 kernel/power/snapenc.c create mode 100644 kernel/power/user.h diff --git a/Documentation/power/userland-swsusp.rst b/Documentation/power/= userland-swsusp.rst index 1cf62d80a9ca..282e01d2fe61 100644 --- a/Documentation/power/userland-swsusp.rst +++ b/Documentation/power/userland-swsusp.rst @@ -115,6 +115,22 @@ SNAPSHOT_S2RAM to resume the system from RAM if there's enough battery power or restore its state on the basis of the saved suspend image otherwise) =20 +SNAPSHOT_ENABLE_ENCRYPTION + Enables encryption of the hibernate image within the kernel. Upon suspend + (ie when the snapshot device was opened for reading), returns a blob + representing the random encryption key the kernel created to encrypt the + hibernate image with. Upon resume (ie when the snapshot device was opened + for writing), receives a blob from usermode containing the key material + previously returned during hibernate. + +SNAPSHOT_SET_USER_KEY + Mixes additional user key material into the data portion of an encrypted + hibernate image. The ioctl argument points to struct uswsusp_user_key. + key_len must be between 8 and USWSUSP_USER_KEY_SIZE bytes, and reserved + must be zero. The kernel writes meta_size with the encrypted metadata + size that userspace may transfer before providing the user key during + resume. + The device's read() operation can be used to transfer the snapshot image f= rom the kernel. It has the following limitations: =20 diff --git a/include/uapi/linux/suspend_ioctls.h b/include/uapi/linux/suspe= nd_ioctls.h index bcce04e21c0d..5e23bf936277 100644 --- a/include/uapi/linux/suspend_ioctls.h +++ b/include/uapi/linux/suspend_ioctls.h @@ -13,6 +13,31 @@ struct resume_swap_area { __u32 dev; } __attribute__((packed)); =20 +#define USWSUSP_KEY_NONCE_SIZE 16 +#define USWSUSP_USER_KEY_SIZE 32 + +/* + * This structure is used to pass the opaque wrapped hibernate image + * encryption key and starting nonce in either direction. + */ +struct uswsusp_key_blob { + __u32 blob_len; + __u8 blob[512]; + __u8 nonce[USWSUSP_KEY_NONCE_SIZE] __kernel_nonstring; +} __attribute__((packed)); + +/* + * Allow user mode to fold in key material for the data portion of the hib= ernate + * image. + */ +struct uswsusp_user_key { + /* Kernel returns the metadata size; resume passes the saved value in. */ + __u64 meta_size; + __u32 key_len; + __u32 reserved; + __u8 key[USWSUSP_USER_KEY_SIZE] __kernel_nonstring; +} __attribute__((packed)); + #define SNAPSHOT_IOC_MAGIC '3' #define SNAPSHOT_FREEZE _IO(SNAPSHOT_IOC_MAGIC, 1) #define SNAPSHOT_UNFREEZE _IO(SNAPSHOT_IOC_MAGIC, 2) @@ -29,6 +54,10 @@ struct resume_swap_area { #define SNAPSHOT_PREF_IMAGE_SIZE _IO(SNAPSHOT_IOC_MAGIC, 18) #define SNAPSHOT_AVAIL_SWAP_SIZE _IOR(SNAPSHOT_IOC_MAGIC, 19, __kernel_lof= f_t) #define SNAPSHOT_ALLOC_SWAP_PAGE _IOR(SNAPSHOT_IOC_MAGIC, 20, __kernel_lof= f_t) -#define SNAPSHOT_IOC_MAXNR 20 +#define SNAPSHOT_ENABLE_ENCRYPTION _IOWR(SNAPSHOT_IOC_MAGIC, 21, \ + struct uswsusp_key_blob) +#define SNAPSHOT_SET_USER_KEY _IOWR(SNAPSHOT_IOC_MAGIC, 22, \ + struct uswsusp_user_key) +#define SNAPSHOT_IOC_MAXNR 22 =20 #endif /* _LINUX_SUSPEND_IOCTLS_H */ diff --git a/kernel/power/Kconfig b/kernel/power/Kconfig index 71165e7f04f4..f23d1c7e0ecd 100644 --- a/kernel/power/Kconfig +++ b/kernel/power/Kconfig @@ -115,6 +115,21 @@ config HIBERNATION_DEF_COMP help Default compressor to be used for hibernation. =20 +config ENCRYPTED_HIBERNATION + bool "Encryption support for userspace snapshots" + depends on HIBERNATION_SNAPSHOT_DEV + select CRYPTO_AES + select CRYPTO_GCM + select CRYPTO_LIB_SHA256 + help + Enable support for kernel-based encryption of hibernation snapshots + created by uswsusp tools. A trusted early userspace component must + provide the snapshot encryption seed before enabling encryption. + + Say N if userspace handles the image encryption. + + If in doubt, say N. + config PM_STD_PARTITION string "Default resume partition" depends on HIBERNATION diff --git a/kernel/power/Makefile b/kernel/power/Makefile index 773e2789412b..2fd31b2a250f 100644 --- a/kernel/power/Makefile +++ b/kernel/power/Makefile @@ -16,6 +16,7 @@ obj-$(CONFIG_SUSPEND) +=3D suspend.o obj-$(CONFIG_PM_TEST_SUSPEND) +=3D suspend_test.o obj-$(CONFIG_HIBERNATION) +=3D hibernate.o snapshot.o swap.o obj-$(CONFIG_HIBERNATION_SNAPSHOT_DEV) +=3D user.o +obj-$(CONFIG_ENCRYPTED_HIBERNATION) +=3D snapenc.o obj-$(CONFIG_PM_AUTOSLEEP) +=3D autosleep.o obj-$(CONFIG_PM_WAKELOCKS) +=3D wakelock.o =20 diff --git a/kernel/power/hibernate.c b/kernel/power/hibernate.c index d2479c69d71a..a95cb447a13a 100644 --- a/kernel/power/hibernate.c +++ b/kernel/power/hibernate.c @@ -36,6 +36,7 @@ #include =20 #include "power.h" +#include "user.h" =20 =20 static int nocompress; @@ -1376,12 +1377,35 @@ static ssize_t reserved_size_store(struct kobject *= kobj, =20 power_attr(reserved_size); =20 +#ifdef CONFIG_ENCRYPTED_HIBERNATION +static ssize_t snapshot_seed_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t n) +{ + int ret; + + ret =3D snapshot_store_encryption_seed(buf, n); + return ret ? ret : n; +} + +static struct kobj_attribute snapshot_seed_attr =3D { + .attr =3D { + .name =3D "snapshot_seed", + .mode =3D 0200, + }, + .store =3D snapshot_seed_store, +}; +#endif + static struct attribute *g[] =3D { &disk_attr.attr, &resume_offset_attr.attr, &resume_attr.attr, &image_size_attr.attr, &reserved_size_attr.attr, +#ifdef CONFIG_ENCRYPTED_HIBERNATION + &snapshot_seed_attr.attr, +#endif NULL, }; =20 diff --git a/kernel/power/power.h b/kernel/power/power.h index 75b63843886e..e080230f97fc 100644 --- a/kernel/power/power.h +++ b/kernel/power/power.h @@ -160,6 +160,7 @@ struct snapshot_handle { =20 extern unsigned int snapshot_additional_pages(struct zone *zone); extern unsigned long snapshot_get_image_size(void); +unsigned long snapshot_get_meta_page_count(void); 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); diff --git a/kernel/power/snapenc.c b/kernel/power/snapenc.c new file mode 100644 index 000000000000..b555a699b564 --- /dev/null +++ b/kernel/power/snapenc.c @@ -0,0 +1,952 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* This file provides encryption support for system snapshots. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "power.h" +#include "user.h" + +#define SNAPSHOT_SEED_SIZE SHA256_DIGEST_SIZE +#define SNAPSHOT_KEY_BLOB_VERSION 1 + +static const u8 snapshot_key_blob_magic[8] =3D { + 'S', 'W', 'S', 'U', 'S', 'P', 'K', '1', +}; + +struct snapshot_wrapped_key { + u8 magic[sizeof(snapshot_key_blob_magic)]; + __le32 version; + u8 wrap_nonce[GCM_AES_IV_SIZE] __nonstring; + u8 encrypted_key[SNAPSHOT_ENCRYPTION_KEY_SIZE] __nonstring; + u8 tag[SNAPSHOT_AUTH_TAG_SIZE] __nonstring; +} __packed; + +static DEFINE_MUTEX(snapshot_seed_mutex); +static u8 snapshot_seed[SNAPSHOT_SEED_SIZE] __nonstring; +static bool snapshot_seed_valid; + +int snapshot_store_encryption_seed(const char *buf, size_t count) +{ + u8 seed[SNAPSHOT_SEED_SIZE] __nonstring; + size_t len =3D count; + int ret; + + if (len && buf[len - 1] =3D=3D '\n') + len--; + if (len !=3D SNAPSHOT_SEED_SIZE * 2) + return -EINVAL; + + ret =3D hex2bin(seed, buf, sizeof(seed)); + if (ret) + return ret; + + mutex_lock(&snapshot_seed_mutex); + if (snapshot_seed_valid) { + ret =3D crypto_memneq(snapshot_seed, seed, sizeof(seed)) ? -EPERM : 0; + goto out; + } + + memcpy(snapshot_seed, seed, sizeof(seed)); + snapshot_seed_valid =3D true; + pr_info("PM: hibernate: snapshot encryption seed locked\n"); + +out: + mutex_unlock(&snapshot_seed_mutex); + memzero_explicit(seed, sizeof(seed)); + return ret; +} + +static bool snapshot_copy_encryption_seed(u8 seed[SNAPSHOT_SEED_SIZE]) +{ + bool valid; + + mutex_lock(&snapshot_seed_mutex); + valid =3D snapshot_seed_valid; + if (valid) + memcpy(seed, snapshot_seed, SNAPSHOT_SEED_SIZE); + mutex_unlock(&snapshot_seed_mutex); + + return valid; +} + +static int snapshot_derive_wrapping_key(u8 key[SNAPSHOT_ENCRYPTION_KEY_SIZ= E]) +{ + static const char label[] =3D "Linux hibernate snapshot key wrap v1"; + u8 digest[SHA256_DIGEST_SIZE]; + u8 seed[SNAPSHOT_SEED_SIZE] __nonstring; + struct sha256_ctx sha256_ctx; + + if (!snapshot_copy_encryption_seed(seed)) + return -ENOKEY; + + sha256_init(&sha256_ctx); + sha256_update(&sha256_ctx, label, strlen(label)); + sha256_update(&sha256_ctx, seed, sizeof(seed)); + sha256_final(&sha256_ctx, digest); + + memcpy(key, digest, SNAPSHOT_ENCRYPTION_KEY_SIZE); + memzero_explicit(seed, sizeof(seed)); + memzero_explicit(digest, sizeof(digest)); + return 0; +} + +static int snapshot_crypt_wrapped_key(struct snapshot_wrapped_key *wrapped, + u8 image_key[SNAPSHOT_ENCRYPTION_KEY_SIZE], + bool encrypt) +{ + u8 wrap_key[SNAPSHOT_ENCRYPTION_KEY_SIZE] __nonstring; + u8 buf[SNAPSHOT_ENCRYPTION_KEY_SIZE + SNAPSHOT_AUTH_TAG_SIZE] __nonstring; + struct crypto_aead *tfm; + struct aead_request *req; + struct scatterlist sg; + DECLARE_CRYPTO_WAIT(wait); + int rc; + + rc =3D snapshot_derive_wrapping_key(wrap_key); + if (rc) + return rc; + + tfm =3D crypto_alloc_aead("gcm(aes)", 0, 0); + if (IS_ERR(tfm)) { + rc =3D PTR_ERR(tfm); + goto out_key; + } + + rc =3D crypto_aead_setkey(tfm, wrap_key, sizeof(wrap_key)); + if (rc) + goto out_tfm; + + rc =3D crypto_aead_setauthsize(tfm, SNAPSHOT_AUTH_TAG_SIZE); + if (rc) + goto out_tfm; + + req =3D aead_request_alloc(tfm, GFP_KERNEL); + if (!req) { + rc =3D -ENOMEM; + goto out_tfm; + } + + if (encrypt) { + get_random_bytes(wrapped->wrap_nonce, sizeof(wrapped->wrap_nonce)); + memcpy(buf, image_key, SNAPSHOT_ENCRYPTION_KEY_SIZE); + } else { + memcpy(buf, wrapped->encrypted_key, SNAPSHOT_ENCRYPTION_KEY_SIZE); + memcpy(buf + SNAPSHOT_ENCRYPTION_KEY_SIZE, wrapped->tag, + SNAPSHOT_AUTH_TAG_SIZE); + } + + sg_init_one(&sg, buf, sizeof(buf)); + aead_request_set_callback(req, 0, crypto_req_done, &wait); + aead_request_set_ad(req, 0); + aead_request_set_crypt(req, &sg, &sg, + encrypt ? SNAPSHOT_ENCRYPTION_KEY_SIZE : + sizeof(buf), + wrapped->wrap_nonce); + + rc =3D crypto_wait_req(encrypt ? crypto_aead_encrypt(req) : + crypto_aead_decrypt(req), + &wait); + if (rc) + goto out_req; + + if (encrypt) { + memcpy(wrapped->encrypted_key, buf, SNAPSHOT_ENCRYPTION_KEY_SIZE); + memcpy(wrapped->tag, buf + SNAPSHOT_ENCRYPTION_KEY_SIZE, + SNAPSHOT_AUTH_TAG_SIZE); + } else { + memcpy(image_key, buf, SNAPSHOT_ENCRYPTION_KEY_SIZE); + } + +out_req: + aead_request_free(req); +out_tfm: + crypto_free_aead(tfm); +out_key: + memzero_explicit(buf, sizeof(buf)); + memzero_explicit(wrap_key, sizeof(wrap_key)); + return rc; +} + +static int snapshot_wrap_image_key(const u8 image_key[SNAPSHOT_ENCRYPTION_= KEY_SIZE], + struct snapshot_wrapped_key *wrapped) +{ + u8 key[SNAPSHOT_ENCRYPTION_KEY_SIZE] __nonstring; + int rc; + + memset(wrapped, 0, sizeof(*wrapped)); + memcpy(wrapped->magic, snapshot_key_blob_magic, sizeof(wrapped->magic)); + wrapped->version =3D cpu_to_le32(SNAPSHOT_KEY_BLOB_VERSION); + + memcpy(key, image_key, sizeof(key)); + rc =3D snapshot_crypt_wrapped_key(wrapped, key, true); + memzero_explicit(key, sizeof(key)); + return rc; +} + +static int snapshot_unwrap_image_key(const struct snapshot_wrapped_key *wr= apped, + u8 image_key[SNAPSHOT_ENCRYPTION_KEY_SIZE]) +{ + struct snapshot_wrapped_key tmp; + int rc; + + if (memcmp(wrapped->magic, snapshot_key_blob_magic, + sizeof(snapshot_key_blob_magic))) + return -EINVAL; + if (le32_to_cpu(wrapped->version) !=3D SNAPSHOT_KEY_BLOB_VERSION) + return -EINVAL; + + tmp =3D *wrapped; + rc =3D snapshot_crypt_wrapped_key(&tmp, image_key, false); + memzero_explicit(&tmp, sizeof(tmp)); + return rc; +} + +static int snapshot_install_image_key(struct snapshot_data *data, + const u8 image_key[SNAPSHOT_ENCRYPTION_KEY_SIZE]) +{ + int rc; + + memcpy(data->encryption_key, image_key, sizeof(data->encryption_key)); + rc =3D crypto_aead_setkey(data->aead_tfm, data->encryption_key, + sizeof(data->encryption_key)); + if (rc) + memzero_explicit(data->encryption_key, + sizeof(data->encryption_key)); + + return rc; +} + +/* Derive a key from the kernel and user keys for data encryption. */ +static int snapshot_use_user_key(struct snapshot_data *data) +{ + u8 digest[SHA256_DIGEST_SIZE]; + struct sha256_ctx sha256_ctx; + int rc; + + /* + * Hash the kernel key and the user key together. This folds in the user + * key, but not in a way that gives the user mode predictable control + * over the key bits. + */ + sha256_init(&sha256_ctx); + + sha256_update(&sha256_ctx, data->encryption_key, + SNAPSHOT_ENCRYPTION_KEY_SIZE); + sha256_update(&sha256_ctx, data->user_key, sizeof(data->user_key)); + sha256_final(&sha256_ctx, digest); + + BUILD_BUG_ON(SNAPSHOT_ENCRYPTION_KEY_SIZE > SHA256_DIGEST_SIZE); + + rc =3D crypto_aead_setkey(data->aead_tfm, + digest, + SNAPSHOT_ENCRYPTION_KEY_SIZE); + memzero_explicit(digest, sizeof(digest)); + + return rc; +} + +/* Check to see if it's time to switch to the user key, and do it if so. */ +static int snapshot_check_user_key_switch(struct snapshot_data *data) +{ + if (data->user_key_valid && data->meta_size && + data->crypt_total =3D=3D data->meta_size) { + return snapshot_use_user_key(data); + } + + return 0; +} + +static loff_t snapshot_encrypted_byte_count(loff_t plain_size); + +static void snapshot_set_meta_size(struct snapshot_data *data, u64 meta_si= ze) +{ + data->meta_size =3D meta_size; + data->crypt_meta_size =3D snapshot_encrypted_byte_count(meta_size); +} + +/* Encrypt more data from the snapshot into the staging area. */ +static int snapshot_encrypt_refill(struct snapshot_data *data) +{ + struct aead_request *req =3D data->aead_req; + u8 nonce[GCM_AES_IV_SIZE]; + DECLARE_CRYPTO_WAIT(wait); + size_t total =3D 0; + int pg_idx; + int res; + + if (data->crypt_total =3D=3D 0) { + snapshot_set_meta_size(data, + snapshot_get_meta_page_count() << PAGE_SHIFT); + } else { + res =3D snapshot_check_user_key_switch(data); + if (res) + return res; + } + + /* + * The first buffer is the associated data, set to the offset to prevent + * attacks that rearrange chunks. + */ + sg_set_buf(&data->sg[0], &data->crypt_total, sizeof(data->crypt_total)); + + /* Load the crypt buffer with snapshot pages. */ + for (pg_idx =3D 0; pg_idx < SNAPSHOT_CHUNK_SIZE; pg_idx++) { + void *buf =3D data->crypt_pages[pg_idx]; + + /* Stop at the meta page boundary before switching keys. */ + if (data->user_key_valid && total && + ((data->crypt_total + total) =3D=3D data->meta_size)) + break; + + res =3D snapshot_read_next(&data->handle); + if (res < 0) + return res; + if (res =3D=3D 0) + break; + + WARN_ON(res !=3D PAGE_SIZE); + + /* + * Copy the page into the staging area. A future optimization + * could potentially skip this copy for lowmem pages. + */ + memcpy(buf, data_of(data->handle), PAGE_SIZE); + sg_set_buf(&data->sg[1 + pg_idx], buf, PAGE_SIZE); + total +=3D PAGE_SIZE; + } + + if (!total) + return 0; + + sg_set_buf(&data->sg[1 + pg_idx], &data->auth_tag, SNAPSHOT_AUTH_TAG_SIZE= ); + aead_request_set_callback(req, 0, crypto_req_done, &wait); + /* + * Use incrementing nonces for each chunk, since a 64 bit value won't + * roll into re-use for any given hibernate image. + */ + memcpy(&nonce[0], &data->nonce_low, sizeof(data->nonce_low)); + memcpy(&nonce[sizeof(data->nonce_low)], + &data->nonce_high, + sizeof(nonce) - sizeof(data->nonce_low)); + + data->nonce_low +=3D 1; + /* Total does not include AAD or the auth tag. */ + aead_request_set_crypt(req, data->sg, data->sg, total, nonce); + res =3D crypto_wait_req(crypto_aead_encrypt(req), &wait); + if (res) + return res; + + data->crypt_size =3D total; + data->crypt_total +=3D total; + return 0; +} + +/* Decrypt data from the staging area and push it to the snapshot. */ +static int snapshot_decrypt_drain(struct snapshot_data *data) +{ + struct aead_request *req =3D data->aead_req; + u8 nonce[GCM_AES_IV_SIZE]; + DECLARE_CRYPTO_WAIT(wait); + int page_count; + size_t total; + int pg_idx; + int res; + + /* Set up the associated data. */ + sg_set_buf(&data->sg[0], &data->crypt_total, sizeof(data->crypt_total)); + + /* + * Get the number of full pages, which could be short at the end. There + * should also be a tag at the end, so the offset won't be an even page. + */ + page_count =3D data->crypt_offset >> PAGE_SHIFT; + total =3D page_count << PAGE_SHIFT; + if (total =3D=3D 0 || total =3D=3D data->crypt_offset) + return -EINVAL; + + /* + * Load the sg list with the crypt buffer. Inline decrypt back into the + * staging buffer. A future optimization could decrypt directly into + * lowmem pages. + */ + for (pg_idx =3D 0; pg_idx < page_count; pg_idx++) + sg_set_buf(&data->sg[1 + pg_idx], data->crypt_pages[pg_idx], PAGE_SIZE); + + /* + * It's possible this is the final decrypt, or the final decrypt of the + * meta region, and there are fewer than SNAPSHOT_CHUNK_SIZE pages. If th= is is + * the case we would have just written the auth tag into the first few + * bytes of a new page. Copy to the tag if so. + */ + if (page_count < SNAPSHOT_CHUNK_SIZE && + (data->crypt_offset - total) =3D=3D sizeof(data->auth_tag)) { + memcpy(data->auth_tag, data->crypt_pages[pg_idx], + sizeof(data->auth_tag)); + } else if (data->crypt_offset !=3D + ((SNAPSHOT_CHUNK_SIZE << PAGE_SHIFT) + SNAPSHOT_AUTH_TAG_SIZE)) { + return -EINVAL; + } + + sg_set_buf(&data->sg[1 + pg_idx], &data->auth_tag, SNAPSHOT_AUTH_TAG_SIZE= ); + aead_request_set_callback(req, 0, crypto_req_done, &wait); + memcpy(&nonce[0], &data->nonce_low, sizeof(data->nonce_low)); + memcpy(&nonce[sizeof(data->nonce_low)], + &data->nonce_high, + sizeof(nonce) - sizeof(data->nonce_low)); + + data->nonce_low +=3D 1; + aead_request_set_crypt(req, data->sg, data->sg, total + SNAPSHOT_AUTH_TAG= _SIZE, nonce); + res =3D crypto_wait_req(crypto_aead_decrypt(req), &wait); + if (res) + return res; + + data->crypt_size =3D 0; + data->crypt_offset =3D 0; + + /* Push the decrypted pages further down the stack. */ + total =3D 0; + for (pg_idx =3D 0; pg_idx < page_count; pg_idx++) { + void *buf =3D data->crypt_pages[pg_idx]; + + res =3D snapshot_write_next(&data->handle); + if (res < 0) + return res; + if (res =3D=3D 0) + break; + + if (!data_of(data->handle)) + return -EINVAL; + + WARN_ON(res !=3D PAGE_SIZE); + + /* Copy the decrypted page into the snapshot image. */ + memcpy(data_of(data->handle), buf, PAGE_SIZE); + total +=3D PAGE_SIZE; + } + + if (data->crypt_total =3D=3D 0) { + u64 meta_size =3D snapshot_get_meta_page_count() << PAGE_SHIFT; + + data->meta_size =3D meta_size; + if (data->user_key_valid && + data->crypt_meta_size !=3D snapshot_encrypted_byte_count(meta_size)) + return -EINVAL; + } + + data->crypt_total +=3D total; + res =3D snapshot_check_user_key_switch(data); + if (res) + return res; + + return 0; +} + +static ssize_t snapshot_read_next_encrypted(struct snapshot_data *data, + void **buf) +{ + size_t tag_off; + + /* Refill the encrypted buffer if it's empty. */ + if (data->crypt_size =3D=3D 0 || + (data->crypt_offset >=3D + (data->crypt_size + SNAPSHOT_AUTH_TAG_SIZE))) { + int rc; + + data->crypt_size =3D 0; + data->crypt_offset =3D 0; + rc =3D snapshot_encrypt_refill(data); + if (rc < 0) + return rc; + if (!data->crypt_size) + return 0; + } + + /* Return data pages if the offset is in that region. */ + if (data->crypt_offset < data->crypt_size) { + size_t pg_idx =3D data->crypt_offset >> PAGE_SHIFT; + size_t pg_off =3D data->crypt_offset & (PAGE_SIZE - 1); + *buf =3D data->crypt_pages[pg_idx] + pg_off; + return PAGE_SIZE - pg_off; + } + + /* Use offsets just beyond the size to return the tag. */ + tag_off =3D data->crypt_offset - data->crypt_size; + if (tag_off > SNAPSHOT_AUTH_TAG_SIZE) + tag_off =3D SNAPSHOT_AUTH_TAG_SIZE; + + *buf =3D data->auth_tag + tag_off; + return SNAPSHOT_AUTH_TAG_SIZE - tag_off; +} + +static ssize_t snapshot_write_next_encrypted(struct snapshot_data *data, + void **buf) +{ + size_t size_avail; + size_t tag_off; + + /* Return data pages if the offset is in that region. */ + if (data->crypt_offset < (PAGE_SIZE * SNAPSHOT_CHUNK_SIZE)) { + size_t pg_idx =3D data->crypt_offset >> PAGE_SHIFT; + size_t pg_off =3D data->crypt_offset & (PAGE_SIZE - 1); + + *buf =3D data->crypt_pages[pg_idx] + pg_off; + size_avail =3D PAGE_SIZE - pg_off; + } else { + /* Use offsets just beyond the size to return the tag. */ + tag_off =3D data->crypt_offset - (PAGE_SIZE * SNAPSHOT_CHUNK_SIZE); + if (tag_off > SNAPSHOT_AUTH_TAG_SIZE) + tag_off =3D SNAPSHOT_AUTH_TAG_SIZE; + + *buf =3D data->auth_tag + tag_off; + size_avail =3D SNAPSHOT_AUTH_TAG_SIZE - tag_off; + } + + if (data->user_key_valid && data->crypt_meta_size && + data->crypt_stream_total < data->crypt_meta_size) { + u64 meta_avail =3D data->crypt_meta_size - data->crypt_stream_total; + + size_avail =3D min_t(u64, size_avail, meta_avail); + } + + return size_avail; +} + +ssize_t snapshot_read_encrypted(struct snapshot_data *data, + char __user *buf, size_t count, loff_t *offp) +{ + size_t copy_size; + size_t not_done; + size_t pg_off; + void *src; + ssize_t src_size; + + if (!count) + return 0; + + pg_off =3D *offp & (PAGE_SIZE - 1); + count =3D min_t(size_t, count, PAGE_SIZE - pg_off); + + src_size =3D snapshot_read_next_encrypted(data, &src); + if (src_size <=3D 0) + return src_size; + + copy_size =3D min_t(size_t, count, src_size); + not_done =3D copy_to_user(buf, src, copy_size); + copy_size -=3D not_done; + if (!copy_size) + return -EFAULT; + + data->crypt_offset +=3D copy_size; + *offp +=3D copy_size; + return copy_size; +} + +ssize_t snapshot_write_encrypted(struct snapshot_data *data, + const char __user *buf, size_t count, + loff_t *offp) +{ + size_t copy_size; + size_t not_done; + size_t pg_off; + void *dst; + ssize_t dst_size; + int rc; + + if (!count) + return 0; + + pg_off =3D *offp & (PAGE_SIZE - 1); + count =3D min_t(size_t, count, PAGE_SIZE - pg_off); + + dst_size =3D snapshot_write_next_encrypted(data, &dst); + if (dst_size <=3D 0) + return dst_size; + + copy_size =3D min_t(size_t, count, dst_size); + not_done =3D copy_from_user(dst, buf, copy_size); + copy_size -=3D not_done; + if (!copy_size) + return -EFAULT; + + data->crypt_offset +=3D copy_size; + data->crypt_stream_total +=3D copy_size; + /* + * Drain the encrypted buffer if it's full, or if we hit the end of the + * encrypted metadata and need a key change before user data pages. + */ + if (data->crypt_offset >=3D + (PAGE_SIZE * SNAPSHOT_CHUNK_SIZE) + SNAPSHOT_AUTH_TAG_SIZE || + (data->user_key_valid && data->crypt_meta_size && + data->crypt_stream_total =3D=3D data->crypt_meta_size)) { + rc =3D snapshot_decrypt_drain(data); + if (rc < 0) + return rc; + } + + *offp +=3D copy_size; + return copy_size; +} + +static void snapshot_reset_encryption_state(struct snapshot_data *data) +{ + data->crypt_offset =3D 0; + data->crypt_size =3D 0; + data->crypt_total =3D 0; + data->crypt_stream_total =3D 0; + data->nonce_low =3D 0; + data->nonce_high =3D 0; + data->meta_size =3D 0; + data->crypt_meta_size =3D 0; + data->user_key_valid =3D false; + memset(data->auth_tag, 0, sizeof(data->auth_tag)); +} + +void snapshot_teardown_encryption(struct snapshot_data *data) +{ + int i; + + if (data->aead_req) { + aead_request_free(data->aead_req); + data->aead_req =3D NULL; + } + + if (data->aead_tfm) { + crypto_free_aead(data->aead_tfm); + data->aead_tfm =3D NULL; + } + + for (i =3D 0; i < SNAPSHOT_CHUNK_SIZE; i++) { + if (data->crypt_pages[i]) { + free_page((unsigned long)data->crypt_pages[i]); + data->crypt_pages[i] =3D NULL; + } + } + + memzero_explicit(data->encryption_key, sizeof(data->encryption_key)); + memzero_explicit(data->user_key, sizeof(data->user_key)); + snapshot_reset_encryption_state(data); +} + +static int snapshot_setup_encryption_common(struct snapshot_data *data) +{ + int i, rc; + + /* This only works once per hibernate. */ + if (data->aead_tfm) + return -EINVAL; + + snapshot_reset_encryption_state(data); + memset(data->crypt_pages, 0, sizeof(data->crypt_pages)); + + /* Set up the encryption transform */ + data->aead_tfm =3D crypto_alloc_aead("gcm(aes)", 0, 0); + if (IS_ERR(data->aead_tfm)) { + rc =3D PTR_ERR(data->aead_tfm); + data->aead_tfm =3D NULL; + return rc; + } + + rc =3D -ENOMEM; + data->aead_req =3D aead_request_alloc(data->aead_tfm, GFP_KERNEL); + if (!data->aead_req) + goto setup_fail; + + /* Allocate the staging area */ + for (i =3D 0; i < SNAPSHOT_CHUNK_SIZE; i++) { + data->crypt_pages[i] =3D (void *)__get_free_page(GFP_KERNEL); + if (!data->crypt_pages[i]) + goto setup_fail; + } + + sg_init_table(data->sg, SNAPSHOT_CHUNK_SIZE + 2); + + /* + * The associated data will be the offset so that blocks can't be + * rearranged. + */ + aead_request_set_ad(data->aead_req, sizeof(data->crypt_total)); + rc =3D crypto_aead_setauthsize(data->aead_tfm, SNAPSHOT_AUTH_TAG_SIZE); + if (rc) + goto setup_fail; + + return 0; + +setup_fail: + snapshot_teardown_encryption(data); + return rc; +} + +int snapshot_get_encryption_key(struct snapshot_data *data, + struct uswsusp_key_blob __user *key) +{ + struct snapshot_wrapped_key wrapped =3D {}; + u8 image_key[SNAPSHOT_ENCRYPTION_KEY_SIZE] __nonstring =3D {}; + u8 nonce[USWSUSP_KEY_NONCE_SIZE]; + int rc; + + /* Don't pull a random key from a world that can be reset. */ + if (data->ready) + return -EPIPE; + + rc =3D snapshot_setup_encryption_common(data); + if (rc) + return rc; + + /* Build a random starting nonce. */ + get_random_bytes(nonce, sizeof(nonce)); + memcpy(&data->nonce_low, &nonce[0], sizeof(data->nonce_low)); + memcpy(&data->nonce_high, &nonce[8], sizeof(data->nonce_high)); + + /* Build and install a random image encryption key. */ + get_random_bytes(image_key, sizeof(image_key)); + rc =3D snapshot_install_image_key(data, image_key); + if (rc) + goto fail; + + rc =3D snapshot_wrap_image_key(image_key, &wrapped); + if (rc) + goto fail; + + /* Hand the wrapped key and clear nonce back to user mode. */ + rc =3D put_user(sizeof(wrapped), &key->blob_len); + if (rc) + goto fail; + + BUILD_BUG_ON(sizeof(wrapped) > + sizeof(((struct uswsusp_key_blob *)0)->blob)); + if (copy_to_user(&key->blob, &wrapped, sizeof(wrapped))) { + rc =3D -EFAULT; + goto fail; + } + + if (copy_to_user(&key->nonce, &nonce, sizeof(nonce))) { + rc =3D -EFAULT; + goto fail; + } + + memzero_explicit(image_key, sizeof(image_key)); + memzero_explicit(&wrapped, sizeof(wrapped)); + return 0; + +fail: + memzero_explicit(image_key, sizeof(image_key)); + memzero_explicit(&wrapped, sizeof(wrapped)); + snapshot_teardown_encryption(data); + return rc; +} + +int snapshot_set_encryption_key(struct snapshot_data *data, + struct uswsusp_key_blob __user *key) +{ + struct uswsusp_key_blob *blob; + struct snapshot_wrapped_key wrapped =3D {}; + u8 image_key[SNAPSHOT_ENCRYPTION_KEY_SIZE] __nonstring =3D {}; + int rc; + + /* It's too late if data's been pushed in. */ + if (data->handle.cur) + return -EPIPE; + + rc =3D snapshot_setup_encryption_common(data); + if (rc) + return rc; + + /* Load the key from user mode. */ + blob =3D memdup_user(key, sizeof(*blob)); + if (IS_ERR(blob)) { + rc =3D PTR_ERR(blob); + goto crypto_setup_fail; + } + + if (blob->blob_len !=3D sizeof(wrapped)) { + rc =3D -EINVAL; + goto out_blob; + } + + memcpy(&wrapped, blob->blob, sizeof(wrapped)); + rc =3D snapshot_unwrap_image_key(&wrapped, image_key); + if (rc) + goto out_blob; + + rc =3D snapshot_install_image_key(data, image_key); + if (rc) + goto out_blob; + + /* Load the starting nonce. */ + memcpy(&data->nonce_low, &blob->nonce[0], sizeof(data->nonce_low)); + memcpy(&data->nonce_high, &blob->nonce[8], sizeof(data->nonce_high)); + +out_blob: + kfree_sensitive(blob); + memzero_explicit(&wrapped, sizeof(wrapped)); + memzero_explicit(image_key, sizeof(image_key)); + if (rc) + goto crypto_setup_fail; + + return 0; + +crypto_setup_fail: + memzero_explicit(&wrapped, sizeof(wrapped)); + memzero_explicit(image_key, sizeof(image_key)); + snapshot_teardown_encryption(data); + return rc; +} + +static loff_t snapshot_encrypted_byte_count(loff_t plain_size) +{ + loff_t pages =3D plain_size >> PAGE_SHIFT; + loff_t chunks =3D (pages + (SNAPSHOT_CHUNK_SIZE - 1)) / SNAPSHOT_CHUNK_SI= ZE; + /* + * The encrypted size is the normal size, plus a stitched in + * authentication tag for every chunk of pages. + */ + return plain_size + (chunks * SNAPSHOT_AUTH_TAG_SIZE); +} + +static loff_t snapshot_encrypted_split_byte_count(loff_t raw_size, + loff_t meta_plain_size) +{ + if (raw_size <=3D meta_plain_size) + return snapshot_encrypted_byte_count(raw_size); + + return snapshot_encrypted_byte_count(meta_plain_size) + + snapshot_encrypted_byte_count(raw_size - meta_plain_size); +} + +int snapshot_set_user_key(struct snapshot_data *data, + struct uswsusp_user_key __user *key) +{ + struct uswsusp_user_key user_key =3D {}; + unsigned int key_len; + u64 size; + int rc; + + if (!snapshot_encryption_enabled(data)) + return -EINVAL; + + if (copy_from_user(&user_key, key, sizeof(struct uswsusp_user_key))) + return -EFAULT; + + BUILD_BUG_ON(sizeof(data->user_key) < sizeof(user_key.key)); + rc =3D -EINVAL; + if (user_key.reserved) + goto out; + if (user_key.key_len > sizeof(data->user_key)) + goto out; + if (user_key.key_len < 8) + goto out; + + if (data->mode =3D=3D O_RDONLY && !data->ready) { + rc =3D -ENODATA; + goto out; + } + + /* + * Return the metadata size, the number of bytes that can be fed in before + * the user data key is needed at resume time. + */ + if (data->mode =3D=3D O_WRONLY && !data->meta_size) { + if (user_key.meta_size < PAGE_SIZE + SNAPSHOT_AUTH_TAG_SIZE) + goto out; + + data->crypt_meta_size =3D user_key.meta_size; + size =3D data->crypt_meta_size; + } else { + snapshot_set_meta_size(data, + snapshot_get_meta_page_count() << PAGE_SHIFT); + size =3D data->crypt_meta_size; + } + + rc =3D put_user(size, &key->meta_size); + if (rc) + goto out; + + key_len =3D user_key.key_len; + + /* Don't allow it if it's too late. */ + if ((data->meta_size && data->crypt_total > data->meta_size) || + (data->crypt_meta_size && + data->crypt_stream_total > data->crypt_meta_size)) { + rc =3D -EBUSY; + goto out; + } + + /* + * Resume may preload the user key before writing any image data, or + * install it after writing exactly the saved metadata stream. In the + * latter case, authenticate and publish the buffered metadata under the + * original image key before switching keys. + */ + if (data->mode =3D=3D O_WRONLY && data->crypt_meta_size && + data->crypt_stream_total =3D=3D data->crypt_meta_size && + data->crypt_offset) { + rc =3D snapshot_decrypt_drain(data); + if (rc) + goto out; + if (data->crypt_total !=3D data->meta_size) { + rc =3D -EINVAL; + goto out; + } + } + + memset(data->user_key, 0, sizeof(data->user_key)); + memcpy(data->user_key, user_key.key, key_len); + data->user_key_valid =3D true; + /* Install the key if the user is just under the wire. */ + rc =3D snapshot_check_user_key_switch(data); + if (rc) + goto out; + + rc =3D 0; + +out: + memzero_explicit(&user_key, sizeof(user_key)); + return rc; +} + +loff_t snapshot_get_encrypted_image_size(struct snapshot_data *data, + loff_t raw_size) +{ + loff_t meta_plain_size =3D data->meta_size; + loff_t split_size; + + if (!meta_plain_size) + meta_plain_size =3D snapshot_get_meta_page_count() << PAGE_SHIFT; + + split_size =3D snapshot_encrypted_split_byte_count(raw_size, + meta_plain_size); + if (!data->user_key_valid) { + /* + * Userspace may install a user key after querying the image size. + * Reserve enough space for either framing so that later splitting at + * the metadata boundary cannot make the reported size too small. + */ + return max(snapshot_encrypted_byte_count(raw_size), split_size); + } + + return split_size; +} + +int snapshot_finalize_decrypted_image(struct snapshot_data *data) +{ + int rc; + + if (data->crypt_offset !=3D 0) { + rc =3D snapshot_decrypt_drain(data); + if (rc) + return rc; + } + + return 0; +} diff --git a/kernel/power/snapshot.c b/kernel/power/snapshot.c index b209712cb2c3..d1a226a25d27 100644 --- a/kernel/power/snapshot.c +++ b/kernel/power/snapshot.c @@ -2177,6 +2177,11 @@ unsigned long snapshot_get_image_size(void) return nr_copy_pages + nr_meta_pages + 1; } =20 +unsigned long snapshot_get_meta_page_count(void) +{ + return nr_meta_pages + 1; +} + static int init_header(struct swsusp_info *info) { memset(info, 0, sizeof(struct swsusp_info)); diff --git a/kernel/power/user.c b/kernel/power/user.c index d0fcfba7ac23..07dbb1e82847 100644 --- a/kernel/power/user.c +++ b/kernel/power/user.c @@ -25,19 +25,10 @@ #include =20 #include "power.h" +#include "user.h" =20 static bool need_wait; - -static struct snapshot_data { - struct snapshot_handle handle; - int swap; - int mode; - bool frozen; - bool ready; - bool platform_support; - bool free_bitmaps; - dev_t dev; -} snapshot_state; +static struct snapshot_data snapshot_state; =20 int is_hibernate_resume_dev(dev_t dev) { @@ -57,13 +48,13 @@ static int snapshot_open(struct inode *inode, struct fi= le *filp) =20 if (!hibernate_acquire()) { error =3D -EBUSY; - goto Unlock; + goto unlock; } =20 if ((filp->f_flags & O_ACCMODE) =3D=3D O_RDWR) { hibernate_release(); error =3D -ENOSYS; - goto Unlock; + goto unlock; } nonseekable_open(inode, filp); data =3D &snapshot_state; @@ -100,7 +91,7 @@ static int snapshot_open(struct inode *inode, struct fil= e *filp) data->platform_support =3D false; data->dev =3D 0; =20 - Unlock: + unlock: unlock_system_sleep(sleep_flags); =20 return error; @@ -125,6 +116,7 @@ static int snapshot_release(struct inode *inode, struct= file *filp) } else if (data->free_bitmaps) { free_basic_memory_bitmaps(); } + snapshot_teardown_encryption(data); pm_notifier_call_chain(data->mode =3D=3D O_RDONLY ? PM_POST_HIBERNATION : PM_POST_RESTORE); hibernate_release(); @@ -147,12 +139,18 @@ static ssize_t snapshot_read(struct file *filp, char = __user *buf, data =3D filp->private_data; if (!data->ready) { res =3D -ENODATA; - goto Unlock; + goto unlock; + } + + if (snapshot_encryption_enabled(data)) { + res =3D snapshot_read_encrypted(data, buf, count, offp); + goto unlock; } + if (!pg_offp) { /* on page boundary? */ res =3D snapshot_read_next(&data->handle); if (res <=3D 0) - goto Unlock; + goto unlock; } else { res =3D PAGE_SIZE - pg_offp; } @@ -162,7 +160,7 @@ static ssize_t snapshot_read(struct file *filp, char __= user *buf, if (res > 0) *offp +=3D res; =20 - Unlock: + unlock: unlock_system_sleep(sleep_flags); =20 return res; @@ -185,6 +183,11 @@ static ssize_t snapshot_write(struct file *filp, const= char __user *buf, =20 data =3D filp->private_data; =20 + if (snapshot_encryption_enabled(data)) { + res =3D snapshot_write_encrypted(data, buf, count, offp); + goto unlock; + } + if (!pg_offp) { res =3D snapshot_write_next(&data->handle); if (res <=3D 0) @@ -328,6 +331,12 @@ static long snapshot_ioctl(struct file *filp, unsigned= int cmd, break; =20 case SNAPSHOT_ATOMIC_RESTORE: + if (snapshot_encryption_enabled(data)) { + error =3D snapshot_finalize_decrypted_image(data); + if (error) + break; + } + error =3D snapshot_write_finalize(&data->handle); if (error) break; @@ -346,6 +355,7 @@ static long snapshot_ioctl(struct file *filp, unsigned = int cmd, swsusp_free(); memset(&data->handle, 0, sizeof(struct snapshot_handle)); data->ready =3D false; + snapshot_teardown_encryption(data); /* * It is necessary to thaw kernel threads here, because * SNAPSHOT_CREATE_IMAGE may be invoked directly after @@ -368,6 +378,8 @@ static long snapshot_ioctl(struct file *filp, unsigned = int cmd, } size =3D snapshot_get_image_size(); size <<=3D PAGE_SHIFT; + if (snapshot_encryption_enabled(data)) + size =3D snapshot_get_encrypted_image_size(data, size); error =3D put_user(size, (loff_t __user *)arg); break; =20 @@ -425,6 +437,17 @@ static long snapshot_ioctl(struct file *filp, unsigned= int cmd, error =3D snapshot_set_swap_area(data, (void __user *)arg); break; =20 + case SNAPSHOT_ENABLE_ENCRYPTION: + if (data->mode =3D=3D O_RDONLY) + error =3D snapshot_get_encryption_key(data, (void __user *)arg); + else + error =3D snapshot_set_encryption_key(data, (void __user *)arg); + break; + + case SNAPSHOT_SET_USER_KEY: + error =3D snapshot_set_user_key(data, (void __user *)arg); + break; + default: error =3D -ENOTTY; =20 @@ -448,6 +471,8 @@ snapshot_compat_ioctl(struct file *file, unsigned int c= md, unsigned long arg) case SNAPSHOT_ALLOC_SWAP_PAGE: case SNAPSHOT_CREATE_IMAGE: case SNAPSHOT_SET_SWAP_AREA: + case SNAPSHOT_ENABLE_ENCRYPTION: + case SNAPSHOT_SET_USER_KEY: return snapshot_ioctl(file, cmd, (unsigned long) compat_ptr(arg)); default: diff --git a/kernel/power/user.h b/kernel/power/user.h new file mode 100644 index 000000000000..b0c20e5d5ee9 --- /dev/null +++ b/kernel/power/user.h @@ -0,0 +1,139 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#ifndef __POWER_USER_H +#define __POWER_USER_H + +#include +#include +#include +#include +#include + +#define SNAPSHOT_ENCRYPTION_KEY_SIZE AES_KEYSIZE_128 +#define SNAPSHOT_AUTH_TAG_SIZE 16 + +/* Define the number of pages in a single AEAD encryption chunk. */ +#define SNAPSHOT_CHUNK_SIZE 16 + +struct snapshot_data { + struct snapshot_handle handle; + int swap; + int mode; + bool frozen; + bool ready; + bool platform_support; + bool free_bitmaps; + dev_t dev; + +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + struct crypto_aead *aead_tfm; + struct aead_request *aead_req; + void *crypt_pages[SNAPSHOT_CHUNK_SIZE]; + u8 auth_tag[SNAPSHOT_AUTH_TAG_SIZE]; + struct scatterlist sg[SNAPSHOT_CHUNK_SIZE + 2]; /* Add room for AD and au= th tag. */ + size_t crypt_offset; + size_t crypt_size; + u64 crypt_total; + u64 crypt_stream_total; + u64 nonce_low; + u64 nonce_high; + u8 encryption_key[SNAPSHOT_ENCRYPTION_KEY_SIZE] __nonstring; + u8 user_key[USWSUSP_USER_KEY_SIZE] __nonstring; + bool user_key_valid; + u64 meta_size; + u64 crypt_meta_size; +#endif + +}; + +/* kernel/power/snapenc.c routines */ +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + +ssize_t snapshot_read_encrypted(struct snapshot_data *data, + char __user *buf, size_t count, loff_t *offp); + +ssize_t snapshot_write_encrypted(struct snapshot_data *data, + const char __user *buf, size_t count, + loff_t *offp); + +void snapshot_teardown_encryption(struct snapshot_data *data); +int snapshot_get_encryption_key(struct snapshot_data *data, + struct uswsusp_key_blob __user *key); + +int snapshot_set_encryption_key(struct snapshot_data *data, + struct uswsusp_key_blob __user *key); + +int snapshot_set_user_key(struct snapshot_data *data, + struct uswsusp_user_key __user *key); + +int snapshot_store_encryption_seed(const char *buf, size_t count); + +loff_t snapshot_get_encrypted_image_size(struct snapshot_data *data, + loff_t raw_size); + +int snapshot_finalize_decrypted_image(struct snapshot_data *data); + +static inline bool snapshot_encryption_enabled(struct snapshot_data *data) +{ + return data->aead_tfm; +} + +#else + +static inline ssize_t snapshot_read_encrypted(struct snapshot_data *data, + char __user *buf, size_t count, + loff_t *offp) +{ + return -ENOTTY; +} + +static inline ssize_t snapshot_write_encrypted(struct snapshot_data *data, + const char __user *buf, + size_t count, loff_t *offp) +{ + return -ENOTTY; +} + +static inline void snapshot_teardown_encryption(struct snapshot_data *data= ) {} +static inline int snapshot_get_encryption_key(struct snapshot_data *data, + struct uswsusp_key_blob __user *key) +{ + return -ENOTTY; +} + +static inline int snapshot_set_encryption_key(struct snapshot_data *data, + struct uswsusp_key_blob __user *key) +{ + return -ENOTTY; +} + +static inline int snapshot_set_user_key(struct snapshot_data *data, + struct uswsusp_user_key __user *key) +{ + return -ENOTTY; +} + +static inline int snapshot_store_encryption_seed(const char *buf, size_t c= ount) +{ + return -ENOTTY; +} + +static inline loff_t snapshot_get_encrypted_image_size(struct snapshot_dat= a *data, + loff_t raw_size) +{ + return raw_size; +} + +static inline int snapshot_finalize_decrypted_image(struct snapshot_data *= data) +{ + return -ENOTTY; +} + +static inline bool snapshot_encryption_enabled(struct snapshot_data *data) +{ + return false; +} + +#endif + +#endif /* __POWER_USER_H */ From nobody Fri Sep 25 02:08:28 2026 Received: from mail-wm2-f12.google.com (mail-wm2-f12.google.com [74.125.225.140]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C8D5D4D0CDF for ; Thu, 17 Sep 2026 13:29:59 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.225.140 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789651806; cv=none; b=JayRXrxGOu13bWePFb4jIpVBtgyTk42KFbT9XKc9VxL2HDjV4N87IZKT4aBFbRjm+ECR8FyUF+VWB4AYQbaNeY4pZz5A1VEOGwO1RtIXbTeDufTCZ/uL+CpDyQzFAlPr+v6zvapDnX/C3fGmdByQFZawHrG+ZPthP85Rj1ylMkk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789651806; c=relaxed/simple; bh=awwlgzxtXDrXgtIayBPQhLrxnIbxDh2MzbBIZ+zdxQA=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=dE8l7StxQFptUwigTEbYBn4ODKAZUGw6p9jShCXcbqTQhYhlUpPjPWMley9Aaief5wNupmJCC5eqf4UiM9FS1unw7SS6FDvRyXMHmDfzFDDt8bjFDpwEe0p9B7cQoAAQyzTVg1ys3ci35GpjEICkKuf08WSrc1H3z3crPhELldw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=starlabs.systems; spf=pass smtp.mailfrom=starlabs.systems; dkim=pass (2048-bit key) header.d=starlabs-systems.20251104.gappssmtp.com header.i=@starlabs-systems.20251104.gappssmtp.com header.b=yw/YPe+D; arc=none smtp.client-ip=74.125.225.140 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=starlabs.systems Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=starlabs.systems Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=starlabs-systems.20251104.gappssmtp.com header.i=@starlabs-systems.20251104.gappssmtp.com header.b="yw/YPe+D" Received: by mail-wm2-f12.google.com with SMTP id 5b1f17b1804b1-49ccfd61ecaso8009425e9.3 for ; Thu, 17 Sep 2026 06:29:59 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=starlabs-systems.20251104.gappssmtp.com; s=20251104; t=1789651797; x=1790256597; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=GKh+va21iR5NhJ1aJ3Y13UNlm+0FMnv30u4Z223eMLE=; b=yw/YPe+DPFXVi0kIGM0sj5v+OJ68LTADyQ9640vxAf6VZZkJzrjDEjhpVYppL2153j Cr2QmLlObfNCXEvAFULJ+5GCtayg6Dr7WSW7fqRAiR1lX+uCNULQ+wadziyJkaPCHWm2 86RkRe8vHZIDJe4Cuy9oY54nuCguMZv5slnCkgK32XiaPQk+H/qNx8CrGMP1RsJikqvv OgEdB9YYA5AD29IXiTQv/cL6Ox2pcOf2DKa8F76SbpaQiXAyza1woo7oq/k5ZW0hqpRQ hK+8ktVCEeYY9fXsC/+kpHnr3KmAO/4ZNnVmE6GIwqfT6rqZMnMWyXaPc7MtCkb9F6rm 9H+w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1789651797; x=1790256597; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=GKh+va21iR5NhJ1aJ3Y13UNlm+0FMnv30u4Z223eMLE=; b=ZsVVFqcleBjBY4H1fAyJ0FHrxJ81s83/DFj6s9W9cZ19bMfbAUyd80jc8voyg+juKj FzzYRvIHl6W0Ib4/D/tE7wwtiJP5HDgdxNu38htEJ12b65Vg1QMBnUmzA78ADthFKNpO vYaGvUygozijeHfmI4j1mRSRnWzEVc8fDUxysSPXbIfIYA92hXd3LXyu2D73wEGyUE1p 0/QOaEC2V+2eBQh4OZwN//lF67xWZER9gsefMJviE63F907hDRfS36pFpWlFmQLwj2wP zKapul9BBWTwhY/wIPIvy9rPyXPnAxzWFALCF9r7IFLWU/orMxPW3GZ1AjcPK0SgSJP5 k4bQ== X-Forwarded-Encrypted: i=1; AKwUvByO6d3zN8z0EstTr3T1CkGsvegc75ppuRmBS4SYLZXRCDj3rK1hGMUC8c33FYDNQpHqTqm/jOBa/ehQSW8=@vger.kernel.org X-Gm-Message-State: AFuF++kDKDec1B1AveaRvEKCu3nhL3Q5leEv5eAe2pvTNcIlc8+8Jx8K juEd2Zp8WwLLRwGqeJkwH5tmBtFtnDp5T/0YwqGib1Hg96DSL5fqrCxrDypAQtqBFQ== X-Gm-Gg: AYBFou254YaFtJfTPtYlFhZGyRUJoIIscmCQv2R3GNCNyjDxaovUMNCUuhsyFiTGwDd R1TZJl9RU1/7Q+exhrSF1LtiX839hnclaCjIgTlX+FaelCz8gLzTV5T7fCaSRzAXNEvrxINcRZT kW8tOG0oW8M95oqJ458wjuwwqKT25C0TOzlAKFadhsOdW7SEOGCLSiIfsd8v5O5qXV/aH5zdEdj cirSHpSqwUL5BEAyLATH/+Rs1737nP5FiYqi5H6pmwJL5SfgBgn6NgW5f17nIsLxmwv80aBGVv8 0PTjQ4vPwdMXoXrGW5ufjdbpG4EpriKNPghJJssYWo+5Lf5uRps6EsIaUVAtOQItn0Q+0Vaw42f lw2gDfFykYgt1w9SC6MnGaVq1qZPcwV7mr0MoUZLZmKPDl8E0j6QaNc5Qn3LsiGr9HaKsVJhFIc pimABCbVeaDhJhfEOkfpt3g+bxpB8oocLwq4Ucf5Wqhh4cpCfySjWEAYYYaJRXj11UT6w9V4Kob KMrwgu9WPBZBnEdHvuA8Z9DDEEqH+RwOgb8fA== X-Received: by 2002:a05:600c:314a:b0:49e:799e:c1c3 with SMTP id 5b1f17b1804b1-49eb73263c6mr82073545e9.17.1789651796582; Thu, 17 Sep 2026 06:29:56 -0700 (PDT) Received: from fighter ([216.128.28.240]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-49fbd2356d1sm97548275e9.6.2026.09.17.06.29.54 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 17 Sep 2026 06:29:55 -0700 (PDT) From: Sean Rhodes To: linux-pm@vger.kernel.org Cc: "Rafael J. Wysocki" , Len Brown , Pavel Machek , Evan Green , Sean Rhodes , linux-kernel@vger.kernel.org, Jens Axboe , Andrew Morton , Chris Li , Kairui Song , Kemeng Shi , Nhat Pham , Baoquan He , Barry Song , Youngjun Park , linux-block@vger.kernel.org, linux-mm@kvack.org, Xueqin Luo , Nicolas Bouchinet Subject: [PATCH v3 RESEND 2/4] PM: hibernate: confine encrypted snapshot writes Date: Thu, 17 Sep 2026 14:29:48 +0100 Message-ID: X-Mailer: git-send-email 2.53.0 In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Userspace writes a uswsusp image through the normal block path. Before allowing encrypted hibernation under lockdown, restrict direct writes to the process that owns the active snapshot, the swap extents allocated through the snapshot API, the recorded swap-header page, and the total number of pages allocated for the image. Apply the checks to the active swap backend even when its block-device inode is not itself marked as swap. Require synchronous buffered writes so reservation accounting follows completed I/O, and release reservations for partial writes. Signed-off-by: Sean Rhodes --- block/fops.c | 58 ++++++++--- include/linux/suspend.h | 24 +++++ include/linux/swap.h | 9 ++ kernel/power/power.h | 1 + kernel/power/swap.c | 87 +++++++++++++++++ kernel/power/user.c | 211 ++++++++++++++++++++++++++++++++++++++++ kernel/power/user.h | 11 +++ mm/swapfile.c | 25 +++++ 8 files changed, 413 insertions(+), 13 deletions(-) diff --git a/block/fops.c b/block/fops.c index 2ce7c6c4714e..bdc9f2cf7156 100644 --- a/block/fops.c +++ b/block/fops.c @@ -725,43 +725,68 @@ static ssize_t blkdev_write_iter(struct kiocb *iocb, = struct iov_iter *from) struct file *file =3D iocb->ki_filp; struct inode *bd_inode =3D bdev_file_inode(file); struct block_device *bdev =3D I_BDEV(bd_inode); + dev_t dev =3D bd_inode->i_rdev; bool atomic =3D iocb->ki_flags & IOCB_ATOMIC; loff_t size =3D bdev_nr_bytes(bdev); + enum hibernate_snapshot_write hibernate_write; + bool hibernate_active; + size_t hibernate_len =3D 0; size_t shorted =3D 0; + ssize_t hibernate_written =3D 0; ssize_t ret; =20 if (bdev_read_only(bdev)) return -EPERM; =20 - if (IS_SWAPFILE(bd_inode) && !is_hibernate_resume_dev(bd_inode->i_rdev)) - return -ETXTBSY; + hibernate_active =3D hibernate_snapshot_write_active(dev); + if ((IS_SWAPFILE(bd_inode) && !is_hibernate_resume_dev(dev)) || + hibernate_active) { + if (!is_sync_kiocb(iocb) || !iocb_is_dsync(iocb) || + (iocb->ki_flags & IOCB_DIRECT)) + return -ETXTBSY; + + hibernate_len =3D iov_iter_count(from); + hibernate_write =3D hibernate_snapshot_write_begin(dev, iocb->ki_pos, hi= bernate_len); + if (hibernate_write =3D=3D HIBERNATE_SNAPSHOT_WRITE_NONE) + return -ETXTBSY; + } else { + hibernate_write =3D HIBERNATE_SNAPSHOT_WRITE_NONE; + } =20 - if (!iov_iter_count(from)) - return 0; + if (!iov_iter_count(from)) { + ret =3D 0; + goto out_hibernate; + } =20 - if (iocb->ki_pos >=3D size) - return -ENOSPC; + if (iocb->ki_pos >=3D size) { + ret =3D -ENOSPC; + goto out_hibernate; + } =20 - if ((iocb->ki_flags & (IOCB_NOWAIT | IOCB_DIRECT)) =3D=3D IOCB_NOWAIT) - return -EOPNOTSUPP; + if ((iocb->ki_flags & (IOCB_NOWAIT | IOCB_DIRECT)) =3D=3D IOCB_NOWAIT) { + ret =3D -EOPNOTSUPP; + goto out_hibernate; + } =20 if (atomic) { ret =3D generic_atomic_write_valid(iocb, from); if (ret) - return ret; + goto out_hibernate; } =20 size -=3D iocb->ki_pos; if (iov_iter_count(from) > size) { - if (atomic) - return -EINVAL; + if (atomic) { + ret =3D -EINVAL; + goto out_hibernate; + } shorted =3D iov_iter_count(from) - size; iov_iter_truncate(from, size); } =20 ret =3D file_update_time(file); if (ret) - return ret; + goto out_hibernate; =20 if (iocb->ki_flags & IOCB_DIRECT) { ret =3D blkdev_direct_write(iocb, from); @@ -779,9 +804,16 @@ static ssize_t blkdev_write_iter(struct kiocb *iocb, s= truct iov_iter *from) inode_unlock_shared(bd_inode); } =20 - if (ret > 0) + if (ret > 0) { + hibernate_written =3D ret; ret =3D generic_write_sync(iocb, ret); + } iov_iter_reexpand(from, iov_iter_count(from) + shorted); + +out_hibernate: + if (hibernate_len) + hibernate_snapshot_write_end(hibernate_write, hibernate_len, + hibernate_written); return ret; } =20 diff --git a/include/linux/suspend.h b/include/linux/suspend.h index b02876f1ae38..a2a8863f89cf 100644 --- a/include/linux/suspend.h +++ b/include/linux/suspend.h @@ -426,10 +426,34 @@ static inline bool pm_hibernation_mode_is_suspend(voi= d) { return false; } =20 int arch_resume_nosmt(void); =20 +enum hibernate_snapshot_write { + HIBERNATE_SNAPSHOT_WRITE_NONE, + HIBERNATE_SNAPSHOT_WRITE_IMAGE, + HIBERNATE_SNAPSHOT_WRITE_HEADER, +}; + #ifdef CONFIG_HIBERNATION_SNAPSHOT_DEV int is_hibernate_resume_dev(dev_t dev); +bool hibernate_snapshot_write_active(dev_t dev); +enum hibernate_snapshot_write +hibernate_snapshot_write_begin(dev_t dev, loff_t pos, size_t count); +void hibernate_snapshot_write_end(enum hibernate_snapshot_write type, + size_t reserved, ssize_t written); #else static inline int is_hibernate_resume_dev(dev_t dev) { return 0; } +static inline bool hibernate_snapshot_write_active(dev_t dev) { return fal= se; } + +static inline enum hibernate_snapshot_write +hibernate_snapshot_write_begin(dev_t dev, loff_t pos, size_t count) +{ + return HIBERNATE_SNAPSHOT_WRITE_NONE; +} + +static inline void hibernate_snapshot_write_end(enum hibernate_snapshot_wr= ite type, + size_t reserved, + ssize_t written) +{ +} #endif =20 /* Hibernation and suspend events */ diff --git a/include/linux/swap.h b/include/linux/swap.h index 5658a1634b85..deab983ef3d6 100644 --- a/include/linux/swap.h +++ b/include/linux/swap.h @@ -377,6 +377,8 @@ extern int find_hibernation_swap_type(dev_t device, sec= tor_t offset); int find_first_swap(dev_t *device); extern unsigned int count_swap_pages(int, int); extern sector_t swapdev_block(int, pgoff_t); +int swapdev_block_to_extent(int type, sector_t block, pgoff_t *offset, + pgoff_t *nr_pages); extern int __swap_count(swp_entry_t entry); extern bool swap_entry_swapped(struct swap_info_struct *si, swp_entry_t en= try); extern int swp_swapcount(swp_entry_t entry); @@ -472,6 +474,13 @@ static inline int add_swap_extent(struct swap_info_str= uct *sis, { return -EINVAL; } + +static inline int swapdev_block_to_extent(int type, sector_t block, + pgoff_t *offset, + pgoff_t *nr_pages) +{ + return -EINVAL; +} #endif /* CONFIG_SWAP */ #ifdef CONFIG_MEMCG void lru_reparent_memcg(struct mem_cgroup *memcg, struct mem_cgroup *paren= t, int nid); diff --git a/kernel/power/power.h b/kernel/power/power.h index e080230f97fc..815c84c1e0dd 100644 --- a/kernel/power/power.h +++ b/kernel/power/power.h @@ -170,6 +170,7 @@ extern bool hibernate_acquire(void); extern void hibernate_release(void); =20 extern sector_t alloc_swapdev_block(int swap); +bool swsusp_swap_range_allocated(int swap, loff_t pos, size_t count); extern void free_all_swap_pages(int swap); extern int swsusp_swap_in_use(void); =20 diff --git a/kernel/power/swap.c b/kernel/power/swap.c index c78f1593600b..ac736d2b59e3 100644 --- a/kernel/power/swap.c +++ b/kernel/power/swap.c @@ -166,6 +166,93 @@ static int swsusp_extents_insert(unsigned long swap_of= fset) return 0; } =20 +static bool swsusp_extents_contain_range(unsigned long swap_offset, + unsigned long nr_pages) +{ + struct rb_node *node =3D swsusp_extents.rb_node; + struct swsusp_extent *ext; + unsigned long end; + + if (!nr_pages) + return false; + + end =3D swap_offset + nr_pages - 1; + if (end < swap_offset) + return false; + + while (node) { + ext =3D rb_entry(node, struct swsusp_extent, node); + if (swap_offset < ext->start) + node =3D node->rb_left; + else if (swap_offset > ext->end) + node =3D node->rb_right; + else + goto found; + } + + return false; + +found: + for (;;) { + if (swap_offset < ext->start) + return false; + if (end <=3D ext->end) + return true; + if (ext->end =3D=3D (unsigned long)-1) + return false; + + swap_offset =3D ext->end + 1; + node =3D rb_next(&ext->node); + if (!node) + return false; + + ext =3D rb_entry(node, struct swsusp_extent, node); + } +} + +bool swsusp_swap_range_allocated(int swap, loff_t pos, size_t count) +{ + u64 block; + u64 end; + u64 end_block; + + if (swap < 0 || pos < 0 || !count) + return false; + + end =3D (u64)pos + count - 1; + if (end < (u64)pos) + return false; + + block =3D (u64)pos >> PAGE_SHIFT; + end_block =3D end >> PAGE_SHIFT; + + for (;;) { + u64 remaining_pages =3D end_block - block + 1; + pgoff_t swap_offset; + pgoff_t mapped_pages; + sector_t page_block =3D block; + u64 pages; + + if (!remaining_pages) + return false; + if ((u64)page_block !=3D block) + return false; + if (swapdev_block_to_extent(swap, page_block, &swap_offset, + &mapped_pages)) + return false; + if (!mapped_pages) + return false; + pages =3D min_t(u64, mapped_pages, remaining_pages); + if ((u64)(unsigned long)pages !=3D pages) + return false; + if (!swsusp_extents_contain_range(swap_offset, pages)) + return false; + if (pages =3D=3D remaining_pages) + return true; + block +=3D pages; + } +} + sector_t alloc_swapdev_block(int swap) { unsigned long offset; diff --git a/kernel/power/user.c b/kernel/power/user.c index 07dbb1e82847..de41f590c98d 100644 --- a/kernel/power/user.c +++ b/kernel/power/user.c @@ -21,6 +21,9 @@ #include #include #include +#include +#include +#include =20 #include =20 @@ -35,6 +38,146 @@ int is_hibernate_resume_dev(dev_t dev) return hibernation_available() && snapshot_state.dev =3D=3D dev; } =20 +bool hibernate_snapshot_write_active(dev_t dev) +{ +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + struct snapshot_data *data =3D &snapshot_state; + + return data->encryption_required && data->mode =3D=3D O_RDONLY && + data->ready && data->dev =3D=3D dev && + snapshot_encryption_enabled(data); +#else + return false; +#endif +} + +#if defined(CONFIG_ENCRYPTED_HIBERNATION) +static bool snapshot_encrypted_output_owned(struct snapshot_data *data, de= v_t dev) +{ + return hibernate_snapshot_write_active(dev) && + data->owner_tgid =3D=3D task_tgid(current); +} + +static bool snapshot_header_write_range(struct snapshot_data *data, + loff_t pos, size_t count) +{ + loff_t header_offset =3D data->swap_header_offset; + loff_t offset; + + if (pos < header_offset) + return false; + + offset =3D pos - header_offset; + return offset < PAGE_SIZE && count <=3D PAGE_SIZE - offset; +} + +static u64 snapshot_swap_write_budget(struct snapshot_data *data) +{ + if (data->crypt_swap_allocated > U64_MAX >> PAGE_SHIFT) + return 0; + + return data->crypt_swap_allocated << PAGE_SHIFT; +} + +static void snapshot_reset_swap_write_state(struct snapshot_data *data, + bool reset_allocation) +{ + spin_lock(&data->crypt_lock); + if (reset_allocation) + data->crypt_swap_allocated =3D 0; + data->crypt_swap_reserved =3D 0; + data->crypt_header_reserved =3D 0; + spin_unlock(&data->crypt_lock); +} +#endif + +enum hibernate_snapshot_write +hibernate_snapshot_write_begin(dev_t dev, loff_t pos, size_t count) +{ +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + struct snapshot_data *data =3D &snapshot_state; + enum hibernate_snapshot_write type =3D HIBERNATE_SNAPSHOT_WRITE_NONE; + bool image_range_allocated; + u64 swap_budget; + + if (!count || !snapshot_encrypted_output_owned(data, dev)) + return HIBERNATE_SNAPSHOT_WRITE_NONE; + + mutex_lock(&system_transition_mutex); + + if (!snapshot_encrypted_output_owned(data, dev)) + goto unlock; + + image_range_allocated =3D swsusp_swap_range_allocated(data->swap, pos, co= unt); + + spin_lock(&data->crypt_lock); + swap_budget =3D snapshot_swap_write_budget(data); + if (snapshot_header_write_range(data, pos, count) && + data->crypt_header_reserved <=3D PAGE_SIZE && + PAGE_SIZE - data->crypt_header_reserved >=3D count) { + data->crypt_header_reserved +=3D count; + type =3D HIBERNATE_SNAPSHOT_WRITE_HEADER; + } else if (image_range_allocated && + swap_budget >=3D data->crypt_swap_reserved && + swap_budget - data->crypt_swap_reserved >=3D count) { + data->crypt_swap_reserved +=3D count; + type =3D HIBERNATE_SNAPSHOT_WRITE_IMAGE; + } + spin_unlock(&data->crypt_lock); + + if (type =3D=3D HIBERNATE_SNAPSHOT_WRITE_NONE) + goto unlock; + + return type; + +unlock: + mutex_unlock(&system_transition_mutex); + return HIBERNATE_SNAPSHOT_WRITE_NONE; +#else + return HIBERNATE_SNAPSHOT_WRITE_NONE; +#endif +} + +void hibernate_snapshot_write_end(enum hibernate_snapshot_write type, + size_t reserved, ssize_t written) +{ +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + struct snapshot_data *data =3D &snapshot_state; + u64 *reserved_total; + size_t unused; + + if (type =3D=3D HIBERNATE_SNAPSHOT_WRITE_NONE) + return; + if (!reserved) + goto unlock; + + if (type =3D=3D HIBERNATE_SNAPSHOT_WRITE_HEADER) + unused =3D reserved; + else if (written <=3D 0) + unused =3D reserved; + else if (written < reserved) + unused =3D reserved - written; + else + goto unlock; + + reserved_total =3D type =3D=3D HIBERNATE_SNAPSHOT_WRITE_HEADER ? + &data->crypt_header_reserved : &data->crypt_swap_reserved; + + spin_lock(&data->crypt_lock); + *reserved_total -=3D min_t(u64, *reserved_total, unused); + spin_unlock(&data->crypt_lock); + +unlock: + mutex_unlock(&system_transition_mutex); +#endif +} + +static bool snapshot_encryption_required(struct snapshot_data *data) +{ + data->encryption_required =3D security_locked_down(LOCKDOWN_HIBERNATION); + return data->encryption_required; +} + static int snapshot_open(struct inode *inode, struct file *filp) { struct snapshot_data *data; @@ -60,6 +203,14 @@ static int snapshot_open(struct inode *inode, struct fi= le *filp) data =3D &snapshot_state; filp->private_data =3D data; memset(&data->handle, 0, sizeof(struct snapshot_handle)); +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + spin_lock_init(&data->crypt_lock); + data->crypt_swap_allocated =3D 0; + data->crypt_swap_reserved =3D 0; + data->crypt_header_reserved =3D 0; + data->swap_header_offset =3D 0; + data->owner_tgid =3D NULL; +#endif if ((filp->f_flags & O_ACCMODE) =3D=3D O_RDONLY) { /* Hibernating. The image device should be accessible. */ data->swap =3D pin_hibernation_swap_type(swsusp_resume_device, 0); @@ -90,6 +241,11 @@ static int snapshot_open(struct inode *inode, struct fi= le *filp) data->ready =3D false; data->platform_support =3D false; data->dev =3D 0; + data->encryption_required =3D snapshot_encryption_required(data); +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + if (!error) + data->owner_tgid =3D get_task_pid(current, PIDTYPE_TGID); +#endif =20 unlock: unlock_system_sleep(sleep_flags); @@ -107,6 +263,10 @@ static int snapshot_release(struct inode *inode, struc= t file *filp) swsusp_free(); data =3D filp->private_data; data->dev =3D 0; +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + put_pid(data->owner_tgid); + data->owner_tgid =3D NULL; +#endif free_all_swap_pages(data->swap); unpin_hibernation_swap_type(data->swap); if (data->frozen) { @@ -141,6 +301,11 @@ static ssize_t snapshot_read(struct file *filp, char _= _user *buf, res =3D -ENODATA; goto unlock; } + if (snapshot_encryption_required(data) && + !snapshot_encryption_enabled(data)) { + res =3D -EPERM; + goto unlock; + } =20 if (snapshot_encryption_enabled(data)) { res =3D snapshot_read_encrypted(data, buf, count, offp); @@ -183,6 +348,12 @@ static ssize_t snapshot_write(struct file *filp, const= char __user *buf, =20 data =3D filp->private_data; =20 + if (snapshot_encryption_required(data) && + !snapshot_encryption_enabled(data)) { + res =3D -EPERM; + goto unlock; + } + if (snapshot_encryption_enabled(data)) { res =3D snapshot_write_encrypted(data, buf, count, offp); goto unlock; @@ -255,6 +426,9 @@ static int snapshot_set_swap_area(struct snapshot_data = *data, if (data->swap < 0) return swdev ? -ENODEV : -EINVAL; data->dev =3D swdev; +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + data->swap_header_offset =3D (loff_t)offset << PAGE_SHIFT; +#endif return 0; } =20 @@ -321,6 +495,11 @@ static long snapshot_ioctl(struct file *filp, unsigned= int cmd, error =3D -EPERM; break; } + if (snapshot_encryption_required(data) && + !snapshot_encryption_enabled(data)) { + error =3D -EPERM; + break; + } pm_restore_gfp_mask(); error =3D hibernation_snapshot(data->platform_support); if (!error) { @@ -331,6 +510,11 @@ static long snapshot_ioctl(struct file *filp, unsigned= int cmd, break; =20 case SNAPSHOT_ATOMIC_RESTORE: + if (snapshot_encryption_required(data) && + !snapshot_encryption_enabled(data)) { + error =3D -EPERM; + break; + } if (snapshot_encryption_enabled(data)) { error =3D snapshot_finalize_decrypted_image(data); if (error) @@ -356,6 +540,9 @@ static long snapshot_ioctl(struct file *filp, unsigned = int cmd, memset(&data->handle, 0, sizeof(struct snapshot_handle)); data->ready =3D false; snapshot_teardown_encryption(data); +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + snapshot_reset_swap_write_state(data, false); +#endif /* * It is necessary to thaw kernel threads here, because * SNAPSHOT_CREATE_IMAGE may be invoked directly after @@ -398,6 +585,13 @@ static long snapshot_ioctl(struct file *filp, unsigned= int cmd, if (offset) { offset <<=3D PAGE_SHIFT; error =3D put_user(offset, (loff_t __user *)arg); +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + if (!error) { + spin_lock(&data->crypt_lock); + data->crypt_swap_allocated++; + spin_unlock(&data->crypt_lock); + } +#endif } else { error =3D -ENOSPC; } @@ -409,6 +603,9 @@ static long snapshot_ioctl(struct file *filp, unsigned = int cmd, break; } free_all_swap_pages(data->swap); +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + snapshot_reset_swap_write_state(data, true); +#endif break; =20 case SNAPSHOT_S2RAM: @@ -416,6 +613,11 @@ static long snapshot_ioctl(struct file *filp, unsigned= int cmd, error =3D -EPERM; break; } + if (snapshot_encryption_required(data) && + !snapshot_encryption_enabled(data)) { + error =3D -EPERM; + break; + } /* * Tasks are frozen and the notifiers have been called with * PM_HIBERNATION_PREPARE @@ -429,6 +631,11 @@ static long snapshot_ioctl(struct file *filp, unsigned= int cmd, break; =20 case SNAPSHOT_POWER_OFF: + if (snapshot_encryption_required(data) && + !snapshot_encryption_enabled(data)) { + error =3D -EPERM; + break; + } if (data->platform_support) error =3D hibernation_platform_enter(); break; @@ -442,6 +649,10 @@ static long snapshot_ioctl(struct file *filp, unsigned= int cmd, error =3D snapshot_get_encryption_key(data, (void __user *)arg); else error =3D snapshot_set_encryption_key(data, (void __user *)arg); +#if defined(CONFIG_ENCRYPTED_HIBERNATION) + if (!error) + snapshot_reset_swap_write_state(data, false); +#endif break; =20 case SNAPSHOT_SET_USER_KEY: diff --git a/kernel/power/user.h b/kernel/power/user.h index b0c20e5d5ee9..0e1e123e3230 100644 --- a/kernel/power/user.h +++ b/kernel/power/user.h @@ -5,10 +5,13 @@ =20 #include #include +#include #include #include #include =20 +struct pid; + #define SNAPSHOT_ENCRYPTION_KEY_SIZE AES_KEYSIZE_128 #define SNAPSHOT_AUTH_TAG_SIZE 16 =20 @@ -23,6 +26,7 @@ struct snapshot_data { bool ready; bool platform_support; bool free_bitmaps; + bool encryption_required; dev_t dev; =20 #if defined(CONFIG_ENCRYPTED_HIBERNATION) @@ -42,6 +46,13 @@ struct snapshot_data { bool user_key_valid; u64 meta_size; u64 crypt_meta_size; + /* Protect the swap allocation and block-write reservations. */ + spinlock_t crypt_lock; + u64 crypt_swap_allocated; + u64 crypt_swap_reserved; + u64 crypt_header_reserved; + loff_t swap_header_offset; + struct pid *owner_tgid; #endif =20 }; diff --git a/mm/swapfile.c b/mm/swapfile.c index 601979b97f95..ebddf954ad84 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -2379,6 +2379,31 @@ sector_t swapdev_block(int type, pgoff_t offset) return se->start_block + (offset - se->start_page); } =20 +int swapdev_block_to_extent(int type, sector_t block, pgoff_t *offset, + pgoff_t *nr_pages) +{ + struct swap_info_struct *si =3D swap_type_to_info(type); + struct swap_extent *se; + struct rb_node *rb; + + if (!si || !(si->flags & SWP_WRITEOK)) + return -ENODEV; + + for (rb =3D rb_first(&si->swap_extent_root); rb; rb =3D rb_next(rb)) { + se =3D rb_entry(rb, struct swap_extent, rb_node); + if (block >=3D se->start_block && + block - se->start_block < se->nr_pages) { + pgoff_t page =3D block - se->start_block; + + *offset =3D se->start_page + page; + *nr_pages =3D se->nr_pages - page; + return 0; + } + } + + return -ENOENT; +} + /* * Return either the total number of swap pages of given type, or the numb= er * of free pages of that type (depending on @free) From nobody Fri Sep 25 02:08:28 2026 Received: from mail-wm2-f13.google.com (mail-wm2-f13.google.com [74.125.225.141]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id ED3534FECD7 for ; Thu, 17 Sep 2026 13:30:02 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.225.141 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789651806; cv=none; b=OkJMRsJRETh3t0MCA86tOPlACAG0LAPEhUyHTaYOhVrvZhBeg+adbOpGKY/OeRkFxyPONDdZZDqKZJZhWpKvan+EHEYwZZ+bBvLgMVM9NQkGKrW9ncMDiNCuwIuletMvSlLm1gaucQCA9zV5DSmm93hlhOPYK4K1W+WKZQcMGNw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789651806; c=relaxed/simple; bh=m2UU0/l7oXH4nDDm4aNiVdsK/0ORRSvCe00W37MZM54=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=TvhxbpMbYekUUbqmtcDNizEfhxBPYDIIo+j+4F7ISAdOGWmxlEmTfpv9H4Qhy7rnQBClH8rVGpX2FltdClHfm4x4DpO1ZRhEzBb8oc1l/IcMqnbnhzAI/U3gI6XoRf2Z/kGj0Iq6AV0uYlYvVQGkhMMnqyFetoNWhEMsahHU6OA= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=starlabs.systems; spf=pass smtp.mailfrom=starlabs.systems; dkim=pass (2048-bit key) header.d=starlabs-systems.20251104.gappssmtp.com header.i=@starlabs-systems.20251104.gappssmtp.com header.b=EkGNaUPH; arc=none smtp.client-ip=74.125.225.141 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=starlabs.systems Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=starlabs.systems Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=starlabs-systems.20251104.gappssmtp.com header.i=@starlabs-systems.20251104.gappssmtp.com header.b="EkGNaUPH" Received: by mail-wm2-f13.google.com with SMTP id 5b1f17b1804b1-49e66390995so4613995e9.2 for ; Thu, 17 Sep 2026 06:30:01 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=starlabs-systems.20251104.gappssmtp.com; s=20251104; t=1789651798; x=1790256598; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=cYqy4JvgQiRzCuvqvHp97HhL8V2ft2LLYeHwmXteui8=; b=EkGNaUPHk0gCOURnQPHeX9tSlGgDfIPzVjd/lnLCeRErwDbYKU4BQDoajk1jwvDy+j NBa0QpS7P47So6cifHV8RRnY4WadJVeszl2Nsf/8i3nDeQaYjMKCeYOKSx/hxBSomZEw rnXazoXlGfmpepFfToUV0s6lnvTCkFMMCgS0Sbq6F9WumcWYWyg2Ub8WXsFqfXytRoQi L3cvrslXCgqzO0zKviYkehDLwSeO/HZoMBgY3yFjoykyymVLXNJDb9/E4EBlJximhl5g sxZyax/5Kt5VChk4VBXhKrYXBnpzeSVI7OxWa+FFL8VhNit1cwxjtoO4pKQXtIZjNL5C X1JA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1789651798; x=1790256598; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=cYqy4JvgQiRzCuvqvHp97HhL8V2ft2LLYeHwmXteui8=; b=yiaFdP4uX8lSxIFgV/cY7O8wJNMtKO3HU3vCLSLZF2wtgnxEtHeTqixNwxwoGKhTNp vmqs8JCs9TUBDCQ6jdKC2q3Y0nTwoqH72r636G7krB4EGYFuOw9X+g6f0JWqNg5fPLaj o9XuSyJDV/0lgJ/mz9LJRJNnwgNDCk+YPuO2tQN9LQ94N/8ukMsvDMpssoj9vE31ycMl fa2oZj7ZLuZHbdEy6BwzMe9BUKMr47iFMQk0TrNEvIqDK4pQoBvxO3XqNmzEIRvjuOxR GgFpmAEnuRJjEPIfwbH6VlhBlLKv+/5dqvVg6W9Q+mzWb7I9S9KaoMKMEMpEgDmI2t86 +Ibw== X-Forwarded-Encrypted: i=1; AKwUvBxU5OeMs9/W9oUrYyXig6gl2a6O+LkLj3bVjvkK8ZHo+dj3qxrMs5ffslE2KWdbjpzfpbNE3nraYWtfLJQ=@vger.kernel.org X-Gm-Message-State: AFuF++nc9WuJNhGHvOA71bxDoILcYpFX7XJe0wW2ngyE6NcJ8EnLIQcQ dzmV1Lb6iuZT7l28SkadI1S4m6i97Z/2tUvwh4wiYQnZg3E4PsUYayOFywSsAAxRcQ== X-Gm-Gg: AYBFou15U+tx9cmznhZgEi1dePRp7+h2jlOYSyrSZ0rOqFyEFrjQZOmFIpPHeg4EJmN 1ykQSHWOFEiqT3Z/EQeuuzl+OOMy6emCXg/S8eCY1UKbpJha7IdBpsmiAvW+okTC25okahDlVYB tQ1dmHOmh/pP028ffSdi34kiKH435u81FzRofB0H8SXwyvfurd40q0EdsdWiebnMDFm4EWV7ocs Z1eIryPL/vOicUxiQnbHkjl5PrmeuA6izQzq5YlLavAhQqNtZAT2S4myjZ09/Prq0KXxJtY9xuG h7CVevmJh9aDcTgE0LvcaIK19GxmdrI8M5R5UcjG3pa0KZpNo0kkXfv/YwTbqbvrhqmrBdXrjPG evdyyy+AzGcWMHU9CUicPqea+t2TMuTBbEV0loMR5PFlONGAc+44qwYPYILM5QBGaXdZ/kxiBRd OVA9ddk3dxR+Dma68wMGN2PcmhrqpezY+DirQZdKkDz21bWVhuothX+VTCGnvMR4zN4uKBpOb1x 23tVrQcKoMdspeUx0ipHI7WcsY= X-Received: by 2002:a05:600d:644f:20b0:49f:c199:e1e2 with SMTP id 5b1f17b1804b1-49fc199e215mr12253705e9.28.1789651798061; Thu, 17 Sep 2026 06:29:58 -0700 (PDT) Received: from fighter ([216.128.28.240]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-49fbd2356d1sm97548275e9.6.2026.09.17.06.29.56 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 17 Sep 2026 06:29:57 -0700 (PDT) From: Sean Rhodes To: linux-pm@vger.kernel.org Cc: "Rafael J. Wysocki" , Len Brown , Pavel Machek , Evan Green , Sean Rhodes , linux-kernel@vger.kernel.org, Jens Axboe , Andrew Morton , Chris Li , Kairui Song , Kemeng Shi , Nhat Pham , Baoquan He , Barry Song , Youngjun Park , linux-block@vger.kernel.org, linux-mm@kvack.org, Xueqin Luo , Nicolas Bouchinet Subject: [PATCH v3 RESEND 3/4] PM: hibernate: permit encrypted snapshot device under lockdown Date: Thu, 17 Sep 2026 14:29:49 +0100 Message-ID: X-Mailer: git-send-email 2.53.0 In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Lockdown disables hibernation because restoring attacker-controlled image data can modify kernel memory. Permit opening the userspace snapshot device under lockdown when encrypted hibernation support is built, but require encryption to be enabled before image data can be read or written and before restore or power-off ioctls can proceed. Tested on StarFighter MTL with lockdown enabled: encrypted uswsusp hibernate and restore used the snapshot device path. Signed-off-by: Sean Rhodes --- kernel/power/hibernate.c | 8 ++++++++ kernel/power/power.h | 1 + kernel/power/user.c | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/kernel/power/hibernate.c b/kernel/power/hibernate.c index a95cb447a13a..0b8626bdf81a 100644 --- a/kernel/power/hibernate.c +++ b/kernel/power/hibernate.c @@ -114,6 +114,14 @@ bool hibernation_available(void) !secretmem_active() && !cxl_mem_active(); } =20 +bool hibernation_snapshot_dev_available(void) +{ + return nohibernate =3D=3D 0 && + (!security_locked_down(LOCKDOWN_HIBERNATION) || + IS_ENABLED(CONFIG_ENCRYPTED_HIBERNATION)) && + !secretmem_active() && !cxl_mem_active(); +} + /** * hibernation_set_ops - Set the global hibernate operations. * @ops: Hibernation operations to use in subsequent hibernation transitio= ns. diff --git a/kernel/power/power.h b/kernel/power/power.h index 815c84c1e0dd..351b5f68bde3 100644 --- a/kernel/power/power.h +++ b/kernel/power/power.h @@ -168,6 +168,7 @@ extern int snapshot_image_loaded(struct snapshot_handle= *handle); =20 extern bool hibernate_acquire(void); extern void hibernate_release(void); +bool hibernation_snapshot_dev_available(void); =20 extern sector_t alloc_swapdev_block(int swap); bool swsusp_swap_range_allocated(int swap, loff_t pos, size_t count); diff --git a/kernel/power/user.c b/kernel/power/user.c index de41f590c98d..b8d3342f1548 100644 --- a/kernel/power/user.c +++ b/kernel/power/user.c @@ -184,7 +184,7 @@ static int snapshot_open(struct inode *inode, struct fi= le *filp) unsigned int sleep_flags; int error; =20 - if (!hibernation_available()) + if (!hibernation_snapshot_dev_available()) return -EPERM; =20 sleep_flags =3D lock_system_sleep(); From nobody Fri Sep 25 02:08:28 2026 Received: from mail-wm2-f12.google.com (mail-wm2-f12.google.com [74.125.225.140]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9DA1D4F6474 for ; Thu, 17 Sep 2026 13:30:03 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.225.140 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789651810; cv=none; b=g/k0eK9vce+cmECqzBVFhnuXEwvGz0c9ePHeuFlSxDg+//qqlj87LOSUk6zPcIi9zN1wX9KQiO/tPYdt0psATOK1gTUFjWlZRtESpjjoySQ9nyCrt/5p/x8RmWACcxYyej0U4BiQEBnOSEuqgSNC61SPtg47A45kPzzlY/zdBdc= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789651810; c=relaxed/simple; bh=kgacaXhkkji4P8QOQ+aFj100L+etrdW7UzgN1ijHzHY=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=k5Ml871CtELv6RSZqneAbYNyoAcs0b6W1vgL2IuGUMxUNc9sDlXc/fmBqPcn4GrdKhAorid6vP9TIDVtN4Md8BNHIEic4bKSjK3cyjFRRDIDha1j/hZoVp/4IU7COcQJiztAU1yk8cmMaKOA4QL6zGJg0X9tdjHUBKMkZeO5WGk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=starlabs.systems; spf=pass smtp.mailfrom=starlabs.systems; dkim=pass (2048-bit key) header.d=starlabs-systems.20251104.gappssmtp.com header.i=@starlabs-systems.20251104.gappssmtp.com header.b=KMGzSQHn; arc=none smtp.client-ip=74.125.225.140 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=starlabs.systems Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=starlabs.systems Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=starlabs-systems.20251104.gappssmtp.com header.i=@starlabs-systems.20251104.gappssmtp.com header.b="KMGzSQHn" Received: by mail-wm2-f12.google.com with SMTP id 5b1f17b1804b1-49b912d37b5so5362255e9.2 for ; Thu, 17 Sep 2026 06:30:03 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=starlabs-systems.20251104.gappssmtp.com; s=20251104; t=1789651799; x=1790256599; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=GhZfe7j97gcGMqNS+7HeL0ehn+NCntLK64e5hLR53vg=; b=KMGzSQHnXHagzGVPGxif3eDEd30RorkGW7q3GcXqowT/bgNasMll94uFTZKymf/XG8 tovBR3YGZZOlDcpBFR9wjKvC3RkVKSe1cY/FlrKNfW986zXRRsF2lT1VADCagcwL90a5 GfaxBWYIppR1k5e7Aa75NjbzWo8GQwToSUjIepXWrWLusfwJJrem7JVwToUUAk7dqJ9L MLf/5obAblKj6wQng8tqO6e4icV9XxFxqjR/VdXS3S+bb59pb0SrNo8MUMfv3iyJLeDN 2url79nAawKo0yVbBYc4Azey4+g6qeyzXG+Ang3FbwdFSpkbSkQIdc5sH3M1tsPcL5Xx EBxw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1789651799; x=1790256599; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=GhZfe7j97gcGMqNS+7HeL0ehn+NCntLK64e5hLR53vg=; b=GaSHHOXoUwYd0BLzb0zg35TbM5SlZmgs9xpT1AguqLSa6WPuMJkW0WYo878fM1d8UZ TJJlQFFos8te4Sa0AdVZhtePP37oyKxFqQ4hlPcjmTTyk+g0y96asQBNJ/cuXbKy1nsb KtFdK/LKDqv4GOn9IPup0Xh5VTf8dlxHO3/evLKqvZgmizO/FvH8D6bchQTxpVzgp63N jI8aLg2FcKlyl+5peGOeIakiCdfoduQy/+qlOJgzzEZfCYQNui/Zi58fM4DplzwtklnP ZTSrTWI9elk9r4tviD7TNAhS/NYUGLAI7pc24bnBVBCQRWaM7RsjcPXjvqikJQ0MnDul EpXQ== X-Forwarded-Encrypted: i=1; AKwUvBwRsuhtTA1eSgydxQDB73Nod6ocwBjyZL4t/NoSHGXUOVWeQpF0uBnYqjDyv15V3zl6HRfr9Rb3aZQcfdA=@vger.kernel.org X-Gm-Message-State: AFuF++khZVZjTPYHXuQ4dEVp+jibJP8IVvyzv+zFIlR7eXlGh69n8SBU /EaE1Zx3lAXKHohqM52Z95afGh7o24K3Gl1eyK380J9O3ywFKGErbs8KeYbxpGtzpg== X-Gm-Gg: AYBFou1COEQ4kNOXmrkfK9ytobXxKodmOAw/ZuQw1X1qJ4HSB9MnAl8Qz+ni9rvqcEe dY+Vz4EFufpOHBgvOyb65ZdFsyJPUjFDRoYc11hDHHhW4eGUQDFvaorOS2IMY1tySq6hI3hApxf qDtqWiJjrrHCsYpClpLEASHahBZuq/ZME98AArUzDm2+B6VHCH0MBoIa3ygzLerRQd/Al031RFo 189GnCCBw9YlNZbGPj68LZEBXif6CnAzEOIrbippHmHGqmhZOOj6ytNLM+UK5YPXFRff1C3IpB1 Im8LYZCJJqxqp/EkC79uG0nzY3ibvabQ57rj/dQcRzB93pMr1JTbOS9XVNPt/zA1SU4wj4d7LXy 2QyYSM0Y13hseAW7XY+tk4fLMdFt9yB0/gHDSLvYSeARabq4h3uc7sgzTAntK9nQWN4d3JaB3k+ UrzIGWrhrYOleRI0FFPp4a8/h9sp6DQ0WuPAo76g5XVRNeqrPMPh+86rLbqwg9qJkgtaTIrRxP5 qWJGxXajPC+t/OdtvR0SoQyKrpXSfcpVlXbYg== X-Received: by 2002:a05:600c:4e91:b0:49d:726:cc9f with SMTP id 5b1f17b1804b1-49eb72f39d7mr156045335e9.5.1789651799329; Thu, 17 Sep 2026 06:29:59 -0700 (PDT) Received: from fighter ([216.128.28.240]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-49fbd2356d1sm97548275e9.6.2026.09.17.06.29.58 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 17 Sep 2026 06:29:58 -0700 (PDT) From: Sean Rhodes To: linux-pm@vger.kernel.org Cc: "Rafael J. Wysocki" , Len Brown , Pavel Machek , Evan Green , Sean Rhodes , linux-kernel@vger.kernel.org, Jens Axboe , Andrew Morton , Chris Li , Kairui Song , Kemeng Shi , Nhat Pham , Baoquan He , Barry Song , Youngjun Park , linux-block@vger.kernel.org, linux-mm@kvack.org, Xueqin Luo , Nicolas Bouchinet Subject: [PATCH v3 RESEND 4/4] PM: hibernate: document encrypted snapshot seed ABI Date: Thu, 17 Sep 2026 14:29:50 +0100 Message-ID: <81c284e9246faa817050f34d46a9fbe558ddba5b.1789651778.git.sean@starlabs.systems> X-Mailer: git-send-email 2.53.0 In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Document the write-only snapshot_seed sysfs file and update the userspace snapshot interface text to describe the opaque wrapped-key flow used by encrypted hibernation. Signed-off-by: Sean Rhodes --- Documentation/ABI/testing/sysfs-power | 18 +++++++++++++ Documentation/power/userland-swsusp.rst | 35 ++++++++++++++++++------- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-power b/Documentation/ABI/test= ing/sysfs-power index d38da077905a..c756712a9df5 100644 --- a/Documentation/ABI/testing/sysfs-power +++ b/Documentation/ABI/testing/sysfs-power @@ -470,3 +470,21 @@ Description: =20 Minimum value: 1 Default value: 3 + +What: /sys/power/snapshot_seed +Date: July 2026 +Contact: linux-pm@vger.kernel.org +Description: + Write-only file present when CONFIG_ENCRYPTED_HIBERNATION=3Dy. + + Trusted early userspace writes a 32-byte seed, encoded as + 64 hexadecimal characters plus an optional newline, before + enabling encrypted userspace hibernation snapshots. + + The first successful write locks the seed until the next boot. + Writing the same seed again succeeds. Writing a different seed + fails with EPERM. + + The kernel does not authenticate the first writer. Trusted early + userspace must provision this file before untrusted privileged + code can run. diff --git a/Documentation/power/userland-swsusp.rst b/Documentation/power/= userland-swsusp.rst index 282e01d2fe61..957be396dcf5 100644 --- a/Documentation/power/userland-swsusp.rst +++ b/Documentation/power/userland-swsusp.rst @@ -116,20 +116,37 @@ SNAPSHOT_S2RAM its state on the basis of the saved suspend image otherwise) =20 SNAPSHOT_ENABLE_ENCRYPTION - Enables encryption of the hibernate image within the kernel. Upon suspend - (ie when the snapshot device was opened for reading), returns a blob - representing the random encryption key the kernel created to encrypt the - hibernate image with. Upon resume (ie when the snapshot device was opened - for writing), receives a blob from usermode containing the key material - previously returned during hibernate. + Enables encryption of the hibernate image within the kernel. Trusted + early userspace must write the 32-byte snapshot encryption seed to + /sys/power/snapshot_seed before calling this ioctl. Upon suspend + (ie when the snapshot device was opened for reading), this ioctl returns + an opaque wrapped key blob and the starting nonce. Upon resume (ie when + the snapshot device was opened for writing), this ioctl receives the + same blob and nonce from usermode after the same seed has been written + to /sys/power/snapshot_seed. The plaintext image key is generated and + unwrapped inside the kernel. + + The first successful seed write establishes the wrapping key for that + boot. The kernel does not authenticate the writer, the TPM policy used to + unseal the seed, or image freshness. Early userspace must provision the + seed before untrusted privileged code can run and must enforce any TPM + policy and rollback protection. + + Under lockdown, direct block-device write() calls to the active swap + backend are restricted to the process which owns the snapshot device and + the extents allocated with SNAPSHOT_ALLOC_SWAP_PAGE. These writes must + use synchronous buffered I/O (O_DSYNC without O_DIRECT), and cannot + exceed the number of pages returned by that ioctl. The swap header may + only be written at the offset supplied with SNAPSHOT_SET_SWAP_AREA. =20 SNAPSHOT_SET_USER_KEY Mixes additional user key material into the data portion of an encrypted hibernate image. The ioctl argument points to struct uswsusp_user_key. key_len must be between 8 and USWSUSP_USER_KEY_SIZE bytes, and reserved - must be zero. The kernel writes meta_size with the encrypted metadata - size that userspace may transfer before providing the user key during - resume. + must be zero. During hibernation the kernel writes meta_size with the + encrypted metadata size. During resume, userspace passes the saved + meta_size back so the kernel can switch keys at the metadata boundary + before the snapshot header has been decrypted. =20 The device's read() operation can be used to transfer the snapshot image f= rom the kernel. It has the following limitations: