#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 {
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 {
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);
if (sh == &luo_session_global.outgoing) {
err = kho_block_set_grow(&sh->block_set, sh->count + 1);
if (err)
return err;
}
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 (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;
};
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:
return session->retrieved;
case LUO_IOCTL_OUTGOING:
return !session->retrieved;
case LUO_IOCTL_ALL:
return true;
}
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,
};
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 (is_deserialized)
return saved_err;
is_deserialized = true;
if (!sh->active)
return 0;
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);
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;
}