From: Jakub Kicinski <kuba@kernel.org>
To: jackbb_wu@compal.com
Cc: Jakub Kicinski <kuba@kernel.org>,
loic.poulain@oss.qualcomm.com, ryazanov.s.a@gmail.com,
johannes@sipsolutions.net, andrew+netdev@lunn.ch,
davem@davemloft.net, edumazet@google.com, pabeni@redhat.com,
wen-zhi.huang@mediatek.com, shi-wei.yeh@mediatek.com,
Minano.tseng@mediatek.com, matthias.bgg@gmail.com,
angelogioacchino.delregno@collabora.com, horms@kernel.org,
corbet@lwn.net, skhan@linuxfoundation.org, robert_yu@compal.com,
Jeff_Chang@compal.com, linux-kernel@vger.kernel.org,
netdev@vger.kernel.org, linux-arm-kernel@lists.infradead.org,
linux-mediatek@lists.infradead.org, linux-doc@vger.kernel.org
Subject: Re: [PATCH v8 4/6] net: wwan: t9xx: Add control port
Date: Sat, 19 Sep 2026 16:54:51 -0700 [thread overview]
Message-ID: <20260919235451.839752-1-kuba@kernel.org> (raw)
In-Reply-To: <20260914-t9xx_driver_v1-v8-4-5206c2e6bea0@compal.com>
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: wwan: t9xx: Add control port
This adds the t9xx control port layer: a port manager that creates and
destroys ports, keeps them in a global "stale list" across driver
re-creation, and moves data between port I/O and CLDMA via TRBs. It also
adds the internal port operations (open, close, write, recv_register) and
wires the port configuration into the control plane init path.
A few questions about the new code below.
> diff --git a/drivers/net/wwan/t9xx/mtk_port.c b/drivers/net/wwan/t9xx/mtk_port.c
> new file mode 100644
> index 0000000000000..d2a5db11afd6b
> --- /dev/null
> +++ b/drivers/net/wwan/t9xx/mtk_port.c
> @@ -0,0 +1,873 @@
[ ... ]
> +static int mtk_port_tbl_add(struct mtk_port_mngr *port_mngr, struct mtk_port *port)
> +{
> + int ret;
> +
> + ret = radix_tree_insert(&port_mngr->port_tbl[MTK_PORT_TBL_TYPE(port->info.rx_ch)],
> + port->info.rx_ch & 0xFFF, port);
> + if (ret)
> + dev_err(port_mngr->ctrl_blk->mdev->dev,
> + "port(%s) add to port_tbl failed, return %d\n",
> + port->info.name, ret);
> + else
> + port_mngr->port_cnt++;
> +
> + return ret;
> +}
> +
> +static void mtk_port_tbl_del(struct mtk_port_mngr *port_mngr, struct mtk_port *port)
> +{
> + radix_tree_delete(&port_mngr->port_tbl[MTK_PORT_TBL_TYPE(port->info.rx_ch)],
> + port->info.rx_ch & 0xFFF);
> + port_mngr->port_cnt--;
> +}
> +
> +static struct mtk_port *mtk_port_restore_from_stale_list(struct mtk_port_mngr *port_mngr,
> + struct mtk_stale_list *s_list)
> +{
> + struct mtk_port *port, *next_port;
> + int ret;
> +
> + mutex_lock(&port_mngr_grp_mtx);
> + list_for_each_entry_safe(port, next_port, &s_list->ports, stale_entry) {
> + kref_get(&port->kref);
> + list_del(&port->stale_entry);
> + ret = mtk_port_tbl_add(port_mngr, port);
> + if (ret) {
> + list_add_tail(&port->stale_entry, &s_list->ports);
> + kref_put(&port->kref, mtk_port_release);
> + mutex_unlock(&port_mngr_grp_mtx);
> + dev_err(port_mngr->ctrl_blk->mdev->dev,
> + "Failed when adding (%s) to port mngr\n",
> + port->info.name);
> + return ERR_PTR(ret);
> + }
[ ... ]
> +static int mtk_port_tbl_create(struct mtk_port_mngr *port_mngr, struct mtk_port_cfg *cfg,
> + const int port_cnt, struct mtk_stale_list *s_list)
> +{
> + struct mtk_port_cfg *dflt_port;
> + struct mtk_port *port;
> + int i;
> +
> + INIT_RADIX_TREE(&port_mngr->port_tbl[PORT_TBL_SAP], GFP_KERNEL);
> + INIT_RADIX_TREE(&port_mngr->port_tbl[PORT_TBL_MD], GFP_KERNEL);
> +
> + mtk_port_restore_from_stale_list(port_mngr, s_list);
[Severity: Medium]
mtk_port_restore_from_stale_list() reports failure through ERR_PTR(), but
mtk_port_tbl_create() discards the result. Should this be checked with
IS_ERR(), keeping in mind the success return is NULL rather than a port?
When mtk_port_tbl_add() -> radix_tree_insert() fails (-ENOMEM for a tree
node, or -EEXIST), the helper re-queues the current port, drops the extra
kref and returns early, so the remaining stale ports are never restored:
they keep PORT_S_ON_STALE_LIST set and port->port_mngr == NULL.
Since the error is swallowed, the loop below then finds nothing for those
rx_ch values:
if (!mtk_port_search_by_id(port_mngr, dflt_port->rx_ch)) {
port = mtk_port_alloc_and_add(port_mngr, dflt_port);
and creates a second port object for the same channel, while
mtk_port_mngr_init() still returns 0.
Does the user still holding the stale handle then end up with a port whose
port_mngr is NULL and PORT_S_ENABLE clear, so every mtk_port_send_data()
on it fails in mtk_port_status_check() with -ENODEV?
> +
> + /* copy ports from static port cfg table */
> + for (i = 0; i < port_cnt; i++) {
> + dflt_port = cfg + i;
> + if (!mtk_port_search_by_id(port_mngr, dflt_port->rx_ch)) {
> + port = mtk_port_alloc_and_add(port_mngr, dflt_port);
> + if (IS_ERR(port))
> + return PTR_ERR(port);
> + }
> + }
> +
> + return 0;
> +}
> +
> +static void mtk_port_tbl_destroy(struct mtk_port_mngr *port_mngr, struct mtk_stale_list *s_list)
> +{
> + struct radix_tree_iter iter;
> + struct mtk_port *port;
> + void __rcu **slot;
> + int tbl_type;
> +
> + tbl_type = PORT_TBL_SAP;
> + do {
> + radix_tree_for_each_slot(slot, &port_mngr->port_tbl[tbl_type], &iter, 0) {
> + port = radix_tree_deref_slot(slot);
> + if (!port)
> + continue;
> + ports_ops[port->info.type]->disable(port);
> + }
[Severity: Medium]
radix_tree_deref_slot() is rcu_dereference(*slot), and this loop runs in
plain process context (driver removal and the mtk_port_mngr_init() error
path) with no rcu_read_lock() held. With CONFIG_PROVE_RCU=y, does this
produce a "suspicious rcu_dereference_check() usage" splat on every
teardown?
Wrapping the loop in rcu_read_lock() would not work either, since
ports_ops[]->disable() sleeps: mtk_port_ch_disable() does a GFP_KERNEL
skb allocation and then wait_event_timeout() for up to
MTK_DFLT_TRB_TIMEOUT.
More generally, the synchronisation the radix-tree API asks callers to
provide looks incomplete here. The mutators run under port_mngr_grp_mtx
from mtk_port_restore_from_stale_list() and mtk_port_free_or_backup(), but
with no lock at all from mtk_port_tbl_create() -> mtk_port_alloc_and_add()
-> mtk_port_tbl_add(); the readers mtk_port_search_by_id() and
mtk_port_search_by_name() hold neither the mutex nor rcu_read_lock(), and
mtk_port_release() frees a tree-published object with a plain kfree():
ports_ops[port->info.type]->exit(port);
kfree(port);
Would a single writer lock covering all mutators (including the port_cnt
updates) plus RCU-deferred freeing be more appropriate?
[ ... ]
> +static struct mtk_stale_list *mtk_port_stale_list_init(struct mtk_ctrl_blk *ctrl_blk, int *dev_id)
> +{
> + struct mtk_stale_list *s_list;
> +
> + s_list = mtk_port_stale_list_search(ctrl_blk->mdev->dev_str);
> + if (!s_list) {
> + s_list = mtk_port_stale_list_create(ctrl_blk);
> + if (unlikely(!s_list))
> + return NULL;
> + }
> +
> + mutex_lock(&port_mngr_grp_mtx);
> + if (s_list->dev_id < 0) {
> + *dev_id = ida_alloc_range(&ccci_dev_ids, 0, MTK_DFLT_MAX_DEV_CNT - 1, GFP_KERNEL);
[Severity: Low]
The ida_alloc_range() return value is stored in *dev_id without being
tested, and mtk_port_stale_list_init() still reports success. On -ENOMEM,
or -ENOSPC once MTK_DFLT_MAX_DEV_CNT (10) devices are bound, does an errno
end up in port_mngr->dev_id while mtk_port_mngr_init() returns 0?
That value is later passed to ida_free() unconditionally in
mtk_port_stale_list_exit() below, while the other two ida_free() call
sites in this file guard it, in mtk_port_release():
list_empty(&s_list->ports) && s_list->dev_id >= 0) {
ida_free(&ccci_dev_ids, s_list->dev_id);
and in mtk_port_stale_list_grp_cleanup():
if (s_list->dev_id >= 0)
ida_free(&ccci_dev_ids, s_list->dev_id);
The bogus free itself is harmless, since ida_free() starts with
"if ((int)id < 0) return;", so this is about the unchecked allocation and
the inconsistency with the guarded call sites.
> + } else {
> + *dev_id = s_list->dev_id;
> + s_list->dev_id = -1;
> + }
> + mutex_unlock(&port_mngr_grp_mtx);
> +
> + return s_list;
> +}
> +
> +static void mtk_port_stale_list_exit(struct mtk_ctrl_blk *ctrl_blk,
> + struct mtk_stale_list *s_list, int dev_id)
> +{
> + if (!s_list)
> + return;
> + mutex_lock(&port_mngr_grp_mtx);
> + if (list_empty(&s_list->ports)) {
> + ida_free(&ccci_dev_ids, dev_id);
[ ... ]
> diff --git a/drivers/net/wwan/t9xx/mtk_port.h b/drivers/net/wwan/t9xx/mtk_port.h
> new file mode 100644
> index 0000000000000..a0248d7f939ae
> --- /dev/null
> +++ b/drivers/net/wwan/t9xx/mtk_port.h
> @@ -0,0 +1,150 @@
[ ... ]
> +enum mtk_port_flag {
> + PORT_F_DFLT = 0,
> + PORT_F_BLOCKING = BIT(1),
> + PORT_F_ALLOW_DROP = BIT(2),
[Severity: Low]
This isn't a bug, but PORT_F_ALLOW_DROP has no reader anywhere. It is set
on every entry of port_cfg_m9xx[], yet the only runtime consumer of
port->info.flags is mtk_port_internal_write():
return mtk_port_send_data(port, skb,
!!(port->info.flags & PORT_F_BLOCKING),
!!(port->info.flags &
(PORT_F_BLOCKING | PORT_F_FORCE_SEND)));
Ports with and without the flag behave identically. Could it be dropped
until the code that honours it is added?
> + PORT_F_FORCE_SEND = BIT(6),
> +};
[ ... ]
> diff --git a/drivers/net/wwan/t9xx/mtk_port_io.c b/drivers/net/wwan/t9xx/mtk_port_io.c
> new file mode 100644
> index 0000000000000..27b490fde40f2
> --- /dev/null
> +++ b/drivers/net/wwan/t9xx/mtk_port_io.c
> @@ -0,0 +1,247 @@
[ ... ]
> +static void mtk_port_internal_enable(struct mtk_port *port)
> +{
> + int ret;
> +
> + if (test_bit(PORT_S_ENABLE, &port->status))
> + return;
> +
> + ret = mtk_port_ch_enable(port);
> + if (ret && ret != -EBUSY) {
> + /* On -ETIMEDOUT the ENABLE trb may still be queued; queue a
> + * DISABLE behind it so the channel does not end up armed with
> + * no software owner.
> + */
> + mtk_port_ch_disable(port);
> + return;
> + }
> +
> + set_bit(PORT_S_WR, &port->status);
> + set_bit(PORT_S_ENABLE, &port->status);
> +}
> +
> +static void mtk_port_internal_disable(struct mtk_port *port)
> +{
> + if (!test_and_clear_bit(PORT_S_ENABLE, &port->status))
> + return;
> +
> + clear_bit(PORT_S_WR, &port->status);
> + mtk_port_ch_disable(port);
> +}
[Severity: High]
mtk_port_internal_disable() clears PORT_S_ENABLE and then discards the
return value of mtk_port_ch_disable(), which can fail with -ENOMEM (skb
allocation), -EIO (mtk_pcie_hif_submit_skb() whenever
atomic_read(&trans->available) == 0) or -ETIMEDOUT after
MTK_DFLT_TRB_TIMEOUT. Can this leave the software state claiming the
channel is down while trans->usr_cnt[hif_id][txqno] still counts a user?
The only decrement is in mtk_ch_status_check():
case TRB_CMD_DISABLE:
if (trans->usr_cnt[que->hif_id][que->txqno] > 0) {
trans->usr_cnt[que->hif_id][que->txqno]--;
if (!trans->usr_cnt[que->hif_id][que->txqno])
break;
}
trb->status = -EBUSY;
and mtk_ctrl_ch_flush() / mtk_ctrl_chs_flush() complete queued
ENABLE/DISABLE TRBs with
trb->status = -EIO;
trb->trb_complete(skb);
without passing through mtk_ch_status_check() at all.
For a modem that stops responding during teardown:
FSM_STATE_OFF
mtk_port_disable() -> mtk_port_ch_disable() returns -ETIMEDOUT, the
DISABLE skb is still queued
mtk_port_internal_disable() ignores it and clears PORT_S_ENABLE
ops->exit() -> mtk_pcie_hif_exit()
mtk_cldma_exit() frees drv_info->txq[]/rxq[]
mtk_ctrl_trb_srv_exit() -> kthread_stop() -> mtk_ctrl_chs_flush()
completes the queued DISABLE with -EIO, usr_cnt stays at 1
On the next bootup the ENABLE raises usr_cnt to 2, so
mtk_ch_status_check() takes the "already open" branch,
mtk_cldma_check_ch_cfg() finds drv_info->txq/rxq == NULL and returns
-EINVAL, and mtk_port_ch_enable() fails. The compensating
mtk_port_ch_disable() in mtk_port_internal_enable() then only takes
usr_cnt from 2 back to 1, never to 0.
Does that make the control channel unusable for the rest of the probe, so
the MD/SAP handshake never completes and the modem never reaches
FSM_STATE_READY, with every later boot cycle repeating it?
Related question on the same function: is -EBUSY safe to treat as a
successful enable without distinguishing where it came from?
mtk_cldma_open() rolls the usr_cnt reference back at its out: label when
it returns -EBUSY, while mtk_ch_status_check() keeps it.
[ ... ]
> diff --git a/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c b/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c
> index 0019b64b037af..050416e0b914d 100644
> --- a/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c
> +++ b/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c
> @@ -21,7 +22,24 @@ static const struct queue_info mtk_queue_info_m9xx[] = {
> Q_MTU_3_5K, Q_MTU_3_5K, TX_GPD_NUM, RX_GPD_NUM, Q_FRAG_3_5K, Q_FRAG_3_5K, 0},
> };
>
> +static const struct mtk_port_cfg port_cfg_m9xx[] = {
> + {CCCI_CONTROL_TX, CCCI_CONTROL_RX, PORT_TYPE_INTERNAL, "MDCTRL",
> + PORT_F_ALLOW_DROP},
> + {CCCI_SAP_CONTROL_TX, CCCI_SAP_CONTROL_RX, PORT_TYPE_INTERNAL, "SAPCTRL",
> + PORT_F_ALLOW_DROP},
> +};
These are the two descriptors that request the PORT_F_ALLOW_DROP behaviour
mentioned above.
[ ... ]
next prev parent reply other threads:[~2026-09-19 23:54 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-14 10:31 [PATCH v8 0/6] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jack Wu via B4 Relay
2026-09-14 10:31 ` [PATCH v8 1/6] net: wwan: t9xx: Add PCIe core Jack Wu via B4 Relay
2026-09-19 23:54 ` Jakub Kicinski
2026-09-19 23:54 ` Jakub Kicinski
2026-09-14 10:31 ` [PATCH v8 2/6] net: wwan: t9xx: Add control plane transaction layer Jack Wu via B4 Relay
2026-09-14 10:31 ` [PATCH v8 3/6] net: wwan: t9xx: Add control DMA interface Jack Wu via B4 Relay
2026-09-14 10:31 ` [PATCH v8 4/6] net: wwan: t9xx: Add control port Jack Wu via B4 Relay
2026-09-19 23:54 ` Jakub Kicinski [this message]
2026-09-14 10:31 ` [PATCH v8 5/6] net: wwan: t9xx: Add FSM thread Jack Wu via B4 Relay
2026-09-19 23:54 ` Jakub Kicinski
2026-09-14 10:31 ` [PATCH v8 6/6] net: wwan: t9xx: Add AT & MBIM WWAN ports Jack Wu via B4 Relay
2026-09-19 23:54 ` Jakub Kicinski
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260919235451.839752-1-kuba@kernel.org \
--to=kuba@kernel.org \
--cc=Jeff_Chang@compal.com \
--cc=Minano.tseng@mediatek.com \
--cc=andrew+netdev@lunn.ch \
--cc=angelogioacchino.delregno@collabora.com \
--cc=corbet@lwn.net \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=jackbb_wu@compal.com \
--cc=johannes@sipsolutions.net \
--cc=linux-arm-kernel@lists.infradead.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mediatek@lists.infradead.org \
--cc=loic.poulain@oss.qualcomm.com \
--cc=matthias.bgg@gmail.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=robert_yu@compal.com \
--cc=ryazanov.s.a@gmail.com \
--cc=shi-wei.yeh@mediatek.com \
--cc=skhan@linuxfoundation.org \
--cc=wen-zhi.huang@mediatek.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®