Skip to content

Commit 1dcd6a1

Browse files
committed
runtime/cgo: improve error messages after ptr panic
This commit improves the error messages after panics due to the sharing of an unpinned Go pointer (or a pointer to an unpinned Go pointer) between Go and C. This occurs when it is: 1. returned from Go to C (through cgoCheckResult) 2. passed as argument to a C function (through cgoCheckPointer). An unpinned Go pointer refers to a memory location that might be moved or freed by the garbage collector. Therefore: - change the signature of cgoCheckArg (it does the real work behind cgoCheckResult and cgoCheckPointer) - change the signature of cgoCheckUnknownPointer (called by cgoCheckArg for checking unexpected pointers) - introduce cgoFormatErr (it formats an error message with the caller name) - update the cgo pointer tests (add new tests, and a field errTextRegexp to the struct ptrTest) - remove an unused assignment in TestPointerChecks. 1. cgoCheckResult When an unpinned Go pointer (or a pointer to an unpinned Go pointer) is returned from Go to C, > package main > > import ( > "C" > ) > > //export foo > func foo() map[int]int { > return map[int]int{0: 1,} > } This error shows up at runtime: > panic: runtime error: cgo result is unpinned Go pointer or points to unpinned Go pointer > > goroutine 17 [running, locked to thread]: > panic({0x78ac36001d40?, 0xc000194000?}) > /usr/lib/go/src/runtime/panic.go:802 +0x168 > runtime.cgoCheckArg(0x78ac36002f40, 0xc000190000, 0xd8?, 0x0, {0x78ac35fdda62, 0x42}) > /usr/lib/go/src/runtime/cgocall.go:628 +0x4c5 > runtime.cgoCheckResult({0x78ac36002f40, 0xc000190000}) > /usr/lib/go/src/runtime/cgocall.go:795 +0x4b > _cgoexp_1c29bd2c0b96_foo(0x7fffc16d87b0) > _cgo_gotypes.go:46 +0x6f > runtime.cgocallbackg1(0x78ac35fd4d20, 0x7fffc16d87b0, 0x0) > /usr/lib/go/src/runtime/cgocall.go:446 +0x289 > runtime.cgocallbackg(0x78ac35fd4d20, 0x7fffc16d87b0, 0x0) > /usr/lib/go/src/runtime/cgocall.go:350 +0x132 > runtime.cgocallbackg(0x78ac35fd4d20, 0x7fffc16d87b0, 0x0) > <autogenerated>:1 +0x2b > runtime.cgocallback(0x0, 0x0, 0x0) > /usr/lib/go/src/runtime/asm_amd64.s:1082 +0xcd > runtime.goexit({}) > /usr/lib/go/src/runtime/asm_amd64.s:1693 +0x1 _cgoexp_1c29bd2c0b96_foo is the faulty caller; it is not obvious to find it in the stack trace. Moreover the error does say which kind of pointer caused the panic; for instance, a Go map. Retrieve caller name and pointer kind; use them in the error message: > panic: runtime error: result of cgo function foo is unpinned Go map or points to unpinned Go map > > goroutine 17 [running, locked to thread]: > panic({0x76f2fd69b3c0?, 0x1fba8c79c000?}) > /mnt/go/src/runtime/panic.go:877 +0x16f > runtime.cgoCheckArg(0x76f2fd69c5c0, 0x1fba8c790000, 0x0?, 0x0, 0x1) > /mnt/go/src/runtime/cgocall.go:631 +0x499 > runtime.cgoCheckResult({0x76f2fd69c5c0, 0x1fba8c790000}) > /mnt/go/src/runtime/cgocall.go:798 +0x45 > _cgoexp_1c29bd2c0b96_foo(0x7ffd0803b8c0) > _cgo_gotypes.go:46 +0x6f > runtime.cgocallbackg1(0x76f2fd667680, 0x7ffd0803b8c0, 0x0) > /mnt/go/src/runtime/cgocall.go:446 +0x289 > runtime.cgocallbackg(0x76f2fd667680, 0x7ffd0803b8c0, 0x0) > /mnt/go/src/runtime/cgocall.go:350 +0x132 > runtime.cgocallbackg(0x76f2fd667680, 0x7ffd0803b8c0, 0x0) > <autogenerated>:1 +0x2b > runtime.cgocallback(0x0, 0x0, 0x0) > /mnt/go/src/runtime/asm_amd64.s:1101 +0xcd > runtime.goexit({}) > /mnt/go/src/runtime/asm_amd64.s:1712 +0x1 2. cgoCheckPointer When an unpinned Go pointer (or a pointer to an unpinned Go pointer) is passed to a C function, > package main > > /* > #include <stdio.h> > void foo(void *bar) {} > */ > import "C" > import "unsafe" > > func main() { > m := map[int]int{0: 1,} > C.foo(unsafe.Pointer(&m)) > } This error shows up at runtime: > panic: runtime error: cgo argument has Go pointer to unpinned Go pointer > > goroutine 1 [running]: > main.main.func1(...) > /mnt/chexit/cgomap.go:12 > main.main() > /mnt/chexit/cgomap.go:12 +0x91 > exit status 2 Retrieve callee name and pointer kind; use them in the error message. > panic: runtime error: cgo argument of function main.main.func1 has Go pointer to unpinned Go map > > goroutine 1 [running]: > main.main.func1(...) > /mnt/chexit/cgomap.go:12 > main.main() > /mnt/chexit/cgomap.go:12 +0x9b > exit status 2 Link: https://pkg.go.dev/cmd/cgo#hdr-Passing_pointers Suggested-by: Ian Lance Taylor <[email protected]> Fixes #75856
1 parent 29d43df commit 1dcd6a1

