Skip to content

Commit 61e5af3

Browse files
jakebaileydeadprogram
authored andcommitted
runtime: fix multicore RISC-V GC synchronization
Separate scheduler wakeups, GC pause requests, and GC phase signals so software interrupts cannot consume requests for another subsystem. Make RISC-V task stack switches atomic with respect to interrupts and derive GC stack selection from saved system stack state. This prevents the collector from scanning a nil or incorrect stack during a switch. Include mtval in exception reports to identify invalid access addresses. Fixes 5679
1 parent 3a84b1a commit 61e5af3

7 files changed

Lines changed: 102 additions & 43 deletions

File tree

builder/testdata/binary-size.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
target package code rodata data bss
2-
hifive1b examples/echo 4526 346 0 2268
2+
hifive1b examples/echo 4542 346 0 2268
33
microbit examples/serial 2993 391 8 2264
44
wioterminal examples/pininterrupt 8275 1741 148 7496

main_test.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -468,15 +468,6 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) {
468468
continue
469469
}
470470
}
471-
if options.Target == "riscv-qemu" {
472-
switch name {
473-
case "finalizerinvariants.go":
474-
// The finalizer code stops or fails on multicore RISC-V.
475-
// See https://github.com/tinygo-org/tinygo/issues/5679
476-
continue
477-
}
478-
}
479-
480471
name := name // redefine to avoid race condition
481472
t.Run(name, func(t *testing.T) {
482473
t.Parallel()

src/internal/task/task_stack.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,3 @@ func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
7676
t.state.initialize(fn, args, stackSize)
7777
scheduleTask(t)
7878
}
79-
80-
// OnSystemStack returns whether the caller is running on the system stack.
81-
func OnSystemStack() bool {
82-
// If there is not an active goroutine, then this must be running on the system stack.
83-
return Current() == nil
84-
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build (scheduler.tasks || scheduler.cores) && !tinygo.riscv
2+
3+
package task
4+
5+
// OnSystemStack returns whether the caller is running on the system stack.
6+
func OnSystemStack() bool {
7+
// If there is no active goroutine, this must be the system stack.
8+
return Current() == nil
9+
}

src/internal/task/task_stack_tinygoriscv.S

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,21 @@ tinygo_startTask:
2121
// After return, exit this goroutine. This is a tail call.
2222
tail tinygo_task_exit
2323

24-
.section .text.tinygo_swapTask
24+
.section .text.tinygo_swapTaskRISC
25+
.global tinygo_swapTaskRISC
26+
.type tinygo_swapTaskRISC, %function
27+
tinygo_swapTaskRISC:
2528
.global tinygo_swapTask
2629
.type tinygo_swapTask, %function
27-
tinygo_swapTask:
30+
.set tinygo_swapTask, tinygo_swapTaskRISC
31+
2832
// This function gets the following parameters:
2933
// a0 = newStack uintptr
3034
// a1 = oldStack *uintptr
35+
// a2 = stack pointer to clear after switching, or nil
36+
37+
// Keep the prior hart state in t0. It is not part of the task context.
38+
csrrci t0, mstatus, 8
3139

3240
// Push all callee-saved registers.
3341
addi sp, sp, -52
@@ -51,6 +59,11 @@ tinygo_swapTask:
5159
// Switch to the new stack pointer.
5260
mv sp, a0
5361

62+
// Clear the saved system stack only after switching back to it.
63+
beqz a2, 1f
64+
sw zero, 0(a2)
65+
1:
66+
5467
// Pop all saved registers from this new stack.
5568
lw ra, 48(sp)
5669
lw s11, 44(sp)
@@ -67,5 +80,11 @@ tinygo_swapTask:
6780
lw s0, (sp)
6881
addi sp, sp, 52
6982

83+
// Restore the interrupt-enable state.
84+
andi t0, t0, 8
85+
beqz t0, 2f
86+
csrsi mstatus, 8
87+
2:
88+
7089
// Return into the task.
7190
ret

src/internal/task/task_stack_tinygoriscv.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,29 @@ func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
5656
}
5757

5858
func (s *state) resume() {
59-
swapTask(s.sp, runtime_systemStackPtr())
59+
swapTaskRISC(s.sp, runtime_systemStackPtr(), nil)
6060
}
6161

6262
func (s *state) pause() {
6363
systemStackPtr := runtime_systemStackPtr()
6464
newStack := *systemStackPtr
65-
*systemStackPtr = 0
66-
swapTask(newStack, &s.sp)
65+
swapTaskRISC(newStack, &s.sp, systemStackPtr)
6766
}
6867

68+
//export tinygo_swapTaskRISC
69+
func swapTaskRISC(oldStack uintptr, newStack, clearStack *uintptr)
70+
6971
// SystemStack returns the system stack pointer when called from a task stack.
7072
// When called from the system stack, it returns 0.
7173
func SystemStack() uintptr {
7274
return *runtime_systemStackPtr()
7375
}
7476

77+
// OnSystemStack returns whether the caller is running on the system stack.
78+
func OnSystemStack() bool {
79+
return SystemStack() == 0
80+
}
81+
7582
//export tinygo_task_current
7683
func tinygo_task_current() unsafe.Pointer {
7784
return unsafe.Pointer(Current())

src/runtime/runtime_tinygoriscv_qemu.go

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func main() {
5656
// should exit immediately.
5757
// Signal hart 0 to exit.
5858
exitCodePlusOne.Store(0 + 1) // exit code 0
59-
aclintMSWI.MSIP[0].Set(1)
59+
signalHart(0)
6060

6161
// Unlock the scheduler to be sure. Shouldn't be needed.
6262
schedulerLock.Unlock()
@@ -96,16 +96,24 @@ func handleInterrupt() {
9696
hartID := currentCPU()
9797
switch code {
9898
case riscv.MachineSoftwareInterrupt:
99+
// Clear the interrupt before checking state so a new request stays pending.
100+
// See RISC-V Unprivileged ISA, section 2.7.
101+
aclintMSWI.MSIP[hartID].Set(0)
102+
riscv.Asm("fence")
99103
if exitCodePlusOne.Load() != 0 {
100104
exitNow(exitCodePlusOne.Load() - 1)
101105
}
102-
if gcScanState.Load() != 0 {
106+
if gcPauseRequest[hartID].Swap(0) != 0 {
103107
// The GC needs to run.
104108
gcInterruptHandler(hartID)
105109
}
110+
if exitCodePlusOne.Load() != 0 {
111+
exitNow(exitCodePlusOne.Load() - 1)
112+
}
106113
checkpoint := &schedulerWaitCheckpoints[hartID]
107-
if checkpoint.Saved() {
108-
aclintMSWI.MSIP[hartID].Set(0)
114+
// schedulerLock prevents this flag from being set before the
115+
// checkpoint is saved.
116+
if schedulerWakePending[hartID].Swap(0) != 0 && checkpoint.Saved() {
109117
riscv.MCAUSE.Set(0)
110118
checkpoint.Jump()
111119
}
@@ -132,6 +140,14 @@ func handleInterrupt() {
132140
riscv.MCAUSE.Set(0)
133141
}
134142

143+
var (
144+
// State used to request a GC pause on each hart.
145+
gcPauseRequest [numCPU]atomic.Uint32
146+
147+
// State used to signal the next GC phase to each paused hart.
148+
gcSignalWait [numCPU]atomic.Uint32
149+
)
150+
135151
// The GC interrupted this core for the stop-the-world phase.
136152
// This function handles that, and only returns after the stop-the-world phase
137153
// ended.
@@ -140,17 +156,11 @@ func gcInterruptHandler(hartID uint32) {
140156
savedMIE := riscv.MIE.Get()
141157
riscv.MIE.Set(riscv.MIE_MSIE)
142158

143-
// Disable this interrupt (to be enabled again soon).
144-
aclintMSWI.MSIP[hartID].Set(0)
145-
146159
// Let the GC know we're ready.
147160
gcScanState.Add(1)
148161

149162
// Wait until we get a signal to start scanning.
150-
for riscv.MIP.Get()&riscv.MIP_MSIP == 0 {
151-
riscv.Asm("wfi")
152-
}
153-
aclintMSWI.MSIP[hartID].Set(0)
163+
gcWaitForSignal(hartID)
154164

155165
// Scan the stack(s) of this core.
156166
scanCurrentStack()
@@ -163,10 +173,7 @@ func gcInterruptHandler(hartID uint32) {
163173
gcScanState.Store(1)
164174

165175
// Wait until we get a signal that the stop-the-world phase has ended.
166-
for riscv.MIP.Get()&riscv.MIP_MSIP == 0 {
167-
riscv.Asm("wfi")
168-
}
169-
aclintMSWI.MSIP[hartID].Set(0)
176+
gcWaitForSignal(hartID)
170177

171178
// Restore MIE bits.
172179
riscv.MIE.Set(savedMIE)
@@ -175,6 +182,24 @@ func gcInterruptHandler(hartID uint32) {
175182
gcScanState.Add(1)
176183
}
177184

185+
func gcWaitForSignal(hartID uint32) {
186+
for gcSignalWait[hartID].Load() == 0 {
187+
// Clear unrelated wakeups before checking state to avoid losing a signal.
188+
// See RISC-V Unprivileged ISA, section 2.7.
189+
aclintMSWI.MSIP[hartID].Set(0)
190+
riscv.Asm("fence")
191+
if hartID == 0 && exitCodePlusOne.Load() != 0 {
192+
exitNow(exitCodePlusOne.Load() - 1)
193+
}
194+
if gcSignalWait[hartID].Load() == 0 {
195+
riscv.Asm("wfi")
196+
}
197+
}
198+
gcSignalWait[hartID].Store(0)
199+
aclintMSWI.MSIP[hartID].Set(0)
200+
riscv.Asm("fence")
201+
}
202+
178203
//go:extern _stack_top
179204
var stack0TopSymbol [0]byte
180205

@@ -391,7 +416,7 @@ func startSecondaryCores() {
391416
for hart := 1; hart < numCPU; hart++ {
392417
// Signal the given hart it is ready to start using a software
393418
// interrupt.
394-
aclintMSWI.MSIP[hart].Set(1)
419+
signalHart(uint32(hart))
395420
}
396421
}
397422

@@ -403,6 +428,9 @@ var sleepingHarts uint8
403428
// Checkpoints for cores waiting for runnable tasks.
404429
var schedulerWaitCheckpoints [numCPU]interrupt.Checkpoint
405430

431+
// State used to distinguish scheduler wakeups from other software interrupts.
432+
var schedulerWakePending [numCPU]atomic.Uint32
433+
406434
// Put the scheduler to sleep, since there are no tasks to run.
407435
// This will unlock the scheduler lock, and must be called with the scheduler
408436
// lock held.
@@ -449,21 +477,32 @@ func schedulerWake() {
449477

450478
if hart < 8 {
451479
// There is a sleeping hart. Wake it.
452-
sleepingHarts &^= 1 << hart // clear the bit
453-
aclintMSWI.MSIP[hart].Set(1) // send software interrupt
480+
// Clear the sleeping bit before sending the wakeup.
481+
sleepingHarts &^= 1 << hart
482+
schedulerWakePending[hart].Store(1)
483+
signalHart(uint32(hart))
454484
}
455485
}
456486

457487
// Pause the given core by sending it an interrupt.
458488
func gcPauseCore(core uint32) {
459-
aclintMSWI.MSIP[core].Set(1) // send software interrupt
489+
gcPauseRequest[core].Store(1)
490+
signalHart(core)
460491
}
461492

462493
// Signal the given core that it can resume one step.
463494
// This is called twice after gcPauseCore: the first time to scan the stack of
464495
// the core, and the second time to end the stop-the-world phase.
465496
func gcSignalCore(core uint32) {
466-
aclintMSWI.MSIP[core].Set(1) // send software interrupt
497+
gcSignalWait[core].Store(1)
498+
signalHart(core)
499+
}
500+
501+
func signalHart(hart uint32) {
502+
// Order state writes before the interrupt notification.
503+
// See RISC-V Unprivileged ISA, section 2.7.
504+
riscv.Asm("fence")
505+
aclintMSWI.MSIP[hart].Set(1)
467506
}
468507

469508
func abort() {
@@ -485,7 +524,7 @@ func exit(code int) {
485524
if currentCPU() != 0 {
486525
// Signal hart 0 to exit.
487526
exitCodePlusOne.Store(uint32(code) + 1)
488-
aclintMSWI.MSIP[0].Set(1)
527+
signalHart(0)
489528

490529
// Wait for the interrupt to happen. This should happen immediately.
491530
for {
@@ -519,6 +558,6 @@ func exitNow(code uint32) {
519558
func handleException(code uint) {
520559
// For a list of exception codes, see:
521560
// https://content.riscv.org/wp-content/uploads/2019/08/riscv-privileged-20190608-1.pdf#page=49
522-
print("fatal error: exception with mcause=", code, " pc=", riscv.MEPC.Get(), " hart=", uint(riscv.MHARTID.Get()), "\r\n")
561+
print("fatal error: exception with mcause=", code, " pc=", riscv.MEPC.Get(), " mtval=", riscv.MTVAL.Get(), " hart=", uint(riscv.MHARTID.Get()), "\r\n")
523562
abort()
524563
}

0 commit comments

Comments
 (0)