Skip to content

Commit

Permalink
cmd/cgo: add #cgo noescape/nocallback annotations
Browse files Browse the repository at this point in the history
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: f1a17b0
GitHub-Pull-Request: #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>
  • Loading branch information
doujiang24 authored and gopherbot committed Aug 25, 2023
1 parent 1a01cb2 commit 24b9ef1
Show file tree
Hide file tree
Showing 15 changed files with 311 additions and 32 deletions.
24 changes: 24 additions & 0 deletions src/cmd/cgo/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,30 @@ passing uninitialized C memory to Go code if the Go code is going to
store pointer values in it. Zero out the memory in C before passing it
to Go.
# Optimizing calls of C code
When passing a Go pointer to a C function the compiler normally ensures
that the Go object lives on the heap. If the C function does not keep
a copy of the Go pointer, and never passes the Go pointer back to Go code,
then this is unnecessary. The #cgo noescape directive may be used to tell
the compiler that no Go pointers escape via the named C function.
If the noescape directive is used and the C function does not handle the
pointer safely, the program may crash or see memory corruption.
For example:
// #cgo noescape cFunctionName
When a Go function calls a C function, it prepares for the C function to
call back to a Go function. the #cgo nocallback directive may be used to
tell the compiler that these preparations are not necessary.
If the nocallback directive is used and the C function does call back into
Go code, the program will panic.
For example:
// #cgo nocallback cFunctionName
# Special cases
A few special C types which would normally be represented by a pointer
Expand Down
22 changes: 18 additions & 4 deletions src/cmd/cgo/gcc.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,32 @@ func cname(s string) string {
return s
}

