v0.11.0-alpha.8 d476432

syscall_linux_test.go RawHistoryFolder

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(t, "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}

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