diff --git a/bpf/Makefile b/bpf/Makefile index acc08de67ec..77ff1d2267e 100644 --- a/bpf/Makefile +++ b/bpf/Makefile @@ -34,7 +34,8 @@ PROCESS = bpf_execve_event.o bpf_execve_event_v53.o bpf_fork.o bpf_exit.o bpf_ge bpf_loader.o \ bpf_cgroup.o \ bpf_enforcer.o bpf_multi_enforcer.o bpf_fmodret_enforcer.o \ - bpf_map_test_p1.o bpf_map_test_p2.o bpf_map_test_p3.o + bpf_map_test_p1.o bpf_map_test_p2.o bpf_map_test_p3.o \ + bpf_prog_iter.o CGROUP = bpf_cgroup_mkdir.o bpf_cgroup_rmdir.o bpf_cgroup_release.o BPFTEST = bpf_lseek.o diff --git a/bpf/include/api.h b/bpf/include/api.h index c216b92bb66..6a133b10d31 100644 --- a/bpf/include/api.h +++ b/bpf/include/api.h @@ -273,6 +273,8 @@ static long BPF_FUNC(sock_ops_cb_flags_set, struct bpf_sock_ops *bpf_sock, int a static long BPF_FUNC(ima_file_hash, struct file *file, void *dst, uint32_t size); static long BPF_FUNC(ima_inode_hash, struct inode *inode, void *dst, uint32_t size); +static int BPF_FUNC(seq_write, struct seq_file *m, const void *data, uint32_t len); + /** LLVM built-ins, mem*() routines work for constant size */ #ifndef lock_xadd diff --git a/bpf/include/vmlinux.h b/bpf/include/vmlinux.h index b9c391429b0..8aa9348fa56 100644 --- a/bpf/include/vmlinux.h +++ b/bpf/include/vmlinux.h @@ -33383,6 +33383,15 @@ struct bpf_iter_seq_task_file_info { u32 fd; }; +struct bpf_iter__bpf_prog { + union { + struct bpf_iter_meta *meta; + }; + union { + struct bpf_prog *prog; + }; +}; + struct bpf_iter__task_file { union { struct bpf_iter_meta *meta; diff --git a/bpf/process/bpf_prog_iter.c b/bpf/process/bpf_prog_iter.c new file mode 100644 index 00000000000..cb2449d68f6 --- /dev/null +++ b/bpf/process/bpf_prog_iter.c @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +/* Copyright Authors of Cilium */ + +#include "vmlinux.h" +#include "bpf_helpers.h" + +SEC("iter/bpf_prog") +int iter(struct bpf_iter__bpf_prog *ctx) +{ + struct bpf_prog *prog = ctx->prog; + __u32 id; + + if (!prog) + return 0; + + _(id = prog->aux->id); + seq_write(ctx->meta->seq, &id, sizeof(id)); + return 0; +} + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; diff --git a/cmd/tetra/debug/debug.go b/cmd/tetra/debug/debug.go index c4456c4e1df..0206aeb2f8a 100644 --- a/cmd/tetra/debug/debug.go +++ b/cmd/tetra/debug/debug.go @@ -15,5 +15,6 @@ func New() *cobra.Command { } cmd.AddCommand(NewMapCmd()) cmd.AddCommand(NewDumpCommand()) + cmd.AddCommand(NewProgsCmd()) return &cmd } diff --git a/cmd/tetra/debug/progs.go b/cmd/tetra/debug/progs.go new file mode 100644 index 00000000000..136f74b9a41 --- /dev/null +++ b/cmd/tetra/debug/progs.go @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Tetragon + +package debug + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "io" + "log" + "os" + "path" + "path/filepath" + "runtime" + "sort" + "sync" + "text/tabwriter" + "time" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/btf" + "github.com/cilium/ebpf/link" + "github.com/spf13/cobra" + "golang.org/x/sys/unix" +) + +type prog struct { + id uint32 + name string + pin string + cnt uint64 + time time.Duration + alive bool +} + +type overhead struct { + p *prog + pct float64 + cnt uint64 + time time.Duration +} + +type progsConfig struct { + all bool + lib string + bpffs string + once bool + noclr bool + timeout int +} + +var ( + initOnce sync.Once + initErr error + initProg *ebpf.Program + cfg progsConfig +) + +func NewProgsCmd() *cobra.Command { + cmd := cobra.Command{ + Use: "progs", + Aliases: []string{"top"}, + Short: "Retrieve information about BPF programs on the host", + Long: `Retrieve information about BPF programs on the host. + +Examples: +- tetragon BPF programs top style + # tetra debug progs +- all BPF programs top style + # tetra debug progs --all +- one shot mode (displays one interval data) + # tetra debug progs --once +- change interval to 10 seconds + # tetra debug progs --timeout 10 +- change interval to 10 seconds in one shot mode + # tetra debug progs --once --timeout 10 +`, + + Run: func(_ *cobra.Command, _ []string) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // cfg.timeout is set by user in seconds unit, but let's convert + // it to nanoseconds, because it will be used like that below + cfg.timeout = int(time.Second) * cfg.timeout + + if err := runProgs(ctx); err != nil { + log.Fatal(err) + } + }, + } + + flags := cmd.Flags() + flags.BoolVar(&cfg.all, "all", false, "Get all programs") + flags.StringVar(&cfg.lib, "bpf-lib", "bpf/objs/", "Location of Tetragon libs (btf and bpf files)") + flags.StringVar(&cfg.bpffs, "bpf-dir", "/sys/fs/bpf/tetragon", "Location of bpffs tetragon directory") + flags.IntVar(&cfg.timeout, "timeout", 1, "Interval in seconds (delay in one shot mode)") + flags.BoolVar(&cfg.once, "once", false, "Run in one shot mode") + flags.BoolVar(&cfg.noclr, "no-clear", false, "Do not clear screen between rounds") + return &cmd +} + +func runProgs(ctx context.Context) error { + // Enable bpf stats + stats, err := ebpf.EnableStats(uint32(unix.BPF_STATS_RUN_TIME)) + if err != nil { + return fmt.Errorf("failed to enable stats: %v", err) + } + defer stats.Close() + + state := make(map[uint32]*prog) + + // Gather initial data + if err = round(state); err != nil { + return err + } + + // and cycle.. + ticker := time.NewTicker(time.Duration(cfg.timeout)) + defer ticker.Stop() + + for ctx.Err() == nil { + <-ticker.C + if !cfg.noclr && !cfg.once { + clearScreen() + } + if err = round(state); err != nil { + return err + } + if cfg.once { + return nil + } + } + return err +} + +func round(state map[uint32]*prog) error { + // Get BPF programs + progs, err := getProgs(cfg.all, cfg.lib, cfg.bpffs) + if err != nil { + return err + } + + // Get BPF programs overheads + overheads, err := getOverheads(state, progs) + if err != nil || len(overheads) == 0 { + return err + } + + // Compute overheads + for _, ovh := range overheads { + var pct float64 + + if ovh.time != 0 { + pct = float64(ovh.time) / float64(cfg.timeout*runtime.NumCPU()) * 100 + } + ovh.pct = pct + } + + // Sort by overhead percentage + sort.Slice(overheads, func(i, j int) bool { + return overheads[i].pct > overheads[j].pct + }) + + // And dump it to the terminal, time.. + fmt.Println(time.Now().String()) + fmt.Println("") + + // ..and overhead + writer := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', tabwriter.AlignRight) + fmt.Fprintln(writer, "Ovh(%)\tId\tCnt\tTime\tName\tPin") + + for _, ovh := range overheads { + p := ovh.p + fmt.Fprintf(writer, "%6.2f\t%d\t%d\t%d\t%s\t%s\n", + ovh.pct, p.id, ovh.cnt, ovh.time, p.name, p.pin) + } + + fmt.Fprintln(writer) + writer.Flush() + + // Remove stale programs from state map + for id, p := range state { + if p.alive { + p.alive = false + } else { + delete(state, id) + } + } + return nil +} + +func clearScreen() { + fmt.Print("\033[2J") + fmt.Print("\033[H") +} + +func getOverheads(state map[uint32]*prog, progs []*prog) ([]*overhead, error) { + var overheads []*overhead + + for _, p := range progs { + old, ok := state[p.id] + if !ok { + state[p.id] = p + continue + } + ovh := &overhead{ + p: p, + cnt: p.cnt - old.cnt, + time: p.time - old.time, + } + overheads = append(overheads, ovh) + *old = *p + } + + return overheads, nil +} + +func getProgs(all bool, libDir, mapDir string) ([]*prog, error) { + if all { + return getAllProgs(libDir) + } + + return getTetragonProgs(mapDir) +} + +func getAllProgs(lib string) ([]*prog, error) { + // Open the object file just once + initOnce.Do(func() { + file := path.Join(lib, "bpf_prog_iter.o") + spec, err := ebpf.LoadCollectionSpec(file) + if err != nil { + initErr = err + return + } + + coll, err := ebpf.NewCollection(spec) + if err != nil { + initErr = err + return + } + defer coll.Close() + + prog, ok := coll.Programs["iter"] + if !ok { + initErr = fmt.Errorf("can't file iter program") + return + } + initProg, initErr = prog.Clone() + }) + + if initErr != nil { + return nil, initErr + } + + // Setup the iterator + it, err := link.AttachIter(link.IterOptions{ + Program: initProg, + }) + if err != nil { + return nil, err + } + defer it.Close() + + rd, err := it.Open() + if err != nil { + return nil, err + } + defer rd.Close() + + var ( + progs []*prog + id uint32 + ) + + // Read all the IDs + for { + err = binary.Read(rd, binary.LittleEndian, &id) + if err != nil { + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { + break + } + } + p, err := getProg(id) + if err != nil { + return nil, err + } + + progs = append(progs, p) + } + + return progs, nil +} + +func getProg(id uint32) (*prog, error) { + p, err := ebpf.NewProgramFromID(ebpf.ProgramID(id)) + if err != nil { + return nil, err + } + defer p.Close() + + info, err := p.Info() + if err != nil { + return nil, err + } + + runTime, _ := info.Runtime() + runCnt, _ := info.RunCount() + + return &prog{ + id: id, + name: info.Name, + pin: "-", + cnt: runCnt, + time: runTime, + alive: true, + }, nil +} + +func getTetragonProgs(base string) ([]*prog, error) { + var progs []*prog + + // Walk bpffs/tetragon and look for programs + err := filepath.Walk(base, + func(path string, finfo os.FileInfo, err error) error { + if err != nil { + return err + } + if finfo.IsDir() { + return nil + } + p, err := ebpf.LoadPinnedProgram(path, nil) + if err != nil { + return err + } + defer p.Close() + + if !isProg(p.FD()) { + return nil + } + + info, err := p.Info() + if err != nil { + return err + } + + id, ok := info.ID() + if !ok { + return err + } + + runTime, _ := info.Runtime() + runCnt, _ := info.RunCount() + + progs = append(progs, &prog{ + id: uint32(id), + name: getName(p, info), + pin: path, + cnt: runCnt, + time: runTime, + alive: true, + }) + return nil + }) + return progs, err +} + +func isProg(fd int) bool { + return isBPFObject("prog", fd) +} + +func isBPFObject(object string, fd int) bool { + readlink, err := os.Readlink(fmt.Sprintf("/proc/self/fd/%d", fd)) + if err != nil { + return false + } + return readlink == fmt.Sprintf("anon_inode:bpf-%s", object) +} + +func getName(p *ebpf.Program, info *ebpf.ProgramInfo) string { + handle, err := p.Handle() + if err != nil { + return info.Name + } + + spec, err := handle.Spec(nil) + if err != nil { + return info.Name + } + + iter := spec.Iterate() + for iter.Next() { + if fn, ok := iter.Type.(*btf.Func); ok { + return fn.Name + } + } + return info.Name +} diff --git a/pkg/sensors/tracing/enforcer.go b/pkg/sensors/tracing/enforcer.go index e5117f7bdb8..b576bef93cd 100644 --- a/pkg/sensors/tracing/enforcer.go +++ b/pkg/sensors/tracing/enforcer.go @@ -291,7 +291,8 @@ func (kp *enforcerPolicy) createEnforcerSensor( label, "kprobe", "enforcer"). - SetLoaderData(policyName) + SetLoaderData(policyName). + SetPolicy(policyName) progs = append(progs, load) case OverrideMethodFmodRet: @@ -304,7 +305,8 @@ func (kp *enforcerPolicy) createEnforcerSensor( "fmod_ret/security_task_prctl", fmt.Sprintf("fmod_ret_%s", syscallSym), "enforcer"). - SetLoaderData(policyName) + SetLoaderData(policyName). + SetPolicy(policyName) progs = append(progs, load) } default: diff --git a/pkg/sensors/tracing/genericlsm.go b/pkg/sensors/tracing/genericlsm.go index 78ede697b53..b5120807493 100644 --- a/pkg/sensors/tracing/genericlsm.go +++ b/pkg/sensors/tracing/genericlsm.go @@ -443,7 +443,8 @@ func createLsmSensorFromEntry(lsmEntry *genericLsm, "lsm/generic_lsm_output", lsmEntry.hook, "generic_lsm"). - SetLoaderData(lsmEntry.tableId) + SetLoaderData(lsmEntry.tableId). + SetPolicy(lsmEntry.policyName) progs = append(progs, loadOutput) load := program.Builder( @@ -452,7 +453,8 @@ func createLsmSensorFromEntry(lsmEntry *genericLsm, "lsm/generic_lsm_core", lsmEntry.hook, "generic_lsm"). - SetLoaderData(lsmEntry.tableId) + SetLoaderData(lsmEntry.tableId). + SetPolicy(lsmEntry.policyName) // Load ima program for hash calculating if lsmEntry.imaProgLoad { @@ -465,7 +467,8 @@ func createLsmSensorFromEntry(lsmEntry *genericLsm, "lsm.s/generic_lsm_ima_"+loadProgImaType, lsmEntry.hook, "generic_lsm"). - SetLoaderData(lsmEntry.tableId) + SetLoaderData(lsmEntry.tableId). + SetPolicy(lsmEntry.policyName) progs = append(progs, loadIma) imaHashMap := program.MapBuilderProgram("ima_hash_map", loadIma) maps = append(maps, imaHashMap) diff --git a/pkg/sensors/tracing/generictracepoint.go b/pkg/sensors/tracing/generictracepoint.go index 667c0149c26..d0cb760b3ce 100644 --- a/pkg/sensors/tracing/generictracepoint.go +++ b/pkg/sensors/tracing/generictracepoint.go @@ -400,7 +400,7 @@ func createGenericTracepointSensor( "tracepoint/generic_tracepoint", pinProg, "generic_tracepoint", - ) + ).SetPolicy(policyName) err := tp.InitKernelSelectors(lists) if err != nil { diff --git a/pkg/sensors/tracing/genericuprobe.go b/pkg/sensors/tracing/genericuprobe.go index 2df495f6b73..34a679f7d8f 100644 --- a/pkg/sensors/tracing/genericuprobe.go +++ b/pkg/sensors/tracing/genericuprobe.go @@ -277,7 +277,7 @@ func createGenericUprobeSensor( } if in.useMulti { - progs, maps, err = createMultiUprobeSensor(name, ids) + progs, maps, err = createMultiUprobeSensor(name, ids, policyName) } else { progs, maps, err = createSingleUprobeSensor(ids) } @@ -397,7 +397,7 @@ func multiUprobePinPath(sensorPath string) string { return sensors.PathJoin(sensorPath, "multi_kprobe") } -func createMultiUprobeSensor(sensorPath string, multiIDs []idtable.EntryID) ([]*program.Program, []*program.Map, error) { +func createMultiUprobeSensor(sensorPath string, multiIDs []idtable.EntryID, policyName string) ([]*program.Program, []*program.Map, error) { var progs []*program.Program var maps []*program.Map @@ -407,11 +407,12 @@ func createMultiUprobeSensor(sensorPath string, multiIDs []idtable.EntryID) ([]* load := program.Builder( path.Join(option.Config.HubbleLib, loadProgName), - fmt.Sprintf("%d functions", len(multiIDs)), + fmt.Sprintf("uprobe_multi (%d functions)", len(multiIDs)), "uprobe.multi/generic_uprobe", pinPath, "generic_uprobe"). - SetLoaderData(multiIDs) + SetLoaderData(multiIDs). + SetPolicy(policyName) progs = append(progs, load) @@ -460,12 +461,13 @@ func createUprobeSensorFromEntry(uprobeEntry *genericUprobe, load := program.Builder( path.Join(option.Config.HubbleLib, loadProgName), - "", + fmt.Sprintf("%s %s", uprobeEntry.path, uprobeEntry.symbol), "uprobe/generic_uprobe", fmt.Sprintf("%d-%s", uprobeEntry.tableId.ID, uprobeEntry.symbol), "generic_uprobe"). SetAttachData(attachData). - SetLoaderData(uprobeEntry) + SetLoaderData(uprobeEntry). + SetPolicy(uprobeEntry.policyName) progs = append(progs, load)