Skip to content

amd64 struct arguments ignore field offsets, corrupting values and shifting later args #520

Description

@kumagi

PureGo Version

main @ 763bb99

Operating System

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

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:

  1. 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.
  2. 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.
  3. 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.

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions