Skip to content

amd64: C variadic calls lose all float args (AL zeroed in syscall trampoline) #527

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?

The amd64 syscall trampoline (sys_amd64.s:114) executes XORL AX, AX // vararg: say "no float args" before every C call. Under System V, AL must hold the number of XMM registers used for a variadic call, so a C variadic function never saves/restores XMM and every float vararg arrives as garbage (observed 0.0).

Repro — pure Go + libc, no C toolchain needed (run on linux/amd64):

// main.go — go run main.go
package main

import (
	"fmt"
	"os"

	"github.com/ebitengine/purego"
)

func main() {
	libc, err := purego.Dlopen("libc.so.6", purego.RTLD_NOW|purego.RTLD_GLOBAL)
	if err != nil {
		fmt.Println("Dlopen:", err)
		os.Exit(1)
	}
	var snprintf func(buf []byte, n uintptr, format string, args ...any) int32
	purego.RegisterLibFunc(&snprintf, libc, "snprintf")
	buf := make([]byte, 64)
	n := snprintf(buf, uintptr(len(buf)), "%f", 3.14)
	s := string(buf[:n])
	fmt.Printf("got: %q want: %q\n", s, "3.140000")
	var snprintf2 func(buf []byte, n uintptr, format string, args ...any) int32
	purego.RegisterLibFunc(&snprintf2, libc, "snprintf")
	buf2 := make([]byte, 64)
	n2 := snprintf2(buf2, uintptr(len(buf2)), "%d", 42)
	fmt.Printf("int control: %q (want \"42\")\n", string(buf2[:n2]))
	if s != "3.140000" {
		fmt.Println("BUG REPRODUCED: float vararg reads as 0.000000")
		os.Exit(2)
	}
	fmt.Println("OK")
}

Minimality: libc only; one float vararg vs one int control. The int case proves argument passing itself works — only floats break.

Use case: printf/sprintf/snprintf/scanf-style C APIs with %f/%g — integer-only calls never show the bug, which is why it stayed latent.

What is the expected result?

snprintf(buf, "%f", 3.14) writes "3.140000" (and "%f %f", 1.5, 2.5 writes "1.500000 2.500000"), while "%d", 42 writes "42".

What happens instead?

Float varargs always format as "0.000000" ("%f", 3.14"0.000000", "%f %f", 1.5, 2.5"0.000000 0.000000"), while "%d", 42 correctly yields "42". Deterministic data corruption for any float in a C variadic call.

Anything else you feel useful to add?

Verified at the code level on main @ 763bb99: sys_amd64.s:114 zeroes AL on the shared darwin || freebsd || linux || netbsd path, and nothing afterwards sets the XMM count. The Go repro cross-builds cleanly (GOOS=linux GOARCH=amd64 go build); the "0.000000" vs "42" split was confirmed on linux/amd64.

Note: this is distinct from purego's ...any expansion (a separate mechanism) and from issue #116 — here the Go→C float delivery works, but the ABI AL field tells the callee there are zero float args. Suggested fix: extend the call convention so the number of consumed float slots is passed through to the trampoline and set in AL.

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