testing/testrunner/utils.go (367 lines of code) (raw):
// SPDX-License-Identifier: Elastic-2.0
/*
* Copyright 2022 Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under
* one or more contributor license agreements. Licensed under the Elastic
* License 2.0; you may not use this file except in compliance with the Elastic
* License 2.0.
*/
package testrunner
import (
"bufio"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
// This is a JSON type printed by the test binaries (not by EventsTrace), it's
// used all over the place, so define it here to save space
type TestPidInfo struct {
Tid int64 `json:"tid"`
Tgid int64 `json:"tgid"`
Ppid int64 `json:"ppid"`
Pgid int64 `json:"pgid"`
Sid int64 `json:"sid"`
CapPermitted uint64 `json:"cap_permitted,string"`
CapEffective uint64 `json:"cap_effective,string"`
}
// Definitions of types printed by EventsTrace for conversion from JSON
type InitMsg struct {
InitSuccess bool `json:"probes_initialized"`
Features struct {
BpfTramp bool `json:"bpf_tramp"`
} `json:"features"`
}
type PidInfo struct {
Tid int64 `json:"tid"`
Tgid int64 `json:"tgid"`
Ppid int64 `json:"ppid"`
Pgid int64 `json:"pgid"`
Sid int64 `json:"sid"`
StartTimeNs int64 `json:"start_time_ns"`
}
type CredInfo struct {
Ruid int64 `json:"ruid"`
Rgid int64 `json:"rgid"`
Euid int64 `json:"euid"`
Egid int64 `json:"egid"`
Suid int64 `json:"suid"`
Sgid int64 `json:"sgid"`
CapPermitted uint64 `json:"cap_permitted,string"`
CapEffective uint64 `json:"cap_effective,string"`
}
type TtyInfo struct {
Major int64 `json:"major"`
Minor int64 `json:"minor"`
}
type NetInfo struct {
Transport string `json:"transport"`
Family string `json:"family"`
SourceAddr string `json:"source_address"`
SourcePort int64 `json:"source_port"`
DestAddr string `json:"destination_address"`
DestPort int64 `json:"destination_port"`
NetNs int64 `json:"network_namespace"`
}
type FileInfo struct {
Type string `json:"type"`
Inode uint64 `json:"inode"`
Mode uint64 `json:"mode"`
Size uint64 `json:"size"`
Uid uint64 `json:"uid"`
Gid uint64 `json:"gid"`
Mtime uint64 `json:"mtime"`
Ctime uint64 `json:"ctime"`
}
type NsInfo struct {
Uts uint32 `json:"uts"`
Ipc uint32 `json:"ipc"`
Mnt uint32 `json:"mnt"`
Net uint32 `json:"net"`
Cgroup uint32 `json:"cgroup"`
Time uint32 `json:"time"`
Pid uint32 `json:"pid"`
}
type ProcessForkEvent struct {
ParentPids PidInfo `json:"parent_pids"`
ChildPids PidInfo `json:"child_pids"`
Creds CredInfo `json:"creds"`
Ctty TtyInfo `json:"ctty"`
Ns NsInfo `json:"ns"`
}
type ProcessExecEvent struct {
Pids PidInfo `json:"pids"`
Creds CredInfo `json:"creds"`
Ctty TtyInfo `json:"ctty"`
IsSetUid bool `json:"is_setuid"`
IsSetGid bool `json:"is_setgid"`
IsMemfd bool `json:"is_memfd"`
InodeNlink uint64 `json:"inode_nlink"`
FileName string `json:"filename"`
Cwd string `json:"cwd"`
Argv []string `json:"argv"`
Env []string `json:"env"`
}
type ProcessKernelLoadModuleEvent struct {
Pids PidInfo `json:"pids"`
FileName string `json:"filename"`
ModVersion string `json:"mod_version"`
ModSrcVersion string `json:"mod_srcversion"`
}
type ProcessShmgetEvent struct {
Pids PidInfo `json:"pids"`
Key uint32 `json:"key"`
Size uint32 `json:"size"`
ShmFlg int64 `json:"shmflg"`
}
type MemfdCreateEvent struct {
Pids PidInfo `json:"pids"`
Flags uint32 `json:"flags"`
FlagCloexec bool `json:"flag_cloexec"`
FlagAllowSeal bool `json:"flag_allow_seal"`
FlagHugetlb bool `json:"flag_hugetlb"`
FlagNoexecSeal bool `json:"flag_noexec_seal"`
FlagExec bool `json:"flag_exec"`
FileName string `json:"filename"`
}
type ProcessPtraceEvent struct {
Pids PidInfo `json:"pids"`
ChildPid int64 `json:"child_pid"`
Request int64 `json:"request"`
}
type FileCreateEvent struct {
Pids PidInfo `json:"pids"`
Path string `json:"path"`
Finfo FileInfo `json:"file_info"`
}
type FileDeleteEvent struct {
Pids PidInfo `json:"pids"`
Path string `json:"path"`
Finfo FileInfo `json:"file_info"`
}
type FileModifyEvent struct {
Pids PidInfo `json:"pids"`
Path string `json:"path"`
ChangeType string `json:"change_type"`
Finfo FileInfo `json:"file_info"`
}
type FileRenameEvent struct {
Pids PidInfo `json:"pids"`
OldPath string `json:"old_path"`
NewPath string `json:"new_path"`
Finfo FileInfo `json:"file_info"`
}
type SetUidEvent struct {
Pids PidInfo `json:"pids"`
NewRuid int64 `json:"new_ruid"`
NewEuid int64 `json:"new_euid"`
}
type SetGidEvent struct {
Pids PidInfo `json:"pids"`
NewRgid int64 `json:"new_rgid"`
NewEgid int64 `json:"new_egid"`
}
type ttyDevInfo struct {
Major int64 `json:"major"`
Minor int64 `json:"minor"`
WinsizeRows int64 `json:"winsize_rows"`
WinsizeCols int64 `json:"winsize_cols"`
Termios_C_Iflag string `json:"termios_c_iflag"`
Termios_C_Oflag string `json:"termios_c_oflag"`
Termios_C_Lflag string `json:"termios_c_lflag"`
Termios_C_Cflag string `json:"termios_c_cflag"`
}
type TtyWriteEvent struct {
Pids PidInfo `json:"pids"`
Truncated int64 `json:"tty_out_truncated"`
Out string `json:"tty_out"`
TtyDev ttyDevInfo `json:"tty"`
}
type NetConnAttemptEvent struct {
Pids PidInfo `json:"pids"`
Net NetInfo `json:"net"`
Comm string `json:"comm"`
}
type NetConnAcceptEvent struct {
Pids PidInfo `json:"pids"`
Net NetInfo `json:"net"`
Comm string `json:"comm"`
}
type NetBinOut struct {
PidInfo TestPidInfo `json:"pid_info"`
ClientPort int64 `json:"client_port"`
ServerPort int64 `json:"server_port"`
NetNs int64 `json:"netns"`
}
type PtraceBinOut struct {
PtracePid int64 `json:"ptrace_pid"`
ChildPid int64 `json:"child_pid"`
Request int64 `json:"request"`
}
type ShmgetBinOut struct {
PidInfo TestPidInfo `json:"pid_info"`
Key uint32 `json:"key"`
Size uint32 `json:"size"`
ShmFlg int64 `json:"shmflg"`
}
type MemfdBinOut struct {
PidInfo TestPidInfo `json:"pid_info"`
Flags struct {
Value uint32 `json:"value"`
MfdCloexec bool `json:"mfd_cloexec"`
MfdAllowSealing bool `json:"mfd_allow_sealing"`
MfdHugetlb bool `json:"mfd_hugetlb"`
MfdNoexecSeal bool `json:"mfd_noexec_seal"`
MfdExec bool `json:"mfd_exec"`
} `json:"flags"`
FileName string `json:"filename"`
}
type NetConnCloseEvent struct {
Pids PidInfo `json:"pids"`
Net NetInfo `json:"net"`
Comm string `json:"comm"`
}
// path to the test binaries we use to create events for EventsTrace
var testBinaryPath = "/"
// path to the EventsTrace binary
var eventsTracePath = "/EventsTrace"
// Path to the TC filter test binary and probe. This one is weird and lives outside the rest of the test binaries
var (
tcTestPath = "/BPFTcFilterTests"
tcObjPath = "/TcFilter.bpf.o"
)
// init will run at startup and figure out if we're running in the bluebox test env or not,
// and set paths for the binaries as needed
func init() {
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
gitRootPath, err := cmd.CombinedOutput()
// if there's an error, assume that we're in the test environment,
// and we're using the root path
if err != nil {
fmt.Printf("using root path '%s' for test binary path\n", testBinaryPath)
return
}
// if we have a root path, create the path to test_bins
// convert GOARCH values to the gcc tuple values
var arch string
switch runtime.GOARCH {
case "amd64":
arch = "x86_64"
case "arm64":
arch = "aarch64"
default:
fmt.Printf("unsupported arch %s, reverting to root path for test binaries\n", runtime.GOARCH)
return
}
rootEbpfPath := strings.TrimSpace(string(gitRootPath))
testBinaryPath = filepath.Join(rootEbpfPath, "testing/test_bins/bin", arch)
fmt.Printf("using root path '%s' for binary path\n", testBinaryPath)
// if running in a non-root path, assume we're not in bluebox, set binary path accordingly
artifactDir := fmt.Sprintf("artifacts-%s", arch)
eventsTracePath = filepath.Join(rootEbpfPath, artifactDir, "package/bin/EventsTrace")
tcTestPath = filepath.Join(rootEbpfPath, artifactDir, "package/bin/BPFTcFilterTests")
tcObjPath = filepath.Join(rootEbpfPath, artifactDir, "package/probes/TcFilter.bpf.o")
fmt.Printf("using path '%s' for EventsTrace\n", eventsTracePath)
fmt.Printf("using path '%s' for BPFTcFilterTests\n", tcTestPath)
}
func getEventType(t *testing.T, jsonLine string) string {
var jsonUnmarshaled struct {
EventType string `json:"event_type"`
}
err := json.Unmarshal([]byte(jsonLine), &jsonUnmarshaled)
require.NoError(t, err, "error unmarshaling JSON to fetch event type")
return jsonUnmarshaled.EventType
}
func runTestBin(t *testing.T, binName string, args ...string) []byte {
cmd := exec.Command(filepath.Join(testBinaryPath, binName), args...)
output, err := cmd.CombinedOutput()
// the "correct" way to do this would be errors.Is(), but it doesn't seem to work reliably for the errors that exec returns
if err != nil {
if strings.Contains(err.Error(), "no such file") {
require.NoError(t, err, "test binary %s not found; try `make testbins` to compile test binaries", binName)
}
}
require.NoError(t, err, "error running command %s\n OUTPUT: \n %s", binName, string(output))
return output
}
func runTestUnmarshalOutput(t *testing.T, binName string, body any) {
raw := runTestBin(t, binName)
err := json.Unmarshal(raw, &body)
require.NoError(t, err, "error unmarshaling output from %s, got:\n %s", binName, string(raw))
}
func TestPidEqual(t *testing.T, tpi TestPidInfo, pi PidInfo) {
require.Equal(t, pi.Tid, tpi.Tid)
require.Equal(t, pi.Tgid, tpi.Tgid)
require.Equal(t, pi.Ppid, tpi.Ppid)
require.Equal(t, pi.Pgid, tpi.Pgid)
require.Equal(t, pi.Sid, tpi.Sid)
}
func IsOverlayFsSupported(t *testing.T) bool {
file, err := os.Open("/proc/filesystems")
require.NoError(t, err)
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasSuffix(line, "overlay") {
return true
}
}
err = scanner.Err()
require.NoError(t, err)
return false
}
func PrintBPFDebugOutput() {
file, err := os.Open("/sys/kernel/debug/tracing/trace")
if err != nil {
fmt.Printf("Could not open /sys/kernel/debug/tracing/trace: %s", err)
return
}
defer file.Close()
b, err := io.ReadAll(file)
if err != nil {
fmt.Printf("Could not read /sys/kernel/debug/tracing/trace: %s", err)
return
}
fmt.Print(string(b))
}
func PrintDebugOutputOnFail() {
fmt.Println("===== CONTENTS OF /sys/kernel/debug/tracing/trace =====")
PrintBPFDebugOutput()
fmt.Println("===== END CONTENTS OF /sys/kernel/debug/tracing/trace =====")
fmt.Print("\n")
fmt.Println("#######################################################################")
fmt.Println("# NOTE: /sys/kernel/debug/tracing/trace will only be populated if #")
fmt.Println("# -DBPF_ENABLE_PRINTK was set to true in the CMake build. #")
fmt.Println("# CI builds do NOT enable -DBPF_ENABLE_PRINTK for performance reasons #")
fmt.Println("#######################################################################")
fmt.Print("\n")
fmt.Println("BPF test failed, see errors and stacktrace above")
}
func FetchNsFromProc() (NsInfo, error) {
var ns NsInfo
fetch := func(name string, dst *uint32) error {
s, err := os.Readlink("/proc/self/ns/" + name)
if err != nil {
return err
}
start := strings.IndexByte(s, '[')
if start == -1 {
return fmt.Errorf("`[` not found for ns %s", name)
}
start++
end := strings.IndexByte(s, ']')
if end == -1 {
return fmt.Errorf("`]` not found for ns %s", name)
}
v, err := strconv.Atoi(s[start:end])
if err != nil {
return err
}
*dst = uint32(v)
return nil
}
calls := []struct {
name string
dst *uint32
}{
{"uts", &ns.Uts},
{"ipc", &ns.Ipc},
{"mnt", &ns.Mnt},
{"net", &ns.Net},
{"cgroup", &ns.Cgroup},
{"time", &ns.Time},
{"pid", &ns.Pid},
}
for _, call := range calls {
if err := fetch(call.name, call.dst); err != nil {
return ns, err
}
}
return ns, nil
}