Description
We already detect incompatible C declarations if both declarations are in the same file. For instance,
/*
void f();
int f();
*/
import "C"
Gives an error conflicting types for 'f'
.
When the conflicting definitions are in different files, however, we don't detect it. That can lead to very tricky runtime failures. See #67670 for an example.
Here's a small reproducer:
test1.go
:
package main
/*
int f(int x, int y, int z, int w);
*/
import "C"
import "fmt"
func main() {
var x [4]uint64
pre(&x)
C.f(5, 6, 7, 8)
post(&x)
main2()
}
//go:noinline
func pre(p *[4]uint64) {
for i := range p {
p[i] = 0xaaaaaaaaaaaaaaaa
}
}
//go:noinline
func post(p *[4]uint64) {
for i := range p {
if p[i] != 0xaaaaaaaaaaaaaaaa {
fmt.Printf("%d %x\n", i, p[i])
panic("bad")
}
}
}
test2.go
:
package main
/*
void f(int x, int y, int z, int w) { }
*/
import "C"
func main2() {
var x [4]uint64
pre(&x)
C.f(5, 6, 7, 8)
post(&x)
}
When run with go run test1.go test2.go
, we get (darwin/arm64):
0 aaaaaaaa00000005
panic: bad
goroutine 1 [running]:
main.post(0x14000080f18)
/Users/khr/gowork/test1.go:29 +0xe4
main.main()
/Users/khr/gowork/test1.go:13 +0x4c
exit status 2
Which means that a stack variable got partially overwritten by a cgo call.
I'm pretty sure what is happening is that at the callsite the compiler assumes C.f
has no return value, so does not allocate space for it. But the cgo wrapper assumes C.f
does have a return value, and generates code to move f
's return value to the stack frame. That write lands on the local variable x
.
I'm not sure how the compiler and/or cgo decide which version of f
to use. Perhaps if they just chose consistently things would work. But probably better to detect the conflicting definitions and error out.