Skip to content

darwin/arm64 struct corruption for homogeneous int32 structs #359

Description

@tmc

PureGo Version

v0.10.0-alpha.2

Operating System

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

Go Version (go version)

1.25

What steps will reproduce the problem?

type FourInt32s struct {
    f0, f1, f2, f3 int32
}

// C function
int32_t FourInt32sSum(struct FourInt32s s) {
    return s.f0 + s.f1 + s.f2 + s.f3;
}

// Go call
var fn func(FourInt32s) int32
purego.RegisterLibFunc(&fn, lib, "FourInt32sSum")
sum := fn(FourInt32s{1, 2, 3, 4})
// Expected: 10 (1+2+3+4)
// Actual: 12 (1+2+3+6) - f3 corrupted from 4 to 6

What is the expected result?

// Expected: 10 (1+2+3+4)

What happens instead?

// Actual: 12 (1+2+3+6) - f3 corrupted from 4 to 6

Anything else you feel useful to add?

The Bug Chain

  1. isHVA() returns true for FourInt32s

    • Located in struct_arm64.go:258-284
    • Checks if struct has homogeneous integer fields (line 266: case reflect.Int32)
    • Returns true for any struct with all int32 fields, size 8 or 16 bytes
  2. addStruct() calls placeRegisters() for HVAs

    • Located in struct_arm64.go:73
    • Condition: hva || hfa || size <= 16
    • Since isHVA() returns true, it calls placeRegisters()
  3. placeRegisters() uses placeRegistersAArch64() logic

    • Located in struct_arm64.go:89-198 (on non-Darwin ARM64)
    • Uses field-by-field bit-shifting and OR operations
    • For int32 fields (line 155-158):
      case reflect.Int32:
          val |= uint64(f.Int()&0xFFFF_FFFF) << shift
          shift += 32
          class |= _INT
  4. Bit-shifting overflow corrupts the 4th field

    • After field 0 (f0=1): val = 0x0000000000000001, shift = 32
    • After field 1 (f1=2): val = 0x0000000200000001, shift = 64
    • After field 2 (f2=3): shift = 64 causes flush, new val = 0x0000000000000003, shift = 32
    • After field 3 (f3=4): shift = 32, so val |= 4 << 32 = 0x0000000400000003
    • But this is wrong! The flush happened too late, causing f3 to OR with f1's position
    • Result: Second register contains 0x0000000600000003 instead of 0x0000000400000003

Memory Layout Evidence

Expected (byte-level packing):

Chunk 0 (X0): 0x0000000200000001  (f1:f0 = 2:1)
Chunk 1 (X1): 0x0000000400000003  (f3:f2 = 4:3)

Actual (with bug):

Chunk 0 (X0): 0x0000000200000001  (f1:f0 = 2:1)
Chunk 1 (X1): 0x0000000600000003  (f3:f2 = 6:3) <- f3 corrupted!

C side receives:

byte[12] = 0x06  // Should be 0x04
// Interpreted as f3 = 6 instead of 4

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions