diff --git a/GNUmakefile b/GNUmakefile index f9e19f9de..398b19547 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -413,6 +413,7 @@ TEST_PACKAGES_LINUX := \ context \ crypto/aes \ crypto/des \ + crypto/ecdh \ crypto/hmac \ debug/dwarf \ debug/plan9obj \ diff --git a/compiler/defer.go b/compiler/defer.go index 3a91f8767..0553d1528 100644 --- a/compiler/defer.go +++ b/compiler/defer.go @@ -116,8 +116,8 @@ func (b *builder) createLandingPad() { func (b *builder) createCheckpoint(ptr llvm.Value) llvm.Value { // Construct inline assembly equivalents of setjmp. // The assembly works as follows: - // * All registers (both callee-saved and caller saved) are clobbered - // after the inline assembly returns. + // * Registers are either clobbered or, on 386, saved for longjmp to + // restore if the ABI requires them to survive calls. // * The assembly stores the address just past the end of the assembly // into the jump buffer. // * The return value (eax, rax, r0, etc) is set to zero in the inline @@ -130,8 +130,12 @@ func (b *builder) createCheckpoint(ptr llvm.Value) llvm.Value { asmString = ` xorl %eax, %eax movl $$1f, 4(%ebx) +movl %ebx, 8(%ebx) +movl %esi, 12(%ebx) +movl %edi, 16(%ebx) +movl %ebp, 20(%ebx) 1:` - constraints = "={eax},{ebx},~{ebx},~{ecx},~{edx},~{esi},~{edi},~{ebp},~{xmm0},~{xmm1},~{xmm2},~{xmm3},~{xmm4},~{xmm5},~{xmm6},~{xmm7},~{fpsr},~{fpcr},~{flags},~{dirflag},~{memory}" + constraints = "={eax},{ebx},~{ecx},~{edx},~{xmm0},~{xmm1},~{xmm2},~{xmm3},~{xmm4},~{xmm5},~{xmm6},~{xmm7},~{fpsr},~{fpcr},~{flags},~{dirflag},~{memory}" // This doesn't include the floating point stack because TinyGo uses // newer floating point instructions. case "x86_64": diff --git a/compiler/testdata/defer-cortex-m-qemu.ll b/compiler/testdata/defer-cortex-m-qemu.ll index 852fcff7b..7182b61c6 100644 --- a/compiler/testdata/defer-cortex-m-qemu.ll +++ b/compiler/testdata/defer-cortex-m-qemu.ll @@ -3,7 +3,7 @@ source_filename = "defer.go" target datalayout = "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64" target triple = "thumbv7m-unknown-unknown-eabi" -%runtime.deferFrame = type { ptr, ptr, [0 x ptr], ptr, i8, %runtime._interface, ptr } +%runtime.deferFrame = type { ptr, ptr, [0 x ptr], ptr, i8, %runtime._interface, ptr, i1 } %runtime._interface = type { ptr, ptr } ; Function Attrs: nounwind @@ -111,9 +111,9 @@ rundefers.end3: ; preds = %rundefers.loophead6 ; Function Attrs: nocallback nofree nosync nounwind willreturn declare ptr @llvm.stacksave.p0() #2 -declare void @runtime.setupDeferFrame(ptr dereferenceable_or_null(28), ptr, ptr) #1 +declare void @runtime.setupDeferFrame(ptr dereferenceable_or_null(32), ptr, ptr) #1 -declare void @runtime.destroyDeferFrame(ptr dereferenceable_or_null(28), ptr) #1 +declare void @runtime.destroyDeferFrame(ptr dereferenceable_or_null(32), ptr) #1 ; Function Attrs: nounwind define internal void @"main.deferSimple$1"(ptr %context) unnamed_addr #0 { diff --git a/main_test.go b/main_test.go index 3dab99b80..53bade009 100644 --- a/main_test.go +++ b/main_test.go @@ -473,6 +473,12 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c options.Directory = path pkgName = "." } + isWebAssembly := strings.HasPrefix(options.Target, "wasi") || + strings.HasPrefix(options.Target, "wasm") || + strings.HasPrefix(options.GOARCH, "wasm") + if name == "testing.go" && isWebAssembly { + expectedOutputPath = TESTDATA + "/testing-wasm.txt" + } config, err := builder.NewConfig(&options) if err != nil { @@ -948,6 +954,32 @@ func checkOutputData(t *testing.T, expectedOutput, actual []byte) { } } +func TestGoexitCrash(t *testing.T) { + t.Parallel() + + options := optionsFromTarget("", sema) + config, err := builder.NewConfig(&options) + if err != nil { + t.Fatal(err) + } + + output := &bytes.Buffer{} + _, err = buildAndRun("testdata/goexit.go", config, output, nil, nil, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error { + cmd.Stdout = nil + cmd.Stderr = nil + data, err := cmd.CombinedOutput() + output.Write(data) + return err + }) + if err == nil { + t.Fatal("program unexpectedly exited successfully") + } + const want = "panic: panic after Goexit" + if !strings.Contains(output.String(), want) { + t.Fatalf("output does not contain %q:\n%s", want, output.String()) + } +} + func TestTest(t *testing.T) { t.Parallel() diff --git a/src/runtime/arch_386.go b/src/runtime/arch_386.go index 43130a8fe..5339ea42d 100644 --- a/src/runtime/arch_386.go +++ b/src/runtime/arch_386.go @@ -5,7 +5,7 @@ const GOARCH = "386" // The bitness of the CPU (e.g. 8, 32, 64). const TargetBits = 32 -const deferExtraRegs = 0 +const deferExtraRegs = 4 const callInstSize = 5 // "call someFunction" is 5 bytes diff --git a/src/runtime/asm_386.S b/src/runtime/asm_386.S index f463ffa0a..0e083a381 100644 --- a/src/runtime/asm_386.S +++ b/src/runtime/asm_386.S @@ -43,6 +43,10 @@ tinygo_longjmp: // Note: the code we jump to assumes eax is set to a non-zero value if we // jump from here. movl 4(%esp), %eax + movl 8(%eax), %ebx + movl 12(%eax), %esi + movl 16(%eax), %edi + movl 20(%eax), %ebp movl 0(%eax), %esp // jumpSP movl 4(%eax), %eax // jumpPC (stash in volatile register) jmpl *%eax diff --git a/src/runtime/panic.go b/src/runtime/panic.go index f28cb75a4..cec4f74fe 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -41,6 +41,7 @@ type deferFrame struct { Panicking panicState // not panicking, panicking, or in Goexit PanicValue interface{} // panic value, might be nil for panic(nil) for example DeferPtr unsafe.Pointer // head of the stack-allocated defer list + Goexit bool // whether Goexit is still pending after a recovered deferred panic } type panicState uint8 @@ -57,7 +58,7 @@ func _panic(message interface{}) { } func panicOrGoexit(message interface{}, panicking panicState) { - if panicStrategy() == tinygo.PanicStrategyTrap { + if panicking != panicGoexit && panicStrategy() == tinygo.PanicStrategyTrap { trap() } // Note: recover is not supported inside interrupts. @@ -65,6 +66,9 @@ func panicOrGoexit(message interface{}, panicking panicState) { if supportsRecover() && !interrupt.In() { frame := (*deferFrame)(task.Current().DeferFrame) if frame != nil { + if panicking == panicGoexit { + frame.Goexit = true + } frame.PanicValue = message frame.Panicking = panicking tinygo_longjmp(frame) @@ -138,6 +142,7 @@ func setupDeferFrame(frame *deferFrame, jumpSP unsafe.Pointer) { frame.JumpSP = jumpSP frame.Panicking = panicFalse frame.DeferPtr = nil + frame.Goexit = false currentTask.DeferFrame = unsafe.Pointer(frame) } @@ -154,6 +159,11 @@ func destroyDeferFrame(frame *deferFrame) { // Re-raise the panic now. panicOrGoexit(frame.PanicValue, frame.Panicking) } + if frame.Goexit { + // A deferred function panicked during Goexit, and that panic was + // recovered. Continue the original Goexit instead of returning. + panicOrGoexit(nil, panicGoexit) + } } // _recover is the built-in recover() function. It tries to recover a currently diff --git a/src/testing/benchmark.go b/src/testing/benchmark.go index b942d2589..2d7b68528 100644 --- a/src/testing/benchmark.go +++ b/src/testing/benchmark.go @@ -179,7 +179,12 @@ func (b *B) run1() bool { ctx.maxLen = n + 8 // Add additional slack to avoid too many jumps in size. } } - b.runN(1) + done := make(chan struct{}) + go func() { + defer close(done) + b.runN(1) + }() + <-done return !b.hasSub } @@ -195,8 +200,12 @@ func (b *B) run() { } func (b *B) doBench() BenchmarkResult { - // in upstream, this uses a goroutine - b.launch() + done := make(chan struct{}) + go func() { + defer close(done) + b.launch() + }() + <-done return b.result } diff --git a/src/testing/testing.go b/src/testing/testing.go index 5a28a87b3..25b01b0fc 100644 --- a/src/testing/testing.go +++ b/src/testing/testing.go @@ -214,9 +214,8 @@ func (c *common) Failed() bool { // current goroutine). func (c *common) FailNow() { c.Fail() - c.finished = true - c.Error("FailNow is incomplete, requires runtime.Goexit()") + runtime.Goexit() } // log generates the output. @@ -288,7 +287,7 @@ func (c *common) Skipf(format string, args ...any) { func (c *common) SkipNow() { c.skip() c.finished = true - c.Error("SkipNow is incomplete, requires runtime.Goexit()") + runtime.Goexit() } func (c *common) skip() { @@ -480,19 +479,19 @@ type InternalTest struct { } func tRunner(t *T, fn func(t *T)) { + t.start = time.Now() defer func() { + t.duration += time.Since(t.start) // TODO: capture cleanup time, too. t.runCleanup() + t.report() // Report after all subtests have finished. + if t.parent != nil && !t.hasSub { + t.setRan() + } }() // Run the test. - t.start = time.Now() fn(t) - t.duration += time.Since(t.start) // TODO: capture cleanup time, too. - - t.report() // Report after all subtests have finished. - if t.parent != nil && !t.hasSub { - t.setRan() - } + t.finished = true } // Run runs f as a subtest of t called name. It waits until the subtest is finished @@ -524,7 +523,12 @@ func (t *T) Run(name string, f func(t *T)) bool { fmt.Fprintf(t.output, "=== RUN %s\n", sub.name) } - tRunner(&sub, f) + done := make(chan struct{}) + go func() { + defer close(done) + tRunner(&sub, f) + }() + <-done return !sub.failed } diff --git a/testdata/goexit.go b/testdata/goexit.go new file mode 100644 index 000000000..15c828e9d --- /dev/null +++ b/testdata/goexit.go @@ -0,0 +1,10 @@ +package main + +import "runtime" + +func main() { + defer func() { + panic("panic after Goexit") + }() + runtime.Goexit() +} diff --git a/testdata/recover.go b/testdata/recover.go index 619bfa01a..bb90f0da6 100644 --- a/testdata/recover.go +++ b/testdata/recover.go @@ -158,6 +158,71 @@ func runtimeGoexit() { runtime.Goexit() }() wg.Wait() + + wg.Add(1) + go func() { + defer wg.Done() + runtimeGoexitRecoveredPanic() + println("unreachable after Goexit helper") + }() + wg.Wait() + + wg.Add(1) + go func() { + defer wg.Done() + runtimeGoexitNestedRecoveredPanic() + println("unreachable after nested Goexit helper") + }() + wg.Wait() + + wg.Add(1) + go func() { + defer wg.Done() + runtimeGoexitLoopDefers() + println("unreachable after loop Goexit helper") + }() + wg.Wait() +} + +func runtimeGoexitRecoveredPanic() { + defer func() { + if r := recover(); r != nil { + printitf("Goexit recovered deferred panic:", r) + } + }() + defer func() { + panic("panic during Goexit") + }() + runtime.Goexit() +} + +func runtimeGoexitNestedRecoveredPanic() { + defer func() { + defer func() { + if r := recover(); r != nil { + printitf("Goexit nested recovered deferred panic:", r) + } + }() + panic("nested panic during Goexit") + }() + runtime.Goexit() +} + +func runtimeGoexitLoopDefers() { + for i := 0; i < 2; i++ { + defer func() { + println("Goexit loop defer") + }() + } + defer func() { + if r := recover(); r != nil { + printitf("Goexit loop recovered deferred panic:", r) + } + }() + defer func() { + panic("loop panic during Goexit") + }() + runtime.Goexit() } // Test that a repanic inside a deferred function propagates correctly diff --git a/testdata/recover.txt b/testdata/recover.txt index 693ddf10a..e61cf25fe 100644 --- a/testdata/recover.txt +++ b/testdata/recover.txt @@ -33,6 +33,11 @@ indirect recover returned: indirect panic # runtime.Goexit Goexit deferred function, recover is nil: true +Goexit recovered deferred panic: panic during Goexit +Goexit nested recovered deferred panic: nested panic during Goexit +Goexit loop recovered deferred panic: loop panic during Goexit +Goexit loop defer +Goexit loop defer # repanic inner, repanicking diff --git a/testdata/testing-wasm.txt b/testdata/testing-wasm.txt new file mode 100644 index 000000000..6a8ee0591 --- /dev/null +++ b/testdata/testing-wasm.txt @@ -0,0 +1,21 @@ +--- FAIL: TestBar (0.00s) + log Bar + log g + h + i + + --- FAIL: TestBar/Bar2 (0.00s) + log Bar2 + a + b + c + failed + after failed + log Bar end +--- FAIL: TestAllLowercase (0.00s) + --- FAIL: TestAllLowercase/BETA (0.00s) + expected lowercase name, got BETA + --- FAIL: TestAllLowercase/BELTA (0.00s) + expected lowercase name, got BELTA +FAIL +exitcode: 1 diff --git a/testdata/testing.go b/testdata/testing.go index ff378fea9..2e56d8f57 100644 --- a/testdata/testing.go +++ b/testdata/testing.go @@ -6,6 +6,7 @@ import ( "errors" "flag" "io" + "runtime" "strings" "testing" ) @@ -25,11 +26,21 @@ func TestBar(t *testing.T) { t.Log("after failed") }) t.Run("Bar3", func(t *testing.T) {}) + if runtime.GOARCH != "wasm" { + t.Run("Bar4", func(t *testing.T) { + t.Fatal("fatal") + t.Log("after fatal") + }) + t.Run("Bar5", func(t *testing.T) { + t.SkipNow() + t.Error("after skip") + }) + } t.Log("log Bar end") } func TestAllLowercase(t *testing.T) { - names := []string { + names := []string{ "alpha", "BETA", "gamma", diff --git a/testdata/testing.txt b/testdata/testing.txt index 6a8ee0591..1d61154dc 100644 --- a/testdata/testing.txt +++ b/testdata/testing.txt @@ -11,6 +11,8 @@ c failed after failed + --- FAIL: TestBar/Bar4 (0.00s) + fatal log Bar end --- FAIL: TestAllLowercase (0.00s) --- FAIL: TestAllLowercase/BETA (0.00s)