Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
125 changes: 125 additions & 0 deletions internal/arch/arm64/hfa_return_test.go
Original file line number Diff line number Diff line change
@@ -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])
}
}
18 changes: 10 additions & 8 deletions internal/arch/arm64/implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Loading