mirror of
https://github.com/torvalds/linux.git
synced 2026-09-24 06:24:02 +02:00
Add the core implementation for firmware and host client management within the ISSEI (Intel Silicon Security Engine Interface) subsystem support for a character device to expose the ISSEI HECI interface to user space. The firmware client (fw_client) and host client (host_client) modules are responsible for managing communication between the host software and the firmware. The character device provides a communication channel for user-space applications to interact with the firmware on the platform. The client modules enable the ISSEI driver to manage multiple host clients communicating with corresponding firmware clients, facilitating data transfers and control operations over the HECI interface. The character device allows user-space applications to establish connections to firmware clients using UUIDs, exchange messages, and control the communication flow using standard file operation calls. Reviewed-by: Karol Wachowski <karol.wachowski@linux.intel.com> Co-developed-by: Vitaly Lubart <lubvital@gmail.com> Signed-off-by: Vitaly Lubart <lubvital@gmail.com> Signed-off-by: Alexander Usyskin <alexander.usyskin@intel.com> Link: https://patch.msgid.link/20260513-issei-for-upstream-v1-2-f590038678f9@intel.com Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
447 lines
10 KiB
C
447 lines
10 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/* Copyright (C) 2023-2026 Intel Corporation */
|
|
#include <linux/atomic.h>
|
|
#include <linux/cdev.h>
|
|
#include <linux/cleanup.h>
|
|
#include <linux/device.h>
|
|
#include <linux/err.h>
|
|
#include <linux/export.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/issei.h>
|
|
#include <linux/kobject.h>
|
|
#include <linux/list.h>
|
|
#include <linux/module.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/poll.h>
|
|
#include <linux/sched/signal.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/types.h>
|
|
#include <linux/wait.h>
|
|
#include <linux/xarray.h>
|
|
|
|
#include "issei_dev.h"
|
|
#include "host_client.h"
|
|
#include "cdev.h"
|
|
|
|
struct class *issei_class;
|
|
static dev_t issei_devt;
|
|
|
|
#define ISSEI_MAX_DEVS MINORMASK
|
|
|
|
static DEFINE_XARRAY_ALLOC(issei_minor_xa);
|
|
|
|
static int issei_open(struct inode *inode, struct file *fp)
|
|
{
|
|
struct issei_host_client *cl;
|
|
struct issei_device *idev;
|
|
|
|
xa_lock(&issei_minor_xa);
|
|
idev = xa_load(&issei_minor_xa, iminor(inode));
|
|
if (idev)
|
|
get_device(&idev->dev);
|
|
xa_unlock(&issei_minor_xa);
|
|
if (!idev)
|
|
return -ENODEV;
|
|
|
|
cl = issei_cl_create(idev, fp);
|
|
if (IS_ERR(cl)) {
|
|
put_device(&idev->dev);
|
|
return PTR_ERR(cl);
|
|
}
|
|
fp->private_data = cl;
|
|
|
|
return nonseekable_open(inode, fp);
|
|
}
|
|
|
|
static int issei_release(struct inode *inode, struct file *fp)
|
|
{
|
|
struct issei_host_client *cl = fp->private_data;
|
|
struct issei_device *idev = cl->idev;
|
|
|
|
issei_cl_remove(cl);
|
|
put_device(&idev->dev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static long issei_ioctl(struct file *file, unsigned int cmd, unsigned long data)
|
|
{
|
|
struct issei_host_client *cl = file->private_data;
|
|
struct issei_connect_client_data conn;
|
|
struct issei_device *idev = cl->idev;
|
|
int ret;
|
|
|
|
switch (cmd) {
|
|
case IOCTL_ISSEI_CONNECT_CLIENT:
|
|
dev_dbg(&idev->dev, "IOCTL_ISSEI_CONNECT_CLIENT\n");
|
|
|
|
if (idev->rst_state != ISSEI_RST_STATE_DONE) {
|
|
dev_dbg(&idev->dev, "Device is in transition\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (copy_from_user(&conn, (char __user *)data, sizeof(conn))) {
|
|
dev_dbg(&idev->dev, "failed to copy data from userland\n");
|
|
return -EFAULT;
|
|
}
|
|
|
|
ret = issei_cl_connect(cl, (uuid_t *)&conn.in_client_uuid,
|
|
&conn.out_client_properties.max_msg_length,
|
|
&conn.out_client_properties.protocol_version,
|
|
&conn.out_client_properties.flags);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (copy_to_user((char __user *)data, &conn, sizeof(conn))) {
|
|
dev_dbg(&idev->dev, "failed to copy data to userland\n");
|
|
issei_cl_disconnect(cl);
|
|
return -EFAULT;
|
|
}
|
|
return 0;
|
|
|
|
case IOCTL_ISSEI_DISCONNECT_CLIENT:
|
|
dev_dbg(&idev->dev, "IOCTL_ISSEI_DISCONNECT_CLIENT\n");
|
|
|
|
if (idev->rst_state != ISSEI_RST_STATE_DONE) {
|
|
dev_dbg(&idev->dev, "Device is in transition\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
return issei_cl_disconnect(cl);
|
|
|
|
default:
|
|
return -ENOIOCTLCMD;
|
|
}
|
|
}
|
|
|
|
static ssize_t issei_write(struct file *file, const char __user *ubuf,
|
|
size_t length, loff_t *offset)
|
|
{
|
|
struct issei_host_client *cl = file->private_data;
|
|
struct issei_device *idev = cl->idev;
|
|
ssize_t ret;
|
|
|
|
if (!length)
|
|
return 0;
|
|
|
|
if (idev->rst_state != ISSEI_RST_STATE_DONE) {
|
|
dev_dbg(&idev->dev, "Device is in transition\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
/* sanity check */
|
|
if (length > idev->dma.length.h2f) {
|
|
dev_dbg(&idev->dev, "Write is too big %zu > %zu\n",
|
|
length, idev->dma.length.h2f);
|
|
return -EFBIG;
|
|
}
|
|
|
|
u8 *buf __free(kfree) = memdup_user(ubuf, length);
|
|
if (IS_ERR(buf)) {
|
|
dev_dbg(&idev->dev, "failed to copy data from userland\n");
|
|
return PTR_ERR(buf);
|
|
}
|
|
|
|
do {
|
|
ret = issei_cl_write(cl, buf, length);
|
|
if (ret < 0 && ret != -EAGAIN)
|
|
return ret;
|
|
/* buf is consumed by issei_cl_write on success */
|
|
if (ret >= 0)
|
|
retain_and_null_ptr(buf);
|
|
if (wait_event_interruptible(cl->write_wait, issei_cl_check_write(cl) != 1)) {
|
|
issei_cl_clean_all_wbuf(cl);
|
|
if (signal_pending(current))
|
|
return -EINTR;
|
|
return -ERESTARTSYS;
|
|
}
|
|
} while (ret == -EAGAIN);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static ssize_t issei_read(struct file *file, char __user *ubuf,
|
|
size_t length, loff_t *offset)
|
|
{
|
|
struct issei_host_client *cl = file->private_data;
|
|
struct issei_device *idev = cl->idev;
|
|
u8 *data = NULL;
|
|
ssize_t ret;
|
|
|
|
if (!length)
|
|
return 0;
|
|
|
|
if (idev->rst_state != ISSEI_RST_STATE_DONE) {
|
|
dev_dbg(&idev->dev, "Device is in transition\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
/* sanity check */
|
|
if (length > idev->dma.length.f2h) {
|
|
dev_dbg(&idev->dev, "Read is too big %zu > %zu\n",
|
|
length, idev->dma.length.f2h);
|
|
return -EFBIG;
|
|
}
|
|
|
|
ret = issei_cl_read(cl, &data, length);
|
|
if (ret < 0) {
|
|
if (ret != -ENOENT)
|
|
return ret;
|
|
|
|
if (wait_event_interruptible(cl->read_wait, issei_cl_check_read(cl) != 0)) {
|
|
if (signal_pending(current))
|
|
return -EINTR;
|
|
return -ERESTARTSYS;
|
|
}
|
|
|
|
ret = issei_cl_read(cl, &data, length);
|
|
if (ret < 0)
|
|
return ret;
|
|
}
|
|
|
|
if (copy_to_user(ubuf, data, ret)) {
|
|
dev_dbg(&idev->dev, "failed to copy data to userland\n");
|
|
ret = -EFAULT;
|
|
} else {
|
|
*offset = 0;
|
|
}
|
|
|
|
kfree(data);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static __poll_t issei_poll(struct file *file, poll_table *wait)
|
|
{
|
|
__poll_t req_events = poll_requested_events(wait);
|
|
struct issei_host_client *cl = file->private_data;
|
|
struct issei_device *idev = cl->idev;
|
|
__poll_t mask = 0;
|
|
int ret;
|
|
|
|
if (idev->rst_state != ISSEI_RST_STATE_DONE) {
|
|
dev_dbg(&idev->dev, "Device is in transition\n");
|
|
return EPOLLERR;
|
|
}
|
|
|
|
if (req_events & (EPOLLIN | EPOLLRDNORM)) {
|
|
poll_wait(file, &cl->read_wait, wait);
|
|
ret = issei_cl_check_read(cl);
|
|
if (ret == 1)
|
|
mask |= EPOLLIN | EPOLLRDNORM;
|
|
else if (ret < 0)
|
|
mask |= EPOLLERR;
|
|
}
|
|
|
|
if (req_events & (EPOLLOUT | EPOLLWRNORM)) {
|
|
poll_wait(file, &cl->write_wait, wait);
|
|
ret = issei_cl_check_write(cl);
|
|
if (ret == 0)
|
|
mask |= EPOLLOUT | EPOLLWRNORM;
|
|
else if (ret < 0)
|
|
mask |= EPOLLERR;
|
|
}
|
|
|
|
return mask;
|
|
}
|
|
|
|
static ssize_t fw_ver_show(struct device *device,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct issei_device *idev = dev_get_drvdata(device);
|
|
|
|
return sysfs_emit(buf, "%u.%u.%u.%u\n", idev->fw_version[0], idev->fw_version[1],
|
|
idev->fw_version[2], idev->fw_version[3]);
|
|
}
|
|
static DEVICE_ATTR_RO(fw_ver);
|
|
|
|
static struct attribute *issei_attrs[] = {
|
|
&dev_attr_fw_ver.attr,
|
|
NULL
|
|
};
|
|
ATTRIBUTE_GROUPS(issei);
|
|
|
|
static const struct file_operations issei_fops = {
|
|
.owner = THIS_MODULE,
|
|
.open = issei_open,
|
|
.unlocked_ioctl = issei_ioctl,
|
|
.compat_ioctl = compat_ptr_ioctl,
|
|
.write = issei_write,
|
|
.read = issei_read,
|
|
.release = issei_release,
|
|
.poll = issei_poll,
|
|
};
|
|
|
|
static void issei_device_release(struct device *dev)
|
|
{
|
|
kfree(dev_get_drvdata(dev));
|
|
}
|
|
|
|
static void issei_device_init(struct issei_device *idev, struct device *parent,
|
|
const struct issei_dma_length *dma_length,
|
|
const struct issei_hw_ops *ops)
|
|
{
|
|
idev->parent = parent;
|
|
idev->power_down = false;
|
|
init_waitqueue_head(&idev->wait_has_data);
|
|
idev->has_data = false;
|
|
init_waitqueue_head(&idev->wait_rst_state);
|
|
idev->rst_state = ISSEI_RST_STATE_INIT;
|
|
|
|
mutex_init(&idev->client_lock);
|
|
INIT_LIST_HEAD(&idev->host_client_list);
|
|
idev->host_client_last_id = 0;
|
|
idev->host_client_count = 0;
|
|
INIT_LIST_HEAD(&idev->fw_client_list);
|
|
INIT_LIST_HEAD(&idev->write_queue);
|
|
idev->last_write_ts = 0;
|
|
|
|
idev->dma.length = *dma_length;
|
|
|
|
idev->ops = ops;
|
|
}
|
|
|
|
/**
|
|
* issei_register: register issei character device
|
|
* @hw_size: size of the hardware structure to allocate
|
|
* @parent: parent device
|
|
* @dma_length: structure with DMA sizes
|
|
* @ops: hardware-related operations
|
|
*
|
|
* Return: pointer allocated to issei_device structure, error on failure
|
|
*/
|
|
struct issei_device *issei_register(size_t hw_size, struct device *parent,
|
|
const struct issei_dma_length *dma_length,
|
|
const struct issei_hw_ops *ops)
|
|
{
|
|
struct issei_device *idev;
|
|
u32 minor;
|
|
int ret, devno;
|
|
|
|
idev = kzalloc(sizeof(*idev) + hw_size, GFP_KERNEL);
|
|
if (!idev)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
issei_device_init(idev, parent, dma_length, ops);
|
|
|
|
ret = xa_alloc(&issei_minor_xa, &minor, idev, XA_LIMIT(0, ISSEI_MAX_DEVS), GFP_KERNEL);
|
|
if (ret < 0) {
|
|
dev_err(&idev->dev, "Failed to allocate minor. ret = %d\n", ret);
|
|
kfree(idev);
|
|
return ERR_PTR(ret);
|
|
}
|
|
|
|
idev->minor = minor;
|
|
devno = MKDEV(MAJOR(issei_devt), idev->minor);
|
|
|
|
device_initialize(&idev->dev);
|
|
idev->dev.devt = devno;
|
|
idev->dev.class = issei_class;
|
|
idev->dev.parent = parent;
|
|
idev->dev.groups = issei_groups;
|
|
idev->dev.release = issei_device_release;
|
|
dev_set_drvdata(&idev->dev, idev);
|
|
|
|
idev->cdev = cdev_alloc();
|
|
if (!idev->cdev) {
|
|
ret = -ENOMEM;
|
|
goto err;
|
|
}
|
|
idev->cdev->ops = &issei_fops;
|
|
if (parent->driver)
|
|
idev->cdev->owner = parent->driver->owner;
|
|
cdev_set_parent(idev->cdev, &idev->dev.kobj);
|
|
|
|
ret = cdev_add(idev->cdev, devno, 1);
|
|
if (ret) {
|
|
dev_err(parent, "unable to add device %d:%u ret = %d\n",
|
|
MAJOR(issei_devt), idev->minor, ret);
|
|
goto err_del_cdev;
|
|
}
|
|
|
|
ret = dev_set_name(&idev->dev, "issei%u", idev->minor);
|
|
if (ret) {
|
|
dev_err(parent, "unable to set name to device %d:%u ret = %d\n",
|
|
MAJOR(issei_devt), idev->minor, ret);
|
|
goto err_del_cdev;
|
|
}
|
|
|
|
ret = device_add(&idev->dev);
|
|
if (ret) {
|
|
dev_err(parent, "unable to add device %d:%u ret = %d\n",
|
|
MAJOR(issei_devt), idev->minor, ret);
|
|
goto err_del_cdev;
|
|
}
|
|
|
|
idev->fw_clients = kset_create_and_add("fw_clients", NULL, &idev->dev.kobj);
|
|
if (!idev->fw_clients) {
|
|
ret = -ENOMEM;
|
|
goto err_del_dev;
|
|
}
|
|
|
|
return idev;
|
|
|
|
err_del_dev:
|
|
device_del(&idev->dev);
|
|
err_del_cdev:
|
|
cdev_del(idev->cdev);
|
|
err:
|
|
put_device(&idev->dev);
|
|
xa_erase(&issei_minor_xa, minor);
|
|
|
|
return ERR_PTR(ret);
|
|
}
|
|
EXPORT_SYMBOL_GPL(issei_register);
|
|
|
|
/**
|
|
* issei_deregister: remove issei character device
|
|
* @idev: the device structure
|
|
*/
|
|
void issei_deregister(struct issei_device *idev)
|
|
{
|
|
u32 minor = idev->minor;
|
|
|
|
cdev_del(idev->cdev);
|
|
|
|
kset_unregister(idev->fw_clients);
|
|
|
|
device_del(&idev->dev);
|
|
|
|
put_device(&idev->dev);
|
|
|
|
xa_erase(&issei_minor_xa, minor);
|
|
}
|
|
EXPORT_SYMBOL_GPL(issei_deregister);
|
|
|
|
static int __init issei_cdev_init(void)
|
|
{
|
|
int ret;
|
|
|
|
issei_class = class_create("issei");
|
|
if (IS_ERR(issei_class)) {
|
|
pr_err("couldn't create class\n");
|
|
return PTR_ERR(issei_class);
|
|
}
|
|
|
|
ret = alloc_chrdev_region(&issei_devt, 0, ISSEI_MAX_DEVS, "issei");
|
|
if (ret < 0) {
|
|
pr_err("unable to allocate char dev region\n");
|
|
class_destroy(issei_class);
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void __exit issei_cdev_exit(void)
|
|
{
|
|
unregister_chrdev_region(issei_devt, ISSEI_MAX_DEVS);
|
|
class_destroy(issei_class);
|
|
}
|
|
|
|
module_init(issei_cdev_init);
|
|
module_exit(issei_cdev_exit);
|
|
|
|
MODULE_DESCRIPTION("Intel(R) Silicon Security Engine Interface");
|
|
MODULE_LICENSE("GPL");
|