PureGo Version
main @ 763bb99
Operating System
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.
PureGo Version
main @ 763bb99
Operating System
Go Version (
go version)go1.26.5 darwin/arm64 (code inspection +
GOOS=linux GOARCH=amd64cross-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) executesXORL AX, AX // vararg: say "no float args"before every C call. Under System V,ALmust 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 (observed0.0).Repro — pure Go + libc, no C toolchain needed (run on linux/amd64):
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.5writes"1.500000 2.500000"), while"%d", 42writes"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", 42correctly 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:114zeroesALon the shareddarwin || freebsd || linux || netbsdpath, 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
...anyexpansion (a separate mechanism) and from issue #116 — here the Go→C float delivery works, but the ABIALfield 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 inAL.