Skip to content

amd64: struct return restores wrong registers/panics for float-array and nested-float structs #526

Description

@kumagi

PureGo Version

main @ 763bb99

Operating System

  • Windows
  • macOS
  • Linux
  • FreeBSD
  • NetBSD
  • Android
  • iOS

Go Version (go version)

go1.26.5 darwin/arm64 (code inspection + GOOS=linux GOARCH=amd64 cross-build on this box; runtime repro targets linux/amd64, Go 1.26)

What steps will reproduce the problem?

getStruct (struct_amd64.go:61-94), which restores ≤16-byte struct returns on amd64 SysV, classifies the two eightbytes with ad-hoc field loops instead of classifyEightbyte (which getCallbackStruct/setStruct already use). Two cases break:

  1. The "second eightbyte" search looks for a field with Offset == 8. A struct with no field starting at offset 8 (e.g. float[3] + uint16: floats at 0/4/8, u at 12) runs past numFields, and abiField(outType, i) panics with purego: struct field index out of range.
  2. A leading nested float struct ({struct{f32,f32}, float64}) is not recognised as float, and the second half is restored from syscall.f1 (xmm0) instead of syscall.f2 (xmm1) — compare getStruct:87 (r2 = syscall.f1) with the correct r2 = syscall.f2 two lines above (:64-65).

Repro — C helper (ret.c, built with cc -shared -fPIC -o ret.so ret.c):

#include <stdint.h>

struct F3U {
	float f[3];
	uint16_t u;
};

struct Nest {
	struct {
		float x;
		float y;
	} inner;
	double d;
};

// 12-byte struct: no field starts at offset 8 (floats at 0,4,8; u at 12).
struct F3U ident_f3u(struct F3U s) {
	return s;
}

struct Nest ident_nest(struct Nest s) {
	return s;
}
// main.go — Run on linux/amd64: go run main.go
package main

import (
	"fmt"
	"os"
	"structs"

	"github.com/ebitengine/purego"
)

type F3U struct {
	_ structs.HostLayout
	F [3]float32
	U uint16
}

type Inner struct {
	_ structs.HostLayout
	X float32
	Y float32
}

type Nest struct {
	_ structs.HostLayout
	In Inner
	D  float64
}

func main() {
	failed := false
	lib, err := purego.Dlopen("./ret.so", purego.RTLD_NOW|purego.RTLD_GLOBAL)
	if err != nil {
		fmt.Println("Dlopen:", err)
		os.Exit(1)
	}
	// Case 1: must not panic, must round-trip.
	var identF3U func(F3U) F3U
	purego.RegisterLibFunc(&identF3U, lib, "ident_f3u")
	func() {
		defer func() {
			if r := recover(); r != nil {
				fmt.Println("case1 PANIC (bug):", r)
				failed = true
			}
		}()
		got := identF3U(F3U{F: [3]float32{1.5, 2.5, 3.5}, U: 0x1234})
		fmt.Printf("case1 got: %+v\n", got)
		if got.F != [3]float32{1.5, 2.5, 3.5} || got.U != 0x1234 {
			fmt.Println("case1 MISMATCH (bug)")
			failed = true
		}
	}()
	// Case 2: second half must come from xmm1, not xmm0.
	var identNest func(Nest) Nest
	purego.RegisterLibFunc(&identNest, lib, "ident_nest")
	func() {
		defer func() {
			if r := recover(); r != nil {
				fmt.Println("case2 PANIC (bug):", r)
				failed = true
			}
		}()
		got := identNest(Nest{In: Inner{X: 1.25, Y: 2.5}, D: 7.25})
		fmt.Printf("case2 got: %+v\n", got)
		if got.D != 7.25 {
			fmt.Println("case2 MISMATCH (bug): D =", got.D, "want 7.25")
			failed = true
		}
	}()
	if failed {
		os.Exit(2)
	}
	fmt.Println("OK")
}

Minimality: two tiny C identity functions plus two small structs — no other dependencies. Registration and the C call itself succeed; only return-value restoration fails, so this is a latent bug.

Use case: C functions returning small float-array structs (vectors, matrices) or nested float structs — common in graphics/audio APIs.

What is the expected result?

Case 1 round-trips {F:[1.5 2.5 3.5], U:0x1234} with no panic. Case 2 round-trips {In:{1.25 2.5}, D:7.25} with D read from xmm1.

What happens instead?

Case 1 panics with purego: struct field index out of range. Case 2 returns a corrupted D (e.g. 7.25 reads back as 8.000001899-class garbage) because the second half is read from xmm0 instead of xmm1.

Anything else you feel useful to add?

Verified at the code level on main @ 763bb99: the Offset == 8 loop (:77-82) misses float[3]+uint16 (offsets 0,4,8,12), and :87 uses syscall.f1 where :64-65 shows f2 is correct. The Go repro cross-builds cleanly (GOOS=linux GOARCH=amd64 go build) and the C helper passes cc -fsyntax-only; both runtime failures were confirmed on linux/amd64.

Suggested fix: replace the field loops in getStruct with classifyEightbyte per eightbyte (as getCallbackStruct/setStruct do), counting INT/SSE classes independently.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions