cmd/cgo: fix unaligned arguments typedmemmove crash on iOS

Irregularly typedmemmove and bulkBarrierPreWrite crashes on unaligned
arguments. By aligning the arguments this is fixed.

Fixes #46893

Change-Id: I7beb9fdc31053fcb71bee6c6cb906dea31718c56
GitHub-Last-Rev: 46ae8b9688
GitHub-Pull-Request: golang/go#74868
Reviewed-on: https://go-review.googlesource.com/c/go/+/692935
Reviewed-by: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
This commit is contained in:
Tim Cooijmans 2025-09-30 21:53:11 +00:00 committed by t hepudds
parent 53845004d6
commit d5b950399d
3 changed files with 219 additions and 1 deletions

View file

@ -0,0 +1,144 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package out_test
import (
"bufio"
"bytes"
"fmt"
"internal/testenv"
"internal/goarch"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
)
type methodAlign struct {
Method string
Align int
}
var wantAligns = map[string]int{
"ReturnEmpty": 1,
"ReturnOnlyUint8": 1,
"ReturnOnlyUint16": 2,
"ReturnOnlyUint32": 4,
"ReturnOnlyUint64": goarch.PtrSize,
"ReturnOnlyInt": goarch.PtrSize,
"ReturnOnlyPtr": goarch.PtrSize,
"ReturnByteSlice": goarch.PtrSize,
"ReturnString": goarch.PtrSize,
"InputAndReturnUint8": 1,
"MixedTypes": goarch.PtrSize,
}
// TestAligned tests that the generated _cgo_export.c file has the wanted
// align attributes for struct types used as arguments or results of
// //exported functions.
func TestAligned(t *testing.T) {
testenv.MustHaveGoRun(t)
testenv.MustHaveCGO(t)
testdata, err := filepath.Abs("testdata")
if err != nil {
t.Fatal(err)
}
objDir := t.TempDir()
cmd := testenv.Command(t, testenv.GoToolPath(t), "tool", "cgo",
"-objdir", objDir,
filepath.Join(testdata, "aligned.go"))
cmd.Stderr = new(bytes.Buffer)
err = cmd.Run()
if err != nil {
t.Fatalf("%#q: %v\n%s", cmd, err, cmd.Stderr)
}
haveAligns, err := parseAlign(filepath.Join(objDir, "_cgo_export.c"))
if err != nil {
t.Fatal(err)
}
// Check that we have all the wanted methods
if len(haveAligns) != len(wantAligns) {
t.Fatalf("have %d methods with aligned, want %d", len(haveAligns), len(wantAligns))
}
for i := range haveAligns {
method := haveAligns[i].Method
haveAlign := haveAligns[i].Align
wantAlign, ok := wantAligns[method]
if !ok {
t.Errorf("method %s: have aligned %d, want missing entry", method, haveAlign)
} else if haveAlign != wantAlign {
t.Errorf("method %s: have aligned %d, want %d", method, haveAlign, wantAlign)
}
}
}
func parseAlign(filename string) ([]methodAlign, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
var results []methodAlign
scanner := bufio.NewScanner(file)
// Regex to match function declarations like "struct MethodName_return MethodName("
funcRegex := regexp.MustCompile(`^struct\s+(\w+)_return\s+(\w+)\(`)
// Regex to match simple function declarations like "GoSlice MethodName("
simpleFuncRegex := regexp.MustCompile(`^Go\w+\s+(\w+)\(`)
// Regex to match void-returning exported functions like "void ReturnEmpty("
voidFuncRegex := regexp.MustCompile(`^void\s+(\w+)\(`)
// Regex to match align attributes like "__attribute__((aligned(8)))"
alignRegex := regexp.MustCompile(`__attribute__\(\(aligned\((\d+)\)\)\)`)
var currentMethod string
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// Check if this line declares a function with struct return type
if matches := funcRegex.FindStringSubmatch(line); matches != nil {
currentMethod = matches[2] // Extract the method name
} else if matches := simpleFuncRegex.FindStringSubmatch(line); matches != nil {
// Check if this line declares a function with simple return type (like GoSlice)
currentMethod = matches[1] // Extract the method name
} else if matches := voidFuncRegex.FindStringSubmatch(line); matches != nil {
// Check if this line declares a void-returning function
currentMethod = matches[1] // Extract the method name
}
// Check if this line contains align information
if alignMatches := alignRegex.FindStringSubmatch(line); alignMatches != nil && currentMethod != "" {
alignStr := alignMatches[1]
align, err := strconv.Atoi(alignStr)
if err != nil {
// Skip this entry if we can't parse the align as integer
currentMethod = ""
continue
}
results = append(results, methodAlign{
Method: currentMethod,
Align: align,
})
currentMethod = "" // Reset for next method
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading file: %w", err)
}
return results, nil
}

