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 v6 5/7] net: wwan: t9xx: Add FSM thread
Date: Mon, 17 Aug 2026 15:36:57 -0700 [thread overview]
Message-ID: <20260817223657.3688752-1-kuba@kernel.org> (raw)
In-Reply-To: <20260811-t9xx_driver_v1-v6-5-2c969fad57c6@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 FSM thread
This adds a finite-state-machine kthread that serializes asynchronous
events from the device and the OS, plus a notifier mechanism so other
modules can react before and after each state transition. It also wires
the FSM into the PCI probe/remove path and into the control plane and
CLDMA transport.
No Link: tags are present in the commit header.
> net: wwan: t9xx: Add FSM thread
>
> The FSM (Finite-state Machine) thread is responsible for
> synchronizing the actions of different modules. The
> asynchronous events from the device or the OS will trigger
> a state transition.
>
> The FSM thread will append it to the event queue when an
> event arrives. It handles the events sequentially. After
> processing the event, the FSM thread notifies other modules
> before and after the state transition.
>
> Seven FSM states are defined. They can transition from one
> state to another, self-transition in some states, and
> transition in some sub-states.
[Severity: Low]
Does the code match this description? enum mtk_fsm_state in the new
mtk_fsm.h defines five states:
FSM_STATE_INVALID, FSM_STATE_OFF, FSM_STATE_ON,
FSM_STATE_BOOTUP, FSM_STATE_READY
There is also no sub-state anywhere: struct mtk_md_fsm and
struct mtk_fsm_param have no sub-state member, and mtk_fsm_switch_state()
only ever assigns fsm->state. Should the count and the sub-state
sentence be updated?
[Severity: Low]
The changelog only mentions the FSM thread, but the patch carries several
other functional changes. Would it help reviewers to split them out or at
least mention them?
- the HS1/HS2/HS3 handshake and runtime-feature protocol
(mtk_fsm_send_hs1_msg(), mtk_fsm_parse_hs2_msg(),
mtk_fsm_send_hs3_msg() and the two ctrl-msg handlers)
- the whole operational CLDMA bring-up and teardown path
(cldma_drv_info_tbl, mtk_cldma_get_drv_info(), mtk_cldma_isr(),
mtk_cldma_dev_init(), mtk_cldma_dev_exit(), including DMA pools,
a workqueue and MSI-X registration)
- an skb leak fix in mtk_cldma_rxq_free(), adding
dev_kfree_skb_any(req->skb) in the rxq->nr_bds branch to code added
by the earlier CLDMA patch of this series, with no Fixes: tag
- removal of all declared CLDMA4 support (CLDMA4 from enum mtk_hif_id,
which shrinks NR_CLDMA from 3 to 2, plus CLDMA4_HW_ID,
CLDMA4_BASE_ADDR, REG_CLDMA_INT_WF_MASK, the cldma4_base_addr and
reg_cldma4_int_mask fields, and the CLDMA4 branch of
mtk_cldma_drv_init_m9xx())
> diff --git a/drivers/net/wwan/t9xx/mtk_ctrl_plane.c b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
> index 6ed47d0745565..1d9dc53482405 100644
> --- a/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
> +++ b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
> @@ -5,10 +5,46 @@
> +static void mtk_ctrl_trans_fsm_state_handler(struct mtk_fsm_param *param,
> + struct mtk_ctrl_blk *ctrl_blk)
> +{
> + struct mtk_md_dev *mdev = ctrl_blk->mdev;
> +
> + switch (param->to) {
> + case FSM_STATE_OFF:
> + ctrl_blk->ops->fsm_indication(mdev, param);
> + ctrl_blk->ops->exit(mdev);
> + break;
> + case FSM_STATE_ON:
> + ctrl_blk->ops->init(mdev);
> + fallthrough;
[Severity: High]
Can a failing ops->init() here lead to a NULL dereference on removal?
ops->init is mtk_pcie_hif_init(), which has several failure exits (kmemdup
ENOMEM, queue-info validation, radix_tree_insert(), mtk_cldma_init(),
mtk_ctrl_trb_srv_init()). In all of them trans->trb_srv[] is left
all-NULL, but the return value is discarded here and the FSM commits
FSM_STATE_ON anyway.
On removal the FSM_STATE_OFF arm above calls ops->exit()
unconditionally:
mtk_pci_dev_exit() -> FSM_EVT_DEV_RM -> mtk_fsm_dev_rm_act()
-> mtk_fsm_enter_off_state() (guard passes, state == ON)
-> mtk_fsm_switch_state(FSM_STATE_OFF)
-> mtk_ctrl_trans_fsm_state_handler() case FSM_STATE_OFF
-> mtk_pcie_hif_exit() -> mtk_ctrl_trb_srv_exit()
and mtk_ctrl_trb_srv_exit() does:
for (i = 0; i < trans->trb_srv_num; i++) {
srv = trans->trb_srv[i];
kthread_stop(srv->trb_thread);
with no NULL check. The same unchecked-return pattern repeats one level
down, where the void mtk_cldma_fsm_state_listener() discards
mtk_cldma_dev_init() errors.
> + default:
> + ctrl_blk->ops->fsm_indication(mdev, param);
> + break;
> + }
> +}
> +
> +static void mtk_ctrl_fsm_state_listener(struct mtk_fsm_param *param, void *data)
> +{
> + struct mtk_ctrl_blk *ctrl_blk = data;
> +
> + mtk_port_mngr_fsm_state_handler(param, ctrl_blk->port_mngr);
> + mtk_ctrl_trans_fsm_state_handler(param, ctrl_blk);
> + mtk_port_mngr_fsm_state_handler_late(param, ctrl_blk->port_mngr);
> +}
[ ... ]
> diff --git a/drivers/net/wwan/t9xx/mtk_fsm.c b/drivers/net/wwan/t9xx/mtk_fsm.c
> new file mode 100644
> index 0000000000000..8cf9b239caeaf
> --- /dev/null
> +++ b/drivers/net/wwan/t9xx/mtk_fsm.c
[ ... ]
> +static int mtk_fsm_parse_hs2_msg(struct fsm_hs_info *hs_info)
> +{
> + struct mtk_md_fsm *fsm = container_of(hs_info, struct mtk_md_fsm, hs_info[hs_info->id]);
> + char *rt_data = ((struct sk_buff *)hs_info->rt_data)->data;
> + enum runtime_feature_support_type cur_ft_spt;
> + struct runtime_feature_entry *rtft_entry;
> + unsigned int ft_id, offset, data_len;
> + int ret = 0;
> +
> + offset = sizeof(struct feature_query);
> + for (ft_id = 0; ft_id < FEATURE_CNT; ft_id++) {
> + if (offset + sizeof(*rtft_entry) > hs_info->rt_data_len)
> + break;
> +
> + rtft_entry = (struct runtime_feature_entry *)(rt_data + offset);
> + ret = mtk_fsm_feature_set_match(&cur_ft_spt,
> + rtft_entry->support_info,
> + hs_info->query_ft_set[ft_id]);
[Severity: Low]
Should this loop key off rtft_entry->feature_id instead of the loop index?
The wire structure carries an explicit id:
struct runtime_feature_entry {
u8 feature_id;
struct runtime_feature_info support_info;
and the host's own emitter fills it in mtk_fsm_append_rtft_entries():
rtft_entry->feature_id = ft_id;
Here the received entry's feature_id is never read, so the parser assumes
the device returns exactly FEATURE_CNT entries in dense ascending order.
If entries are omitted or reordered, one feature's payload is handed to
another feature's handler, or mtk_fsm_feature_set_match() returns -EPROTO
for a feature the device did answer.
> + if (ret < 0)
> + break;
> +
> + data_len = le32_to_cpu(rtft_entry->data_len);
> + if (data_len > hs_info->rt_data_len - offset - sizeof(*rtft_entry))
> + break;
> +
> + if (cur_ft_spt == RTFT_TYPE_MUST_SUPPORT)
> + if (query_rtft_action[ft_id])
> + ret = query_rtft_action[ft_id](fsm->mdev,
> + rtft_entry->data,
> + data_len);
[Severity: Medium]
Is a minimum length check missing here? Only an upper bound on data_len
is applied, so data_len == 0 is accepted and passed to
mtk_port_status_update(), which dereferences the 12-byte
struct mtk_port_enum_msg header before validating the length:
drivers/net/wwan/t9xx/mtk_port.c:mtk_port_status_update() {
if (le16_to_cpu(msg->version) != MTK_PORT_ENUM_VER ||
le32_to_cpu(msg->head_pattern) != MTK_PORT_ENUM_HEAD_PATTERN ||
le32_to_cpu(msg->tail_pattern) != MTK_PORT_ENUM_TAIL_PATTERN)
return -EPROTO;
if (data_len < sizeof(*msg) + ...
}
With data_len == 0 for QUERY_RTFT_ID_MD_PORT_ENUM or
QUERY_RTFT_ID_SAP_PORT_ENUM (both declared MUST-support by the host, so
cur_ft_spt == RTFT_TYPE_MUST_SUPPORT is reachable), rtft_entry->data
equals rt_data + rt_data_len and the read goes past the received message
into skb tailroom / skb_shared_info. Can the handler be given a
guaranteed-minimum length instead?
> + if (ret < 0)
> + break;
> +
> + offset += sizeof(*rtft_entry) + data_len;
> + }
[ ... ]
> +static int mtk_fsm_sap_ctrl_msg_handler(void *__fsm, struct sk_buff *skb)
> +{
[ ... ]
> + hs_info->rt_data = skb;
> + hs_info->rt_data_len = skb->len;
> + ret = mtk_fsm_evt_submit(fsm->mdev, FSM_EVT_STARTUP,
> + hs_info->fsm_flag_hs2, hs_info, sizeof(*hs_info), 0);
> + if (ret == FSM_EVT_RET_FAIL) {
> + dev_kfree_skb(skb);
> + hs_info->rt_data = NULL;
> + }
> +
> + return 0;
> +}
> +
> +static int mtk_fsm_md_ctrl_msg_handler(void *__fsm, struct sk_buff *skb)
> +{
[ ... ]
> + switch (le32_to_cpu(ctrl_msg_h->id)) {
> + case CTRL_MSG_HS2:
> + skb_pull(skb, sizeof(*ctrl_msg_h));
> + hs_info->rt_data = skb;
> + hs_info->rt_data_len = skb->len;
> + ret = mtk_fsm_evt_submit(fsm->mdev, FSM_EVT_STARTUP,
> + hs_info->fsm_flag_hs2, hs_info, sizeof(*hs_info), 0);
> + if (ret != FSM_EVT_RET_FAIL)
> + consumed_skb = true;
> + break;
[Severity: High]
Can two HS2 messages race on the single hs_info->rt_data slot?
The queued event carries only the hs_info pointer (event->data = hs_info),
never the skb, so no event owns its buffer. The writer here runs in the
CLDMA rx_done work while the reader/freer runs in the FSM kthread, and
neither takes a lock or checks whether a buffer is already pending.
If the modem sends a second CTRL_MSG_HS2 while the FSM thread is still in
mtk_fsm_startup_act() for the first one:
rx work FSM kthread
hs_info->rt_data = skb2 mtk_fsm_parse_hs2_msg() /* skb1 */
submit event B
dev_kfree_skb(hs_info->rt_data);
hs_info->rt_data = NULL;
skb1 is leaked, skb2 is freed early, and event B then reaches:
char *rt_data = ((struct sk_buff *)hs_info->rt_data)->data;
on a NULL pointer. In the failure branches above, dev_kfree_skb(skb) can
also free a buffer the FSM thread is parsing right now, and a torn
rt_data / rt_data_len pair makes the parser bound the wrong buffer.
Should the skb be handed to the event itself rather than stored in the
shared hs_info?
> + default:
> + dev_err(fsm->mdev->dev, "Invalid ctrl msg id\n");
> + }
[ ... ]
> +static void mtk_fsm_switch_state(struct mtk_md_fsm *fsm,
> + enum mtk_fsm_state to_state, struct mtk_fsm_evt *event)
> +{
[ ... ]
> + list_for_each_entry(nt, &fsm->pre_notifiers, entry)
> + nt->cb(¶m, nt->data);
> +
> + fsm->state = to_state;
> + fsm->fsm_flag |= event ? event->fsm_flag : FSM_F_DFLT;
[ ... ]
> + list_for_each_entry(nt, &fsm->post_notifiers, entry)
> + nt->cb(¶m, nt->data);
> +}
[Severity: High]
What protects these two list walks against concurrent unregistration?
The traversal runs in the FSM kthread with plain list_for_each_entry() and
calls nt->cb(), while mtk_fsm_notifier_register() /
mtk_fsm_notifier_unregister() do list_add()/list_add_tail()/list_del() and
kfree() from other threads. There is no mutex, no _rcu list variant, no
synchronize_rcu() before the kfree, and no barrier.
The removal path is ordered against the FSM thread only by the blocking
FSM_EVT_DEV_RM event, whose result mtk_pci_dev_exit() ignores and which
can also time out. When that happens:
mtk_trans_ctrl_exit() -> mtk_ctrl_exit()
-> mtk_fsm_notifier_unregister(mdev, MTK_USER_CTRL) /* list_del + kfree */
-> mtk_port_mngr_exit(ctrl_blk) /* frees port_mngr */
while the FSM thread may still be iterating post_notifiers and about to
call nt->cb() on the freed notifier with the freed port_mngr as data.
Since both register and unregister are exported, any future runtime
register/unregister races with every state transition too.
> +static int mtk_fsm_startup_act(struct mtk_md_fsm *fsm, struct mtk_fsm_evt *event)
> +{
[ ... ]
> + } else if (event->fsm_flag & FSM_HS2_DONE_MASK) {
> + ret = mtk_fsm_parse_hs2_msg(hs_info);
> + if (!ret) {
> + mtk_fsm_switch_state(fsm, to_state, event);
> + ret = mtk_fsm_send_hs3_msg(hs_info);
> + }
> + dev_kfree_skb(hs_info->rt_data);
> + hs_info->rt_data = NULL;
> + if (ret)
> + goto hs_err;
> + }
> +
> + if (((fsm->fsm_flag | event->fsm_flag) & fsm->hs_done_flag) == fsm->hs_done_flag) {
> + to_state = FSM_STATE_READY;
> + mtk_fsm_switch_state(fsm, to_state, NULL);
> + }
[Severity: Medium]
Can the FSM reach FSM_STATE_READY when HS3 was never delivered?
mtk_fsm_switch_state() is called before mtk_fsm_send_hs3_msg(), and it
permanently ORs the event flag in:
fsm->fsm_flag |= event ? event->fsm_flag : FSM_F_DFLT;
If mtk_fsm_send_hs3_msg() then fails (-ENOMEM from
__dev_alloc_skb(RTFT_DATA_SIZE), -EPROTO from
mtk_fsm_append_rtft_entries(), or a failed mtk_port_internal_write()), the
hs_err exit does not clear the already-committed FSM_F_*_HS2_DONE bit.
In the dual-handshake configuration the other side can then complete, and
the hs_done_flag test above sees every HS_START/HS2_DONE bit set and moves
to FSM_STATE_READY.
[ ... ]
> +static int mtk_fsm_hs1_handler(u32 status, void *__hs_info)
> +{
> + struct fsm_hs_info *hs_info = __hs_info;
> + struct mtk_md_dev *mdev;
> + struct mtk_md_fsm *fsm;
> +
> + fsm = container_of(hs_info, struct mtk_md_fsm, hs_info[hs_info->id]);
> + mdev = fsm->mdev;
> + mtk_fsm_evt_submit(mdev, FSM_EVT_STARTUP,
> + hs_info->fsm_flag_hs1, hs_info, sizeof(*hs_info), 0);
> + mtk_dev_mask_dev_evt(mdev, hs_info->mhccif_ch);
> + mtk_dev_clear_dev_evt(mdev, hs_info->mhccif_ch);
> +
> + return 0;
> +}
[Severity: Medium]
Is there a way back if the queued startup action later fails?
The HS1 notification is masked and cleared here, before the asynchronous
event result is known. In mtk_fsm_startup_act():
ret = mtk_fsm_ctrl_ch_start(fsm, hs_info, O_NONBLOCK);
if (!ret)
ret = mtk_fsm_send_hs1_msg(hs_info);
if (ret)
goto hs_err;
mtk_fsm_ctrl_ch_start() returns -ENODEV when the control port is not
enabled (which is what happens when mtk_cldma_dev_init() failed, since
mtk_cldma_fsm_state_listener() drops that error), and
mtk_fsm_send_hs1_msg() can return -ENOMEM or a write error. The hs_err
path only logs; the channel stays masked and no retry is scheduled, so the
FSM stays in FSM_STATE_BOOTUP. The only mtk_dev_unmask_dev_evt() calls
for hs_info->mhccif_ch are in mtk_fsm_idle_evt_handler(), which is not
re-entered because the boot-flow-sync channel is masked as well.
[ ... ]
> +static void mtk_fsm_notifier_insert(struct mtk_fsm_notifier *notifier, struct list_head *head)
> +{
> + struct mtk_fsm_notifier *nt;
> +
> + list_for_each_entry(nt, head, entry) {
> + if (notifier->prio > nt->prio) {
> + list_add(¬ifier->entry, nt->entry.prev);
> + return;
> + }
> + }
> + list_add_tail(¬ifier->entry, head);
> +}
[ ... ]
> + spin_lock_irqsave(&fsm->evtq_lock, flags);
> + if (test_bit(EVT_TF_GATECLOSED, &fsm->t_flag)) {
> + spin_unlock_irqrestore(&fsm->evtq_lock, flags);
> + mtk_fsm_evt_put(event);
> + dev_err(mdev->dev, "Failed to add event, fsm dev has been removed!\n");
> + return FSM_EVT_RET_FAIL;
> + }
> +
> + kref_get(&event->kref);
> + if (mode & EVT_MODE_TOHEAD)
> + list_add(&event->entry, &fsm->evtq);
> + else
> + list_add_tail(&event->entry, &fsm->evtq);
> + wake_up_process(fsm->fsm_handler);
> + spin_unlock_irqrestore(&fsm->evtq_lock, flags);
[Severity: High]
Can fsm->fsm_handler be NULL at this wake_up_process()?
mtk_fsm_exit() clears it under the same lock:
spin_lock_irqsave(&fsm->evtq_lock, flags);
handler = fsm->fsm_handler;
fsm->fsm_handler = NULL;
spin_unlock_irqrestore(&fsm->evtq_lock, flags);
if (handler)
kthread_stop(handler);
and only unregisters mtk_fsm_early_bootup_handler() /
mtk_fsm_hs1_handler() at the very end, after a kthread_stop() that can
block while the FSM thread finishes an event. The only thing stopping a
submit in that window is EVT_TF_GATECLOSED, which is set exclusively in
mtk_fsm_dev_rm_act(), i.e. only if a FSM_EVT_DEV_RM was queued and
processed. On the probe error path (mtk_pci_dev_init() -> goto free_fsm
-> mtk_fsm_exit()) no DEV_RM is ever submitted, so the gate is open, and
on removal mtk_pci_dev_exit() ignores the DEV_RM result.
Any device event arriving then (mtk_mhccif_isr_work() ->
mtk_fsm_early_bootup_handler() -> mtk_fsm_idle_evt_handler() ->
mtk_fsm_evt_submit(), or mtk_fsm_hs1_handler()) passes a NULL task_struct
into try_to_wake_up(). mtk_fsm_start() already has the check this path is
missing:
if (!fsm->fsm_handler)
return -EFAULT;
mtk_fsm_exit() also never clears mdev->fsm, so the torn-down (devm-freed)
fsm stays reachable from those callbacks.
[ ... ]
> +int mtk_fsm_init(struct mtk_md_dev *mdev)
> +{
[ ... ]
> + mtk_fsm_hs_info_init(fsm);
> + mtk_dev_register_dev_evt(mdev, DEV_EVT_D2H_BOOT_FLOW_SYNC,
> + mtk_fsm_early_bootup_handler, fsm);
> + mdev->fsm = fsm;
> + return 0;
> +exit:
> + return ret;
> +}
[Severity: Medium]
Should these registrations be checked? mtk_dev_register_dev_evt() reaches
mtk_pci_register_ext_evt(), which returns -EINVAL for bad arguments,
-EFAULT when the channel bitmask intersects an existing registration, and
-ENOMEM from:
cb = kzalloc(sizeof(*cb), GFP_ATOMIC);
if (!cb) {
ret = -ENOMEM;
goto err_spin_unlock;
}
Here the return value is dropped, and the two per-handshake-channel
registrations inside the void mtk_fsm_hs_info_init() cannot report failure
at all. If the BOOT_FLOW_SYNC registration fails,
mtk_fsm_early_bootup_handler() never runs, so
mtk_fsm_idle_evt_handler() never sets fsm->hs_done_flag and never submits
the STARTUP event: probe succeeds, the FSM never leaves FSM_STATE_ON, no
CLDMA is initialized, no port is created, and nothing is logged.
[ ... ]
> diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma.c b/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
> index 097b20d8fbd38..1431c2797eb5d 100644
> --- a/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
> +++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
> @@ -34,12 +34,172 @@
[ ... ]
> +static int mtk_cldma_isr(int irq_id, void *param)
> +{
> + struct cldma_drv_info *drv_info = param;
> + struct mtk_md_dev *mdev;
> + u32 tx_done, rx_done;
> + u32 tx_sta, rx_sta;
> + struct txq *txq;
> + struct rxq *rxq;
> + int i;
> +
> + mdev = drv_info->mdev;
> + drv_info->drv_ops->cldma_get_intr_status(drv_info, &tx_sta, &rx_sta);
> + tx_done = (tx_sta >> QUEUE_XFER_DONE) & 0xFF;
> + rx_done = (rx_sta >> QUEUE_XFER_DONE) & 0xFF;
[Severity: Medium]
What happens to the QUEUE_ERROR bits that get masked but never handled?
mtk_cldma_get_intr_status() masks every asserted bit but clears only the
XFER_DONE byte:
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.c:mtk_cldma_get_intr_status() {
if (*tx_sta) {
/* TX XFER_DONE and QUEUE_ERROR mask */
mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l2timsr0, *tx_sta);
/* TX XFER_DONE clear */
mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l2tisar0,
(*tx_sta) & (0xFF << QUEUE_XFER_DONE));
}
}
The two shifts above then drop QUEUE_ERROR and QUEUE_ACTIVE_START
entirely. QUEUE_ERROR is unmasked only once, in mtk_cldma_txq_alloc() /
mtk_cldma_rxq_alloc(); the done workers re-unmask only QUEUE_XFER_DONE.
So the first queue error permanently disarms error reporting, leaves the
bit latched in L2TISAR0/L2RISAR0, and leaves the halted queue with no log,
no queue reset and no GPD/HWO recovery.
> +
> + if (tx_done) {
> + for (i = 0; i < HW_QUEUE_NUM; i++) {
> + txq = drv_info->txq[i];
> + if (!(tx_done & BIT(i)) || !txq)
> + continue;
> + queue_work(drv_info->wq, &txq->tx_done_work);
> + }
> + }
[ ... ]
> + mtk_pci_clear_irq(mdev, drv_info->pci_ext_irq_id);
> + mtk_pci_unmask_irq(mdev, drv_info->pci_ext_irq_id);
> +
> + return IRQ_HANDLED;
> +}
[ ... ]
> @@ -885,6 +1046,44 @@ static void mtk_cldma_rxq_free(struct cldma_drv_info *drv_info, u32 rxqno)
> +static int mtk_cldma_dev_exit(struct cldma_dev *cd, int hif_id)
> +{
[ ... ]
> + virq_id = mtk_pci_get_virq_id(mdev, drv_info->pci_ext_irq_id);
> + mtk_pci_mask_irq(mdev, drv_info->pci_ext_irq_id);
> + synchronize_irq(virq_id);
> + for (i = 0; i < HW_QUEUE_NUM; i++) {
> + if (drv_info->txq[i])
> + mtk_cldma_txq_free(drv_info, drv_info->txq[i]->txqno);
> + if (drv_info->rxq[i])
> + mtk_cldma_rxq_free(drv_info, drv_info->rxq[i]->rxqno);
> + }
> +
> + flush_workqueue(drv_info->wq);
> + destroy_workqueue(drv_info->wq);
> + dma_pool_destroy(drv_info->bd_dma_pool);
> + dma_pool_destroy(drv_info->gpd_dma_pool);
> + mtk_pci_unregister_irq(mdev, drv_info->pci_ext_irq_id);
> +
> + kfree(drv_info);
> + cd->cldma_drv_info[hif_id] = NULL;
> +
> + return 0;
> +}
[Severity: High]
Is the initial mask plus synchronize_irq() enough to keep the ISR away
from the freed drv_info?
mtk_cldma_isr() re-arms its own vector on every exit:
mtk_pci_clear_irq(mdev, drv_info->pci_ext_irq_id);
mtk_pci_unmask_irq(mdev, drv_info->pci_ext_irq_id);
so an ISR that was already running when the vector was masked unmasks it
again before returning, and synchronize_irq() only waits for that ISR to
finish. QUEUE_ERROR stays armed in hardware, so a later interrupt can
call mtk_cldma_isr() after the workqueue is destroyed (queue_work() on
freed workqueue memory) or after kfree(drv_info)
(drv_info->drv_ops->cldma_get_intr_status(), drv_info->txq[]).
mtk_pci_unregister_irq() only clears the callback pointer:
priv->irq_cb_list[irq_id] = NULL;
priv->irq_cb_data[irq_id] = NULL;
and is not followed by a synchronize_irq(), while mtk_pci_irq_handler()
invokes priv->irq_cb_list[irq_id] directly from hardirq with no lock or
RCU. Should the callback be unregistered and synchronized before the
workqueue, pools and drv_info are released?
[ ... ]
> @@ -1195,6 +1394,27 @@ int mtk_cldma_trb_process(void *dev, struct sk_buff *skb)
> +void mtk_cldma_fsm_state_listener(struct mtk_fsm_param *param, struct mtk_ctrl_trans *trans)
> +{
> + struct cldma_dev *cd = trans->dev;
> + int i;
> +
> + switch (param->to) {
> + case FSM_STATE_BOOTUP:
> + if (param->fsm_flag & FSM_F_SAP_HS_START)
> + mtk_cldma_dev_init(cd, CLDMA0);
> + else if (param->fsm_flag & FSM_F_MD_HS_START)
> + mtk_cldma_dev_init(cd, CLDMA1);
> + break;
> + case FSM_STATE_OFF:
> + for (i = 0; i < NR_CLDMA; i++)
> + mtk_cldma_dev_exit(cd, i);
> + break;
[Severity: High]
Are the TRB service threads guaranteed to be stopped before this runs?
In mtk_ctrl_trans_fsm_state_handler(), the FSM_STATE_OFF arm calls
fsm_indication() (which lands here and tears down every CLDMA hif)
before ops->exit(), and it is ops->exit() -> mtk_pcie_hif_exit() that
clears trans->available and stops the TRB kthreads. Meanwhile
mtk_cldma_submit_tx() reads the published pointers locklessly:
drv_info = cd->cldma_drv_info[que->hif_id];
if (unlikely(!drv_info))
return -EINVAL;
txq = drv_info->txq[que->txqno];
and mtk_cldma_dev_exit() does kfree(drv_info) before clearing
cd->cldma_drv_info[hif_id], so the pointer is dangling across the free.
One way to have work still in flight: mtk_port_ch_disable()'s
wait_event_timeout() can expire, leaving the DISABLE TRB queued for the
service thread to pick up later.
> + default:
> + break;
> + }
> +}
[ ... ]
> diff --git a/drivers/net/wwan/t9xx/pcie/mtk_pci.c b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
> index dbc17da590bc0..bf4e110b485c8 100644
> --- a/drivers/net/wwan/t9xx/pcie/mtk_pci.c
> +++ b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
> @@ -883,22 +883,34 @@ static int mtk_pci_dev_init(struct mtk_md_dev *mdev)
[ ... ]
> static void mtk_pci_dev_exit(struct mtk_md_dev *mdev)
> {
> + mtk_fsm_evt_submit(mdev, FSM_EVT_DEV_RM, 0, NULL, 0,
> + EVT_MODE_BLOCKING | EVT_MODE_TOHEAD);
> mtk_trans_ctrl_exit(mdev);
> + mtk_fsm_exit(mdev);
> }
[Severity: High]
What happens here when the blocking removal event does not complete?
mtk_fsm_evt_submit() returns FSM_EVT_RET_FAIL if its kzalloc() fails, and
the result is discarded. In that case the FSM_STATE_OFF notifier chain
never runs, so ctrl_blk->ops->exit() = mtk_pcie_hif_exit() is never
called: the CLDMA IRQ callback stays registered, the CLDMA workqueue and
DMA pools stay alive, the TRB service kthreads keep running and
trans->available stays 1. mtk_trans_ctrl_exit() does not compensate:
drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c:mtk_trans_ctrl_exit() {
mtk_ctrl_exit(mdev);
return 0;
}
and the devm-allocated trans/ctrl_blk/fsm are released once remove()
finishes, leaving those kthreads and the IRQ callback pointing at freed
memory. The wait can also expire (BLOCKING_EVT_TIMEOUT is 60s) without
dequeuing or cancelling the event:
ret = wait_event_timeout(fsm->evt_waitq,
(event->status != 0), BLOCKING_EVT_TIMEOUT);
so teardown then races with an FSM thread still using ctrl_blk/trans.
> static int mtk_pci_dev_start(struct mtk_md_dev *mdev)
> {
> + mtk_fsm_evt_submit(mdev, FSM_EVT_DEV_ADD, 0, NULL, 0, 0);
> + mtk_fsm_start(mdev);
> return 0;
> }
[Severity: Medium]
Should these two return values be propagated? mtk_pci_dev_start() is
declared int and mtk_pci_probe() already has an if (ret) handler for it,
but both calls are discarded and 0 is returned unconditionally.
mtk_fsm_evt_submit() returns FSM_EVT_RET_FAIL when its
kzalloc(sizeof(*event), ...) fails, so probe can succeed while the FSM
never leaves FSM_STATE_INVALID. Every later handshake event is then
rejected by mtk_fsm_startup_act():
if (fsm->state != FSM_STATE_ON && fsm->state != FSM_STATE_BOOTUP) {
ret = -EPROTO;
goto free_rt_data;
}
leaving the device unusable with only a generic error log.
> diff --git a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
> index a3ff56ddf86fc..4b9c9db6ad715 100644
> --- a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
> +++ b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
> @@ -29,7 +29,6 @@
> enum mtk_hif_id {
> CLDMA0,
> CLDMA1,
> - CLDMA4,
> NR_CLDMA
> };
next prev parent reply other threads:[~2026-08-17 22:36 UTC|newest]
Thread overview: 15+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-11 7:14 [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jack Wu via B4 Relay
2026-08-11 7:14 ` [PATCH v6 1/7] net: wwan: t9xx: Add PCIe core Jack Wu via B4 Relay
2026-08-17 22:35 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 2/7] net: wwan: t9xx: Add control plane transaction layer Jack Wu via B4 Relay
2026-08-17 22:36 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 3/7] net: wwan: t9xx: Add control DMA interface Jack Wu via B4 Relay
2026-08-17 22:36 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 4/7] net: wwan: t9xx: Add control port Jack Wu via B4 Relay
2026-08-11 7:14 ` [PATCH v6 5/7] net: wwan: t9xx: Add FSM thread Jack Wu via B4 Relay
2026-08-17 22:36 ` Jakub Kicinski [this message]
2026-08-11 7:14 ` [PATCH v6 6/7] net: wwan: t9xx: Add AT & MBIM WWAN ports Jack Wu via B4 Relay
2026-08-17 22:36 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 7/7] net: wwan: t9xx: Add maintainers entry Jack Wu via B4 Relay
2026-08-17 22:38 ` Jakub Kicinski
2026-08-17 22:39 ` [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver 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=20260817223657.3688752-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