nfcmrvl_nci_unregister_dev() destroys the NCI workqueues before draining the firmware receive worker. That worker can still submit NCI commands, while the later abort can release firmware state that it is processing. The firmware timeout can also complete the same download in parallel with the receive worker. Add active and stopping states protected by the firmware receive lock. Once completion or teardown starts, reject new firmware receives and wait for active receive entry points. Shut down the firmware timer, cancel the receive and timeout workers, and purge their queue before releasing firmware state. Perform this shutdown before unregistering the NCI device so its command workqueue remains available until firmware work has ended. Drain the NCI command timer, command worker, and command queue as one dependency chain. Delete the timer before and after cancelling command work so a detached timer callback cannot queue work which rearms the timer. This also removes command skbs which cannot run after command admission is closed. Queue timeout completion on the firmware workqueue and claim completion under the receive lock so firmware state is released once. Hold received frames until the initial reset pulse has finished, preventing an error path from asserting reset while the start path later deasserts it. Report firmware completion with the positive errno value expected by the NFC API. Reject a new download while the current one is active or still completing, so its firmware pointers and state cannot be replaced during asynchronous processing or cleanup. Initialize the firmware timer while setting up the receive workqueue. This makes teardown valid even when a download was never started. Serialize the stopping transition with firmware start through the NFC device lock. Use non-synchronous timeout-work cancellation during completion because completion can run from the timeout work itself. Discovered during an AI-assisted source audit using OpenAI Codex, gpt-5.6-sol. Patch development and test planning used the same assistant. Confirmed with an unmodified kernel and a pty-backed user-space controller exercising firmware start, timeout, completion, and device teardown; no error injection was used. With the fix applied, the reproducer passed 500 firmware teardown cycles and all five timer-boundary modes under KASAN, UBSAN, and lockdep, plus 200 cycles under strict-mode KCSAN. No physical Marvell controller was available, and no in-tree selftest covers this firmware path. Fixes: 3194c6870158 ("NFC: nfcmrvl: add firmware download support") Signed-off-by: Shubham Antil --- drivers/nfc/nfcmrvl/fw_dnld.c | 243 +++++++++++++++++++++++++--------- drivers/nfc/nfcmrvl/fw_dnld.h | 11 ++ drivers/nfc/nfcmrvl/main.c | 5 +- 3 files changed, 192 insertions(+), 67 deletions(-) diff --git a/drivers/nfc/nfcmrvl/fw_dnld.c b/drivers/nfc/nfcmrvl/fw_dnld.c index 8b9d52573..6e8608655 100644 --- a/drivers/nfc/nfcmrvl/fw_dnld.c +++ b/drivers/nfc/nfcmrvl/fw_dnld.c @@ -90,22 +90,50 @@ static struct sk_buff *alloc_lc_skb(struct nfcmrvl_private *priv, uint8_t plen) return skb; } -static void fw_dnld_over(struct nfcmrvl_private *priv, u32 error) +static void fw_dnld_stop_cmds(struct nci_dev *ndev) { - if (priv->fw_dnld.fw) { - release_firmware(priv->fw_dnld.fw); - priv->fw_dnld.fw = NULL; - priv->fw_dnld.header = NULL; - priv->fw_dnld.binary_config = NULL; - } - - atomic_set(&priv->ndev->cmd_cnt, 0); - - if (timer_pending(&priv->ndev->cmd_timer)) - timer_delete_sync(&priv->ndev->cmd_timer); + /* + * The timer callback can queue cmd_work after a workqueue flush, while + * cmd_work can rearm the timer. Drain both directions before purging + * commands which belong to the firmware state machine. + */ + timer_delete_sync(&ndev->cmd_timer); + atomic_set(&ndev->cmd_cnt, 0); + cancel_work_sync(&ndev->cmd_work); + timer_delete_sync(&ndev->cmd_timer); + skb_queue_purge(&ndev->cmd_q); +} - if (timer_pending(&priv->fw_dnld.timer)) - timer_delete_sync(&priv->fw_dnld.timer); +static void fw_dnld_over(struct nfcmrvl_private *priv, int error) +{ + struct nfcmrvl_fw_dnld *fw_dnld = &priv->fw_dnld; + char name[NFC_FIRMWARE_NAME_MAXSIZE + 1]; + u32 result = error < 0 ? -error : error; + unsigned long flags; + + spin_lock_irqsave(&fw_dnld->rx_lock, flags); + if (!fw_dnld->active) { + spin_unlock_irqrestore(&fw_dnld->rx_lock, flags); + return; + } + fw_dnld->active = false; + fw_dnld->completing = true; + spin_unlock_irqrestore(&fw_dnld->rx_lock, flags); + + strscpy(name, fw_dnld->name); + wait_event(fw_dnld->rx_wait, !READ_ONCE(fw_dnld->rx_active)); + timer_delete_sync(&fw_dnld->timer); + /* Timeout and receive work share a single-thread workqueue. */ + cancel_work(&fw_dnld->timeout_work); + fw_dnld_stop_cmds(priv->ndev); + skb_queue_purge(&fw_dnld->rx_q); + + if (fw_dnld->fw) { + release_firmware(fw_dnld->fw); + fw_dnld->fw = NULL; + fw_dnld->header = NULL; + fw_dnld->binary_config = NULL; + } nfc_info(priv->dev, "FW loading over (%d)]\n", error); @@ -114,7 +142,11 @@ static void fw_dnld_over(struct nfcmrvl_private *priv, u32 error) nfcmrvl_chip_halt(priv); } - nfc_fw_download_done(priv->ndev->nfc_dev, priv->fw_dnld.name, error); + nfc_fw_download_done(priv->ndev->nfc_dev, name, result); + + spin_lock_irqsave(&fw_dnld->rx_lock, flags); + fw_dnld->completing = false; + spin_unlock_irqrestore(&fw_dnld->rx_lock, flags); } static void fw_dnld_timeout(struct timer_list *t) @@ -122,6 +154,18 @@ static void fw_dnld_timeout(struct timer_list *t) struct nfcmrvl_private *priv = timer_container_of(priv, t, fw_dnld.timer); + queue_work(priv->fw_dnld.rx_wq, &priv->fw_dnld.timeout_work); +} + +static void fw_dnld_timeout_work(struct work_struct *work) +{ + struct nfcmrvl_fw_dnld *fw_dnld = container_of(work, + struct nfcmrvl_fw_dnld, + timeout_work); + struct nfcmrvl_private *priv = container_of(fw_dnld, + struct nfcmrvl_private, + fw_dnld); + nfc_err(priv->dev, "FW loading timeout"); priv->fw_dnld.state = STATE_RESET; fw_dnld_over(priv, -ETIMEDOUT); @@ -395,62 +439,74 @@ static int process_state_boot(struct nfcmrvl_private *priv, static void fw_dnld_rx_work(struct work_struct *work) { - int ret; - struct sk_buff *skb; struct nfcmrvl_fw_dnld *fw_dnld = container_of(work, struct nfcmrvl_fw_dnld, rx_work); struct nfcmrvl_private *priv = container_of(fw_dnld, struct nfcmrvl_private, fw_dnld); + struct sk_buff *skb; + unsigned long flags; + int ret; - while ((skb = skb_dequeue(&fw_dnld->rx_q))) { - nfc_send_to_raw_sock(priv->ndev->nfc_dev, skb, - RAW_PAYLOAD_NCI, NFC_DIRECTION_RX); - switch (fw_dnld->state) { - case STATE_RESET: - ret = process_state_reset(priv, skb); - break; - case STATE_INIT: - ret = process_state_init(priv, skb); - break; - case STATE_SET_REF_CLOCK: - ret = process_state_set_ref_clock(priv, skb); - break; - case STATE_SET_HI_CONFIG: - ret = process_state_set_hi_config(priv, skb); - break; - case STATE_OPEN_LC: - ret = process_state_open_lc(priv, skb); - break; - case STATE_FW_DNLD: - ret = process_state_fw_dnld(priv, skb); - break; - case STATE_CLOSE_LC: - ret = process_state_close_lc(priv, skb); - break; - case STATE_BOOT: - ret = process_state_boot(priv, skb); - break; - default: - ret = -EFAULT; - } + skb = skb_dequeue(&fw_dnld->rx_q); + if (!skb) + return; - kfree_skb(skb); + nfc_send_to_raw_sock(priv->ndev->nfc_dev, skb, + RAW_PAYLOAD_NCI, NFC_DIRECTION_RX); + switch (fw_dnld->state) { + case STATE_RESET: + ret = process_state_reset(priv, skb); + break; + case STATE_INIT: + ret = process_state_init(priv, skb); + break; + case STATE_SET_REF_CLOCK: + ret = process_state_set_ref_clock(priv, skb); + break; + case STATE_SET_HI_CONFIG: + ret = process_state_set_hi_config(priv, skb); + break; + case STATE_OPEN_LC: + ret = process_state_open_lc(priv, skb); + break; + case STATE_FW_DNLD: + ret = process_state_fw_dnld(priv, skb); + break; + case STATE_CLOSE_LC: + ret = process_state_close_lc(priv, skb); + break; + case STATE_BOOT: + ret = process_state_boot(priv, skb); + break; + default: + ret = -EFAULT; + } - if (ret != 0) { - nfc_err(priv->dev, "FW loading error"); - fw_dnld_over(priv, ret); - break; - } + kfree_skb(skb); + + if (ret) { + nfc_err(priv->dev, "FW loading error"); + fw_dnld_over(priv, ret); } + + spin_lock_irqsave(&fw_dnld->rx_lock, flags); + if (!fw_dnld->stopping && !fw_dnld->starting && fw_dnld->active && + !skb_queue_empty(&fw_dnld->rx_q)) + queue_work(fw_dnld->rx_wq, &fw_dnld->rx_work); + spin_unlock_irqrestore(&fw_dnld->rx_lock, flags); } int nfcmrvl_fw_dnld_init(struct nfcmrvl_private *priv) { char name[32]; + spin_lock_init(&priv->fw_dnld.rx_lock); + init_waitqueue_head(&priv->fw_dnld.rx_wait); INIT_WORK(&priv->fw_dnld.rx_work, fw_dnld_rx_work); + INIT_WORK(&priv->fw_dnld.timeout_work, fw_dnld_timeout_work); + timer_setup(&priv->fw_dnld.timer, fw_dnld_timeout, 0); snprintf(name, sizeof(name), "%s_nfcmrvl_fw_dnld_rx_wq", dev_name(&priv->ndev->nfc_dev->dev)); priv->fw_dnld.rx_wq = create_singlethread_workqueue(name); @@ -462,26 +518,64 @@ int nfcmrvl_fw_dnld_init(struct nfcmrvl_private *priv) void nfcmrvl_fw_dnld_deinit(struct nfcmrvl_private *priv) { + nfcmrvl_fw_dnld_abort(priv); destroy_workqueue(priv->fw_dnld.rx_wq); } void nfcmrvl_fw_dnld_recv_frame(struct nfcmrvl_private *priv, struct sk_buff *skb) { + unsigned long flags; + + spin_lock_irqsave(&priv->fw_dnld.rx_lock, flags); + if (priv->fw_dnld.stopping || !priv->fw_dnld.active) { + spin_unlock_irqrestore(&priv->fw_dnld.rx_lock, flags); + kfree_skb(skb); + return; + } + priv->fw_dnld.rx_active++; + spin_unlock_irqrestore(&priv->fw_dnld.rx_lock, flags); + /* Discard command timer */ - if (timer_pending(&priv->ndev->cmd_timer)) - timer_delete_sync(&priv->ndev->cmd_timer); + timer_delete_sync(&priv->ndev->cmd_timer); /* Allow next command */ atomic_set(&priv->ndev->cmd_cnt, 1); - /* Queue and trigger rx work */ - skb_queue_tail(&priv->fw_dnld.rx_q, skb); - queue_work(priv->fw_dnld.rx_wq, &priv->fw_dnld.rx_work); + spin_lock_irqsave(&priv->fw_dnld.rx_lock, flags); + if (!priv->fw_dnld.stopping && priv->fw_dnld.active) { + /* Queue the frame, but do not process it during the reset pulse. */ + skb_queue_tail(&priv->fw_dnld.rx_q, skb); + if (!priv->fw_dnld.starting) + queue_work(priv->fw_dnld.rx_wq, + &priv->fw_dnld.rx_work); + skb = NULL; + } + priv->fw_dnld.rx_active--; + if (!priv->fw_dnld.rx_active && + (priv->fw_dnld.stopping || !priv->fw_dnld.active)) + wake_up_all(&priv->fw_dnld.rx_wait); + spin_unlock_irqrestore(&priv->fw_dnld.rx_lock, flags); + kfree_skb(skb); } void nfcmrvl_fw_dnld_abort(struct nfcmrvl_private *priv) { + struct nfcmrvl_fw_dnld *fw_dnld = &priv->fw_dnld; + unsigned long flags; + + device_lock(&priv->ndev->nfc_dev->dev); + spin_lock_irqsave(&fw_dnld->rx_lock, flags); + fw_dnld->stopping = true; + spin_unlock_irqrestore(&fw_dnld->rx_lock, flags); + device_unlock(&priv->ndev->nfc_dev->dev); + + wait_event(fw_dnld->rx_wait, !READ_ONCE(fw_dnld->rx_active)); + timer_shutdown_sync(&fw_dnld->timer); + cancel_work_sync(&fw_dnld->rx_work); + cancel_work_sync(&fw_dnld->timeout_work); + skb_queue_purge(&fw_dnld->rx_q); + fw_dnld_over(priv, -EHOSTDOWN); } @@ -489,6 +583,7 @@ int nfcmrvl_fw_dnld_start(struct nci_dev *ndev, const char *firmware_name) { struct nfcmrvl_private *priv = nci_get_drvdata(ndev); struct nfcmrvl_fw_dnld *fw_dnld = &priv->fw_dnld; + unsigned long flags; int res; if (!priv->support_fw_dnld) @@ -497,6 +592,14 @@ int nfcmrvl_fw_dnld_start(struct nci_dev *ndev, const char *firmware_name) if (!firmware_name || !firmware_name[0]) return -EINVAL; + spin_lock_irqsave(&fw_dnld->rx_lock, flags); + if (fw_dnld->stopping || fw_dnld->active || fw_dnld->completing) { + res = fw_dnld->stopping ? -ENODEV : -EBUSY; + spin_unlock_irqrestore(&fw_dnld->rx_lock, flags); + return res; + } + spin_unlock_irqrestore(&fw_dnld->rx_lock, flags); + strscpy(fw_dnld->name, firmware_name); /* @@ -520,6 +623,7 @@ int nfcmrvl_fw_dnld_start(struct nci_dev *ndev, const char *firmware_name) firmware_name, fw_dnld->header->magic, fw_dnld->header->phy); release_firmware(fw_dnld->fw); + fw_dnld->fw = NULL; fw_dnld->header = NULL; return -EINVAL; } @@ -532,11 +636,6 @@ int nfcmrvl_fw_dnld_start(struct nci_dev *ndev, const char *firmware_name) fw_dnld->binary_config = &fw_dnld->header->firmware; } - /* Configure a timer for timeout */ - timer_setup(&priv->fw_dnld.timer, fw_dnld_timeout, 0); - mod_timer(&priv->fw_dnld.timer, - jiffies + msecs_to_jiffies(FW_DNLD_TIMEOUT)); - /* Ronfigure HI to be sure that it is the bootrom values */ priv->if_ops->nci_update_config(priv, &fw_dnld->header->bootrom.config); @@ -546,8 +645,24 @@ int nfcmrvl_fw_dnld_start(struct nci_dev *ndev, const char *firmware_name) /* First, reset the chip */ priv->fw_dnld.state = STATE_RESET; + + spin_lock_irqsave(&fw_dnld->rx_lock, flags); + fw_dnld->active = true; + fw_dnld->starting = true; + spin_unlock_irqrestore(&fw_dnld->rx_lock, flags); + nfcmrvl_chip_reset(priv); + spin_lock_irqsave(&fw_dnld->rx_lock, flags); + fw_dnld->starting = false; + if (fw_dnld->active) { + mod_timer(&fw_dnld->timer, + jiffies + msecs_to_jiffies(FW_DNLD_TIMEOUT)); + if (!skb_queue_empty(&fw_dnld->rx_q)) + queue_work(fw_dnld->rx_wq, &fw_dnld->rx_work); + } + spin_unlock_irqrestore(&fw_dnld->rx_lock, flags); + /* Now wait for CORE_RESET_NTF or timeout */ return 0; diff --git a/drivers/nfc/nfcmrvl/fw_dnld.h b/drivers/nfc/nfcmrvl/fw_dnld.h index 7c4d91b01..a8ed7bfcc 100644 --- a/drivers/nfc/nfcmrvl/fw_dnld.h +++ b/drivers/nfc/nfcmrvl/fw_dnld.h @@ -8,6 +8,8 @@ #ifndef __NFCMRVL_FW_DNLD_H__ #define __NFCMRVL_FW_DNLD_H__ +#include +#include #include #define NFCMRVL_FW_MAGIC 0x88888888 @@ -72,7 +74,16 @@ struct nfcmrvl_fw_dnld { struct workqueue_struct *rx_wq; struct work_struct rx_work; + struct work_struct timeout_work; struct sk_buff_head rx_q; + /* Protects stopping, starting, active, completing, and rx_active. */ + spinlock_t rx_lock; + wait_queue_head_t rx_wait; + unsigned int rx_active; + bool stopping; + bool starting; + bool active; + bool completing; struct timer_list timer; }; diff --git a/drivers/nfc/nfcmrvl/main.c b/drivers/nfc/nfcmrvl/main.c index 81c760534..02ea3356b 100644 --- a/drivers/nfc/nfcmrvl/main.c +++ b/drivers/nfc/nfcmrvl/main.c @@ -185,13 +185,12 @@ void nfcmrvl_nci_unregister_dev(struct nfcmrvl_private *priv) { struct nci_dev *ndev = priv->ndev; + nfcmrvl_fw_dnld_abort(priv); nci_unregister_device(ndev); + if (priv->if_ops->nci_quiesce) priv->if_ops->nci_quiesce(priv); - if (priv->ndev->nfc_dev->fw_download_in_progress) - nfcmrvl_fw_dnld_abort(priv); - nfcmrvl_fw_dnld_deinit(priv); nci_free_device(ndev); -- 2.54.0