Skip to content

cgo: Taking a reference to a cgo Handle, as documented, appears to return bad handles. #71566

Closed as not planned
@stroiman

Description

@stroiman

Go version

go version go1.23.6 darwin/arm64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/peter/Library/Caches/go-build'
GOENV='/Users/peter/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/peter/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/peter/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/opt/homebrew/Cellar/go/1.23.6/libexec'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/opt/homebrew/Cellar/go/1.23.6/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.23.6'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/peter/Library/Application Support/go/telemetry'
GCCGO='gccgo'
GOARM64='v8.0'
AR='ar'
CC='cc'
CXX='c++'
CGO_ENABLED='1'
GOMOD='/Users/peter/src/harmony/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/ww/5ffgmr_s2kq5pfqbgf_9tf800000gq/T/go-build1440665527=/tmp/go-build -gno-record-gcc-switches -fno-common'

What did you do?

Using v8go, I store a reference to a Go object as an "external" in an internal field, a mechanism in V8 intended to store pointers to objects in the host. The API takes a C void* as argument.

The v8go version is my own fork, using the following branch: https://github.com/stroiman/v8go/tree/go-dom-support

The cgo package documentation suggests that you can take a reference to a cgo.Handle.

It is not safe to coerce a cgo.Handle (an integer) to a Go unsafe.Pointer, but instead we can pass the address of the cgo.Handle to the void* parameter, as in this variant of the previous example:

//export MyGoPrint
func MyGoPrint(context unsafe.Pointer) {
	h := *(*cgo.Handle)(context)
	val := h.Value().(string)
	println(val)
	h.Delete()
}

func main() {
	val := "hello Go"
	h := cgo.NewHandle(val)
	C.myprint(unsafe.Pointer(&h))
	// Output: hello Go
}

So to store a reference to a Go object as a V8 external, I

func NewValueExternal(iso *Isolate, val unsafe.Pointer) *Value {
	return &Value{
		ptr: C.NewValueExternal(iso.ptr, val),
	}
}

func NewValueExternalHandle(iso *Isolate, val cgo.Handle) *Value {
	return &Value{
		ptr: C.NewValueExternal(iso.ptr, unsafe.Pointer(&val)),
	}
}

I read the handle again using:

func (v *Value) External() unsafe.Pointer {
	if !v.IsExternal() {
		return nil
	}
	return C.ValueToExternal(v.ptr)
}

func (v *Value) ExternalHandle() cgo.Handle {
	unsafePtr := v.External()
	if unsafePtr == nil {
		return 0
	}
	return *(*cgo.Handle)(unsafePtr)
}

What did you see happen?

I get erratic panics

panic: runtime/cgo: misuse of an invalid Handle

After a lot of trial and error, I logged

  • The cgo.Handle after conversion
  • The void* value retrieved from the external value

During a single test, the same value is read many times from the same object, and I get the following output

 -------- XMLHttpRequest HANDLE: 31 - 0x1400047c698
Prototype: XMLHttpRequest
By us: true - <nil>
 -------- XMLHttpRequest HANDLE: 31 - 0x1400047c698
Prototype: XMLHttpRequest
By us: true - <nil>
 -------- XMLHttpRequest HANDLE: 31 - 0x1400047c698
Prototype: XMLHttpRequest
By us: true - <nil>
 -------- XMLHttpRequest HANDLE: 31 - 0x1400047c698
Prototype: XMLHttpRequest
By us: true - <nil>
 -------- XMLHttpRequest HANDLE: 31 - 0x1400047c698
Prototype: XMLHttpRequest
By us: true - <nil>
 -------- XMLHttpRequest HANDLE: 31 - 0x1400047c698
Prototype: XMLHttpRequest
By us: true - <nil>
 -------- XMLHttpRequest HANDLE: 3985729651616 - 0x1400047c698

So I get the same void* from the v8 external, but suddenly, it translates to a different value. I assume that this is because GC has kicked in?

I can trigger an even harder crash by calling runtime.GC() before reading the handle.

What did you expect to see?

I expected that the same void*/unsafe.Pointer should consistently be converted to the same cgo.Handle when using the documented approach.

I tried to create a minimal example in the playground, create a handle, convert to unsafe ptr, GC, and convert back, but that resulted in build errors in the playground.

I will be happy to try to reproduce a more minimal example, but it will not be this week.

As a workaround, I'll try to treat the handles as uint32 values, which is also possible to store in a v8 object.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.DocumentationIssues describing a change to documentation.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions