* [PATCH v4 1/3] HID: Intel-thc-hid: Intel-thc: Add API to reset read DMA
2026-07-17 3:56 [PATCH RESEND v4 0/3] HID: Intel-thc-hid: Refine error recovery flow Even Xu
@ 2026-07-17 3:56 ` Even Xu
2026-07-17 4:08 ` sashiko-bot
2026-07-17 3:56 ` [PATCH v4 2/3] HID: Intel-thc-hid: Intel-quicki2c: Refine recover callback Even Xu
2026-07-17 3:56 ` [PATCH v4 3/3] HID: Intel-thc-hid: Intel-quickspi: " Even Xu
2 siblings, 1 reply; 8+ messages in thread
From: Even Xu @ 2026-07-17 3:56 UTC (permalink / raw)
To: bentiss, jikos; +Cc: srinivas.pandruvada, linux-input, linux-kernel, Even Xu
Add a helper function thc_rxdma_reset() to do read DMA reset, it can be
called when fatal DMA error happens.
Signed-off-by: Even Xu <even.xu@intel.com>
---
.../intel-thc-hid/intel-thc/intel-thc-dma.c | 51 +++++++++++++++++++
.../intel-thc-hid/intel-thc/intel-thc-dma.h | 1 +
2 files changed, 52 insertions(+)
diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c
index 6ee675e0a738..7ceb8aeeccd3 100644
--- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c
+++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c
@@ -561,6 +561,57 @@ static int thc_wait_for_dma_pause(struct thc_device *dev, enum thc_dma_channel c
return 0;
}
+/**
+ * thc_rxdma_reset - Reset all read DMA engines
+ *
+ * @dev: The pointer of THC private device context
+ *
+ * This is a helper function to reset RxDMA configure. It's typically used
+ * for RxDMA recovery when fatal error happens.
+ *
+ * Return: 0 if successful or error code on failure.
+ */
+int thc_rxdma_reset(struct thc_device *dev)
+{
+ int ret;
+
+ if (mutex_lock_interruptible(&dev->thc_bus_lock))
+ return -EINTR;
+
+ ret = thc_interrupt_quiesce(dev, true);
+ if (ret) {
+ dev_err(dev->dev, "Quiesce interrupt failed during RxDMA reset\n");
+ goto end;
+ }
+
+ ret = thc_wait_for_dma_pause(dev, THC_RXDMA1);
+ if (ret) {
+ dev_err(dev->dev, "Wait for RxDMA1 pause failed during RxDMA reset\n");
+ goto end;
+ }
+
+ ret = thc_wait_for_dma_pause(dev, THC_RXDMA2);
+ if (ret) {
+ dev_err(dev->dev, "Wait for RxDMA2 pause failed during RxDMA reset\n");
+ goto end;
+ }
+
+ thc_dma_unconfigure(dev);
+
+ ret = thc_dma_configure(dev);
+ if (ret) {
+ dev_err(dev->dev, "Re-config DMA failed during RxDMA reset\n");
+ goto end;
+ }
+
+ thc_interrupt_quiesce(dev, false);
+
+end:
+ mutex_unlock(&dev->thc_bus_lock);
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(thc_rxdma_reset, "INTEL_THC");
+
static int read_dma_buffer(struct thc_device *dev,
struct thc_dma_configuration *read_config,
u8 prd_table_index, void *read_buff)
diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h
index 541d33995baf..715423453a9d 100644
--- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h
+++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h
@@ -145,6 +145,7 @@ int thc_dma_allocate(struct thc_device *dev);
int thc_dma_configure(struct thc_device *dev);
void thc_dma_unconfigure(struct thc_device *dev);
void thc_dma_release(struct thc_device *dev);
+int thc_rxdma_reset(struct thc_device *dev);
int thc_rxdma_read(struct thc_device *dev, enum thc_dma_channel dma_channel,
void *read_buff, size_t *read_len, int *read_finished);
int thc_swdma_read(struct thc_device *dev, void *write_buff, size_t write_len,
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* Re: [PATCH v4 1/3] HID: Intel-thc-hid: Intel-thc: Add API to reset read DMA
2026-07-17 3:56 ` [PATCH v4 1/3] HID: Intel-thc-hid: Intel-thc: Add API to reset read DMA Even Xu
@ 2026-07-17 4:08 ` sashiko-bot
0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-07-17 4:08 UTC (permalink / raw)
To: Even Xu; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [Medium] `thc_rxdma_reset()` aborts the reset sequence if pausing THC_RXDMA1 fails, leaving THC_RXDMA2 running.
Pre-existing issues:
- [High] thc_dma_unconfigure() zeroes PRD base addresses before DMA engines are stopped, risking IOMMU faults.
--
--- Patch [1]: [PATCH v4 1/3] HID: Intel-thc-hid: Intel-thc: Add API to reset read DMA ---
commit b0b099ba1dc08199ebeeb23013c7c472a6f2c1bb
Author: Even Xu <even.xu@intel.com>
HID: Intel-thc-hid: Intel-thc: Add API to reset read DMA
This commit adds a helper function thc_rxdma_reset() to perform a read DMA
reset. It is intended to be called for recovery when a fatal DMA error
happens.
> diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c
> index 6ee675e0a7384..7ceb8aeeccd37 100644
> --- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c
> +++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c
[ ... ]
> @@ -561,6 +561,57 @@ static int thc_wait_for_dma_pause(struct thc_device *dev, enum thc_dma_channel c
> return 0;
> }
>
> +/**
> + * thc_rxdma_reset - Reset all read DMA engines
[ ... ]
> +int thc_rxdma_reset(struct thc_device *dev)
> +{
> + int ret;
> +
> + if (mutex_lock_interruptible(&dev->thc_bus_lock))
> + return -EINTR;
> +
> + ret = thc_interrupt_quiesce(dev, true);
> + if (ret) {
> + dev_err(dev->dev, "Quiesce interrupt failed during RxDMA reset\n");
> + goto end;
> + }
> +
> + ret = thc_wait_for_dma_pause(dev, THC_RXDMA1);
> + if (ret) {
> + dev_err(dev->dev, "Wait for RxDMA1 pause failed during RxDMA reset\n");
> + goto end;
> + }
> +
[Severity: Medium]
If pausing THC_RXDMA1 fails and times out, does jumping to the end label
leave THC_RXDMA2 running?
Bailing out here without attempting to stop the second RX DMA engine might
leave the hardware in an inconsistent state during a fatal error recovery.
> + ret = thc_wait_for_dma_pause(dev, THC_RXDMA2);
> + if (ret) {
> + dev_err(dev->dev, "Wait for RxDMA2 pause failed during RxDMA reset\n");
> + goto end;
> + }
> +
> + thc_dma_unconfigure(dev);
> +
[Severity: High]
This isn't a bug introduced by this patch, but does calling
thc_dma_unconfigure() here without first pausing TXDMA or SWDMA risk IOMMU
faults?
Looking at thc_dma_unconfigure(), it zeroes the PRD base addresses for all
channels before clearing the START bits:
void thc_dma_unconfigure(struct thc_device *dev)
{
int chan;
for (chan = 0; chan < MAX_THC_DMA_CHANNEL; chan++) {
dma_set_prd_base_addr(dev, 0, &dev->dma_ctx->dma_config[chan]);
...
If a previous thc_dma_write() timed out, its TXDMA engine might still be
active since the timeout handler does not pause it.
Zeroing the PRD base address of an active DMA engine could force the
hardware to fetch descriptors at physical address 0, which would cause an
immediate memory corruption or IOMMU panic.
> + ret = thc_dma_configure(dev);
> + if (ret) {
> + dev_err(dev->dev, "Re-config DMA failed during RxDMA reset\n");
> + goto end;
> + }
> +
> + thc_interrupt_quiesce(dev, false);
> +
> +end:
> + mutex_unlock(&dev->thc_bus_lock);
> + return ret;
> +}
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717035609.3874015-1-even.xu@intel.com?part=1
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v4 2/3] HID: Intel-thc-hid: Intel-quicki2c: Refine recover callback
2026-07-17 3:56 [PATCH RESEND v4 0/3] HID: Intel-thc-hid: Refine error recovery flow Even Xu
2026-07-17 3:56 ` [PATCH v4 1/3] HID: Intel-thc-hid: Intel-thc: Add API to reset read DMA Even Xu
@ 2026-07-17 3:56 ` Even Xu
2026-07-17 4:06 ` sashiko-bot
2026-07-17 3:56 ` [PATCH v4 3/3] HID: Intel-thc-hid: Intel-quickspi: " Even Xu
2 siblings, 1 reply; 8+ messages in thread
From: Even Xu @ 2026-07-17 3:56 UTC (permalink / raw)
To: bentiss, jikos; +Cc: srinivas.pandruvada, linux-input, linux-kernel, Even Xu
Refine recover flow:
1. Use workqueue to handle recover flow instead of processing in irq
handler.
2. Call thc_rxdma_reset() API to simplify the recover operation.
3. Disable interrupt during whole recover flow.
4. If recover fails, disable interrupt to avoid interrupt storm.
Signed-off-by: Even Xu <even.xu@intel.com>
---
.../intel-quicki2c/pci-quicki2c.c | 66 ++++++++++++-------
.../intel-quicki2c/quicki2c-dev.h | 5 ++
2 files changed, 48 insertions(+), 23 deletions(-)
diff --git a/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c b/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
index 46d3e9a01999..3ccbddfe8e5e 100644
--- a/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
+++ b/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
@@ -245,28 +245,31 @@ static irqreturn_t quicki2c_irq_quick_handler(int irq, void *dev_id)
}
/**
- * try_recover - Try to recovery THC and Device
- * @qcdev: Pointer to quicki2c_device structure
+ * try_recover - Recover callback to recover THC
+ * @work: pointer to work_struct
*
* This function is an error handler, called when fatal error happens.
- * It try to reset touch device and re-configure THC to recovery
- * communication between touch device and THC.
- *
- * Return: 0 if successful or error code on failure
+ * It try to reset Touch Device and re-configure THC to recover
+ * transferring between Device and THC.
*/
-static int try_recover(struct quicki2c_device *qcdev)
+static void try_recover(struct work_struct *work)
{
- int ret;
+ struct quicki2c_device *qcdev = container_of(work, struct quicki2c_device, recover_work);
- thc_dma_unconfigure(qcdev->thc_hw);
+ if (READ_ONCE(qcdev->recovery_disabled))
+ return;
- ret = thc_dma_configure(qcdev->thc_hw);
- if (ret) {
- dev_err(qcdev->dev, "Reconfig DMA failed\n");
- return ret;
- }
+ if (pm_runtime_resume_and_get(qcdev->dev))
+ return;
- return 0;
+ thc_interrupt_enable(qcdev->thc_hw, false);
+
+ if (thc_rxdma_reset(qcdev->thc_hw))
+ qcdev->state = QUICKI2C_DISABLED;
+ else
+ thc_interrupt_enable(qcdev->thc_hw, true);
+
+ pm_runtime_put_autosuspend(qcdev->dev);
}
static int handle_input_report(struct quicki2c_device *qcdev)
@@ -343,11 +346,10 @@ static irqreturn_t quicki2c_irq_thread_handler(int irq, void *dev_id)
}
exit:
- thc_interrupt_enable(qcdev->thc_hw, true);
-
if (err_recover)
- if (try_recover(qcdev))
- qcdev->state = QUICKI2C_DISABLED;
+ schedule_work(&qcdev->recover_work);
+ else
+ thc_interrupt_enable(qcdev->thc_hw, true);
pm_runtime_put_autosuspend(qcdev->dev);
@@ -386,6 +388,8 @@ static struct quicki2c_device *quicki2c_dev_init(struct pci_dev *pdev, void __io
qcdev->ddata = ddata;
init_waitqueue_head(&qcdev->reset_ack_wq);
+ WRITE_ONCE(qcdev->recovery_disabled, false);
+ INIT_WORK(&qcdev->recover_work, try_recover);
/* THC hardware init */
qcdev->thc_hw = thc_dev_init(qcdev->dev, qcdev->mem_addr);
@@ -439,6 +443,9 @@ static struct quicki2c_device *quicki2c_dev_init(struct pci_dev *pdev, void __io
*/
static void quicki2c_dev_deinit(struct quicki2c_device *qcdev)
{
+ WRITE_ONCE(qcdev->recovery_disabled, true);
+ cancel_work_sync(&qcdev->recover_work);
+
thc_interrupt_quiesce(qcdev->thc_hw, true);
thc_interrupt_enable(qcdev->thc_hw, false);
thc_ltr_unconfig(qcdev->thc_hw);
@@ -772,12 +779,13 @@ static void quicki2c_remove(struct pci_dev *pdev)
return;
quicki2c_hid_remove(qcdev);
+
+ quicki2c_dev_deinit(qcdev);
+
quicki2c_dma_deinit(qcdev);
pm_runtime_get_noresume(qcdev->dev);
- quicki2c_dev_deinit(qcdev);
-
pci_clear_master(pdev);
}
@@ -796,10 +804,10 @@ static void quicki2c_shutdown(struct pci_dev *pdev)
if (!qcdev)
return;
+ quicki2c_dev_deinit(qcdev);
+
/* Must stop DMA before reboot to avoid DMA entering into unknown state */
quicki2c_dma_deinit(qcdev);
-
- quicki2c_dev_deinit(qcdev);
}
static int quicki2c_suspend(struct device *device)
@@ -826,6 +834,9 @@ static int quicki2c_suspend(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qcdev->recovery_disabled, true);
+ cancel_work_sync(&qcdev->recover_work);
+
ret = thc_interrupt_quiesce(qcdev->thc_hw, true);
if (ret)
return ret;
@@ -867,6 +878,8 @@ static int quicki2c_resume(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qcdev->recovery_disabled, false);
+
if (!device_may_wakeup(qcdev->dev))
return quicki2c_set_power(qcdev, HIDI2C_ON);
@@ -883,6 +896,9 @@ static int quicki2c_freeze(struct device *device)
if (!qcdev)
return -ENODEV;
+ WRITE_ONCE(qcdev->recovery_disabled, true);
+ cancel_work_sync(&qcdev->recover_work);
+
ret = thc_interrupt_quiesce(qcdev->thc_hw, true);
if (ret)
return ret;
@@ -914,6 +930,8 @@ static int quicki2c_thaw(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qcdev->recovery_disabled, false);
+
return 0;
}
@@ -938,6 +956,8 @@ static int quicki2c_poweroff(struct device *device)
thc_ltr_unconfig(qcdev->thc_hw);
+ quicki2c_dev_deinit(qcdev);
+
quicki2c_dma_deinit(qcdev);
return 0;
diff --git a/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h b/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h
index 61dbdece59a1..d72565de67b9 100644
--- a/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h
+++ b/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h
@@ -191,6 +191,8 @@ struct acpi_device;
* @i2c_max_frame_size: Max RX frame size (unit in Bytes)
* @i2c_int_delay_enable: Indicate interrupt delay feature enabled or not
* @i2c_int_delay: Interrupt detection delay value (unit in 10 us)
+ * @recover_work: Work structure for recovery
+ * @recovery_disabled: Whether recovery work is blocked during teardown
*/
struct quicki2c_device {
struct device *dev;
@@ -226,6 +228,9 @@ struct quicki2c_device {
u32 i2c_max_frame_size;
u32 i2c_int_delay_enable;
u32 i2c_int_delay;
+
+ struct work_struct recover_work;
+ bool recovery_disabled;
};
#endif /* _QUICKI2C_DEV_H_ */
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* Re: [PATCH v4 2/3] HID: Intel-thc-hid: Intel-quicki2c: Refine recover callback
2026-07-17 3:56 ` [PATCH v4 2/3] HID: Intel-thc-hid: Intel-quicki2c: Refine recover callback Even Xu
@ 2026-07-17 4:06 ` sashiko-bot
0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-07-17 4:06 UTC (permalink / raw)
To: Even Xu; +Cc: dmitry.torokhov, linux-input
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
New issues:
- [High] Race condition during device teardown leading to a Use-After-Free of `qcdev`.
- [Medium] Race condition in resume/thaw paths permanently disables device interrupts.
- [Medium] Premature interrupt unmasking causes an interrupt storm for shared IRQs.
- [Low] Redundant hardware teardown in `quicki2c_poweroff()` triggers kernel warnings.
Pre-existing issues:
- [High] Hardirq handler accesses memory-mapped hardware registers without verifying runtime PM state.
- [High] Unvalidated length field from hardware allows out-of-bounds read in the HID core.
--
--- Patch [2]: [PATCH v4 2/3] HID: Intel-thc-hid: Intel-quicki2c: Refine recover callback ---
commit 8f342c689f3a83c1f792680893d01fe944f899d6
Author: Even Xu <even.xu@intel.com>
Subject: HID: Intel-thc-hid: Intel-quicki2c: Refine recover callback
This commit refines the recover flow by offloading it to a workqueue instead
of processing it in the IRQ handler. It simplifies the operation using the
thc_rxdma_reset() API and ensures interrupts are disabled during the entire
recovery flow.
> diff --git a/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c b/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
> index 46d3e9a019995..3ccbddfe8e5e8 100644
> --- a/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
> +++ b/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
> @@ -245,28 +245,31 @@ static irqreturn_t quicki2c_irq_quick_handler(int irq, void *dev_id)
> }
[Severity: High]
This is a pre-existing issue, but does quicki2c_irq_quick_handler() risk
accessing memory-mapped hardware registers while the device is in a
runtime-suspended state?
The driver allows fallback to shared INTx interrupts, so the hardirq
handler could be invoked by another device on the same IRQ line while this
device is suspended. Calling thc_interrupt_enable() unconditionally could
access powered-off registers and cause Unsupported Requests (UR).
>
> /**
> - * try_recover - Try to recovery THC and Device
> - * @qcdev: Pointer to quicki2c_device structure
> + * try_recover - Recover callback to recover THC
> + * @work: pointer to work_struct
> *
> * This function is an error handler, called when fatal error happens.
> - * It try to reset touch device and re-configure THC to recovery
> - * communication between touch device and THC.
> - *
> - * Return: 0 if successful or error code on failure
> + * It try to reset Touch Device and re-configure THC to recover
> + * transferring between Device and THC.
> */
> -static int try_recover(struct quicki2c_device *qcdev)
> +static void try_recover(struct work_struct *work)
> {
> - int ret;
> + struct quicki2c_device *qcdev = container_of(work, struct quicki2c_device, recover_work);
>
> - thc_dma_unconfigure(qcdev->thc_hw);
> + if (READ_ONCE(qcdev->recovery_disabled))
> + return;
>
> - ret = thc_dma_configure(qcdev->thc_hw);
> - if (ret) {
> - dev_err(qcdev->dev, "Reconfig DMA failed\n");
> - return ret;
> - }
> + if (pm_runtime_resume_and_get(qcdev->dev))
> + return;
>
> - return 0;
> + thc_interrupt_enable(qcdev->thc_hw, false);
> +
> + if (thc_rxdma_reset(qcdev->thc_hw))
> + qcdev->state = QUICKI2C_DISABLED;
> + else
> + thc_interrupt_enable(qcdev->thc_hw, true);
> +
> + pm_runtime_put_autosuspend(qcdev->dev);
> }
>
> static int handle_input_report(struct quicki2c_device *qcdev)
[Severity: High]
This is a pre-existing issue, but does handle_input_report() trust the
length field from the hardware without validating it?
If a buggy or malicious device sends a packet with pkt->len equal to 1,
HIDI2C_DATA_LEN could underflow to SIZE_MAX when passed to the HID core:
quicki2c_hid_send_report(qcdev, pkt->data,
HIDI2C_DATA_LEN(le16_to_cpu(pkt->len)));
Could this unvalidated length cause an out-of-bounds read in the HID core?
[ ... ]
> @@ -343,11 +346,10 @@ static irqreturn_t quicki2c_irq_thread_handler(int irq, void *dev_id)
> }
>
> exit:
> - thc_interrupt_enable(qcdev->thc_hw, true);
> -
> if (err_recover)
> - if (try_recover(qcdev))
> - qcdev->state = QUICKI2C_DISABLED;
> + schedule_work(&qcdev->recover_work);
> + else
> + thc_interrupt_enable(qcdev->thc_hw, true);
[Severity: Medium]
Could this unconditionally re-enable the hardware interrupt while a recovery
is already pending?
If err_recover is 0 (for instance, if another shared interrupt fires), the
else branch unmasks the broken device's interrupt prematurely.
Does this cause an interrupt storm before try_recover() has a chance to run?
[ ... ]
> @@ -439,6 +443,9 @@ static struct quicki2c_device *quicki2c_dev_init(struct pci_dev *pdev, void __io
> */
> static void quicki2c_dev_deinit(struct quicki2c_device *qcdev)
> {
> + WRITE_ONCE(qcdev->recovery_disabled, true);
> + cancel_work_sync(&qcdev->recover_work);
> +
> thc_interrupt_quiesce(qcdev->thc_hw, true);
> thc_interrupt_enable(qcdev->thc_hw, false);
> thc_ltr_unconfig(qcdev->thc_hw);
[Severity: High]
Is there a race condition here that could lead to a use-after-free of qcdev?
cancel_work_sync() is called before the hardware interrupts are quiesced
and disabled. If the threaded IRQ handler is preempted right before calling
schedule_work(), it could queue recover_work after cancel_work_sync()
completes:
CPU1 CPU2
quicki2c_irq_thread_handler() quicki2c_dev_deinit()
err_recover = 1;
// preempted
cancel_work_sync();
thc_interrupt_quiesce();
// device removed, qcdev freed
schedule_work(&qcdev->recover_work);
When try_recover() eventually runs, will it dereference the freed qcdev
pointer?
[ ... ]
> @@ -867,6 +878,8 @@ static int quicki2c_resume(struct device *device)
> if (ret)
> return ret;
>
> + WRITE_ONCE(qcdev->recovery_disabled, false);
> +
[Severity: Medium]
Does unmasking the interrupt before setting recovery_disabled to false
create a window where the device can become permanently disabled?
If the hardware asserts an interrupt after thc_interrupt_quiesce() enables
it, but before WRITE_ONCE(qcdev->recovery_disabled, false) runs, the IRQ
handler will queue the recovery work.
The work will execute try_recover(), read recovery_disabled as true, and
return immediately without re-enabling interrupts. Will this leave the
device permanently unresponsive?
> if (!device_may_wakeup(qcdev->dev))
> return quicki2c_set_power(qcdev, HIDI2C_ON);
[ ... ]
> @@ -938,6 +956,8 @@ static int quicki2c_poweroff(struct device *device)
>
> thc_ltr_unconfig(qcdev->thc_hw);
>
> + quicki2c_dev_deinit(qcdev);
> +
> quicki2c_dma_deinit(qcdev);
>
> return 0;
[Severity: Low]
Does adding quicki2c_dev_deinit() here result in redundant hardware teardown
steps?
quicki2c_poweroff() manually calls thc_interrupt_quiesce() and
thc_ltr_unconfig() just before this line. Since quicki2c_dev_deinit() also
calls those functions, it looks like they are executed twice, which might
trigger kernel warnings from the underlying hardware functions.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717035609.3874015-1-even.xu@intel.com?part=2
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v4 3/3] HID: Intel-thc-hid: Intel-quickspi: Refine recover callback
2026-07-17 3:56 [PATCH RESEND v4 0/3] HID: Intel-thc-hid: Refine error recovery flow Even Xu
2026-07-17 3:56 ` [PATCH v4 1/3] HID: Intel-thc-hid: Intel-thc: Add API to reset read DMA Even Xu
2026-07-17 3:56 ` [PATCH v4 2/3] HID: Intel-thc-hid: Intel-quicki2c: Refine recover callback Even Xu
@ 2026-07-17 3:56 ` Even Xu
2026-07-17 4:08 ` sashiko-bot
2 siblings, 1 reply; 8+ messages in thread
From: Even Xu @ 2026-07-17 3:56 UTC (permalink / raw)
To: bentiss, jikos; +Cc: srinivas.pandruvada, linux-input, linux-kernel, Even Xu
Refine recover flow:
1. Use workqueue to handle recover flow instead of processing in irq
handler.
2. Call thc_rxdma_reset() API to simplify the recover operation.
3. Disable interrupt during whole recover flow.
4. If recover fails, disable interrupt to avoid interrupt storm.
Signed-off-by: Even Xu <even.xu@intel.com>
---
.../intel-quickspi/pci-quickspi.c | 71 +++++++++++--------
.../intel-quickspi/quickspi-dev.h | 6 ++
2 files changed, 49 insertions(+), 28 deletions(-)
diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
index 4ae2e1718b30..db7ede5cc7a2 100644
--- a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
+++ b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
@@ -252,34 +252,31 @@ static irqreturn_t quickspi_irq_quick_handler(int irq, void *dev_id)
}
/**
- * try_recover - Try to recovery THC and Device
- * @qsdev: pointer to quickspi device
+ * try_recover - Recover callback to recover THC
+ * @work: pointer to work_struct
*
- * This function is a error handler, called when fatal error happens.
- * It try to reset Touch Device and re-configure THC to recovery
+ * This function is an error handler, called when fatal error happens.
+ * It try to reset Touch Device and re-configure THC to recover
* transferring between Device and THC.
- *
- * Return: 0 if successful or error code on failed.
*/
-static int try_recover(struct quickspi_device *qsdev)
+static void try_recover(struct work_struct *work)
{
- int ret;
+ struct quickspi_device *qsdev = container_of(work, struct quickspi_device, recover_work);
- ret = reset_tic(qsdev);
- if (ret) {
- dev_err(qsdev->dev, "Reset touch device failed, ret = %d\n", ret);
- return ret;
- }
+ if (READ_ONCE(qsdev->recovery_disabled))
+ return;
- thc_dma_unconfigure(qsdev->thc_hw);
+ if (pm_runtime_resume_and_get(qsdev->dev))
+ return;
- ret = thc_dma_configure(qsdev->thc_hw);
- if (ret) {
- dev_err(qsdev->dev, "Re-configure THC DMA failed, ret = %d\n", ret);
- return ret;
- }
+ thc_interrupt_enable(qsdev->thc_hw, false);
- return 0;
+ if (thc_rxdma_reset(qsdev->thc_hw))
+ qsdev->state = QUICKSPI_DISABLED;
+ else
+ thc_interrupt_enable(qsdev->thc_hw, true);
+
+ pm_runtime_put_autosuspend(qsdev->dev);
}
/**
@@ -337,11 +334,10 @@ static irqreturn_t quickspi_irq_thread_handler(int irq, void *dev_id)
}
end:
- thc_interrupt_enable(qsdev->thc_hw, true);
-
if (err_recover)
- if (try_recover(qsdev))
- qsdev->state = QUICKSPI_DISABLED;
+ schedule_work(&qsdev->recover_work);
+ else
+ thc_interrupt_enable(qsdev->thc_hw, true);
pm_runtime_put_autosuspend(qsdev->dev);
@@ -385,6 +381,8 @@ static struct quickspi_device *quickspi_dev_init(struct pci_dev *pdev, void __io
init_waitqueue_head(&qsdev->report_desc_got_wq);
init_waitqueue_head(&qsdev->get_report_cmpl_wq);
init_waitqueue_head(&qsdev->set_report_cmpl_wq);
+ WRITE_ONCE(qsdev->recovery_disabled, false);
+ INIT_WORK(&qsdev->recover_work, try_recover);
/* thc hw init */
qsdev->thc_hw = thc_dev_init(qsdev->dev, qsdev->mem_addr);
@@ -461,6 +459,10 @@ static struct quickspi_device *quickspi_dev_init(struct pci_dev *pdev, void __io
*/
static void quickspi_dev_deinit(struct quickspi_device *qsdev)
{
+ WRITE_ONCE(qsdev->recovery_disabled, true);
+ cancel_work_sync(&qsdev->recover_work);
+
+ thc_interrupt_quiesce(qsdev->thc_hw, true);
thc_interrupt_enable(qsdev->thc_hw, false);
thc_ltr_unconfig(qsdev->thc_hw);
thc_wot_unconfig(qsdev->thc_hw);
@@ -711,12 +713,13 @@ static void quickspi_remove(struct pci_dev *pdev)
return;
quickspi_hid_remove(qsdev);
+
+ quickspi_dev_deinit(qsdev);
+
quickspi_dma_deinit(qsdev);
pm_runtime_get_noresume(qsdev->dev);
- quickspi_dev_deinit(qsdev);
-
pci_clear_master(pdev);
}
@@ -737,10 +740,10 @@ static void quickspi_shutdown(struct pci_dev *pdev)
if (!qsdev)
return;
+ quickspi_dev_deinit(qsdev);
+
/* Must stop DMA before reboot to avoid DMA entering into unknown state */
quickspi_dma_deinit(qsdev);
-
- quickspi_dev_deinit(qsdev);
}
static int quickspi_suspend(struct device *device)
@@ -759,6 +762,9 @@ static int quickspi_suspend(struct device *device)
return ret;
}
+ WRITE_ONCE(qsdev->recovery_disabled, true);
+ cancel_work_sync(&qsdev->recover_work);
+
ret = thc_interrupt_quiesce(qsdev->thc_hw, true);
if (ret)
return ret;
@@ -784,6 +790,8 @@ static int quickspi_resume(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qsdev->recovery_disabled, false);
+
/*
* A wake-enabled device keeps its power and state across suspend, so
* only restore the THC context. Resetting it here would discard a
@@ -864,6 +872,9 @@ static int quickspi_freeze(struct device *device)
if (!qsdev)
return -ENODEV;
+ WRITE_ONCE(qsdev->recovery_disabled, true);
+ cancel_work_sync(&qsdev->recover_work);
+
ret = thc_interrupt_quiesce(qsdev->thc_hw, true);
if (ret)
return ret;
@@ -895,6 +906,8 @@ static int quickspi_thaw(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qsdev->recovery_disabled, false);
+
return 0;
}
@@ -919,6 +932,8 @@ static int quickspi_poweroff(struct device *device)
thc_ltr_unconfig(qsdev->thc_hw);
+ quickspi_dev_deinit(qsdev);
+
quickspi_dma_deinit(qsdev);
return 0;
diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
index bf5e18f5a5f4..2936c8b1532c 100644
--- a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
+++ b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
@@ -8,6 +8,7 @@
#include <linux/hid-over-spi.h>
#include <linux/sizes.h>
#include <linux/wait.h>
+#include <linux/workqueue.h>
#include "quickspi-protocol.h"
@@ -126,6 +127,8 @@ struct acpi_device;
* @get_feature_cmpl: indicate get feature received or not
* @set_feature_cmpl_wq: workqueue for waiting set feature to device
* @set_feature_cmpl: indicate set feature send complete or not
+ * @recover_work: Work structure for recovery
+ * @recovery_disabled: Whether recovery work is blocked during teardown
*/
struct quickspi_device {
struct device *dev;
@@ -173,6 +176,9 @@ struct quickspi_device {
wait_queue_head_t set_report_cmpl_wq;
bool set_report_cmpl;
+
+ struct work_struct recover_work;
+ bool recovery_disabled;
};
#endif /* _QUICKSPI_DEV_H_ */
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* Re: [PATCH v4 3/3] HID: Intel-thc-hid: Intel-quickspi: Refine recover callback
2026-07-17 3:56 ` [PATCH v4 3/3] HID: Intel-thc-hid: Intel-quickspi: " Even Xu
@ 2026-07-17 4:08 ` sashiko-bot
0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-07-17 4:08 UTC (permalink / raw)
To: Even Xu; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Race condition during device teardown allows the IRQ thread handler to unconditionally schedule `recover_work` after `cancel_work_sync()` has completed, leading to a Use-After-Free.
--
--- Patch [3]: [PATCH v4 3/3] HID: Intel-thc-hid: Intel-quickspi: Refine recover callback ---
commit 476846b5d6215f9a1a461a86f427618ddd19b6d1
Author: Even Xu <even.xu@intel.com>
HID: Intel-thc-hid: Intel-quickspi: Refine recover callback
This patch updates the recovery flow to execute in a workqueue rather than
directly in the IRQ handler. It introduces a recover_work item and a
recovery_disabled flag to manage the teardown process.
> diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
> index 4ae2e1718b30c..db7ede5cc7a29 100644
> --- a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
> +++ b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
[ ... ]
> @@ -337,11 +334,10 @@ static irqreturn_t quickspi_irq_thread_handler(int irq, void *dev_id)
> }
>
> end:
> if (err_recover)
> - if (try_recover(qsdev))
> - qsdev->state = QUICKSPI_DISABLED;
> + schedule_work(&qsdev->recover_work);
[Severity: High]
Can this schedule the work after it has been cancelled during device teardown?
Since the threaded IRQ handler doesn't check the recovery_disabled flag
before calling schedule_work(), if quickspi_irq_thread_handler() runs
concurrently with quickspi_dev_deinit():
CPU 1
quickspi_dev_deinit()
WRITE_ONCE(qsdev->recovery_disabled, true);
cancel_work_sync(&qsdev->recover_work);
// returns, and eventually devres frees qsdev
CPU 2
quickspi_irq_thread_handler()
...
if (err_recover)
schedule_work(&qsdev->recover_work);
Could CPU 2 queue the work after CPU 1 finishes cancel_work_sync()?
If the scheduled work executes after qsdev has been freed, will this cause a
use-after-free when try_recover() reads qsdev->recovery_disabled?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717035609.3874015-1-even.xu@intel.com?part=3
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v4 2/3] HID: Intel-thc-hid: Intel-quicki2c: Refine recover callback
2026-07-17 3:37 [PATCH v4 0/3] HID: Intel-thc-hid: Refine error recovery flow Even Xu
@ 2026-07-17 3:37 ` Even Xu
0 siblings, 0 replies; 8+ messages in thread
From: Even Xu @ 2026-07-17 3:37 UTC (permalink / raw)
To: bentiss, jikos; +Cc: srinivas.pandruvada, linux-input, linux-kernel, Even Xu
Refine recover flow:
1. Use workqueue to handle recover flow instead of processing in irq
handler.
2. Call thc_rxdma_reset() API to simplify the recover operation.
3. Disable interrupt during whole recover flow.
4. If recover fails, disable interrupt to avoid interrupt storm.
Signed-off-by: Even Xu <even.xu@intel.com>
---
.../intel-quicki2c/pci-quicki2c.c | 66 ++++++++++++-------
.../intel-quicki2c/quicki2c-dev.h | 5 ++
2 files changed, 48 insertions(+), 23 deletions(-)
diff --git a/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c b/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
index 46d3e9a01999..3ccbddfe8e5e 100644
--- a/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
+++ b/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
@@ -245,28 +245,31 @@ static irqreturn_t quicki2c_irq_quick_handler(int irq, void *dev_id)
}
/**
- * try_recover - Try to recovery THC and Device
- * @qcdev: Pointer to quicki2c_device structure
+ * try_recover - Recover callback to recover THC
+ * @work: pointer to work_struct
*
* This function is an error handler, called when fatal error happens.
- * It try to reset touch device and re-configure THC to recovery
- * communication between touch device and THC.
- *
- * Return: 0 if successful or error code on failure
+ * It try to reset Touch Device and re-configure THC to recover
+ * transferring between Device and THC.
*/
-static int try_recover(struct quicki2c_device *qcdev)
+static void try_recover(struct work_struct *work)
{
- int ret;
+ struct quicki2c_device *qcdev = container_of(work, struct quicki2c_device, recover_work);
- thc_dma_unconfigure(qcdev->thc_hw);
+ if (READ_ONCE(qcdev->recovery_disabled))
+ return;
- ret = thc_dma_configure(qcdev->thc_hw);
- if (ret) {
- dev_err(qcdev->dev, "Reconfig DMA failed\n");
- return ret;
- }
+ if (pm_runtime_resume_and_get(qcdev->dev))
+ return;
- return 0;
+ thc_interrupt_enable(qcdev->thc_hw, false);
+
+ if (thc_rxdma_reset(qcdev->thc_hw))
+ qcdev->state = QUICKI2C_DISABLED;
+ else
+ thc_interrupt_enable(qcdev->thc_hw, true);
+
+ pm_runtime_put_autosuspend(qcdev->dev);
}
static int handle_input_report(struct quicki2c_device *qcdev)
@@ -343,11 +346,10 @@ static irqreturn_t quicki2c_irq_thread_handler(int irq, void *dev_id)
}
exit:
- thc_interrupt_enable(qcdev->thc_hw, true);
-
if (err_recover)
- if (try_recover(qcdev))
- qcdev->state = QUICKI2C_DISABLED;
+ schedule_work(&qcdev->recover_work);
+ else
+ thc_interrupt_enable(qcdev->thc_hw, true);
pm_runtime_put_autosuspend(qcdev->dev);
@@ -386,6 +388,8 @@ static struct quicki2c_device *quicki2c_dev_init(struct pci_dev *pdev, void __io
qcdev->ddata = ddata;
init_waitqueue_head(&qcdev->reset_ack_wq);
+ WRITE_ONCE(qcdev->recovery_disabled, false);
+ INIT_WORK(&qcdev->recover_work, try_recover);
/* THC hardware init */
qcdev->thc_hw = thc_dev_init(qcdev->dev, qcdev->mem_addr);
@@ -439,6 +443,9 @@ static struct quicki2c_device *quicki2c_dev_init(struct pci_dev *pdev, void __io
*/
static void quicki2c_dev_deinit(struct quicki2c_device *qcdev)
{
+ WRITE_ONCE(qcdev->recovery_disabled, true);
+ cancel_work_sync(&qcdev->recover_work);
+
thc_interrupt_quiesce(qcdev->thc_hw, true);
thc_interrupt_enable(qcdev->thc_hw, false);
thc_ltr_unconfig(qcdev->thc_hw);
@@ -772,12 +779,13 @@ static void quicki2c_remove(struct pci_dev *pdev)
return;
quicki2c_hid_remove(qcdev);
+
+ quicki2c_dev_deinit(qcdev);
+
quicki2c_dma_deinit(qcdev);
pm_runtime_get_noresume(qcdev->dev);
- quicki2c_dev_deinit(qcdev);
-
pci_clear_master(pdev);
}
@@ -796,10 +804,10 @@ static void quicki2c_shutdown(struct pci_dev *pdev)
if (!qcdev)
return;
+ quicki2c_dev_deinit(qcdev);
+
/* Must stop DMA before reboot to avoid DMA entering into unknown state */
quicki2c_dma_deinit(qcdev);
-
- quicki2c_dev_deinit(qcdev);
}
static int quicki2c_suspend(struct device *device)
@@ -826,6 +834,9 @@ static int quicki2c_suspend(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qcdev->recovery_disabled, true);
+ cancel_work_sync(&qcdev->recover_work);
+
ret = thc_interrupt_quiesce(qcdev->thc_hw, true);
if (ret)
return ret;
@@ -867,6 +878,8 @@ static int quicki2c_resume(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qcdev->recovery_disabled, false);
+
if (!device_may_wakeup(qcdev->dev))
return quicki2c_set_power(qcdev, HIDI2C_ON);
@@ -883,6 +896,9 @@ static int quicki2c_freeze(struct device *device)
if (!qcdev)
return -ENODEV;
+ WRITE_ONCE(qcdev->recovery_disabled, true);
+ cancel_work_sync(&qcdev->recover_work);
+
ret = thc_interrupt_quiesce(qcdev->thc_hw, true);
if (ret)
return ret;
@@ -914,6 +930,8 @@ static int quicki2c_thaw(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qcdev->recovery_disabled, false);
+
return 0;
}
@@ -938,6 +956,8 @@ static int quicki2c_poweroff(struct device *device)
thc_ltr_unconfig(qcdev->thc_hw);
+ quicki2c_dev_deinit(qcdev);
+
quicki2c_dma_deinit(qcdev);
return 0;
diff --git a/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h b/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h
index 61dbdece59a1..d72565de67b9 100644
--- a/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h
+++ b/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h
@@ -191,6 +191,8 @@ struct acpi_device;
* @i2c_max_frame_size: Max RX frame size (unit in Bytes)
* @i2c_int_delay_enable: Indicate interrupt delay feature enabled or not
* @i2c_int_delay: Interrupt detection delay value (unit in 10 us)
+ * @recover_work: Work structure for recovery
+ * @recovery_disabled: Whether recovery work is blocked during teardown
*/
struct quicki2c_device {
struct device *dev;
@@ -226,6 +228,9 @@ struct quicki2c_device {
u32 i2c_max_frame_size;
u32 i2c_int_delay_enable;
u32 i2c_int_delay;
+
+ struct work_struct recover_work;
+ bool recovery_disabled;
};
#endif /* _QUICKI2C_DEV_H_ */
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread