PureGo Version
main @ 763bb99
Operating System
Go Version (go version)
go1.26.7 linux/amd64
What steps will reproduce the problem?
On amd64, tryPlaceRegister in struct_amd64.go packs struct fields back-to-back into a 64-bit accumulator without looking at each field's in-memory offset:
- 64-bit kinds (
Pointer, Int64/Int, Uint64/Uint/Uintptr, Float64) do val = ... instead of val |= ..., so a 64-bit field following a smaller field overwrites it. The first eightbyte is dropped and all later arguments shift by one slot.
- Small fields (
Int8/16/32, Uint8/16/32, Bool, Float32) ignore padding, so e.g. the int32 at offset 4 in {int8; int32} is placed at bit 8 instead of bit 32.
- The recursive
place() clobbers the outer eightbyte cursor with the inner tail position, so later siblings of nested structs are misclassified (e.g. StructInStruct{A, B, C inner} loses B and C).
Repro — C helper (pack.c, built with cc -shared -fPIC -o pack.so pack.c):
#include <stdint.h>
struct S1 { int8_t a; int64_t b; }; // B at offset 8, size 16
struct S2 { int8_t a; int32_t b; }; // B at offset 4, size 8
struct S1 ident_S1(struct S1 s) { return s; }
struct S2 ident_S2(struct S2 s) { return s; }
int64_t trailing_S1(struct S1 s, int64_t extra) { (void)s; return extra; }
type S1np struct {
_ structs.HostLayout
A int8
B int64
}
type S2np struct {
_ structs.HostLayout
A int8
B int32
}
var identS1 func(S1np) S1np
purego.RegisterLibFunc(&identS1, lib, "ident_S1")
got1 := identS1(S1np{A: 0x11, B: 0x2222222222222222})
// got1 = {A:0x22 B:0x0}, want {A:0x11 B:0x2222222222222222}
var identS2 func(S2np) S2np
purego.RegisterLibFunc(&identS2, lib, "ident_S2")
got2 := identS2(S2np{A: 0x11, B: 0x22222222})
// got2 = {A:0x11 B:0x22}, want {A:0x11 B:0x22222222}
var trailing func(S1np, int64) int64
purego.RegisterLibFunc(&trailing, lib, "trailing_S1")
got3 := trailing(S1np{A: 0x11, B: 0x2222222222222222}, 0x3333333333333333)
// got3 = 0x0, want 0x3333333333333333 (later argument shifted by one slot)
The existing tests only pass because they use explicit padding fields (e.g. _[7]int8), which happen to flush the accumulator. Naturally-padded structs hit this every time.
What is the expected result?
Struct arguments are packed exactly as their in-memory image (per System V AMD64 ABI §3.2.3): {int8; int64} goes out as eightbyte0 = {A + pad}, eightbyte1 = {B}, and later arguments keep their slots. All three repro cases return the input unchanged.
What happens instead?
Field values are corrupted (A=0x22 B=0x0, B=0x22) and arguments after the struct shift by one slot (extra reads as 0x0). Depending on the callee this is silent data corruption or a crash.
Anything else you feel useful to add?
Minimized reproducible case: the pack.c helper plus the three Go snippets above (only a C compiler needed, no other deps). All three verified failing on main @ 763bb99, Linux/amd64, Go 1.26.7.
Related PR: #515.
PureGo Version
main @ 763bb99
Operating System
Go Version (
go version)go1.26.7 linux/amd64
What steps will reproduce the problem?
On amd64,
tryPlaceRegisterinstruct_amd64.gopacks struct fields back-to-back into a 64-bit accumulator without looking at each field's in-memory offset:Pointer,Int64/Int,Uint64/Uint/Uintptr,Float64) doval = ...instead ofval |= ..., so a 64-bit field following a smaller field overwrites it. The first eightbyte is dropped and all later arguments shift by one slot.Int8/16/32,Uint8/16/32,Bool,Float32) ignore padding, so e.g. theint32at offset 4 in{int8; int32}is placed at bit 8 instead of bit 32.place()clobbers the outer eightbyte cursor with the inner tail position, so later siblings of nested structs are misclassified (e.g.StructInStruct{A, B, C inner}loses B and C).Repro — C helper (
pack.c, built withcc -shared -fPIC -o pack.so pack.c):The existing tests only pass because they use explicit padding fields (e.g.
_[7]int8), which happen to flush the accumulator. Naturally-padded structs hit this every time.What is the expected result?
Struct arguments are packed exactly as their in-memory image (per System V AMD64 ABI §3.2.3):
{int8; int64}goes out aseightbyte0 = {A + pad},eightbyte1 = {B}, and later arguments keep their slots. All three repro cases return the input unchanged.What happens instead?
Field values are corrupted (
A=0x22 B=0x0,B=0x22) and arguments after the struct shift by one slot (extrareads as0x0). Depending on the callee this is silent data corruption or a crash.Anything else you feel useful to add?
Minimized reproducible case: the
pack.chelper plus the three Go snippets above (only a C compiler needed, no other deps). All three verified failing onmain @ 763bb99, Linux/amd64, Go 1.26.7.Related PR: #515.