aboutsummaryrefslogtreecommitdiffhomepage
path: root/test/internal/testsuite
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-10-03 18:46:38 +0900
committerOphestra <cat@gensokyo.uk>2026-10-03 18:52:11 +0900
commitf2e188c4a0f472bcbd87189f63099d2c73936789 (patch)
tree1b96bd5bb8ae99460d93e6d477e5ee024a08b009 /test/internal/testsuite
parentaa41002e8078a3ab00896cbd403e573c6dd817c2 (diff)
test/internal/testsuite: move ptrace helpers
This also cleans up their API. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'test/internal/testsuite')
-rw-r--r--test/internal/testsuite/ptrace.go136
-rw-r--r--test/internal/testsuite/ptrace_test.go13
-rw-r--r--test/internal/testsuite/testsuite.go4
-rw-r--r--test/internal/testsuite/testsuite_root.go14
4 files changed, 163 insertions, 4 deletions
diff --git a/test/internal/testsuite/ptrace.go b/test/internal/testsuite/ptrace.go
new file mode 100644
index 00000000..4fcf1508
--- /dev/null
+++ b/test/internal/testsuite/ptrace.go
@@ -0,0 +1,136 @@
+package testsuite
+
+import (
+ "crypto/sha512"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "os"
+ "syscall"
+ "unsafe"
+)
+
+const (
+ // _PTRACE_ATTACH attaches to the process specified in pid.
+ _PTRACE_ATTACH = 16
+ // _PTRACE_DETACH restarts the stopped tracee as for PTRACE_CONT, but first
+ // detaches from it.
+ _PTRACE_DETACH = 17
+
+ // _PTRACE_SECCOMP_GET_FILTER allows the tracer to dump the tracee's classic
+ // BPF filters.
+ _PTRACE_SECCOMP_GET_FILTER = 0x420c
+)
+
+// ptrace wraps the ptrace syscall.
+func ptrace(
+ op uintptr,
+ pid, addr int,
+ data unsafe.Pointer,
+) (r uintptr, errno syscall.Errno) {
+ r, _, errno = syscall.Syscall6(
+ syscall.SYS_PTRACE,
+ op,
+ uintptr(pid),
+ uintptr(addr),
+ uintptr(data),
+ 0, 0,
+ )
+ return
+}
+
+// ptraceAttach attaches to the process referred to by pid.
+func ptraceAttach(pid int) error {
+ if _, errno := ptrace(_PTRACE_ATTACH, pid, 0, nil); errno != 0 {
+ return os.NewSyscallError("PTRACE_ATTACH", errno)
+ }
+
+ var status syscall.WaitStatus
+ for {
+ if _, err := syscall.Wait4(
+ pid,
+ &status,
+ syscall.WALL,
+ nil,
+ ); err != nil {
+ if errors.Is(err, syscall.EINTR) {
+ continue
+ }
+ return os.NewSyscallError("wait4", err)
+ }
+ break
+ }
+
+ return nil
+}
+
+// ptraceDetach detaches from the attached process referred to by pid.
+func ptraceDetach(pid int) error {
+ if _, errno := ptrace(_PTRACE_DETACH, pid, 0, nil); errno != 0 {
+ return os.NewSyscallError("PTRACE_DETACH", errno)
+ }
+ return nil
+}
+
+// getFilter dumps the specified tracee's cBPF filter at the specified index
+// and returns the resulting payload. T must be eight bytes long and must not
+// contain pointers.
+func getFilter(pid, index int) ([]syscall.SockFilter, error) {
+ var buf []syscall.SockFilter
+ if n, errno := ptrace(
+ _PTRACE_SECCOMP_GET_FILTER,
+ pid, index, nil,
+ ); errno != 0 {
+ return nil, os.NewSyscallError("PTRACE_SECCOMP_GET_FILTER", errno)
+ } else {
+ buf = make([]syscall.SockFilter, n)
+ }
+ if _, errno := ptrace(
+ _PTRACE_SECCOMP_GET_FILTER,
+ pid, index, unsafe.Pointer(&buf[0]),
+ ); errno != 0 {
+ return nil, os.NewSyscallError("PTRACE_SECCOMP_GET_FILTER", errno)
+ }
+ return buf, nil
+}
+
+// CheckFilter checks the process at pid to have its first filter's contents
+// match the sha512 checksum specified in hexadecimal string representation.
+func CheckFilter(pid, index int, sum string) (err error) {
+ if err = ptraceAttach(pid); err != nil {
+ return
+ }
+ defer func() {
+ if detachErr := ptraceDetach(pid); err == nil {
+ err = detachErr
+ }
+ }()
+
+ var (
+ buf []syscall.SockFilter
+ want []byte
+ )
+
+ if want, err = hex.DecodeString(sum); err != nil {
+ return
+ }
+
+ h := sha512.New()
+ if buf, err = getFilter(pid, index); err != nil {
+ return
+ } else {
+ h.Write(unsafe.Slice(
+ (*byte)(unsafe.Pointer(&buf[0])),
+ uintptr(len(buf))*unsafe.Sizeof(buf[0]),
+ ))
+ }
+
+ if got := h.Sum(nil); string(got) != string(want) {
+ return fmt.Errorf(
+ "bad filter\n\t got: %s\n\twant: %s",
+ hex.EncodeToString(got),
+ sum,
+ )
+ }
+ return
+}
diff --git a/test/internal/testsuite/ptrace_test.go b/test/internal/testsuite/ptrace_test.go
new file mode 100644
index 00000000..eaaed131
--- /dev/null
+++ b/test/internal/testsuite/ptrace_test.go
@@ -0,0 +1,13 @@
+package testsuite
+
+import (
+ "syscall"
+ "testing"
+ "unsafe"
+)
+
+func TestBlockSize(t *testing.T) {
+ if sz := unsafe.Sizeof(syscall.SockFilter{}); sz != 8 {
+ t.Fatalf("invalid filter block size %d", sz)
+ }
+}
diff --git a/test/internal/testsuite/testsuite.go b/test/internal/testsuite/testsuite.go
index 9233818d..2173509d 100644
--- a/test/internal/testsuite/testsuite.go
+++ b/test/internal/testsuite/testsuite.go
@@ -13,10 +13,6 @@ import (
func init() {
log.SetFlags(0)
log.SetPrefix("testsuite: ")
-
- if os.Geteuid() != 0 {
- log.Fatal("this program must run as root")
- }
}
// ReceiveSignals blocks until a termination signal arrives, and terminates.
diff --git a/test/internal/testsuite/testsuite_root.go b/test/internal/testsuite/testsuite_root.go
new file mode 100644
index 00000000..830e272c
--- /dev/null
+++ b/test/internal/testsuite/testsuite_root.go
@@ -0,0 +1,14 @@
+//go:build testsuite
+
+package testsuite
+
+import (
+ "log"
+ "os"
+)
+
+func init() {
+ if os.Geteuid() != 0 {
+ log.Fatal("this program must run as root")
+ }
+}