Subject: [PATCH] usb: dwc_otg: fix system lockup when interrupts are threaded
From: Oussama Ghorbel <[email-masked]>
Date: Sun, 6 Nov 2016 00:16:02 +0100

Fix lockup in dwc_otg driver that leads to a system freeze of the
4-core Raspberry Pi board when RT Preempt kernel is in use or when
interrupts are threaded in general.
The lockup occurs when the irq handler thread gets preempted while it
holds the fiq spin lock.
The patch makes sure to disable local irq while fiq spin lock is held
irrespective of whether the interrupt is threaded or not.
The patch also unifies the use of the fiq spin lock outside the fiq
handler by introducing two function-like macros fiq_fsm_spin_lock_irqsave
and fiq_fsm_spin_unlock_irqrestore.

Under RT kernel, the bug can be reproduced in a few minutes by running
hackbench and cyclictest in this way
$ ( while true; do nice hackbench 30 >/dev/null; done )&
$ echo "run 'kill $!' to stop hackbench"
$ cyclictest -a -t -n -p 80

Signed-off-by: Oussama Ghorbel <[email-masked]>

---
 drivers/usb/host/dwc_otg/dwc_otg_fiq_fsm.h   |   14 +++++++++
 drivers/usb/host/dwc_otg/dwc_otg_hcd.c       |   40 +++++++++++----------------
 drivers/usb/host/dwc_otg/dwc_otg_hcd_intr.c  |   39 ++++++++++----------------
 drivers/usb/host/dwc_otg/dwc_otg_hcd_queue.c |   14 ++++-----
 4 files changed, 52 insertions(+), 55 deletions(-)

