| // 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); |
| } |