Add a BMC-side driver for the IPMI channels of NVIDIA LSTP USB adapters. The adapter carries IPMI messages between the host and the BMC; each channel is exposed to BMC userspace as an ipmi-lstp<n> character device that reads requests from the host and writes the responses back.
By default each message on the node carries a one-byte message number between the length and the payload, and a response echoes the number of the request it answers. The number exists only on the node, not in the LSTP protocol. With USB_LSTP_IPMI_MSG_NUM disabled the node uses the ssif_bmc layout (struct ipmi_ssif_msg) instead, so userspace written for ssif_bmc works unchanged. Signed-off-by: Adrian Ambrozewicz <[email protected]> --- MAINTAINERS | 1 + drivers/char/ipmi/Kconfig | 32 +++ drivers/char/ipmi/Makefile | 1 + drivers/char/ipmi/ipmi-lstp.c | 464 ++++++++++++++++++++++++++++++++++ 4 files changed, 498 insertions(+) create mode 100644 drivers/char/ipmi/ipmi-lstp.c diff --git a/MAINTAINERS b/MAINTAINERS index 8e58895b7d5a..96667b3baa7a 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19577,6 +19577,7 @@ F: Documentation/devicetree/bindings/spi/nvidia,lstp-spi.yaml F: Documentation/devicetree/bindings/usb/nvidia,lstp.yaml F: Documentation/driver-api/usb/lstp.rst F: Documentation/firmware-guide/acpi/lstp.rst +F: drivers/char/ipmi/ipmi-lstp.c F: drivers/gpio/gpio-lstp.c F: drivers/i2c/busses/i2c-lstp.c F: drivers/leds/leds-lstp.c diff --git a/drivers/char/ipmi/Kconfig b/drivers/char/ipmi/Kconfig index 538a7d3c65bf..eb51a2d56fc9 100644 --- a/drivers/char/ipmi/Kconfig +++ b/drivers/char/ipmi/Kconfig @@ -119,6 +119,38 @@ config IPMI_POWEROFF endif # IPMI_HANDLER +config LSTP_IPMI_BMC + tristate "NVIDIA LSTP IPMI BMC support" + depends on USB_LSTP + default USB_LSTP + help + Say Y or M here to support the IPMI channels of NVIDIA LSTP USB + adapters on a BMC. Each channel is exposed as an ipmi-lstp<n> + character device through which BMC userspace receives requests + from the host and sends the responses back. + + This driver can also be built as a module. If so, the module + will be called ipmi-lstp. + +config USB_LSTP_IPMI_MSG_NUM + bool "Message-number header on LSTP IPMI nodes" + depends on LSTP_IPMI_BMC + default y + help + Messages read from and written to an LSTP IPMI device node carry a + one-byte message number between the length and the payload, and a + response must echo the number of the request it answers. The number + exists only on the device node, not in the LSTP protocol, and no + other BMC-side IPMI driver has it. + + Disable this option to drop it. The node then uses the layout of + struct ipmi_ssif_msg from the ssif_bmc driver, a length followed by + the payload, so that userspace written for ssif_bmc works unchanged. + A response answers the request read last, and is dropped once a newer + request has arrived. + + Say Y if userspace parses the message number. + config IPMI_KCS_BMC tristate diff --git a/drivers/char/ipmi/Makefile b/drivers/char/ipmi/Makefile index 4ea450a82242..2e320ce2c901 100644 --- a/drivers/char/ipmi/Makefile +++ b/drivers/char/ipmi/Makefile @@ -29,3 +29,4 @@ obj-$(CONFIG_ASPEED_KCS_IPMI_BMC) += kcs_bmc_aspeed.o obj-$(CONFIG_NPCM7XX_KCS_IPMI_BMC) += kcs_bmc_npcm7xx.o obj-$(CONFIG_IPMB_DEVICE_INTERFACE) += ipmb_dev_int.o obj-$(CONFIG_SSIF_IPMI_BMC) += ssif_bmc.o +obj-$(CONFIG_LSTP_IPMI_BMC) += ipmi-lstp.o diff --git a/drivers/char/ipmi/ipmi-lstp.c b/drivers/char/ipmi/ipmi-lstp.c new file mode 100644 index 000000000000..787d633d4ada --- /dev/null +++ b/drivers/char/ipmi/ipmi-lstp.c @@ -0,0 +1,464 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * IPMI driver for LSTP USB interface. + * + * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + */ + +#include <linux/build_bug.h> +#include <linux/err.h> +#include <linux/idr.h> +#include <linux/ipmi_ssif_bmc.h> +#include <linux/kfifo.h> +#include <linux/kref.h> +#include <linux/miscdevice.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/poll.h> +#include <linux/property.h> +#include <linux/slab.h> + +#include <linux/usb/lstp.h> + +/* ~1 s of queued requests at OpenBMC rates; past that the host has retried. */ +#define BUFFER_SIZE 4096 + +static DEFINE_IDA(lstp_ipmi_ida); + +#define IPMI_LSTP_PAYLOAD_MAX 254 +#define IPMI_MIN_MSG_LEN 2 /* netfn/lun + cmd */ + +/* + * read(2) and write(2) layout with CONFIG_USB_LSTP_IPMI_MSG_NUM. Deployed + * userspace parses these bytes, so the field widths and offsets are fixed; + * native byte order, as both ends are the same host. @msg_num exists only on + * this interface, never on the wire. + */ +struct ipmi_lstp_msg_header { + __u32 len; + __u8 msg_num; +} __packed; + +struct ipmi_lstp_msg { + struct ipmi_lstp_msg_header header; + __u8 payload[IPMI_LSTP_PAYLOAD_MAX]; +}; + +static_assert(sizeof(struct ipmi_lstp_msg_header) == 5, "IPMI read(2) header layout is ABI"); +static_assert(offsetof(struct ipmi_lstp_msg_header, msg_num) == 4, + "IPMI read(2) header layout is ABI"); + +/* + * The layout without CONFIG_USB_LSTP_IPMI_MSG_NUM is the ssif_bmc driver's: + * the same len and payload, no @msg_num. + */ +static_assert(offsetof(struct ipmi_ssif_msg, payload) == + sizeof_field(struct ipmi_lstp_msg_header, len), + "Untagged layout must be the LSTP header without msg_num"); +static_assert(IPMI_SSIF_PAYLOAD_MAX == IPMI_LSTP_PAYLOAD_MAX, + "Untagged layout must carry the LSTP payload"); + +#define LSTP_IPMI_MSG_NUM IS_ENABLED(CONFIG_USB_LSTP_IPMI_MSG_NUM) + +/* The leading bytes of struct ipmi_lstp_msg_header that userspace sees. */ +#define LSTP_IPMI_UAPI_HDR_LEN \ + (LSTP_IPMI_MSG_NUM ? sizeof(struct ipmi_lstp_msg_header) : \ + sizeof_field(struct ipmi_lstp_msg_header, len)) +#define LSTP_IPMI_UAPI_MSG_MAX \ + (LSTP_IPMI_MSG_NUM ? sizeof(struct ipmi_lstp_msg) : sizeof(struct ipmi_ssif_msg)) + +/* One per bind; the bind and each open fd hold @ref. */ +struct lstp_ipmi_ctx { + struct kref ref; + struct lstp_channel *ch; /* pinned for the life of @ctx */ + struct miscdevice miscdev; + int ida_id; /* -1 until allocated */ + bool running; /* exclusive-open flag */ + spinlock_t lock; /* protects @running, @msg_count, @read_tag, @fifo */ + struct mutex tx_lock; /* held across write()'s gone test and send; teardown waits on it */ + wait_queue_head_t req_wq; + struct kfifo_rec_ptr_2 fifo; /* pending requests, oldest dropped to fit */ + u8 msg_count; /* tag of the outstanding request; write() echoes it */ + u8 read_tag; /* untagged ABI: tag of the request read() handed out last, 0 for none */ + bool stopped; /* node torn down; see lstp_ipmi_gone() */ +}; + +enum lstp_ipmi_cmd { + LSTP_IPMI_CMD_MESSAGE = 0x00, +}; + +/* @stopped is per bind and never cleared; see Documentation/driver-api/usb/lstp.rst. */ +static inline bool lstp_ipmi_gone(struct lstp_ipmi_ctx *ctx) +{ + return READ_ONCE(ctx->stopped) || lstp_channel_disconnected(ctx->ch); +} + +static inline bool lstp_ipmi_readable(struct lstp_ipmi_ctx *ctx) +{ + return !kfifo_is_empty(&ctx->fifo) || lstp_ipmi_gone(ctx); +} + +/* Partial-init safe: a failed lstp_ipmi_init() unwinds through it as well. */ +static void lstp_ipmi_ctx_release(struct kref *ref) +{ + struct lstp_ipmi_ctx *ctx = container_of(ref, struct lstp_ipmi_ctx, ref); + struct lstp_channel *ch = ctx->ch; + + kfifo_free(&ctx->fifo); + if (ctx->ida_id >= 0) + ida_free(&lstp_ipmi_ida, ctx->ida_id); + kfree(ctx->miscdev.name); + mutex_destroy(&ctx->tx_lock); + kfree(ctx); + lstp_channel_put(ch); +} + +/* Devres action for the bind's reference, booked ahead of the node's own. */ +static void lstp_ipmi_ctx_put(void *data) +{ + struct lstp_ipmi_ctx *ctx = data; + + kref_put(&ctx->ref, lstp_ipmi_ctx_release); +} + +static inline struct lstp_ipmi_ctx *to_ctx(struct file *file) +{ + return container_of(file->private_data, struct lstp_ipmi_ctx, miscdev); +} + +static int lstp_ipmi_open(struct inode *inode, struct file *file) +{ + struct lstp_ipmi_ctx *ctx = to_ctx(file); + int ret = 0; + + spin_lock_irq(&ctx->lock); + if (lstp_ipmi_gone(ctx)) + ret = -ENODEV; + else if (ctx->running) + ret = -EBUSY; + else + ctx->running = true; + spin_unlock_irq(&ctx->lock); + + if (ret) + return ret; + + /* + * misc_mtx spans this ->open and misc_deregister(), which the bind's + * @ref outlasts, so @ctx is live here and this cannot race the final put. + */ + kref_get(&ctx->ref); + return 0; +} + +/* Read one queued request: the userspace header, then the payload. */ +static ssize_t lstp_ipmi_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) +{ + struct lstp_ipmi_ctx *ctx = to_ctx(file); + struct ipmi_lstp_msg msg; + unsigned long flags; + unsigned int len = 0; + size_t ulen = 0; + + /* + * copy_to_user() may sleep, so drain to @msg under @ctx->lock first. + * Loop: two readers on one fd can both wake, only one wins the record. + */ + while (!len) { + /* Before the queue: a request queued when the bind went cannot be answered. */ + if (lstp_ipmi_gone(ctx)) + return -ENODEV; + if (kfifo_is_empty(&ctx->fifo)) { + if (file->f_flags & O_NONBLOCK) + return -EAGAIN; + int ret = wait_event_interruptible(ctx->req_wq, lstp_ipmi_readable(ctx)); + + if (ret) + return ret; + } + + spin_lock_irqsave(&ctx->lock, flags); + /* + * Peek before consuming: kfifo_out() drops whatever of the + * record does not fit, so a short @count would retire a + * request the caller never saw. Leave it queued and let the + * caller come back with room for it. + */ + len = kfifo_out_peek(&ctx->fifo, &msg, sizeof(msg)); + ulen = len ? LSTP_IPMI_UAPI_HDR_LEN + msg.header.len : 0; + if (len && ulen <= count) { + kfifo_skip(&ctx->fifo); + WRITE_ONCE(ctx->read_tag, msg.header.msg_num); + } + spin_unlock_irqrestore(&ctx->lock, flags); + + if (ulen > count) + return -EINVAL; + } + + if (copy_to_user(buf, &msg.header, LSTP_IPMI_UAPI_HDR_LEN) || + copy_to_user(buf + LSTP_IPMI_UAPI_HDR_LEN, msg.payload, msg.header.len)) + return -EFAULT; + return ulen; +} + +/* + * Forward a response. A write whose @msg_num misses the outstanding tag is + * dropped and reported as consumed, so userspace need not track the race + * against a request that arrived meanwhile. Without the tag in the ABI, a + * response answers the request read() handed out last. + */ +static ssize_t lstp_ipmi_write(struct file *file, const char __user *buf, size_t count, + loff_t *ppos) +{ + struct lstp_ipmi_ctx *ctx = to_ctx(file); + struct lstp_channel *ch = ctx->ch; + struct ipmi_lstp_msg msg; + ssize_t ret; + + if (count < LSTP_IPMI_UAPI_HDR_LEN || count > LSTP_IPMI_UAPI_MSG_MAX) + return -EINVAL; + + /* The untagged bound admits struct padding past the payload; drop it. */ + if (copy_from_user(&msg.header, buf, LSTP_IPMI_UAPI_HDR_LEN) || + copy_from_user(msg.payload, buf + LSTP_IPMI_UAPI_HDR_LEN, + min(count - LSTP_IPMI_UAPI_HDR_LEN, sizeof(msg.payload)))) + return -EFAULT; + + /* Held through the send; see lstp_ipmi_stop_and_deregister(). */ + guard(mutex)(&ctx->tx_lock); + if (lstp_ipmi_gone(ctx)) + return -ENODEV; + + if (!LSTP_IPMI_MSG_NUM) + msg.header.msg_num = READ_ONCE(ctx->read_tag); + + /* + * 0 tags no request; it is only @msg_count's value before the first. + * Stale tag read is fine; pairs with WRITE_ONCE in the IRQ producer. + */ + if (!msg.header.msg_num || msg.header.msg_num != READ_ONCE(ctx->msg_count)) + return count; + + if (!msg.header.len || msg.header.len > IPMI_LSTP_PAYLOAD_MAX || + count < LSTP_IPMI_UAPI_HDR_LEN + msg.header.len) + return -EINVAL; + + ret = lstp_send(ch, LSTP_IPMI_CMD_MESSAGE, msg.payload, msg.header.len); + if (ret && ret != -ENODEV) + dev_err_ratelimited(ch->dev, "%s: ch_%d: Could not forward response (%pe)\n", + __func__, ch->ch_id, ERR_PTR(ret)); + + return (ret < 0) ? ret : count; +} + +static int lstp_ipmi_release(struct inode *inode, struct file *file) +{ + struct lstp_ipmi_ctx *ctx = to_ctx(file); + + spin_lock_irq(&ctx->lock); + ctx->running = false; + spin_unlock_irq(&ctx->lock); + + kref_put(&ctx->ref, lstp_ipmi_ctx_release); + + return 0; +} + +static __poll_t lstp_ipmi_poll(struct file *file, poll_table *wait) +{ + struct lstp_ipmi_ctx *ctx = to_ctx(file); + + poll_wait(file, &ctx->req_wq, wait); + if (lstp_ipmi_gone(ctx)) + return EPOLLHUP; + if (!kfifo_is_empty(&ctx->fifo)) + return EPOLLIN | EPOLLRDNORM; + + return 0; +} + +static const struct file_operations lstp_ipmi_fops = { + .owner = THIS_MODULE, + .open = lstp_ipmi_open, + .read = lstp_ipmi_read, + .write = lstp_ipmi_write, + .release = lstp_ipmi_release, + .poll = lstp_ipmi_poll, +}; + +/* + * @stopped before the wake: this runs ahead of the framework's fence action, so + * a reader woken without it would find nothing changed and sleep again. The + * empty @tx_lock section then waits out a write() that tested @stopped before + * it went up, so no response is sent once the unwind moves on; a send is + * bounded by the USB timeout. + */ +static void lstp_ipmi_stop_and_deregister(void *data) +{ + struct lstp_ipmi_ctx *ctx = data; + + misc_deregister(&ctx->miscdev); + WRITE_ONCE(ctx->stopped, true); + wake_up_all(&ctx->req_wq); + scoped_guard(mutex, &ctx->tx_lock) { + } +} + +/* + * ops->irq_packet. Requests are never NACKed, so drop-oldest is the whole + * flow-control policy. + */ +static void lstp_ipmi_irq_callback(struct lstp_channel *ch, struct lstp_packet *pkt) +{ + struct lstp_ipmi_ctx *ctx = ch->priv; + u16 rx_len = le16_to_cpu(pkt->hdr.length); + struct ipmi_lstp_msg msg; + unsigned long flags; + unsigned int dropped = 0; + + if (rx_len < IPMI_MIN_MSG_LEN || rx_len > IPMI_LSTP_PAYLOAD_MAX) { + dev_warn_ratelimited(ch->dev, "%s: ch_%d: Invalid IPMI request length %u\n", + __func__, ch->ch_id, rx_len); + return; + } + + unsigned int msg_size = sizeof(msg.header) + rx_len; + + msg.header.len = rx_len; + memcpy(msg.payload, pkt->payload, rx_len); + + spin_lock_irqsave(&ctx->lock, flags); + /* + * Skip 0 on wrap: it is the initial value, meaning nothing outstanding, + * so no request may carry it. Pairs with the READ_ONCE in write(). + */ + u8 tag = ctx->msg_count + 1; + + msg.header.msg_num = tag ?: 1; + WRITE_ONCE(ctx->msg_count, msg.header.msg_num); + while (kfifo_avail(&ctx->fifo) < msg_size) { + kfifo_skip(&ctx->fifo); + dropped++; + } + kfifo_in(&ctx->fifo, &msg, msg_size); + spin_unlock_irqrestore(&ctx->lock, flags); + + if (dropped) + dev_warn_ratelimited(ch->dev, "%s: ch_%d: FIFO full, dropped %u stale request(s)\n", + __func__, ch->ch_id, dropped); + + wake_up_all(&ctx->req_wq); +} + +/* + * Build this bind's context and its "ipmi-lstpN" node name. An fd held across + * the unbind keeps the context, and with it the number, until it closes; the + * next bind takes the lowest number free. + */ +static int lstp_ipmi_init(struct lstp_channel *ch, const struct lstp_channel_init_info *info) +{ + struct lstp_ipmi_ctx *ctx = kzalloc_obj(*ctx); + + if (!ctx) + return -ENOMEM; + + kref_init(&ctx->ref); + ctx->ida_id = -1; + ctx->ch = lstp_channel_get(ch); + spin_lock_init(&ctx->lock); + mutex_init(&ctx->tx_lock); + init_waitqueue_head(&ctx->req_wq); + + int ret = kfifo_alloc(&ctx->fifo, BUFFER_SIZE, GFP_KERNEL); + + if (ret) + goto err_put; + + ret = ida_alloc(&lstp_ipmi_ida, GFP_KERNEL); + if (ret < 0) + goto err_put; + ctx->ida_id = ret; + ctx->miscdev.name = kasprintf(GFP_KERNEL, "ipmi-lstp%d", ctx->ida_id); + if (!ctx->miscdev.name) { + ret = -ENOMEM; + goto err_put; + } + + if (fwnode_property_present(ch->fwnode, "label")) + dev_notice_once(ch->dev, + "%s: ch_%d: Firmware \"label\" no longer names the node, which is %s; use a udev symlink for a stable name\n", + __func__, ch->ch_id, ctx->miscdev.name); + + ctx->miscdev.minor = MISC_DYNAMIC_MINOR; + ctx->miscdev.fops = &lstp_ipmi_fops; + ctx->miscdev.parent = ch->dev; + + ch->priv = ctx; + dev_dbg(ch->dev, "%s: ch_%d: Initialized as %s\n", __func__, ch->ch_id, ch->display_name); + return 0; + +err_put: + kref_put(&ctx->ref, lstp_ipmi_ctx_release); + return ret; +} + +static int lstp_ipmi_start(struct lstp_channel *ch, struct lstp_ipmi_ctx *ctx) +{ + int ret = misc_register(&ctx->miscdev); + + if (ret) { + dev_err(ch->dev, "%s: ch_%d: Could not register miscdevice (%pe)\n", __func__, + ch->ch_id, ERR_PTR(ret)); + return ret; + } + + ret = devm_add_action_or_reset(ch->dev, lstp_ipmi_stop_and_deregister, ctx); + if (ret) + return ret; + + dev_info(ch->dev, "%s: ch_%d: Started as %s\n", __func__, ch->ch_id, ch->display_name); + return 0; +} + +static int lstp_ipmi_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id) +{ + struct lstp_channel *ch = lstp_channel_from_auxdev(auxdev); + const struct lstp_channel_init_info *info = dev_get_platdata(&auxdev->dev); + + if (!info) + return -ENODEV; + + int ret = lstp_ipmi_init(ch, info); + + if (ret) + return ret; + + /* Booked first, so it unwinds after the node's deregister action. */ + ret = devm_add_action_or_reset(ch->dev, lstp_ipmi_ctx_put, ch->priv); + if (ret) + return ret; + + return lstp_ipmi_start(ch, ch->priv); +} + +static const struct lstp_ops lstp_ipmi_ops = { + .irq_packet = lstp_ipmi_irq_callback, +}; + +static const struct auxiliary_device_id lstp_ipmi_id_table[] = { { .name = "lstp.ipmi" }, {} }; +MODULE_DEVICE_TABLE(auxiliary, lstp_ipmi_id_table); + +static struct lstp_auxiliary_driver lstp_ipmi_drv = { + .adrv = { + .name = "ipmi", + .probe = lstp_ipmi_probe, + .id_table = lstp_ipmi_id_table, + }, + .ops = &lstp_ipmi_ops, +}; +module_lstp_auxiliary_driver(lstp_ipmi_drv); + +MODULE_IMPORT_NS("LSTP"); +MODULE_DESCRIPTION("LSTP BMC-side IPMI driver"); +MODULE_LICENSE("GPL"); -- 2.43.0 _______________________________________________ Openipmi-developer mailing list [email protected] https://lists.sourceforge.net/lists/listinfo/openipmi-developer
