diff options
| author | Ariel Otilibili <otilibil@eurecom.fr> | 2025-11-05 21:00:52 +0000 |
|---|---|---|
| committer | t hepudds <thepudds1460@gmail.com> | 2025-11-07 12:55:16 -0800 |
| commit | 91ca80f970c2a20d1ed6603281c97e7e617b87e8 (patch) | |
| tree | 2aab6af0e917cb14bc52b7eadd4301d13c551372 /src/runtime | |
| parent | d36e88f21f56dcf45fed2231fe28f948a31c936b (diff) | |
| download | go-91ca80f970c2a20d1ed6603281c97e7e617b87e8.tar.xz | |
runtime/cgo: improve error messages after pointer panic
This CL 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 is called by cgoCheckArg and
cgoCheckUnknownPointer to format panic error messages)
- update the cgo pointer tests (add new tests, and a field errTextRegexp
to the struct ptrTest)
- remove a loop variable in TestPointerChecks (similar to CL 711640).
1. cgoCheckResult
When an unpinned Go pointer (or a pointer to an unpinned Go pointer) is
returned from Go to C,
1 package main
2
3 /*
4 #include <stdio.h>
5
6 extern void* GoFoo();
7
8 static void CFoo() { GoFoo();}
9 */
10 import (
11 "C"
12 )
13
14 //export GoFoo
15 func GoFoo() map[int]int {
16 return map[int]int{0: 1,}
17 }
18
19 func main() {
20 C.CFoo();
21 }
This error shows up at runtime:
panic: runtime error: cgo result is unpinned Go pointer or points to unpinned Go pointer
goroutine 1 [running]:
main._Cfunc_CFoo()
_cgo_gotypes.go:46 +0x3a
main.main()
/mnt/tmp/parse.go:20 +0xf
exit status 2
GoFoo is the faulty Go function; it is not mentioned in the error message.
Moreover the error does not say which kind of pointer caused the panic; for
instance, a Go map.
Retrieve name and file/line of the Go function, as well as the kind of
pointer; use them in the error message:
panic: runtime error: /mnt/tmp/parse.go:15: result of Go function GoFoo called from cgo is unpinned Go map or points to unpinned Go map
goroutine 1 [running]:
main._Cfunc_CFoo()
_cgo_gotypes.go:46 +0x3a
main.main()
/mnt/tmp/parse.go:20 +0xf
exit status 2
2. cgoCheckPointer
When a pointer to an unpinned Go pointer is passed to a C function,
1 package main
2
3 /*
4 #include <stdio.h>
5 void foo(void *bar) {}
6 */
7 import "C"
8 import "unsafe"
9
10 func main() {
11 m := map[int]int{0: 1,}
12 C.foo(unsafe.Pointer(&m))
13 }
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/tmp/cgomap.go:12
main.main()
/mnt/tmp/cgomap.go:12 +0x91
exit status 2
Retrieve kind of pointer; use it in the error message.
panic: runtime error: argument of cgo function has Go pointer to unpinned Go map
goroutine 1 [running]:
main.main.func1(...)
/mnt/tmp/cgomap.go:12
main.main()
/mnt/tmp/cgomap.go:12 +0x9b
exit status 2
Link: https://pkg.go.dev/cmd/cgo#hdr-Passing_pointers
Suggested-by: Ian Lance Taylor <iant@golang.org>
Fixes #75856
Change-Id: Ia72f01df016feeae0cddb2558ced51a1b07e4486
GitHub-Last-Rev: 76257c7dd7c52a3d88234d43ec7f22bda81edcdd
GitHub-Pull-Request: golang/go#75894
Reviewed-on: https://go-review.googlesource.com/c/go/+/711801
Reviewed-by: Funda Secgin <fundasecgin73@gmail.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Keith Randall <khr@google.com>
Diffstat (limited to 'src/runtime')
| -rw-r--r-- | src/runtime/cgocall.go | 98 |
1 files changed, 85 insertions, 13 deletions
diff --git a/src/runtime/cgocall.go b/src/runtime/cgocall.go index 9b9a47b87e..a53fd6da34 100644 --- a/src/runtime/cgocall.go +++ b/src/runtime/cgocall.go @@ -591,15 +591,18 @@ func cgoCheckPointer(ptr any, arg any) { cgoCheckArg(t, ep.data, !t.IsDirectIface(), top, cgoCheckPointerFail) } -const cgoCheckPointerFail = "cgo argument has Go pointer to unpinned Go pointer" -const cgoResultFail = "cgo result is unpinned Go pointer or points to unpinned Go pointer" +type cgoErrorMsg int +const ( + cgoCheckPointerFail cgoErrorMsg = iota + cgoResultFail +) // cgoCheckArg is the real work of cgoCheckPointer and cgoCheckResult. // The argument p is either a pointer to the value (of type t), or the value // itself, depending on indir. The top parameter is whether we are at the top // level, where Go pointers are allowed. Go pointers to pinned objects are // allowed as long as they don't reference other unpinned pointers. -func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) { +func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg cgoErrorMsg) { if !t.Pointers() || p == nil { // If the type has no pointers there is nothing to do. return @@ -625,7 +628,7 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) { // These types contain internal pointers that will // always be allocated in the Go heap. It's never OK // to pass them to C. - panic(errorString(msg)) + panic(cgoFormatErr(msg, t.Kind())) case abi.Func: if indir { p = *(*unsafe.Pointer)(p) @@ -633,7 +636,7 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) { if !cgoIsGoPointer(p) { return } - panic(errorString(msg)) + panic(cgoFormatErr(msg, t.Kind())) case abi.Interface: it := *(**_type)(p) if it == nil { @@ -643,14 +646,14 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) { // constant. A type not known at compile time will be // in the heap and will not be OK. if inheap(uintptr(unsafe.Pointer(it))) { - panic(errorString(msg)) + panic(cgoFormatErr(msg, t.Kind())) } p = *(*unsafe.Pointer)(add(p, goarch.PtrSize)) if !cgoIsGoPointer(p) { return } if !top && !isPinned(p) { - panic(errorString(msg)) + panic(cgoFormatErr(msg, t.Kind())) } cgoCheckArg(it, p, !it.IsDirectIface(), false, msg) case abi.Slice: @@ -661,7 +664,7 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) { return } if !top && !isPinned(p) { - panic(errorString(msg)) + panic(cgoFormatErr(msg, t.Kind())) } if !st.Elem.Pointers() { return @@ -676,7 +679,7 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) { return } if !top && !isPinned(ss.str) { - panic(errorString(msg)) + panic(cgoFormatErr(msg, t.Kind())) } case abi.Struct: st := (*structtype)(unsafe.Pointer(t)) @@ -705,7 +708,7 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) { return } if !top && !isPinned(p) { - panic(errorString(msg)) + panic(cgoFormatErr(msg, t.Kind())) } cgoCheckUnknownPointer(p, msg) @@ -716,7 +719,7 @@ func cgoCheckArg(t *_type, p unsafe.Pointer, indir, top bool, msg string) { // memory. It checks whether that Go memory contains any other // pointer into unpinned Go memory. If it does, we panic. // The return values are unused but useful to see in panic tracebacks. -func cgoCheckUnknownPointer(p unsafe.Pointer, msg string) (base, i uintptr) { +func cgoCheckUnknownPointer(p unsafe.Pointer, msg cgoErrorMsg) (base, i uintptr) { if inheap(uintptr(p)) { b, span, _ := findObject(uintptr(p), 0, 0) base = b @@ -731,7 +734,7 @@ func cgoCheckUnknownPointer(p unsafe.Pointer, msg string) (base, i uintptr) { } pp := *(*unsafe.Pointer)(unsafe.Pointer(addr)) if cgoIsGoPointer(pp) && !isPinned(pp) { - panic(errorString(msg)) + panic(cgoFormatErr(msg, abi.Pointer)) } } return @@ -741,7 +744,7 @@ func cgoCheckUnknownPointer(p unsafe.Pointer, msg string) (base, i uintptr) { if cgoInRange(p, datap.data, datap.edata) || cgoInRange(p, datap.bss, datap.ebss) { // We have no way to know the size of the object. // We have to assume that it might contain a pointer. - panic(errorString(msg)) + panic(cgoFormatErr(msg, abi.Pointer)) } // In the text or noptr sections, we know that the // pointer does not point to a Go pointer. @@ -794,3 +797,72 @@ func cgoCheckResult(val any) { t := ep._type cgoCheckArg(t, ep.data, !t.IsDirectIface(), false, cgoResultFail) } + +// cgoFormatErr is called by cgoCheckArg and cgoCheckUnknownPointer +// to format panic error messages. +func cgoFormatErr(error cgoErrorMsg, kind abi.Kind) errorString { + var msg, kindname string + var cgoFunction string = "unknown" + var offset int + var buf [20]byte + + // We expect one of these abi.Kind from cgoCheckArg + switch kind { + case abi.Chan: + kindname = "channel" + case abi.Func: + kindname = "function" + case abi.Interface: + kindname = "interface" + case abi.Map: + kindname = "map" + case abi.Pointer: + kindname = "pointer" + case abi.Slice: + kindname = "slice" + case abi.String: + kindname = "string" + case abi.Struct: + kindname = "struct" + case abi.UnsafePointer: + kindname = "unsafe pointer" + default: + kindname = "pointer" + } + + // The cgo function name might need an offset to be obtained + if error == cgoResultFail { + offset = 21 + } + + // Relatively to cgoFormatErr, this is the stack frame: + // 0. cgoFormatErr + // 1. cgoCheckArg or cgoCheckUnknownPointer + // 2. cgoCheckPointer or cgoCheckResult + // 3. cgo function + pc, path, line, ok := Caller(3) + if ok && error == cgoResultFail { + function := FuncForPC(pc) + + if function != nil { + // Expected format of cgo function name: + // - caller: _cgoexp_3c910ddb72c4_foo + if offset > len(function.Name()) { + cgoFunction = function.Name() + } else { + cgoFunction = function.Name()[offset:] + } + } + } + + switch error { + case cgoResultFail: + msg = path + ":" + string(itoa(buf[:], uint64(line))) + msg += ": result of Go function " + cgoFunction + " called from cgo" + msg += " is unpinned Go " + kindname + " or points to unpinned Go " + kindname + case cgoCheckPointerFail: + msg += "argument of cgo function has Go pointer to unpinned Go " + kindname + } + + return errorString(msg) +} |
