scsi: ufs: core: Serialize AHIT register access between sysfs and host driver

A race exists between ufshcd_auto_hibern8_update() and
ufs_mtk_pwr_change_notify() when both paths concurrently access
REG_AUTO_HIBERNATE_IDLE_TIMER. Additionally, the static local variable used
to save/restore AHIT in the power change path is not re-entrant and may
hold a stale value if a sysfs write races with a gear change.

Introduce ahit_mutex and ahit_disable_depth in struct ufs_hba to
serialize AHIT register access. The depth counter tracks how many
host-driver paths have forced AHIT off; sysfs writes are suppressed
while it is non-zero and the register is restored from hba->ahit
only when the last disabler drops it to zero.

Fixes: f5ca8d0c7a ("scsi: ufs: host: mediatek: Disable auto-hibern8 during power mode changes")
Signed-off-by: Peter Wang <peter.wang@mediatek.com>
Reviewed-by: Chun-Hung Wu <chun-hung.wu@mediatek.com>
Link: https://patch.msgid.link/20260914085148.136429-3-peter.wang@mediatek.com
Signed-off-by: Martin K. Petersen (Oracle) <mkp@kernel.org>
This commit is contained in:
Peter Wang 2026-09-14 16:49:25 +08:00 committed by Martin K. Petersen (Oracle)
parent 6efee14ca0
commit 7dec5cedf7
3 changed files with 33 additions and 5 deletions

View File

@ -4789,7 +4789,17 @@ void ufshcd_auto_hibern8_update(struct ufs_hba *hba, u32 ahit)
if (!pm_runtime_suspended(&hba->ufs_device_wlun->sdev_gendev)) {
ufshcd_rpm_get_sync(hba);
ufshcd_hold(hba);
ufshcd_configure_auto_hibern8(hba);
/*
* Serialize with the host driver's AHIT control. Skip the
* register write while the driver has AHIT forced off; the
* driver programs hba->ahit back when it re-enables it.
* Mutex is taken here (not across the rpm calls above) so it
* never blocks a resume that the driver's path may need.
*/
mutex_lock(&hba->ahit_mutex);
if (!hba->ahit_disable_depth)
ufshcd_configure_auto_hibern8(hba);
mutex_unlock(&hba->ahit_mutex);
ufshcd_release(hba);
ufshcd_rpm_put_sync(hba);
}
@ -11318,6 +11328,8 @@ int ufshcd_init(struct ufs_hba *hba, void __iomem *mmio_base, unsigned int irq)
mutex_init(&hba->wb_mutex);
mutex_init(&hba->ahit_mutex);
init_rwsem(&hba->clk_scaling_lock);
ufshcd_init_clk_gating(hba);

View File

@ -1507,19 +1507,27 @@ static int ufs_mtk_pwr_change_notify(struct ufs_hba *hba,
struct ufs_pa_layer_attr *dev_req_params)
{
int ret = 0;
static u32 reg;
switch (stage) {
case PRE_CHANGE:
if (ufshcd_is_auto_hibern8_supported(hba)) {
reg = ufshcd_readl(hba, REG_AUTO_HIBERNATE_IDLE_TIMER);
/* Block sysfs AHIT writes while we force AHIT off */
mutex_lock(&hba->ahit_mutex);
hba->ahit_disable_depth++;
mutex_unlock(&hba->ahit_mutex);
ufs_mtk_auto_hibern8_disable(hba);
}
ret = ufs_mtk_pre_pwr_change(hba, dev_req_params);
break;
case POST_CHANGE:
if (ufshcd_is_auto_hibern8_supported(hba))
ufshcd_writel(hba, reg, REG_AUTO_HIBERNATE_IDLE_TIMER);
if (ufshcd_is_auto_hibern8_supported(hba)) {
mutex_lock(&hba->ahit_mutex);
/* re-enable only when the last disabler drops off */
if (!--hba->ahit_disable_depth)
ufshcd_writel(hba, hba->ahit,
REG_AUTO_HIBERNATE_IDLE_TIMER);
mutex_unlock(&hba->ahit_mutex);
}
break;
default:
ret = -EINVAL;

View File

@ -1132,6 +1132,14 @@ struct ufs_hba {
/* Auto-Hibernate Idle Timer register value */
u32 ahit;
/* Serializes AHIT register access between sysfs and the host driver */
struct mutex ahit_mutex;
/*
* Nesting count of host-driver paths that have AHIT forced off.
* Non-zero blocks sysfs AHIT writes; AHIT is reprogrammed only when
* the last disabler drops it back to zero. Guarded by ahit_mutex.
*/
int ahit_disable_depth;
unsigned long outstanding_tasks;
spinlock_t outstanding_lock;