Skip to content

Commit 14cb5e6

Browse files
committed
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/nocallback annotations 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, that means the C function won't callback to Go, if it do callback to Go, the Go process will crash. Fixes #56378
1 parent c6fd0c2 commit 14cb5e6

File tree

10 files changed

+150
-20
lines changed

10 files changed

+150
-20
lines changed

src/cmd/cgo/gcc.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"math"
2525
"os"
2626
"os/exec"
27+
"regexp"
2728
"strconv"
2829
"strings"
2930
"unicode"
@@ -49,6 +50,8 @@ var nameToC = map[string]string{
4950

5051
var incomplete = "_cgopackage.Incomplete"
5152

53+
var cgoRx = regexp.MustCompile(`^#cgo\s+(nocallback|noescape)\s+(\S+)\s*$`)
54+
5255
// cname returns the C name to use for C.s.
5356
// The expansions are listed in nameToC and also
5457
// struct_foo becomes "struct foo", and similarly for
@@ -73,18 +76,30 @@ func cname(s string) string {
7376
return s
7477
}
7578

76-
// DiscardCgoDirectives processes the import C preamble, and discards
77-
// all #cgo CFLAGS and LDFLAGS directives, so they don't make their
78-
// way into _cgo_export.h.
79-
func (f *File) DiscardCgoDirectives() {
79+
// ProcessCgoDirectives processes the import C preamble:
80+
// 1. discards all #cgo CFLAGS and LDFLAGS directives, so they don't make their
81+
// way into _cgo_export.h.
82+
// 2. parse the nocallback and noescape directives.
83+
func (f *File) ProcessCgoDirectives() {
8084
linesIn := strings.Split(f.Preamble, "\n")
8185
linesOut := make([]string, 0, len(linesIn))
86+
f.NoCallbacks = make(map[string]struct{})
87+
f.NoEscapes = make(map[string]struct{})
8288
for _, line := range linesIn {
8389
l := strings.TrimSpace(line)
8490
if len(l) < 5 || l[:4] != "#cgo" || !unicode.IsSpace(rune(l[4])) {
8591
linesOut = append(linesOut, line)
8692
} else {
8793
linesOut = append(linesOut, "")
94+
95+
match := cgoRx.FindStringSubmatch(line)
96+
if match != nil {
97+
if match[1] == "nocallback" {
98+
f.NoCallbacks[match[2]] = struct{}{}
99+
} else {
100+
f.NoEscapes[match[2]] = struct{}{}
101+
}
102+
}
88103
}
89104
}
90105
f.Preamble = strings.Join(linesOut, "\n")

src/cmd/cgo/main.go

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ type Package struct {
4848
Preamble string // collected preamble for _cgo_export.h
4949
typedefs map[string]bool // type names that appear in the types of the objects we're interested in
5050
typedefList []typedefInfo
51+
noCallbacks map[string]struct{} // C function names that with #cgo nocallback directive
52+
noEscapes map[string]struct{} // C function names that with #cgo noescape directive
5153
}
5254

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

6062
// A File collects information about a single Go input file.
6163
type File struct {
62-
AST *ast.File // parsed AST
63-
Comments []*ast.CommentGroup // comments from file
64-
Package string // Package name
65-
Preamble string // C preamble (doc comment on import "C")
66-
Ref []*Ref // all references to C.xxx in AST
67-
Calls []*Call // all calls to C.xxx in AST
68-
ExpFunc []*ExpFunc // exported functions for this file
69-
Name map[string]*Name // map from Go name to Name
70-
NamePos map[*Name]token.Pos // map from Name to position of the first reference
71-
Edit *edit.Buffer
64+
AST *ast.File // parsed AST
65+
Comments []*ast.CommentGroup // comments from file
66+
Package string // Package name
67+
Preamble string // C preamble (doc comment on import "C")
68+
Ref []*Ref // all references to C.xxx in AST
69+
Calls []*Call // all calls to C.xxx in AST
70+
ExpFunc []*ExpFunc // exported functions for this file
71+
Name map[string]*Name // map from Go name to Name
72+
NamePos map[*Name]token.Pos // map from Name to position of the first reference
73+
Edit *edit.Buffer
74+
NoCallbacks map[string]struct{} // C function names that with #cgo nocallback directive
75+
NoEscapes map[string]struct{} // C function names that with #cgo noescape directive
7276
}
7377

7478
func (f *File) offset(p token.Pos) int {
@@ -368,7 +372,7 @@ func main() {
368372
f := new(File)
369373
f.Edit = edit.NewBuffer(b)
370374
f.ParseGo(input, b)
371-
f.DiscardCgoDirectives()
375+
f.ProcessCgoDirectives()
372376
fs[i] = f
373377
}
374378

@@ -481,6 +485,22 @@ func (p *Package) Record(f *File) {
481485
}
482486
}
483487

488+
// merge nocallback & noescape
489+
if p.noCallbacks == nil {
490+
p.noCallbacks = f.NoCallbacks
491+
} else {
492+
for k, v := range f.NoCallbacks {
493+
p.noCallbacks[k] = v
494+
}
495+
}
496+
if p.noEscapes == nil {
497+
p.noEscapes = f.NoEscapes
498+
} else {
499+
for k, v := range f.NoEscapes {
500+
p.noEscapes[k] = v
501+
}
502+
}
503+
484504
if f.ExpFunc != nil {
485505
p.ExpFunc = append(p.ExpFunc, f.ExpFunc...)
486506
p.Preamble += "\n" + f.Preamble

src/cmd/cgo/out.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ func (p *Package) writeDefs() {
103103
fmt.Fprintf(fgo2, "var _Cgo_always_false bool\n")
104104
fmt.Fprintf(fgo2, "//go:linkname _Cgo_use runtime.cgoUse\n")
105105
fmt.Fprintf(fgo2, "func _Cgo_use(interface{})\n")
106+
fmt.Fprintf(fgo2, "//go:linkname _Cgo_no_callback runtime.cgoNoCallback\n")
107+
fmt.Fprintf(fgo2, "func _Cgo_no_callback(bool)\n")
106108
}
107109

108110
typedefNames := make([]string, 0, len(typedef))
@@ -610,6 +612,17 @@ func (p *Package) writeDefsFunc(fgo2 io.Writer, n *Name, callsMalloc *bool) {
610612
arg = "uintptr(unsafe.Pointer(&r1))"
611613
}
612614

615+
_, noCallback := p.noCallbacks[n.C]
616+
_, noEscape := p.noEscapes[n.C]
617+
noCgoCallback := false
618+
if noEscape && noCallback {
619+
noCgoCallback = true
620+
}
621+
if noCgoCallback {
622+
// disable cgocallback, will check it in runtime.
623+
fmt.Fprintf(fgo2, "\t_Cgo_no_callback(true)\n")
624+
}
625+
613626
prefix := ""
614627
if n.AddError {
615628
prefix = "errno := "
@@ -618,13 +631,19 @@ func (p *Package) writeDefsFunc(fgo2 io.Writer, n *Name, callsMalloc *bool) {
618631
if n.AddError {
619632
fmt.Fprintf(fgo2, "\tif errno != 0 { r2 = syscall.Errno(errno) }\n")
620633
}
621-
fmt.Fprintf(fgo2, "\tif _Cgo_always_false {\n")
622-
if d.Type.Params != nil {
623-
for i := range d.Type.Params.List {
624-
fmt.Fprintf(fgo2, "\t\t_Cgo_use(p%d)\n", i)
634+
if noCgoCallback {
635+
fmt.Fprintf(fgo2, "\t_Cgo_no_callback(false)\n")
636+
} else {
637+
// skip _Cgo_use when noescape & nocallback both exist,
638+
// so that the compiler won't force to escape them to heap.
639+
fmt.Fprintf(fgo2, "\tif _Cgo_always_false {\n")
640+
if d.Type.Params != nil {
641+
for i := range d.Type.Params.List {
642+
fmt.Fprintf(fgo2, "\t\t_Cgo_use(p%d)\n", i)
643+
}
625644
}
645+
fmt.Fprintf(fgo2, "\t}\n")
626646
}
627-
fmt.Fprintf(fgo2, "\t}\n")
628647
fmt.Fprintf(fgo2, "\treturn\n")
629648
fmt.Fprintf(fgo2, "}\n")
630649
}

src/cmd/go/internal/modindex/build.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"io"
2121
"io/fs"
2222
"path/filepath"
23+
"regexp"
2324
"sort"
2425
"strings"
2526
"unicode"
@@ -108,6 +109,8 @@ type Context struct {
108109
OpenFile func(path string) (io.ReadCloser, error)
109110
}
110111

112+
var cgoRx = regexp.MustCompile(`^#cgo\s+(?:nocallback|noescape)\s+(?:\S+)\s*$`)
113+
111114
// joinPath calls ctxt.JoinPath (if not nil) or else filepath.Join.
112115
func (ctxt *Context) joinPath(elem ...string) string {
113116
if f := ctxt.JoinPath; f != nil {
@@ -622,6 +625,10 @@ func (ctxt *Context) saveCgo(filename string, di *build.Package, text string) er
622625
continue
623626
}
624627

628+
if cgoRx.FindStringSubmatch(line) != nil {
629+
continue
630+
}
631+
625632
// Split at colon.
626633
line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":")
627634
if !ok {

src/go/build/build.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"os/exec"
2424
pathpkg "path"
2525
"path/filepath"
26+
"regexp"
2627
"runtime"
2728
"sort"
2829
"strconv"
@@ -113,6 +114,8 @@ type Context struct {
113114
OpenFile func(path string) (io.ReadCloser, error)
114115
}
115116

117+
var cgoRx = regexp.MustCompile(`^#cgo\s+(?:nocallback|noescape)\s+(?:\S+)\s*$`)
118+
116119
// joinPath calls ctxt.JoinPath (if not nil) or else filepath.Join.
117120
func (ctxt *Context) joinPath(elem ...string) string {
118121
if f := ctxt.JoinPath; f != nil {
@@ -1687,6 +1690,10 @@ func (ctxt *Context) saveCgo(filename string, di *Package, cg *ast.CommentGroup)
16871690
continue
16881691
}
16891692

1693+
if cgoRx.FindStringSubmatch(line) != nil {
1694+
continue
1695+
}
1696+
16901697
// Split at colon.
16911698
line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":")
16921699
if !ok {

src/runtime/cgo.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,8 @@ func cgoUse(any) { throw("cgoUse should not be called") }
6161
var cgoAlwaysFalse bool
6262

6363
var cgo_yield = &_cgo_yield
64+
65+
func cgoNoCallback(v bool) {
66+
g := getg()
67+
g.nocgocallback = v
68+
}

src/runtime/cgocall.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ func cgocallbackg(fn, frame unsafe.Pointer, ctxt uintptr) {
216216
exit(2)
217217
}
218218

219+
if gp.nocgocallback {
220+
throw("runtime: disable callback from C")
221+
}
222+
219223
// The call from C is on gp.m's g0 stack, so we must ensure
220224
// that we stay on that M. We have to do this before calling
221225
// exitsyscall, since it would otherwise be free to move us to

src/runtime/crash_cgo_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,18 @@ func TestNeedmDeadlock(t *testing.T) {
749749
}
750750
}
751751

752+
func TestCgoNoCallback(t *testing.T) {
753+
switch runtime.GOOS {
754+
case "plan9", "windows":
755+
t.Skipf("no signals on %s", runtime.GOOS)
756+
}
757+
got := runTestProg(t, "testprogcgo", "CgoNoCallback")
758+
want := "runtime: disable callback from C"
759+
if !strings.Contains(got, want) {
760+
t.Fatalf("did not see %q in output: %q", want, got)
761+
}
762+
}
763+
752764
func TestCgoTracebackGoroutineProfile(t *testing.T) {
753765
output := runTestProg(t, "testprogcgo", "GoroutineProfile")
754766
want := "OK\n"

src/runtime/runtime2.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ type g struct {
473473

474474
raceignore int8 // ignore race detection events
475475
tracking bool // whether we're tracking this G for sched latency statistics
476+
nocgocallback bool // whether disable callback from C
476477
trackingSeq uint8 // used to decide whether to track this G
477478
trackingStamp int64 // timestamp of when the G last started being tracked
478479
runnableTime int64 // the amount of time spent runnable, cleared when running, only used when tracking
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build !plan9 && !windows
6+
// +build !plan9,!windows
7+
8+
package main
9+
10+
// #cgo noescape/nocallback annotations for a C function means it should not callback to Go.
11+
// But it do callback to go in this test, Go should crash here.
12+
13+
/*
14+
#cgo nocallback runCShouldNotCallback
15+
#cgo noescape runCShouldNotCallback
16+
17+
extern void CallbackToGo();
18+
19+
static void runCShouldNotCallback() {
20+
CallbackToGo();
21+
}
22+
*/
23+
import "C"
24+
25+
import (
26+
"fmt"
27+
)
28+
29+
func init() {
30+
register("CgoNoCallback", CgoNoCallback)
31+
}
32+
33+
//export CallbackToGo
34+
func CallbackToGo() {
35+
}
36+
37+
func CgoNoCallback() {
38+
C.runCShouldNotCallback()
39+
fmt.Println("OK")
40+
}

0 commit comments

Comments
 (0)