810 lines · 20.5 KB · C
RawDownload
// SPDX-License-Identifier: GPL-2.0+/* * Provides user-space access to the SSAM EC via the /dev/surface/aggregator * misc device. Intended for debugging and development. * * Copyright (C) 2020-2022 Maximilian Luz <luzmaximilian@gmail.com> */ #include <linux/fs.h>#include <linux/ioctl.h>#include <linux/kernel.h>#include <linux/kfifo.h>#include <linux/kref.h>#include <linux/miscdevice.h>#include <linux/module.h>#include <linux/platform_device.h>#include <linux/poll.h>#include <linux/rwsem.h>#include <linux/slab.h>#include <linux/uaccess.h>#include <linux/vmalloc.h> #include <linux/surface_aggregator/cdev.h>#include <linux/surface_aggregator/controller.h>#include <linux/surface_aggregator/serial_hub.h> #define SSAM_CDEV_DEVICE_NAME	"surface_aggregator_cdev"  /* -- Main structures. ------------------------------------------------------ */ enum ssam_cdev_device_state {	SSAM_CDEV_DEVICE_SHUTDOWN_BIT = BIT(0),}; struct ssam_cdev {	struct kref kref;	struct rw_semaphore lock; 	struct device *dev;	struct ssam_controller *ctrl;	struct miscdevice mdev;	unsigned long flags; 	struct rw_semaphore client_lock;  /* Guards client list. */	struct list_head client_list;}; struct ssam_cdev_client; struct ssam_cdev_notifier {	struct ssam_cdev_client *client;	struct ssam_event_notifier nf;}; struct ssam_cdev_client {	struct ssam_cdev *cdev;	struct list_head node; 	struct mutex notifier_lock;	/* Guards notifier access for registration */	struct ssam_cdev_notifier *notifier[SSH_NUM_EVENTS]; 	struct mutex read_lock;		/* Guards FIFO buffer read access */	struct mutex write_lock;	/* Guards FIFO buffer write access */	DECLARE_KFIFO(buffer, u8, 4096); 	wait_queue_head_t waitq;	struct fasync_struct *fasync;}; static void __ssam_cdev_release(struct kref *kref){	kfree(container_of(kref, struct ssam_cdev, kref));} static struct ssam_cdev *ssam_cdev_get(struct ssam_cdev *cdev){	if (cdev)		kref_get(&cdev->kref); 	return cdev;} static void ssam_cdev_put(struct ssam_cdev *cdev){	if (cdev)		kref_put(&cdev->kref, __ssam_cdev_release);}  /* -- Notifier handling. ---------------------------------------------------- */ static u32 ssam_cdev_notifier(struct ssam_event_notifier *nf, const struct ssam_event *in){	struct ssam_cdev_notifier *cdev_nf = container_of(nf, struct ssam_cdev_notifier, nf);	struct ssam_cdev_client *client = cdev_nf->client;	struct ssam_cdev_event event;	size_t n = struct_size(&event, data, in->length); 	/* Translate event. */	event.target_category = in->target_category;	event.target_id = in->target_id;	event.command_id = in->command_id;	event.instance_id = in->instance_id;	event.length = in->length; 	mutex_lock(&client->write_lock); 	/* Make sure we have enough space. */	if (kfifo_avail(&client->buffer) < n) {		dev_warn(client->cdev->dev,			 "buffer full, dropping event (tc: %#04x, tid: %#04x, cid: %#04x, iid: %#04x)\n",			 in->target_category, in->target_id, in->command_id, in->instance_id);		mutex_unlock(&client->write_lock);		return 0;	} 	/* Copy event header and payload. */	kfifo_in(&client->buffer, (const u8 *)&event, struct_size(&event, data, 0));	kfifo_in(&client->buffer, &in->data[0], in->length); 	mutex_unlock(&client->write_lock); 	/* Notify waiting readers. */	kill_fasync(&client->fasync, SIGIO, POLL_IN);	wake_up_interruptible(&client->waitq); 	/*	 * Don't mark events as handled, this is the job of a proper driver and	 * not the debugging interface.	 */	return 0;} static int ssam_cdev_notifier_register(struct ssam_cdev_client *client, u8 tc, int priority){	const u16 rqid = ssh_tc_to_rqid(tc);	const u16 event = ssh_rqid_to_event(rqid);	struct ssam_cdev_notifier *nf;	int status; 	lockdep_assert_held_read(&client->cdev->lock); 	/* Validate notifier target category. */	if (!ssh_rqid_is_event(rqid))		return -EINVAL; 	mutex_lock(&client->notifier_lock); 	/* Check if the notifier has already been registered. */	if (client->notifier[event]) {		mutex_unlock(&client->notifier_lock);		return -EEXIST;	} 	/* Allocate new notifier. */	nf = kzalloc(sizeof(*nf), GFP_KERNEL);	if (!nf) {		mutex_unlock(&client->notifier_lock);		return -ENOMEM;	} 	/*	 * Create a dummy notifier with the minimal required fields for	 * observer registration. Note that we can skip fully specifying event	 * and registry here as we do not need any matching and use silent	 * registration, which does not enable the corresponding event.	 */	nf->client = client;	nf->nf.base.fn = ssam_cdev_notifier;	nf->nf.base.priority = priority;	nf->nf.event.id.target_category = tc;	nf->nf.event.mask = 0;	/* Do not do any matching. */	nf->nf.flags = SSAM_EVENT_NOTIFIER_OBSERVER; 	/* Register notifier. */	status = ssam_notifier_register(client->cdev->ctrl, &nf->nf);	if (status)		kfree(nf);	else		client->notifier[event] = nf; 	mutex_unlock(&client->notifier_lock);	return status;} static int ssam_cdev_notifier_unregister(struct ssam_cdev_client *client, u8 tc){	const u16 rqid = ssh_tc_to_rqid(tc);	const u16 event = ssh_rqid_to_event(rqid);	int status; 	lockdep_assert_held_read(&client->cdev->lock); 	/* Validate notifier target category. */	if (!ssh_rqid_is_event(rqid))		return -EINVAL; 	mutex_lock(&client->notifier_lock); 	/* Check if the notifier is currently registered. */	if (!client->notifier[event]) {		mutex_unlock(&client->notifier_lock);		return -ENOENT;	} 	/* Unregister and free notifier. */	status = ssam_notifier_unregister(client->cdev->ctrl, &client->notifier[event]->nf);	kfree(client->notifier[event]);	client->notifier[event] = NULL; 	mutex_unlock(&client->notifier_lock);	return status;} static void ssam_cdev_notifier_unregister_all(struct ssam_cdev_client *client){	int i; 	down_read(&client->cdev->lock); 	/*	 * This function may be used during shutdown, thus we need to test for	 * cdev->ctrl instead of the SSAM_CDEV_DEVICE_SHUTDOWN_BIT bit.	 */	if (client->cdev->ctrl) {		for (i = 0; i < SSH_NUM_EVENTS; i++)			ssam_cdev_notifier_unregister(client, i + 1); 	} else {		int count = 0; 		/*		 * Device has been shut down. Any notifier remaining is a bug,		 * so warn about that as this would otherwise hardly be		 * noticeable. Nevertheless, free them as well.		 */		mutex_lock(&client->notifier_lock);		for (i = 0; i < SSH_NUM_EVENTS; i++) {			count += !!(client->notifier[i]);			kfree(client->notifier[i]);			client->notifier[i] = NULL;		}		mutex_unlock(&client->notifier_lock); 		WARN_ON(count > 0);	} 	up_read(&client->cdev->lock);}  /* -- IOCTL functions. ------------------------------------------------------ */ static long ssam_cdev_request(struct ssam_cdev_client *client, struct ssam_cdev_request __user *r){	struct ssam_cdev_request rqst;	struct ssam_request spec = {};	struct ssam_response rsp = {};	const void __user *plddata;	void __user *rspdata;	int status = 0, ret = 0, tmp; 	lockdep_assert_held_read(&client->cdev->lock); 	ret = copy_struct_from_user(&rqst, sizeof(rqst), r, sizeof(*r));	if (ret)		goto out; 	plddata = u64_to_user_ptr(rqst.payload.data);	rspdata = u64_to_user_ptr(rqst.response.data); 	/* Setup basic request fields. */	spec.target_category = rqst.target_category;	spec.target_id = rqst.target_id;	spec.command_id = rqst.command_id;	spec.instance_id = rqst.instance_id;	spec.flags = 0;	spec.length = rqst.payload.length;	spec.payload = NULL; 	if (rqst.flags & SSAM_CDEV_REQUEST_HAS_RESPONSE)		spec.flags |= SSAM_REQUEST_HAS_RESPONSE; 	if (rqst.flags & SSAM_CDEV_REQUEST_UNSEQUENCED)		spec.flags |= SSAM_REQUEST_UNSEQUENCED; 	rsp.capacity = rqst.response.length;	rsp.length = 0;	rsp.pointer = NULL; 	/* Get request payload from user-space. */	if (spec.length) {		if (!plddata) {			ret = -EINVAL;			goto out;		} 		/*		 * Note: spec.length is limited to U16_MAX bytes via struct		 * ssam_cdev_request. This is slightly larger than the		 * theoretical maximum (SSH_COMMAND_MAX_PAYLOAD_SIZE) of the		 * underlying protocol (note that nothing remotely this size		 * should ever be allocated in any normal case). This size is		 * validated later in ssam_request_sync(), for allocation the		 * bound imposed by u16 should be enough.		 */		spec.payload = kzalloc(spec.length, GFP_KERNEL);		if (!spec.payload) {			ret = -ENOMEM;			goto out;		} 		if (copy_from_user((void *)spec.payload, plddata, spec.length)) {			ret = -EFAULT;			goto out;		}	} 	/* Allocate response buffer. */	if (rsp.capacity) {		if (!rspdata) {			ret = -EINVAL;			goto out;		} 		/*		 * Note: rsp.capacity is limited to U16_MAX bytes via struct		 * ssam_cdev_request. This is slightly larger than the		 * theoretical maximum (SSH_COMMAND_MAX_PAYLOAD_SIZE) of the		 * underlying protocol (note that nothing remotely this size		 * should ever be allocated in any normal case). In later use,		 * this capacity does not have to be strictly bounded, as it		 * is only used as an output buffer to be written to. For		 * allocation the bound imposed by u16 should be enough.		 */		rsp.pointer = kzalloc(rsp.capacity, GFP_KERNEL);		if (!rsp.pointer) {			ret = -ENOMEM;			goto out;		}	} 	/* Perform request. */	status = ssam_request_sync(client->cdev->ctrl, &spec, &rsp);	if (status)		goto out; 	/* Copy response to user-space. */	if (rsp.length && copy_to_user(rspdata, rsp.pointer, rsp.length))		ret = -EFAULT; out:	/* Always try to set response-length and status. */	tmp = put_user(rsp.length, &r->response.length);	if (tmp)		ret = tmp; 	tmp = put_user(status, &r->status);	if (tmp)		ret = tmp; 	/* Cleanup. */	kfree(spec.payload);	kfree(rsp.pointer); 	return ret;} static long ssam_cdev_notif_register(struct ssam_cdev_client *client,				     const struct ssam_cdev_notifier_desc __user *d){	struct ssam_cdev_notifier_desc desc;	long ret; 	lockdep_assert_held_read(&client->cdev->lock); 	ret = copy_struct_from_user(&desc, sizeof(desc), d, sizeof(*d));	if (ret)		return ret; 	return ssam_cdev_notifier_register(client, desc.target_category, desc.priority);} static long ssam_cdev_notif_unregister(struct ssam_cdev_client *client,				       const struct ssam_cdev_notifier_desc __user *d){	struct ssam_cdev_notifier_desc desc;	long ret; 	lockdep_assert_held_read(&client->cdev->lock); 	ret = copy_struct_from_user(&desc, sizeof(desc), d, sizeof(*d));	if (ret)		return ret; 	return ssam_cdev_notifier_unregister(client, desc.target_category);} static long ssam_cdev_event_enable(struct ssam_cdev_client *client,				   const struct ssam_cdev_event_desc __user *d){	struct ssam_cdev_event_desc desc;	struct ssam_event_registry reg;	struct ssam_event_id id;	long ret; 	lockdep_assert_held_read(&client->cdev->lock); 	/* Read descriptor from user-space. */	ret = copy_struct_from_user(&desc, sizeof(desc), d, sizeof(*d));	if (ret)		return ret; 	/* Translate descriptor. */	reg.target_category = desc.reg.target_category;	reg.target_id = desc.reg.target_id;	reg.cid_enable = desc.reg.cid_enable;	reg.cid_disable = desc.reg.cid_disable; 	id.target_category = desc.id.target_category;	id.instance = desc.id.instance; 	/* Disable event. */	return ssam_controller_event_enable(client->cdev->ctrl, reg, id, desc.flags);} static long ssam_cdev_event_disable(struct ssam_cdev_client *client,				    const struct ssam_cdev_event_desc __user *d){	struct ssam_cdev_event_desc desc;	struct ssam_event_registry reg;	struct ssam_event_id id;	long ret; 	lockdep_assert_held_read(&client->cdev->lock); 	/* Read descriptor from user-space. */	ret = copy_struct_from_user(&desc, sizeof(desc), d, sizeof(*d));	if (ret)		return ret; 	/* Translate descriptor. */	reg.target_category = desc.reg.target_category;	reg.target_id = desc.reg.target_id;	reg.cid_enable = desc.reg.cid_enable;	reg.cid_disable = desc.reg.cid_disable; 	id.target_category = desc.id.target_category;	id.instance = desc.id.instance; 	/* Disable event. */	return ssam_controller_event_disable(client->cdev->ctrl, reg, id, desc.flags);}  /* -- File operations. ------------------------------------------------------ */ static int ssam_cdev_device_open(struct inode *inode, struct file *filp){	struct miscdevice *mdev = filp->private_data;	struct ssam_cdev_client *client;	struct ssam_cdev *cdev = container_of(mdev, struct ssam_cdev, mdev); 	/* Initialize client */	client = vzalloc(sizeof(*client));	if (!client)		return -ENOMEM; 	client->cdev = ssam_cdev_get(cdev); 	INIT_LIST_HEAD(&client->node); 	mutex_init(&client->notifier_lock); 	mutex_init(&client->read_lock);	mutex_init(&client->write_lock);	INIT_KFIFO(client->buffer);	init_waitqueue_head(&client->waitq); 	filp->private_data = client; 	/* Attach client. */	down_write(&cdev->client_lock); 	if (test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &cdev->flags)) {		up_write(&cdev->client_lock);		mutex_destroy(&client->write_lock);		mutex_destroy(&client->read_lock);		mutex_destroy(&client->notifier_lock);		ssam_cdev_put(client->cdev);		vfree(client);		return -ENODEV;	}	list_add_tail(&client->node, &cdev->client_list); 	up_write(&cdev->client_lock); 	stream_open(inode, filp);	return 0;

Lines 1–500 of 810.