Files
linux/net/qrtr/mhi.c
Manivannan Sadhasivam 51731792a2 net: qrtr: Drop the MHI auto_queue feature for IPCR DL channels
MHI stack offers the 'auto_queue' feature, which allows the MHI stack to
auto queue the buffers for the RX path (DL channel). Though this feature
simplifies the client driver design, it introduces race between the client
drivers and the MHI stack. For instance, with auto_queue, the 'dl_callback'
for the DL channel may get called before the client driver is fully probed.
This means, by the time the dl_callback gets called, the client driver's
structures might not be initialized, leading to NULL ptr dereference.

Currently, the drivers have to workaround this issue by initializing the
internal structures before calling mhi_prepare_for_transfer_autoqueue().
But even so, there is a chance that the client driver's internal code path
may call the MHI queue APIs before mhi_prepare_for_transfer_autoqueue() is
called, leading to similar NULL ptr dereference. This issue has been
reported on the Qcom X1E80100 CRD machines affecting boot.

So to properly fix all these races, drop the MHI 'auto_queue' feature
altogether and let the client driver (QRTR) manage the RX buffers manually.
In the QRTR driver, queue the RX buffers based on the ring length during
probe and recycle the buffers in 'dl_callback' once they are consumed. This
also warrants removing the setting of 'auto_queue' flag from controller
drivers.

Currently, this 'auto_queue' feature is only enabled for IPCR DL channel.
So only the QRTR client driver requires the modification.

Fixes: 227fee5fc9 ("bus: mhi: core: Add an API for auto queueing buffers for DL channel")
Fixes: 68a838b84e ("net: qrtr: start MHI channel after endpoit creation")
Reported-by: Johan Hovold <johan@kernel.org>
Closes: https://lore.kernel.org/linux-arm-msm/ZyTtVdkCCES0lkl4@hovoldconsulting.com
Suggested-by: Chris Lew <quic_clew@quicinc.com>
Signed-off-by: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>
Reviewed-by: Jeff Hugo <jeff.hugo@oss.qualcomm.com>
Reviewed-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
Acked-by: Jeff Johnson <jjohnson@kernel.org> # drivers/net/wireless/ath/...
Acked-by: Jeff Hugo <jeff.hugo@oss.qualcomm.com>
Acked-by: Paolo Abeni <pabeni@redhat.com>
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20251218-qrtr-fix-v2-1-c7499bfcfbe0@oss.qualcomm.com
2025-12-31 16:24:04 +05:30

231 lines
5.3 KiB
C

// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2018-2020, The Linux Foundation. All rights reserved.
*/
#include <linux/mhi.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <net/sock.h>
#include "qrtr.h"
struct qrtr_mhi_dev {
struct qrtr_endpoint ep;
struct mhi_device *mhi_dev;
struct device *dev;
};
/* From MHI to QRTR */
static void qcom_mhi_qrtr_dl_callback(struct mhi_device *mhi_dev,
struct mhi_result *mhi_res)
{
struct qrtr_mhi_dev *qdev = dev_get_drvdata(&mhi_dev->dev);
int rc;
if (!qdev || (mhi_res->transaction_status && mhi_res->transaction_status != -ENOTCONN))
return;
/* Channel got reset. So just free the buffer */
if (mhi_res->transaction_status == -ENOTCONN) {
devm_kfree(&mhi_dev->dev, mhi_res->buf_addr);
return;
}
rc = qrtr_endpoint_post(&qdev->ep, mhi_res->buf_addr,
mhi_res->bytes_xferd);
if (rc == -EINVAL)
dev_err(qdev->dev, "invalid ipcrouter packet\n");
/* Done with the buffer, now recycle it for future use */
rc = mhi_queue_buf(mhi_dev, DMA_FROM_DEVICE, mhi_res->buf_addr,
mhi_dev->mhi_cntrl->buffer_len, MHI_EOT);
if (rc)
dev_err(&mhi_dev->dev, "Failed to recycle the buffer: %d\n", rc);
}
/* From QRTR to MHI */
static void qcom_mhi_qrtr_ul_callback(struct mhi_device *mhi_dev,
struct mhi_result *mhi_res)
{
struct sk_buff *skb = mhi_res->buf_addr;
if (skb->sk)
sock_put(skb->sk);
consume_skb(skb);
}
/* Send data over MHI */
static int qcom_mhi_qrtr_send(struct qrtr_endpoint *ep, struct sk_buff *skb)
{
struct qrtr_mhi_dev *qdev = container_of(ep, struct qrtr_mhi_dev, ep);
int rc;
if (skb->sk)
sock_hold(skb->sk);
rc = skb_linearize(skb);
if (rc)
goto free_skb;
rc = mhi_queue_skb(qdev->mhi_dev, DMA_TO_DEVICE, skb, skb->len,
MHI_EOT);
if (rc)
goto free_skb;
return rc;
free_skb:
if (skb->sk)
sock_put(skb->sk);
kfree_skb(skb);
return rc;
}
static int qcom_mhi_qrtr_queue_dl_buffers(struct mhi_device *mhi_dev)
{
u32 free_desc;
void *buf;
int ret;
free_desc = mhi_get_free_desc_count(mhi_dev, DMA_FROM_DEVICE);
while (free_desc--) {
buf = devm_kmalloc(&mhi_dev->dev, mhi_dev->mhi_cntrl->buffer_len, GFP_KERNEL);
if (!buf)
return -ENOMEM;
ret = mhi_queue_buf(mhi_dev, DMA_FROM_DEVICE, buf, mhi_dev->mhi_cntrl->buffer_len,
MHI_EOT);
if (ret) {
dev_err(&mhi_dev->dev, "Failed to queue buffer: %d\n", ret);
return ret;
}
}
return 0;
}
static int qcom_mhi_qrtr_probe(struct mhi_device *mhi_dev,
const struct mhi_device_id *id)
{
struct qrtr_mhi_dev *qdev;
int rc;
qdev = devm_kzalloc(&mhi_dev->dev, sizeof(*qdev), GFP_KERNEL);
if (!qdev)
return -ENOMEM;
qdev->mhi_dev = mhi_dev;
qdev->dev = &mhi_dev->dev;
qdev->ep.xmit = qcom_mhi_qrtr_send;
dev_set_drvdata(&mhi_dev->dev, qdev);
/* start channels */
rc = mhi_prepare_for_transfer(mhi_dev);
if (rc)
return rc;
rc = qrtr_endpoint_register(&qdev->ep, QRTR_EP_NID_AUTO);
if (rc)
goto err_unprepare;
rc = qcom_mhi_qrtr_queue_dl_buffers(mhi_dev);
if (rc)
goto err_unregister;
dev_dbg(qdev->dev, "Qualcomm MHI QRTR driver probed\n");
return 0;
err_unregister:
qrtr_endpoint_unregister(&qdev->ep);
err_unprepare:
mhi_unprepare_from_transfer(mhi_dev);
return rc;
}
static void qcom_mhi_qrtr_remove(struct mhi_device *mhi_dev)
{
struct qrtr_mhi_dev *qdev = dev_get_drvdata(&mhi_dev->dev);
qrtr_endpoint_unregister(&qdev->ep);
mhi_unprepare_from_transfer(mhi_dev);
dev_set_drvdata(&mhi_dev->dev, NULL);
}
static const struct mhi_device_id qcom_mhi_qrtr_id_table[] = {
{ .chan = "IPCR" },
{}
};
MODULE_DEVICE_TABLE(mhi, qcom_mhi_qrtr_id_table);
static int __maybe_unused qcom_mhi_qrtr_pm_suspend_late(struct device *dev)
{
struct mhi_device *mhi_dev = container_of(dev, struct mhi_device, dev);
enum mhi_state state;
state = mhi_get_mhi_state(mhi_dev->mhi_cntrl);
/*
* If the device is in suspend state, then no need for the
* client driver to unprepare the channels.
*/
if (state == MHI_STATE_M3)
return 0;
mhi_unprepare_from_transfer(mhi_dev);
return 0;
}
static int __maybe_unused qcom_mhi_qrtr_pm_resume_early(struct device *dev)
{
struct mhi_device *mhi_dev = container_of(dev, struct mhi_device, dev);
enum mhi_state state;
int rc;
state = mhi_get_mhi_state(mhi_dev->mhi_cntrl);
/*
* If the device is in suspend state, we won't unprepare channels
* in suspend callback, therefore no need to prepare channels when
* resume.
*/
if (state == MHI_STATE_M3)
return 0;
rc = mhi_prepare_for_transfer(mhi_dev);
if (rc) {
dev_err(dev, "failed to prepare for autoqueue transfer %d\n", rc);
return rc;
}
return qcom_mhi_qrtr_queue_dl_buffers(mhi_dev);
}
static const struct dev_pm_ops qcom_mhi_qrtr_pm_ops = {
SET_LATE_SYSTEM_SLEEP_PM_OPS(qcom_mhi_qrtr_pm_suspend_late,
qcom_mhi_qrtr_pm_resume_early)
};
static struct mhi_driver qcom_mhi_qrtr_driver = {
.probe = qcom_mhi_qrtr_probe,
.remove = qcom_mhi_qrtr_remove,
.dl_xfer_cb = qcom_mhi_qrtr_dl_callback,
.ul_xfer_cb = qcom_mhi_qrtr_ul_callback,
.id_table = qcom_mhi_qrtr_id_table,
.driver = {
.name = "qcom_mhi_qrtr",
.pm = &qcom_mhi_qrtr_pm_ops,
},
};
module_mhi_driver(qcom_mhi_qrtr_driver);
MODULE_AUTHOR("Chris Lew <clew@codeaurora.org>");
MODULE_AUTHOR("Manivannan Sadhasivam <manivannan.sadhasivam@linaro.org>");
MODULE_DESCRIPTION("Qualcomm IPC-Router MHI interface driver");
MODULE_LICENSE("GPL v2");