detached ebd8567

purego: fix callback crash for arm64 (#117)

crosscall2 clobbers FP in the frame record. Use NOFRAME flag for callbackasm1
to disable the assembler generated epilogue that restores the corrupted FP.
Save and restore FP manually instead.

Co-authored-by: Erdi Chen <erdi@erdi.us>
commit
ebd8567e49db0b4514b146f222a59dd1acd618b7
author
erdichen · · 2023-03-27 17:33 UTC
committer
GitHub · · 2023-03-27 17:33 UTC
parent
09d3b2f
signature
GPG Unverified4AEE18F83AFDEB23 The key is unknown
3 files · +100 -8
A callback_test.go +74 -0
@@ -0,0 +1,74 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2023 The Ebitengine Authors
3+
4+//go:build darwin
5+
6+package purego_test
7+
8+import (
9+ "errors"
10+ "fmt"
11+ "os"
12+ "os/exec"
13+ "path/filepath"
14+ "strings"
15+ "testing"
16+ "unsafe"
17+
18+ "github.com/ebitengine/purego"
19+)
20+
21+// TestCallGoFromSharedLib is a test that checks for stack corruption on darwin/arm64
22+// when C calls Go code from a non-Go thread in a dynamically loaded share library.
23+func TestCallGoFromSharedLib(t *testing.T) {
24+ libFileName := filepath.Join(t.TempDir(), "libcbtest.so")
25+ t.Logf("Build %v", libFileName)
26+
27+ if err := buildSharedLib(libFileName, filepath.Join("libcbtest", "callback.c")); err != nil {
28+ t.Fatal(err)
29+ }
30+ defer os.Remove(libFileName)
31+
32+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
33+ if err != nil {
34+ t.Fatalf("Dlopen(%q) faled: %v", libFileName, err)
35+ }
36+
37+ var callCallback func(p uintptr, s string) int
38+ purego.RegisterLibFunc(&callCallback, lib, "callCallback")
39+
40+ goFunc := func(cstr *byte, n int) int {
41+ s := string(unsafe.Slice(cstr, n))
42+ t.Logf("FROM Go: %s\n", s)
43+ return 1
44+ }
45+
46+ const want = 10101
47+ cb := purego.NewCallback(goFunc)
48+ for i := 0; i < 10; i++ {
49+ got := callCallback(cb, "a test string")
50+ if got != want {
51+ t.Fatalf("%d: callCallback() got %v want %v", i, got, want)
52+ }
53+ }
54+}
55+
56+func buildSharedLib(libFile string, sources ...string) error {
57+ out, err := exec.Command("go", "env", "CC").Output()
58+ if err != nil {
59+ return fmt.Errorf("go env error: %w", err)
60+ }
61+
62+ cc := strings.TrimSpace(string(out))
63+ if cc == "" {
64+ return errors.New("CC not found")
65+ }
66+
67+ args := append([]string{"-shared", "-Wall", "-Werror", "-o", libFile}, sources...)
68+ cmd := exec.Command(cc, args...)
69+ if out, err := cmd.CombinedOutput(); err != nil {
70+ return fmt.Errorf("compile lib: %w\n%q\n%s", err, cmd, string(out))
71+ }
72+
73+ return nil
74+}
@@ -0,0 +1,74 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2023 The Ebitengine Authors
3+
4+//go:build darwin
5+
6+package purego_test
7+
8+import (
9+ "errors"
10+ "fmt"
11+ "os"
12+ "os/exec"
13+ "path/filepath"
14+ "strings"
15+ "testing"
16+ "unsafe"
17+
18+ "github.com/ebitengine/purego"
19+)
20+
21+// TestCallGoFromSharedLib is a test that checks for stack corruption on darwin/arm64
22+// when C calls Go code from a non-Go thread in a dynamically loaded share library.
23+func TestCallGoFromSharedLib(t *testing.T) {
24+ libFileName := filepath.Join(t.TempDir(), "libcbtest.so")
25+ t.Logf("Build %v", libFileName)
26+
27+ if err := buildSharedLib(libFileName, filepath.Join("libcbtest", "callback.c")); err != nil {
28+ t.Fatal(err)
29+ }
30+ defer os.Remove(libFileName)
31+
32+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
33+ if err != nil {
34+ t.Fatalf("Dlopen(%q) faled: %v", libFileName, err)
35+ }
36+
37+ var callCallback func(p uintptr, s string) int
38+ purego.RegisterLibFunc(&callCallback, lib, "callCallback")
39+
40+ goFunc := func(cstr *byte, n int) int {
41+ s := string(unsafe.Slice(cstr, n))
42+ t.Logf("FROM Go: %s\n", s)
43+ return 1
44+ }
45+
46+ const want = 10101
47+ cb := purego.NewCallback(goFunc)
48+ for i := 0; i < 10; i++ {
49+ got := callCallback(cb, "a test string")
50+ if got != want {
51+ t.Fatalf("%d: callCallback() got %v want %v", i, got, want)
52+ }
53+ }
54+}
55+
56+func buildSharedLib(libFile string, sources ...string) error {
57+ out, err := exec.Command("go", "env", "CC").Output()
58+ if err != nil {
59+ return fmt.Errorf("go env error: %w", err)
60+ }
61+
62+ cc := strings.TrimSpace(string(out))
63+ if cc == "" {
64+ return errors.New("CC not found")
65+ }
66+
67+ args := append([]string{"-shared", "-Wall", "-Werror", "-o", libFile}, sources...)
68+ cmd := exec.Command(cc, args...)
69+ if out, err := cmd.CombinedOutput(); err != nil {
70+ return fmt.Errorf("compile lib: %w\n%q\n%s", err, cmd, string(out))
71+ }
72+
73+ return nil
74+}
A libcbtest/callback.c +13 -0
@@ -0,0 +1,13 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2023 The Ebitengine Authors
3+
4+#include <string.h>
5+
6+typedef int (*callback)(const char *, int);
7+
8+int callCallback(const void *fp, const char *s) {
9+ // If the callback corrupts FP, this local variable on the stack will have incorrect value.
10+ int sentinel = 10101;
11+ ((callback)(fp))(s, strlen(s));
12+ return sentinel;
13+}
@@ -0,0 +1,13 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2023 The Ebitengine Authors
3+
4+#include <string.h>
5+
6+typedef int (*callback)(const char *, int);
7+
8+int callCallback(const void *fp, const char *s) {
9+ // If the callback corrupts FP, this local variable on the stack will have incorrect value.
10+ int sentinel = 10101;
11+ ((callback)(fp))(s, strlen(s));
12+ return sentinel;
13+}
M sys_arm64.s +13 -8
@@ -63,7 +63,7 @@ TEXT syscall9X(SB), NOSPLIT, $0
6363 FMOVD F0, syscall9Args_r2(R2) // save r2
6464 RET
6565
66-TEXT callbackasm1(SB), NOSPLIT, $208-0
66+TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
6767 NO_LOCAL_POINTERS
6868
6969 // On entry, the trampoline in zcallback_darwin_arm64.s left
@@ -71,18 +71,19 @@ TEXT callbackasm1(SB), NOSPLIT, $208-0
7171
7272 // Save callback register arguments R0-R7.
7373 // We do this at the top of the frame so they're contiguous with stack arguments.
74- // The 7*8 setting up R14 looks like a bug but is not: the eighth word
75- // is the space the assembler reserved for our caller's frame pointer,
76- // but we are not called from Go so that space is ours to use,
77- // and we must to be contiguous with the stack arguments.
78- MOVD $arg0-(7*8)(SP), R14
74+ SUB $(8*8), RSP, R14
7975 STP (R0, R1), (0*8)(R14)
8076 STP (R2, R3), (2*8)(R14)
8177 STP (R4, R5), (4*8)(R14)
8278 STP (R6, R7), (6*8)(R14)
8379
80+ // Adjust SP by frame size.
81+ // crosscall2 clobbers FP in the frame record so only save/restore SP.
82+ SUB $(28*8), RSP
83+ MOVD R30, (RSP)
84+
8485 // Create a struct callbackArgs on our stack.
85- MOVD $cbargs-(18*8+callbackArgs__size)(SP), R13
86+ ADD $(callbackArgs__size + 3*8), RSP, R13
8687 MOVD R12, callbackArgs_index(R13) // callback index
8788 MOVD R14, R0
8889 MOVD R0, callbackArgs_args(R13) // address of args vector
@@ -100,7 +101,11 @@ TEXT callbackasm1(SB), NOSPLIT, $208-0
100101 BL crosscall2(SB)
101102
102103 // Get callback result.
103- MOVD $cbargs-(18*8+callbackArgs__size)(SP), R13
104+ ADD $(callbackArgs__size + 3*8), RSP, R13
104105 MOVD callbackArgs_result(R13), R0
105106
107+ // Restore SP
108+ MOVD (RSP), R30
109+ ADD $(28*8), RSP
110+
106111 RET
@@ -63,7 +63,7 @@ TEXT syscall9X(SB), NOSPLIT, $0
63 FMOVD F0, syscall9Args_r2(R2) // save r263 FMOVD F0, syscall9Args_r2(R2) // save r2
64 RET64 RET
65 65
66-TEXT callbackasm1(SB), NOSPLIT, $208-066+TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
67 NO_LOCAL_POINTERS67 NO_LOCAL_POINTERS
68 68
69 // On entry, the trampoline in zcallback_darwin_arm64.s left69 // On entry, the trampoline in zcallback_darwin_arm64.s left
@@ -71,18 +71,19 @@ TEXT callbackasm1(SB), NOSPLIT, $208-0
71 71
72 // Save callback register arguments R0-R7.72 // Save callback register arguments R0-R7.
73 // We do this at the top of the frame so they're contiguous with stack arguments.73 // We do this at the top of the frame so they're contiguous with stack arguments.
74- // The 7*8 setting up R14 looks like a bug but is not: the eighth word74+ SUB $(8*8), RSP, R14
75- // is the space the assembler reserved for our caller's frame pointer,
76- // but we are not called from Go so that space is ours to use,
77- // and we must to be contiguous with the stack arguments.
78- MOVD $arg0-(7*8)(SP), R14
79 STP (R0, R1), (0*8)(R14)75 STP (R0, R1), (0*8)(R14)
80 STP (R2, R3), (2*8)(R14)76 STP (R2, R3), (2*8)(R14)
81 STP (R4, R5), (4*8)(R14)77 STP (R4, R5), (4*8)(R14)
82 STP (R6, R7), (6*8)(R14)78 STP (R6, R7), (6*8)(R14)
83 79
80+ // Adjust SP by frame size.
81+ // crosscall2 clobbers FP in the frame record so only save/restore SP.
82+ SUB $(28*8), RSP
83+ MOVD R30, (RSP)
84+
84 // Create a struct callbackArgs on our stack.85 // Create a struct callbackArgs on our stack.
85- MOVD $cbargs-(18*8+callbackArgs__size)(SP), R1386+ ADD $(callbackArgs__size + 3*8), RSP, R13
86 MOVD R12, callbackArgs_index(R13) // callback index87 MOVD R12, callbackArgs_index(R13) // callback index
87 MOVD R14, R088 MOVD R14, R0
88 MOVD R0, callbackArgs_args(R13) // address of args vector89 MOVD R0, callbackArgs_args(R13) // address of args vector
@@ -100,7 +101,11 @@ TEXT callbackasm1(SB), NOSPLIT, $208-0
100 BL crosscall2(SB)101 BL crosscall2(SB)
101 102
102 // Get callback result.103 // Get callback result.
103- MOVD $cbargs-(18*8+callbackArgs__size)(SP), R13104+ ADD $(callbackArgs__size + 3*8), RSP, R13
104 MOVD callbackArgs_result(R13), R0105 MOVD callbackArgs_result(R13), R0
105 106
107+ // Restore SP
108+ MOVD (RSP), R30
109+ ADD $(28*8), RSP
110+
106 RET111 RET

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