diff --git a/CHANGELOG.md b/CHANGELOG.md index f614413..9bbd595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.3] - 2026-08-01 + +### Fixed +- **ARM64: HFA return checkptr crash** — `handleHFAReturn` cast `(*[4]float64)(rvalue)` was oversized for 2-3 element HFAs (CGSize, CGPoint = 16 bytes, cast = 32 bytes). `go test -race` (checkptr) crashed with "converted pointer straddles multiple allocations". Fix: per-element writes via `unsafe.Add`. ([#67](https://github.com/go-webgpu/goffi/issues/67), reported by @jbunds) +- **ARM64: 9-16B struct return silent corruption** — `(*[2]uint64)(rvalue)` wrote 8 full bytes for the hi word even when struct was smaller than 16 bytes. Fix: `copy` with exact remaining size, matching AMD64 pattern. Proactive fix — checkptr doesn't catch this (GC pads to 16), but packed C structs could trigger corruption +- Added `TestHandleHFAReturn_Checkptr` and `TestHandleHFAReturn_Float32` unit tests + ## [0.6.2] - 2026-07-22 ### Fixed diff --git a/internal/arch/arm64/hfa_return_test.go b/internal/arch/arm64/hfa_return_test.go new file mode 100644 index 0000000..95f80bc --- /dev/null +++ b/internal/arch/arm64/hfa_return_test.go @@ -0,0 +1,125 @@ +//go:build arm64 + +package arm64 + +import ( + "math" + "testing" + "unsafe" + + "github.com/go-webgpu/goffi/types" +) + +// TestHandleHFAReturn_Checkptr verifies that handleHFAReturn does not +// cast rvalue to an oversized array. Before the fix, (*[4]float64)(rvalue) +// on a 2-element HFA (CGSize = 16 bytes) triggered checkptr: +// +// "converted pointer straddles multiple allocations" +// +// This test allocates exact-sized buffers and writes HFA return values. +// Under -race (which enables checkptr), the old code would crash here. +func TestHandleHFAReturn_Checkptr(t *testing.T) { + impl := &Implementation{} + + tests := []struct { + name string + flags int + count int + isFloat bool + values [4]uint64 + expected []float64 + }{ + { + name: "HFA2 float64 (CGSize/CGPoint)", + flags: types.ReturnHFA2 | types.ReturnInXMM64, + count: 2, + values: [4]uint64{math.Float64bits(1.5), math.Float64bits(2.5), 0, 0}, + expected: []float64{1.5, 2.5}, + }, + { + name: "HFA3 float64", + flags: types.ReturnHFA3 | types.ReturnInXMM64, + count: 3, + values: [4]uint64{math.Float64bits(10.0), math.Float64bits(20.0), math.Float64bits(30.0), 0}, + expected: []float64{10.0, 20.0, 30.0}, + }, + { + name: "HFA4 float64", + flags: types.ReturnHFA4 | types.ReturnInXMM64, + count: 4, + values: [4]uint64{math.Float64bits(1.0), math.Float64bits(2.0), math.Float64bits(3.0), math.Float64bits(4.0)}, + expected: []float64{1.0, 2.0, 3.0, 4.0}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Allocate exact-sized buffer matching real usage. + // checkptr validates cast target size against allocation. + buf := make([]float64, tc.count) + rvalue := unsafe.Pointer(&buf[0]) + + cif := &types.CallInterface{ + Flags: tc.flags, + ReturnType: &types.TypeDescriptor{ + Kind: types.StructType, + Members: func() []*types.TypeDescriptor { + m := make([]*types.TypeDescriptor, tc.count) + for i := range m { + m[i] = types.DoubleTypeDescriptor + } + return m + }(), + }, + } + + err := impl.handleHFAReturn(cif, rvalue, tc.values) + if err != nil { + t.Fatalf("handleHFAReturn error: %v", err) + } + + for i, want := range tc.expected { + if buf[i] != want { + t.Errorf("element[%d] = %f, want %f", i, buf[i], want) + } + } + }) + } +} + +// TestHandleHFAReturn_Float32 tests float32 HFA returns with exact-sized buffers. +func TestHandleHFAReturn_Float32(t *testing.T) { + impl := &Implementation{} + + buf := make([]float32, 2) + rvalue := unsafe.Pointer(&buf[0]) + + cif := &types.CallInterface{ + Flags: types.ReturnHFA2 | types.ReturnInXMM32, + ReturnType: &types.TypeDescriptor{ + Kind: types.StructType, + Members: []*types.TypeDescriptor{ + types.FloatTypeDescriptor, + types.FloatTypeDescriptor, + }, + }, + } + + fret := [4]uint64{ + uint64(math.Float32bits(3.14)), + uint64(math.Float32bits(2.71)), + 0, 0, + } + + err := impl.handleHFAReturn(cif, rvalue, fret) + if err != nil { + t.Fatalf("handleHFAReturn error: %v", err) + } + + if buf[0] != 3.14 { + t.Errorf("float32[0] = %f, want 3.14", buf[0]) + } + if buf[1] != 2.71 { + t.Errorf("float32[1] = %f, want 2.71", buf[1]) + } +} diff --git a/internal/arch/arm64/implementation.go b/internal/arch/arm64/implementation.go index a265cbe..d2c202a 100644 --- a/internal/arch/arm64/implementation.go +++ b/internal/arch/arm64/implementation.go @@ -88,10 +88,14 @@ func (i *Implementation) handleReturn( if cif.ReturnType.Size <= 8 { *(*uint64)(rvalue) = retLo } else if cif.ReturnType.Size <= 16 { - // 9-16 byte struct returned in X0-X1 - dest := (*[2]uint64)(rvalue) - dest[0] = retLo - dest[1] = retHi + // 9-16 byte struct returned in X0-X1. + // Write hi word via copy to avoid oversized cast on packed structs. + *(*uint64)(rvalue) = retLo + remaining := cif.ReturnType.Size - 8 + copy( + (*[8]byte)(unsafe.Add(rvalue, 8))[:remaining], + (*[8]byte)(unsafe.Pointer(&retHi))[:remaining], + ) } else { return types.ErrUnsupportedReturnType } @@ -133,16 +137,14 @@ func (i *Implementation) handleHFAReturn( isFloat32 := elemKind == types.FloatType if isFloat32 { - dest := (*[4]float32)(rvalue) for idx := 0; idx < hfaCount; idx++ { - dest[idx] = math.Float32frombits(uint32(fret[idx])) + *(*float32)(unsafe.Add(rvalue, uintptr(idx)*4)) = math.Float32frombits(uint32(fret[idx])) } return nil } - dest := (*[4]float64)(rvalue) for idx := 0; idx < hfaCount; idx++ { - dest[idx] = math.Float64frombits(fret[idx]) + *(*float64)(unsafe.Add(rvalue, uintptr(idx)*8)) = math.Float64frombits(fret[idx]) } return nil }