linux/drivers/misc/bcm-vk/bcm_vk_tty.c
Johan Hovold d9d1232b48 misc: bcm-vk: fix tty registration race
Make sure to set the tty class-device driver data before registering the
tty to avoid having a racing open() dereference a NULL pointer.

Fixes: 91ca10d6fa ("misc: bcm-vk: add ttyVK support")
Cc: stable@vger.kernel.org      # 5.12
Signed-off-by: Johan Hovold <johan@kernel.org>
Link: https://lore.kernel.org/r/20210917115736.5816-1-johan@kernel.org
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
2021-09-21 16:17:15 +02:00

340 lines
8.2 KiB
C

// SPDX-License-Identifier: GPL-2.0
/*
* Copyright 2018-2020 Broadcom.
*/
#include <linux/tty.h>
#include <linux/tty_driver.h>
#include <linux/tty_flip.h>
#include "bcm_vk.h"
/* TTYVK base offset is 0x30000 into BAR1 */
#define BAR1_TTYVK_BASE_OFFSET 0x300000
/* Each TTYVK channel (TO or FROM) is 0x10000 */
#define BAR1_TTYVK_CHAN_OFFSET 0x100000
/* Each TTYVK channel has TO and FROM, hence the * 2 */
#define BAR1_TTYVK_BASE(index) (BAR1_TTYVK_BASE_OFFSET + \
((index) * BAR1_TTYVK_CHAN_OFFSET * 2))
/* TO TTYVK channel base comes before FROM for each index */
#define TO_TTYK_BASE(index) BAR1_TTYVK_BASE(index)
#define FROM_TTYK_BASE(index) (BAR1_TTYVK_BASE(index) + \
BAR1_TTYVK_CHAN_OFFSET)
struct bcm_vk_tty_chan {
u32 reserved;
u32 size;
u32 wr;
u32 rd;
u32 *data;
};
#define VK_BAR_CHAN(v, DIR, e) ((v)->DIR##_offset \
+ offsetof(struct bcm_vk_tty_chan, e))
#define VK_BAR_CHAN_SIZE(v, DIR) VK_BAR_CHAN(v, DIR, size)
#define VK_BAR_CHAN_WR(v, DIR) VK_BAR_CHAN(v, DIR, wr)
#define VK_BAR_CHAN_RD(v, DIR) VK_BAR_CHAN(v, DIR, rd)
#define VK_BAR_CHAN_DATA(v, DIR, off) (VK_BAR_CHAN(v, DIR, data) + (off))
#define VK_BAR0_REGSEG_TTY_DB_OFFSET 0x86c
/* Poll every 1/10 of second - temp hack till we use MSI interrupt */
#define SERIAL_TIMER_VALUE (HZ / 10)
static void bcm_vk_tty_poll(struct timer_list *t)
{
struct bcm_vk *vk = from_timer(vk, t, serial_timer);
queue_work(vk->tty_wq_thread, &vk->tty_wq_work);
mod_timer(&vk->serial_timer, jiffies + SERIAL_TIMER_VALUE);
}
irqreturn_t bcm_vk_tty_irqhandler(int irq, void *dev_id)
{
struct bcm_vk *vk = dev_id;
queue_work(vk->tty_wq_thread, &vk->tty_wq_work);
return IRQ_HANDLED;
}
static void bcm_vk_tty_wq_handler(struct work_struct *work)
{
struct bcm_vk *vk = container_of(work, struct bcm_vk, tty_wq_work);
struct bcm_vk_tty *vktty;
int card_status;
int count;
unsigned char c;
int i;
int wr;
card_status = vkread32(vk, BAR_0, BAR_CARD_STATUS);
if (BCM_VK_INTF_IS_DOWN(card_status))
return;
for (i = 0; i < BCM_VK_NUM_TTY; i++) {
count = 0;
/* Check the card status that the tty channel is ready */
if ((card_status & BIT(i)) == 0)
continue;
vktty = &vk->tty[i];
/* Don't increment read index if tty app is closed */
if (!vktty->is_opened)
continue;
/* Fetch the wr offset in buffer from VK */
wr = vkread32(vk, BAR_1, VK_BAR_CHAN_WR(vktty, from));
/* safe to ignore until bar read gives proper size */
if (vktty->from_size == 0)
continue;
if (wr >= vktty->from_size) {
dev_err(&vk->pdev->dev,
"ERROR: wq handler ttyVK%d wr:0x%x > 0x%x\n",
i, wr, vktty->from_size);
/* Need to signal and close device in this case */
continue;
}
/*
* Simple read of circular buffer and
* insert into tty flip buffer
*/
while (vk->tty[i].rd != wr) {
c = vkread8(vk, BAR_1,
VK_BAR_CHAN_DATA(vktty, from, vktty->rd));
vktty->rd++;
if (vktty->rd >= vktty->from_size)
vktty->rd = 0;
tty_insert_flip_char(&vktty->port, c, TTY_NORMAL);
count++;
}
if (count) {
tty_flip_buffer_push(&vktty->port);
/* Update read offset from shadow register to card */
vkwrite32(vk, vktty->rd, BAR_1,
VK_BAR_CHAN_RD(vktty, from));
}
}
}
static int bcm_vk_tty_open(struct tty_struct *tty, struct file *file)
{
int card_status;
struct bcm_vk *vk;
struct bcm_vk_tty *vktty;
int index;
/* initialize the pointer in case something fails */
tty->driver_data = NULL;
vk = (struct bcm_vk *)dev_get_drvdata(tty->dev);
index = tty->index;
if (index >= BCM_VK_NUM_TTY)
return -EINVAL;
vktty = &vk->tty[index];
vktty->pid = task_pid_nr(current);
vktty->to_offset = TO_TTYK_BASE(index);
vktty->from_offset = FROM_TTYK_BASE(index);
/* Do not allow tty device to be opened if tty on card not ready */
card_status = vkread32(vk, BAR_0, BAR_CARD_STATUS);
if (BCM_VK_INTF_IS_DOWN(card_status) || ((card_status & BIT(index)) == 0))
return -EBUSY;
/*
* Get shadow registers of the buffer sizes and the "to" write offset
* and "from" read offset
*/
vktty->to_size = vkread32(vk, BAR_1, VK_BAR_CHAN_SIZE(vktty, to));
vktty->wr = vkread32(vk, BAR_1, VK_BAR_CHAN_WR(vktty, to));
vktty->from_size = vkread32(vk, BAR_1, VK_BAR_CHAN_SIZE(vktty, from));
vktty->rd = vkread32(vk, BAR_1, VK_BAR_CHAN_RD(vktty, from));
vktty->is_opened = true;
if (tty->count == 1 && !vktty->irq_enabled) {
timer_setup(&vk->serial_timer, bcm_vk_tty_poll, 0);
mod_timer(&vk->serial_timer, jiffies + SERIAL_TIMER_VALUE);
}
return 0;
}
static void bcm_vk_tty_close(struct tty_struct *tty, struct file *file)
{
struct bcm_vk *vk = dev_get_drvdata(tty->dev);
if (tty->index >= BCM_VK_NUM_TTY)
return;
vk->tty[tty->index].is_opened = false;
if (tty->count == 1)
del_timer_sync(&vk->serial_timer);
}
static void bcm_vk_tty_doorbell(struct bcm_vk *vk, u32 db_val)
{
vkwrite32(vk, db_val, BAR_0,
VK_BAR0_REGSEG_DB_BASE + VK_BAR0_REGSEG_TTY_DB_OFFSET);
}
static int bcm_vk_tty_write(struct tty_struct *tty,
const unsigned char *buffer,
int count)
{
int index;
struct bcm_vk *vk;
struct bcm_vk_tty *vktty;
int i;
index = tty->index;
vk = dev_get_drvdata(tty->dev);
vktty = &vk->tty[index];
/* Simple write each byte to circular buffer */
for (i = 0; i < count; i++) {
vkwrite8(vk, buffer[i], BAR_1,
VK_BAR_CHAN_DATA(vktty, to, vktty->wr));
vktty->wr++;
if (vktty->wr >= vktty->to_size)
vktty->wr = 0;
}
/* Update write offset from shadow register to card */
vkwrite32(vk, vktty->wr, BAR_1, VK_BAR_CHAN_WR(vktty, to));
bcm_vk_tty_doorbell(vk, 0);
return count;
}
static unsigned int bcm_vk_tty_write_room(struct tty_struct *tty)
{
struct bcm_vk *vk = dev_get_drvdata(tty->dev);
return vk->tty[tty->index].to_size - 1;
}
static const struct tty_operations serial_ops = {
.open = bcm_vk_tty_open,
.close = bcm_vk_tty_close,
.write = bcm_vk_tty_write,
.write_room = bcm_vk_tty_write_room,
};
int bcm_vk_tty_init(struct bcm_vk *vk, char *name)
{
int i;
int err;
struct tty_driver *tty_drv;
struct device *dev = &vk->pdev->dev;
tty_drv = tty_alloc_driver
(BCM_VK_NUM_TTY,
TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV);
if (IS_ERR(tty_drv))
return PTR_ERR(tty_drv);
/* Save struct tty_driver for uninstalling the device */
vk->tty_drv = tty_drv;
/* initialize the tty driver */
tty_drv->driver_name = KBUILD_MODNAME;
tty_drv->name = kstrdup(name, GFP_KERNEL);
if (!tty_drv->name) {
err = -ENOMEM;
goto err_tty_driver_kref_put;
}
tty_drv->type = TTY_DRIVER_TYPE_SERIAL;
tty_drv->subtype = SERIAL_TYPE_NORMAL;
tty_drv->init_termios = tty_std_termios;
tty_set_operations(tty_drv, &serial_ops);
/* register the tty driver */
err = tty_register_driver(tty_drv);
if (err) {
dev_err(dev, "tty_register_driver failed\n");
goto err_kfree_tty_name;
}
for (i = 0; i < BCM_VK_NUM_TTY; i++) {
struct device *tty_dev;
tty_port_init(&vk->tty[i].port);
tty_dev = tty_port_register_device_attr(&vk->tty[i].port,
tty_drv, i, dev, vk,
NULL);
if (IS_ERR(tty_dev)) {
err = PTR_ERR(tty_dev);
goto unwind;
}
vk->tty[i].is_opened = false;
}
INIT_WORK(&vk->tty_wq_work, bcm_vk_tty_wq_handler);
vk->tty_wq_thread = create_singlethread_workqueue("tty");
if (!vk->tty_wq_thread) {
dev_err(dev, "Fail to create tty workqueue thread\n");
err = -ENOMEM;
goto unwind;
}
return 0;
unwind:
while (--i >= 0)
tty_port_unregister_device(&vk->tty[i].port, tty_drv, i);
tty_unregister_driver(tty_drv);
err_kfree_tty_name:
kfree(tty_drv->name);
tty_drv->name = NULL;
err_tty_driver_kref_put:
tty_driver_kref_put(tty_drv);
return err;
}
void bcm_vk_tty_exit(struct bcm_vk *vk)
{
int i;
del_timer_sync(&vk->serial_timer);
for (i = 0; i < BCM_VK_NUM_TTY; ++i) {
tty_port_unregister_device(&vk->tty[i].port,
vk->tty_drv,
i);
tty_port_destroy(&vk->tty[i].port);
}
tty_unregister_driver(vk->tty_drv);
kfree(vk->tty_drv->name);
vk->tty_drv->name = NULL;
tty_driver_kref_put(vk->tty_drv);
}
void bcm_vk_tty_terminate_tty_user(struct bcm_vk *vk)
{
struct bcm_vk_tty *vktty;
int i;
for (i = 0; i < BCM_VK_NUM_TTY; ++i) {
vktty = &vk->tty[i];
if (vktty->pid)
kill_pid(find_vpid(vktty->pid), SIGKILL, 1);
}
}
void bcm_vk_tty_wq_exit(struct bcm_vk *vk)
{
cancel_work_sync(&vk->tty_wq_work);
destroy_workqueue(vk->tty_wq_thread);
}