diff --git a/pkg/ebpf/c/bpf_bypass.h b/pkg/ebpf/c/bpf_bypass.h index 6ab05047d2264..1fd6b8ecddc25 100644 --- a/pkg/ebpf/c/bpf_bypass.h +++ b/pkg/ebpf/c/bpf_bypass.h @@ -48,4 +48,23 @@ static __always_inline typeof(name(0)) ____##name(struct pt_regs *ctx, ##args) #define BPF_BYPASSABLE_UPROBE(name, args...) BPF_BYPASSABLE_KPROBE(name, ##args) #define BPF_BYPASSABLE_URETPROBE(name, args...) BPF_BYPASSABLE_KRETPROBE(name, ##args) +/* BPF_BYPASSABLE_PROG is identical to BPF_PROG (bpf_tracing.h), but with a stub (CHECK_BPF_PROGRAM_BYPASSED) + * that checks if the program is bypassed. This is useful for testings, as we want to dynamically control + * the execution of the program. + */ +#define BPF_BYPASSABLE_PROG(name, args...) \ +name(unsigned long long *ctx); \ +static __always_inline typeof(name(0)) \ +____##name(unsigned long long *ctx, ##args); \ +typeof(name(0)) name(unsigned long long *ctx) \ +{ \ + CHECK_BPF_PROGRAM_BYPASSED() \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ + return ____##name(___bpf_ctx_cast(args)); \ + _Pragma("GCC diagnostic pop") \ +} \ +static __always_inline typeof(name(0)) \ +____##name(unsigned long long *ctx, ##args) + #endif diff --git a/pkg/network/ebpf/c/shared-libraries/probes.h b/pkg/network/ebpf/c/shared-libraries/probes.h index de2db6d9af5f3..bfa1d004748aa 100644 --- a/pkg/network/ebpf/c/shared-libraries/probes.h +++ b/pkg/network/ebpf/c/shared-libraries/probes.h @@ -18,42 +18,35 @@ static __always_inline void fill_path_safe(lib_path_t *path, const char *path_ar } } -static __always_inline void do_sys_open_helper_enter(const char *filename) { - lib_path_t path = { 0 }; - if (bpf_probe_read_user_with_telemetry(path.buf, sizeof(path.buf), filename) >= 0) { +static __always_inline bool fill_lib_path(lib_path_t *path, const char *path_argument) { + path->pid = GET_USER_MODE_PID(bpf_get_current_pid_tgid()); + if (bpf_probe_read_user_with_telemetry(path->buf, sizeof(path->buf), path_argument) >= 0) { // Find the null character and clean up the garbage following it #pragma unroll for (int i = 0; i < LIB_PATH_MAX_SIZE; i++) { - if (path.buf[i] == 0) { - path.len = i; + if (path->buf[i] == 0) { + path->len = i; break; } } } else { - fill_path_safe(&path, filename); - } - - // Bail out if the path size is larger than our buffer - if (!path.len) { - return; + fill_path_safe(path, path_argument); } - u64 pid_tgid = bpf_get_current_pid_tgid(); - path.pid = GET_USER_MODE_PID(pid_tgid); - bpf_map_update_with_telemetry(open_at_args, &pid_tgid, &path, BPF_ANY); - return; + return path->len > 0; } -static __always_inline void do_sys_open_helper_exit(exit_sys_ctx *args) { - u64 pid_tgid = bpf_get_current_pid_tgid(); - - // If file couldn't be opened, bail out - if (args->ret < 0) { - goto cleanup; +static __always_inline void do_sys_open_helper_enter(const char *filename) { + lib_path_t path = { 0 }; + if (fill_lib_path(&path, filename)) { + u64 pid_tgid = bpf_get_current_pid_tgid(); + bpf_map_update_with_telemetry(open_at_args, &pid_tgid, &path, BPF_ANY); } + return; +} - lib_path_t *path = bpf_map_lookup_elem(&open_at_args, &pid_tgid); - if (path == NULL) { +static __always_inline void push_event_if_relevant(void *ctx, lib_path_t *path, long return_code) { + if (return_code < 0) { return; } @@ -79,25 +72,33 @@ static __always_inline void do_sys_open_helper_exit(exit_sys_ctx *args) { } } if (!is_shared_library) { - goto cleanup; + return; } u64 crypto_libset_enabled = 0; LOAD_CONSTANT("crypto_libset_enabled", crypto_libset_enabled); if (crypto_libset_enabled && (match6chars(0, 'l', 'i', 'b', 's', 's', 'l') || match6chars(0, 'c', 'r', 'y', 'p', 't', 'o') || match6chars(0, 'g', 'n', 'u', 't', 'l', 's'))) { - bpf_perf_event_output((void *)args, &crypto_shared_libraries, BPF_F_CURRENT_CPU, path, sizeof(lib_path_t)); - goto cleanup; + bpf_perf_event_output(ctx, &crypto_shared_libraries, BPF_F_CURRENT_CPU, path, sizeof(lib_path_t)); + return; } u64 gpu_libset_enabled = 0; LOAD_CONSTANT("gpu_libset_enabled", gpu_libset_enabled); if (gpu_libset_enabled && (match6chars(0, 'c', 'u', 'd', 'a', 'r', 't'))) { - bpf_perf_event_output((void *)args, &gpu_shared_libraries, BPF_F_CURRENT_CPU, path, sizeof(lib_path_t)); + bpf_perf_event_output(ctx, &gpu_shared_libraries, BPF_F_CURRENT_CPU, path, sizeof(lib_path_t)); + } +} + +static __always_inline void do_sys_open_helper_exit(exit_sys_ctx *args) { + u64 pid_tgid = bpf_get_current_pid_tgid(); + lib_path_t *path = bpf_map_lookup_elem(&open_at_args, &pid_tgid); + if (path == NULL) { + return; } -cleanup: + push_event_if_relevant(args, path, args->ret); bpf_map_delete_elem(&open_at_args, &pid_tgid); return; } @@ -153,8 +154,15 @@ int tracepoint__syscalls__sys_exit_openat(exit_sys_ctx *args) { SEC("tracepoint/syscalls/sys_enter_openat2") int tracepoint__syscalls__sys_enter_openat2(enter_sys_openat2_ctx *args) { CHECK_BPF_PROGRAM_BYPASSED() - // Unlike the other variants, openat2(2) has the flags embedded inside the - // how argument; we don't bother trying to accessing it for now. + + if (args->how != NULL) { + __u64 flags = 0; + bpf_probe_read_user(&flags, sizeof(flags), &args->how->flags); + if (should_ignore_flags(flags)) { + return 0; + } + } + do_sys_open_helper_enter(args->filename); return 0; } @@ -166,4 +174,17 @@ int tracepoint__syscalls__sys_exit_openat2(exit_sys_ctx *args) { return 0; } +SEC("fexit/do_sys_openat2") +int BPF_BYPASSABLE_PROG(do_sys_openat2_exit, int dirfd, const char *pathname, openat2_open_how *how, long ret) { + if (how != NULL && should_ignore_flags(how->flags)) { + return 0; + } + + lib_path_t path = { 0 }; + if (fill_lib_path(&path, pathname)) { + push_event_if_relevant(ctx, &path, ret); + } + return 0; +} + #endif diff --git a/pkg/network/ebpf/c/shared-libraries/types.h b/pkg/network/ebpf/c/shared-libraries/types.h index ea2159d5d07eb..2680088c0cc64 100644 --- a/pkg/network/ebpf/c/shared-libraries/types.h +++ b/pkg/network/ebpf/c/shared-libraries/types.h @@ -37,6 +37,12 @@ typedef struct { int mode; } enter_sys_openat_ctx; +typedef struct { + __u64 flags; + __u64 mode; + __u64 resolve; +} openat2_open_how; + typedef struct { unsigned short common_type; unsigned char common_flags; @@ -46,7 +52,7 @@ typedef struct { int dfd; const char* filename; - void *how; + openat2_open_how *how; size_t usize; } enter_sys_openat2_ctx; diff --git a/pkg/network/usm/sharedlibraries/ebpf.go b/pkg/network/usm/sharedlibraries/ebpf.go index e583b6886c0f9..085f429cb2ead 100644 --- a/pkg/network/usm/sharedlibraries/ebpf.go +++ b/pkg/network/usm/sharedlibraries/ebpf.go @@ -15,6 +15,11 @@ import ( "strings" "sync" + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/features" + "github.com/cilium/ebpf/link" + manager "github.com/DataDog/ebpf-manager" ddebpf "github.com/DataDog/datadog-agent/pkg/ebpf" @@ -105,6 +110,11 @@ type EbpfProgram struct { // otherwise used to check if the program needs to be stopped and re-started // when adding new libsets isInitialized bool + + // enabledProbes is a list of the probes that are enabled for the current system. + enabledProbes []manager.ProbeIdentificationPair + // disabledProbes is a list of the probes that are disabled for the current system. + disabledProbes []manager.ProbeIdentificationPair } // IsSupported returns true if the shared libraries monitoring is supported on the current system. @@ -196,8 +206,8 @@ func (e *EbpfProgram) setupManagerAndPerfHandlers() { handler.perfHandler = perfHandler } - probeIDs := getSysOpenHooksIdentifiers() - for _, identifier := range probeIDs { + e.initializeProbes() + for _, identifier := range e.enabledProbes { mgr.Probes = append(mgr.Probes, &manager.Probe{ ProbeIdentificationPair: identifier, @@ -480,13 +490,16 @@ func (e *EbpfProgram) stopImpl() { func (e *EbpfProgram) init(buf bytecode.AssetReader, options manager.Options) error { options.RemoveRlimit = true - for _, probe := range e.Probes { + for _, probe := range e.enabledProbes { options.ActivatedProbes = append(options.ActivatedProbes, &manager.ProbeSelector{ - ProbeIdentificationPair: probe.ProbeIdentificationPair, + ProbeIdentificationPair: probe, }, ) } + for _, probe := range e.disabledProbes { + options.ExcludedFunctions = append(options.ExcludedFunctions, probe.EBPFFuncName) + } var enabledMsgs []string for libset := range LibsetToLibSuffixes { @@ -537,25 +550,60 @@ func (e *EbpfProgram) initPrebuilt() error { func sysOpenAt2Supported() bool { missing, err := ddebpf.VerifyKernelFuncs("do_sys_openat2") - if err == nil && len(missing) == 0 { - return true + return err == nil && len(missing) == 0 +} + +// fexitSupported checks if fexit type of probe is supported on the current host. +// It does this by creating a dummy program that attaches to the given function name, and returns true if it succeeds. +// Method was adapted from the CWS code. +func fexitSupported(funcName string) bool { + if features.HaveProgramType(ebpf.Tracing) != nil { + return false } - kversion, err := kernel.HostVersion() + spec := &ebpf.ProgramSpec{ + Type: ebpf.Tracing, + AttachType: ebpf.AttachTraceFExit, + AttachTo: funcName, + Instructions: asm.Instructions{ + asm.LoadImm(asm.R0, 0, asm.DWord), + asm.Return(), + }, + } + prog, err := ebpf.NewProgramWithOptions(spec, ebpf.ProgramOptions{ + LogDisabled: true, + }) + if err != nil { + return false + } + defer prog.Close() + l, err := link.AttachTracing(link.TracingOptions{ + Program: prog, + }) if err != nil { - log.Error("could not determine the current kernel version. fallback to do_sys_open") return false } + defer l.Close() - return kversion >= kernel.VersionCode(5, 6, 0) + return true } -// getSysOpenHooksIdentifiers returns the enter and exit tracepoints for supported open* -// system calls. -func getSysOpenHooksIdentifiers() []manager.ProbeIdentificationPair { +// initializedProbes initializes the probes that are enabled for the current system +func (e *EbpfProgram) initializeProbes() { + openat2Supported := sysOpenAt2Supported() + isFexitSupported := fexitSupported("do_sys_openat2") + + // Tracing represents fentry/fexit probes. + tracingProbes := []manager.ProbeIdentificationPair{ + { + EBPFFuncName: fmt.Sprintf("do_sys_%s_exit", openat2SysCall), + UID: probeUID, + }, + } + openatProbes := []string{openatSysCall} - if sysOpenAt2Supported() { + if openat2Supported { openatProbes = append(openatProbes, openat2SysCall) } // amd64 has open(2), arm64 doesn't @@ -563,17 +611,24 @@ func getSysOpenHooksIdentifiers() []manager.ProbeIdentificationPair { openatProbes = append(openatProbes, openSysCall) } - res := make([]manager.ProbeIdentificationPair, 0, len(traceTypes)*len(openatProbes)) + // tp stands for tracepoints, which is the older format of the probes. + tpProbes := make([]manager.ProbeIdentificationPair, 0, len(traceTypes)*len(openatProbes)) for _, probe := range openatProbes { for _, traceType := range traceTypes { - res = append(res, manager.ProbeIdentificationPair{ + tpProbes = append(tpProbes, manager.ProbeIdentificationPair{ EBPFFuncName: fmt.Sprintf("tracepoint__syscalls__sys_%s_%s", traceType, probe), UID: probeUID, }) } } - return res + if isFexitSupported && openat2Supported { + e.enabledProbes = tracingProbes + e.disabledProbes = tpProbes + } else { + e.enabledProbes = tpProbes + e.disabledProbes = tracingProbes + } } func getAssetName(module string, debug bool) string { diff --git a/pkg/network/usm/sharedlibraries/watcher_test.go b/pkg/network/usm/sharedlibraries/watcher_test.go index d55635fd2ecc7..fedcdbede3f1d 100644 --- a/pkg/network/usm/sharedlibraries/watcher_test.go +++ b/pkg/network/usm/sharedlibraries/watcher_test.go @@ -17,12 +17,14 @@ import ( "regexp" "strings" "sync" + "syscall" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "golang.org/x/sys/unix" "github.com/DataDog/datadog-agent/pkg/ebpf/ebpftest" "github.com/DataDog/datadog-agent/pkg/ebpf/prebuilt" @@ -104,40 +106,87 @@ func (s *SharedLibrarySuite) TestSharedLibraryDetection() { }, time.Second*10, 100*time.Millisecond) } +// open abstracts open, openat, and openat2 +func open(dirfd int, pathname string, how *unix.OpenHow, syscallType string) (int, error) { + switch syscallType { + case "open": + return unix.Open(pathname, int(how.Flags), uint32(how.Mode)) + case "openat": + return unix.Openat(dirfd, pathname, int(how.Flags), uint32(how.Mode)) + case "openat2": + return unix.Openat2(dirfd, pathname, how) + default: + return -1, fmt.Errorf("unsupported syscall type: %s", syscallType) + } +} + // Test that shared library files opened for writing only are ignored. func (s *SharedLibrarySuite) TestSharedLibraryIgnoreWrite() { t := s.T() - // Since we want to detect that the write _hasn't_ been detected, verify the - // read too to try to ensure that test isn't broken and failing to detect - // the write due to some bug in the test itself. - readPath, readPathID := createTempTestFile(t, "read-foo-libssl.so") - writePath, writePathID := createTempTestFile(t, "write-foo-libssl.so") - - registerRecorder := new(utils.CallbackRecorder) - unregisterRecorder := new(utils.CallbackRecorder) - - watcher, err := NewWatcher(utils.NewUSMEmptyConfig(), LibsetCrypto, - Rule{ - Re: regexp.MustCompile(`foo-libssl.so`), - RegisterCB: registerRecorder.Callback(), - UnregisterCB: unregisterRecorder.Callback(), + tests := []struct { + syscallType string + skipFunc func(t *testing.T) + }{ + { + syscallType: "open", }, - ) - require.NoError(t, err) - watcher.Start() - t.Cleanup(watcher.Stop) - - // Use a sleep 1 as in TestSharedLibraryDetectionWithPIDAndRootNamespace - // below to give the watcher a chance to detect the process. - _, err = exec.Command("sh", "-c", - fmt.Sprintf("sleep 1 < %s > %s", readPath, writePath)).CombinedOutput() - require.NoError(t, err) - - require.EventuallyWithT(t, func(c *assert.CollectT) { - assert.Equal(c, 1, registerRecorder.CallsForPathID(readPathID)) - assert.Equal(c, 0, registerRecorder.CallsForPathID(writePathID)) - }, time.Second*5, 10*time.Millisecond) + { + syscallType: "openat", + }, + { + syscallType: "openat2", + skipFunc: func(t *testing.T) { + if !sysOpenAt2Supported() { + t.Skip("openat2 not supported") + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.syscallType, func(t *testing.T) { + if tt.skipFunc != nil { + tt.skipFunc(t) + } + // Since we want to detect that the write _hasn't_ been detected, verify the + // read too to try to ensure that test isn't broken and failing to detect + // the write due to some bug in the test itself. + readPath, readPathID := createTempTestFile(t, "read-foo-libssl.so") + writePath, writePathID := createTempTestFile(t, "write-foo-libssl.so") + + registerRecorder := new(utils.CallbackRecorder) + unregisterRecorder := new(utils.CallbackRecorder) + + watcher, err := NewWatcher(utils.NewUSMEmptyConfig(), LibsetCrypto, + Rule{ + Re: regexp.MustCompile(`foo-libssl.so`), + RegisterCB: registerRecorder.Callback(), + UnregisterCB: unregisterRecorder.Callback(), + }, + ) + require.NoError(t, err) + watcher.Start() + t.Cleanup(watcher.Stop) + // Overriding PID, to allow the watcher to watch the test process + watcher.thisPID = 0 + + how := unix.OpenHow{Mode: 0644} + + require.EventuallyWithT(t, func(c *assert.CollectT) { + how.Flags = syscall.O_CREAT | syscall.O_RDONLY + fd, err := open(unix.AT_FDCWD, readPath, &how, tt.syscallType) + require.NoError(c, err) + require.NoError(c, syscall.Close(fd)) + require.GreaterOrEqual(c, 1, registerRecorder.CallsForPathID(readPathID)) + + how.Flags = syscall.O_CREAT | syscall.O_WRONLY + fd, err = open(unix.AT_FDCWD, writePath, &how, tt.syscallType) + require.NoError(c, err) + require.NoError(c, syscall.Close(fd)) + require.Equal(c, 0, registerRecorder.CallsForPathID(writePathID)) + }, time.Second*5, 100*time.Millisecond) + }) + } } func (s *SharedLibrarySuite) TestLongPath() {