Index: linux-rpi-4.18.4-test/drivers/usb/host/dwc_otg/dwc_otg_fiq_fsm.h
===================================================================
--- linux-rpi-4.18.4-test.orig/drivers/usb/host/dwc_otg/dwc_otg_fiq_fsm.h
+++ linux-rpi-4.18.4-test/drivers/usb/host/dwc_otg/dwc_otg_fiq_fsm.h
@@ -393,4 +393,18 @@ extern void dwc_otg_fiq_fsm(struct fiq_s
 
 extern void dwc_otg_fiq_nop(struct fiq_state *state);
 
+#define fiq_fsm_spin_lock_irqsave(lock, flags)		\
+	do {						\
+		local_fiq_disable();			\
+		local_irq_save(flags);			\
+		fiq_fsm_spin_lock(lock);		\
+	} while (0)
+
+#define fiq_fsm_spin_unlock_irqrestore(lock, flags)	\
+	do {						\
+		fiq_fsm_spin_unlock(lock);		\
+		local_irq_restore(flags);		\
+		local_fiq_enable();			\
+	} while (0)
+
 #endif /* DWC_OTG_FIQ_FSM_H_ */
Index: linux-rpi-4.18.4-test/drivers/usb/host/dwc_otg/dwc_otg_hcd.c
===================================================================
--- linux-rpi-4.18.4-test.orig/drivers/usb/host/dwc_otg/dwc_otg_hcd.c
+++ linux-rpi-4.18.4-test/drivers/usb/host/dwc_otg/dwc_otg_hcd.c
@@ -1414,12 +1414,11 @@ static void assign_and_init_hc(dwc_otg_h
 
 	dwc_otg_hc_init(hcd->core_if, hc);
 
-	local_irq_save(flags);
 
-	if (fiq_enable) {
-		local_fiq_disable();
-		fiq_fsm_spin_lock(&hcd->fiq_state->lock);
-	}
+	if (fiq_enable)
+		fiq_fsm_spin_lock_irqsave(&hcd->fiq_state->lock, flags);
+	else
+		local_irq_save(flags);
 
 	/* Enable the top level host channel interrupt. */
 	intr_enable = (1 << hc->hc_num);
@@ -1429,12 +1428,11 @@ static void assign_and_init_hc(dwc_otg_h
 	gintmsk.b.hcintr = 1;
 	DWC_MODIFY_REG32(&hcd->core_if->core_global_regs->gintmsk, 0, gintmsk.d32);
 
-	if (fiq_enable) {
-		fiq_fsm_spin_unlock(&hcd->fiq_state->lock);
-		local_fiq_enable();
-	}
+	if (fiq_enable)
+		fiq_fsm_spin_unlock_irqrestore(&hcd->fiq_state->lock, flags);
+	else
+		local_irq_restore(flags);
 
-	local_irq_restore(flags);
 	hc->qh = qh;
 }
 
@@ -1646,6 +1644,7 @@ int fiq_fsm_queue_isoc_transaction(dwc_o
 	int xfer_len, nrpackets;
 	hcdma_data_t hcdma;
 	hfnum_data_t hfnum;
+	unsigned long flags;
 
 	if (st->fsm != FIQ_PASSTHROUGH)
 		return 0;
@@ -1721,8 +1720,7 @@ int fiq_fsm_queue_isoc_transaction(dwc_o
 	fiq_print(FIQDBG_INT, hcd->fiq_state, "%08x", st->hctsiz_copy.d32);
 	fiq_print(FIQDBG_INT, hcd->fiq_state, "%08x", st->hcdma_copy.d32);
 	hfnum.d32 = DWC_READ_REG32(&hcd->core_if->host_if->host_global_regs->hfnum);
-	local_fiq_disable();
-	fiq_fsm_spin_lock(&hcd->fiq_state->lock);
+	fiq_fsm_spin_lock_irqsave(&hcd->fiq_state->lock, flags);
 	DWC_WRITE_REG32(&hc_regs->hctsiz, st->hctsiz_copy.d32);
 	DWC_WRITE_REG32(&hc_regs->hcsplt, st->hcsplt_copy.d32);
 	DWC_WRITE_REG32(&hc_regs->hcdma, st->hcdma_copy.d32);
@@ -1742,8 +1740,7 @@ int fiq_fsm_queue_isoc_transaction(dwc_o
 	}
 	mb();
 	st->hcchar_copy.b.chen = 0;
-	fiq_fsm_spin_unlock(&hcd->fiq_state->lock);
-	local_fiq_enable();
+	fiq_fsm_spin_unlock_irqrestore(&hcd->fiq_state->lock, flags);
 	return 0;
 }
 
@@ -1769,6 +1766,7 @@ int fiq_fsm_queue_split_transaction(dwc_
 	/* Program HC registers, setup FIQ_state, examine FIQ if periodic, start transfer (not if uframe 5) */
 	int hub_addr, port_addr, frame, uframe;
 	struct fiq_channel_state *st = &hcd->fiq_state->channel[hc->hc_num];
+	unsigned long flags;
 
 	/*
 	 * Non-periodic channel assignments stay in the non_periodic_active queue.
@@ -1889,8 +1887,7 @@ int fiq_fsm_queue_split_transaction(dwc_
 	DWC_WRITE_REG32(&hc_regs->hcchar, st->hcchar_copy.d32);
 	DWC_WRITE_REG32(&hc_regs->hcintmsk, st->hcintmsk_copy.d32);
 
-	local_fiq_disable();
-	fiq_fsm_spin_lock(&hcd->fiq_state->lock);
+	fiq_fsm_spin_lock_irqsave(&hcd->fiq_state->lock, flags);
 
 	if (hc->ep_type & 0x1) {
 		hfnum.d32 = DWC_READ_REG32(&hcd->core_if->host_if->host_global_regs->hfnum);
@@ -1999,8 +1996,7 @@ int fiq_fsm_queue_split_transaction(dwc_
 		DWC_WRITE_REG32(&hc_regs->hcchar, st->hcchar_copy.d32);
 	}
 	mb();
-	fiq_fsm_spin_unlock(&hcd->fiq_state->lock);
-	local_fiq_enable();
+	fiq_fsm_spin_unlock_irqrestore(&hcd->fiq_state->lock, flags);
 	return 0;
 }
 
@@ -2481,6 +2477,7 @@ static void process_non_periodic_channel
 void dwc_otg_hcd_queue_transactions(dwc_otg_hcd_t * hcd,
 				    dwc_otg_transaction_type_e tr_type)
 {
+	unsigned long flags;
 #ifdef DEBUG_SOF
 	DWC_DEBUGPL(DBG_HCD, "Queue Transactions\n");
 #endif
@@ -2506,11 +2503,9 @@ void dwc_otg_hcd_queue_transactions(dwc_
 			gintmsk.b.nptxfempty = 1;
 
 			if (fiq_enable) {
-				local_fiq_disable();
-				fiq_fsm_spin_lock(&hcd->fiq_state->lock);
+				fiq_fsm_spin_lock_irqsave(&hcd->fiq_state->lock, flags);
 				DWC_MODIFY_REG32(&hcd->core_if->core_global_regs->gintmsk, gintmsk.d32, 0);
-				fiq_fsm_spin_unlock(&hcd->fiq_state->lock);
-				local_fiq_enable();
+				fiq_fsm_spin_unlock_irqrestore(&hcd->fiq_state->lock, flags);
 			} else {
 				DWC_MODIFY_REG32(&hcd->core_if->core_global_regs->gintmsk, gintmsk.d32, 0);
 			}
Index: linux-rpi-4.18.4-test/drivers/usb/host/dwc_otg/dwc_otg_hcd_intr.c
===================================================================
--- linux-rpi-4.18.4-test.orig/drivers/usb/host/dwc_otg/dwc_otg_hcd_intr.c
+++ linux-rpi-4.18.4-test/drivers/usb/host/dwc_otg/dwc_otg_hcd_intr.c
@@ -83,6 +83,7 @@ int32_t dwc_otg_hcd_handle_intr(dwc_otg_
 	gintmsk_data_t gintmsk;
 	hfnum_data_t hfnum;
 	haintmsk_data_t haintmsk;
+	unsigned long flags;
 
 #ifdef DEBUG
 	dwc_otg_core_global_regs_t *global_regs = core_if->core_global_regs;
@@ -100,8 +101,7 @@ int32_t dwc_otg_hcd_handle_intr(dwc_otg_
 	/* Check if HOST Mode */
 	if (dwc_otg_is_host_mode(core_if)) {
 		if (fiq_enable) {
-			local_fiq_disable();
-			fiq_fsm_spin_lock(&dwc_otg_hcd->fiq_state->lock);
+			fiq_fsm_spin_lock_irqsave(&dwc_otg_hcd->fiq_state->lock, flags);
 			/* Pull in from the FIQ's disabled mask */
 			gintmsk.d32 = gintmsk.d32 | ~(dwc_otg_hcd->fiq_state->gintmsk_saved.d32);
 			dwc_otg_hcd->fiq_state->gintmsk_saved.d32 = ~0;
@@ -118,8 +118,7 @@ int32_t dwc_otg_hcd_handle_intr(dwc_otg_
 		gintsts.d32 &= gintmsk.d32;
 
 		if (fiq_enable) {
-			fiq_fsm_spin_unlock(&dwc_otg_hcd->fiq_state->lock);
-			local_fiq_enable();
+			fiq_fsm_spin_unlock_irqrestore(&dwc_otg_hcd->fiq_state->lock, flags);
 		}
 
 		if (!gintsts.d32) {
@@ -166,11 +165,9 @@ int32_t dwc_otg_hcd_handle_intr(dwc_otg_
 			gintmsk_data_t gintmsk = { .b.portintr = 1};
 			retval |= dwc_otg_hcd_handle_port_intr(dwc_otg_hcd);
 			if (fiq_enable) {
-				local_fiq_disable();
-				fiq_fsm_spin_lock(&dwc_otg_hcd->fiq_state->lock);
+				fiq_fsm_spin_lock_irqsave(&dwc_otg_hcd->fiq_state->lock, flags);
 				DWC_MODIFY_REG32(&dwc_otg_hcd->core_if->core_global_regs->gintmsk, 0, gintmsk.d32);
-				fiq_fsm_spin_unlock(&dwc_otg_hcd->fiq_state->lock);
-				local_fiq_enable();
+				fiq_fsm_spin_unlock_irqrestore(&dwc_otg_hcd->fiq_state->lock, flags);
 			} else {
 				DWC_MODIFY_REG32(&dwc_otg_hcd->core_if->core_global_regs->gintmsk, 0, gintmsk.d32);
 			}
@@ -210,8 +207,7 @@ exit_handler_routine:
 	if (fiq_enable)	{
 		gintmsk_data_t gintmsk_new;
 		haintmsk_data_t haintmsk_new;
-		local_fiq_disable();
-		fiq_fsm_spin_lock(&dwc_otg_hcd->fiq_state->lock);
+		fiq_fsm_spin_lock_irqsave(&dwc_otg_hcd->fiq_state->lock, flags);
 		gintmsk_new.d32 = *(volatile uint32_t *)&dwc_otg_hcd->fiq_state->gintmsk_saved.d32;
 		if(fiq_fsm_enable)
 			haintmsk_new.d32 = *(volatile uint32_t *)&dwc_otg_hcd->fiq_state->haintmsk_saved.d32;
@@ -234,8 +230,7 @@ exit_handler_routine:
 		haintmsk.d32 = DWC_READ_REG32(&core_if->host_if->host_global_regs->haintmsk);
 		/* Re-enable interrupts that the FIQ masked (first time round) */
 		FIQ_WRITE(dwc_otg_hcd->fiq_state->dwc_regs_base + GINTMSK, gintmsk.d32);
-		fiq_fsm_spin_unlock(&dwc_otg_hcd->fiq_state->lock);
-		local_fiq_enable();
+		fiq_fsm_spin_unlock_irqrestore(&dwc_otg_hcd->fiq_state->lock, flags);
 
 		if ((jiffies / HZ) > last_time) {
 			//dwc_otg_qh_t *qh;
@@ -637,6 +632,7 @@ int32_t dwc_otg_hcd_handle_hc_intr(dwc_o
 {
 	int i;
 	int retval = 0;
+	unsigned long flags;
 	haint_data_t haint = { .d32 = 0 } ;
 
 	/* Clear appropriate bits in HCINTn to clear the interrupt bit in
@@ -649,12 +645,10 @@ int32_t dwc_otg_hcd_handle_hc_intr(dwc_o
 	if(fiq_fsm_enable)
 	{
 		/* check the mask? */
-		local_fiq_disable();
-		fiq_fsm_spin_lock(&dwc_otg_hcd->fiq_state->lock);
+		fiq_fsm_spin_lock_irqsave(&dwc_otg_hcd->fiq_state->lock, flags);
 		haint.b2.chint |= ~(dwc_otg_hcd->fiq_state->haintmsk_saved.b2.chint);
 		dwc_otg_hcd->fiq_state->haintmsk_saved.b2.chint = ~0;
-		fiq_fsm_spin_unlock(&dwc_otg_hcd->fiq_state->lock);
-		local_fiq_enable();
+		fiq_fsm_spin_unlock_irqrestore(&dwc_otg_hcd->fiq_state->lock, flags);
 	}
 
 	for (i = 0; i < dwc_otg_hcd->core_if->core_params->host_channels; i++) {
@@ -1061,6 +1055,7 @@ static void halt_channel(dwc_otg_hcd_t *
 			 dwc_hc_t * hc,
 			 dwc_otg_qtd_t * qtd, dwc_otg_halt_status_e halt_status)
 {
+	unsigned long flags;
 	if (hcd->core_if->dma_enable) {
 		release_channel(hcd, hc, qtd, halt_status);
 		return;
@@ -1083,11 +1078,9 @@ static void halt_channel(dwc_otg_hcd_t *
 			 */
 			gintmsk.b.nptxfempty = 1;
 			if (fiq_enable) {
-				local_fiq_disable();
-				fiq_fsm_spin_lock(&hcd->fiq_state->lock);
+				fiq_fsm_spin_lock_irqsave(&hcd->fiq_state->lock, flags);
 				DWC_MODIFY_REG32(&global_regs->gintmsk, 0, gintmsk.d32);
-				fiq_fsm_spin_unlock(&hcd->fiq_state->lock);
-				local_fiq_enable();
+				fiq_fsm_spin_unlock_irqrestore(&hcd->fiq_state->lock, flags);
 			} else {
 				DWC_MODIFY_REG32(&global_regs->gintmsk, 0, gintmsk.d32);
 			}
@@ -1108,11 +1101,9 @@ static void halt_channel(dwc_otg_hcd_t *
 			 */
 			gintmsk.b.ptxfempty = 1;
 			if (fiq_enable) {
-				local_fiq_disable();
-				fiq_fsm_spin_lock(&hcd->fiq_state->lock);
+				fiq_fsm_spin_lock_irqsave(&hcd->fiq_state->lock, flags);
 				DWC_MODIFY_REG32(&global_regs->gintmsk, 0, gintmsk.d32);
-				fiq_fsm_spin_unlock(&hcd->fiq_state->lock);
-				local_fiq_enable();
+				fiq_fsm_spin_unlock_irqrestore(&hcd->fiq_state->lock, flags);
 			} else {
 				DWC_MODIFY_REG32(&global_regs->gintmsk, 0, gintmsk.d32);
 			}
Index: linux-rpi-4.18.4-test/drivers/usb/host/dwc_otg/dwc_otg_hcd_queue.c
===================================================================
--- linux-rpi-4.18.4-test.orig/drivers/usb/host/dwc_otg/dwc_otg_hcd_queue.c
+++ linux-rpi-4.18.4-test/drivers/usb/host/dwc_otg/dwc_otg_hcd_queue.c
@@ -676,6 +676,7 @@ static int schedule_periodic(dwc_otg_hcd
 int dwc_otg_hcd_qh_add(dwc_otg_hcd_t * hcd, dwc_otg_qh_t * qh)
 {
 	int status = 0;
+	unsigned long flags;
 	gintmsk_data_t intr_mask = {.d32 = 0 };
 
 	if (!DWC_LIST_EMPTY(&qh->qh_list_entry)) {
@@ -694,11 +695,9 @@ int dwc_otg_hcd_qh_add(dwc_otg_hcd_t * h
 		if ( !hcd->periodic_qh_count ) {
 			intr_mask.b.sofintr = 1;
 			if (fiq_enable) {
-				local_fiq_disable();
-				fiq_fsm_spin_lock(&hcd->fiq_state->lock);
+				fiq_fsm_spin_lock_irqsave(&hcd->fiq_state->lock, flags);
 				DWC_MODIFY_REG32(&hcd->core_if->core_global_regs->gintmsk, intr_mask.d32, intr_mask.d32);
-				fiq_fsm_spin_unlock(&hcd->fiq_state->lock);
-				local_fiq_enable();
+				fiq_fsm_spin_unlock_irqrestore(&hcd->fiq_state->lock, flags);
 			} else {
 				DWC_MODIFY_REG32(&hcd->core_if->core_global_regs->gintmsk, intr_mask.d32, intr_mask.d32);
 			}
@@ -742,6 +741,7 @@ static void deschedule_periodic(dwc_otg_
  * @param qh QH to remove from schedule. */
 void dwc_otg_hcd_qh_remove(dwc_otg_hcd_t * hcd, dwc_otg_qh_t * qh)
 {
+	unsigned long flags;
 	gintmsk_data_t intr_mask = {.d32 = 0 };
 
 	if (DWC_LIST_EMPTY(&qh->qh_list_entry)) {
@@ -763,11 +763,9 @@ void dwc_otg_hcd_qh_remove(dwc_otg_hcd_t
 		if( !hcd->periodic_qh_count && !fiq_fsm_enable ) {
 			intr_mask.b.sofintr = 1;
 			if (fiq_enable) {
-				local_fiq_disable();
-				fiq_fsm_spin_lock(&hcd->fiq_state->lock);
+				fiq_fsm_spin_lock_irqsave(&hcd->fiq_state->lock, flags);
 				DWC_MODIFY_REG32(&hcd->core_if->core_global_regs->gintmsk, intr_mask.d32, 0);
-				fiq_fsm_spin_unlock(&hcd->fiq_state->lock);
-				local_fiq_enable();
+				fiq_fsm_spin_unlock_irqrestore(&hcd->fiq_state->lock, flags);
 			} else {
 				DWC_MODIFY_REG32(&hcd->core_if->core_global_regs->gintmsk, intr_mask.d32, 0);
 			}