// SPDX-License-Identifier: GPL-2.0
/* Copyright (C) 2023-2026 Intel Corporation */
#include <linux/cleanup.h>
#include <linux/dev_printk.h>
#include <linux/err.h>
#include <linux/fs.h>
#include <linux/kobject.h>
#include <linux/list.h>
#include <linux/overflow.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/wait.h>
#include <linux/uuid.h>
#include "fw_client.h"
#include "host_client.h"
#include "issei_dev.h"
static inline u8 __issei_cl_fw_id(const struct issei_host_client *cl)
{
return cl->fw_cl ? cl->fw_cl->id : 0;
}
#define ISSEI_CL_FMT "cl:host=%02d fw=%02d "
#define cl_dbg(_dev_, _cl_, format, arg...) do { \
struct issei_host_client *_l_cl_ = _cl_; \
dev_dbg(&(_dev_)->dev, ISSEI_CL_FMT format, _l_cl_->id, \
__issei_cl_fw_id(_l_cl_), ##arg); \
} while (0)
#define cl_warn(_dev_, _cl_, format, arg...) do { \
struct issei_host_client *_l_cl_ = _cl_; \
dev_warn(&(_dev_)->dev, ISSEI_CL_FMT format, _l_cl_->id, \
__issei_cl_fw_id(_l_cl_), ##arg); \
} while (0)
#define cl_err(_dev_, _cl_, format, arg...) do { \
struct issei_host_client *_l_cl_ = _cl_; \
dev_err(&(_dev_)->dev, ISSEI_CL_FMT format, _l_cl_->id, \
__issei_cl_fw_id(_l_cl_), ##arg); \
} while (0)
static void __issei_cl_clean_wbuf(struct issei_write_buf *wbuf)
{
list_del(&wbuf->list);
kfree(wbuf->data);
kfree(wbuf);
}
static void __issei_cl_release_rbuf(struct issei_host_client *cl)
{
cl->read_data = NULL;
cl->read_data_size = 0;
}
static void __issei_cl_clean_rbuf(struct issei_host_client *cl)
{
kfree(cl->read_data);
__issei_cl_release_rbuf(cl);
}
static void __issei_cl_clean_all_wbuf(struct issei_device *idev, struct issei_host_client *cl)
{
struct issei_write_buf *wbuf, *next;
if (!cl->write_in_progress)
return;
list_for_each_entry_safe(wbuf, next, &idev->write_queue, list) {
if (wbuf->cl == cl) {
__issei_cl_clean_wbuf(wbuf);
break;
}
}
cl->write_in_progress = false;
/* synchronized under host client mutex */
if (waitqueue_active(&cl->write_wait))
wake_up_interruptible(&cl->write_wait);
}
static struct issei_host_client *__issei_cl_by_id(struct issei_device *idev, u16 id)
{
struct issei_host_client *cl;
list_for_each_entry(cl, &idev->host_client_list, list) {
if (cl->id == id)
return cl;
}
return NULL;
}
static void __issei_cl_disconnect(struct issei_device *idev, struct issei_host_client *cl)
{
if (cl->state == ISSEI_HOST_CL_STATE_DISCONNECTED)
return;
__issei_cl_clean_all_wbuf(idev, cl);
if (!WARN_ON(!cl->fw_cl)) {
issei_fw_cl_disconnect(cl->fw_cl);
cl->fw_cl = NULL;
}
cl->state = ISSEI_HOST_CL_STATE_DISCONNECTED;
if (cl->read_data)
__issei_cl_clean_rbuf(cl);
/* synchronized under host client mutex */
if (waitqueue_active(&cl->read_wait))
wake_up_interruptible(&cl->read_wait);
cl_dbg(idev, cl, "Disconnected\n");
}
static void __issei_cl_init(struct issei_host_client *cl, struct issei_device *idev,
u16 id, struct file *fp)
{
INIT_LIST_HEAD(&cl->list);
cl->idev = idev;
cl->id = id;
cl->state = ISSEI_HOST_CL_STATE_DISCONNECTED;
cl->fp = fp;
init_waitqueue_head(&cl->write_wait);
init_waitqueue_head(&cl->read_wait);
__issei_cl_release_rbuf(cl);
}
/**
* issei_cl_create - create the host client
* @idev: issei device
* @fp: file pointer to associate with host client
*
* Return: client pointer on success, ERR_PTR on error
*/
struct issei_host_client *issei_cl_create(struct issei_device *idev, struct file *fp)
{
struct issei_host_client *cl;
u16 id;
guard(mutex)(&idev->client_lock);
if (idev->host_client_count == ISSEI_HOST_CLIENTS_MAX) {
dev_err(&idev->dev, "Maximum open clients %d is reached.\n",
ISSEI_HOST_CLIENTS_MAX);
return ERR_PTR(-EMFILE);
}
do {
if (check_add_overflow(idev->host_client_last_id, 1, &id)) /* overflow */
id = 1;
idev->host_client_last_id = id;
/* Not an endless loop as we have less clients then id's */
} while (__issei_cl_by_id(idev, id));
cl = kzalloc_obj(*cl);
if (!cl)
return ERR_PTR(-ENOMEM);
__issei_cl_init(cl, idev, id, fp);
list_add_tail(&cl->list, &idev->host_client_list);
idev->host_client_count++;
cl_dbg(idev, cl, "Created\n");
return cl;
}
/**
* issei_cl_remove - disconnect and free the host client
* @cl: host client
*/
void issei_cl_remove(struct issei_host_client *cl)
{
struct issei_device *idev;
/* don't shout on error exit path */
if (!cl)
return;
idev = cl->idev;
guard(mutex)(&idev->client_lock);
idev->host_client_count--;
list_del(&cl->list);
__issei_cl_disconnect(idev, cl);
cl_dbg(idev, cl, "Removed\n");
kfree(cl);
}
/**
* issei_cl_connect - connect between FW and host client
* @cl: host client
* @uuid: FW client unique ID
* @mtu: memory for FW client max message size
* @ver: memory for FW client version
* @flags: memory for FW client flags
*
* Search for firmware client by UUID and connect it to provided
* host client, if not already connected to some client.
*
* Return: 0 on success, <0 on error
*/
int issei_cl_connect(struct issei_host_client *cl, const uuid_t *uuid, u32 *mtu, u8 *ver,
u32 *flags)
{
struct issei_device *idev = cl->idev;
struct issei_fw_client *fw_cl;
int ret;
guard(mutex)(&idev->client_lock);
if (cl->state == ISSEI_HOST_CL_STATE_CONNECTED) {
cl_err(idev, cl, "Already connected\n");
return -EISCONN;
}
fw_cl = issei_fw_cl_find_by_uuid(idev, uuid);
if (!fw_cl) {
cl_dbg(idev, cl, "FW client %pUb not found\n", uuid);
return -ENOTTY;
}
ret = issei_fw_cl_connect(fw_cl, cl); /* calls kobject_get for fw_cl on success */
kobject_put(&fw_cl->kobj);
if (ret) {
cl_err(idev, cl, "FW client is already connected ret = %d\n", ret);
return ret;
}
cl->fw_cl = fw_cl;
cl->state = ISSEI_HOST_CL_STATE_CONNECTED;
*mtu = fw_cl->mtu;
*ver = fw_cl->ver;
*flags = fw_cl->flags;
cl_dbg(idev, cl, "Connected\n");
return 0;
}
/**
* issei_cl_disconnect - disconnect between FW and host client
* @cl: host client
*
* Return: 0 on success, -ENOTCONN if not connected
*/
int issei_cl_disconnect(struct issei_host_client *cl)
{
struct issei_device *idev = cl->idev;
guard(mutex)(&idev->client_lock);
if (cl->state != ISSEI_HOST_CL_STATE_CONNECTED)
return -ENOTCONN;
__issei_cl_disconnect(idev, cl);
return 0;
}
/**
* issei_cl_all_disconnect - disconnect all FW clients
* @idev: issei device
*/
void issei_cl_all_disconnect(struct issei_device *idev)
{
struct issei_host_client *cl;
guard(mutex)(&idev->client_lock);
list_for_each_entry(cl, &idev->host_client_list, list)
__issei_cl_disconnect(idev, cl);
}
/**
* issei_cl_write - enqueue write request
* @cl: host client
* @buf: buffer to write
* @buf_size: buffer size
*
* Add write request to the write queue and wakes working thread.
* This call takes ownership of buf memory, if succeeded.
*
* Return: size of data on success, <0 on error
*/
ssize_t issei_cl_write(struct issei_host_client *cl, const u8 *buf, size_t buf_size)
{
struct issei_device *idev = cl->idev;
struct issei_write_buf *wbuf;
guard(mutex)(&idev->client_lock);
if (cl->state != ISSEI_HOST_CL_STATE_CONNECTED)
return -ENOTCONN;
if (cl->write_in_progress) {
cl_dbg(idev, cl, "Another write is in progress\n");
return -EAGAIN;
}
if (buf_size > cl->fw_cl->mtu) {
cl_err(idev, cl, "Write is too big %zu > %u\n", buf_size, cl->fw_cl->mtu);
return -EFBIG;
}
wbuf = kmalloc_obj(*wbuf);
if (!wbuf)
return -ENOMEM;
wbuf->cl = cl;
wbuf->data = buf;
wbuf->data_size = buf_size;
list_add_tail(&wbuf->list, &idev->write_queue);
cl->write_in_progress = true;
cl_dbg(idev, cl, "Write queued %zu bytes\n", buf_size);
issei_poke_process_thread(idev);
return buf_size;
}
/**
* issei_cl_write_from_queue - writes first request from queue to firmware
* @idev: issei device
*
* Tries to write first request from the write queue to firmware.
* Releases buf memory, if succeeded.
*
* Return: 0 on success, <0 on error
*/
int issei_cl_write_from_queue(struct issei_device *idev)
{
struct issei_write_buf *wbuf;
struct issei_dma_data data;
struct issei_host_client *cl;
int ret;
guard(mutex)(&idev->client_lock);
wbuf = list_first_entry_or_null(&idev->write_queue, struct issei_write_buf, list);
if (!wbuf)
return 0;
cl = wbuf->cl;
data.fw_id = cl->fw_cl->id;
data.host_id = cl->id;
data.flags = 0;
data.status = 0;
data.length = wbuf->data_size;
data.buf = (void *)wbuf->data;
ret = issei_dma_write(idev, &data);
if (ret == -EBUSY)
return 0;
if (ret == -EIO)
return ret;
if (ret >= 0)
idev->ops->irq_write_generate(idev);
cl->write_in_progress = false;
/* synchronized under host client mutex */
if (waitqueue_active(&cl->write_wait))
wake_up_interruptible(&cl->write_wait);
cl_dbg(idev, cl, "Write %zu bytes\n", wbuf->data_size);
__issei_cl_clean_wbuf(wbuf);
return 0;
}
static struct issei_host_client *__issei_cl_read_buf_check(struct issei_device *idev, u16 fw_id,
u16 host_id, size_t buf_size)
{
struct issei_host_client *cl;
cl = __issei_cl_by_id(idev, host_id);
if (!cl) {
dev_dbg(&idev->dev, "No client %u\n", host_id);
return ERR_PTR(-ENOTTY);
}
if (cl->state != ISSEI_HOST_CL_STATE_CONNECTED) {
cl_dbg(idev, cl, "Not connected\n");
return ERR_PTR(-ENODEV);
}
if (cl->fw_cl->id != fw_id) {
cl_dbg(idev, cl, "Wrong firmware client %u ?= %u\n", cl->fw_cl->id, fw_id);
return ERR_PTR(-ENODEV);
}
if (buf_size > cl->fw_cl->mtu) {
cl_err(idev, cl, "Read is too big %zu > %u\n", buf_size, cl->fw_cl->mtu);
__issei_cl_disconnect(idev, cl);
return NULL;
}
if (cl->read_data) {
cl_err(idev, cl, "Previous data was not read by user-space, disconnecting\n");
__issei_cl_disconnect(idev, cl);
return NULL;
}
return cl;
}
/**
* issei_cl_read_buf - process data from firmware
* @idev: issei device
* @fw_id: firmware client id
* @host_id: host client id
* @buf: buffer with data
* @buf_size: buffer size
*
* Puts data from firmware into provided host client storage.
* Free buffer or consume it.
*
* Return: 0 on success or recoverable error, <0 on unrecoverable error
*/
int issei_cl_read_buf(struct issei_device *idev, u16 fw_id, u16 host_id, u8 *buf, size_t buf_size)
{
struct issei_host_client *cl;
guard(mutex)(&idev->client_lock);
cl = __issei_cl_read_buf_check(idev, fw_id, host_id, buf_size);
if (IS_ERR_OR_NULL(cl)) {
kfree(buf);
return PTR_ERR(cl);
}
cl->read_data = buf;
cl->read_data_size = buf_size;
/* synchronized under host client mutex */
if (waitqueue_active(&cl->read_wait))
wake_up_interruptible(&cl->read_wait);
cl_dbg(idev, cl, "Read %zu bytes\n", buf_size);
return 0;
}
/**
* issei_cl_read - read data from queue to provided buffer
* @cl: host client
* @buf: buffer to store data
* @buf_size: buffer size
*
* Tries to take data buffer from client and return it to caller.
* The caller receives ownership of the data buffer.
*
* Return: read data size on success, <0 on error
*/
ssize_t issei_cl_read(struct issei_host_client *cl, u8 **buf, size_t buf_size)
{
struct issei_device *idev = cl->idev;
size_t read_data_size;
guard(mutex)(&idev->client_lock);
if (cl->state != ISSEI_HOST_CL_STATE_CONNECTED)
return -ENOTCONN;
if (!cl->read_data)
return -ENOENT;
if (cl->read_data_size > buf_size) {
cl_err(idev, cl, "Buffer is too small %zu > %zu\n",
cl->read_data_size, buf_size);
return -EFBIG;
}
*buf = cl->read_data;
read_data_size = cl->read_data_size;
cl_dbg(idev, cl, "Read by client %zu bytes\n", read_data_size);
__issei_cl_release_rbuf(cl);
return read_data_size;
}
/**
* issei_cl_check_read - check if client has data to read
*
* @cl: host client
*
* Return: 1 - data available, 0 - no data, < 0 on error
*/
int issei_cl_check_read(struct issei_host_client *cl)
{
struct issei_device *idev = cl->idev;
guard(mutex)(&idev->client_lock);
if (cl->state != ISSEI_HOST_CL_STATE_CONNECTED)
return -ENOTCONN;
if (!cl->read_data)
return 0;
return 1;
}
/**
* issei_cl_check_write - check if client is ready to write
*
* @cl: host client
*
* Return: 1 - can not write, 0 - can write, < 0 on error
*/
int issei_cl_check_write(struct issei_host_client *cl)
{
struct issei_device *idev = cl->idev;
guard(mutex)(&idev->client_lock);
if (cl->state != ISSEI_HOST_CL_STATE_CONNECTED)
return -ENOTCONN;
if (cl->write_in_progress)
return 1;
return 0;
}
void issei_cl_clean_all_wbuf(struct issei_host_client *cl)
{
struct issei_device *idev = cl->idev;
guard(mutex)(&idev->client_lock);
__issei_cl_clean_all_wbuf(idev, cl);
}