detached b9d062c

purego: fix arm64 argument corruption on alignment flush (#360)

Fix a critical bug in ARM64 struct argument packing where the register
value (val) and class were not being reset after flushing due to
alignment requirements.

When packing struct fields into registers, if a field's alignment
causes shift >= 64, the current register is flushed. However, the
code was not resetting 'val' and 'class' after the flush, causing
subsequent fields to be ORed with stale data from previous fields.

Example bug with FourInt32s{1, 2, 3, 4}:
- Fields 0,1 packed: val = 0x0000000200000001
- Flush at field 2 due to shift >= 64
- BUG: val still contains 0x0000000200000001
- Field 2 packs: val |= 3 becomes 0x0000000200000003 (should be 0x03)
- Field 3 packs: val |= (4<<32) becomes 0x0000000400000003
- Result: field 3 = 6 instead of 4 (bit 1 from field 1 leaked)

This fix ensures val and class are properly reset after each flush,
preventing data corruption between register boundaries.

Closes #359
commit
b9d062cb1754bcd2f306c75ff1bf229023c7cd30
author
Travis Cline · · 2025-10-31 18:09 UTC
committer
Hajime Hoshi · · 2025-10-31 18:10 UTC
parent
cdb7cab
signature
unsigned
refs
tag: v0.9.1
3 files · +31 -3
M struct_arm64.go +2 -0
@@ -117,6 +117,8 @@ func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr
117117 } else {
118118 addInt(uintptr(val))
119119 }
120+ val = 0
121+ class = _NO_CLASS
120122 }
121123 switch f.Type().Kind() {
122124 case reflect.Struct:
@@ -117,6 +117,8 @@ func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr
117 } else {117 } else {
118 addInt(uintptr(val))118 addInt(uintptr(val))
119 }119 }
120+ val = 0
121+ class = _NO_CLASS
120 }122 }
121 switch f.Type().Kind() {123 switch f.Type().Kind() {
122 case reflect.Struct:124 case reflect.Struct:
M struct_test.go +15 -2
@@ -373,8 +373,9 @@ func TestRegisterFunc_structArgs(t *testing.T) {
373373 }
374374 var Array4CharsFn func(chars Array4Chars) int32
375375 purego.RegisterLibFunc(&Array4CharsFn, lib, "Array4Chars")
376- if ret := Array4CharsFn(Array4Chars{a: [...]int8{100, -127, 4, -100}}); ret != expectedSigned {
377- t.Fatalf("Array4CharsFn returned %#x wanted %#x", ret, expectedSigned)
376+ const expectedSum = 1 + 2 + 4 + 8
377+ if ret := Array4CharsFn(Array4Chars{a: [...]int8{1, 2, 4, 8}}); ret != expectedSum {
378+ t.Fatalf("Array4CharsFn returned %d wanted %d", ret, expectedSum)
378379 }
379380 }
380381 {
@@ -486,6 +487,18 @@ func TestRegisterFunc_structArgs(t *testing.T) {
486487 t.Fatalf("FloatAndBool(y: false) = %d, want 0", ret)
487488 }
488489 }
490+ {
491+ type FourInt32s struct {
492+ f0, f1, f2, f3 int32
493+ }
494+ var FourInt32sFn func(FourInt32s) int32
495+ purego.RegisterLibFunc(&FourInt32sFn, lib, "FourInt32s")
496+ result := FourInt32sFn(FourInt32s{100, -127, 4, -100})
497+ const want = 100 - 127 + 4 - 100
498+ if result != want {
499+ t.Fatalf("FourInt32s returned %d wanted %d", result, want)
500+ }
501+ }
489502 }
490503
491504 func TestRegisterFunc_structReturns(t *testing.T) {
@@ -373,8 +373,9 @@ func TestRegisterFunc_structArgs(t *testing.T) {
373 }373 }
374 var Array4CharsFn func(chars Array4Chars) int32374 var Array4CharsFn func(chars Array4Chars) int32
375 purego.RegisterLibFunc(&Array4CharsFn, lib, "Array4Chars")375 purego.RegisterLibFunc(&Array4CharsFn, lib, "Array4Chars")
376- if ret := Array4CharsFn(Array4Chars{a: [...]int8{100, -127, 4, -100}}); ret != expectedSigned {376+ const expectedSum = 1 + 2 + 4 + 8
377- t.Fatalf("Array4CharsFn returned %#x wanted %#x", ret, expectedSigned)377+ if ret := Array4CharsFn(Array4Chars{a: [...]int8{1, 2, 4, 8}}); ret != expectedSum {
378+ t.Fatalf("Array4CharsFn returned %d wanted %d", ret, expectedSum)
378 }379 }
379 }380 }
380 {381 {
@@ -486,6 +487,18 @@ func TestRegisterFunc_structArgs(t *testing.T) {
486 t.Fatalf("FloatAndBool(y: false) = %d, want 0", ret)487 t.Fatalf("FloatAndBool(y: false) = %d, want 0", ret)
487 }488 }
488 }489 }
490+ {
491+ type FourInt32s struct {
492+ f0, f1, f2, f3 int32
493+ }
494+ var FourInt32sFn func(FourInt32s) int32
495+ purego.RegisterLibFunc(&FourInt32sFn, lib, "FourInt32s")
496+ result := FourInt32sFn(FourInt32s{100, -127, 4, -100})
497+ const want = 100 - 127 + 4 - 100
498+ if result != want {
499+ t.Fatalf("FourInt32s returned %d wanted %d", result, want)
500+ }
501+ }
489 }502 }
490 503
491 func TestRegisterFunc_structReturns(t *testing.T) {504 func TestRegisterFunc_structReturns(t *testing.T) {
M testdata/structtest/struct_test.c +14 -1
@@ -1,7 +1,9 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2024 The Ebitengine Authors
33
4-#include "stdint.h"
4+#include <stdint.h>
5+#include <stdio.h>
6+#include <stdlib.h>
57
68 #if defined(__x86_64__) || defined(__aarch64__)
79 typedef int64_t GoInt;
@@ -361,3 +363,14 @@ struct FloatAndBool {
361363 int FloatAndBool(struct FloatAndBool f) {
362364 return f.has_value;
363365 }
366+
367+struct FourInt32s {
368+ int32_t f0;
369+ int32_t f1;
370+ int32_t f2;
371+ int32_t f3;
372+};
373+
374+int32_t FourInt32s(struct FourInt32s s) {
375+ return s.f0 + s.f1 + s.f2 + s.f3;
376+}
@@ -1,7 +1,9 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2024 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2024 The Ebitengine Authors
3 3
4-#include "stdint.h"4+#include <stdint.h>
5+#include <stdio.h>
6+#include <stdlib.h>
5 7
6 #if defined(__x86_64__) || defined(__aarch64__)8 #if defined(__x86_64__) || defined(__aarch64__)
7 typedef int64_t GoInt;9 typedef int64_t GoInt;
@@ -361,3 +363,14 @@ struct FloatAndBool {
361 int FloatAndBool(struct FloatAndBool f) {363 int FloatAndBool(struct FloatAndBool f) {
362 return f.has_value;364 return f.has_value;
363 }365 }
366+
367+struct FourInt32s {
368+ int32_t f0;
369+ int32_t f1;
370+ int32_t f2;
371+ int32_t f3;
372+};
373+
374+int32_t FourInt32s(struct FourInt32s s) {
375+ return s.f0 + s.f1 + s.f2 + s.f3;
376+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close