| // 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 | 
 |  | 
 | #define FCS_INIT		0xffff | 
 |  | 
 | 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 < dev->txlen; i++) { | 
 | 		if (needs_escape(dev->txbuf[dev->txpos + i])) | 
 | 			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(FCS_INIT, 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(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(FCS_INIT, 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 u8 *c, | 
 | 					const u8 *f, size_t 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 void mctp_serial_uninit(struct net_device *ndev) | 
 | { | 
 | 	struct mctp_serial *dev = netdev_priv(ndev); | 
 |  | 
 | 	cancel_work_sync(&dev->tx_work); | 
 | } | 
 |  | 
 | static const struct net_device_ops mctp_serial_netdev_ops = { | 
 | 	.ndo_start_xmit = mctp_serial_tx, | 
 | 	.ndo_uninit = mctp_serial_uninit, | 
 | }; | 
 |  | 
 | 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); | 
 | 	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"); |