File tree

2 files changed

+123
-24
lines changed

2 files changed

+123
-24
lines changed

src/cmd/cgo/internal/testerrors/ptr_test.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"os"
1515
"os/exec"
1616
"path/filepath"
17+
"regexp"
1718
"slices"
1819
"strings"
1920
"sync/atomic"
@@ -24,15 +25,16 @@ var tmp = flag.String("tmp", "", "use `dir` for temporary files and do not clean
2425

2526
// ptrTest is the tests without the boilerplate.
2627
type ptrTest struct {
27-
name string // for reporting
28-
c string // the cgo comment
29-
c1 string // cgo comment forced into non-export cgo file
30-
imports []string // a list of imports
31-
support string // supporting functions
32-
body string // the body of the main function
33-
extra []extra // extra files
34-
fail bool // whether the test should fail
35-
expensive bool // whether the test requires the expensive check
28+
name string // for reporting
29+
c string // the cgo comment
30+
c1 string // cgo comment forced into non-export cgo file
31+
imports []string // a list of imports
32+
support string // supporting functions
33+
body string // the body of the main function
34+
extra []extra // extra files
35+
fail bool // whether the test should fail
36+
expensive bool // whether the test requires the expensive check
37+
errTextRegexp string // expected regexp of error message
3638
}
3739

3840
type extra struct {
@@ -489,6 +491,27 @@ var ptrTests = []ptrTest{
489491
body: `i := 0; a := &[2]unsafe.Pointer{nil, unsafe.Pointer(&i)}; C.f45(&a[0])`,
490492
fail: true,
491493
},
494+
{
495+
// Passing a Go map as argument to C.
496+
name: "argmap",
497+
c: `void f46(void* p) {}`,
498+
imports: []string{"unsafe"},
499+
body: `m := map[int]int{0: 1,}; C.f46(unsafe.Pointer(&m))`,
500+
fail: true,
501+
errTextRegexp: `.*has Go pointer to unpinned Go map.*`,
502+
},
503+
{
504+
// Returning a Go map to C.
505+
name: "retmap",
506+
c: `extern void f47();`,
507+
support: `//export GoMap47
508+
func GoMap47() map[int]int { return map[int]int{0: 1,} }`,
509+
body: `C.f47()`,
510+
c1: `extern void* GoMap47();
511+
void f47() { GoMap47(); }`,
512+
fail: true,
513+
errTextRegexp: `.*is unpinned Go map or points to unpinned Go map.*`,
514+
},
492515
}
493516

494517
func TestPointerChecks(t *testing.T) {
@@ -519,7 +542,6 @@ func TestPointerChecks(t *testing.T) {
519542
// after testOne finishes.
520543
var pending int32
521544
for _, pt := range ptrTests {
522-
pt := pt
523545
t.Run(pt.name, func(t *testing.T) {
524546
atomic.AddInt32(&pending, +1)
525547
defer func() {
@@ -690,11 +712,18 @@ func testOne(t *testing.T, pt ptrTest, exe, exe2 string) {
690712
}
691713

692714
buf, err := runcmd(cgocheck)
715+
716+
// TODO: add a regexp on each ptrTest
717+
var pattern string = pt.errTextRegexp
718+
if pt.errTextRegexp == "" {
719+
pattern = `.*unpinned Go.*`
720+
}
721+
693722
if pt.fail {
694723
if err == nil {
695724
t.Logf("%s", buf)
696725
t.Fatalf("did not fail as expected")
697-
} else if !bytes.Contains(buf, []byte("Go pointer")) {
726+
} else if ok, _ := regexp.Match(pattern, buf); !ok {
698727
t.Logf("%s", buf)
699728
t.Fatalf("did not print expected error (failed with %v)", err)
700729
}

src/runtime/cgocall.go

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -591,15 +591,18 @@ func cgoCheckPointer(ptr any, arg any) {
591591
cgoCheckArg(t, ep.data, !t.IsDirectIface(), top, cgoCheckPointerFail)
592592
}
593593

594-
const cgoCheckPointerFail = "cgo argument has Go pointer to unpinned Go pointer"
595-
const cgoResultFail = "cgo result is unpinned Go pointer or points to unpinned Go pointer"
594+
type cgoErrorMsg int
595+
const (
596+
cgoCheckPointerFail cgoErrorMsg = iota
597+
cgoResultFail
598+
)
596599

597600
// cgoCheckArg is the real work of cgoCheckPointer. The argument p
598601
// is either a pointer to the value (of type t), or the value itself,
599602
// depending on indir. The top parameter is whether we are at the top
600603
// level, where Go pointers are allowed. Go pointers to pinned objects are
601604
// allowed as long as they don't reference other unpinned pointers.
602-
func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) {
605+
func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg cgoErrorMsg) {
603606
if !t.Pointers() || p == nil {
604607
// If the type has no pointers there is nothing to do.
605608
return
@@ -625,15 +628,15 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) {
625628
// These types contain internal pointers that will
626629
// always be allocated in the Go heap. It's never OK
627630
// to pass them to C.
628-
panic(errorString(msg))
631+
panic(cgoFormatErr(msg, t.Kind()))
629632
case abi.Func:
630633
if indir {
631634
p = *(*unsafe.Pointer)(p)
632635
}
633636
if !cgoIsGoPointer(p) {
634637
return
635638
}
636-
panic(errorString(msg))
639+
panic(cgoFormatErr(msg, t.Kind()))
637640
case abi.Interface:
638641
it := *(**_type)(p)
639642
if it == nil {
@@ -643,14 +646,14 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) {
643646
// constant. A type not known at compile time will be
644647
// in the heap and will not be OK.
645648
if inheap(uintptr(unsafe.Pointer(it))) {
646-
panic(errorString(msg))
649+
panic(cgoFormatErr(msg, t.Kind()))
647650
}
648651
p = *(*unsafe.Pointer)(add(p, goarch.PtrSize))
649652
if !cgoIsGoPointer(p) {
650653
return
651654
}
652655
if !top && !isPinned(p) {
653-
panic(errorString(msg))
656+
panic(cgoFormatErr(msg, t.Kind()))
654657
}
655658
cgoCheckArg(it, p, !it.IsDirectIface(), false, msg)
656659
case abi.Slice:
@@ -661,7 +664,7 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) {
661664
return
662665
}
663666
if !top && !isPinned(p) {
664-
panic(errorString(msg))
667+
panic(cgoFormatErr(msg, t.Kind()))
665668
}
666669
if !st.Elem.Pointers() {
667670
return
@@ -676,7 +679,7 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) {
676679
return
677680
}
678681
if !top && !isPinned(ss.str) {
679-
panic(errorString(msg))
682+
panic(cgoFormatErr(msg, t.Kind()))
680683
}
681684
case abi.Struct:
682685
st := (*structtype)(unsafe.Pointer(t))
@@ -705,7 +708,7 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) {
705708
return
706709
}
707710
if !top && !isPinned(p) {
708-
panic(errorString(msg))
711+
panic(cgoFormatErr(msg, t.Kind()))
709712
}
710713

711714
cgoCheckUnknownPointer(p, msg)
@@ -716,7 +719,7 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) {
716719
// memory. It checks whether that Go memory contains any other
717720
// pointer into unpinned Go memory. If it does, we panic.
718721
// The return values are unused but useful to see in panic tracebacks.
719-
func cgoCheckUnknownPointer(p unsafe.Pointer, msg string) (base, i uintptr) {
722+
func cgoCheckUnknownPointer(p unsafe.Pointer, msg cgoErrorMsg) (base, i uintptr) {
720723
if inheap(uintptr(p)) {
721724
b, span, _ := findObject(uintptr(p), 0, 0)
722725
base = b
@@ -731,7 +734,7 @@ func cgoCheckUnknownPointer(p unsafe.Pointer, msg string) (base, i uintptr) {
731734
}
732735
pp := *(*unsafe.Pointer)(unsafe.Pointer(addr))
733736
if cgoIsGoPointer(pp) && !isPinned(pp) {
734-
panic(errorString(msg))
737+
panic(cgoFormatErr(msg, abi.Pointer))
735738
}
736739
}
737740
return
@@ -741,7 +744,7 @@ func cgoCheckUnknownPointer(p unsafe.Pointer, msg string) (base, i uintptr) {
741744
if cgoInRange(p, datap.data, datap.edata) || cgoInRange(p, datap.bss, datap.ebss) {
742745
// We have no way to know the size of the object.
743746
// We have to assume that it might contain a pointer.
744-
panic(errorString(msg))
747+
panic(cgoFormatErr(msg, abi.Pointer))
745748
}
746749
// In the text or noptr sections, we know that the
747750
// pointer does not point to a Go pointer.
@@ -794,3 +797,70 @@ func cgoCheckResult(val any) {
794797
t := ep._type
795798
cgoCheckArg(t, ep.data, !t.IsDirectIface(), false, cgoResultFail)
796799
}
800+
801+
// cgoFormatErr is called to format an error message with the caller name.
802+
func cgoFormatErr(error cgoErrorMsg, kind abi.Kind) errorString {
803+
var msg, kindname string
804+
var cgoFunction string = "unknown"
805+
var offset int
806+
807+
// We expect one of these abi.Kind from cgoCheckArg
808+
switch kind {
809+
case abi.Chan:
810+
kindname = "channel"
811+
case abi.Func:
812+
kindname = "function"
813+
case abi.Interface:
814+
kindname = "interface"
815+
case abi.Map:
816+
kindname = "map"
817+
case abi.Pointer:
818+
kindname = "pointer"
819+
case abi.Slice:
820+
kindname = "slice"
821+
case abi.String:
822+
kindname = "string"
823+
case abi.Struct:
824+
kindname = "struct"
825+
case abi.UnsafePointer:
826+
kindname = "unsafe pointer"
827+
default:
828+
kindname = "pointer"
829+
}
830+
831+
// The cgo function name might need an offset to be obtained
832+
if error == cgoResultFail {
833+
offset = 21
834+
}
835+
836+
// Relatively to cgoFormatErr, this is the stack frame:
837+
// 0. cgoFormatErr
838+
// 1. cgoCheckArg or cgoCheckUnknownPointer
839+
// 2. cgoCheckPointer or cgoCheckResult
840+
// 3. cgo function
841+
pc, _, _, ok := Caller(3)
842+
if ok {
843+
function := FuncForPC(pc)
844+
845+
if function != nil {
846+
// Expected format of cgo function name:
847+
// - caller: _cgoexp_3c910ddb72c4_foo
848+
// - callee: Module.Function.funcX
849+
if offset > len(function.Name()) {
850+
cgoFunction = function.Name()
851+
} else {
852+
cgoFunction = function.Name()[offset:]
853+
}
854+
}
855+
}
856+
857+
if error == cgoResultFail {
858+
msg = "result of cgo function " + cgoFunction
859+
msg += " is unpinned Go " + kindname + " or points to unpinned Go " + kindname
860+
} else if error == cgoCheckPointerFail {
861+
msg = "cgo argument of function " + cgoFunction
862+
msg += " has Go pointer to unpinned Go " + kindname
863+
}
864+
865+
return errorString(msg)
866+
}

0 commit comments

Comments
 (0)