git: a2b34aaa8d80 - stable/14 - powerpc: Avoid deadlock when CPUs exchange threads

From: Minsoo Choo <mchoo_at_FreeBSD.org>
Date: Tue, 27 Oct 2026 01:18:35 UTC
The branch stable/14 has been updated by mchoo:

URL: https://cgit.FreeBSD.org/src/commit/?id=a2b34aaa8d805782b8786e2deb90527bb49d99b0

commit a2b34aaa8d805782b8786e2deb90527bb49d99b0
Author:     Minsoo Choo <mchoo@FreeBSD.org>
AuthorDate: 2026-09-16 19:37:01 +0000
Commit:     Minsoo Choo <mchoo@FreeBSD.org>
CommitDate: 2026-10-27 01:18:14 +0000

    powerpc: Avoid deadlock when CPUs exchange threads
    
    The PowerPC context switch waited for the incoming thread to leave
    blocked_lock before releasing the outgoing thread.  If two CPUs selected
    each other's running threads concurrently, both CPUs waited for the
    other to perform the release and neither could progress.  This appeared
    as intermittent boot hangs under QEMU MTTCG when taskqueue threads were
    bound to opposite CPUs.
    
    8df2e5421468 ("powerpc: put the isync inside the TD_LOCK()...")
    attempted to fix the same hang by moving isync into the polling loop.
    That did not address the circular wait: isync cannot cause either CPU to
    execute the release store after the loop.  It only added context
    synchronization to failed iterations and changed timing. Move isync back
    to the successful exit, which preserves acquire ordering for subsequent
    context loads.
    
    Match the ordering used by other architectures: publish the outgoing
    thread's new lock before waiting for the incoming thread.  Apply the
    correction to both 32-bit and 64-bit switch paths.
    
    The 2015 change moved the release after the stack switch because the old
    implementation continued through pmap_activate() and optional state
    restoration on the outgoing thread's stack after making that thread
    runnable.  The switch path has since changed: after the release it only
    polls wired td_lock state using registers, then immediately loads the
    incoming PCB_SP before updating per-CPU state or making any calls.
    cpu_switch() also runs with external and decrementer interrupts disabled
    by spinlock_enter().  Thus this does not restore the post-release call
    window that the 2015 change corrected.
    
    Reviewed by:    jhb, jhibbits
    Approved by:    jhb (mentor)
    Fixes: 53607fe3cc8d ("Fix an extremely subtle concurrency bug...")
    Fixes: 7a49d964d3f9 ("Merge r278429 from ppc64:")
    Fixes: 8df2e5421468 ("powerpc: put the isync inside the TD_LOCK()...")
    MFC after:      2 weeks
    Sponsored by:   FreeBSD Foundation
    Differential Revision:  https://reviews.freebsd.org/D59742
    
    (cherry picked from commit 0571d8a0cc606be9a216439b3cfea685b709d6ea)
---
 sys/powerpc/powerpc/swtch32.S | 18 +++++++++++-------
 sys/powerpc/powerpc/swtch64.S | 18 +++++++++++-------
 2 files changed, 22 insertions(+), 14 deletions(-)

diff --git a/sys/powerpc/powerpc/swtch32.S b/sys/powerpc/powerpc/swtch32.S
index ef1a397bf74d..fd531de42a71 100644
--- a/sys/powerpc/powerpc/swtch32.S
+++ b/sys/powerpc/powerpc/swtch32.S
@@ -129,6 +129,15 @@ ENTRY(cpu_switch)
 	sync				/* Make sure all of that finished */
 
 cpu_switchin:
+	/*
+	 * Release the old thread before waiting for the new one.  Two CPUs
+	 * may exchange threads, so waiting first would leave each CPU waiting
+	 * for the other to perform this store.
+	 */
+	cmpwi	%r14,0
+	beq-	1f
+	stw	%r16,TD_LOCK(%r14)	/* ULE:	update old thread's lock */
+1:
 #if defined(SMP) && defined(SCHED_ULE)
 	/* Wait for the new thread to become unblocked */
 	bl	1f
@@ -141,19 +150,14 @@ cpu_switchin:
 blocked_loop:
 	lwz	%r7,TD_LOCK(%r2)
 	cmpw	%r6,%r7 
-	isync
 	beq-	blocked_loop
+	isync				/* Acquire thread state. */
 #endif
 
 	lwz	%r17,TD_PCB(%r2)	/* Get new current PCB */
 	lwz	%r1,PCB_SP(%r17)	/* Load new stack pointer */
 
-	/* Release old thread now that we have a stack pointer set up */
-	cmpwi	%r14,0
-	beq-	1f
-	stw	%r16,TD_LOCK(%r14)	/* ULE:	update old thread's lock */
-
-1:	mfsprg	%r7,0			/* Get the pcpu pointer */
+	mfsprg	%r7,0			/* Get the pcpu pointer */
 	stw	%r2,PC_CURTHREAD(%r7)	/* Store new current thread */
 	lwz	%r17,TD_PCB(%r2)	/* Store new current PCB */
 	stw	%r17,PC_CURPCB(%r7)
diff --git a/sys/powerpc/powerpc/swtch64.S b/sys/powerpc/powerpc/swtch64.S
index ba37274d32bb..2ea97891190e 100644
--- a/sys/powerpc/powerpc/swtch64.S
+++ b/sys/powerpc/powerpc/swtch64.S
@@ -187,6 +187,15 @@ save_tar:
 	sync				/* Make sure all of that finished */
 
 cpu_switchin:
+	/*
+	 * Release the old thread before waiting for the new one.  Two CPUs
+	 * may exchange threads, so waiting first would leave each CPU waiting
+	 * for the other to perform this store.
+	 */
+	cmpdi	%r14,0
+	beq-	1f
+	std	%r16,TD_LOCK(%r14)	/* ULE:	update old thread's lock */
+1:
 #if defined(SMP) && defined(SCHED_ULE)
 	/* Wait for the new thread to become unblocked */
 	addis	%r6,%r2,TOC_REF(blocked_lock)@ha
@@ -194,20 +203,15 @@ cpu_switchin:
 blocked_loop:
 	ld	%r7,TD_LOCK(%r13)
 	cmpd	%r6,%r7 
-	isync
 	beq-	blocked_loop
+	isync				/* Acquire thread state. */
 #endif
 	
 	ld	%r17,TD_PCB(%r13)	/* Get new PCB */
 	ld	%r1,PCB_SP(%r17)	/* Load the stack pointer */
 	addi	%r1,%r1,-48		/* Remember about cpu_switch stack frame */
 
-	/* Release old thread now that we have a stack pointer set up */
-	cmpdi	%r14,0
-	beq-	1f
-	std	%r16,TD_LOCK(%r14)	/* ULE:	update old thread's lock */
-
-1:	mfsprg	%r7,0			/* Get the pcpu pointer */
+	mfsprg	%r7,0			/* Get the pcpu pointer */
 	std	%r13,PC_CURTHREAD(%r7)	/* Store new current thread */
 	ld	%r17,TD_PCB(%r13)	/* Store new current PCB */
 	std	%r17,PC_CURPCB(%r7)