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.
The
RequestDevice(nil)path (and any descriptor without aDeviceLostCallback) works normally. Bug found in version v1.34.1 (not tested in other versions). It makes device-loss observation via callback impossible.Environment
github.com/oliverbestmann/webgpuv1.34.1go version go1.26.4 windows/amd64(CGO_ENABLED=1,CC=gcc)Root cause
The
DeviceDescriptoris 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)
Why
RequestAdapterdoes not panic with the same patternRequestAdapter(instance.go:215) embeds the callback handle inC.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.RequestDevicepassesdesc(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.
calloczero-initializes, matching the previous composite literal, and the existingC.calloc/C.freepattern is already used forrequiredFeatures,requiredLimitsanddeviceExtrasin 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.wgpuAdapterRequestDeviceis synchronous (it blocks until the request callback completes), so freeingdescat function exit is safe. Thedeferis registered inside theif descriptor != nilblock, so thenilpath is unchanged.Validation
Minimal reproduction code (no GLFW, no window, no surface)
adapter.go:241.OK: device created.RequestDevice(nil)still works.go run -raceclean (the DeviceLostCallback runs on a separate C thread).go build ./examples/triangle/andgo vet ./wgpu/pass.