diff options
| author | doujiang24 <doujiang24@gmail.com> | 2023-08-25 17:06:31 +0000 |
|---|---|---|
| committer | Gopher Robot <gobot@golang.org> | 2023-08-25 17:39:23 +0000 |
| commit | 24b9ef1a7366fe751880ab2098cff630144b8ac8 (patch) | |
| tree | 0f1de5cac722c84485bdfd9c1cc80f6422d7a037 /src/runtime | |
| parent | 1a01cb22f9ab07d55ee61c95a34e1e18e49596c0 (diff) | |
| download | go-24b9ef1a7366fe751880ab2098cff630144b8ac8.tar.xz | |
cmd/cgo: add #cgo noescape/nocallback annotations
When passing pointers of Go objects from Go to C, the cgo command generate _Cgo_use(pN) for the unsafe.Pointer type arguments, so that the Go compiler will escape these object to heap.
Since the C function may callback to Go, then the Go stack might grow/shrink, that means the pointers that the C function have will be invalid.
After adding the #cgo noescape annotation for a C function, the cgo command won't generate _Cgo_use(pN), and the Go compiler won't force the object escape to heap.
After adding the #cgo nocallback annotation for a C function, which means the C function won't callback to Go, if it do callback to Go, the Go process will crash.
Fixes #56378
Change-Id: Ifdca070584e0d349c7b12276270e50089e481f7a
GitHub-Last-Rev: f1a17b08b0590eca2670e404bbfedad5461df72f
GitHub-Pull-Request: golang/go#60399
Reviewed-on: https://go-review.googlesource.com/c/go/+/497837
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
Auto-Submit: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Diffstat (limited to 'src/runtime')
| -rw-r--r-- | src/runtime/cgo.go | 8 | ||||
| -rw-r--r-- | src/runtime/cgocall.go | 4 | ||||
| -rw-r--r-- | src/runtime/crash_cgo_test.go | 16 | ||||
| -rw-r--r-- | src/runtime/runtime2.go | 1 | ||||
| -rw-r--r-- | src/runtime/testdata/testprogcgo/cgonocallback.c | 9 | ||||
| -rw-r--r-- | src/runtime/testdata/testprogcgo/cgonocallback.go | 32 | ||||
| -rw-r--r-- | src/runtime/testdata/testprogcgo/cgonoescape.go | 84 |
7 files changed, 154 insertions, 0 deletions
diff --git a/src/runtime/cgo.go b/src/runtime/cgo.go index 395303552c..40c8c748d3 100644 --- a/src/runtime/cgo.go +++ b/src/runtime/cgo.go @@ -61,3 +61,11 @@ func cgoUse(any) { throw("cgoUse should not be called") } var cgoAlwaysFalse bool var cgo_yield = &_cgo_yield + +func cgoNoCallback(v bool) { + g := getg() + if g.nocgocallback && v { + panic("runtime: unexpected setting cgoNoCallback") + } + g.nocgocallback = v +} diff --git a/src/runtime/cgocall.go b/src/runtime/cgocall.go index f6e2f63813..802d6f2084 100644 --- a/src/runtime/cgocall.go +++ b/src/runtime/cgocall.go @@ -242,6 +242,10 @@ func cgocallbackg(fn, frame unsafe.Pointer, ctxt uintptr) { osPreemptExtExit(gp.m) + if gp.nocgocallback { + panic("runtime: function marked with #cgo nocallback called back into Go") + } + cgocallbackg1(fn, frame, ctxt) // will call unlockOSThread // At this point unlockOSThread has been called. diff --git a/src/runtime/crash_cgo_test.go b/src/runtime/crash_cgo_test.go index e1851808f3..88044caacf 100644 --- a/src/runtime/crash_cgo_test.go +++ b/src/runtime/crash_cgo_test.go @@ -753,6 +753,22 @@ func TestNeedmDeadlock(t *testing.T) { } } +func TestCgoNoCallback(t *testing.T) { + got := runTestProg(t, "testprogcgo", "CgoNoCallback") + want := "function marked with #cgo nocallback called back into Go" + if !strings.Contains(got, want) { + t.Fatalf("did not see %q in output:\n%s", want, got) + } +} + +func TestCgoNoEscape(t *testing.T) { + got := runTestProg(t, "testprogcgo", "CgoNoEscape") + want := "OK\n" + if got != want { + t.Fatalf("want %s, got %s\n", want, got) + } +} + func TestCgoTracebackGoroutineProfile(t *testing.T) { output := runTestProg(t, "testprogcgo", "GoroutineProfile") want := "OK\n" diff --git a/src/runtime/runtime2.go b/src/runtime/runtime2.go index c3a3679302..8809b5d569 100644 --- a/src/runtime/runtime2.go +++ b/src/runtime/runtime2.go @@ -481,6 +481,7 @@ type g struct { parkingOnChan atomic.Bool raceignore int8 // ignore race detection events + nocgocallback bool // whether disable callback from C tracking bool // whether we're tracking this G for sched latency statistics trackingSeq uint8 // used to decide whether to track this G trackingStamp int64 // timestamp of when the G last started being tracked diff --git a/src/runtime/testdata/testprogcgo/cgonocallback.c b/src/runtime/testdata/testprogcgo/cgonocallback.c new file mode 100644 index 0000000000..465a484361 --- /dev/null +++ b/src/runtime/testdata/testprogcgo/cgonocallback.c @@ -0,0 +1,9 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "_cgo_export.h" + +void runCShouldNotCallback() { + CallbackToGo(); +} diff --git a/src/runtime/testdata/testprogcgo/cgonocallback.go b/src/runtime/testdata/testprogcgo/cgonocallback.go new file mode 100644 index 0000000000..8cbbfd1957 --- /dev/null +++ b/src/runtime/testdata/testprogcgo/cgonocallback.go @@ -0,0 +1,32 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// #cgo nocallback annotations for a C function means it should not callback to Go. +// But it do callback to go in this test, Go should crash here. + +/* +#cgo nocallback runCShouldNotCallback + +extern void runCShouldNotCallback(); +*/ +import "C" + +import ( + "fmt" +) + +func init() { + register("CgoNoCallback", CgoNoCallback) +} + +//export CallbackToGo +func CallbackToGo() { +} + +func CgoNoCallback() { + C.runCShouldNotCallback() + fmt.Println("OK") +} diff --git a/src/runtime/testdata/testprogcgo/cgonoescape.go b/src/runtime/testdata/testprogcgo/cgonoescape.go new file mode 100644 index 0000000000..056be44889 --- /dev/null +++ b/src/runtime/testdata/testprogcgo/cgonoescape.go @@ -0,0 +1,84 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// #cgo noescape annotations for a C function means its arguments won't escape to heap. + +// We assume that there won't be 100 new allocated heap objects in other places, +// i.e. runtime.ReadMemStats or other runtime background works. +// So, the tests are: +// 1. at least 100 new allocated heap objects after invoking withoutNoEscape 100 times. +// 2. less than 100 new allocated heap objects after invoking withoutNoEscape 100 times. + +/* +#cgo noescape runCWithNoEscape + +void runCWithNoEscape(void *p) { +} +void runCWithoutNoEscape(void *p) { +} +*/ +import "C" + +import ( + "fmt" + "runtime" + "runtime/debug" + "unsafe" +) + +const num = 100 + +func init() { + register("CgoNoEscape", CgoNoEscape) +} + +//go:noinline +func withNoEscape() { + var str string + C.runCWithNoEscape(unsafe.Pointer(&str)) +} + +//go:noinline +func withoutNoEscape() { + var str string + C.runCWithoutNoEscape(unsafe.Pointer(&str)) +} + +func CgoNoEscape() { + // make GC stop to see the heap objects allocated + debug.SetGCPercent(-1) + + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + preHeapObjects := stats.HeapObjects + + for i := 0; i < num; i++ { + withNoEscape() + } + + runtime.ReadMemStats(&stats) + nowHeapObjects := stats.HeapObjects + + if nowHeapObjects-preHeapObjects >= num { + fmt.Printf("too many heap objects allocated, pre: %v, now: %v\n", preHeapObjects, nowHeapObjects) + } + + runtime.ReadMemStats(&stats) + preHeapObjects = stats.HeapObjects + + for i := 0; i < num; i++ { + withoutNoEscape() + } + + runtime.ReadMemStats(&stats) + nowHeapObjects = stats.HeapObjects + + if nowHeapObjects-preHeapObjects < num { + fmt.Printf("too few heap objects allocated, pre: %v, now: %v\n", preHeapObjects, nowHeapObjects) + } + + fmt.Println("OK") +} |
