Skip to content

Commit 07e02b3

Browse files
committed
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 ea3327a commit 07e02b3

6 files changed

Lines changed: 101 additions & 30 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 4558 346 0 2268
33
microbit examples/serial 2993 391 8 2264
44
wioterminal examples/pininterrupt 8309 1739 148 7496

src/internal/task/task_stack_tinygoriscv.S

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

24-
.section .text.tinygo_swapTask
25-
.global tinygo_swapTask
26-
.type tinygo_swapTask, %function
27-
tinygo_swapTask:
24+
.section .text.tinygo_swapTaskRISC
25+
.global tinygo_swapTaskRISC
26+
.type tinygo_swapTaskRISC, %function
27+
tinygo_swapTaskRISC:
2828
// This function gets the following parameters:
2929
// a0 = newStack uintptr
3030
// a1 = oldStack *uintptr
31+
// a2 = stack pointer to clear after switching, or nil
32+
33+
// Prevent an interrupt from observing a half-completed stack switch.
34+
csrrci t0, mstatus, 8
3135

3236
// Push all callee-saved registers.
3337
addi sp, sp, -52
@@ -51,6 +55,11 @@ tinygo_swapTask:
5155
// Switch to the new stack pointer.
5256
mv sp, a0
5357

58+
// Clear the saved system stack only after switching back to it.
59+
beqz a2, 1f
60+
sw zero, 0(a2)
61+
1:
62+
5463
// Pop all saved registers from this new stack.
5564
lw ra, 48(sp)
5665
lw s11, 44(sp)
@@ -67,5 +76,11 @@ tinygo_swapTask:
6776
lw s0, (sp)
6877
addi sp, sp, 52
6978

79+
// Restore the interrupt-enable state.
80+
andi t0, t0, 8
81+
beqz t0, 2f
82+
csrsi mstatus, 8
83+
2:
84+
7085
// Return into the task.
7186
ret

src/internal/task/task_stack_tinygoriscv.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,18 @@ 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+
//go: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 {

src/runtime/gc_stack_cores.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func gcMarkReachable() {
2020
if !secondaryCoresStarted {
2121
// Scan the stack(s) of the current core.
2222
scanCurrentStack()
23-
if !task.OnSystemStack() {
23+
if !gcOnSystemStack() {
2424
// Mark system stack.
2525
markRoots(task.SystemStack(), stackTop)
2626
}
@@ -51,7 +51,7 @@ func gcMarkReachable() {
5151

5252
// Scan the stack(s) of the current core.
5353
scanCurrentStack()
54-
if !task.OnSystemStack() {
54+
if !gcOnSystemStack() {
5555
// Mark system stack.
5656
markRoots(task.SystemStack(), coreStackTop(core))
5757
}
@@ -86,7 +86,7 @@ func scanstack(sp uintptr) {
8686
// Mark the current stack.
8787
// This function is called by scanCurrentStack, after pushing all registers
8888
// onto the stack.
89-
if task.OnSystemStack() {
89+
if gcOnSystemStack() {
9090
// This is the system stack.
9191
// Scan all words on the stack.
9292
markRoots(sp, coreStackTop(currentCPU()))

src/runtime/runtime_rp2.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import (
1515
const numCPU = 2
1616
const numSpinlocks = 32
1717

18+
func gcOnSystemStack() bool {
19+
return task.OnSystemStack()
20+
}
21+
1822
// machineTicks is provided by package machine.
1923
func machineTicks() uint64
2024

src/runtime/runtime_tinygoriscv_qemu.go

Lines changed: 69 additions & 19 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,21 @@ func handleInterrupt() {
9696
hartID := currentCPU()
9797
switch code {
9898
case riscv.MachineSoftwareInterrupt:
99+
// Acknowledge the interrupt before checking its consumers. A new
100+
// request that arrives after this point must remain pending.
101+
aclintMSWI.MSIP[hartID].Set(0)
99102
if exitCodePlusOne.Load() != 0 {
100103
exitNow(exitCodePlusOne.Load() - 1)
101104
}
102-
if gcScanState.Load() != 0 {
105+
if gcPauseRequest[hartID].Swap(0) != 0 {
103106
// The GC needs to run.
104107
gcInterruptHandler(hartID)
105108
}
109+
if exitCodePlusOne.Load() != 0 {
110+
exitNow(exitCodePlusOne.Load() - 1)
111+
}
106112
checkpoint := &schedulerWaitCheckpoints[hartID]
107-
if checkpoint.Saved() {
108-
aclintMSWI.MSIP[hartID].Set(0)
113+
if schedulerWakePending[hartID].Swap(0) != 0 && checkpoint.Saved() {
109114
riscv.MCAUSE.Set(0)
110115
checkpoint.Jump()
111116
}
@@ -132,6 +137,14 @@ func handleInterrupt() {
132137
riscv.MCAUSE.Set(0)
133138
}
134139

140+
var (
141+
// State used to request a GC pause on each hart.
142+
gcPauseRequest [numCPU]atomic.Uint32
143+
144+
// State used to signal the next GC phase to each paused hart.
145+
gcSignalWait [numCPU]atomic.Uint32
146+
)
147+
135148
// The GC interrupted this core for the stop-the-world phase.
136149
// This function handles that, and only returns after the stop-the-world phase
137150
// ended.
@@ -140,21 +153,30 @@ func gcInterruptHandler(hartID uint32) {
140153
savedMIE := riscv.MIE.Get()
141154
riscv.MIE.Set(riscv.MIE_MSIE)
142155

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

149159
// Wait until we get a signal to start scanning.
150-
for riscv.MIP.Get()&riscv.MIP_MSIP == 0 {
151-
riscv.Asm("wfi")
160+
for gcSignalWait[hartID].Load() == 0 {
161+
if hartID == 0 && exitCodePlusOne.Load() != 0 {
162+
exitNow(exitCodePlusOne.Load() - 1)
163+
}
164+
// Clear unrelated scheduler wakeups before waiting again. Check pending
165+
// requests after the clear so a concurrent signal is not lost.
166+
aclintMSWI.MSIP[hartID].Set(0)
167+
if hartID == 0 && exitCodePlusOne.Load() != 0 {
168+
exitNow(exitCodePlusOne.Load() - 1)
169+
}
170+
if gcSignalWait[hartID].Load() == 0 {
171+
riscv.Asm("wfi")
172+
}
152173
}
174+
gcSignalWait[hartID].Store(0)
153175
aclintMSWI.MSIP[hartID].Set(0)
154176

155177
// Scan the stack(s) of this core.
156178
scanCurrentStack()
157-
if !task.OnSystemStack() {
179+
if !gcOnSystemStack() {
158180
// Mark system stack.
159181
markRoots(task.SystemStack(), coreStackTop(hartID))
160182
}
@@ -163,9 +185,19 @@ func gcInterruptHandler(hartID uint32) {
163185
gcScanState.Store(1)
164186

165187
// 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")
188+
for gcSignalWait[hartID].Load() == 0 {
189+
if hartID == 0 && exitCodePlusOne.Load() != 0 {
190+
exitNow(exitCodePlusOne.Load() - 1)
191+
}
192+
aclintMSWI.MSIP[hartID].Set(0)
193+
if hartID == 0 && exitCodePlusOne.Load() != 0 {
194+
exitNow(exitCodePlusOne.Load() - 1)
195+
}
196+
if gcSignalWait[hartID].Load() == 0 {
197+
riscv.Asm("wfi")
198+
}
168199
}
200+
gcSignalWait[hartID].Store(0)
169201
aclintMSWI.MSIP[hartID].Set(0)
170202

171203
// Restore MIE bits.
@@ -386,12 +418,16 @@ func currentCPU() uint32 {
386418
return uint32(riscv.MHARTID.Get())
387419
}
388420

421+
func gcOnSystemStack() bool {
422+
return task.SystemStack() == 0
423+
}
424+
389425
func startSecondaryCores() {
390426
// Start all the other cores besides hart 0.
391427
for hart := 1; hart < numCPU; hart++ {
392428
// Signal the given hart it is ready to start using a software
393429
// interrupt.
394-
aclintMSWI.MSIP[hart].Set(1)
430+
signalHart(uint32(hart))
395431
}
396432
}
397433

@@ -403,6 +439,9 @@ var sleepingHarts uint8
403439
// Checkpoints for cores waiting for runnable tasks.
404440
var schedulerWaitCheckpoints [numCPU]interrupt.Checkpoint
405441

442+
// State used to distinguish scheduler wakeups from other software interrupts.
443+
var schedulerWakePending [numCPU]atomic.Uint32
444+
406445
// Put the scheduler to sleep, since there are no tasks to run.
407446
// This will unlock the scheduler lock, and must be called with the scheduler
408447
// lock held.
@@ -449,21 +488,32 @@ func schedulerWake() {
449488

450489
if hart < 8 {
451490
// There is a sleeping hart. Wake it.
452-
sleepingHarts &^= 1 << hart // clear the bit
453-
aclintMSWI.MSIP[hart].Set(1) // send software interrupt
491+
// Clear the sleeping bit before sending the wakeup.
492+
sleepingHarts &^= 1 << hart
493+
schedulerWakePending[hart].Store(1)
494+
signalHart(uint32(hart))
454495
}
455496
}
456497

457498
// Pause the given core by sending it an interrupt.
458499
func gcPauseCore(core uint32) {
459-
aclintMSWI.MSIP[core].Set(1) // send software interrupt
500+
gcPauseRequest[core].Store(1)
501+
signalHart(core)
460502
}
461503

462504
// Signal the given core that it can resume one step.
463505
// This is called twice after gcPauseCore: the first time to scan the stack of
464506
// the core, and the second time to end the stop-the-world phase.
465507
func gcSignalCore(core uint32) {
466-
aclintMSWI.MSIP[core].Set(1) // send software interrupt
508+
gcSignalWait[core].Store(1)
509+
signalHart(core)
510+
}
511+
512+
func signalHart(hart uint32) {
513+
// Order state writes before the interrupt notification.
514+
// See RISC-V Unprivileged ISA, section 2.7.
515+
riscv.Asm("fence")
516+
aclintMSWI.MSIP[hart].Set(1)
467517
}
468518

469519
func abort() {
@@ -485,7 +535,7 @@ func exit(code int) {
485535
if currentCPU() != 0 {
486536
// Signal hart 0 to exit.
487537
exitCodePlusOne.Store(uint32(code) + 1)
488-
aclintMSWI.MSIP[0].Set(1)
538+
signalHart(0)
489539

490540
// Wait for the interrupt to happen. This should happen immediately.
491541
for {
@@ -519,6 +569,6 @@ func exitNow(code uint32) {
519569
func handleException(code uint) {
520570
// For a list of exception codes, see:
521571
// 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")
572+
print("fatal error: exception with mcause=", code, " pc=", riscv.MEPC.Get(), " mtval=", riscv.MTVAL.Get(), " hart=", uint(riscv.MHARTID.Get()), "\r\n")
523573
abort()
524574
}

0 commit comments

Comments
 (0)