// 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 #include #include #include #include #include #include #include #include #include #include #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; u8 txbuf[BUFSIZE], rxbuf[BUFSIZE]; }; static bool needs_escape(u8 c) { return c == BYTE_ESC || c == BYTE_FRAME; } static unsigned int next_chunk_len(struct mctp_serial *dev) { unsigned 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 ssize_t write_chunk(struct mctp_serial *dev, u8 *buf, size_t 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 long flags; ssize_t txlen; unsigned int len; u8 c, buf[3]; 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, u8 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, u8 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, u8 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; size_t 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 = mctp_register_netdev(ndev, NULL, MCTP_PHYS_BINDING_SERIAL); 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; mctp_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 "); MODULE_DESCRIPTION("MCTP Serial transport"); #if IS_ENABLED(CONFIG_MCTP_SERIAL_TEST) #include #define MAX_CHUNKS 6 struct test_chunk_tx { u8 input_len; u8 input[MCTP_SERIAL_MTU]; u8 chunks[MAX_CHUNKS]; }; static void test_next_chunk_len(struct kunit *test) { struct mctp_serial devx; struct mctp_serial *dev = &devx; int next; const struct test_chunk_tx *params = test->param_value; memset(dev, 0x0, sizeof(*dev)); memcpy(dev->txbuf, params->input, params->input_len); dev->txlen = params->input_len; for (size_t i = 0; i < MAX_CHUNKS; i++) { next = next_chunk_len(dev); dev->txpos += next; KUNIT_EXPECT_EQ(test, next, params->chunks[i]); if (next == 0) { KUNIT_EXPECT_EQ(test, dev->txpos, dev->txlen); return; } } KUNIT_FAIL_AND_ABORT(test, "Ran out of chunks"); } static struct test_chunk_tx chunk_tx_tests[] = { { .input_len = 5, .input = { 0x00, 0x11, 0x22, 0x7e, 0x80 }, .chunks = { 3, 1, 1, 0}, }, { .input_len = 5, .input = { 0x00, 0x11, 0x22, 0x7e, 0x7d }, .chunks = { 3, 1, 1, 0}, }, { .input_len = 3, .input = { 0x7e, 0x11, 0x22, }, .chunks = { 1, 2, 0}, }, { .input_len = 3, .input = { 0x7e, 0x7e, 0x7d, }, .chunks = { 1, 1, 1, 0}, }, { .input_len = 4, .input = { 0x7e, 0x7e, 0x00, 0x7d, }, .chunks = { 1, 1, 1, 1, 0}, }, { .input_len = 6, .input = { 0x7e, 0x7e, 0x00, 0x7d, 0x10, 0x10}, .chunks = { 1, 1, 1, 1, 2, 0}, }, { .input_len = 1, .input = { 0x7e }, .chunks = { 1, 0 }, }, { .input_len = 1, .input = { 0x80 }, .chunks = { 1, 0 }, }, { .input_len = 3, .input = { 0x80, 0x80, 0x00 }, .chunks = { 3, 0 }, }, { .input_len = 7, .input = { 0x01, 0x00, 0x08, 0xc8, 0x00, 0x80, 0x02 }, .chunks = { 7, 0 }, }, { .input_len = 7, .input = { 0x01, 0x00, 0x08, 0xc8, 0x7e, 0x80, 0x02 }, .chunks = { 4, 1, 2, 0 }, }, }; KUNIT_ARRAY_PARAM(chunk_tx, chunk_tx_tests, NULL); static struct kunit_case mctp_serial_test_cases[] = { KUNIT_CASE_PARAM(test_next_chunk_len, chunk_tx_gen_params), }; static struct kunit_suite mctp_serial_test_suite = { .name = "mctp_serial", .test_cases = mctp_serial_test_cases, }; kunit_test_suite(mctp_serial_test_suite); #endif /* CONFIG_MCTP_SERIAL_TEST */