| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Management Component Transport Protocol (MCTP) - serial transport |
| * binding. This driver is an implementation of the DMTF specificiation |
| * "DSP0253 - Management Component Transport Protocol (MCTP) Serial Transport |
| * Binding", available at: |
| * |
| * https://www.dmtf.org/sites/default/files/standards/documents/DSP0253_1.0.0.pdf |
| * |
| * This driver provides DSP0253-type MCTP-over-serial transport using a Linux |
| * tty device, by setting the N_MCTP line discipline on the tty. |
| * |
| * Copyright (c) 2021 Code Construct |
| */ |
| |
| #include <linux/idr.h> |
| #include <linux/if_arp.h> |
| #include <linux/module.h> |
| #include <linux/skbuff.h> |
| #include <linux/tty.h> |
| #include <linux/workqueue.h> |
| #include <linux/crc-ccitt.h> |
| |
| #include <linux/mctp.h> |
| #include <net/mctp.h> |
| #include <net/pkt_sched.h> |
| |
| #define MCTP_SERIAL_MTU 68 /* base mtu (64) + mctp header */ |
| #define MCTP_SERIAL_FRAME_MTU (MCTP_SERIAL_MTU + 6) /* + serial framing */ |
| |
| #define MCTP_SERIAL_VERSION 0x1 /* DSP0253 defines a single version: 1 */ |
| |
| #define BUFSIZE MCTP_SERIAL_FRAME_MTU |
| |
| #define BYTE_FRAME 0x7e |
| #define BYTE_ESC 0x7d |
| |
| static DEFINE_IDA(mctp_serial_ida); |
| |
| enum mctp_serial_state { |
| STATE_IDLE, |
| STATE_START, |
| STATE_HEADER, |
| STATE_DATA, |
| STATE_ESCAPE, |
| STATE_TRAILER, |
| STATE_DONE, |
| STATE_ERR, |
| }; |
| |
| struct mctp_serial { |
| struct net_device *netdev; |
| struct tty_struct *tty; |
| |
| int idx; |
| |
| /* protects our rx & tx state machines; held during both paths */ |
| spinlock_t lock; |
| |
| struct work_struct tx_work; |
| enum mctp_serial_state txstate, rxstate; |
| u16 txfcs, rxfcs, rxfcs_rcvd; |
| unsigned int txlen, rxlen; |
| unsigned int txpos, rxpos; |
| unsigned char txbuf[BUFSIZE], |
| rxbuf[BUFSIZE]; |
| }; |
| |
| static bool needs_escape(unsigned char c) |
| { |
| return c == BYTE_ESC || c == BYTE_FRAME; |
| } |
| |
| static int next_chunk_len(struct mctp_serial *dev) |
| { |
| int i; |
| |
| /* either we have no bytes to send ... */ |
| if (dev->txpos == dev->txlen) |
| return 0; |
| |
| /* ... or the next byte to send is an escaped byte; requiring a |
| * single-byte chunk... |
| */ |
| if (needs_escape(dev->txbuf[dev->txpos])) |
| return 1; |
| |
| /* ... or we have one or more bytes up to the next escape - this chunk |
| * will be those non-escaped bytes, and does not include the escaped |
| * byte. |
| */ |
| for (i = 1; i + dev->txpos + 1 < dev->txlen; i++) { |
| if (needs_escape(dev->txbuf[dev->txpos + i + 1])) |
| break; |
| } |
| |
| return i; |
| } |
| |
| static int write_chunk(struct mctp_serial *dev, unsigned char *buf, int len) |
| { |
| return dev->tty->ops->write(dev->tty, buf, len); |
| } |
| |
| static void mctp_serial_tx_work(struct work_struct *work) |
| { |
| struct mctp_serial *dev = container_of(work, struct mctp_serial, |
| tx_work); |
| unsigned char c, buf[3]; |
| unsigned long flags; |
| int len, txlen; |
| |
| spin_lock_irqsave(&dev->lock, flags); |
| |
| /* txstate represents the next thing to send */ |
| switch (dev->txstate) { |
| case STATE_START: |
| dev->txpos = 0; |
| fallthrough; |
| case STATE_HEADER: |
| buf[0] = BYTE_FRAME; |
| buf[1] = MCTP_SERIAL_VERSION; |
| buf[2] = dev->txlen; |
| |
| if (!dev->txpos) |
| dev->txfcs = crc_ccitt(0, buf + 1, 2); |
| |
| txlen = write_chunk(dev, buf + dev->txpos, 3 - dev->txpos); |
| if (txlen <= 0) { |
| dev->txstate = STATE_ERR; |
| } else { |
| dev->txpos += txlen; |
| if (dev->txpos == 3) { |
| dev->txstate = STATE_DATA; |
| dev->txpos = 0; |
| } |
| } |
| break; |
| |
| case STATE_ESCAPE: |
| buf[0] = dev->txbuf[dev->txpos] & ~0x20; |
| txlen = write_chunk(dev, buf, 1); |
| if (txlen <= 0) { |
| dev->txstate = STATE_ERR; |
| } else { |
| dev->txpos += txlen; |
| if (dev->txpos == dev->txlen) { |
| dev->txstate = STATE_TRAILER; |
| dev->txpos = 0; |
| } |
| } |
| |
| break; |
| |
| case STATE_DATA: |
| len = next_chunk_len(dev); |
| if (len) { |
| c = dev->txbuf[dev->txpos]; |
| if (len == 1 && needs_escape(c)) { |
| buf[0] = BYTE_ESC; |
| buf[1] = c & ~0x20; |
| dev->txfcs = crc_ccitt_byte(dev->txfcs, c); |
| txlen = write_chunk(dev, buf, 2); |
| if (txlen == 2) |
| dev->txpos++; |
| else if (txlen == 1) |
| dev->txstate = STATE_ESCAPE; |
| else |
| dev->txstate = STATE_ERR; |
| } else { |
| txlen = write_chunk(dev, |
| dev->txbuf + dev->txpos, |
| len); |
| if (txlen <= 0) { |
| dev->txstate = STATE_ERR; |
| } else { |
| dev->txfcs = crc_ccitt(dev->txfcs, |
| dev->txbuf + |
| dev->txpos, |
| txlen); |
| dev->txpos += txlen; |
| } |
| } |
| if (dev->txstate == STATE_DATA && |
| dev->txpos == dev->txlen) { |
| dev->txstate = STATE_TRAILER; |
| dev->txpos = 0; |
| } |
| break; |
| } |
| dev->txstate = STATE_TRAILER; |
| dev->txpos = 0; |
| fallthrough; |
| |
| case STATE_TRAILER: |
| buf[0] = dev->txfcs >> 8; |
| buf[1] = dev->txfcs & 0xff; |
| buf[2] = BYTE_FRAME; |
| txlen = write_chunk(dev, buf + dev->txpos, 3 - dev->txpos); |
| if (txlen <= 0) { |
| dev->txstate = STATE_ERR; |
| } else { |
| dev->txpos += txlen; |
| if (dev->txpos == 3) { |
| dev->txstate = STATE_DONE; |
| dev->txpos = 0; |
| } |
| } |
| break; |
| default: |
| netdev_err_once(dev->netdev, "invalid tx state %d\n", |
| dev->txstate); |
| } |
| |
| if (dev->txstate == STATE_DONE) { |
| dev->netdev->stats.tx_packets++; |
| dev->netdev->stats.tx_bytes += dev->txlen; |
| dev->txlen = 0; |
| dev->txpos = 0; |
| clear_bit(TTY_DO_WRITE_WAKEUP, &dev->tty->flags); |
| dev->txstate = STATE_IDLE; |
| spin_unlock_irqrestore(&dev->lock, flags); |
| |
| netif_wake_queue(dev->netdev); |
| } else { |
| spin_unlock_irqrestore(&dev->lock, flags); |
| } |
| } |
| |
| static netdev_tx_t mctp_serial_tx(struct sk_buff *skb, struct net_device *ndev) |
| { |
| struct mctp_serial *dev = netdev_priv(ndev); |
| unsigned long flags; |
| |
| WARN_ON(dev->txstate != STATE_IDLE); |
| |
| if (skb->len > MCTP_SERIAL_MTU) { |
| dev->netdev->stats.tx_dropped++; |
| goto out; |
| } |
| |
| spin_lock_irqsave(&dev->lock, flags); |
| netif_stop_queue(dev->netdev); |
| skb_copy_bits(skb, 0, dev->txbuf, skb->len); |
| dev->txpos = 0; |
| dev->txlen = skb->len; |
| dev->txstate = STATE_START; |
| spin_unlock_irqrestore(&dev->lock, flags); |
| |
| set_bit(TTY_DO_WRITE_WAKEUP, &dev->tty->flags); |
| schedule_work(&dev->tx_work); |
| |
| out: |
| kfree_skb(skb); |
| return NETDEV_TX_OK; |
| } |
| |
| static void mctp_serial_tty_write_wakeup(struct tty_struct *tty) |
| { |
| struct mctp_serial *dev = tty->disc_data; |
| |
| schedule_work(&dev->tx_work); |
| } |
| |
| static void mctp_serial_rx(struct mctp_serial *dev) |
| { |
| struct mctp_skb_cb *cb; |
| struct sk_buff *skb; |
| |
| if (dev->rxfcs != dev->rxfcs_rcvd) { |
| dev->netdev->stats.rx_dropped++; |
| dev->netdev->stats.rx_crc_errors++; |
| return; |
| } |
| |
| skb = netdev_alloc_skb(dev->netdev, dev->rxlen); |
| if (!skb) { |
| dev->netdev->stats.rx_dropped++; |
| return; |
| } |
| |
| skb->protocol = htons(ETH_P_MCTP); |
| skb_put_data(skb, dev->rxbuf, dev->rxlen); |
| skb_reset_network_header(skb); |
| |
| cb = __mctp_cb(skb); |
| cb->halen = 0; |
| |
| netif_rx_ni(skb); |
| dev->netdev->stats.rx_packets++; |
| dev->netdev->stats.rx_bytes += dev->rxlen; |
| } |
| |
| static void mctp_serial_push_header(struct mctp_serial *dev, unsigned char c) |
| { |
| switch (dev->rxpos) { |
| case 0: |
| if (c == BYTE_FRAME) |
| dev->rxpos++; |
| else |
| dev->rxstate = STATE_ERR; |
| break; |
| case 1: |
| if (c == MCTP_SERIAL_VERSION) { |
| dev->rxpos++; |
| dev->rxfcs = crc_ccitt_byte(0, c); |
| } else { |
| dev->rxstate = STATE_ERR; |
| } |
| break; |
| case 2: |
| if (c > MCTP_SERIAL_FRAME_MTU) { |
| dev->rxstate = STATE_ERR; |
| } else { |
| dev->rxlen = c; |
| dev->rxpos = 0; |
| dev->rxstate = STATE_DATA; |
| dev->rxfcs = crc_ccitt_byte(dev->rxfcs, c); |
| } |
| break; |
| } |
| } |
| |
| static void mctp_serial_push_trailer(struct mctp_serial *dev, unsigned char c) |
| { |
| switch (dev->rxpos) { |
| case 0: |
| dev->rxfcs_rcvd = c << 8; |
| dev->rxpos++; |
| break; |
| case 1: |
| dev->rxfcs_rcvd |= c; |
| dev->rxpos++; |
| break; |
| case 2: |
| if (c != BYTE_FRAME) { |
| dev->rxstate = STATE_ERR; |
| } else { |
| mctp_serial_rx(dev); |
| dev->rxlen = 0; |
| dev->rxpos = 0; |
| dev->rxstate = STATE_IDLE; |
| } |
| break; |
| } |
| } |
| |
| static void mctp_serial_push(struct mctp_serial *dev, unsigned char c) |
| { |
| switch (dev->rxstate) { |
| case STATE_IDLE: |
| dev->rxstate = STATE_HEADER; |
| fallthrough; |
| case STATE_HEADER: |
| mctp_serial_push_header(dev, c); |
| break; |
| |
| case STATE_ESCAPE: |
| c |= 0x20; |
| fallthrough; |
| case STATE_DATA: |
| if (dev->rxstate != STATE_ESCAPE && c == BYTE_ESC) { |
| dev->rxstate = STATE_ESCAPE; |
| } else { |
| dev->rxfcs = crc_ccitt_byte(dev->rxfcs, c); |
| dev->rxbuf[dev->rxpos] = c; |
| dev->rxpos++; |
| dev->rxstate = STATE_DATA; |
| if (dev->rxpos == dev->rxlen) { |
| dev->rxpos = 0; |
| dev->rxstate = STATE_TRAILER; |
| } |
| } |
| break; |
| |
| case STATE_TRAILER: |
| mctp_serial_push_trailer(dev, c); |
| break; |
| |
| case STATE_ERR: |
| if (c == BYTE_FRAME) |
| dev->rxstate = STATE_IDLE; |
| break; |
| |
| default: |
| netdev_err_once(dev->netdev, "invalid rx state %d\n", |
| dev->rxstate); |
| } |
| } |
| |
| static void mctp_serial_tty_receive_buf(struct tty_struct *tty, |
| const unsigned char *c, |
| const char *f, int len) |
| { |
| struct mctp_serial *dev = tty->disc_data; |
| int i; |
| |
| if (!netif_running(dev->netdev)) |
| return; |
| |
| /* we don't (currently) use the flag bytes, just data. */ |
| for (i = 0; i < len; i++) |
| mctp_serial_push(dev, c[i]); |
| } |
| |
| static const struct net_device_ops mctp_serial_netdev_ops = { |
| .ndo_start_xmit = mctp_serial_tx, |
| }; |
| |
| static void mctp_serial_setup(struct net_device *ndev) |
| { |
| ndev->type = ARPHRD_MCTP; |
| |
| /* we limit at the fixed MTU, which is also the MCTP-standard |
| * baseline MTU, so is also our minimum |
| */ |
| ndev->mtu = MCTP_SERIAL_MTU; |
| ndev->max_mtu = MCTP_SERIAL_MTU; |
| ndev->min_mtu = MCTP_SERIAL_MTU; |
| |
| ndev->hard_header_len = 0; |
| ndev->addr_len = 0; |
| ndev->tx_queue_len = DEFAULT_TX_QUEUE_LEN; |
| ndev->flags = IFF_NOARP; |
| ndev->netdev_ops = &mctp_serial_netdev_ops; |
| ndev->needs_free_netdev = true; |
| } |
| |
| static int mctp_serial_open(struct tty_struct *tty) |
| { |
| struct mctp_serial *dev; |
| struct net_device *ndev; |
| char name[32]; |
| int idx, rc; |
| |
| if (!capable(CAP_NET_ADMIN)) |
| return -EPERM; |
| |
| if (!tty->ops->write) |
| return -EOPNOTSUPP; |
| |
| idx = ida_alloc(&mctp_serial_ida, GFP_KERNEL); |
| if (idx < 0) |
| return idx; |
| |
| snprintf(name, sizeof(name), "mctpserial%d", idx); |
| ndev = alloc_netdev(sizeof(*dev), name, NET_NAME_ENUM, |
| mctp_serial_setup); |
| if (!ndev) { |
| rc = -ENOMEM; |
| goto free_ida; |
| } |
| |
| dev = netdev_priv(ndev); |
| dev->idx = idx; |
| dev->tty = tty; |
| dev->netdev = ndev; |
| dev->txstate = STATE_IDLE; |
| dev->rxstate = STATE_IDLE; |
| spin_lock_init(&dev->lock); |
| INIT_WORK(&dev->tx_work, mctp_serial_tx_work); |
| |
| rc = register_netdev(ndev); |
| if (rc) |
| goto free_netdev; |
| |
| tty->receive_room = 64 * 1024; |
| tty->disc_data = dev; |
| |
| return 0; |
| |
| free_netdev: |
| free_netdev(ndev); |
| |
| free_ida: |
| ida_free(&mctp_serial_ida, idx); |
| return rc; |
| } |
| |
| static void mctp_serial_close(struct tty_struct *tty) |
| { |
| struct mctp_serial *dev = tty->disc_data; |
| int idx = dev->idx; |
| |
| unregister_netdev(dev->netdev); |
| cancel_work_sync(&dev->tx_work); |
| ida_free(&mctp_serial_ida, idx); |
| } |
| |
| static struct tty_ldisc_ops mctp_ldisc = { |
| .owner = THIS_MODULE, |
| .num = N_MCTP, |
| .name = "mctp", |
| .open = mctp_serial_open, |
| .close = mctp_serial_close, |
| .receive_buf = mctp_serial_tty_receive_buf, |
| .write_wakeup = mctp_serial_tty_write_wakeup, |
| }; |
| |
| static int __init mctp_serial_init(void) |
| { |
| return tty_register_ldisc(&mctp_ldisc); |
| } |
| |
| static void __exit mctp_serial_exit(void) |
| { |
| tty_unregister_ldisc(&mctp_ldisc); |
| } |
| |
| module_init(mctp_serial_init); |
| module_exit(mctp_serial_exit); |
| |
| MODULE_LICENSE("GPL v2"); |
| MODULE_AUTHOR("Jeremy Kerr <jk@codeconstruct.com.au>"); |
| MODULE_DESCRIPTION("MCTP Serial transport"); |