// DiscardCgoDirectives processes the import C preamble, and discards
// all #cgo CFLAGS and LDFLAGS directives, so they don't make their
// way into _cgo_export.h.
func (f *File) DiscardCgoDirectives() {
// ProcessCgoDirectives processes the import C preamble:
// 1. discards all #cgo CFLAGS, LDFLAGS, nocallback and noescape directives,
// so they don't make their way into _cgo_export.h.
// 2. parse the nocallback and noescape directives.
func (f *File) ProcessCgoDirectives() {
linesIn := strings.Split(f.Preamble, "\n")
linesOut := make([]string, 0, len(linesIn))
f.NoCallbacks = make(map[string]bool)
f.NoEscapes = make(map[string]bool)
for _, line := range linesIn {
l := strings.TrimSpace(line)
if len(l) < 5 || l[:4] != "#cgo" || !unicode.IsSpace(rune(l[4])) {
linesOut = append(linesOut, line)
} else {
linesOut = append(linesOut, "")

// #cgo (nocallback|noescape) <function name>
if fields := strings.Fields(l); len(fields) == 3 {
directive := fields[1]
funcName := fields[2]
if directive == "nocallback" {
f.NoCallbacks[funcName] = true
} else if directive == "noescape" {
f.NoEscapes[funcName] = true
}
}
}
}
f.Preamble = strings.Join(linesOut, "\n")
Expand Down
28 changes: 20 additions & 8 deletions src/cmd/cgo/internal/testerrors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,23 @@ func check(t *testing.T, file string) {
continue
}

_, frag, ok := bytes.Cut(line, []byte("ERROR HERE: "))
if !ok {
continue
if _, frag, ok := bytes.Cut(line, []byte("ERROR HERE: ")); ok {
re, err := regexp.Compile(fmt.Sprintf(":%d:.*%s", i+1, frag))
if err != nil {
t.Errorf("Invalid regexp after `ERROR HERE: `: %#q", frag)
continue
}
errors = append(errors, re)
}
re, err := regexp.Compile(fmt.Sprintf(":%d:.*%s", i+1, frag))
if err != nil {
t.Errorf("Invalid regexp after `ERROR HERE: `: %#q", frag)
continue

if _, frag, ok := bytes.Cut(line, []byte("ERROR MESSAGE: ")); ok {
re, err := regexp.Compile(string(frag))
if err != nil {
t.Errorf("Invalid regexp after `ERROR MESSAGE: `: %#q", frag)
continue
}
errors = append(errors, re)
}
errors = append(errors, re)
}
if len(errors) == 0 {
t.Fatalf("cannot find ERROR HERE")
Expand Down Expand Up @@ -165,3 +172,8 @@ func TestMallocCrashesOnNil(t *testing.T) {
t.Fatalf("succeeded unexpectedly")
}
}

func TestNotMatchedCFunction(t *testing.T) {
file := "notmatchedcfunction.go"
check(t, file)
}
14 changes: 14 additions & 0 deletions src/cmd/cgo/internal/testerrors/testdata/notmatchedcfunction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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

/*
// ERROR MESSAGE: #cgo noescape noMatchedCFunction: no matched C function
#cgo noescape noMatchedCFunction
*/
import "C"

func main() {
}
63 changes: 48 additions & 15 deletions src/cmd/cgo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type Package struct {
Preamble string // collected preamble for _cgo_export.h
typedefs map[string]bool // type names that appear in the types of the objects we're interested in
typedefList []typedefInfo
noCallbacks map[string]bool // C function names with #cgo nocallback directive
noEscapes map[string]bool // C function names with #cgo noescape directive
}

// A typedefInfo is an element on Package.typedefList: a typedef name
Expand All @@ -59,16 +61,18 @@ type typedefInfo struct {

// A File collects information about a single Go input file.
type File struct {
AST *ast.File // parsed AST
Comments []*ast.CommentGroup // comments from file
Package string // Package name
Preamble string // C preamble (doc comment on import "C")
Ref []*Ref // all references to C.xxx in AST
Calls []*Call // all calls to C.xxx in AST
ExpFunc []*ExpFunc // exported functions for this file
Name map[string]*Name // map from Go name to Name
NamePos map[*Name]token.Pos // map from Name to position of the first reference
Edit *edit.Buffer
AST *ast.File // parsed AST
Comments []*ast.CommentGroup // comments from file
Package string // Package name
Preamble string // C preamble (doc comment on import "C")
Ref []*Ref // all references to C.xxx in AST
Calls []*Call // all calls to C.xxx in AST
ExpFunc []*ExpFunc // exported functions for this file
Name map[string]*Name // map from Go name to Name
NamePos map[*Name]token.Pos // map from Name to position of the first reference
NoCallbacks map[string]bool // C function names that with #cgo nocallback directive
NoEscapes map[string]bool // C function names that with #cgo noescape directive
Edit *edit.Buffer
}

func (f *File) offset(p token.Pos) int {
Expand Down Expand Up @@ -374,7 +378,7 @@ func main() {
f := new(File)
f.Edit = edit.NewBuffer(b)
f.ParseGo(input, b)
f.DiscardCgoDirectives()
f.ProcessCgoDirectives()
fs[i] = f
}

Expand Down Expand Up @@ -413,6 +417,25 @@ func main() {
p.writeOutput(f, input)
}
}
cFunctions := make(map[string]bool)
for _, key := range nameKeys(p.Name) {
n := p.Name[key]
if n.FuncType != nil {
cFunctions[n.C] = true
}
}

for funcName := range p.noEscapes {
if _, found := cFunctions[funcName]; !found {
error_(token.NoPos, "#cgo noescape %s: no matched C function", funcName)
}
}

for funcName := range p.noCallbacks {
if _, found := cFunctions[funcName]; !found {
error_(token.NoPos, "#cgo nocallback %s: no matched C function", funcName)
}
}

if !*godefs {
p.writeDefs()
Expand Down Expand Up @@ -450,10 +473,12 @@ func newPackage(args []string) *Package {
os.Setenv("LC_ALL", "C")

p := &Package{
PtrSize: ptrSize,
IntSize: intSize,
CgoFlags: make(map[string][]string),
Written: make(map[string]bool),
PtrSize: ptrSize,
IntSize: intSize,
CgoFlags: make(map[string][]string),
Written: make(map[string]bool),
noCallbacks: make(map[string]bool),
noEscapes: make(map[string]bool),
}
p.addToFlag("CFLAGS", args)
return p
Expand Down Expand Up @@ -487,6 +512,14 @@ func (p *Package) Record(f *File) {
}
}

// merge nocallback & noescape
for k, v := range f.NoCallbacks {
p.noCallbacks[k] = v
}
for k, v := range f.NoEscapes {
p.noEscapes[k] = v
}

if f.ExpFunc != nil {
p.ExpFunc = append(p.ExpFunc, f.ExpFunc...)
p.Preamble += "\n" + f.Preamble
Expand Down
28 changes: 23 additions & 5 deletions src/cmd/cgo/out.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func (p *Package) writeDefs() {
fmt.Fprintf(fgo2, "//go:linkname _Cgo_use runtime.cgoUse\n")
fmt.Fprintf(fgo2, "func _Cgo_use(interface{})\n")
}
fmt.Fprintf(fgo2, "//go:linkname _Cgo_no_callback runtime.cgoNoCallback\n")
fmt.Fprintf(fgo2, "func _Cgo_no_callback(bool)\n")

typedefNames := make([]string, 0, len(typedef))
for name := range typedef {
Expand Down Expand Up @@ -612,6 +614,12 @@ func (p *Package) writeDefsFunc(fgo2 io.Writer, n *Name, callsMalloc *bool) {
arg = "uintptr(unsafe.Pointer(&r1))"
}

noCallback := p.noCallbacks[n.C]
if noCallback {
// disable cgocallback, will check it in runtime.
fmt.Fprintf(fgo2, "\t_Cgo_no_callback(true)\n")
}

prefix := ""
if n.AddError {
prefix = "errno := "
Expand All @@ -620,13 +628,21 @@ func (p *Package) writeDefsFunc(fgo2 io.Writer, n *Name, callsMalloc *bool) {
if n.AddError {
fmt.Fprintf(fgo2, "\tif errno != 0 { r2 = syscall.Errno(errno) }\n")
}
fmt.Fprintf(fgo2, "\tif _Cgo_always_false {\n")
if d.Type.Params != nil {
for i := range d.Type.Params.List {
fmt.Fprintf(fgo2, "\t\t_Cgo_use(p%d)\n", i)
if noCallback {
fmt.Fprintf(fgo2, "\t_Cgo_no_callback(false)\n")
}

// skip _Cgo_use when noescape exist,
// so that the compiler won't force to escape them to heap.
if !p.noEscapes[n.C] {
fmt.Fprintf(fgo2, "\tif _Cgo_always_false {\n")
if d.Type.Params != nil {
for i := range d.Type.Params.List {
fmt.Fprintf(fgo2, "\t\t_Cgo_use(p%d)\n", i)
}
}
fmt.Fprintf(fgo2, "\t}\n")
}
fmt.Fprintf(fgo2, "\t}\n")
fmt.Fprintf(fgo2, "\treturn\n")
fmt.Fprintf(fgo2, "}\n")
}
Expand Down Expand Up @@ -1612,9 +1628,11 @@ const goProlog = `
func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32
//go:linkname _cgoCheckPointer runtime.cgoCheckPointer
//go:noescape
func _cgoCheckPointer(interface{}, interface{})
//go:linkname _cgoCheckResult runtime.cgoCheckResult
//go:noescape
func _cgoCheckResult(interface{})
`

Expand Down
5 changes: 5 additions & 0 deletions src/cmd/go/internal/modindex/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,11 @@ func (ctxt *Context) saveCgo(filename string, di *build.Package, text string) er
continue
}

// #cgo (nocallback|noescape) <function name>
if fields := strings.Fields(line); len(fields) == 3 && (fields[1] == "nocallback" || fields[1] == "noescape") {
continue
}

// Split at colon.
line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":")
if !ok {
Expand Down
5 changes: 5 additions & 0 deletions src/go/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -1687,6 +1687,11 @@ func (ctxt *Context) saveCgo(filename string, di *Package, cg *ast.CommentGroup)
continue
}

// #cgo (nocallback|noescape) <function name>
if fields := strings.Fields(line); len(fields) == 3 && (fields[1] == "nocallback" || fields[1] == "noescape") {
continue
}

// Split at colon.
line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":")
if !ok {
Expand Down
8 changes: 8 additions & 0 deletions src/runtime/cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions src/runtime/cgocall.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions src/runtime/crash_cgo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/runtime/runtime2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/runtime/testdata/testprogcgo/cgonocallback.c
Original file line number Diff line number Diff line change
@@ -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();
}

0 comments on commit 24b9ef1

Please sign in to comment.