detached b883928

fix: harden struct classification across architectures

commit
b883928226ea452a87bcf382f30a67ad967190ed
author
brice · · 2026-04-13 10:09 UTC
parent
0879f2b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +144 -52
M struct_amd64.go +22 -27
@@ -23,35 +23,30 @@ func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
2323 // up to 8 bytes is returned in RAX
2424 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.a1})).Elem()
2525 case outSize <= 16:
26- r1, r2 := syscall.a1, syscall.a2
27- if isAllFloats(outType) {
28- r1 = syscall.f1
29- r2 = syscall.f2
30- } else {
31- // check first 8 bytes if it's floats
32- hasFirstFloat := false
33- f1 := outType.Field(0).Type
34- if f1.Kind() == reflect.Float64 || f1.Kind() == reflect.Float32 && outType.Field(1).Type.Kind() == reflect.Float32 {
35- r1 = syscall.f1
36- hasFirstFloat = true
37- }
38-
39- // find index of the field that starts the second 8 bytes
40- var i int
41- for i = 0; i < outType.NumField(); i++ {
42- if outType.Field(i).Offset == 8 {
43- break
26+ var r1, r2 uintptr
27+ var intRegs, floatRegs int
28+ for i := 0; i < int((outSize+7)/8); i++ {
29+ class := classifyEightbyte(outType, uintptr(i)*8, uintptr(i)*8+8)
30+ var reg uintptr
31+ if class == _SSE {
32+ if floatRegs == 0 {
33+ reg = syscall.f1
34+ } else {
35+ reg = syscall.f2
4436 }
37+ floatRegs++
38+ } else {
39+ if intRegs == 0 {
40+ reg = syscall.a1
41+ } else {
42+ reg = syscall.a2
43+ }
44+ intRegs++
4545 }
46-
47- // check last 8 bytes if they are floats
48- f1 = outType.Field(i).Type
49- if f1.Kind() == reflect.Float64 || f1.Kind() == reflect.Float32 && i+1 == outType.NumField() {
50- r2 = syscall.f1
51- } else if hasFirstFloat {
52- // if the first field was a float then that means the second integer field
53- // comes from the first integer register
54- r2 = syscall.a1
46+ if i == 0 {
47+ r1 = reg
48+ } else {
49+ r2 = reg
5550 }
5651 }
5752 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem()
@@ -23,35 +23,30 @@ func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
23 // up to 8 bytes is returned in RAX23 // up to 8 bytes is returned in RAX
24 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.a1})).Elem()24 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.a1})).Elem()
25 case outSize <= 16:25 case outSize <= 16:
26- r1, r2 := syscall.a1, syscall.a226+ var r1, r2 uintptr
27- if isAllFloats(outType) {27+ var intRegs, floatRegs int
28- r1 = syscall.f128+ for i := 0; i < int((outSize+7)/8); i++ {
29- r2 = syscall.f229+ class := classifyEightbyte(outType, uintptr(i)*8, uintptr(i)*8+8)
30- } else {30+ var reg uintptr
31- // check first 8 bytes if it's floats31+ if class == _SSE {
32- hasFirstFloat := false32+ if floatRegs == 0 {
33- f1 := outType.Field(0).Type33+ reg = syscall.f1
34- if f1.Kind() == reflect.Float64 || f1.Kind() == reflect.Float32 && outType.Field(1).Type.Kind() == reflect.Float32 {34+ } else {
35- r1 = syscall.f135+ reg = syscall.f2
36- hasFirstFloat = true
37- }
38-
39- // find index of the field that starts the second 8 bytes
40- var i int
41- for i = 0; i < outType.NumField(); i++ {
42- if outType.Field(i).Offset == 8 {
43- break
44 }36 }
37+ floatRegs++
38+ } else {
39+ if intRegs == 0 {
40+ reg = syscall.a1
41+ } else {
42+ reg = syscall.a2
43+ }
44+ intRegs++
45 }45 }
46-46+ if i == 0 {
47- // check last 8 bytes if they are floats47+ r1 = reg
48- f1 = outType.Field(i).Type48+ } else {
49- if f1.Kind() == reflect.Float64 || f1.Kind() == reflect.Float32 && i+1 == outType.NumField() {49+ r2 = reg
50- r2 = syscall.f1
51- } else if hasFirstFloat {
52- // if the first field was a float then that means the second integer field
53- // comes from the first integer register
54- r2 = syscall.a1
55 }50 }
56 }51 }
57 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem()52 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem()
A struct_amd64_regression_test.go +21 -0
@@ -0,0 +1,21 @@
1+//go:build amd64 && (darwin || linux)
2+
3+package purego
4+
5+import (
6+ "reflect"
7+ "testing"
8+)
9+
10+func TestGetStructHandlesSecondEightbyteWithoutOffset8Field(t *testing.T) {
11+ type awkward struct {
12+ A [3]uint32
13+ B uint32
14+ }
15+ sys := syscall15Args{a1: 0x0000000200000001, a2: 0x0000000400000003}
16+ got := getStruct(reflect.TypeOf(awkward{}), sys).Interface().(awkward)
17+ want := awkward{A: [3]uint32{1, 2, 3}, B: 4}
18+ if got != want {
19+ t.Fatalf("got %+v, want %+v", got, want)
20+ }
21+}
@@ -0,0 +1,21 @@
1+//go:build amd64 && (darwin || linux)
2+
3+package purego
4+
5+import (
6+ "reflect"
7+ "testing"
8+)
9+
10+func TestGetStructHandlesSecondEightbyteWithoutOffset8Field(t *testing.T) {
11+ type awkward struct {
12+ A [3]uint32
13+ B uint32
14+ }
15+ sys := syscall15Args{a1: 0x0000000200000001, a2: 0x0000000400000003}
16+ got := getStruct(reflect.TypeOf(awkward{}), sys).Interface().(awkward)
17+ want := awkward{A: [3]uint32{1, 2, 3}, B: 4}
18+ if got != want {
19+ t.Fatalf("got %+v, want %+v", got, want)
20+ }
21+}
M struct_arm.go +8 -3
@@ -5,6 +5,7 @@ package purego
55
66 import (
77 "reflect"
8+ "runtime"
89 "unsafe"
910 )
1011
@@ -16,8 +17,10 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
1617
1718 // TODO: ARM EABI: small structs are passed in registers or on stack
1819 // For simplicity, pass by pointer for now
19- ptr := v.Addr().UnsafePointer()
20- keepAlive = append(keepAlive, ptr)
20+ ptr, live := stableValuePointer(v)
21+ if live != nil {
22+ keepAlive = append(keepAlive, live)
23+ }
2124 if *numInts < 4 {
2225 addInt(uintptr(ptr))
2326 *numInts++
@@ -25,6 +28,7 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
2528 addStack(uintptr(ptr))
2629 *numStack++
2730 }
31+ runtime.KeepAlive(live)
2832 return keepAlive
2933 }
3034
@@ -52,13 +56,14 @@ func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr
5256 if size == 0 {
5357 return
5458 }
55- ptr := unsafe.Pointer(v.UnsafeAddr())
59+ ptr, live := stableValuePointer(v)
5660 if size <= 4 {
5761 addInt(*(*uintptr)(ptr))
5862 } else if size <= 8 {
5963 addInt(*(*uintptr)(ptr))
6064 addInt(*(*uintptr)(unsafe.Add(ptr, 4)))
6165 }
66+ runtime.KeepAlive(live)
6267 }
6368
6469 // shouldBundleStackArgs always returns false on arm
@@ -5,6 +5,7 @@ package purego
5 5
6 import (6 import (
7 "reflect"7 "reflect"
8+ "runtime"
8 "unsafe"9 "unsafe"
9 )10 )
10 11
@@ -16,8 +17,10 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
16 17
17 // TODO: ARM EABI: small structs are passed in registers or on stack18 // TODO: ARM EABI: small structs are passed in registers or on stack
18 // For simplicity, pass by pointer for now19 // For simplicity, pass by pointer for now
19- ptr := v.Addr().UnsafePointer()20+ ptr, live := stableValuePointer(v)
20- keepAlive = append(keepAlive, ptr)21+ if live != nil {
22+ keepAlive = append(keepAlive, live)
23+ }
21 if *numInts < 4 {24 if *numInts < 4 {
22 addInt(uintptr(ptr))25 addInt(uintptr(ptr))
23 *numInts++26 *numInts++
@@ -25,6 +28,7 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
25 addStack(uintptr(ptr))28 addStack(uintptr(ptr))
26 *numStack++29 *numStack++
27 }30 }
31+ runtime.KeepAlive(live)
28 return keepAlive32 return keepAlive
29 }33 }
30 34
@@ -52,13 +56,14 @@ func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr
52 if size == 0 {56 if size == 0 {
53 return57 return
54 }58 }
55- ptr := unsafe.Pointer(v.UnsafeAddr())59+ ptr, live := stableValuePointer(v)
56 if size <= 4 {60 if size <= 4 {
57 addInt(*(*uintptr)(ptr))61 addInt(*(*uintptr)(ptr))
58 } else if size <= 8 {62 } else if size <= 8 {
59 addInt(*(*uintptr)(ptr))63 addInt(*(*uintptr)(ptr))
60 addInt(*(*uintptr)(unsafe.Add(ptr, 4)))64 addInt(*(*uintptr)(unsafe.Add(ptr, 4)))
61 }65 }
66+ runtime.KeepAlive(live)
62 }67 }
63 68
64 // shouldBundleStackArgs always returns false on arm69 // shouldBundleStackArgs always returns false on arm
M struct_arm64.go +22 -22
@@ -229,38 +229,38 @@ func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
229229 //
230230 // [Arm64 Calling Convention]: https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst
231231 func isHFA(t reflect.Type) bool {
232- // round up struct size to nearest 8 see section B.4
233232 structSize := roundUpTo8(t.Size())
234233 if structSize == 0 || t.NumField() > 4 {
235234 return false
236235 }
237- first := t.Field(0)
238- switch first.Type.Kind() {
239- case reflect.Float32, reflect.Float64:
240- firstKind := first.Type.Kind()
236+ var kind reflect.Kind
237+ var members int
238+ var walk func(reflect.Type) bool
239+ walk = func(t reflect.Type) bool {
241240 for i := 0; i < t.NumField(); i++ {
242- if t.Field(i).Type.Kind() != firstKind {
243- return false
244- }
245- }
246- return true
247- case reflect.Array:
248- switch first.Type.Elem().Kind() {
249- case reflect.Float32, reflect.Float64:
250- return true
251- default:
252- return false
253- }
254- case reflect.Struct:
255- for i := 0; i < first.Type.NumField(); i++ {
256- if !isHFA(first.Type) {
241+ ft := t.Field(i).Type
242+ switch ft.Kind() {
243+ case reflect.Float32, reflect.Float64:
244+ if kind == 0 {
245+ kind = ft.Kind()
246+ } else if ft.Kind() != kind {
247+ return false
248+ }
249+ members++
250+ if members > 4 {
251+ return false
252+ }
253+ case reflect.Struct:
254+ if !walk(ft) {
255+ return false
256+ }
257+ default:
257258 return false
258259 }
259260 }
260261 return true
261- default:
262- return false
263262 }
263+ return walk(t) && members > 0
264264 }
265265
266266 // isHVA reports a Homogeneous Aggregate with a Fundamental Data Type that is a Short-Vector type
@@ -229,38 +229,38 @@ func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
229 //229 //
230 // [Arm64 Calling Convention]: https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst230 // [Arm64 Calling Convention]: https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst
231 func isHFA(t reflect.Type) bool {231 func isHFA(t reflect.Type) bool {
232- // round up struct size to nearest 8 see section B.4
233 structSize := roundUpTo8(t.Size())232 structSize := roundUpTo8(t.Size())
234 if structSize == 0 || t.NumField() > 4 {233 if structSize == 0 || t.NumField() > 4 {
235 return false234 return false
236 }235 }
237- first := t.Field(0)236+ var kind reflect.Kind
238- switch first.Type.Kind() {237+ var members int
239- case reflect.Float32, reflect.Float64:238+ var walk func(reflect.Type) bool
240- firstKind := first.Type.Kind()239+ walk = func(t reflect.Type) bool {
241 for i := 0; i < t.NumField(); i++ {240 for i := 0; i < t.NumField(); i++ {
242- if t.Field(i).Type.Kind() != firstKind {241+ ft := t.Field(i).Type
243- return false242+ switch ft.Kind() {
244- }243+ case reflect.Float32, reflect.Float64:
245- }244+ if kind == 0 {
246- return true245+ kind = ft.Kind()
247- case reflect.Array:246+ } else if ft.Kind() != kind {
248- switch first.Type.Elem().Kind() {247+ return false
249- case reflect.Float32, reflect.Float64:248+ }
250- return true249+ members++
251- default:250+ if members > 4 {
252- return false251+ return false
253- }252+ }
254- case reflect.Struct:253+ case reflect.Struct:
255- for i := 0; i < first.Type.NumField(); i++ {254+ if !walk(ft) {
256- if !isHFA(first.Type) {255+ return false
256+ }
257+ default:
257 return false258 return false
258 }259 }
259 }260 }
260 return true261 return true
261- default:
262- return false
263 }262 }
263+ return walk(t) && members > 0
264 }264 }
265 265
266 // isHVA reports a Homogeneous Aggregate with a Fundamental Data Type that is a Short-Vector type266 // isHVA reports a Homogeneous Aggregate with a Fundamental Data Type that is a Short-Vector type
A struct_arm64_regression_test.go +30 -0
@@ -0,0 +1,30 @@
1+//go:build arm64 && (darwin || linux)
2+
3+package purego
4+
5+import (
6+ "reflect"
7+ "testing"
8+)
9+
10+func TestIsHFARejectsNestedMixedStruct(t *testing.T) {
11+ type inner struct{ X, Y float32 }
12+ type mixed struct {
13+ Inner inner
14+ Tail int32
15+ }
16+ if isHFA(reflect.TypeOf(mixed{})) {
17+ t.Fatal("expected mixed nested struct to be non-HFA")
18+ }
19+}
20+
21+func TestIsHFAAcceptsNestedFloatStructs(t *testing.T) {
22+ type inner struct{ X, Y float32 }
23+ type outer struct {
24+ Left inner
25+ Right inner
26+ }
27+ if !isHFA(reflect.TypeOf(outer{})) {
28+ t.Fatal("expected nested float-only struct to be HFA")
29+ }
30+}
@@ -0,0 +1,30 @@
1+//go:build arm64 && (darwin || linux)
2+
3+package purego
4+
5+import (
6+ "reflect"
7+ "testing"
8+)
9+
10+func TestIsHFARejectsNestedMixedStruct(t *testing.T) {
11+ type inner struct{ X, Y float32 }
12+ type mixed struct {
13+ Inner inner
14+ Tail int32
15+ }
16+ if isHFA(reflect.TypeOf(mixed{})) {
17+ t.Fatal("expected mixed nested struct to be non-HFA")
18+ }
19+}
20+
21+func TestIsHFAAcceptsNestedFloatStructs(t *testing.T) {
22+ type inner struct{ X, Y float32 }
23+ type outer struct {
24+ Left inner
25+ Right inner
26+ }
27+ if !isHFA(reflect.TypeOf(outer{})) {
28+ t.Fatal("expected nested float-only struct to be HFA")
29+ }
30+}
A struct_arm_regression_test.go +41 -0
@@ -0,0 +1,41 @@
1+//go:build arm && (freebsd || linux || netbsd || windows)
2+
3+package purego
4+
5+import (
6+ "reflect"
7+ "testing"
8+ "unsafe"
9+)
10+
11+func TestARMAddStructCopiesUnaddressableValue(t *testing.T) {
12+ type sample struct{ X uint32 }
13+ var numInts, numFloats, numStack int
14+ var ptr uintptr
15+ keepAlive := addStruct(reflect.ValueOf(sample{X: 7}), &numInts, &numFloats, &numStack, func(v uintptr) {
16+ ptr = v
17+ }, func(uintptr) {
18+ t.Fatal("unexpected float register")
19+ }, func(uintptr) {
20+ t.Fatal("unexpected stack slot")
21+ }, nil)
22+ if len(keepAlive) == 0 {
23+ t.Fatal("expected copied keepAlive value")
24+ }
25+ if got := *(*uint32)(unsafe.Pointer(ptr)); got != 7 {
26+ t.Fatalf("copied value = %#x, want %#x", got, uint32(7))
27+ }
28+}
29+
30+func TestARMPlaceRegistersReadsUnaddressableValue(t *testing.T) {
31+ type sample struct{ X uint32 }
32+ var ints []uintptr
33+ placeRegisters(reflect.ValueOf(sample{X: 7}), func(uintptr) {
34+ t.Fatal("unexpected float register")
35+ }, func(v uintptr) {
36+ ints = append(ints, v)
37+ })
38+ if len(ints) != 1 || ints[0] != 7 {
39+ t.Fatalf("placeRegisters() = %#v, want [7]", ints)
40+ }
41+}
@@ -0,0 +1,41 @@
1+//go:build arm && (freebsd || linux || netbsd || windows)
2+
3+package purego
4+
5+import (
6+ "reflect"
7+ "testing"
8+ "unsafe"
9+)
10+
11+func TestARMAddStructCopiesUnaddressableValue(t *testing.T) {
12+ type sample struct{ X uint32 }
13+ var numInts, numFloats, numStack int
14+ var ptr uintptr
15+ keepAlive := addStruct(reflect.ValueOf(sample{X: 7}), &numInts, &numFloats, &numStack, func(v uintptr) {
16+ ptr = v
17+ }, func(uintptr) {
18+ t.Fatal("unexpected float register")
19+ }, func(uintptr) {
20+ t.Fatal("unexpected stack slot")
21+ }, nil)
22+ if len(keepAlive) == 0 {
23+ t.Fatal("expected copied keepAlive value")
24+ }
25+ if got := *(*uint32)(unsafe.Pointer(ptr)); got != 7 {
26+ t.Fatalf("copied value = %#x, want %#x", got, uint32(7))
27+ }
28+}
29+
30+func TestARMPlaceRegistersReadsUnaddressableValue(t *testing.T) {
31+ type sample struct{ X uint32 }
32+ var ints []uintptr
33+ placeRegisters(reflect.ValueOf(sample{X: 7}), func(uintptr) {
34+ t.Fatal("unexpected float register")
35+ }, func(v uintptr) {
36+ ints = append(ints, v)
37+ })
38+ if len(ints) != 1 || ints[0] != 7 {
39+ t.Fatalf("placeRegisters() = %#v, want [7]", ints)
40+ }
41+}

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