1// Copyright 2015 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package purego_test
6
7import (
8 "errors"
9 "fmt"
10 "os"
11 "path/filepath"
12 "runtime"
13 "sort"
14 "strings"
15 "syscall"
16 "testing"
17 "unsafe"
18
19 "github.com/ebitengine/purego"
20)
21
22func TestAllThreadsSyscall(t *testing.T) {
23 _, _, err := syscall.AllThreadsSyscall(syscall.SYS_FCNTL, 0, 0, 0)
24 if err != syscall.ENOTSUP {
25 t.Errorf("AllThreadsSyscall should return ENOTSUP, got: %v", err)
26 }
27}
28
29// TestSetuidEtc performs tests on all of the wrapped system calls
30// that mirror to the 9 glibc syscalls with POSIX semantics. The test
31// here is considered authoritative and should compile and run
32// CGO_ENABLED=0 or 1.
33func TestSetuidEtc(t *testing.T) {
34 vs := []struct {
35 call string
36 fn func() error
37 filter, expect string
38 }{
39 {call: "Setegid(1)", fn: func() error { return syscall.Setegid(1) }, filter: "Gid:", expect: "\t0\t1\t0\t1"},
40 {call: "Setegid(0)", fn: func() error { return syscall.Setegid(0) }, filter: "Gid:", expect: "\t0\t0\t0\t0"},
41
42 {call: "Seteuid(1)", fn: func() error { return syscall.Seteuid(1) }, filter: "Uid:", expect: "\t0\t1\t0\t1"},
43 {call: "Setuid(0)", fn: func() error { return syscall.Setuid(0) }, filter: "Uid:", expect: "\t0\t0\t0\t0"},
44
45 {call: "Setgid(1)", fn: func() error { return syscall.Setgid(1) }, filter: "Gid:", expect: "\t1\t1\t1\t1"},
46 {call: "Setgid(0)", fn: func() error { return syscall.Setgid(0) }, filter: "Gid:", expect: "\t0\t0\t0\t0"},
47
48 {call: "Setgroups([]int{0,1,2,3})", fn: func() error { return syscall.Setgroups([]int{0, 1, 2, 3}) }, filter: "Groups:", expect: "\t0 1 2 3"},
49 {call: "Setgroups(nil)", fn: func() error { return syscall.Setgroups(nil) }, filter: "Groups:", expect: ""},
50 {call: "Setgroups([]int{0})", fn: func() error { return syscall.Setgroups([]int{0}) }, filter: "Groups:", expect: "\t0"},
51
52 {call: "Setregid(101,0)", fn: func() error { return syscall.Setregid(101, 0) }, filter: "Gid:", expect: "\t101\t0\t0\t0"},
53 {call: "Setregid(0,102)", fn: func() error { return syscall.Setregid(0, 102) }, filter: "Gid:", expect: "\t0\t102\t102\t102"},
54 {call: "Setregid(0,0)", fn: func() error { return syscall.Setregid(0, 0) }, filter: "Gid:", expect: "\t0\t0\t0\t0"},
55
56 {call: "Setreuid(1,0)", fn: func() error { return syscall.Setreuid(1, 0) }, filter: "Uid:", expect: "\t1\t0\t0\t0"},
57 {call: "Setreuid(0,2)", fn: func() error { return syscall.Setreuid(0, 2) }, filter: "Uid:", expect: "\t0\t2\t2\t2"},
58 {call: "Setreuid(0,0)", fn: func() error { return syscall.Setreuid(0, 0) }, filter: "Uid:", expect: "\t0\t0\t0\t0"},
59
60 {call: "Setresgid(101,0,102)", fn: func() error { return syscall.Setresgid(101, 0, 102) }, filter: "Gid:", expect: "\t101\t0\t102\t0"},
61 {call: "Setresgid(0,102,101)", fn: func() error { return syscall.Setresgid(0, 102, 101) }, filter: "Gid:", expect: "\t0\t102\t101\t102"},
62 {call: "Setresgid(0,0,0)", fn: func() error { return syscall.Setresgid(0, 0, 0) }, filter: "Gid:", expect: "\t0\t0\t0\t0"},
63
64 {call: "Setresuid(1,0,2)", fn: func() error { return syscall.Setresuid(1, 0, 2) }, filter: "Uid:", expect: "\t1\t0\t2\t0"},
65 {call: "Setresuid(0,2,1)", fn: func() error { return syscall.Setresuid(0, 2, 1) }, filter: "Uid:", expect: "\t0\t2\t1\t2"},
66 {call: "Setresuid(0,0,0)", fn: func() error { return syscall.Setresuid(0, 0, 0) }, filter: "Uid:", expect: "\t0\t0\t0\t0"},
67 }
68
69 for i, v := range vs {
70 // Generate some thread churn.
71 c := make(chan struct{})
72 go killAThread(c)
73 close(c)
74
75 if err := v.fn(); err != nil {
76 if syscall.Getuid() == 0 && syscall.Getgid() == 0 {
77 // If root, then the syscall should have succeeded.
78 t.Errorf("[%d] %q setup failed: %v", i, v.call, err)
79 } else if !errors.Is(err, syscall.EPERM) {
80 // If not root, then EPERM is the only acceptable error.
81 t.Errorf("[%d] %q unexpected error: %v", i, v.call, err)
82 }
83 continue
84 }
85 if err := compareStatus(v.filter, v.expect); err != nil {
86 t.Errorf("[%d] %q comparison: %v", i, v.call, err)
87 }
88 }
89}
90
91// killAThread locks the goroutine to an OS thread and exits; this
92// causes an OS thread to terminate.
93func killAThread(c <-chan struct{}) {
94 runtime.LockOSThread()
95 <-c
96}
97
98// compareStatus is used to confirm the contents of the thread
99// specific status files match expectations.
100func compareStatus(filter, expect string) error {
101 expected := filter + expect
102 pid := syscall.Getpid()
103 fs, err := os.ReadDir(fmt.Sprintf("/proc/%d/task", pid))
104 if err != nil {
105 return fmt.Errorf("unable to find %d tasks: %v", pid, err)
106 }
107 expectedProc := fmt.Sprintf("Pid:\t%d", pid)
108 foundAThread := false
109 for _, f := range fs {
110 tf := fmt.Sprintf("/proc/%s/status", f.Name())
111 d, err := os.ReadFile(tf)
112 if err != nil {
113 // There are a surprising number of ways this
114 // can error out on linux. We've seen all of
115 // the following, so treat any error here as
116 // equivalent to the "process is gone":
117 // os.IsNotExist(err),
118 // "... : no such process",
119 // "... : bad file descriptor.
120 continue
121 }
122 lines := strings.Split(string(d), "\n")
123 for _, line := range lines {
124 // Different kernel vintages pad differently.
125 line = strings.TrimSpace(line)
126 if strings.HasPrefix(line, "Pid:\t") {
127 // On loaded systems, it is possible
128 // for a TID to be reused really
129 // quickly. As such, we need to
130 // validate that the thread status
131 // info we just read is a task of the
132 // same process PID as we are
133 // currently running, and not a
134 // recently terminated thread
135 // resurfaced in a different process.
136 if line != expectedProc {
137 break
138 }
139 // Fall through in the unlikely case
140 // that filter at some point is
141 // "Pid:\t".
142 }
143 if strings.HasPrefix(line, filter) {
144 if line == expected {
145 foundAThread = true
146 break
147 }
148 if filter == "Groups:" && strings.HasPrefix(line, "Groups:\t") {
149 // https://github.com/golang/go/issues/46145
150 // Containers don't reliably output this line in sorted order so manually sort and compare that.
151 a := strings.Split(line[8:], " ")
152 sort.Slice(a, func(i, j int) bool { return a[i] < a[j] })
153 got := strings.Join(a, " ")
154 if got == expected[8:] {
155 foundAThread = true
156 break
157 }
158
159 }
160 return fmt.Errorf("%q got:%q want:%q (bad) [pid=%d file:'%s' %v]\n", tf, line, expected, pid, string(d), expectedProc)
161 }
162 }
163 }
164 if !foundAThread {
165 return fmt.Errorf("found no thread /proc/<TID>/status files for process %q", expectedProc)
166 }
167 return nil
168}
169
170// TestDlopenThenAllThreadsSyscall reproduces a crash where loading a shared
171// library and calling a C function that invokes a Go callback, followed by
172// AllThreadsSyscall (used by Setuid etc.), causes a SIGSEGV on amd64.
173func TestDlopenThenAllThreadsSyscall(t *testing.T) {
174 // Step 1: Build and load a shared C library that calls back into Go.
175 libFileName := filepath.Join(t.TempDir(), "libcbtest.so")
176 if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_test.c")); err != nil {
177 t.Fatal(err)
178 }
179 defer os.Remove(libFileName)
180
181 lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
182 if err != nil {
183 t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
184 }
185
186 var callCallback func(p uintptr, s string) int
187 purego.RegisterLibFunc(&callCallback, lib, "callCallback")
188
189 goFunc := func(cstr *byte, n int) int {
190 _ = string(unsafe.Slice(cstr, n))
191 return 1
192 }
193
194 cb := purego.NewCallback(goFunc)
195 for i := 0; i < 10; i++ {
196 callCallback(cb, "hello")
197 }
198
199 // Step 2: Generate thread churn + AllThreadsSyscall (via Setuid).
200 for i := 0; i < 20; i++ {
201 c := make(chan struct{})
202 go func() {
203 runtime.LockOSThread()
204 <-c
205 }()
206 close(c)
207
208 if err := syscall.Setuid(0); err != nil {
209 if syscall.Getuid() == 0 && syscall.Getgid() == 0 {
210 // If root, then the syscall should have succeeded.
211 t.Errorf("setup failed: %v", err)
212 } else if !errors.Is(err, syscall.EPERM) {
213 // If not root, then EPERM is the only acceptable error.
214 t.Errorf("unexpected error: %v", err)
215 }
216 }
217 }
218}