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:55 UTC|newest]
Thread overview: 25+ 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 ` Jack Wu
2026-09-14 10:31 ` [PATCH v8 1/6] net: wwan: t9xx: Add PCIe core Jack Wu via B4 Relay
2026-09-14 10:31 ` Jack Wu
2026-09-19 23:54 ` Jakub Kicinski
2026-09-21 7:06 ` [External Mail] " Wu. JackBB (GSM)
2026-09-21 20:23 ` Jakub Kicinski
2026-09-19 23:54 ` Jakub Kicinski
2026-09-21 10:16 ` [External Mail] " Wu. JackBB (GSM)
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 ` Jack Wu
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 ` Jack Wu
2026-09-14 10:31 ` [PATCH v8 4/6] net: wwan: t9xx: Add control port Jack Wu via B4 Relay
2026-09-14 10:31 ` Jack Wu
2026-09-19 23:54 ` Jakub Kicinski [this message]
2026-09-22 6:28 ` Wu. JackBB (GSM)
2026-09-14 10:31 ` [PATCH v8 5/6] net: wwan: t9xx: Add FSM thread Jack Wu via B4 Relay
2026-09-14 10:31 ` Jack Wu
2026-09-19 23:54 ` Jakub Kicinski
2026-09-22 6:44 ` Wu. JackBB (GSM)
2026-09-14 10:31 ` [PATCH v8 6/6] net: wwan: t9xx: Add AT & MBIM WWAN ports Jack Wu via B4 Relay
2026-09-14 10:31 ` Jack Wu
2026-09-19 23:54 ` Jakub Kicinski
2026-09-22 7:06 ` Wu. JackBB (GSM)
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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.