Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
# SPDX-License-Identifier: GPL-2.0-only
menu "Live Update and Kexec HandOver"
config KEXEC_HANDOVER
bool "kexec handover"
depends on ARCH_SUPPORTS_KEXEC_HANDOVER && ARCH_SUPPORTS_KEXEC_FILE
select MEMBLOCK_KHO_SCRATCH
select KEXEC_FILE
select LIBFDT
select CMA
help
Allow kexec to hand over state across kernels by generating and
passing additional metadata to the target kernel. This is useful
to keep data or state alive across the kexec. For this to work,
both source and target kernels need to have this option enabled.
config KEXEC_HANDOVER_DEBUG
bool "Enable Kexec Handover debug checks"
depends on KEXEC_HANDOVER
help
This option enables extra sanity checks for the Kexec Handover
subsystem. Since, KHO performance is crucial in live update
scenarios and the extra code might be adding overhead it is
only optionally enabled.
config KEXEC_HANDOVER_DEBUGFS
bool "kexec handover debugfs interface"
default KEXEC_HANDOVER
depends on KEXEC_HANDOVER
select DEBUG_FS
help
Allow to control kexec handover device tree via debugfs
interface, i.e. finalize the state or aborting the finalization.
Also, enables inspecting the KHO fdt trees with the debugfs binary
blobs.
config KEXEC_HANDOVER_ENABLE_DEFAULT
bool "Enable kexec handover by default"
depends on KEXEC_HANDOVER
help
Enable Kexec Handover by default. This avoids the need to
explicitly pass 'kho=on' on the kernel command line.
This is useful for systems where KHO is a prerequisite for other
features, such as Live Update, ensuring the mechanism is always
active.
The default behavior can still be overridden at boot time by
passing 'kho=off'.
config LIVEUPDATE
bool "Live Update Orchestrator"
depends on KEXEC_HANDOVER
help
Enable the Live Update Orchestrator. Live Update is a mechanism,
typically based on kexec, that allows the kernel to be updated
while keeping selected devices operational across the transition.
These devices are intended to be reclaimed by the new kernel and
re-attached to their original workload without requiring a device
reset.
Ability to handover a device from current to the next kernel depends
on specific support within device drivers and related kernel
subsystems.
This feature primarily targets virtual machine hosts to quickly update
the kernel hypervisor with minimal disruption to the running virtual
machines.
If unsure, say N.
config LIVEUPDATE_MEMFD
bool "Live update support for memfd"
depends on LIVEUPDATE
depends on MEMFD_CREATE
depends on SHMEM
default LIVEUPDATE
help
Enable live update support for memfd regions. This allows preserving
memfd-backed memory across kernel live updates.
This can be used to back VM memory with memfds, allowing the guest
memory to persist, or for other user workloads needing to preserve
pages.
If unsure, say N.
endmenu
+14
View File
@@ -0,0 +1,14 @@
# SPDX-License-Identifier: GPL-2.0
luo-y := \
kho_block.o \
luo_core.o \
luo_file.o \
luo_flb.o \
luo_session.o
obj-$(CONFIG_KEXEC_HANDOVER) += kexec_handover.o
obj-$(CONFIG_KEXEC_HANDOVER_DEBUG) += kexec_handover_debug.o
obj-$(CONFIG_KEXEC_HANDOVER_DEBUGFS) += kexec_handover_debugfs.o
obj-$(CONFIG_LIVEUPDATE) += luo.o
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* kexec_handover_debug.c - kexec handover optional debug functionality
* Copyright (C) 2025 Google LLC, Pasha Tatashin <pasha.tatashin@soleen.com>
*/
#define pr_fmt(fmt) "KHO: " fmt
#include "kexec_handover_internal.h"
bool kho_scratch_overlap(phys_addr_t phys, size_t size)
{
phys_addr_t scratch_start, scratch_end;
unsigned int i;
for (i = 0; i < kho_scratch_cnt; i++) {
scratch_start = kho_scratch[i].addr;
scratch_end = kho_scratch[i].addr + kho_scratch[i].size;
if (phys < scratch_end && (phys + size) > scratch_start)
return true;
}
return false;
}
@@ -0,0 +1,214 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* kexec_handover_debugfs.c - kexec handover debugfs interfaces
* Copyright (C) 2023 Alexander Graf <graf@amazon.com>
* Copyright (C) 2025 Microsoft Corporation, Mike Rapoport <rppt@kernel.org>
* Copyright (C) 2025 Google LLC, Changyuan Lyu <changyuanl@google.com>
* Copyright (C) 2025 Google LLC, Pasha Tatashin <pasha.tatashin@soleen.com>
*/
#define pr_fmt(fmt) "KHO: " fmt
#include <linux/init.h>
#include <linux/io.h>
#include <linux/libfdt.h>
#include <linux/mm.h>
#include <linux/kho/abi/kexec_handover.h>
#include "kexec_handover_internal.h"
static struct dentry *debugfs_root;
struct fdt_debugfs {
struct list_head list;
struct debugfs_blob_wrapper wrapper;
struct dentry *file;
};
static int __kho_debugfs_blob_add(struct list_head *list, struct dentry *dir,
const char *name, const void *blob,
size_t size)
{
struct fdt_debugfs *f;
struct dentry *file;
f = kmalloc_obj(*f);
if (!f)
return -ENOMEM;
f->wrapper.data = (void *)blob;
f->wrapper.size = size;
file = debugfs_create_blob(name, 0400, dir, &f->wrapper);
if (IS_ERR(file)) {
kfree(f);
return PTR_ERR(file);
}
f->file = file;
list_add(&f->list, list);
return 0;
}
int kho_debugfs_blob_add(struct kho_debugfs *dbg, const char *name,
const void *blob, size_t size, bool root)
{
struct dentry *dir;
if (root)
dir = dbg->dir;
else
dir = dbg->sub_fdt_dir;
return __kho_debugfs_blob_add(&dbg->fdt_list, dir, name, blob, size);
}
void kho_debugfs_blob_remove(struct kho_debugfs *dbg, void *blob)
{
struct fdt_debugfs *ff;
list_for_each_entry(ff, &dbg->fdt_list, list) {
if (ff->wrapper.data == blob) {
debugfs_remove(ff->file);
list_del(&ff->list);
kfree(ff);
break;
}
}
}
static int scratch_phys_show(struct seq_file *m, void *v)
{
for (int i = 0; i < kho_scratch_cnt; i++)
seq_printf(m, "0x%llx\n", kho_scratch[i].addr);
return 0;
}
DEFINE_SHOW_ATTRIBUTE(scratch_phys);
static int scratch_len_show(struct seq_file *m, void *v)
{
for (int i = 0; i < kho_scratch_cnt; i++)
seq_printf(m, "0x%llx\n", kho_scratch[i].size);
return 0;
}
DEFINE_SHOW_ATTRIBUTE(scratch_len);
__init void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt)
{
struct dentry *dir, *sub_fdt_dir;
int err, child;
INIT_LIST_HEAD(&dbg->fdt_list);
dir = debugfs_create_dir("in", debugfs_root);
if (IS_ERR(dir)) {
err = PTR_ERR(dir);
goto err_out;
}
sub_fdt_dir = debugfs_create_dir("sub_fdts", dir);
if (IS_ERR(sub_fdt_dir)) {
err = PTR_ERR(sub_fdt_dir);
goto err_rmdir;
}
err = __kho_debugfs_blob_add(&dbg->fdt_list, dir, "fdt", fdt,
fdt_totalsize(fdt));
if (err)
goto err_rmdir;
fdt_for_each_subnode(child, fdt, 0) {
int len = 0;
const char *name = fdt_get_name(fdt, child, NULL);
const u64 *blob_phys;
const u64 *blob_size;
void *blob;
blob_phys = fdt_getprop(fdt, child,
KHO_SUB_TREE_PROP_NAME, &len);
if (!blob_phys)
continue;
if (len != sizeof(*blob_phys)) {
pr_warn("node %s prop %s has invalid length: %d\n",
name, KHO_SUB_TREE_PROP_NAME, len);
continue;
}
blob_size = fdt_getprop(fdt, child,
KHO_SUB_TREE_SIZE_PROP_NAME, &len);
if (!blob_size || len != sizeof(*blob_size)) {
pr_warn("node %s missing or invalid %s property\n",
name, KHO_SUB_TREE_SIZE_PROP_NAME);
continue;
}
blob = phys_to_virt(*blob_phys);
err = __kho_debugfs_blob_add(&dbg->fdt_list, sub_fdt_dir, name,
blob, *blob_size);
if (err) {
pr_warn("failed to add blob %s to debugfs: %pe\n",
name, ERR_PTR(err));
continue;
}
}
dbg->dir = dir;
dbg->sub_fdt_dir = sub_fdt_dir;
return;
err_rmdir:
debugfs_remove_recursive(dir);
err_out:
/*
* Failure to create /sys/kernel/debug/kho/in does not prevent
* reviving state from KHO and setting up KHO for the next
* kexec.
*/
if (err) {
pr_err("failed exposing handover FDT in debugfs: %pe\n",
ERR_PTR(err));
}
}
__init int kho_out_debugfs_init(struct kho_debugfs *dbg)
{
struct dentry *dir, *f, *sub_fdt_dir;
INIT_LIST_HEAD(&dbg->fdt_list);
dir = debugfs_create_dir("out", debugfs_root);
if (IS_ERR(dir))
return -ENOMEM;
sub_fdt_dir = debugfs_create_dir("sub_fdts", dir);
if (IS_ERR(sub_fdt_dir))
goto err_rmdir;
f = debugfs_create_file("scratch_phys", 0400, dir, NULL,
&scratch_phys_fops);
if (IS_ERR(f))
goto err_rmdir;
f = debugfs_create_file("scratch_len", 0400, dir, NULL,
&scratch_len_fops);
if (IS_ERR(f))
goto err_rmdir;
dbg->dir = dir;
dbg->sub_fdt_dir = sub_fdt_dir;
return 0;
err_rmdir:
debugfs_remove_recursive(dir);
return -ENOENT;
}
__init int kho_debugfs_init(void)
{
debugfs_root = debugfs_create_dir("kho", NULL);
if (IS_ERR(debugfs_root))
return -ENOENT;
return 0;
}
@@ -0,0 +1,53 @@
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef LINUX_KEXEC_HANDOVER_INTERNAL_H
#define LINUX_KEXEC_HANDOVER_INTERNAL_H
#include <linux/kexec_handover.h>
#include <linux/list.h>
#include <linux/types.h>
#ifdef CONFIG_KEXEC_HANDOVER_DEBUGFS
#include <linux/debugfs.h>
struct kho_debugfs {
struct dentry *dir;
struct dentry *sub_fdt_dir;
struct list_head fdt_list;
};
#else
struct kho_debugfs {};
#endif
extern struct kho_scratch *kho_scratch;
extern unsigned int kho_scratch_cnt;
#ifdef CONFIG_KEXEC_HANDOVER_DEBUGFS
int kho_debugfs_init(void);
void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt);
int kho_out_debugfs_init(struct kho_debugfs *dbg);
int kho_debugfs_blob_add(struct kho_debugfs *dbg, const char *name,
const void *blob, size_t size, bool root);
void kho_debugfs_blob_remove(struct kho_debugfs *dbg, void *blob);
#else
static inline int kho_debugfs_init(void) { return 0; }
static inline void kho_in_debugfs_init(struct kho_debugfs *dbg,
const void *fdt) { }
static inline int kho_out_debugfs_init(struct kho_debugfs *dbg) { return 0; }
static inline int kho_debugfs_blob_add(struct kho_debugfs *dbg,
const char *name, const void *blob,
size_t size, bool root) { return 0; }
static inline void kho_debugfs_blob_remove(struct kho_debugfs *dbg,
void *blob) { }
#endif /* CONFIG_KEXEC_HANDOVER_DEBUGFS */
#ifdef CONFIG_KEXEC_HANDOVER_DEBUG
bool kho_scratch_overlap(phys_addr_t phys, size_t size);
#else
static inline bool kho_scratch_overlap(phys_addr_t phys, size_t size)
{
return false;
}
#endif /* CONFIG_KEXEC_HANDOVER_DEBUG */
#endif /* LINUX_KEXEC_HANDOVER_INTERNAL_H */
+416
View File
@@ -0,0 +1,416 @@
// 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);
}
+442
View File
@@ -0,0 +1,442 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2025, Google LLC.
* Pasha Tatashin <pasha.tatashin@soleen.com>
*/
/**
* DOC: Live Update Orchestrator (LUO)
*
* Live Update is a specialized, kexec-based reboot process that allows a
* running kernel to be updated from one version to another while preserving
* the state of selected resources and keeping designated hardware devices
* operational. For these devices, DMA activity may continue throughout the
* kernel transition.
*
* While the primary use case driving this work is supporting live updates of
* the Linux kernel when it is used as a hypervisor in cloud environments, the
* LUO framework itself is designed to be workload-agnostic. Live Update
* facilitates a full kernel version upgrade for any type of system.
*
* For example, a non-hypervisor system running an in-memory cache like
* memcached with many gigabytes of data can use LUO. The userspace service
* can place its cache into a memfd, have its state preserved by LUO, and
* restore it immediately after the kernel kexec.
*
* Whether the system is running virtual machines, containers, a
* high-performance database, or networking services, LUO's primary goal is to
* enable a full kernel update by preserving critical userspace state and
* keeping essential devices operational.
*
* The core of LUO is a mechanism that tracks the progress of a live update,
* along with a callback API that allows other kernel subsystems to participate
* in the process. Example subsystems that can hook into LUO include: kvm,
* iommu, interrupts, vfio, participating filesystems, and memory management.
*
* LUO uses Kexec Handover to transfer memory state from the current kernel to
* the next kernel. For more details see Documentation/core-api/kho/index.rst.
*
* .. note::
* To enable LUO, boot the kernel with the ``liveupdate=on`` command line
* parameter.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/atomic.h>
#include <linux/errno.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/kernel.h>
#include <linux/kexec_handover.h>
#include <linux/kho/abi/luo.h>
#include <linux/kobject.h>
#include <linux/liveupdate.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/rwsem.h>
#include <linux/sizes.h>
#include <linux/string.h>
#include "kexec_handover_internal.h"
#include "luo_internal.h"
static struct {
bool enabled;
struct luo_ser *luo_ser_out;
u64 liveupdate_num;
} luo_global;
/*
* luo_register_rwlock - Protects registration of file handlers and FLBs.
*/
DECLARE_RWSEM(luo_register_rwlock);
static int __init early_liveupdate_param(char *buf)
{
return kstrtobool(buf, &luo_global.enabled);
}
early_param("liveupdate", early_liveupdate_param);
static int __init luo_early_startup(void)
{
phys_addr_t luo_ser_phys;
struct luo_ser *luo_ser;
size_t len;
int err;
if (!kho_is_enabled()) {
if (liveupdate_enabled())
pr_warn("Disabling liveupdate because KHO is disabled\n");
luo_global.enabled = false;
return 0;
}
/* Retrieve LUO state from KHO. */
err = kho_retrieve_subtree(LUO_KHO_ENTRY_NAME, &luo_ser_phys, &len);
if (err) {
if (err != -ENOENT) {
pr_err("failed to retrieve LUO state '%s' from KHO: %pe\n",
LUO_KHO_ENTRY_NAME, ERR_PTR(err));
return err;
}
return 0;
}
if (len < sizeof(*luo_ser)) {
pr_err("LUO state is too small (%zu < %zu)\n", len, sizeof(*luo_ser));
return -EINVAL;
}
luo_ser = phys_to_virt(luo_ser_phys);
if (strncmp(luo_ser->compatible, LUO_ABI_COMPATIBLE, LUO_ABI_COMPAT_LEN)) {
pr_err("LUO state is incompatible with '%s'\n", LUO_ABI_COMPATIBLE);
return -EINVAL;
}
luo_global.liveupdate_num = luo_ser->liveupdate_num;
pr_info("Retrieved live update data, liveupdate number: %lld\n",
luo_global.liveupdate_num);
err = luo_session_setup_incoming(luo_ser->sessions_pa);
if (err)
goto out_free_ser;
luo_flb_setup_incoming(luo_ser->flbs_pa);
err = 0;
out_free_ser:
kho_restore_free(luo_ser);
return err;
}
static int __init liveupdate_early_init(void)
{
int err;
err = luo_early_startup();
if (err) {
luo_global.enabled = false;
luo_restore_fail("The incoming tree failed to initialize properly [%pe], disabling live update\n",
ERR_PTR(err));
}
return err;
}
early_initcall(liveupdate_early_init);
/* Called during boot to create outgoing LUO state */
static int __init luo_state_setup(void)
{
struct luo_ser *luo_ser;
int err;
luo_ser = kho_alloc_preserve(sizeof(*luo_ser));
if (IS_ERR(luo_ser)) {
pr_err("failed to allocate/preserve LUO state memory\n");
return PTR_ERR(luo_ser);
}
strscpy(luo_ser->compatible, LUO_ABI_COMPATIBLE, sizeof(luo_ser->compatible));
luo_ser->liveupdate_num = luo_global.liveupdate_num + 1;
luo_session_setup_outgoing(&luo_ser->sessions_pa);
err = luo_flb_setup_outgoing(&luo_ser->flbs_pa);
if (err)
goto exit_free_luo_ser;
err = kho_add_subtree(LUO_KHO_ENTRY_NAME, luo_ser, sizeof(*luo_ser));
if (err)
goto exit_free_luo_ser;
luo_global.luo_ser_out = luo_ser;
return 0;
exit_free_luo_ser:
kho_unpreserve_free(luo_ser);
pr_err("failed to prepare LUO state: %d\n", err);
return err;
}
/*
* late initcall because it initializes the outgoing tree that is needed only
* once userspace starts using /dev/liveupdate.
*/
static int __init luo_late_startup(void)
{
int err;
if (!liveupdate_enabled())
return 0;
err = luo_state_setup();
if (err)
luo_global.enabled = false;
return err;
}
late_initcall(luo_late_startup);
/* Public Functions */
/**
* liveupdate_reboot() - Kernel reboot notifier for live update final
* serialization.
*
* This function is invoked directly from the reboot() syscall pathway
* if kexec is in progress.
*
* If any callback fails, this function aborts KHO, undoes the freeze()
* callbacks, and returns an error.
*/
int liveupdate_reboot(void)
{
int err;
if (!liveupdate_enabled())
return 0;
err = luo_session_serialize();
if (err)
return err;
luo_flb_serialize();
return 0;
}
/**
* liveupdate_enabled - Check if the live update feature is enabled.
*
* This function returns the state of the live update feature flag, which
* can be controlled via the ``liveupdate`` kernel command-line parameter.
*
* @return true if live update is enabled, false otherwise.
*/
bool liveupdate_enabled(void)
{
return luo_global.enabled;
}
/**
* DOC: LUO ioctl Interface
*
* The IOCTL user-space control interface for the LUO subsystem.
* It registers a character device, typically found at ``/dev/liveupdate``,
* which allows a userspace agent to manage the LUO state machine and its
* associated resources, such as preservable file descriptors.
*
* To ensure that the state machine is controlled by a single entity, access
* to this device is exclusive: only one process is permitted to have
* ``/dev/liveupdate`` open at any given time. Subsequent open attempts will
* fail with -EBUSY until the first process closes its file descriptor.
* This singleton model simplifies state management by preventing conflicting
* commands from multiple userspace agents.
*/
struct luo_device_state {
struct miscdevice miscdev;
atomic_t in_use;
};
static int luo_ioctl_create_session(struct luo_ucmd *ucmd)
{
struct liveupdate_ioctl_create_session *argp = ucmd->cmd;
struct file *file;
int err;
argp->fd = get_unused_fd_flags(O_CLOEXEC);
if (argp->fd < 0)
return argp->fd;
err = luo_session_create(argp->name, &file);
if (err)
goto err_put_fd;
err = luo_ucmd_respond(ucmd, sizeof(*argp));
if (err)
goto err_put_file;
fd_install(argp->fd, file);
return 0;
err_put_file:
fput(file);
err_put_fd:
put_unused_fd(argp->fd);
return err;
}
static int luo_ioctl_retrieve_session(struct luo_ucmd *ucmd)
{
struct liveupdate_ioctl_retrieve_session *argp = ucmd->cmd;
struct file *file;
int err;
argp->fd = get_unused_fd_flags(O_CLOEXEC);
if (argp->fd < 0)
return argp->fd;
err = luo_session_retrieve(argp->name, &file);
if (err < 0)
goto err_put_fd;
err = luo_ucmd_respond(ucmd, sizeof(*argp));
if (err)
goto err_put_file;
fd_install(argp->fd, file);
return 0;
err_put_file:
fput(file);
err_put_fd:
put_unused_fd(argp->fd);
return err;
}
static int luo_open(struct inode *inodep, struct file *filep)
{
struct luo_device_state *ldev = container_of(filep->private_data,
struct luo_device_state,
miscdev);
if (atomic_cmpxchg(&ldev->in_use, 0, 1))
return -EBUSY;
/* Always return -EIO to user if deserialization fail */
if (luo_session_deserialize()) {
atomic_set(&ldev->in_use, 0);
return -EIO;
}
return 0;
}
static int luo_release(struct inode *inodep, struct file *filep)
{
struct luo_device_state *ldev = container_of(filep->private_data,
struct luo_device_state,
miscdev);
atomic_set(&ldev->in_use, 0);
return 0;
}
union ucmd_buffer {
struct liveupdate_ioctl_create_session create;
struct liveupdate_ioctl_retrieve_session retrieve;
};
struct luo_ioctl_op {
unsigned int size;
unsigned int min_size;
unsigned int ioctl_num;
int (*execute)(struct luo_ucmd *ucmd);
};
#define IOCTL_OP(_ioctl, _fn, _struct, _last) \
[_IOC_NR(_ioctl) - LIVEUPDATE_CMD_BASE] = { \
.size = sizeof(_struct) + \
BUILD_BUG_ON_ZERO(sizeof(union ucmd_buffer) < \
sizeof(_struct)), \
.min_size = offsetofend(_struct, _last), \
.ioctl_num = _ioctl, \
.execute = _fn, \
}
static const struct luo_ioctl_op luo_ioctl_ops[] = {
IOCTL_OP(LIVEUPDATE_IOCTL_CREATE_SESSION, luo_ioctl_create_session,
struct liveupdate_ioctl_create_session, name),
IOCTL_OP(LIVEUPDATE_IOCTL_RETRIEVE_SESSION, luo_ioctl_retrieve_session,
struct liveupdate_ioctl_retrieve_session, name),
};
static long luo_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
{
const struct luo_ioctl_op *op;
struct luo_ucmd ucmd = {};
union ucmd_buffer buf;
unsigned int nr;
int err;
nr = _IOC_NR(cmd);
if (nr - LIVEUPDATE_CMD_BASE >= ARRAY_SIZE(luo_ioctl_ops))
return -EINVAL;
ucmd.ubuffer = (void __user *)arg;
err = get_user(ucmd.user_size, (u32 __user *)ucmd.ubuffer);
if (err)
return err;
op = &luo_ioctl_ops[nr - LIVEUPDATE_CMD_BASE];
if (op->ioctl_num != cmd)
return -ENOIOCTLCMD;
if (ucmd.user_size < op->min_size)
return -EINVAL;
ucmd.cmd = &buf;
err = copy_struct_from_user(ucmd.cmd, op->size, ucmd.ubuffer,
ucmd.user_size);
if (err)
return err;
return op->execute(&ucmd);
}
static const struct file_operations luo_fops = {
.owner = THIS_MODULE,
.open = luo_open,
.release = luo_release,
.unlocked_ioctl = luo_ioctl,
};
static struct luo_device_state luo_dev = {
.miscdev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "liveupdate",
.fops = &luo_fops,
},
.in_use = ATOMIC_INIT(0),
};
static int __init liveupdate_ioctl_init(void)
{
if (!liveupdate_enabled())
return 0;
return misc_register(&luo_dev.miscdev);
}
late_initcall(liveupdate_ioctl_init);
+928
View File
@@ -0,0 +1,928 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2025, Google LLC.
* Pasha Tatashin <pasha.tatashin@soleen.com>
*/
/**
* DOC: LUO File Descriptors
*
* LUO provides the infrastructure to preserve specific, stateful file
* descriptors across a kexec-based live update. The primary goal is to allow
* workloads, such as virtual machines using vfio, memfd, or iommufd, to
* retain access to their essential resources without interruption.
*
* The framework is built around a callback-based handler model and a well-
* defined lifecycle for each preserved file.
*
* Handler Registration:
* Kernel modules responsible for a specific file type (e.g., memfd, vfio)
* register a &struct liveupdate_file_handler. This handler provides a set of
* callbacks that LUO invokes at different stages of the update process, most
* notably:
*
* - can_preserve(): A lightweight check to determine if the handler is
* compatible with a given 'struct file'.
* - preserve(): The heavyweight operation that saves the file's state and
* returns an opaque u64 handle. This is typically performed while the
* workload is still active to minimize the downtime during the
* actual reboot transition.
* - unpreserve(): Cleans up any resources allocated by .preserve(), called
* if the preservation process is aborted before the reboot (i.e. session is
* closed).
* - freeze(): A final pre-reboot opportunity to prepare the state for kexec.
* We are already in reboot syscall, and therefore userspace cannot mutate
* the file anymore.
* - unfreeze(): Undoes the actions of .freeze(), called if the live update
* is aborted after the freeze phase.
* - retrieve(): Reconstructs the file in the new kernel from the preserved
* handle.
* - finish(): Performs final check and cleanup in the new kernel. After
* succesul finish call, LUO gives up ownership to this file.
*
* File Preservation Lifecycle happy path:
*
* 1. Preserve (Normal Operation): A userspace agent preserves files one by one
* via an ioctl. For each file, luo_preserve_file() finds a compatible
* handler, calls its .preserve() operation, and creates an internal &struct
* luo_file to track the live state.
*
* 2. Freeze (Pre-Reboot): Just before the kexec, luo_file_freeze() is called.
* It iterates through all preserved files, calls their respective .freeze()
* operation, and serializes their final metadata (compatible string, token,
* and data handle) into a contiguous memory block for KHO.
*
* 3. Deserialize: After kexec, luo_file_deserialize() runs when session gets
* deserialized (which is when /dev/liveupdate is first opened). It reads the
* serialized data from the KHO memory region and reconstructs the in-memory
* list of &struct luo_file instances for the new kernel, linking them to
* their corresponding handlers.
*
* 4. Retrieve (New Kernel - Userspace Ready): The userspace agent can now
* restore file descriptors by providing a token. luo_retrieve_file()
* searches for the matching token, calls the handler's .retrieve() op to
* re-create the 'struct file', and returns a new FD. Files can be
* retrieved in ANY order.
*
* 5. Finish (New Kernel - Cleanup): Once a session retrival is complete,
* luo_file_finish() is called. It iterates through all files, invokes their
* .finish() operations for final cleanup, and releases all associated kernel
* resources.
*
* File Preservation Lifecycle unhappy paths:
*
* 1. Abort Before Reboot: If the userspace agent aborts the live update
* process before calling reboot (e.g., by closing the session file
* descriptor), the session's release handler calls
* luo_file_unpreserve_files(). This invokes the .unpreserve() callback on
* all preserved files, ensuring all allocated resources are cleaned up and
* returning the system to a clean state.
*
* 2. Freeze Failure: During the reboot() syscall, if any handler's .freeze()
* op fails, the .unfreeze() op is invoked on all previously *successful*
* freezes to roll back their state. The reboot() syscall then returns an
* error to userspace, canceling the live update.
*
* 3. Finish Failure: In the new kernel, if a handler's .finish() op fails,
* the luo_file_finish() operation is aborted. LUO retains ownership of
* all files within that session, including those that were not yet
* processed. The userspace agent can attempt to call the finish operation
* again later. If the issue cannot be resolved, these resources will be held
* by LUO until the next live update cycle, at which point they will be
* discarded.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/cleanup.h>
#include <linux/compiler.h>
#include <linux/err.h>
#include <linux/errno.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/kexec_handover.h>
#include <linux/kho/abi/luo.h>
#include <linux/list_private.h>
#include <linux/liveupdate.h>
#include <linux/module.h>
#include <linux/sizes.h>
#include <linux/xarray.h>
#include <linux/slab.h>
#include <linux/string.h>
#include "luo_internal.h"
static LIST_HEAD(luo_file_handler_list);
/* Keep track of files being preserved by LUO */
static DEFINE_XARRAY(luo_preserved_files);
/**
* struct luo_file - Represents a single preserved file instance.
* @fh: Pointer to the &struct liveupdate_file_handler that manages
* this type of file.
* @file: Pointer to the kernel's &struct file that is being preserved.
* This is NULL in the new kernel until the file is successfully
* retrieved.
* @serialized_data: The opaque u64 handle to the serialized state of the file.
* This handle is passed back to the handler's .freeze(),
* .retrieve(), and .finish() callbacks, allowing it to track
* and update its serialized state across phases.
* @private_data: Pointer to the private data for the file used to hold runtime
* state that is not preserved. Set by the handler's .preserve()
* callback, and must be freed in the handler's .unpreserve()
* callback.
* @retrieve_status: Status code indicating whether a user/kernel in the new kernel has
* successfully called retrieve() on this file. This prevents
* multiple retrieval attempts. A value of 0 means a retrieve()
* has not been attempted, a positive value means the retrieve()
* was successful, and a negative value means the retrieve()
* failed, and the value is the error code of the call.
* @mutex: A mutex that protects the fields of this specific instance
* (e.g., @retrieved, @file), ensuring that operations like
* retrieving or finishing a file are atomic.
* @list: The list_head linking this instance into its parent
* file_set's list of preserved files.
* @token: The user-provided unique token used to identify this file.
*
* This structure is the core in-kernel representation of a single file being
* managed through a live update. An instance is created by luo_preserve_file()
* to link a 'struct file' to its corresponding handler, a user-provided token,
* and the serialized state handle returned by the handler's .preserve()
* operation.
*
* These instances are tracked in a per-file_set list. The @serialized_data
* field, which holds a handle to the file's serialized state, may be updated
* during the .freeze() callback before being serialized for the next kernel.
* After reboot, these structures are recreated by luo_file_deserialize() and
* are finally cleaned up by luo_file_finish().
*/
struct luo_file {
struct liveupdate_file_handler *fh;
struct file *file;
u64 serialized_data;
void *private_data;
int retrieve_status;
struct mutex mutex;
struct list_head list;
u64 token;
};
static unsigned long luo_get_id(struct liveupdate_file_handler *fh,
struct file *file)
{
return fh->ops->get_id ? fh->ops->get_id(file) : (unsigned long)file;
}
static bool luo_token_is_used(struct luo_file_set *file_set, u64 token)
{
struct luo_file *iter;
list_for_each_entry(iter, &file_set->files_list, list) {
if (iter->token == token)
return true;
}
return false;
}
/**
* luo_preserve_file - Initiate the preservation of a file descriptor.
* @file_set: The file_set to which the preserved file will be added.
* @token: A unique, user-provided identifier for the file.
* @fd: The file descriptor to be preserved.
*
* This function orchestrates the first phase of preserving a file. Upon entry,
* it takes a reference to the 'struct file' via fget(), effectively making LUO
* a co-owner of the file. This reference is held until the file is either
* unpreserved or successfully finished in the next kernel, preventing the file
* from being prematurely destroyed.
*
* This function orchestrates the first phase of preserving a file. It performs
* the following steps:
*
* 1. Validates that the @token is not already in use within the file_set.
* 2. Ensures the file_set's memory for files serialization is allocated
* (allocates if needed).
* 3. Iterates through registered handlers, calling can_preserve() to find one
* compatible with the given @fd.
* 4. Calls the handler's .preserve() operation, which saves the file's state
* and returns an opaque private data handle.
* 5. Adds the new instance to the file_set's internal list.
*
* On success, LUO takes a reference to the 'struct file' and considers it
* under its management until it is unpreserved or finished.
*
* In case of any failure, all intermediate allocations (file reference, memory
* for the 'luo_file' struct, etc.) are cleaned up before returning an error.
*
* Context: Can be called from an ioctl handler during normal system operation.
* Return: 0 on success. Returns a negative errno on failure:
* -EEXIST if the token is already used.
* -EBUSY if the file descriptor is already preserved by another session.
* -EBADF if the file descriptor is invalid.
* -ENOSPC if the file_set is full.
* -ENOENT if no compatible handler is found.
* -ENOMEM on memory allocation failure.
* Other erros might be returned by .preserve().
*/
int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd)
{
struct liveupdate_file_op_args args = {0};
struct liveupdate_file_handler *fh;
struct luo_file *luo_file;
struct file *file;
int err;
if (luo_token_is_used(file_set, token))
return -EEXIST;
err = kho_block_set_grow(&file_set->block_set, file_set->count + 1);
if (err)
return err;
file = fget(fd);
if (!file) {
err = -EBADF;
goto err_shrink;
}
err = -ENOENT;
down_read(&luo_register_rwlock);
list_private_for_each_entry(fh, &luo_file_handler_list, list) {
if (fh->ops->can_preserve(fh, file)) {
if (try_module_get(fh->ops->owner))
err = 0;
break;
}
}
up_read(&luo_register_rwlock);
/* err is still -ENOENT if no handler was found */
if (err)
goto err_fput;
err = xa_insert(&luo_preserved_files, luo_get_id(fh, file),
file, GFP_KERNEL);
if (err)
goto err_module_put;
err = luo_flb_file_preserve(fh);
if (err)
goto err_erase_xa;
luo_file = kzalloc_obj(*luo_file);
if (!luo_file) {
err = -ENOMEM;
goto err_flb_unpreserve;
}
luo_file->file = file;
luo_file->fh = fh;
luo_file->token = token;
mutex_init(&luo_file->mutex);
args.handler = fh;
args.file = file;
err = fh->ops->preserve(&args);
if (err)
goto err_kfree;
luo_file->serialized_data = args.serialized_data;
luo_file->private_data = args.private_data;
list_add_tail(&luo_file->list, &file_set->files_list);
file_set->count++;
return 0;
err_kfree:
kfree(luo_file);
err_flb_unpreserve:
luo_flb_file_unpreserve(fh);
err_erase_xa:
xa_erase(&luo_preserved_files, luo_get_id(fh, file));
err_module_put:
module_put(fh->ops->owner);
err_fput:
fput(file);
err_shrink:
kho_block_set_shrink(&file_set->block_set, file_set->count);
return err;
}
/**
* luo_file_unpreserve_files - Unpreserves all files from a file_set.
* @file_set: The files to be cleaned up.
*
* This function serves as the primary cleanup path for a file_set. It is
* invoked when the userspace agent closes the file_set's file descriptor.
*
* For each file, it performs the following cleanup actions:
* 1. Calls the handler's .unpreserve() callback to allow the handler to
* release any resources it allocated.
* 2. Removes the file from the file_set's internal tracking list.
* 3. Releases the reference to the 'struct file' that was taken by
* luo_preserve_file() via fput(), returning ownership.
* 4. Frees the memory associated with the internal 'struct luo_file'.
*
* After all individual files are unpreserved, it frees the contiguous memory
* block that was allocated to hold their serialization data.
*/
void luo_file_unpreserve_files(struct luo_file_set *file_set)
{
struct luo_file *luo_file;
while (!list_empty(&file_set->files_list)) {
struct liveupdate_file_op_args args = {0};
luo_file = list_last_entry(&file_set->files_list,
struct luo_file, list);
args.handler = luo_file->fh;
args.file = luo_file->file;
args.serialized_data = luo_file->serialized_data;
args.private_data = luo_file->private_data;
luo_file->fh->ops->unpreserve(&args);
luo_flb_file_unpreserve(luo_file->fh);
xa_erase(&luo_preserved_files,
luo_get_id(luo_file->fh, luo_file->file));
module_put(luo_file->fh->ops->owner);
list_del(&luo_file->list);
file_set->count--;
kho_block_set_shrink(&file_set->block_set, file_set->count);
fput(luo_file->file);
mutex_destroy(&luo_file->mutex);
kfree(luo_file);
}
kho_block_set_destroy(&file_set->block_set);
}
static int luo_file_freeze_one(struct luo_file_set *file_set,
struct luo_file *luo_file)
{
int err = 0;
guard(mutex)(&luo_file->mutex);
if (luo_file->fh->ops->freeze) {
struct liveupdate_file_op_args args = {0};
args.handler = luo_file->fh;
args.file = luo_file->file;
args.serialized_data = luo_file->serialized_data;
args.private_data = luo_file->private_data;
err = luo_file->fh->ops->freeze(&args);
if (!err)
luo_file->serialized_data = args.serialized_data;
}
return err;
}
static void luo_file_unfreeze_one(struct luo_file_set *file_set,
struct luo_file *luo_file)
{
guard(mutex)(&luo_file->mutex);
if (luo_file->fh->ops->unfreeze) {
struct liveupdate_file_op_args args = {0};
args.handler = luo_file->fh;
args.file = luo_file->file;
args.serialized_data = luo_file->serialized_data;
args.private_data = luo_file->private_data;
luo_file->fh->ops->unfreeze(&args);
}
}
static void __luo_file_unfreeze(struct luo_file_set *file_set,
struct luo_file *failed_entry)
{
struct list_head *files_list = &file_set->files_list;
struct luo_file *luo_file;
list_for_each_entry(luo_file, files_list, list) {
if (luo_file == failed_entry)
break;
luo_file_unfreeze_one(file_set, luo_file);
}
kho_block_set_clear(&file_set->block_set);
}
/**
* luo_file_freeze - Freezes all preserved files and serializes their metadata.
* @file_set: The file_set whose files are to be frozen.
* @file_set_ser: Where to put the serialized file_set.
*
* This function is called from the reboot() syscall path, just before the
* kernel transitions to the new image via kexec. Its purpose is to perform the
* final preparation and serialization of all preserved files in the file_set.
*
* It iterates through each preserved file in FIFO order (the order of
* preservation) and performs two main actions:
*
* 1. Freezes the File: It calls the handler's .freeze() callback for each
* file. This gives the handler a final opportunity to quiesce the device or
* prepare its state for the upcoming reboot. The handler may update its
* private data handle during this step.
*
* 2. Serializes Metadata: After a successful freeze, it copies the final file
* metadata—the handler's compatible string, the user token, and the final
* private data handle—into the pre-allocated contiguous memory buffer
* (file_set->files) that will be handed over to the next kernel via KHO.
*
* Error Handling (Rollback):
* This function is atomic. If any handler's .freeze() operation fails, the
* entire live update is aborted. The __luo_file_unfreeze() helper is
* immediately called to invoke the .unfreeze() op on all files that were
* successfully frozen before the point of failure, rolling them back to a
* running state. The function then returns an error, causing the reboot()
* syscall to fail.
*
* Context: Called only from the liveupdate_reboot() path.
* Return: 0 on success, or a negative errno on failure.
*/
int luo_file_freeze(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser)
{
struct luo_file *luo_file;
struct kho_block_set_it it;
int err;
if (!file_set->count)
return 0;
kho_block_set_it_init(&it, &file_set->block_set);
list_for_each_entry(luo_file, &file_set->files_list, list) {
struct luo_file_ser *file_ser = kho_block_set_it_reserve_entry(&it);
/* This should not fail normally as blocks were pre-allocated */
if (WARN_ON_ONCE(!file_ser)) {
err = -ENOSPC;
goto err_unfreeze;
}
err = luo_file_freeze_one(file_set, luo_file);
if (err < 0) {
pr_warn("Freeze failed for token[%#0llx] handler[%s] err[%pe]\n",
luo_file->token, luo_file->fh->compatible,
ERR_PTR(err));
goto err_unfreeze;
}
strscpy(file_ser->compatible, luo_file->fh->compatible,
sizeof(file_ser->compatible));
file_ser->data = luo_file->serialized_data;
file_ser->token = luo_file->token;
}
file_set_ser->count = file_set->count;
file_set_ser->files = kho_block_set_head_pa(&file_set->block_set);
return 0;
err_unfreeze:
__luo_file_unfreeze(file_set, luo_file);
return err;
}
/**
* luo_file_unfreeze - Unfreezes all files in a file_set and clear serialization
* @file_set: The file_set whose files are to be unfrozen.
* @file_set_ser: Serialized file_set.
*
* This function rolls back the state of all files in a file_set after the
* freeze phase has begun but must be aborted. It is the counterpart to
* luo_file_freeze().
*
* It invokes the __luo_file_unfreeze() helper with a NULL argument, which
* signals the helper to iterate through all files in the file_set and call
* their respective .unfreeze() handler callbacks.
*
* Context: This is called when the live update is aborted during
* the reboot() syscall, after luo_file_freeze() has been called.
*/
void luo_file_unfreeze(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser)
{
if (!file_set->count)
return;
__luo_file_unfreeze(file_set, NULL);
memset(file_set_ser, 0, sizeof(*file_set_ser));
}
/**
* luo_retrieve_file - Restores a preserved file from a file_set by its token.
* @file_set: The file_set from which to retrieve the file.
* @token: The unique token identifying the file to be restored.
* @filep: Output parameter; on success, this is populated with a pointer
* to the newly retrieved 'struct file'.
*
* This function is the primary mechanism for recreating a file in the new
* kernel after a live update. It searches the file_set's list of deserialized
* files for an entry matching the provided @token.
*
* The operation is idempotent: if a file has already been successfully
* retrieved, this function will simply return a pointer to the existing
* 'struct file' and report success without re-executing the retrieve
* operation. This is handled by checking the 'retrieved' flag under a lock.
*
* File retrieval can happen in any order; it is not bound by the order of
* preservation.
*
* Context: Can be called from an ioctl or other in-kernel code in the new
* kernel.
* Return: 0 on success. Returns a negative errno on failure:
* -ENOENT if no file with the matching token is found.
* Any error code returned by the handler's .retrieve() op.
*/
int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
struct file **filep)
{
struct liveupdate_file_op_args args = {0};
struct luo_file *luo_file;
bool found = false;
int err;
if (list_empty(&file_set->files_list))
return -ENOENT;
list_for_each_entry(luo_file, &file_set->files_list, list) {
if (luo_file->token == token) {
found = true;
break;
}
}
if (!found)
return -ENOENT;
guard(mutex)(&luo_file->mutex);
if (luo_file->retrieve_status < 0) {
/* Retrieve was attempted and it failed. Return the error code. */
return luo_file->retrieve_status;
}
if (luo_file->retrieve_status > 0) {
/*
* Someone is asking for this file again, so get a reference
* for them.
*/
get_file(luo_file->file);
*filep = luo_file->file;
return 0;
}
args.handler = luo_file->fh;
args.serialized_data = luo_file->serialized_data;
err = luo_file->fh->ops->retrieve(&args);
if (err) {
/* Keep the error code for later use. */
luo_file->retrieve_status = err;
return err;
}
luo_file->file = args.file;
/* Get reference so we can keep this file in LUO until finish */
get_file(luo_file->file);
WARN_ON(xa_insert(&luo_preserved_files,
luo_get_id(luo_file->fh, luo_file->file),
luo_file->file, GFP_KERNEL));
*filep = luo_file->file;
luo_file->retrieve_status = 1;
return 0;
}
static int luo_file_can_finish_one(struct luo_file_set *file_set,
struct luo_file *luo_file)
{
bool can_finish = true;
guard(mutex)(&luo_file->mutex);
if (luo_file->fh->ops->can_finish) {
struct liveupdate_file_op_args args = {0};
args.handler = luo_file->fh;
args.file = luo_file->file;
args.serialized_data = luo_file->serialized_data;
args.retrieve_status = luo_file->retrieve_status;
can_finish = luo_file->fh->ops->can_finish(&args);
}
return can_finish ? 0 : -EBUSY;
}
static void luo_file_finish_one(struct luo_file_set *file_set,
struct luo_file *luo_file)
{
struct liveupdate_file_op_args args = {0};
guard(mutex)(&luo_file->mutex);
args.handler = luo_file->fh;
args.file = luo_file->file;
args.serialized_data = luo_file->serialized_data;
args.retrieve_status = luo_file->retrieve_status;
luo_file->fh->ops->finish(&args);
luo_flb_file_finish(luo_file->fh);
}
/**
* luo_file_finish - Completes the lifecycle for all files in a file_set.
* @file_set: The file_set to be finalized.
*
* This function orchestrates the final teardown of a live update file_set in
* the new kernel. It should be called after all necessary files have been
* retrieved and the userspace agent is ready to release the preserved state.
*
* The function iterates through all tracked files. For each file, it performs
* the following sequence of cleanup actions:
*
* 1. If file is not yet retrieved, retrieves it, and calls can_finish() on
* every file in the file_set. If all can_finish return true, continue to
* finish.
* 2. Calls the handler's .finish() callback (via luo_file_finish_one) to
* allow for final resource cleanup within the handler.
* 3. Releases LUO's ownership reference on the 'struct file' via fput(). This
* is the counterpart to the get_file() call in luo_retrieve_file().
* 4. Removes the 'struct luo_file' from the file_set's internal list.
* 5. Frees the memory for the 'struct luo_file' instance itself.
*
* After successfully finishing all individual files, it frees the
* contiguous memory block that was used to transfer the serialized metadata
* from the previous kernel.
*
* Error Handling (Atomic Failure):
* This operation is atomic. If any handler's .can_finish() op fails, the entire
* function aborts immediately and returns an error.
*
* Context: Can be called from an ioctl handler in the new kernel.
* Return: 0 on success, or a negative errno on failure.
*/
int luo_file_finish(struct luo_file_set *file_set)
{
struct list_head *files_list = &file_set->files_list;
struct luo_file *luo_file;
int err;
if (!file_set->count)
return 0;
list_for_each_entry(luo_file, files_list, list) {
err = luo_file_can_finish_one(file_set, luo_file);
if (err)
return err;
}
while (!list_empty(&file_set->files_list)) {
luo_file = list_last_entry(&file_set->files_list,
struct luo_file, list);
luo_file_finish_one(file_set, luo_file);
if (luo_file->file) {
xa_erase(&luo_preserved_files,
luo_get_id(luo_file->fh, luo_file->file));
fput(luo_file->file);
}
module_put(luo_file->fh->ops->owner);
list_del(&luo_file->list);
file_set->count--;
kho_block_set_shrink(&file_set->block_set, file_set->count);
mutex_destroy(&luo_file->mutex);
kfree(luo_file);
}
kho_block_set_destroy(&file_set->block_set);
return 0;
}
static int luo_file_deserialize_one(struct luo_file_set *file_set,
struct luo_file_ser *ser)
{
struct liveupdate_file_handler *fh;
bool handler_found = false;
struct luo_file *luo_file;
down_read(&luo_register_rwlock);
list_private_for_each_entry(fh, &luo_file_handler_list, list) {
if (!strcmp(fh->compatible, ser->compatible)) {
if (try_module_get(fh->ops->owner))
handler_found = true;
break;
}
}
up_read(&luo_register_rwlock);
if (!handler_found) {
pr_warn("No registered handler for compatible '%.*s'\n",
(int)sizeof(ser->compatible),
ser->compatible);
return -ENOENT;
}
luo_file = kzalloc_obj(*luo_file);
if (!luo_file) {
module_put(fh->ops->owner);
return -ENOMEM;
}
luo_file->fh = fh;
luo_file->file = NULL;
luo_file->serialized_data = ser->data;
luo_file->token = ser->token;
mutex_init(&luo_file->mutex);
list_add_tail(&luo_file->list, &file_set->files_list);
return 0;
}
/**
* luo_file_deserialize - Reconstructs the list of preserved files in the new kernel.
* @file_set: The incoming file_set to fill with deserialized data.
* @file_set_ser: Serialized KHO file_set data from the previous kernel.
*
* This function is called during the early boot process of the new kernel. It
* takes the raw, contiguous memory block of 'struct luo_file_ser' entries,
* provided by the previous kernel, and transforms it back into a live,
* in-memory linked list of 'struct luo_file' instances.
*
* For each serialized entry, it performs the following steps:
* 1. Reads the 'compatible' string.
* 2. Searches the global list of registered file handlers for one that
* matches the compatible string.
* 3. Allocates a new 'struct luo_file'.
* 4. Populates the new structure with the deserialized data (token, private
* data handle) and links it to the found handler. The 'file' pointer is
* initialized to NULL, as the file has not been retrieved yet.
* 5. Adds the new 'struct luo_file' to the file_set's files_list.
*
* This prepares the file_set for userspace, which can later call
* luo_retrieve_file() to restore the actual file descriptors.
*
* Context: Called from session deserialization.
*/
int luo_file_deserialize(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser)
{
struct luo_file_ser *file_ser;
struct kho_block_set_it it;
int err;
if (!file_set_ser->files) {
WARN_ON(file_set_ser->count);
return 0;
}
file_set->count = 0;
err = kho_block_set_restore(&file_set->block_set, file_set_ser->files);
if (err)
return err;
/*
* Note on error handling:
*
* If deserialization fails (e.g., allocation failure or corrupt data),
* we intentionally skip cleanup of files that were already restored.
*
* A partial failure leaves the preserved state inconsistent.
* Implementing a safe "undo" to unwind complex dependencies (sessions,
* files, hardware state) is error-prone and provides little value, as
* the system is effectively in a broken state.
*
* We treat these resources as leaked. The expected recovery path is for
* userspace to detect the failure and trigger a reboot, which will
* reliably reset devices and reclaim memory.
*/
kho_block_set_it_init(&it, &file_set->block_set);
while ((file_ser = kho_block_set_it_read_entry(&it))) {
err = luo_file_deserialize_one(file_set, file_ser);
if (err)
goto err_destroy_blocks;
file_set->count++;
}
if (file_set->count != file_set_ser->count) {
pr_warn("File count mismatch: expected %llu, found %llu\n",
file_set_ser->count, file_set->count);
err = -EINVAL;
goto err_destroy_blocks;
}
return 0;
err_destroy_blocks:
while (!list_empty(&file_set->files_list)) {
struct luo_file *luo_file;
luo_file = list_first_entry(&file_set->files_list,
struct luo_file, list);
list_del(&luo_file->list);
module_put(luo_file->fh->ops->owner);
mutex_destroy(&luo_file->mutex);
kfree(luo_file);
}
file_set->count = 0;
kho_block_set_destroy(&file_set->block_set);
return err;
}
void luo_file_set_init(struct luo_file_set *file_set)
{
INIT_LIST_HEAD(&file_set->files_list);
kho_block_set_init(&file_set->block_set, sizeof(struct luo_file_ser));
}
void luo_file_set_destroy(struct luo_file_set *file_set)
{
WARN_ON(file_set->count);
WARN_ON(!list_empty(&file_set->files_list));
WARN_ON(!kho_block_set_is_empty(&file_set->block_set));
}
/**
* liveupdate_register_file_handler - Register a file handler with LUO.
* @fh: Pointer to a caller-allocated &struct liveupdate_file_handler.
* The caller must initialize this structure, including a unique
* 'compatible' string and a valid 'fh' callbacks. This function adds the
* handler to the global list of supported file handlers.
*
* Context: Typically called during module initialization for file types that
* support live update preservation.
*
* Return: 0 on success. Negative errno on failure.
*/
int liveupdate_register_file_handler(struct liveupdate_file_handler *fh)
{
struct liveupdate_file_handler *fh_iter;
int err;
if (!liveupdate_enabled())
return -EOPNOTSUPP;
/* Sanity check that all required callbacks are set */
if (!fh->ops->preserve || !fh->ops->unpreserve || !fh->ops->retrieve ||
!fh->ops->finish || !fh->ops->can_preserve) {
return -EINVAL;
}
down_write(&luo_register_rwlock);
/* Check for duplicate compatible strings */
list_private_for_each_entry(fh_iter, &luo_file_handler_list, list) {
if (!strcmp(fh_iter->compatible, fh->compatible)) {
pr_err("File handler registration failed: Compatible string '%s' already registered.\n",
fh->compatible);
err = -EEXIST;
goto err_unlock;
}
}
INIT_LIST_HEAD(&ACCESS_PRIVATE(fh, flb_list));
INIT_LIST_HEAD(&ACCESS_PRIVATE(fh, list));
list_add_tail(&ACCESS_PRIVATE(fh, list), &luo_file_handler_list);
up_write(&luo_register_rwlock);
liveupdate_test_register(fh);
return 0;
err_unlock:
up_write(&luo_register_rwlock);
return err;
}
/**
* liveupdate_unregister_file_handler - Unregister a liveupdate file handler
* @fh: The file handler to unregister
*
* Unregisters the file handler from the liveupdate core. This function
* reverses the operations of liveupdate_register_file_handler().
*/
void liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh)
{
if (!liveupdate_enabled())
return;
guard(rwsem_write)(&luo_register_rwlock);
luo_flb_unregister_all(fh);
list_del(&ACCESS_PRIVATE(fh, list));
}
+618
View File
@@ -0,0 +1,618 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2025, Google LLC.
* Pasha Tatashin <pasha.tatashin@soleen.com>
*/
/**
* DOC: LUO File Lifecycle Bound Global Data
*
* File-Lifecycle-Bound (FLB) objects provide a mechanism for managing global
* state that is shared across multiple live-updatable files. The lifecycle of
* this shared state is tied to the preservation of the files that depend on it.
*
* An FLB represents a global resource, such as the IOMMU core state, that is
* required by multiple file descriptors (e.g., all VFIO fds).
*
* The preservation of the FLB's state is triggered when the *first* file
* depending on it is preserved. The cleanup of this state (unpreserve or
* finish) is triggered when the *last* file depending on it is unpreserved or
* finished.
*
* Handler Dependency: A file handler declares its dependency on one or more
* FLBs by registering them via liveupdate_register_flb().
*
* Callback Model: Each FLB is defined by a set of operations
* (&struct liveupdate_flb_ops) that LUO invokes at key points:
*
* - .preserve(): Called for the first file. Saves global state.
* - .unpreserve(): Called for the last file (if aborted pre-reboot).
* - .retrieve(): Called on-demand in the new kernel to restore the state.
* - .finish(): Called for the last file in the new kernel for cleanup.
*
* This reference-counted approach ensures that shared state is saved exactly
* once and restored exactly once, regardless of how many files depend on it,
* and that its lifecycle is correctly managed across the kexec transition.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/cleanup.h>
#include <linux/err.h>
#include <linux/errno.h>
#include <linux/io.h>
#include <linux/kexec_handover.h>
#include <linux/kho/abi/luo.h>
#include <linux/list_private.h>
#include <linux/liveupdate.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include "luo_internal.h"
#define LUO_FLB_PGCNT 1ul
#define LUO_FLB_MAX (((LUO_FLB_PGCNT << PAGE_SHIFT) - \
sizeof(struct luo_flb_header_ser)) / sizeof(struct luo_flb_ser))
struct luo_flb_header {
struct luo_flb_header_ser *header_ser;
struct luo_flb_ser *ser;
bool active;
};
struct luo_flb_global {
struct luo_flb_header incoming;
struct luo_flb_header outgoing;
struct list_head list;
long count;
};
static struct luo_flb_global luo_flb_global = {
.list = LIST_HEAD_INIT(luo_flb_global.list),
};
/*
* struct luo_flb_link - Links an FLB definition to a file handler's internal
* list of dependencies.
* @flb: A pointer to the registered &struct liveupdate_flb definition.
* @list: The list_head for linking.
*/
struct luo_flb_link {
struct liveupdate_flb *flb;
struct list_head list;
};
/* luo_flb_get_private - Access private field, and if needed initialize it. */
static struct luo_flb_private *luo_flb_get_private(struct liveupdate_flb *flb)
{
struct luo_flb_private *private = &ACCESS_PRIVATE(flb, private);
static DEFINE_SPINLOCK(luo_flb_init_lock);
if (smp_load_acquire(&private->initialized))
return private;
guard(spinlock)(&luo_flb_init_lock);
if (!private->initialized) {
mutex_init(&private->incoming.lock);
mutex_init(&private->outgoing.lock);
INIT_LIST_HEAD(&private->list);
private->users = 0;
smp_store_release(&private->initialized, true);
}
return private;
}
static int luo_flb_file_preserve_one(struct liveupdate_flb *flb)
{
struct luo_flb_private *private = luo_flb_get_private(flb);
scoped_guard(mutex, &private->outgoing.lock) {
if (!refcount_read(&private->outgoing.count)) {
struct liveupdate_flb_op_args args = {0};
int err;
if (!try_module_get(flb->ops->owner))
return -ENODEV;
args.flb = flb;
err = flb->ops->preserve(&args);
if (err) {
module_put(flb->ops->owner);
return err;
}
private->outgoing.data = args.data;
private->outgoing.obj = args.obj;
refcount_set(&private->outgoing.count, 1);
} else {
refcount_inc(&private->outgoing.count);
}
}
return 0;
}
static void luo_flb_file_unpreserve_one(struct liveupdate_flb *flb)
{
struct luo_flb_private *private = luo_flb_get_private(flb);
scoped_guard(mutex, &private->outgoing.lock) {
if (refcount_dec_and_test(&private->outgoing.count)) {
struct liveupdate_flb_op_args args = {0};
args.flb = flb;
args.data = private->outgoing.data;
args.obj = private->outgoing.obj;
if (flb->ops->unpreserve)
flb->ops->unpreserve(&args);
private->outgoing.data = 0;
private->outgoing.obj = NULL;
module_put(flb->ops->owner);
}
}
}
static int luo_flb_retrieve_one(struct liveupdate_flb *flb)
{
struct luo_flb_private *private = luo_flb_get_private(flb);
struct luo_flb_header *fh = &luo_flb_global.incoming;
struct liveupdate_flb_op_args args = {0};
bool found = false;
int err;
lockdep_assert_held(&private->incoming.lock);
if (private->incoming.finished)
return -ENODATA;
if (private->incoming.retrieved)
return 0;
if (!fh->active)
return -ENODATA;
for (int i = 0; i < fh->header_ser->count; i++) {
if (!strcmp(fh->ser[i].name, flb->compatible)) {
private->incoming.data = fh->ser[i].data;
refcount_set(&private->incoming.count, fh->ser[i].count);
found = true;
break;
}
}
if (!found)
return -ENOENT;
if (!try_module_get(flb->ops->owner))
return -ENODEV;
args.flb = flb;
args.data = private->incoming.data;
err = flb->ops->retrieve(&args);
if (err) {
module_put(flb->ops->owner);
return err;
}
private->incoming.obj = args.obj;
private->incoming.retrieved = true;
return 0;
}
void liveupdate_flb_put_incoming(struct liveupdate_flb *flb)
{
struct luo_flb_private *private = luo_flb_get_private(flb);
struct liveupdate_flb_op_args args = {0};
scoped_guard(mutex, &private->incoming.lock) {
if (!refcount_dec_and_test(&private->incoming.count))
return;
if (!private->incoming.retrieved) {
int err = luo_flb_retrieve_one(flb);
if (WARN_ON(err))
return;
}
args.flb = flb;
args.obj = private->incoming.obj;
flb->ops->finish(&args);
private->incoming.data = 0;
private->incoming.obj = NULL;
private->incoming.finished = true;
module_put(flb->ops->owner);
}
}
/**
* luo_flb_file_preserve - Notifies FLBs that a file is about to be preserved.
* @fh: The file handler for the preserved file.
*
* This function iterates through all FLBs associated with the given file
* handler. It increments the reference count for each FLB. If the count becomes
* 1, it triggers the FLB's .preserve() callback to save the global state.
*
* This operation is atomic. If any FLB's .preserve() op fails, it will roll
* back by calling .unpreserve() on any FLBs that were successfully preserved
* during this call.
*
* Context: Called from luo_preserve_file()
* Return: 0 on success, or a negative errno on failure.
*/
int luo_flb_file_preserve(struct liveupdate_file_handler *fh)
{
struct list_head *flb_list = &ACCESS_PRIVATE(fh, flb_list);
struct luo_flb_link *iter;
int err = 0;
down_read(&luo_register_rwlock);
list_for_each_entry(iter, flb_list, list) {
err = luo_flb_file_preserve_one(iter->flb);
if (err)
goto exit_err;
}
up_read(&luo_register_rwlock);
return 0;
exit_err:
list_for_each_entry_continue_reverse(iter, flb_list, list)
luo_flb_file_unpreserve_one(iter->flb);
up_read(&luo_register_rwlock);
return err;
}
/**
* luo_flb_file_unpreserve - Notifies FLBs that a dependent file was unpreserved.
* @fh: The file handler for the unpreserved file.
*
* This function iterates through all FLBs associated with the given file
* handler, in reverse order of registration. It decrements the reference count
* for each FLB. If the count becomes 0, it triggers the FLB's .unpreserve()
* callback to clean up the global state.
*
* Context: Called when a preserved file is being cleaned up before reboot
* (e.g., from luo_file_unpreserve_files()).
*/
void luo_flb_file_unpreserve(struct liveupdate_file_handler *fh)
{
struct list_head *flb_list = &ACCESS_PRIVATE(fh, flb_list);
struct luo_flb_link *iter;
guard(rwsem_read)(&luo_register_rwlock);
list_for_each_entry_reverse(iter, flb_list, list)
luo_flb_file_unpreserve_one(iter->flb);
}
/**
* luo_flb_file_finish - Notifies FLBs that a dependent file has been finished.
* @fh: The file handler for the finished file.
*
* This function iterates through all FLBs associated with the given file
* handler, in reverse order of registration. It decrements the incoming
* reference count for each FLB. If the count becomes 0, it triggers the FLB's
* .finish() callback for final cleanup in the new kernel.
*
* Context: Called from luo_file_finish() for each file being finished.
*/
void luo_flb_file_finish(struct liveupdate_file_handler *fh)
{
struct list_head *flb_list = &ACCESS_PRIVATE(fh, flb_list);
struct luo_flb_link *iter;
guard(rwsem_read)(&luo_register_rwlock);
list_for_each_entry_reverse(iter, flb_list, list)
liveupdate_flb_put_incoming(iter->flb);
}
static void luo_flb_unregister_one(struct liveupdate_file_handler *fh,
struct liveupdate_flb *flb)
{
struct luo_flb_private *private = luo_flb_get_private(flb);
struct list_head *flb_list = &ACCESS_PRIVATE(fh, flb_list);
struct luo_flb_link *iter;
bool found = false;
/* Find and remove the link from the file handler's list */
list_for_each_entry(iter, flb_list, list) {
if (iter->flb == flb) {
list_del(&iter->list);
kfree(iter);
found = true;
break;
}
}
if (!found) {
pr_warn("Failed to unregister FLB '%s': not found in file handler '%s'\n",
flb->compatible, fh->compatible);
return;
}
private->users--;
/*
* If this is the last file-handler with which we are registred, remove
* from the global list.
*/
if (!private->users) {
list_del_init(&private->list);
luo_flb_global.count--;
}
}
/**
* luo_flb_unregister_all - Unregister all FLBs associated with a file handler.
* @fh: The file handler whose FLBs should be unregistered.
*
* This function iterates through the list of FLBs associated with the given
* file handler and unregisters them all one by one.
*/
void luo_flb_unregister_all(struct liveupdate_file_handler *fh)
{
struct list_head *flb_list = &ACCESS_PRIVATE(fh, flb_list);
struct luo_flb_link *iter, *tmp;
if (!liveupdate_enabled())
return;
lockdep_assert_held_write(&luo_register_rwlock);
list_for_each_entry_safe(iter, tmp, flb_list, list)
luo_flb_unregister_one(fh, iter->flb);
}
/**
* liveupdate_register_flb - Associate an FLB with a file handler and register it globally.
* @fh: The file handler that will now depend on the FLB.
* @flb: The File-Lifecycle-Bound object to associate.
*
* Establishes a dependency, informing the LUO core that whenever a file of
* type @fh is preserved, the state of @flb must also be managed.
*
* On the first registration of a given @flb object, it is added to a global
* registry. This function checks for duplicate registrations, both for a
* specific handler and globally, and ensures the total number of unique
* FLBs does not exceed the system limit.
*
* Context: Typically called from a subsystem's module init function after
* both the handler and the FLB have been defined and initialized.
* Return: 0 on success. Returns a negative errno on failure:
* -EINVAL if arguments are NULL or not initialized.
* -ENOMEM on memory allocation failure.
* -EEXIST if this FLB is already registered with this handler.
* -ENOSPC if the maximum number of global FLBs has been reached.
* -EOPNOTSUPP if live update is disabled or not configured.
*/
int liveupdate_register_flb(struct liveupdate_file_handler *fh,
struct liveupdate_flb *flb)
{
struct luo_flb_private *private = luo_flb_get_private(flb);
struct list_head *flb_list = &ACCESS_PRIVATE(fh, flb_list);
struct luo_flb_link *link __free(kfree) = NULL;
struct liveupdate_flb *gflb;
struct luo_flb_link *iter;
if (!liveupdate_enabled())
return -EOPNOTSUPP;
if (WARN_ON(!flb->ops->preserve || !flb->ops->unpreserve ||
!flb->ops->retrieve || !flb->ops->finish)) {
return -EINVAL;
}
/*
* File handler must already be registered, as it initializes the
* flb_list
*/
if (WARN_ON(list_empty(&ACCESS_PRIVATE(fh, list))))
return -EINVAL;
link = kzalloc_obj(*link);
if (!link)
return -ENOMEM;
guard(rwsem_write)(&luo_register_rwlock);
/* Check that this FLB is not already linked to this file handler */
list_for_each_entry(iter, flb_list, list) {
if (iter->flb == flb)
return -EEXIST;
}
/*
* If this FLB is not linked to global list it's the first time the FLB
* is registered
*/
if (!private->users) {
if (WARN_ON(!list_empty(&private->list)))
return -EINVAL;
if (luo_flb_global.count == LUO_FLB_MAX)
return -ENOSPC;
/* Check that compatible string is unique in global list */
list_private_for_each_entry(gflb, &luo_flb_global.list, private.list) {
if (!strcmp(gflb->compatible, flb->compatible))
return -EEXIST;
}
list_add_tail(&private->list, &luo_flb_global.list);
luo_flb_global.count++;
}
/* Finally, link the FLB to the file handler */
private->users++;
link->flb = flb;
list_add_tail(&no_free_ptr(link)->list, flb_list);
return 0;
}
/**
* liveupdate_unregister_flb - Remove an FLB dependency from a file handler.
* @fh: The file handler that is currently depending on the FLB.
* @flb: The File-Lifecycle-Bound object to remove.
*
* Removes the association between the specified file handler and the FLB
* previously established by liveupdate_register_flb().
*
* This function manages the global lifecycle of the FLB. It decrements the
* FLB's usage count. If this was the last file handler referencing this FLB,
* the FLB is removed from the global registry and the reference to its
* owner module (acquired during registration) is released.
*
* Context: It is typically called from a subsystem's module exit function.
*/
void liveupdate_unregister_flb(struct liveupdate_file_handler *fh,
struct liveupdate_flb *flb)
{
if (!liveupdate_enabled())
return;
guard(rwsem_write)(&luo_register_rwlock);
luo_flb_unregister_one(fh, flb);
}
/**
* liveupdate_flb_get_incoming - Retrieve the incoming FLB object.
* @flb: The FLB definition.
* @objp: Output parameter; will be populated with the live shared object.
*
* Returns a pointer to its shared live object for the incoming (post-reboot)
* path.
*
* If this is the first time the object is requested in the new kernel, this
* function will trigger the FLB's .retrieve() callback to reconstruct the
* object from its preserved state. Subsequent calls will return the same
* cached object.
*
* Return: 0 on success, or a negative errno on failure. -ENODATA means no
* incoming FLB data, -ENOENT means specific flb not found in the incoming
* data, -ENODEV if the FLB's module is unloading, and -EOPNOTSUPP when
* live update is disabled or not configured.
*/
int liveupdate_flb_get_incoming(struct liveupdate_flb *flb, void **objp)
{
struct luo_flb_private *private = luo_flb_get_private(flb);
if (!liveupdate_enabled())
return -EOPNOTSUPP;
guard(mutex)(&private->incoming.lock);
if (!private->incoming.obj) {
int err = luo_flb_retrieve_one(flb);
if (err)
return err;
}
refcount_inc(&private->incoming.count);
*objp = private->incoming.obj;
return 0;
}
/**
* liveupdate_flb_get_outgoing - Retrieve the outgoing FLB object.
* @flb: The FLB definition.
* @objp: Output parameter; will be populated with the live shared object.
*
* Returns a pointer to its shared live object for the outgoing (pre-reboot)
* path.
*
* This function assumes the object has already been created by the FLB's
* .preserve() callback, which is triggered when the first dependent file
* is preserved.
*
* Return: 0 on success, or a negative errno on failure.
*/
int liveupdate_flb_get_outgoing(struct liveupdate_flb *flb, void **objp)
{
struct luo_flb_private *private = luo_flb_get_private(flb);
if (!liveupdate_enabled())
return -EOPNOTSUPP;
guard(mutex)(&private->outgoing.lock);
*objp = private->outgoing.obj;
return 0;
}
int __init luo_flb_setup_outgoing(u64 *flbs_pa)
{
struct luo_flb_header_ser *header_ser;
header_ser = kho_alloc_preserve(LUO_FLB_PGCNT << PAGE_SHIFT);
if (IS_ERR(header_ser))
return PTR_ERR(header_ser);
*flbs_pa = virt_to_phys(header_ser);
header_ser->pgcnt = LUO_FLB_PGCNT;
luo_flb_global.outgoing.header_ser = header_ser;
luo_flb_global.outgoing.ser = (void *)(header_ser + 1);
luo_flb_global.outgoing.active = true;
return 0;
}
void __init luo_flb_setup_incoming(u64 flbs_pa)
{
struct luo_flb_header_ser *header_ser;
if (!flbs_pa)
return;
header_ser = phys_to_virt(flbs_pa);
luo_flb_global.incoming.header_ser = header_ser;
luo_flb_global.incoming.ser = (void *)(header_ser + 1);
luo_flb_global.incoming.active = true;
}
/**
* luo_flb_serialize - Serializes all active FLB objects for KHO.
*
* This function is called from the reboot path. It iterates through all
* registered File-Lifecycle-Bound (FLB) objects. For each FLB that has been
* preserved (i.e., its reference count is greater than zero), it writes its
* metadata into the memory region designated for Kexec Handover.
*
* The serialized data includes the FLB's compatibility string, its opaque
* data handle, and the final reference count. This allows the new kernel to
* find the appropriate handler and reconstruct the FLB's state.
*
* Context: Called from liveupdate_reboot() just before kho_finalize().
*/
void luo_flb_serialize(void)
{
struct luo_flb_header *fh = &luo_flb_global.outgoing;
struct liveupdate_flb *gflb;
int i = 0;
guard(rwsem_read)(&luo_register_rwlock);
list_private_for_each_entry(gflb, &luo_flb_global.list, private.list) {
struct luo_flb_private *private = luo_flb_get_private(gflb);
long count = refcount_read(&private->outgoing.count);
if (count > 0) {
strscpy(fh->ser[i].name, gflb->compatible,
sizeof(fh->ser[i].name));
fh->ser[i].data = private->outgoing.data;
fh->ser[i].count = count;
i++;
}
}
fh->header_ser->count = i;
}
@@ -0,0 +1,115 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2025, Google LLC.
* Pasha Tatashin <pasha.tatashin@soleen.com>
*/
#ifndef _LINUX_LUO_INTERNAL_H
#define _LINUX_LUO_INTERNAL_H
#include <linux/liveupdate.h>
#include <linux/uaccess.h>
#include <linux/kho_block.h>
struct luo_ucmd {
void __user *ubuffer;
u32 user_size;
void *cmd;
};
static inline int luo_ucmd_respond(struct luo_ucmd *ucmd,
size_t kernel_cmd_size)
{
/*
* Copy the minimum of what the user provided and what we actually
* have.
*/
if (copy_to_user(ucmd->ubuffer, ucmd->cmd,
min_t(size_t, ucmd->user_size, kernel_cmd_size))) {
return -EFAULT;
}
return 0;
}
/*
* Handles a deserialization failure: devices and memory is in unpredictable
* state.
*
* Continuing the boot process after a failure is dangerous because it could
* lead to leaks of private data.
*/
#define luo_restore_fail(__fmt, ...) panic(__fmt, ##__VA_ARGS__)
/**
* struct luo_file_set - A set of files that belong to the same sessions.
* @files_list: An ordered list of files associated with this session, it is
* ordered by preservation time.
* @block_set: The set of serialization blocks.
* @count: A counter tracking the number of files currently stored in the
* @files_list for this session.
*/
struct luo_file_set {
struct list_head files_list;
struct kho_block_set block_set;
u64 count;
};
/**
* struct luo_session - Represents an active or incoming Live Update session.
* @name: A unique name for this session, used for identification and
* retrieval.
* @list: A list_head member used to link this session into a global list
* of either outgoing (to be preserved) or incoming (restored from
* previous kernel) sessions.
* @retrieved: A boolean flag indicating whether this session has been
* retrieved by a consumer in the new kernel.
* @file_set: A set of files that belong to this session.
* @mutex: protects fields in the luo_session.
*/
struct luo_session {
char name[LIVEUPDATE_SESSION_NAME_LENGTH];
struct list_head list;
bool retrieved;
struct luo_file_set file_set;
struct mutex mutex;
};
extern struct rw_semaphore luo_register_rwlock;
int luo_session_create(const char *name, struct file **filep);
int luo_session_retrieve(const char *name, struct file **filep);
void __init luo_session_setup_outgoing(u64 *sessions_pa);
int __init luo_session_setup_incoming(u64 sessions_pa);
int luo_session_serialize(void);
int luo_session_deserialize(void);
int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd);
void luo_file_unpreserve_files(struct luo_file_set *file_set);
int luo_file_freeze(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser);
void luo_file_unfreeze(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser);
int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
struct file **filep);
int luo_file_finish(struct luo_file_set *file_set);
int luo_file_deserialize(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser);
void luo_file_set_init(struct luo_file_set *file_set);
void luo_file_set_destroy(struct luo_file_set *file_set);
int luo_flb_file_preserve(struct liveupdate_file_handler *fh);
void luo_flb_file_unpreserve(struct liveupdate_file_handler *fh);
void luo_flb_file_finish(struct liveupdate_file_handler *fh);
void luo_flb_unregister_all(struct liveupdate_file_handler *fh);
int __init luo_flb_setup_outgoing(u64 *flbs_pa);
void __init luo_flb_setup_incoming(u64 flbs_pa);
void luo_flb_serialize(void);
#ifdef CONFIG_LIVEUPDATE_TEST
void liveupdate_test_register(struct liveupdate_file_handler *fh);
#else
static inline void liveupdate_test_register(struct liveupdate_file_handler *fh) { }
#endif
#endif /* _LINUX_LUO_INTERNAL_H */
+682
View File
@@ -0,0 +1,682 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2025, Google LLC.
* Pasha Tatashin <pasha.tatashin@soleen.com>
*/
/**
* DOC: LUO Sessions
*
* LUO Sessions provide the core mechanism for grouping and managing `struct
* file *` instances that need to be preserved across a kexec-based live
* update. Each session acts as a named container for a set of file objects,
* allowing a userspace agent to manage the lifecycle of resources critical to a
* workload.
*
* Core Concepts:
*
* - Named Containers: Sessions are identified by a unique, user-provided name,
* which is used for both creation in the current kernel and retrieval in the
* next kernel.
*
* - Userspace Interface: Session management is driven from userspace via
* ioctls on /dev/liveupdate.
*
* - Serialization: Session metadata is preserved using the KHO framework. When
* a live update is triggered via kexec, session metadata is serialized into
* a chain of linked-blocks and placed in a preserved memory region. The
* physical address of the first block header is stored in the centralized
* `struct luo_ser` structure.
*
* Session Lifecycle:
*
* 1. Creation: A userspace agent calls `luo_session_create()` to create a
* new, empty session and receives a file descriptor for it.
*
* 2. Serialization: When the `reboot(LINUX_REBOOT_CMD_KEXEC)` syscall is
* made, `luo_session_serialize()` is called. It iterates through all
* active sessions and writes their metadata into a memory area preserved
* by KHO.
*
* 3. Deserialization (in new kernel): After kexec, `luo_session_deserialize()`
* runs, reading the serialized data and creating a list of `struct
* luo_session` objects representing the preserved sessions.
*
* 4. Retrieval: A userspace agent in the new kernel can then call
* `luo_session_retrieve()` with a session name to get a new file
* descriptor and access the preserved state.
*
* Locking:
*
* The LUO session subsystem uses a three-tier locking hierarchy to ensure thread
* safety and prevent deadlocks during concurrent session mutations and kexec
* serialization:
*
* 1. `luo_session_serialize_rwsem` (global rwsem):
* Protects session mutations (creation, retrieval, release, and ioctls)
* against the serialization process during reboot.
*
* - Readers: Taken by any path modifying or accessing session state (e.g.,
* `luo_session_create()`, `luo_session_retrieve()`, `luo_session_release()`,
* and `luo_session_ioctl()`).
* - Writer: Taken by the serialization process (`luo_session_serialize()`)
* during reboot. On success, the write lock is held indefinitely to freeze
* the subsystem. On failure, it is released to allow recovery.
*
* 2. `luo_session_header->rwsem` (per-list rwsem):
* Synchronizes list-level operations for the incoming and outgoing session headers.
*
* - Writer: Taken during list mutation operations (inserting or removing a
* session from the list).
* - Reader: Taken when traversing the list (e.g., retrieving a session by name).
*
* 3. `luo_session->mutex` (per-session mutex):
* Protects the internal state and file sets of an individual session. It is
* acquired during per-session operations such as preserving, retrieving,
* or freezing files.
*
* Lock Hierarchy:
* `luo_session_serialize_rwsem` -> `luo_session_header->rwsem` -> `luo_session->mutex`
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/anon_inodes.h>
#include <linux/cleanup.h>
#include <linux/err.h>
#include <linux/errno.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/kexec_handover.h>
#include <linux/kho_block.h>
#include <linux/kho/abi/luo.h>
#include <linux/list.h>
#include <linux/liveupdate.h>
#include <linux/mutex.h>
#include <linux/rwsem.h>
#include <linux/slab.h>
#include <uapi/linux/liveupdate.h>
#include "luo_internal.h"
static DECLARE_RWSEM(luo_session_serialize_rwsem);
/**
* struct luo_session_header - Header struct for managing LUO sessions.
* @count: The number of sessions currently tracked in the @list.
* @list: The head of the linked list of `struct luo_session` instances.
* @rwsem: A read-write semaphore providing synchronized access to the
* session list and other fields in this structure.
* @block_set: The set of serialization blocks.
* @sessions_pa: Points to the location of sessions_pa within struct luo_ser.
* @active: Set to true when first initialized. If previous kernel did not
* send session data, active stays false for incoming.
*/
struct luo_session_header {
long count;
struct list_head list;
struct rw_semaphore rwsem;
struct kho_block_set block_set;
u64 *sessions_pa;
bool active;
};
/**
* struct luo_session_global - Global container for managing LUO sessions.
* @incoming: The sessions passed from the previous kernel.
* @outgoing: The sessions that are going to be passed to the next kernel.
*/
struct luo_session_global {
struct luo_session_header incoming;
struct luo_session_header outgoing;
};
static struct luo_session_global luo_session_global = {
.incoming = {
.list = LIST_HEAD_INIT(luo_session_global.incoming.list),
.rwsem = __RWSEM_INITIALIZER(luo_session_global.incoming.rwsem),
.block_set = KHO_BLOCK_SET_INIT(luo_session_global.incoming.block_set,
sizeof(struct luo_session_ser)),
},
.outgoing = {
.list = LIST_HEAD_INIT(luo_session_global.outgoing.list),
.rwsem = __RWSEM_INITIALIZER(luo_session_global.outgoing.rwsem),
.block_set = KHO_BLOCK_SET_INIT(luo_session_global.outgoing.block_set,
sizeof(struct luo_session_ser)),
},
};
static struct luo_session *luo_session_alloc(const char *name)
{
struct luo_session *session = kzalloc_obj(*session);
if (!session)
return ERR_PTR(-ENOMEM);
strscpy(session->name, name, sizeof(session->name));
INIT_LIST_HEAD(&session->file_set.files_list);
luo_file_set_init(&session->file_set);
INIT_LIST_HEAD(&session->list);
mutex_init(&session->mutex);
return session;
}
static void luo_session_free(struct luo_session *session)
{
luo_file_set_destroy(&session->file_set);
mutex_destroy(&session->mutex);
kfree(session);
}
static int luo_session_insert(struct luo_session_header *sh,
struct luo_session *session)
{
struct luo_session *it;
int err;
guard(rwsem_write)(&sh->rwsem);
/*
* For outgoing we should make sure there is room in serialization array
* for new session.
*/
if (sh == &luo_session_global.outgoing) {
err = kho_block_set_grow(&sh->block_set, sh->count + 1);
if (err)
return err;
}
/*
* For small number of sessions this loop won't hurt performance
* but if we ever start using a lot of sessions, this might
* become a bottle neck during deserialization time, as it would
* cause O(n*n) complexity.
*/
list_for_each_entry(it, &sh->list, list) {
if (!strncmp(it->name, session->name, sizeof(it->name)))
return -EEXIST;
}
list_add_tail(&session->list, &sh->list);
sh->count++;
return 0;
}
static void luo_session_remove(struct luo_session_header *sh,
struct luo_session *session)
{
guard(rwsem_write)(&sh->rwsem);
list_del(&session->list);
sh->count--;
if (sh == &luo_session_global.outgoing)
kho_block_set_shrink(&sh->block_set, sh->count);
}
static int luo_session_finish_one(struct luo_session *session)
{
guard(mutex)(&session->mutex);
return luo_file_finish(&session->file_set);
}
static void luo_session_unfreeze_one(struct luo_session *session,
struct luo_session_ser *ser)
{
guard(mutex)(&session->mutex);
luo_file_unfreeze(&session->file_set, &ser->file_set_ser);
}
static int luo_session_freeze_one(struct luo_session *session,
struct luo_session_ser *ser)
{
guard(mutex)(&session->mutex);
return luo_file_freeze(&session->file_set, &ser->file_set_ser);
}
static int luo_session_release(struct inode *inodep, struct file *filep)
{
struct luo_session *session = filep->private_data;
struct luo_session_header *sh;
guard(rwsem_read)(&luo_session_serialize_rwsem);
/* If retrieved is set, it means this session is from incoming list */
if (session->retrieved) {
int err = luo_session_finish_one(session);
if (err) {
pr_warn("Unable to finish session [%s] on release\n",
session->name);
return err;
}
sh = &luo_session_global.incoming;
} else {
scoped_guard(mutex, &session->mutex)
luo_file_unpreserve_files(&session->file_set);
sh = &luo_session_global.outgoing;
}
luo_session_remove(sh, session);
luo_session_free(session);
return 0;
}
static int luo_session_preserve_fd(struct luo_session *session,
struct luo_ucmd *ucmd)
{
struct liveupdate_session_preserve_fd *argp = ucmd->cmd;
int err;
guard(mutex)(&session->mutex);
err = luo_preserve_file(&session->file_set, argp->token, argp->fd);
if (err)
return err;
err = luo_ucmd_respond(ucmd, sizeof(*argp));
if (err)
pr_warn("The file was successfully preserved, but response to user failed\n");
return err;
}
static int luo_session_retrieve_fd(struct luo_session *session,
struct luo_ucmd *ucmd)
{
struct liveupdate_session_retrieve_fd *argp = ucmd->cmd;
struct file *file;
int err;
argp->fd = get_unused_fd_flags(O_CLOEXEC);
if (argp->fd < 0)
return argp->fd;
mutex_lock(&session->mutex);
err = luo_retrieve_file(&session->file_set, argp->token, &file);
mutex_unlock(&session->mutex);
if (err < 0)
goto err_put_fd;
err = luo_ucmd_respond(ucmd, sizeof(*argp));
if (err)
goto err_put_file;
fd_install(argp->fd, file);
return 0;
err_put_file:
fput(file);
err_put_fd:
put_unused_fd(argp->fd);
return err;
}
static int luo_session_finish(struct luo_session *session,
struct luo_ucmd *ucmd)
{
struct liveupdate_session_finish *argp = ucmd->cmd;
int err = luo_session_finish_one(session);
if (err)
return err;
return luo_ucmd_respond(ucmd, sizeof(*argp));
}
static int luo_session_get_name(struct luo_session *session,
struct luo_ucmd *ucmd)
{
struct liveupdate_session_get_name *argp = ucmd->cmd;
if (argp->reserved != 0)
return -EINVAL;
strscpy((char *)argp->name, session->name, sizeof(argp->name));
return luo_ucmd_respond(ucmd, sizeof(*argp));
}
union ucmd_buffer {
struct liveupdate_session_finish finish;
struct liveupdate_session_preserve_fd preserve;
struct liveupdate_session_retrieve_fd retrieve;
struct liveupdate_session_get_name get_name;
};
/* Type of sessions the ioctl applies to. */
enum luo_ioctl_type {
LUO_IOCTL_INCOMING,
LUO_IOCTL_OUTGOING,
LUO_IOCTL_ALL,
};
struct luo_ioctl_op {
unsigned int size;
unsigned int min_size;
unsigned int ioctl_num;
enum luo_ioctl_type type;
int (*execute)(struct luo_session *session, struct luo_ucmd *ucmd);
};
#define IOCTL_OP(_ioctl, _fn, _struct, _last, _type) \
[_IOC_NR(_ioctl) - LIVEUPDATE_CMD_SESSION_BASE] = { \
.size = sizeof(_struct) + \
BUILD_BUG_ON_ZERO(sizeof(union ucmd_buffer) < \
sizeof(_struct)), \
.min_size = offsetofend(_struct, _last), \
.ioctl_num = _ioctl, \
.type = _type, \
.execute = _fn, \
}
static const struct luo_ioctl_op luo_session_ioctl_ops[] = {
IOCTL_OP(LIVEUPDATE_SESSION_FINISH, luo_session_finish,
struct liveupdate_session_finish, reserved, LUO_IOCTL_INCOMING),
IOCTL_OP(LIVEUPDATE_SESSION_PRESERVE_FD, luo_session_preserve_fd,
struct liveupdate_session_preserve_fd, token, LUO_IOCTL_OUTGOING),
IOCTL_OP(LIVEUPDATE_SESSION_RETRIEVE_FD, luo_session_retrieve_fd,
struct liveupdate_session_retrieve_fd, token, LUO_IOCTL_INCOMING),
IOCTL_OP(LIVEUPDATE_SESSION_GET_NAME, luo_session_get_name,
struct liveupdate_session_get_name, name, LUO_IOCTL_ALL),
};
static bool luo_ioctl_type_valid(struct luo_session *session,
const struct luo_ioctl_op *op)
{
switch (op->type) {
case LUO_IOCTL_INCOMING:
/* Retrieved is only set on incoming sessions */
return session->retrieved;
case LUO_IOCTL_OUTGOING:
return !session->retrieved;
case LUO_IOCTL_ALL:
return true;
}
/* Catch-all. */
return false;
}
static long luo_session_ioctl(struct file *filep, unsigned int cmd,
unsigned long arg)
{
struct luo_session *session = filep->private_data;
const struct luo_ioctl_op *op;
struct luo_ucmd ucmd = {};
union ucmd_buffer buf;
unsigned int nr;
int ret;
nr = _IOC_NR(cmd);
if (nr < LIVEUPDATE_CMD_SESSION_BASE || (nr - LIVEUPDATE_CMD_SESSION_BASE) >=
ARRAY_SIZE(luo_session_ioctl_ops)) {
return -EINVAL;
}
ucmd.ubuffer = (void __user *)arg;
ret = get_user(ucmd.user_size, (u32 __user *)ucmd.ubuffer);
if (ret)
return ret;
op = &luo_session_ioctl_ops[nr - LIVEUPDATE_CMD_SESSION_BASE];
if (op->ioctl_num != cmd)
return -ENOIOCTLCMD;
if (!luo_ioctl_type_valid(session, op))
return -EINVAL;
if (ucmd.user_size < op->min_size)
return -EINVAL;
ucmd.cmd = &buf;
ret = copy_struct_from_user(ucmd.cmd, op->size, ucmd.ubuffer,
ucmd.user_size);
if (ret)
return ret;
guard(rwsem_read)(&luo_session_serialize_rwsem);
return op->execute(session, &ucmd);
}
static const struct file_operations luo_session_fops = {
.owner = THIS_MODULE,
.release = luo_session_release,
.unlocked_ioctl = luo_session_ioctl,
};
/* Create a "struct file" for session */
static int luo_session_getfile(struct luo_session *session, struct file **filep)
{
char name_buf[128];
struct file *file;
lockdep_assert_held(&session->mutex);
snprintf(name_buf, sizeof(name_buf), "[luo_session] %s", session->name);
file = anon_inode_getfile(name_buf, &luo_session_fops, session, O_RDWR);
if (IS_ERR(file))
return PTR_ERR(file);
*filep = file;
return 0;
}
int luo_session_create(const char *name, struct file **filep)
{
size_t len = strnlen(name, LIVEUPDATE_SESSION_NAME_LENGTH);
struct luo_session *session;
int err;
if (len == 0 || len > LIVEUPDATE_SESSION_NAME_LENGTH - 1)
return -EINVAL;
session = luo_session_alloc(name);
if (IS_ERR(session))
return PTR_ERR(session);
down_read(&luo_session_serialize_rwsem);
err = luo_session_insert(&luo_session_global.outgoing, session);
if (err)
goto err_free;
mutex_lock(&session->mutex);
err = luo_session_getfile(session, filep);
mutex_unlock(&session->mutex);
if (err)
goto err_remove;
up_read(&luo_session_serialize_rwsem);
return 0;
err_remove:
luo_session_remove(&luo_session_global.outgoing, session);
err_free:
luo_session_free(session);
up_read(&luo_session_serialize_rwsem);
return err;
}
int luo_session_retrieve(const char *name, struct file **filep)
{
struct luo_session_header *sh = &luo_session_global.incoming;
struct luo_session *session = NULL;
struct luo_session *it;
int err;
guard(rwsem_read)(&luo_session_serialize_rwsem);
guard(rwsem_read)(&sh->rwsem);
list_for_each_entry(it, &sh->list, list) {
if (!strncmp(it->name, name, sizeof(it->name))) {
session = it;
break;
}
}
if (!session)
return -ENOENT;
guard(mutex)(&session->mutex);
if (session->retrieved)
return -EINVAL;
err = luo_session_getfile(session, filep);
if (!err)
session->retrieved = true;
return err;
}
void __init luo_session_setup_outgoing(u64 *sessions_pa)
{
luo_session_global.outgoing.sessions_pa = sessions_pa;
luo_session_global.outgoing.active = true;
}
int __init luo_session_setup_incoming(u64 sessions_pa)
{
struct luo_session_header *sh = &luo_session_global.incoming;
int err;
if (!sessions_pa)
return 0;
err = kho_block_set_restore(&sh->block_set, sessions_pa);
if (err)
return err;
sh->active = true;
return 0;
}
static int luo_session_deserialize_one(struct luo_session_header *sh,
struct luo_session_ser *ser)
{
struct luo_session *session;
int err;
session = luo_session_alloc(ser->name);
if (IS_ERR(session)) {
pr_warn("Failed to allocate session [%.*s] during deserialization %pe\n",
(int)sizeof(ser->name), ser->name, session);
return PTR_ERR(session);
}
err = luo_session_insert(sh, session);
if (err) {
pr_warn("Failed to insert session [%s] %pe\n",
session->name, ERR_PTR(err));
luo_session_free(session);
return err;
}
scoped_guard(mutex, &session->mutex) {
err = luo_file_deserialize(&session->file_set,
&ser->file_set_ser);
}
if (err) {
pr_warn("Failed to deserialize files for session [%s] %pe\n",
session->name, ERR_PTR(err));
return err;
}
return 0;
}
int luo_session_deserialize(void)
{
struct luo_session_header *sh = &luo_session_global.incoming;
static bool is_deserialized;
struct luo_session_ser *ser;
struct kho_block_set_it it;
static int saved_err;
int err;
/* If has been deserialized, always return the same error code */
if (is_deserialized)
return saved_err;
is_deserialized = true;
if (!sh->active)
return 0;
/*
* Note on error handling:
*
* If deserialization fails (e.g., allocation failure or corrupt data),
* we intentionally skip cleanup of sessions that were already restored.
*
* A partial failure leaves the preserved state inconsistent.
* Implementing a safe "undo" to unwind complex dependencies (sessions,
* files, hardware state) is error-prone and provides little value, as
* the system is effectively in a broken state.
*
* We treat these resources as leaked. The expected recovery path is for
* userspace to detect the failure and trigger a reboot, which will
* reliably reset devices and reclaim memory.
*/
kho_block_set_it_init(&it, &sh->block_set);
while ((ser = kho_block_set_it_read_entry(&it))) {
err = luo_session_deserialize_one(sh, ser);
if (err)
goto save_err;
}
kho_block_set_destroy(&sh->block_set);
return 0;
save_err:
kho_block_set_destroy(&sh->block_set);
saved_err = err;
return err;
}
int luo_session_serialize(void)
{
struct luo_session_header *sh = &luo_session_global.outgoing;
struct luo_session *session;
struct kho_block_set_it it;
int err;
down_write(&luo_session_serialize_rwsem);
down_write(&sh->rwsem);
*sh->sessions_pa = 0;
kho_block_set_it_init(&it, &sh->block_set);
list_for_each_entry(session, &sh->list, list) {
struct luo_session_ser *ser = kho_block_set_it_reserve_entry(&it);
/* This should not fail normally as blocks were pre-allocated */
if (WARN_ON_ONCE(!ser)) {
err = -ENOSPC;
goto err_undo;
}
err = luo_session_freeze_one(session, ser);
if (err) {
kho_block_set_it_prev(&it);
goto err_undo;
}
strscpy(ser->name, session->name, sizeof(ser->name));
}
if (sh->count > 0)
*sh->sessions_pa = kho_block_set_head_pa(&sh->block_set);
up_write(&sh->rwsem);
return 0;
err_undo:
list_for_each_entry_continue_reverse(session, &sh->list, list) {
struct luo_session_ser *ser = kho_block_set_it_prev(&it);
luo_session_unfreeze_one(session, ser);
memset(ser->name, 0, sizeof(ser->name));
}
up_write(&sh->rwsem);
up_write(&luo_session_serialize_rwsem);
return err;
}