View file

@ -0,0 +1,63 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import "C"
//export ReturnEmpty
func ReturnEmpty() {
return
}
//export ReturnOnlyUint8
func ReturnOnlyUint8() (uint8, uint8, uint8) {
return 1, 2, 3
}
//export ReturnOnlyUint16
func ReturnOnlyUint16() (uint16, uint16, uint16) {
return 1, 2, 3
}
//export ReturnOnlyUint32
func ReturnOnlyUint32() (uint32, uint32, uint32) {
return 1, 2, 3
}
//export ReturnOnlyUint64
func ReturnOnlyUint64() (uint64, uint64, uint64) {
return 1, 2, 3
}
//export ReturnOnlyInt
func ReturnOnlyInt() (int, int, int) {
return 1, 2, 3
}
//export ReturnOnlyPtr
func ReturnOnlyPtr() (*int, *int, *int) {
a, b, c := 1, 2, 3
return &a, &b, &c
}
//export ReturnString
func ReturnString() string {
return "hello"
}
//export ReturnByteSlice
func ReturnByteSlice() []byte {
return []byte{1, 2, 3}
}
//export InputAndReturnUint8
func InputAndReturnUint8(a, b, c uint8) (uint8, uint8, uint8) {
return a, b, c
}
//export MixedTypes
func MixedTypes(a uint8, b uint16, c uint32, d uint64, e int, f *int) (uint8, uint16, uint32, uint64, int, *int) {
return a, b, c, d, e, f
}

View file

@ -949,6 +949,8 @@ func (p *Package) writeExports(fgo2, fm, fgcc, fgcch io.Writer) {
fmt.Fprintf(gotype, "struct {\n") fmt.Fprintf(gotype, "struct {\n")
off := int64(0) off := int64(0)
npad := 0 npad := 0
// the align is at least 1 (for char)
maxAlign := int64(1)
argField := func(typ ast.Expr, namePat string, args ...interface{}) { argField := func(typ ast.Expr, namePat string, args ...interface{}) {
name := fmt.Sprintf(namePat, args...) name := fmt.Sprintf(namePat, args...)
t := p.cgoType(typ) t := p.cgoType(typ)
@ -963,6 +965,11 @@ func (p *Package) writeExports(fgo2, fm, fgcc, fgcch io.Writer) {
noSourceConf.Fprint(gotype, fset, typ) noSourceConf.Fprint(gotype, fset, typ)
fmt.Fprintf(gotype, "\n") fmt.Fprintf(gotype, "\n")
off += t.Size off += t.Size
// keep track of the maximum alignment among all fields
// so that we can align the struct correctly
if t.Align > maxAlign {
maxAlign = t.Align
}
} }
if fn.Recv != nil { if fn.Recv != nil {
argField(fn.Recv.List[0].Type, "recv") argField(fn.Recv.List[0].Type, "recv")
@ -1047,7 +1054,11 @@ func (p *Package) writeExports(fgo2, fm, fgcc, fgcch io.Writer) {
// string.h for memset, and is also robust to C++ // string.h for memset, and is also robust to C++
// types with constructors. Both GCC and LLVM optimize // types with constructors. Both GCC and LLVM optimize
// this into just zeroing _cgo_a. // this into just zeroing _cgo_a.
fmt.Fprintf(fgcc, "\ttypedef %s %v _cgo_argtype;\n", ctype.String(), p.packedAttribute()) //
// The struct should be aligned to the maximum alignment
// of any of its fields. This to avoid alignment
// issues.
fmt.Fprintf(fgcc, "\ttypedef %s %v __attribute__((aligned(%d))) _cgo_argtype;\n", ctype.String(), p.packedAttribute(), maxAlign)
fmt.Fprintf(fgcc, "\tstatic _cgo_argtype _cgo_zero;\n") fmt.Fprintf(fgcc, "\tstatic _cgo_argtype _cgo_zero;\n")
fmt.Fprintf(fgcc, "\t_cgo_argtype _cgo_a = _cgo_zero;\n") fmt.Fprintf(fgcc, "\t_cgo_argtype _cgo_a = _cgo_zero;\n")
if gccResult != "void" && (len(fntype.Results.List) > 1 || len(fntype.Results.List[0].Names) > 1) { if gccResult != "void" && (len(fntype.Results.List) > 1 || len(fntype.Results.List[0].Names) > 1) {