Repository navigation
Conversation
…with -scheduler=cores Timer callbacks run on the scheduler stack with no current goroutine. If the channel lock is held by another core, the callback tried to sleep as a nil task, faulted while holding the global futex lock, and left the other core stalled. Spinning in that case lets the timer send complete once the other core releases the channel lock. Updates tinygo-org#6047
Contributor
|
@dgryski thanks for this, I have a pico set up with the software debugger and I’ll test this. |
Member
Author
|
I don't think this will affect the pico2 crash. Claude tried to model the rp2 cores scheduler in Spin and couldn't reproduce this hang and didn't find any other issues. Also, this particular hang found by the Spin model required three cores, and the rp2 only has two. |
A hart can take the GC stop-the-world interrupt while it holds schedulerLock and park in the handler with the lock held. Another hart spinning on the lock with interrupts off then blocks the GC forever. Keep interrupts off while the lock is held and restore the previous state on unlock. Updates tinygo-org#6047.
dgryski
force-pushed
the
dgryski/qemu-scheduler-hang
branch
from
October 28, 2026 17:44
86ce286 to
be3c825
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Updates #6047. Based on #6060, so the first commit here is from that PR.
Summary
With
-scheduler=coreson riscv-qemu, the reproducer from #6047 still hangs after #6060. It does not panic. The program stops with no output.A hart can take the GC stop-the-world interrupt while it holds
schedulerLockwith interrupts enabled. It then parks ingcInterruptHandlerwith the lock held. If another hart is spinning onschedulerLockwith interrupts off, that hart cannot take the interrupt, and the GC initiator waits for it forever. This needs at least three harts.A hart can end up spinning with interrupts off in two ways seen in hung runs.
chanTrySenddisables interrupts and then callsscheduleTask, for example fromtime.sendTimein a timer callback.Pausecalled fromallocthroughgcLockandFutex.Wait. In the captures this stack hadmstatus.MIEclear. The next section explains why.A hart also holds the lock with interrupts enabled after it wakes from
schedulerUnlockAndWait. The checkpoint jump leaves the trap withmret, which restores interrupts, and the scheduler loop then runs with the lock held.This PR adds
schedulerSpinLock.Lockdisables interrupts andUnlockrestores the previous state, saved per hart. EveryschedulerLockcall site gets the change, including the re-lock after idle wake.Interrupt state across task switches
This PR does not change how task switches handle interrupt state, but it matters for the hang.
tinygo_swapTaskcarries the old context'smstatus.MIEinto the new one. A task that pauses with interrupts off can leave the next task running with interrupts off. On riscv-qemu this is common. In the #6047 reproducer,allocwas entered withMIEclear in 32 to 64 of 135 calls per run, always after a resume withMIEclear. I think this deserves a separate issue.How this was found
I looped the #6047 reproducer under qemu with the gdb stub open and attached lldb to hung runs. Each hang had one hart in
gcMarkReachable, one or more harts parked ingcInterruptHandler, and one or more harts spinning inschedulerLock.Lockwithmstatus.MIEclear. With a temporary owner field on the spinlock, the lock owner was a parked hart.I then wrote Spin models. The first covers the lock, per-hart interrupt enable and the GC rendezvous. It deadlocks with 3 and 4 harts and not with 2. With interrupts disabled at every lock site it has no deadlock, checked exhaustively for 3 and 4 harts.
The second model adds the
gcLockfutex mutex, pause and resume, and idle wake. It shows the same deadlock with 3 harts. With the fix it has no deadlock, checked exhaustively for 3 harts. The variant that also allowsallocwith interrupts off was only a partial search. WrappinglockScheduleralone is not enough, because many sites callschedulerLock.Lock()directly.I can share the model files if they are useful.
Testing
On the build with #6060 but without this change, the reproducer hung in 5 of 76 runs across five loops. Some of those builds had temporary debug fields. With this change it passed 300 of 300 runs.
I also wrote three stress programs that mix sleepers, mutexes,
sync.Cond, wait groups, timers, tickers andruntime.GC(). Results over 100 runs each on riscv-qemu with 4 harts.allocgc does not catch the bug. The timers program hangs about half the time without the change, so it is a good candidate for a regression test. I have not added any of these programs to the repo yet.
I also ran the reproducer and the stress programs with
-tags runtime_assertsand a temporary check thatschedulerLockis unlocked on the same hart that locked it. The check never fired. It passed 20 runs each of the reproducer, mixed and timers, and 5 runs of allocgc. The check is not part of this PR. It may be worth adding in the future.Not done
builder/testdata/binary-size.txtneeds regenerating for riscv-qemu. The size test compares it exactly and this change grows the code.sleepTicksMulticoreand flash-safe sections, found no deadlock. The model is simplified, with one task per core, so this does not prove the code is safe. One hypothetical remains. An interrupt handler that wakes a goroutine throughscheduleTaskwould spin onschedulerLockheld by the code it interrupted on the same core. I found no rp2 runtime or machine handler that does this. A user callback could.schedulerAssertsenabled,runtimeFatalatscheduler_cores.go:68now runs with interrupts off while holding the lock. Printing takesprintLock, which a parked hart may hold, so the message could hang instead of printing. This only affects assert builds and I have not tried it.