Skip to content

[BUG] RequestDevice cause cgo panic when DeviceLostCallback is provided #7

Description

@dhannyell

The RequestDevice(nil) path (and any descriptor without a DeviceLostCallback) works normally. Bug found in version v1.34.1 (not tested in other versions). It makes device-loss observation via callback impossible.

Environment

  • Package: github.com/oliverbestmann/webgpu v1.34.1
  • Go Version: go version go1.26.4 windows/amd64 (CGO_ENABLED=1, CC=gcc)
  • GPU/OS: NVIDIA GeForce RTX 4080 SUPER, Vulkan backend, driver 610.62

Root cause

The DeviceDescriptor is allocated in Go memory and then passed by pointer to C, but the callback handle is a Go pointer embedded inside it — violating the cgo pointer rule ("Go memory passed to C must not contain Go pointers").

Click to see the code analysis (wgpu/adapter.go)
// line 132 — desc is GO memory (composite literal allocated in Go)
desc = &C.WGPUDeviceDescriptor{}
// lines 204-210 — when DeviceLostCallback != nil:
handle := newHandle(descriptor.DeviceLostCallback)
desc.deviceLostCallbackInfo = C.WGPUDeviceLostCallbackInfo{
    callback:  C.WGPUDeviceLostCallback(C.gowebgpu_device_lost_callback_c),
    userdata1: handle.ToPointer(),   // = &pointers[0]  (a GO pointer)
}
// line 238 — passes desc (a Go pointer that now CONTAINS a Go pointer) to C:
C.wgpuAdapterRequestDevice(g.ref, desc, ...)
//                                      ^^ PANIC

Why RequestAdapter does not panic with the same pattern

RequestAdapter (instance.go:215) embeds the callback handle in C.WGPURequestAdapterCallbackInfo{...} but passes that struct by value. The cgo pointer checker only follows Go pointers in arguments that are themselves pointers; by-value composite arguments are not scanned recursively, so the embedded Go pointer slips through.

RequestDevice passes desc (a pointer) → the checker follows it, scans the Go memory, finds the nested Go pointer → panic.

Fix

C-allocate the descriptor so the cgo pointer checker does not scan it as Go memory. calloc zero-initializes, matching the previous composite literal, and the existing C.calloc/C.free pattern is already used for requiredFeatures, requiredLimits and deviceExtras in the same function.

 	if descriptor != nil {
-		desc = &C.WGPUDeviceDescriptor{}
+		desc = (*C.WGPUDeviceDescriptor)(C.calloc(1, C.size_t(unsafe.Sizeof(C.WGPUDeviceDescriptor{}))))
+		defer C.free(unsafe.Pointer(desc))

C.wgpuAdapterRequestDevice is synchronous (it blocks until the request callback completes), so freeing desc at function exit is safe. The defer is registered inside the if descriptor != nil block, so the nil path is unchanged.

Validation

Minimal reproduction code (no GLFW, no window, no surface)
device, err := adapter.RequestDevice(&wgpu.DeviceDescriptor{
	Label: "repro device",
	DeviceLostCallback: func(reason wgpu.DeviceLostReason, message string) {
		fmt.Fprintf(os.Stderr, "device lost: %s %q\n", reason, message)
	},
})
  • Before fix: panic at adapter.go:241.
  • After fix: OK: device created.
  • Regression: RequestDevice(nil) still works.
  • Heap safety: three devices created and released in sequence (nil → label+callback → label+RequiredLimits+callback) all succeed — no heap corruption.
  • Race detector: go run -race clean (the DeviceLostCallback runs on a separate C thread).
  • Existing examples: go build ./examples/triangle/ and go vet ./wgpu/ pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions