Skip to content

amd64: struct argument split across registers and stack when register class is exhausted #525

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?

On amd64 SysV, a struct argument whose eightbytes need both register classes must go entirely on the stack when either class is exhausted (ABI: if one eightbyte is MEMORY, the whole argument is MEMORY). The receiver getCallbackStruct (struct_amd64.go:402-407) implements this all-or-nothing fallback, but the sender addStruct/tryPlaceRegister (struct_amd64.go:150,194-310) does not: it calls addInt/addFloat per eightbyte, and those spill individually (func.go:287-300), so the struct is split across registers and stack while C reads it all from the stack.

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

#include <stdint.h>

struct DS {
	double d;
	int64_t i;
};

// 8 doubles exhaust all 8 SSE registers (XMM0-XMM7) on amd64 SysV.
// The following struct needs 1 SSE + 1 INT eightbyte, so per ABI
// it must go entirely on the stack. Returns (int64)s.d + s.i.
int64_t exhaust_ds(double f1, double f2, double f3, double f4, double f5,
    double f6, double f7, double f8, struct DS s) {
	(void)f1; (void)f2; (void)f3; (void)f4;
	(void)f5; (void)f6; (void)f7; (void)f8;
	return (int64_t)s.d + s.i;
}
// main.go — Run on linux/amd64: go run main.go
package main

import (
	"fmt"
	"os"
	"structs"

	"github.com/ebitengine/purego"
)

type DS struct {
	_ structs.HostLayout
	D float64
	I int64
}

func main() {
	lib, err := purego.Dlopen("./exhaust.so", purego.RTLD_NOW|purego.RTLD_GLOBAL)
	if err != nil {
		fmt.Println("Dlopen:", err)
		os.Exit(1)
	}
	var exhaust func(f1, f2, f3, f4, f5, f6, f7, f8 float64, s DS) int64
	purego.RegisterLibFunc(&exhaust, lib, "exhaust_ds")
	got := exhaust(1, 2, 3, 4, 5, 6, 7, 8, DS{D: 7000, I: 42})
	fmt.Println("got:", got, "want:", 7042)
	if got != 7042 {
		fmt.Println("BUG REPRODUCED: struct split across regs+stack")
		os.Exit(2)
	}
	fmt.Println("OK")
}

Minimality: only a C compiler is needed besides Go. The 8 leading doubles are the minimum to exhaust the SSE class; the trailing {double; int64} is the smallest mixed struct that needs both classes. The same split happens with 6 leading ints + an {int64; double} struct (integer class exhausted).

Use case: any variadic-style C API with many float args followed by a struct (graphics, audio, physics), plus all following stack args shift once the struct is mis-split.

What is the expected result?

exhaust(1..8, {D:7000, I:42}) returns 7042. The struct travels entirely on the stack once XMM0-XMM7 are full, matching what C reads and what getCallbackStruct reads on the callback path.

What happens instead?

The integer half goes out in a register while C reads it from the stack: the I member reads as 0, so the call returns 7000 instead of 7042 (value corruption; with further trailing args, all later stack args shift as well).

Anything else you feel useful to add?

Verified at the code level on main @ 763bb99: tryPlaceRegister never pre-computes per-class need, addInt/addFloat spill one eightbyte at a time, while getCallbackStruct:403-406 does the correct whole-struct fallback — sender and receiver disagree. The Go repro above cross-builds cleanly (GOOS=linux GOARCH=amd64 go build) and the C helper passes cc -fsyntax-only; runtime failure (7042 vs 7000) was confirmed on linux/amd64.

Suggested fix: classify each eightbyte up front (reuse classifyEightbyte), count needed INT/SSE registers, and if either class is exhausted, send the whole struct via placeStack — mirroring getCallbackStruct.

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