mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-07-16 17:57:38 -04:00
Introduce a linked-block serialization mechanism for state handover. Previously, LUO used contiguous memory blocks for serializing sessions and files, which imposed limits on the total number of items that could be preserved across a live update. This commit adds the infrastructure for a more flexible, block-based approach where serialized data is stored in a chain of linked blocks. This is a generic KHO serialization block infrastructure that can be used by multiple subsystems. Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com> Link: https://patch.msgid.link/20260603154402.468928-8-pasha.tatashin@soleen.com Signed-off-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
417 lines
10 KiB
C
417 lines
10 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
|
|
/*
|
|
* Copyright (c) 2026, Google LLC.
|
|
* Pasha Tatashin <pasha.tatashin@soleen.com>
|
|
*/
|
|
|
|
/**
|
|
* DOC: KHO Serialization Blocks
|
|
*
|
|
* KHO provides a mechanism to preserve stateful data across a kexec handover
|
|
* by serializing it into memory blocks, and provides the common
|
|
* infrastructure for managing these blocks.
|
|
*
|
|
* Each block consists of a header (struct kho_block_header_ser) followed by an
|
|
* array of serialized entries. Multiple blocks are linked together via a
|
|
* physical pointer in the header, forming a linked list that can be easily
|
|
* traversed in both the current and the next kernel.
|
|
*/
|
|
|
|
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
|
|
|
|
#include <linux/io.h>
|
|
#include <linux/kexec_handover.h>
|
|
#include <linux/kho/abi/block.h>
|
|
#include <linux/kho_block.h>
|
|
#include <linux/slab.h>
|
|
|
|
/*
|
|
* Safeguard limit for the number of serialization blocks. This is used to
|
|
* prevent infinite loops and excessive memory allocation in case of memory
|
|
* corruption in the preserved state.
|
|
*
|
|
* With a 4KB page size, 10k blocks is about 40MB. For 32-byte entries
|
|
* (e.g. 4 u64s), each block holds up to 127 entries (accounting for the
|
|
* 16-byte header), allowing the block set to hold up to 1.27M entries.
|
|
*/
|
|
#define KHO_MAX_BLOCKS 10000
|
|
|
|
/**
|
|
* kho_block_set_init - Initialize a block set.
|
|
* @bs: The block set to initialize.
|
|
* @entry_size: The size of each entry in the blocks.
|
|
*/
|
|
void kho_block_set_init(struct kho_block_set *bs, size_t entry_size)
|
|
{
|
|
*bs = (struct kho_block_set)KHO_BLOCK_SET_INIT(*bs, entry_size);
|
|
WARN_ON_ONCE(!bs->count_per_block);
|
|
}
|
|
|
|
/* Serialized entries start immediately after the block header */
|
|
static void *kho_block_entries(struct kho_block *block)
|
|
{
|
|
return (void *)(block->ser + 1);
|
|
}
|
|
|
|
/* Get the address of the serialized entry at the specified index */
|
|
static void *kho_block_entry(struct kho_block_set_it *it, u64 index)
|
|
{
|
|
return kho_block_entries(it->block) + (index * it->bs->entry_size);
|
|
}
|
|
|
|
/* Free serialized data */
|
|
static void kho_block_free_ser(struct kho_block_set *bs,
|
|
struct kho_block_header_ser *ser)
|
|
{
|
|
if (bs->incoming)
|
|
kho_restore_free(ser);
|
|
else
|
|
kho_unpreserve_free(ser);
|
|
}
|
|
|
|
static struct kho_block_header_ser *kho_block_alloc_ser(struct kho_block_set *bs)
|
|
{
|
|
WARN_ON_ONCE(bs->incoming);
|
|
return kho_alloc_preserve(KHO_BLOCK_SIZE);
|
|
}
|
|
|
|
static int kho_block_add(struct kho_block_set *bs,
|
|
struct kho_block_header_ser *ser)
|
|
{
|
|
struct kho_block *block, *last;
|
|
|
|
if (bs->nblocks >= KHO_MAX_BLOCKS)
|
|
return -ENOSPC;
|
|
|
|
block = kzalloc_obj(*block);
|
|
if (!block)
|
|
return -ENOMEM;
|
|
|
|
block->ser = ser;
|
|
last = list_last_entry_or_null(&bs->blocks, struct kho_block, list);
|
|
list_add_tail(&block->list, &bs->blocks);
|
|
bs->nblocks++;
|
|
|
|
if (last)
|
|
last->ser->next = virt_to_phys(ser);
|
|
else
|
|
bs->head_pa = virt_to_phys(ser);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int kho_block_set_grow_one(struct kho_block_set *bs)
|
|
{
|
|
struct kho_block_header_ser *ser;
|
|
int err;
|
|
|
|
ser = kho_block_alloc_ser(bs);
|
|
if (IS_ERR(ser))
|
|
return PTR_ERR(ser);
|
|
|
|
err = kho_block_add(bs, ser);
|
|
if (err) {
|
|
kho_block_free_ser(bs, ser);
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void kho_block_set_shrink_one(struct kho_block_set *bs)
|
|
{
|
|
struct kho_block *last, *new_last;
|
|
|
|
if (list_empty(&bs->blocks))
|
|
return;
|
|
|
|
last = list_last_entry(&bs->blocks, struct kho_block, list);
|
|
list_del(&last->list);
|
|
bs->nblocks--;
|
|
kho_block_free_ser(bs, last->ser);
|
|
kfree(last);
|
|
|
|
new_last = list_last_entry_or_null(&bs->blocks, struct kho_block, list);
|
|
if (new_last)
|
|
new_last->ser->next = 0;
|
|
else
|
|
bs->head_pa = 0;
|
|
}
|
|
|
|
/**
|
|
* kho_block_set_grow - Expand the block set to accommodate the target count.
|
|
* @bs: The block set.
|
|
* @count: The target number of valid entries to accommodate.
|
|
*
|
|
* Dynamically preallocates and links preserved memory blocks if the target
|
|
* entry count exceeds the current total capacity of the set, ensuring they
|
|
* are available during serialization/deserialization.
|
|
*
|
|
* Context: Caller must hold a lock protecting the block set.
|
|
* Return: 0 on success, or a negative errno on failure.
|
|
*/
|
|
int kho_block_set_grow(struct kho_block_set *bs, u64 count)
|
|
{
|
|
long orig_nblocks = bs->nblocks;
|
|
int err;
|
|
|
|
if (WARN_ON_ONCE(bs->incoming))
|
|
return -EINVAL;
|
|
|
|
while (count > bs->nblocks * bs->count_per_block) {
|
|
err = kho_block_set_grow_one(bs);
|
|
if (err)
|
|
goto err_shrink;
|
|
}
|
|
|
|
return 0;
|
|
|
|
err_shrink:
|
|
while (bs->nblocks > orig_nblocks)
|
|
kho_block_set_shrink_one(bs);
|
|
return err;
|
|
}
|
|
|
|
/**
|
|
* kho_block_set_shrink - Shrink the block set to accommodate the target count.
|
|
* @bs: The block set.
|
|
* @count: The target number of valid entries to accommodate.
|
|
*
|
|
* Releases and unallocates redundant preserved memory blocks. Checks if the
|
|
* last block in the set can be removed because the remaining entry count is
|
|
* fully accommodated by the preceding blocks.
|
|
*
|
|
* Note: It is the caller's responsibility to ensure that entries are removed
|
|
* in the reverse order of their insertion. Because shrinking destroys the last
|
|
* block in the set, removing entries in any other order would corrupt active
|
|
* data.
|
|
*
|
|
* Context: Caller must hold a lock protecting the block set.
|
|
*/
|
|
void kho_block_set_shrink(struct kho_block_set *bs, u64 count)
|
|
{
|
|
while (bs->nblocks > 0 && count <= (bs->nblocks - 1) * bs->count_per_block)
|
|
kho_block_set_shrink_one(bs);
|
|
}
|
|
|
|
/*
|
|
* kho_block_set_is_cyclic - Check for cycles in a linked list of blocks.
|
|
* Uses Floyd's cycle-finding algorithm to ensure sanity of the incoming list.
|
|
*
|
|
* Return: true if a cycle or corruption is detected, false otherwise.
|
|
*/
|
|
static bool kho_block_set_is_cyclic(struct kho_block_set *bs)
|
|
{
|
|
struct kho_block_header_ser *fast;
|
|
struct kho_block_header_ser *slow;
|
|
int count = 0;
|
|
|
|
fast = phys_to_virt(bs->head_pa);
|
|
slow = fast;
|
|
|
|
while (fast) {
|
|
if (count++ >= KHO_MAX_BLOCKS) {
|
|
pr_err("Block set is corrupted\n");
|
|
return true;
|
|
}
|
|
|
|
if (!fast->next)
|
|
break;
|
|
|
|
fast = phys_to_virt(fast->next);
|
|
if (!fast->next)
|
|
break;
|
|
|
|
fast = phys_to_virt(fast->next);
|
|
slow = phys_to_virt(slow->next);
|
|
|
|
if (slow == fast) {
|
|
pr_err("Block set is corrupted\n");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* kho_block_set_restore - Restore a block set from a physical address.
|
|
* @bs: The block set to restore.
|
|
* @head_pa: Physical address of the first block header.
|
|
*
|
|
* Restores a serialized block set from a given physical address. The caller is
|
|
* responsible for ensuring that the block set @bs has been allocated and
|
|
* initialized prior to calling this function.
|
|
*
|
|
* Return: 0 on success, or a negative errno on failure.
|
|
*/
|
|
int kho_block_set_restore(struct kho_block_set *bs, u64 head_pa)
|
|
{
|
|
struct kho_block_header_ser *ser;
|
|
u64 next_pa = head_pa;
|
|
int err;
|
|
|
|
/* Restored block sets use size from the previous kernel */
|
|
bs->incoming = true;
|
|
if (!head_pa)
|
|
return 0;
|
|
|
|
bs->head_pa = head_pa;
|
|
if (kho_block_set_is_cyclic(bs)) {
|
|
bs->head_pa = 0;
|
|
return -EINVAL;
|
|
}
|
|
|
|
while (next_pa) {
|
|
ser = phys_to_virt(next_pa);
|
|
if (!ser->count || ser->count > bs->count_per_block) {
|
|
pr_warn("Block contains invalid entry count: %llu\n",
|
|
ser->count);
|
|
err = -EINVAL;
|
|
goto err_destroy;
|
|
}
|
|
err = kho_block_add(bs, ser);
|
|
if (err)
|
|
goto err_destroy;
|
|
next_pa = ser->next;
|
|
}
|
|
|
|
return 0;
|
|
|
|
err_destroy:
|
|
kho_block_set_destroy(bs);
|
|
|
|
/* Free the remaining un-restored blocks in the physical chain */
|
|
while (next_pa) {
|
|
struct kho_block_header_ser *next_ser = phys_to_virt(next_pa);
|
|
|
|
next_pa = next_ser->next;
|
|
kho_block_free_ser(bs, next_ser);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
/**
|
|
* kho_block_set_destroy - Destroy all blocks in a block set.
|
|
* @bs: The block set.
|
|
*/
|
|
void kho_block_set_destroy(struct kho_block_set *bs)
|
|
{
|
|
struct kho_block *block, *tmp;
|
|
|
|
list_for_each_entry_safe(block, tmp, &bs->blocks, list) {
|
|
list_del(&block->list);
|
|
kho_block_free_ser(bs, block->ser);
|
|
kfree(block);
|
|
}
|
|
bs->nblocks = 0;
|
|
bs->head_pa = 0;
|
|
}
|
|
|
|
/**
|
|
* kho_block_set_clear - Clear all serialized data in a block set.
|
|
* @bs: The block set to clear.
|
|
*/
|
|
void kho_block_set_clear(struct kho_block_set *bs)
|
|
{
|
|
struct kho_block *block;
|
|
|
|
list_for_each_entry(block, &bs->blocks, list) {
|
|
block->ser->count = 0;
|
|
memset(block->ser + 1, 0, KHO_BLOCK_SIZE - sizeof(*block->ser));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* kho_block_set_it_init - Initialize a block set iterator.
|
|
* @it: The iterator to initialize.
|
|
* @bs: The block set to iterate over.
|
|
*/
|
|
void kho_block_set_it_init(struct kho_block_set_it *it, struct kho_block_set *bs)
|
|
{
|
|
it->bs = bs;
|
|
it->block = list_first_entry_or_null(&bs->blocks, struct kho_block, list);
|
|
it->i = 0;
|
|
}
|
|
|
|
/**
|
|
* kho_block_set_it_reserve_entry - Reserve and return the next available slot for writing.
|
|
* @it: The block iterator.
|
|
*
|
|
* Reserves a slot in the current block during state serialization to add a new
|
|
* entry, advancing the internal index. If the current block is full, it
|
|
* automatically moves to the next block in the set.
|
|
*
|
|
* Return: A pointer to the reserved entry slot, or NULL if the block set's
|
|
* capacity is fully exhausted.
|
|
*/
|
|
void *kho_block_set_it_reserve_entry(struct kho_block_set_it *it)
|
|
{
|
|
void *entry;
|
|
|
|
if (!it->block)
|
|
return NULL;
|
|
|
|
if (it->i == it->bs->count_per_block) {
|
|
if (list_is_last(&it->block->list, &it->bs->blocks))
|
|
return NULL;
|
|
it->block = list_next_entry(it->block, list);
|
|
it->i = 0;
|
|
}
|
|
|
|
entry = kho_block_entry(it, it->i++);
|
|
it->block->ser->count = it->i;
|
|
return entry;
|
|
}
|
|
|
|
/**
|
|
* kho_block_set_it_read_entry - Read the next serialized entry from the block set.
|
|
* @it: The block iterator.
|
|
*
|
|
* Iterates through previously written entries during state deserialization,
|
|
* respecting the actual count stored in each block's header.
|
|
*
|
|
* Return: A pointer to the next serialized entry, or NULL if all serialized
|
|
* entries have been read.
|
|
*/
|
|
void *kho_block_set_it_read_entry(struct kho_block_set_it *it)
|
|
{
|
|
if (!it->block)
|
|
return NULL;
|
|
|
|
if (it->i == it->block->ser->count) {
|
|
if (list_is_last(&it->block->list, &it->bs->blocks))
|
|
return NULL;
|
|
it->block = list_next_entry(it->block, list);
|
|
it->i = 0;
|
|
}
|
|
|
|
return kho_block_entry(it, it->i++);
|
|
}
|
|
|
|
/**
|
|
* kho_block_set_it_prev - Return the previous entry slot in the block set.
|
|
* @it: The block iterator.
|
|
*
|
|
* If the current index is at the start of a block, it automatically moves to
|
|
* the end of the previous block.
|
|
*
|
|
* Return: A pointer to the previous entry slot, or NULL if at the very
|
|
* beginning of the block set.
|
|
*/
|
|
void *kho_block_set_it_prev(struct kho_block_set_it *it)
|
|
{
|
|
if (!it->block)
|
|
return NULL;
|
|
|
|
if (it->i == 0) {
|
|
if (list_is_first(&it->block->list, &it->bs->blocks))
|
|
return NULL;
|
|
it->block = list_prev_entry(it->block, list);
|
|
it->i = it->bs->count_per_block;
|
|
}
|
|
|
|
return kho_block_entry(it, --it->i);
|
|
}
|