summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--drivers/net/mctp/Kconfig16
-rw-r--r--drivers/net/mctp/Makefile1
-rw-r--r--drivers/net/mctp/mctp-usb.c348
-rw-r--r--drivers/net/mctp/mctp-usblib-test.c412
-rw-r--r--drivers/net/mctp/mctp-usblib.c625
-rw-r--r--include/linux/usb/mctp-usb.h88
6 files changed, 1336 insertions, 154 deletions
diff --git a/drivers/net/mctp/Kconfig b/drivers/net/mctp/Kconfig
index cf325ab0b1ef..c40ac9c665b7 100644
--- a/drivers/net/mctp/Kconfig
+++ b/drivers/net/mctp/Kconfig
@@ -47,9 +47,25 @@ config MCTP_TRANSPORT_I3C
A MCTP protocol network device is created for each I3C bus
having a "mctp-controller" devicetree property.
+config MCTP_TRANSPORT_USBLIB
+ tristate "MCTP over USB common library"
+ depends on USB
+ help
+ Common protocol handling functions for MCTP-over-USB transport
+ implementations, suitable for use in either host- or gadget-side
+ transport driver
+
+ This will be automatically enabled by the transport driver.
+
+config MCTP_TRANSPORT_USBLIB_TEST
+ bool "MCTP usblib tests" if !KUNIT_ALL_TESTS
+ depends on MCTP_TRANSPORT_USBLIB=y && KUNIT=y
+ default KUNIT_ALL_TESTS
+
config MCTP_TRANSPORT_USB
tristate "MCTP USB transport"
depends on USB
+ select MCTP_TRANSPORT_USBLIB
help
Provides a driver to access MCTP devices over USB transport,
defined by DMTF specification DSP0283.
diff --git a/drivers/net/mctp/Makefile b/drivers/net/mctp/Makefile
index c36006849a1e..c870b62d3f1c 100644
--- a/drivers/net/mctp/Makefile
+++ b/drivers/net/mctp/Makefile
@@ -2,3 +2,4 @@ obj-$(CONFIG_MCTP_SERIAL) += mctp-serial.o
obj-$(CONFIG_MCTP_TRANSPORT_I2C) += mctp-i2c.o
obj-$(CONFIG_MCTP_TRANSPORT_I3C) += mctp-i3c.o
obj-$(CONFIG_MCTP_TRANSPORT_USB) += mctp-usb.o
+obj-$(CONFIG_MCTP_TRANSPORT_USBLIB) += mctp-usblib.o
diff --git a/drivers/net/mctp/mctp-usb.c b/drivers/net/mctp/mctp-usb.c
index fade65f2f269..542a570c76cc 100644
--- a/drivers/net/mctp/mctp-usb.c
+++ b/drivers/net/mctp/mctp-usb.c
@@ -3,9 +3,9 @@
* mctp-usb.c - MCTP-over-USB (DMTF DSP0283) transport binding driver.
*
* DSP0283 is available at:
- * https://www.dmtf.org/sites/default/files/standards/documents/DSP0283_1.0.1.pdf
+ * https://www.dmtf.org/sites/default/files/standards/documents/DSP0283_1.1.0.pdf
*
- * Copyright (C) 2024-2025 Code Construct Pty Ltd
+ * Copyright (C) 2024-2026 Code Construct Pty Ltd
*/
#include <linux/module.h>
@@ -22,95 +22,115 @@
struct mctp_usb {
struct usb_device *usbdev;
struct usb_interface *intf;
+ bool span;
struct net_device *netdev;
u8 ep_in;
u8 ep_out;
- struct urb *tx_urb;
+ struct mctp_usblib_rx rx;
struct urb *rx_urb;
+ int in_err_count;
+ int in_err_orig;
+ bool clear_halt;
/* enforces atomic access to rx_stopped and requeuing the retry work */
spinlock_t rx_lock;
bool rx_stopped;
struct delayed_work rx_retry_work;
+
+ struct mctp_usblib_tx tx;
+ struct usb_anchor tx_anchor;
+ /* serialises tx_qmem updates to netdev queue states */
+ spinlock_t tx_qmem_lock;
+ int tx_qmem;
+};
+
+enum {
+ MCTP_USB_SUBCLASS_BASE = 0x00,
+ MCTP_USB_SUBCLASS_SPAN = 0x02,
};
+/* We use a total-size limit for outstanding URBs, as the transfer counts
+ * may vary a lot between spanning- and non-spanning modes. In spanning mode,
+ * this will allow for a couple of max-sized transfers to be in flight. In
+ * non-spanning mode, 32.
+ *
+ * We want to avoid disabling the tx queue if possible; doing so will end up
+ * requeueing to gso_skb, and we only dequeue from that one skb at a time,
+ * so can no longer perform transfer packing.
+ */
+static const unsigned int TX_QMEM_MAX = 16384;
+
static void mctp_usb_out_complete(struct urb *urb)
{
- struct sk_buff *skb = urb->context;
- struct net_device *netdev = skb->dev;
- int status;
+ struct mctp_usblib_tx_ctx *tx_ctx = urb->context;
+ struct mctp_usb *mctp_usb = mctp_usblib_tx_ctx_priv(tx_ctx);
+ unsigned int len = urb->transfer_buffer_length;
+ struct net_device *netdev = mctp_usb->netdev;
+ unsigned long flags;
- status = urb->status;
+ mctp_usblib_tx_send_complete(tx_ctx, netdev, urb->status == 0);
- switch (status) {
- case -ENOENT:
- case -ECONNRESET:
- case -ESHUTDOWN:
- case -EPROTO:
- dev_dstats_tx_dropped(netdev);
- break;
- case 0:
- dev_dstats_tx_add(netdev, skb->len);
- netif_wake_queue(netdev);
- consume_skb(skb);
- return;
- default:
- netdev_dbg(netdev, "unexpected tx urb status: %d\n", status);
- dev_dstats_tx_dropped(netdev);
- }
+ usb_free_urb(urb);
- kfree_skb(skb);
+ spin_lock_irqsave(&mctp_usb->tx_qmem_lock, flags);
+ mctp_usb->tx_qmem -= len;
+ if (mctp_usb->tx_qmem < TX_QMEM_MAX && netif_running(netdev))
+ netif_wake_queue(netdev);
+ spin_unlock_irqrestore(&mctp_usb->tx_qmem_lock, flags);
}
-static netdev_tx_t mctp_usb_start_xmit(struct sk_buff *skb,
- struct net_device *dev)
+static int mctp_usb_tx_send(struct mctp_usblib_tx_ctx *tx_ctx,
+ void *data, size_t len)
{
- struct mctp_usb *mctp_usb = netdev_priv(dev);
- struct mctp_usb_hdr *hdr;
- unsigned int plen;
+ struct mctp_usb *mctp_usb = mctp_usblib_tx_ctx_priv(tx_ctx);
+ unsigned long flags;
struct urb *urb;
int rc;
- plen = skb->len;
-
- if (plen + sizeof(*hdr) > MCTP_USB_XFER_SIZE)
- goto err_drop;
-
- rc = skb_cow_head(skb, sizeof(*hdr));
- if (rc)
- goto err_drop;
-
- hdr = skb_push(skb, sizeof(*hdr));
- if (!hdr)
- goto err_drop;
-
- hdr->id = cpu_to_be16(MCTP_USB_DMTF_ID);
- hdr->rsvd = 0;
- hdr->len = plen + sizeof(*hdr);
-
- urb = mctp_usb->tx_urb;
+ urb = usb_alloc_urb(0, GFP_ATOMIC);
+ if (!urb)
+ return -ENOMEM;
usb_fill_bulk_urb(urb, mctp_usb->usbdev,
usb_sndbulkpipe(mctp_usb->usbdev, mctp_usb->ep_out),
- skb->data, skb->len,
- mctp_usb_out_complete, skb);
+ data, len, mctp_usb_out_complete, tx_ctx);
+
+ if (mctp_usb->span)
+ urb->transfer_flags |= URB_ZERO_PACKET;
+
+ usb_anchor_urb(urb, &mctp_usb->tx_anchor);
- /* Stops TX queue first to prevent race condition with URB complete */
- netif_stop_queue(dev);
rc = usb_submit_urb(urb, GFP_ATOMIC);
if (rc) {
- netif_wake_queue(dev);
- goto err_drop;
+ netdev_dbg(mctp_usb->netdev, "TX urb submit failed, %d\n", rc);
+ usb_unanchor_urb(urb);
+ usb_free_urb(urb);
+ } else {
+ spin_lock_irqsave(&mctp_usb->tx_qmem_lock, flags);
+ mctp_usb->tx_qmem += len;
+ if (mctp_usb->tx_qmem >= TX_QMEM_MAX)
+ netif_stop_queue(mctp_usb->netdev);
+ spin_unlock_irqrestore(&mctp_usb->tx_qmem_lock, flags);
}
- return NETDEV_TX_OK;
+ return rc;
+}
+
+static const struct mctp_usblib_tx_ops tx_ops = {
+ .send = mctp_usb_tx_send,
+};
+
+static netdev_tx_t mctp_usb_start_xmit(struct sk_buff *skb,
+ struct net_device *dev)
+{
+ struct mctp_usb *mctp_usb = netdev_priv(dev);
+ bool more = netdev_xmit_more();
+
+ mctp_usblib_tx_push(dev, &mctp_usb->tx, skb, more);
-err_drop:
- dev_dstats_tx_dropped(dev);
- kfree_skb(skb);
return NETDEV_TX_OK;
}
@@ -125,24 +145,23 @@ static const unsigned long RX_RETRY_DELAY = HZ / 4;
static int mctp_usb_rx_queue(struct mctp_usb *mctp_usb, gfp_t gfp)
{
unsigned long flags;
- struct sk_buff *skb;
+ size_t len;
+ void *buf;
int rc;
- skb = __netdev_alloc_skb(mctp_usb->netdev, MCTP_USB_XFER_SIZE, gfp);
- if (!skb) {
- rc = -ENOMEM;
+ rc = mctp_usblib_rx_prepare(mctp_usb->netdev, &mctp_usb->rx,
+ &buf, &len, gfp);
+ if (rc)
goto err_retry;
- }
usb_fill_bulk_urb(mctp_usb->rx_urb, mctp_usb->usbdev,
usb_rcvbulkpipe(mctp_usb->usbdev, mctp_usb->ep_in),
- skb->data, MCTP_USB_XFER_SIZE,
- mctp_usb_in_complete, skb);
+ buf, len, mctp_usb_in_complete, mctp_usb);
rc = usb_submit_urb(mctp_usb->rx_urb, gfp);
if (rc) {
netdev_dbg(mctp_usb->netdev, "rx urb submit failure: %d\n", rc);
- kfree_skb(skb);
+ mctp_usblib_rx_cancel(&mctp_usb->rx);
if (rc == -ENOMEM)
goto err_retry;
}
@@ -157,14 +176,34 @@ err_retry:
return 0;
}
+static const unsigned int rx_err_max = 10;
+
+/* Returns -1 if we have hit excessive errors, zero otherwise. */
+static int mctp_usb_in_urb_err(struct mctp_usb *mctp_usb, int status,
+ bool stalled)
+{
+ mctp_usblib_rx_cancel(&mctp_usb->rx);
+
+ if (!mctp_usb->in_err_count++)
+ mctp_usb->in_err_orig = status;
+
+ if (mctp_usb->in_err_count >= rx_err_max) {
+ netdev_err(mctp_usb->netdev,
+ "excessive errors from%s IN EP, first: %d\n",
+ stalled ? " (stalled)" : "",
+ mctp_usb->in_err_orig);
+ return -1;
+ }
+
+ return 0;
+}
+
static void mctp_usb_in_complete(struct urb *urb)
{
- struct sk_buff *skb = urb->context;
- struct net_device *netdev = skb->dev;
- struct mctp_usb *mctp_usb = netdev_priv(netdev);
- struct mctp_skb_cb *cb;
- unsigned int len;
- int status;
+ struct mctp_usb *mctp_usb = urb->context;
+ struct net_device *netdev = mctp_usb->netdev;
+ unsigned long flags;
+ int rc, status;
status = urb->status;
@@ -172,80 +211,43 @@ static void mctp_usb_in_complete(struct urb *urb)
case -ENOENT:
case -ECONNRESET:
case -ESHUTDOWN:
- case -EPROTO:
- kfree_skb(skb);
- return;
- case 0:
- break;
- default:
- netdev_dbg(netdev, "unexpected rx urb status: %d\n", status);
- kfree_skb(skb);
+ /* device shutdown, don't resubmit */
+ mctp_usblib_rx_cancel(&mctp_usb->rx);
return;
- }
-
- len = urb->actual_length;
- __skb_put(skb, len);
-
- while (skb) {
- struct sk_buff *skb2 = NULL;
- struct mctp_usb_hdr *hdr;
- u8 pkt_len; /* length of MCTP packet, no USB header */
-
- skb_reset_mac_header(skb);
- hdr = skb_pull_data(skb, sizeof(*hdr));
- if (!hdr)
- break;
-
- if (be16_to_cpu(hdr->id) != MCTP_USB_DMTF_ID) {
- netdev_dbg(netdev, "rx: invalid id %04x\n",
- be16_to_cpu(hdr->id));
- break;
- }
-
- if (hdr->len <
- sizeof(struct mctp_hdr) + sizeof(struct mctp_usb_hdr)) {
- netdev_dbg(netdev, "rx: short packet (hdr) %d\n",
- hdr->len);
- break;
- }
-
- /* we know we have at least sizeof(struct mctp_usb_hdr) here */
- pkt_len = hdr->len - sizeof(struct mctp_usb_hdr);
- if (pkt_len > skb->len) {
- netdev_dbg(netdev,
- "rx: short packet (xfer) %d, actual %d\n",
- hdr->len, skb->len);
- break;
- }
-
- if (pkt_len < skb->len) {
- /* more packets may follow - clone to a new
- * skb to use on the next iteration
- */
- skb2 = skb_clone(skb, GFP_ATOMIC);
- if (skb2) {
- if (!skb_pull(skb2, pkt_len)) {
- kfree_skb(skb2);
- skb2 = NULL;
- }
- }
- skb_trim(skb, pkt_len);
- }
- dev_dstats_rx_add(netdev, skb->len);
+ case -EPIPE:
+ /* endpoint stall: clear halt, which will cause a resubmit */
+ rc = mctp_usb_in_urb_err(mctp_usb, status, true);
+ if (rc)
+ return;
+
+ mctp_usb->clear_halt = true;
+ spin_lock_irqsave(&mctp_usb->rx_lock, flags);
+ if (!mctp_usb->rx_stopped)
+ schedule_delayed_work(&mctp_usb->rx_retry_work,
+ RX_RETRY_DELAY);
+ spin_unlock_irqrestore(&mctp_usb->rx_lock, flags);
+ return;
- skb->protocol = htons(ETH_P_MCTP);
- skb_reset_network_header(skb);
- cb = __mctp_cb(skb);
- cb->halen = 0;
- netif_rx(skb);
+ default:
+ netdev_dbg(netdev, "unexpected rx urb status: %d\n", status);
+ fallthrough;
+ case -ETIME:
+ case -EPROTO:
+ case -EILSEQ:
+ case -EOVERFLOW:
+ /* possibly transient; record first failure, resubmit */
+ rc = mctp_usb_in_urb_err(mctp_usb, status, false);
+ if (rc)
+ return;
+ break;
- skb = skb2;
+ case 0:
+ mctp_usblib_rx_complete(netdev, &mctp_usb->rx, urb->actual_length);
+ mctp_usb->in_err_count = 0;
+ break;
}
- if (skb)
- kfree_skb(skb);
-
mctp_usb_rx_queue(mctp_usb, GFP_ATOMIC);
}
@@ -253,6 +255,30 @@ static void mctp_usb_rx_retry_work(struct work_struct *work)
{
struct mctp_usb *mctp_usb = container_of(work, struct mctp_usb,
rx_retry_work.work);
+ unsigned long flags;
+ int rc;
+
+ /* We are only called when rx completions are suspended */
+ if (mctp_usb->clear_halt) {
+ int pipe = usb_rcvbulkpipe(mctp_usb->usbdev, mctp_usb->ep_in);
+
+ rc = usb_clear_halt(mctp_usb->usbdev, pipe);
+ if (rc) {
+ netdev_err(mctp_usb->netdev,
+ "can't clear IN EP halt: %d\n", rc);
+
+ if (++mctp_usb->in_err_count >= rx_err_max)
+ return;
+
+ spin_lock_irqsave(&mctp_usb->rx_lock, flags);
+ if (!mctp_usb->rx_stopped)
+ schedule_delayed_work(&mctp_usb->rx_retry_work,
+ RX_RETRY_DELAY);
+ spin_unlock_irqrestore(&mctp_usb->rx_lock, flags);
+ return;
+ }
+ mctp_usb->clear_halt = false;
+ }
mctp_usb_rx_queue(mctp_usb, GFP_KERNEL);
}
@@ -262,6 +288,8 @@ static int mctp_usb_open(struct net_device *dev)
struct mctp_usb *mctp_usb = netdev_priv(dev);
WRITE_ONCE(mctp_usb->rx_stopped, false);
+ mctp_usb->clear_halt = false;
+ mctp_usb->in_err_count = 0;
netif_start_queue(dev);
@@ -284,7 +312,11 @@ static int mctp_usb_stop(struct net_device *dev)
flush_delayed_work(&mctp_usb->rx_retry_work);
usb_kill_urb(mctp_usb->rx_urb);
- usb_kill_urb(mctp_usb->tx_urb);
+
+ usb_kill_anchored_urbs(&mctp_usb->tx_anchor);
+
+ mctp_usblib_tx_cancel(&mctp_usb->tx, dev, SKB_DROP_REASON_DEV_READY);
+ mctp_usblib_rx_cancel(&mctp_usb->rx);
return 0;
}
@@ -301,7 +333,7 @@ static void mctp_usb_netdev_setup(struct net_device *dev)
dev->mtu = MCTP_USB_MTU_MIN;
dev->min_mtu = MCTP_USB_MTU_MIN;
- dev->max_mtu = MCTP_USB_MTU_MAX;
+ dev->max_mtu = MCTP_USB_1_0_MTU_MAX;
dev->hard_header_len = sizeof(struct mctp_usb_hdr);
dev->tx_queue_len = DEFAULT_TX_QUEUE_LEN;
@@ -317,6 +349,7 @@ static int mctp_usb_probe(struct usb_interface *intf,
struct usb_host_interface *iface_desc;
struct net_device *netdev;
struct mctp_usb *dev;
+ bool span;
int rc;
/* only one alternate */
@@ -328,6 +361,8 @@ static int mctp_usb_probe(struct usb_interface *intf,
return rc;
}
+ span = iface_desc->desc.bInterfaceSubClass == MCTP_USB_SUBCLASS_SPAN;
+
netdev = alloc_netdev(sizeof(*dev), "mctpusb%d", NET_NAME_ENUM,
mctp_usb_netdev_setup);
if (!netdev)
@@ -335,33 +370,46 @@ static int mctp_usb_probe(struct usb_interface *intf,
SET_NETDEV_DEV(netdev, &intf->dev);
dev = netdev_priv(netdev);
+ dev->span = span;
dev->netdev = netdev;
dev->usbdev = interface_to_usbdev(intf);
dev->intf = intf;
spin_lock_init(&dev->rx_lock);
+ if (dev->span)
+ netdev->max_mtu = MCTP_USB_1_1_MTU_MAX;
+ spin_lock_init(&dev->tx_qmem_lock);
usb_set_intfdata(intf, dev);
+ rc = mctp_usblib_rx_init(&dev->rx, le16_to_cpu(ep_in->wMaxPacketSize),
+ dev->span);
+ if (rc)
+ goto err_free_netdev;
+ mctp_usblib_tx_init(&dev->tx, &tx_ops, dev, dev->span);
+ init_usb_anchor(&dev->tx_anchor);
+
dev->ep_in = ep_in->bEndpointAddress;
dev->ep_out = ep_out->bEndpointAddress;
- dev->tx_urb = usb_alloc_urb(0, GFP_KERNEL);
dev->rx_urb = usb_alloc_urb(0, GFP_KERNEL);
- if (!dev->tx_urb || !dev->rx_urb) {
+ if (!dev->rx_urb) {
rc = -ENOMEM;
- goto err_free_urbs;
+ goto err_fini_rxtx;
}
INIT_DELAYED_WORK(&dev->rx_retry_work, mctp_usb_rx_retry_work);
rc = mctp_register_netdev(netdev, NULL, MCTP_PHYS_BINDING_USB);
if (rc)
- goto err_free_urbs;
+ goto err_free_urb;
return 0;
-err_free_urbs:
- usb_free_urb(dev->tx_urb);
+err_free_urb:
usb_free_urb(dev->rx_urb);
+err_fini_rxtx:
+ mctp_usblib_tx_fini(&dev->tx);
+ mctp_usblib_rx_fini(&dev->rx);
+err_free_netdev:
free_netdev(netdev);
return rc;
}
@@ -371,13 +419,15 @@ static void mctp_usb_disconnect(struct usb_interface *intf)
struct mctp_usb *dev = usb_get_intfdata(intf);
mctp_unregister_netdev(dev->netdev);
- usb_free_urb(dev->tx_urb);
+ mctp_usblib_rx_fini(&dev->rx);
+ mctp_usblib_tx_fini(&dev->tx);
usb_free_urb(dev->rx_urb);
free_netdev(dev->netdev);
}
static const struct usb_device_id mctp_usb_devices[] = {
- { USB_INTERFACE_INFO(USB_CLASS_MCTP, 0x0, 0x1) },
+ { USB_INTERFACE_INFO(USB_CLASS_MCTP, MCTP_USB_SUBCLASS_BASE, 0x1) },
+ { USB_INTERFACE_INFO(USB_CLASS_MCTP, MCTP_USB_SUBCLASS_SPAN, 0x1) },
{ 0 },
};
diff --git a/drivers/net/mctp/mctp-usblib-test.c b/drivers/net/mctp/mctp-usblib-test.c
new file mode 100644
index 000000000000..9df401a914ff
--- /dev/null
+++ b/drivers/net/mctp/mctp-usblib-test.c
@@ -0,0 +1,412 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * mctp-usblib-test.c - MCTP-over-USB (DMTF DSP0283) transport helper library,
+ * unit test definitions.
+ *
+ * Copyright (C) 2026 Code Construct Pty Ltd
+ */
+
+#include <uapi/linux/netdevice.h>
+#include <linux/netdevice.h>
+#include <kunit/test.h>
+#include <linux/if_arp.h>
+#include <net/mctp.h>
+#include <net/mctpdevice.h>
+#include <linux/usb/mctp-usb.h>
+
+struct mctp_usblib_test_dev {
+ struct net_device *ndev;
+ struct mctp_dev *mdev;
+ struct sk_buff_head rx_pkts;
+};
+
+struct mctp_usblib_test_ctx {
+ struct mctp_usblib_test_dev *dev;
+ struct mctp_route rt;
+};
+
+static netdev_tx_t mctp_usblib_dev_tx(struct sk_buff *skb,
+ struct net_device *ndev)
+{
+ /* we don't track any TXed packets at present */
+ kfree_skb(skb);
+ return NETDEV_TX_OK;
+}
+
+static const struct net_device_ops mctp_test_netdev_ops = {
+ .ndo_start_xmit = mctp_usblib_dev_tx,
+};
+
+static const u16 ep_maxpacket = 512;
+static const mctp_eid_t local_eid = 8;
+
+static void mctp_usblib_dev_setup(struct net_device *ndev)
+{
+ ndev->type = ARPHRD_MCTP;
+ ndev->mtu = 8192;
+ ndev->flags = IFF_NOARP;
+ ndev->netdev_ops = &mctp_test_netdev_ops;
+ ndev->needs_free_netdev = true;
+ ndev->pcpu_stat_type = NETDEV_PCPU_STAT_DSTATS;
+}
+
+static void mctp_usblib_test_dev_action(void *data)
+{
+ struct mctp_usblib_test_dev *dev = data;
+
+ skb_queue_purge(&dev->rx_pkts);
+ if (dev->mdev)
+ mctp_dev_put(dev->mdev);
+ unregister_netdev(dev->ndev);
+}
+
+static struct mctp_usblib_test_dev *
+mctp_usblib_test_create_dev(struct kunit *test)
+{
+ struct mctp_usblib_test_dev *dev;
+ struct net_device *ndev;
+ int rc;
+
+ ndev = alloc_netdev(sizeof(*dev), "mctptest%d", NET_NAME_ENUM,
+ mctp_usblib_dev_setup);
+ if (!ndev)
+ return NULL;
+
+ dev = netdev_priv(ndev);
+ dev->ndev = ndev;
+ skb_queue_head_init(&dev->rx_pkts);
+
+ rc = register_netdev(ndev);
+ if (rc) {
+ free_netdev(ndev);
+ return NULL;
+ }
+
+ rc = kunit_add_action_or_reset(test, mctp_usblib_test_dev_action, dev);
+ if (rc)
+ return NULL;
+
+ rcu_read_lock();
+ dev->mdev = __mctp_dev_get(ndev);
+ if (dev->mdev)
+ dev->mdev->net = mctp_default_net(dev_net(ndev));
+ rcu_read_unlock();
+
+ if (!dev->mdev)
+ return NULL;
+
+ rtnl_lock();
+ rc = dev_open(ndev, NULL);
+ rtnl_unlock();
+ if (rc)
+ return NULL;
+
+ return dev;
+}
+
+static int mctp_usblib_test_dst_output(struct mctp_dst *dst,
+ struct sk_buff *skb)
+{
+ struct mctp_usblib_test_dev *dev = netdev_priv(skb->dev);
+
+ skb_queue_tail(&dev->rx_pkts, skb);
+
+ return 0;
+}
+
+static void mctp_usblib_test_fini_action(void *data)
+{
+ struct mctp_usblib_test_ctx *ctx = data;
+
+ /* The device will have been destroyed, so ->rt will be unlinked.
+ * Just ensure that the refcount is as expected.
+ */
+ KUNIT_EXPECT_TRUE(current->kunit_test,
+ refcount_dec_and_test(&ctx->rt.refs));
+
+ kfree(ctx);
+}
+
+static struct mctp_usblib_test_ctx *mctp_usblib_test_init(struct kunit *test)
+{
+ struct mctp_usblib_test_ctx *ctx;
+ struct mctp_route *rt;
+ int rc;
+
+ ctx = kzalloc_obj(*ctx);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+
+ INIT_LIST_HEAD(&ctx->rt.list);
+ rt = &ctx->rt;
+ refcount_set(&rt->refs, 1);
+
+ rc = kunit_add_action_or_reset(test, mctp_usblib_test_fini_action, ctx);
+ KUNIT_ASSERT_EQ(test, rc, 0);
+
+ ctx->dev = mctp_usblib_test_create_dev(test);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->dev);
+
+ rt->min = local_eid;
+ rt->max = local_eid;
+ rt->dst_type = MCTP_ROUTE_DIRECT;
+ rt->type = RTN_LOCAL;
+ rt->dev = ctx->dev->mdev;
+ rt->output = mctp_usblib_test_dst_output;
+
+ rtnl_lock();
+ list_add_rcu(&ctx->rt.list, &init_net.mctp.routes);
+ refcount_inc(&rt->refs);
+ rtnl_unlock();
+
+ return ctx;
+}
+
+/* Init a MCTP-over-USB packet within a buffer. @len is the length of the
+ * buffer to write, @payload_len is the reported size of the MCTP-over-USB
+ * packet.
+ */
+static void mctp_usblib_test_init_pkt(void *data, size_t len,
+ size_t payload_len)
+{
+ struct {
+ struct mctp_usb_hdr usb;
+ struct mctp_hdr mctp;
+ } hdr;
+
+ hdr.usb.id = cpu_to_be16(MCTP_USB_DMTF_ID);
+ hdr.usb.len = cpu_to_be16(payload_len);
+ hdr.mctp.ver = 1;
+ hdr.mctp.dest = local_eid;
+ hdr.mctp.src = 0;
+ hdr.mctp.flags_seq_tag = 0;
+
+ memcpy(data, &hdr, min(len, sizeof(hdr)));
+ if (len > sizeof(hdr))
+ memset(data + sizeof(hdr), 0, len - sizeof(hdr));
+}
+
+static void action_rx_fini(void *data)
+{
+ struct mctp_usblib_rx *rx = data;
+
+ mctp_usblib_rx_fini(rx);
+ kfree(rx);
+}
+
+static struct mctp_usblib_rx *
+mctp_usblib_test_rx_init(struct kunit *test, bool span)
+{
+ struct mctp_usblib_rx *rx;
+ int rc;
+
+ rx = kzalloc_obj(*rx);
+ if (rx) {
+ rc = kunit_add_action_or_reset(test, action_rx_fini, rx);
+ KUNIT_ASSERT_EQ(test, rc, 0);
+ }
+ KUNIT_ASSERT_NOT_NULL(test, rx);
+
+ rc = mctp_usblib_rx_init(rx, ep_maxpacket, span);
+ KUNIT_ASSERT_EQ(test, rc, 0);
+
+ return rx;
+}
+
+/* Wrappers for usblib's rx_complete callback, which is intended to be called
+ * from atomic context
+ */
+static int mctp_usblib_test_rx_complete(struct net_device *netdev,
+ struct mctp_usblib_rx *rx, size_t len)
+{
+ int rc;
+
+ local_bh_disable();
+ rc = mctp_usblib_rx_complete(netdev, rx, len);
+ local_bh_enable();
+
+ return rc;
+}
+
+/* Single packet, starting on a transfer boundary, contained entirely within
+ * the transfer
+ */
+static void mctp_usblib_test_rx_single(struct kunit *test)
+{
+ struct mctp_usblib_test_dev *dev;
+ struct mctp_usblib_test_ctx *ctx;
+ struct mctp_usblib_rx *rx;
+ struct sk_buff *skb;
+ size_t len;
+ void *buf;
+ int rc;
+
+ ctx = mctp_usblib_test_init(test);
+ dev = ctx->dev;
+
+ rx = mctp_usblib_test_rx_init(test, true);
+
+ rc = mctp_usblib_rx_prepare(dev->ndev, rx,
+ &buf, &len, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, rc, 0);
+
+ /* we should always have a maxpacket of transfer available */
+ KUNIT_ASSERT_GE(test, len, ep_maxpacket);
+
+ mctp_usblib_test_init_pkt(buf, 8, 8);
+
+ rc = mctp_usblib_test_rx_complete(dev->ndev, rx, 8);
+ KUNIT_ASSERT_EQ(test, rc, 0);
+
+ skb = __skb_dequeue(&dev->rx_pkts);
+ KUNIT_EXPECT_NOT_NULL(test, skb);
+ if (skb)
+ KUNIT_EXPECT_EQ(test, skb->len, 4);
+ kfree_skb(skb);
+}
+
+struct mctp_usblib_test_pkt_span {
+ const char *name;
+ size_t n_pkts;
+ size_t pkts[6];
+ size_t n_xfers;
+ size_t xfers[6];
+};
+
+static void
+mctp_usblib_test_pkt_span_to_desc(const struct mctp_usblib_test_pkt_span *t,
+ char *desc)
+{
+ strscpy(desc, t->name, KUNIT_PARAM_DESC_SIZE);
+}
+
+static void
+mctp_usblib_test_pkt_span_validate(struct kunit *test,
+ const struct mctp_usblib_test_pkt_span *span,
+ size_t *len)
+{
+ size_t pkt_len = 0, xfer_len = 0;
+ unsigned int i;
+
+ for (i = 0; i < span->n_pkts; i++) {
+ KUNIT_ASSERT_GE_MSG(test, span->pkts[i], 8,
+ "pkt[%u] len too small (%zu) for %s",
+ i, span->pkts[i], span->name);
+ pkt_len += span->pkts[i];
+ }
+
+ for (i = 0; i < span->n_xfers; i++)
+ xfer_len += span->xfers[i];
+
+ KUNIT_ASSERT_EQ_MSG(test, pkt_len, xfer_len,
+ "invalid pkt_len (%zu) != xfer_len (%zu) for %s",
+ pkt_len, xfer_len, span->name);
+
+ *len = pkt_len;
+}
+
+static void mctp_usblib_test_rx_pkt_span(struct kunit *test)
+{
+ const struct mctp_usblib_test_pkt_span *pkt_span = test->param_value;
+ size_t len, xfer_len, off, xfer_off;
+ struct mctp_usblib_test_dev *dev;
+ struct mctp_usblib_test_ctx *ctx;
+ struct mctp_usblib_rx *rx;
+ unsigned int i;
+ u8 *pktbuf;
+ void *buf;
+ int rc;
+
+ mctp_usblib_test_pkt_span_validate(test, pkt_span, &len);
+ pktbuf = kunit_kmalloc_array(test, 1, len, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, pktbuf);
+
+ /* lay out packets */
+ for (off = 0, i = 0; i < pkt_span->n_pkts; i++) {
+ len = pkt_span->pkts[i];
+ mctp_usblib_test_init_pkt(pktbuf + off, len, len);
+ off += len;
+ }
+
+ ctx = mctp_usblib_test_init(test);
+ dev = ctx->dev;
+
+ rx = mctp_usblib_test_rx_init(test, true);
+
+ /* feed transfers */
+ for (off = 0, xfer_off = 0, i = 0; i < pkt_span->n_xfers;) {
+ xfer_len = pkt_span->xfers[i] - xfer_off;
+ rc = mctp_usblib_rx_prepare(dev->ndev, rx,
+ &buf, &len, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, rc, 0);
+
+ KUNIT_ASSERT_GE(test, len, ep_maxpacket);
+
+ len = min(len, xfer_len);
+ memcpy(buf, pktbuf + off, len);
+
+ if (len == xfer_len) {
+ /* whole/end xfer, proceed to next */
+ xfer_off = 0;
+ i++;
+ } else {
+ /* partial */
+ xfer_off += len;
+ }
+
+ rc = mctp_usblib_test_rx_complete(dev->ndev, rx, len);
+ KUNIT_ASSERT_EQ(test, rc, 0);
+ off += len;
+ }
+
+ /* check received packets */
+ KUNIT_EXPECT_EQ(test, dev->rx_pkts.qlen, pkt_span->n_pkts);
+ for (i = 0; ; i++) {
+ struct sk_buff *skb = __skb_dequeue(&dev->rx_pkts);
+
+ if (!skb)
+ break;
+
+ if (i < pkt_span->n_pkts)
+ KUNIT_EXPECT_EQ(test, skb->len, pkt_span->pkts[i] - 4);
+
+ kfree_skb(skb);
+ }
+}
+
+static const struct mctp_usblib_test_pkt_span mctp_usblib_test_pkt_spans[] = {
+ /* One packet completely within a transfer */
+ { "1p1x-complete", 1, { 8 }, 1, { 8 } },
+ /* Two small packets combined within one transfer */
+ { "2p1x-combined", 2, { 8, 8 }, 1, { 16 } },
+ /* A packet split over two transfers, at the MCTP payload */
+ { "1p2x-split-payload", 1, { 16 }, 2, { 8, 8 } },
+ /* A packet split over two transfers, at the USB transport header */
+ { "1p2x-split-usbhdr", 1, { 16 }, 2, { 2, 14 } },
+ /* A packet split over two transfers, at the MCTP header */
+ { "1p2x-split-mctphdr", 1, { 16 }, 2, { 6, 10 } },
+ /* Single packet split over 3 transfers, middle entirely continuation */
+ { "1p3x-split", 1, { 12 }, 3, { 4, 4, 4 } },
+ /* Max-sized single transfer */
+ { "1p1x-large", 1, { 8191 }, 1, { 8191 } },
+ /* Two large packets, split at the worst-case for allocation, with a
+ * single byte continuing the span
+ */
+ { "2p2x-large-split", 2, { 8190, 8190 }, 2, { 8191, 8189 } },
+};
+
+KUNIT_ARRAY_PARAM(mctp_usblib_test_rx_pkt_span, mctp_usblib_test_pkt_spans,
+ mctp_usblib_test_pkt_span_to_desc);
+
+static struct kunit_case mctp_usblib_test_cases[] = {
+ KUNIT_CASE(mctp_usblib_test_rx_single),
+ KUNIT_CASE_PARAM(mctp_usblib_test_rx_pkt_span,
+ mctp_usblib_test_rx_pkt_span_gen_params),
+ {}
+};
+
+static struct kunit_suite mctp_usblib_test_suite = {
+ .name = "mctp-usblib",
+ .test_cases = mctp_usblib_test_cases,
+};
+
+kunit_test_suite(mctp_usblib_test_suite);
diff --git a/drivers/net/mctp/mctp-usblib.c b/drivers/net/mctp/mctp-usblib.c
new file mode 100644
index 000000000000..31997f989026
--- /dev/null
+++ b/drivers/net/mctp/mctp-usblib.c
@@ -0,0 +1,625 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * mctp-usblib.c - MCTP-over-USB (DMTF DSP0283) transport helper library
+ *
+ * DSP0283 is available at:
+ * https://www.dmtf.org/sites/default/files/standards/documents/DSP0283_1.1.0.pdf
+ *
+ * Copyright (C) 2024-2026 Code Construct Pty Ltd
+ */
+
+#include <linux/module.h>
+#include <linux/netdevice.h>
+#include <linux/skbuff.h>
+#include <linux/usb/ch9.h>
+#include <linux/usb/mctp-usb.h>
+#include <net/mctp.h>
+
+int mctp_usblib_rx_init(struct mctp_usblib_rx *rx, u16 ep_pktlen, bool span)
+{
+ if (!ep_pktlen)
+ return -EINVAL;
+
+ if (ep_pktlen & ~USB_ENDPOINT_MAXP_MASK)
+ return -EINVAL;
+
+ memset(rx, 0, sizeof(*rx));
+ rx->span = span;
+ rx->ep_pktlen = ep_pktlen;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(mctp_usblib_rx_init);
+
+void mctp_usblib_rx_fini(struct mctp_usblib_rx *rx)
+{
+ kfree_skb(rx->skb);
+}
+EXPORT_SYMBOL_GPL(mctp_usblib_rx_fini);
+
+/*
+ * Prepare a transfer buffer for future completion; *bufp and *lenp will
+ * be populated on success.
+ */
+int mctp_usblib_rx_prepare(struct net_device *netdev,
+ struct mctp_usblib_rx *rx,
+ void **bufp, size_t *lenp, gfp_t gfp)
+{
+ struct sk_buff *skb = rx->skb;
+ unsigned int len = 0;
+
+ if (skb && skb->len >= MCTP_USB_1_1_PKTLEN_MAX) {
+ /* something must have gone terribly wrong. clear and restart */
+ mctp_usblib_rx_cancel(rx);
+ skb = NULL;
+ }
+
+ len = rx->span ? roundup(MCTP_USB_1_1_PKTLEN_MAX, rx->ep_pktlen)
+ : MCTP_USB_1_0_XFER_SIZE;
+
+ if (!skb) {
+ skb = __netdev_alloc_skb(netdev, len, gfp);
+ if (!skb)
+ return -ENOMEM;
+
+ } else if (skb->cloned || skb_tailroom(skb) < rx->ep_pktlen) {
+ /* We always need to realloc if ->cloned, as we cannot
+ * resubmit the (now-shared) skb buffer for possible DMA.
+ *
+ * Otherwise (if we have an un-cloned SKB): just ensure we
+ * have sufficient space to prevent babble. Since we allocated
+ * for max size in the last prepare (and have not consumed any
+ * of that space for a prior MCTP packet, because !cloned), we
+ * have sufficient data to finish the current MCTP packet.
+ */
+ struct sk_buff *skb2;
+
+ skb2 = skb_copy_expand(skb, 0, len, gfp);
+ if (!skb2)
+ return -ENOMEM;
+ dev_kfree_skb_any(skb);
+ skb = skb2;
+ }
+
+ rx->skb = skb;
+
+ /* Spanning mode allows ZLPs, so we don't require exactly one
+ * transfer packet. If we have extra tailroom, may as well use it,
+ * and we have ensured that the tailroom >= ep_pktlen.
+ */
+ if (rx->span)
+ len = rounddown(skb_tailroom(skb), rx->ep_pktlen);
+
+ *bufp = skb_tail_pointer(skb);
+ *lenp = len;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(mctp_usblib_rx_prepare);
+
+static void mctp_usblib_rx(struct net_device *netdev, struct sk_buff *skb)
+{
+ struct pcpu_dstats *dstats = this_cpu_ptr(netdev->dstats);
+ struct mctp_skb_cb *cb;
+ unsigned long flags;
+
+ skb_reset_mac_header(skb);
+ skb_pull(skb, sizeof(struct mctp_usb_hdr));
+
+ /* we're called from an URB completion handler, and cannot assume local
+ * irqs are always disabled
+ */
+ flags = u64_stats_update_begin_irqsave(&dstats->syncp);
+ u64_stats_inc(&dstats->rx_packets);
+ u64_stats_add(&dstats->rx_bytes, skb->len);
+ u64_stats_update_end_irqrestore(&dstats->syncp, flags);
+
+ skb->protocol = htons(ETH_P_MCTP);
+ skb_reset_network_header(skb);
+ cb = __mctp_cb(skb);
+ cb->halen = 0;
+ netif_rx(skb);
+}
+
+static void mctp_usblib_rx_stats_single_drop(struct net_device *dev)
+{
+ struct pcpu_dstats *dstats = this_cpu_ptr(dev->dstats);
+ unsigned long flags;
+
+ flags = u64_stats_update_begin_irqsave(&dstats->syncp);
+ u64_stats_inc(&dstats->rx_drops);
+ u64_stats_update_end_irqrestore(&dstats->syncp, flags);
+}
+
+/*
+ * Receive a USB completion of @len bytes of incoming data. We will then split
+ * this into packets and netif_rx() each. Intended to be called in atomic
+ * contexts - ie., URB completion.
+ *
+ * Assumes @netdev uses dstats.
+ */
+int mctp_usblib_rx_complete(struct net_device *netdev,
+ struct mctp_usblib_rx *rx, size_t len)
+{
+ struct sk_buff *skb = rx->skb;
+ int rc = 0;
+
+ __skb_put(skb, len);
+
+ for (;;) {
+ struct mctp_usb_hdr *hdr;
+ struct sk_buff *skb2;
+ /* length of MCTP packet, including USB header */
+ u16 pkt_len;
+
+ /* no header yet, resubmit for the rest of the packet */
+ if (skb->len < sizeof(*hdr)) {
+ if (!rx->span) {
+ netdev_dbg(netdev,
+ "rx: tiny xfer (%d) in non-span mode",
+ skb->len);
+ rc = -ENOMSG;
+ goto err_reset;
+ }
+ break;
+ }
+
+ hdr = (struct mctp_usb_hdr *)skb->data;
+
+ if (be16_to_cpu(hdr->id) != MCTP_USB_DMTF_ID) {
+ /* By resetting here, will start the next IN transfer
+ * at the beginning of the new skb. This will mean
+ * we re-sync when we next see a spanned packet aligned
+ * with the start of a transfer.
+ *
+ * In non-spanning mode, this just means we'll drop
+ * the current transfer only
+ */
+ netdev_dbg(netdev, "rx: invalid id %04x\n",
+ be16_to_cpu(hdr->id));
+ rc = -EPROTO;
+ goto err_reset;
+ }
+
+ pkt_len = be16_to_cpu(hdr->len);
+ /* v1.1, with span enabled, has a 13-bit length */
+ pkt_len &= rx->span ?
+ MCTP_USB_1_1_PKTLEN_MAX : MCTP_USB_1_0_PKTLEN_MAX;
+ if (pkt_len < sizeof(*hdr) + sizeof(struct mctp_hdr)) {
+ netdev_dbg(netdev, "rx: invalid len %d\n", pkt_len);
+ rc = -EPROTO;
+ goto err_reset;
+ }
+
+ /* span continues to the next transfer, resubmit */
+ if (pkt_len > skb->len) {
+ if (!rx->span) {
+ netdev_dbg(netdev,
+ "rx: short xfer (%d vs %d) in non-span mode",
+ pkt_len, skb->len);
+ rc = -EPROTO;
+ goto err_reset;
+ }
+ break;
+ }
+
+ /* we have (exactly) a complete packet, RX it directly */
+ if (pkt_len == skb->len) {
+ mctp_usblib_rx(netdev, skb);
+ rx->skb = NULL;
+ break;
+ }
+
+ /* more packets follow - RX a clone so that we can continue
+ * processing the current SKB, which may be the start of a
+ * span.
+ */
+ skb2 = skb_clone(skb, GFP_ATOMIC);
+ if (skb2) {
+ skb_trim(skb2, pkt_len);
+ mctp_usblib_rx(netdev, skb2);
+ } else {
+ mctp_usblib_rx_stats_single_drop(netdev);
+ }
+ skb_pull(skb, pkt_len);
+ }
+
+ return 0;
+
+err_reset:
+ dev_kfree_skb_any(rx->skb);
+ rx->skb = NULL;
+ return rc;
+}
+EXPORT_SYMBOL_GPL(mctp_usblib_rx_complete);
+
+/*
+ * Cancel a rx context; subsequent prepare/complete calls will not be a
+ * continuation of any data already received.
+ */
+void mctp_usblib_rx_cancel(struct mctp_usblib_rx *rx)
+{
+ dev_kfree_skb_any(rx->skb);
+ rx->skb = NULL;
+}
+EXPORT_SYMBOL_GPL(mctp_usblib_rx_cancel);
+
+/* transmit context: encapsulates one transfer */
+struct mctp_usblib_tx_ctx {
+ struct mctp_usblib_tx *tx;
+ struct sk_buff_head skbs;
+ unsigned int buf_len, len;
+ enum mctp_usblib_tx_buf_type {
+ TX_SINGLE,
+ TX_FLAT,
+ } buf_type;
+ u8 buf[] ____cacheline_aligned;
+};
+
+void mctp_usblib_tx_init(struct mctp_usblib_tx *tx,
+ const struct mctp_usblib_tx_ops *ops,
+ void *priv, bool span)
+{
+ memset(tx, 0, sizeof(*tx));
+ tx->ops = *ops;
+ tx->priv = priv;
+ tx->span = span;
+ spin_lock_init(&tx->lock);
+}
+EXPORT_SYMBOL_GPL(mctp_usblib_tx_init);
+
+static int mctp_usblib_tx_avail(struct mctp_usblib_tx_ctx *ctx)
+{
+ return ctx->buf_type == TX_SINGLE ? 0 : ctx->buf_len - ctx->len;
+}
+
+static bool mctp_usblib_tx_should_send(struct mctp_usblib_tx_ctx *ctx)
+{
+ /* Use the baseline length (ie, BTU) as an approximate
+ * "reasonably-sized" packet we could expect. If there is
+ * insufficient capacity for that, then send.
+ */
+ const size_t pkt_len = MCTP_USB_BTU + sizeof(struct mctp_usb_hdr);
+
+ return mctp_usblib_tx_avail(ctx) < pkt_len;
+}
+
+/*
+ * Returns zero on success, non-zero on failure - indicating that the new skb
+ * could not be appended. So, errors reported here to the TX path will result
+ * in the TX being transmitted.
+ */
+static int mctp_usblib_tx_append(struct mctp_usblib_tx_ctx *ctx,
+ struct sk_buff *skb)
+{
+ if (ctx->buf_type == TX_SINGLE)
+ return -EINVAL;
+
+ if (mctp_usblib_tx_avail(ctx) < skb->len)
+ return -ENOBUFS;
+
+ __skb_queue_tail(&ctx->skbs, skb);
+
+ ctx->len += skb->len;
+
+ return 0;
+}
+
+static int mctp_usblib_tx_send(struct mctp_usblib_tx_ctx *ctx)
+{
+ void *buf;
+
+ /* If we have a qlen of 1, we only ended up packing a single skb,
+ * despite allocating for multiple. Skip the copy and send directly
+ * from the skb data.
+ */
+ if (ctx->buf_type == TX_SINGLE || ctx->skbs.qlen == 1) {
+ buf = ctx->skbs.next->data;
+
+ } else if (ctx->buf_type == TX_FLAT) {
+ struct sk_buff *skb;
+ size_t pos = 0;
+
+ skb_queue_walk(&ctx->skbs, skb) {
+ skb_copy_bits(skb, 0, ctx->buf + pos, skb->len);
+ pos += skb->len;
+ }
+
+ buf = ctx->buf;
+ } else {
+ return -EINVAL;
+ }
+
+ return ctx->tx->ops.send(ctx, buf, ctx->len);
+}
+
+static void mctp_usblib_tx_ctx_free(struct mctp_usblib_tx_ctx *ctx,
+ enum skb_drop_reason reason)
+{
+ struct sk_buff *skb;
+
+ if (!ctx)
+ return;
+
+ while ((skb = __skb_dequeue(&ctx->skbs)) != NULL)
+ dev_kfree_skb_any_reason(skb, reason);
+ kfree(ctx);
+}
+
+void *mctp_usblib_tx_ctx_priv(struct mctp_usblib_tx_ctx *tx_ctx)
+{
+ return tx_ctx->tx->priv;
+}
+EXPORT_SYMBOL_GPL(mctp_usblib_tx_ctx_priv);
+
+/* caller must ensure the tx & completion path is quiesced */
+void mctp_usblib_tx_fini(struct mctp_usblib_tx *tx)
+{
+ mctp_usblib_tx_ctx_free(tx->cur_ctx, SKB_DROP_REASON_NOT_SPECIFIED);
+}
+EXPORT_SYMBOL_GPL(mctp_usblib_tx_fini);
+
+/* Max size of a spanned TX. Since we allocate a separate span buffer, limit
+ * the tx-time allocations to 4k. Larger packets will be sent as single
+ * transfers.
+ */
+static const unsigned int TX_SPAN_MAX = 4096 - sizeof(struct mctp_usblib_tx_ctx);
+
+static struct mctp_usblib_tx_ctx *
+mctp_usblib_tx_ctx_create(struct mctp_usblib_tx *tx, struct sk_buff *skb,
+ bool single)
+{
+ enum mctp_usblib_tx_buf_type type;
+ struct mctp_usblib_tx_ctx *ctx;
+ size_t sz = 0;
+
+ if (single || skb->len > TX_SPAN_MAX) {
+ type = TX_SINGLE;
+ } else {
+ type = TX_FLAT;
+ sz = tx->span ? TX_SPAN_MAX : MCTP_USB_1_0_XFER_SIZE;
+ }
+
+ ctx = kzalloc_flex(*ctx, buf, sz, GFP_ATOMIC);
+ if (!ctx)
+ return NULL;
+
+ ctx->tx = tx;
+ ctx->buf_type = type;
+ ctx->buf_len = sz;
+ ctx->len = skb->len;
+ skb_queue_head_init(&ctx->skbs);
+ __skb_queue_tail(&ctx->skbs, skb);
+
+ return ctx;
+}
+
+static void mctp_usblib_tx_stats_update(struct mctp_usblib_tx_ctx *ctx,
+ struct net_device *dev,
+ bool ok)
+{
+ struct pcpu_dstats *dstats = get_cpu_ptr(dev->dstats);
+ unsigned long flags;
+
+ flags = u64_stats_update_begin_irqsave(&dstats->syncp);
+ if (ok) {
+ /* Only include the network-layer data in tx stats; we know
+ * that there is a 4-byte header pushed to all skbs in
+ * tx_skb_prepare()
+ */
+ u64 n = ctx->skbs.qlen;
+ s64 len = ctx->len - (n * sizeof(struct mctp_usb_hdr));
+
+ u64_stats_add(&dstats->tx_packets, n);
+ u64_stats_add(&dstats->tx_bytes, len);
+ } else {
+ u64_stats_add(&dstats->tx_drops, ctx->skbs.qlen);
+ }
+ u64_stats_update_end_irqrestore(&dstats->syncp, flags);
+ put_cpu_ptr(dev->dstats);
+}
+
+static void mctp_usblib_tx_stats_single_drop(struct net_device *dev)
+{
+ struct pcpu_dstats *dstats = get_cpu_ptr(dev->dstats);
+ unsigned long flags;
+
+ flags = u64_stats_update_begin_irqsave(&dstats->syncp);
+ u64_stats_inc(&dstats->tx_drops);
+ u64_stats_update_end_irqrestore(&dstats->syncp, flags);
+ put_cpu_ptr(dev->dstats);
+}
+
+/*
+ * Completion for the ->send() op. This will update netdev stats and
+ * free the tx context.
+ *
+ * Likely called from (atomic) URB completion context.
+ */
+void mctp_usblib_tx_send_complete(struct mctp_usblib_tx_ctx *tx_ctx,
+ struct net_device *dev, bool ok)
+{
+ enum skb_drop_reason reason =
+ ok ? SKB_CONSUMED : SKB_DROP_REASON_NOT_SPECIFIED;
+
+ mctp_usblib_tx_stats_update(tx_ctx, dev, ok);
+ mctp_usblib_tx_ctx_free(tx_ctx, reason);
+}
+EXPORT_SYMBOL_GPL(mctp_usblib_tx_send_complete);
+
+/* Prepare a skb for push()
+ *
+ * On error, populates @reason.
+ */
+static int mctp_usblib_tx_skb_prepare(struct sk_buff *skb, bool span,
+ enum skb_drop_reason *reason)
+{
+ unsigned long plen, max_len;
+ struct mctp_usb_hdr *hdr;
+ int rc;
+
+ max_len = span ? MCTP_USB_1_1_PKTLEN_MAX : MCTP_USB_1_0_PKTLEN_MAX;
+
+ plen = skb->len;
+ if (plen + sizeof(*hdr) > max_len) {
+ *reason = SKB_DROP_REASON_PKT_TOO_BIG;
+ return -EMSGSIZE;
+ }
+
+ rc = skb_cow_head(skb, sizeof(*hdr));
+ if (rc) {
+ *reason = SKB_DROP_REASON_NOMEM;
+ return rc;
+ }
+
+ hdr = skb_push(skb, sizeof(*hdr));
+ if (!hdr) {
+ *reason = SKB_DROP_REASON_NOMEM;
+ return -ENOMEM;
+ }
+
+ hdr->id = cpu_to_be16(MCTP_USB_DMTF_ID);
+ hdr->len = cpu_to_be16(plen + sizeof(*hdr));
+
+ return 0;
+}
+
+/*
+ * Push a new skb to the transfer. May result in zero or more calls to
+ * ops->send().
+ *
+ * Takes ownership of @skb, including on error.
+ */
+int mctp_usblib_tx_push(struct net_device *dev,
+ struct mctp_usblib_tx *tx,
+ struct sk_buff *skb, bool more)
+{
+ struct mctp_usblib_tx_ctx *ctx, *send_ctx = NULL;
+ enum skb_drop_reason reason;
+ const int max_tries = 3;
+ unsigned long flags;
+ int try = 1, rc;
+
+ rc = mctp_usblib_tx_skb_prepare(skb, tx->span, &reason);
+ if (rc) {
+ mctp_usblib_tx_stats_single_drop(dev);
+ kfree_skb_reason(skb, reason);
+ /* we may still need to proceed, in case an existing ctx
+ * is now sendable (ie.: !more).
+ */
+ skb = NULL;
+ }
+
+ reason = SKB_DROP_REASON_NOT_SPECIFIED;
+retry:
+ /* Try and queue to the current context. We exit this critical section
+ * with a few bits of state:
+ * - send_ctx: indicating a prior context that needs to be sent
+ * - skb: indicating that a skb still needs to be queued/sent
+ */
+ spin_lock_irqsave(&tx->lock, flags);
+ ctx = tx->cur_ctx;
+ if (ctx) {
+ if (skb) {
+ rc = mctp_usblib_tx_append(ctx, skb);
+ if (rc) {
+ /* can't append to the pending tx - detach for
+ * sending, and we'll create a new tx below.
+ */
+ swap(tx->cur_ctx, send_ctx);
+ } else {
+ /* we have queued */
+ skb = NULL;
+ if (!more || mctp_usblib_tx_should_send(ctx))
+ swap(tx->cur_ctx, send_ctx);
+ }
+ } else if (!more) {
+ swap(tx->cur_ctx, send_ctx);
+ }
+ }
+ spin_unlock_irqrestore(&tx->lock, flags);
+
+ if (send_ctx) {
+ rc = mctp_usblib_tx_send(send_ctx);
+ if (rc) {
+ mctp_usblib_tx_stats_update(send_ctx, dev, false);
+ mctp_usblib_tx_ctx_free(send_ctx, reason);
+ }
+ send_ctx = NULL;
+ }
+
+ /* we have either queued, or the prepare failed; nothing more to do */
+ if (!skb)
+ return 0;
+
+ ctx = mctp_usblib_tx_ctx_create(tx, skb, !more);
+ if (!ctx) {
+ netdev_dbg(dev, "TX context create failed\n");
+ mctp_usblib_tx_stats_single_drop(dev);
+ kfree_skb(skb);
+ return -ENOMEM;
+ }
+
+ /* if we're ready to send now, no need to enqueue */
+ if (!more || mctp_usblib_tx_should_send(ctx)) {
+ rc = mctp_usblib_tx_send(ctx);
+ if (rc) {
+ mctp_usblib_tx_stats_update(ctx, dev, false);
+ mctp_usblib_tx_ctx_free(ctx, reason);
+ }
+ return 0;
+ }
+
+ spin_lock_irqsave(&tx->lock, flags);
+ if (!tx->cur_ctx) {
+ tx->cur_ctx = ctx;
+ ctx = NULL;
+ }
+ spin_unlock_irqrestore(&tx->lock, flags);
+
+ /* we may have lost the race with a concurrent tx; shouldn't happen, as
+ * ndo_start_xmit should be serialised over one queue, but try again
+ * from the top, as we may be able to queue the skb to that context.
+ */
+ if (ctx) {
+ /* unlink the new (sole) skb, we don't want it freed with ctx */
+ __skb_queue_head_init(&ctx->skbs);
+ mctp_usblib_tx_ctx_free(ctx, reason);
+ if (++try > max_tries) {
+ kfree_skb(skb);
+ mctp_usblib_tx_stats_single_drop(dev);
+ return -EBUSY;
+ }
+ goto retry;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(mctp_usblib_tx_push);
+
+/* Cancel a tx: any un-sent context is released. */
+void mctp_usblib_tx_cancel(struct mctp_usblib_tx *tx, struct net_device *dev,
+ enum skb_drop_reason reason)
+{
+ struct mctp_usblib_tx_ctx *ctx = NULL;
+ unsigned long flags;
+
+ spin_lock_irqsave(&tx->lock, flags);
+ swap(tx->cur_ctx, ctx);
+ spin_unlock_irqrestore(&tx->lock, flags);
+
+ if (!ctx)
+ return;
+
+ mctp_usblib_tx_stats_update(ctx, dev, false);
+ mctp_usblib_tx_ctx_free(ctx, reason);
+}
+EXPORT_SYMBOL_GPL(mctp_usblib_tx_cancel);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Jeremy Kerr <jk@codeconstruct.com.au>");
+MODULE_DESCRIPTION("MCTP USB transport library");
+
+#if IS_ENABLED(CONFIG_MCTP_TRANSPORT_USBLIB_TEST)
+#include "mctp-usblib-test.c"
+#endif
diff --git a/include/linux/usb/mctp-usb.h b/include/linux/usb/mctp-usb.h
index a2f6f1e04efb..4bb04a371105 100644
--- a/include/linux/usb/mctp-usb.h
+++ b/include/linux/usb/mctp-usb.h
@@ -2,7 +2,7 @@
/*
* mctp-usb.h - MCTP USB transport binding: common definitions,
* based on DMTF0283 specification:
- * https://www.dmtf.org/sites/default/files/standards/documents/DSP0283_1.0.1.pdf
+ * https://www.dmtf.org/sites/default/files/standards/documents/DSP0283_1.1.0.pdf
*
* These are protocol-level definitions, that may be shared between host
* and gadget drivers.
@@ -13,18 +13,96 @@
#ifndef __LINUX_USB_MCTP_USB_H
#define __LINUX_USB_MCTP_USB_H
+#include <linux/netdevice.h>
+#include <linux/skbuff.h>
#include <linux/types.h>
+/*
+ * MCTP-over-USB transport header. DSP0283 v1.0 has an 8-bit length field
+ * (preceded by 8 reserved bits), v1.1 has a 13-bit length field (preceded by
+ * 3 reserved bits). We use a be16 for our length to handle the larger v1.1
+ * representation, and mask as appropriate.
+ */
struct mctp_usb_hdr {
__be16 id;
- u8 rsvd;
- u8 len;
+ __be16 len;
} __packed;
-#define MCTP_USB_XFER_SIZE 512
+/* max transfer size for DSP0283 v1.0 */
+#define MCTP_USB_1_0_XFER_SIZE 512
#define MCTP_USB_BTU 68
#define MCTP_USB_MTU_MIN MCTP_USB_BTU
-#define MCTP_USB_MTU_MAX (U8_MAX - sizeof(struct mctp_usb_hdr))
+#define MCTP_USB_1_0_PKTLEN_MAX U8_MAX
+#define MCTP_USB_1_0_MTU_MAX (MCTP_USB_1_0_PKTLEN_MAX - sizeof(struct mctp_usb_hdr))
+#define MCTP_USB_1_1_PKTLEN_MAX GENMASK(12, 0)
+#define MCTP_USB_1_1_MTU_MAX (MCTP_USB_1_1_PKTLEN_MAX - sizeof(struct mctp_usb_hdr))
#define MCTP_USB_DMTF_ID 0x1ab4
+/* mctp-usblib */
+
+/*
+ * RX handle: drivers will typically create one on init, which persists for
+ * the life of the driver. The same handle is used for progressive
+ * prepare -> complete operations (for each incoming USB transfer), which
+ * result in netif_rx()-ing the MCTP packets received
+ */
+struct mctp_usblib_rx {
+ struct sk_buff *skb;
+ u16 ep_pktlen;
+ bool span;
+};
+
+int mctp_usblib_rx_init(struct mctp_usblib_rx *rx, u16 ep_pktlen, bool span);
+void mctp_usblib_rx_fini(struct mctp_usblib_rx *rx);
+
+int mctp_usblib_rx_prepare(struct net_device *netdev,
+ struct mctp_usblib_rx *rx,
+ void **bufp, size_t *lenp, gfp_t gfp);
+
+int mctp_usblib_rx_complete(struct net_device *netdev,
+ struct mctp_usblib_rx *rx, size_t len);
+
+void mctp_usblib_rx_cancel(struct mctp_usblib_rx *rx);
+
+/*
+ * TX handle: created by mctp_usblib_tx_push() during the tx path, and
+ * may persist across multiple packet transmits.
+ */
+struct mctp_usblib_tx_ctx;
+
+struct mctp_usblib_tx_ops {
+ /* Start a USB TX for @data. On returning success, the implementation
+ * must arrange for mctp_usblib_tx_send_complete() to be called at some
+ * later point (eg., on urb completion).
+ */
+ int (*send)(struct mctp_usblib_tx_ctx *tx_ctx, void *data, size_t len);
+};
+
+struct mctp_usblib_tx {
+ struct mctp_usblib_tx_ops ops;
+ void *priv;
+ bool span;
+ /* protects access to cur_ctx */
+ spinlock_t lock;
+ /* context to which we are adding packets, cleared on send */
+ struct mctp_usblib_tx_ctx *cur_ctx;
+};
+
+void mctp_usblib_tx_init(struct mctp_usblib_tx *tx,
+ const struct mctp_usblib_tx_ops *ops, void *priv,
+ bool span);
+void mctp_usblib_tx_fini(struct mctp_usblib_tx *tx);
+
+void *mctp_usblib_tx_ctx_priv(struct mctp_usblib_tx_ctx *tx_ctx);
+
+int mctp_usblib_tx_push(struct net_device *dev,
+ struct mctp_usblib_tx *tx,
+ struct sk_buff *skb, bool more);
+
+void mctp_usblib_tx_send_complete(struct mctp_usblib_tx_ctx *tx_ctx,
+ struct net_device *dev, bool ok);
+
+void mctp_usblib_tx_cancel(struct mctp_usblib_tx *tx, struct net_device *dev,
+ enum skb_drop_reason reason);
+
#endif /* __LINUX_USB_MCTP_USB_H */