Re: [PATCH v8 4/6] net: wwan: t9xx: Add control port
flat view
From: Jakub Kicinski <kuba@kernel.org>
Date: 2026-09-19 23:55:12
Also in:
linux-arm-kernel, linux-doc, linux-mediatek, lkml
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.
quoted hunk ↗ jump to hunk
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 @@
[ ... ]
quoted hunk ↗ jump to hunk
+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); + }
[ ... ]
quoted hunk ↗ jump to hunk
+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?
quoted hunk ↗ jump to hunk
+ + /* 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? [ ... ]
quoted hunk ↗ jump to hunk
+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.
quoted hunk ↗ jump to hunk
+ } 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);
[ ... ]
quoted hunk ↗ jump to hunk
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 @@
[ ... ]
quoted hunk ↗ jump to hunk
+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), +};
[ ... ]
quoted hunk ↗ jump to hunk
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 @@
[ ... ]
quoted hunk ↗ jump to hunk
+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.
[ ... ]
quoted hunk ↗ jump to hunk
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. [ ... ]