diff --git a/pkg/network/ebpf/c/protocols/flush.h b/pkg/network/ebpf/c/protocols/flush.h index cf040a66dc83f5..cacf458035e591 100644 --- a/pkg/network/ebpf/c/protocols/flush.h +++ b/pkg/network/ebpf/c/protocols/flush.h @@ -28,4 +28,29 @@ int tracepoint__net__netif_receive_skb(void *ctx) { return 0; } +static __always_inline void delete_pid_in_maps() { + u64 pid_tgid = bpf_get_current_pid_tgid(); + + bpf_map_delete_elem(&ssl_read_args, &pid_tgid); + bpf_map_delete_elem(&ssl_read_ex_args, &pid_tgid); + bpf_map_delete_elem(&ssl_write_args, &pid_tgid); + bpf_map_delete_elem(&ssl_write_ex_args, &pid_tgid); + bpf_map_delete_elem(&ssl_ctx_by_pid_tgid, &pid_tgid); + bpf_map_delete_elem(&bio_new_socket_args, &pid_tgid); +} + +SEC("tracepoint/sched/sched_process_exit") +int tracepoint__sched__sched_process_exit(void *ctx) { + CHECK_BPF_PROGRAM_BYPASSED() + delete_pid_in_maps(); + return 0; +} + +SEC("raw_tracepoint/sched_process_exit") +int raw_tracepoint__sched_process_exit(void *ctx) { + CHECK_BPF_PROGRAM_BYPASSED() + delete_pid_in_maps(); + return 0; +} + #endif diff --git a/pkg/network/protocols/http/types.go b/pkg/network/protocols/http/types.go index 3f0eb5c9639377..05a97f01ea2ced 100644 --- a/pkg/network/protocols/http/types.go +++ b/pkg/network/protocols/http/types.go @@ -17,6 +17,9 @@ import "C" type ConnTuple = C.conn_tuple_t type SslSock C.ssl_sock_t type SslReadArgs C.ssl_read_args_t +type SslReadExArgs C.ssl_read_ex_args_t +type SslWriteArgs C.ssl_write_args_t +type SslWriteExArgs C.ssl_write_ex_args_t type EbpfEvent C.http_event_t type EbpfTx C.http_transaction_t diff --git a/pkg/network/protocols/http/types_linux.go b/pkg/network/protocols/http/types_linux.go index db245954dc4fb6..19fa6e778dd1be 100644 --- a/pkg/network/protocols/http/types_linux.go +++ b/pkg/network/protocols/http/types_linux.go @@ -23,6 +23,20 @@ type SslReadArgs struct { Ctx uint64 Buf uint64 } +type SslReadExArgs struct { + Ctx uint64 + Buf uint64 + Out_param uint64 +} +type SslWriteArgs struct { + Ctx uint64 + Buf uint64 +} +type SslWriteExArgs struct { + Ctx uint64 + Buf uint64 + Out_param uint64 +} type EbpfEvent struct { Tuple ConnTuple diff --git a/pkg/network/protocols/http/types_linux_test.go b/pkg/network/protocols/http/types_linux_test.go index 7a5617dce5c046..87250bf1fe5954 100644 --- a/pkg/network/protocols/http/types_linux_test.go +++ b/pkg/network/protocols/http/types_linux_test.go @@ -16,6 +16,18 @@ func TestCgoAlignment_SslReadArgs(t *testing.T) { ebpftest.TestCgoAlignment[SslReadArgs](t) } +func TestCgoAlignment_SslReadExArgs(t *testing.T) { + ebpftest.TestCgoAlignment[SslReadExArgs](t) +} + +func TestCgoAlignment_SslWriteArgs(t *testing.T) { + ebpftest.TestCgoAlignment[SslWriteArgs](t) +} + +func TestCgoAlignment_SslWriteExArgs(t *testing.T) { + ebpftest.TestCgoAlignment[SslWriteExArgs](t) +} + func TestCgoAlignment_EbpfEvent(t *testing.T) { ebpftest.TestCgoAlignment[EbpfEvent](t) } diff --git a/pkg/network/usm/ebpf_ssl.go b/pkg/network/usm/ebpf_ssl.go index eaa7633d917dab..3a00ea5a2df7df 100644 --- a/pkg/network/usm/ebpf_ssl.go +++ b/pkg/network/usm/ebpf_ssl.go @@ -22,6 +22,7 @@ import ( manager "github.com/DataDog/ebpf-manager" "github.com/cilium/ebpf" + "github.com/cilium/ebpf/features" "github.com/davecgh/go-spew/spew" ddebpf "github.com/DataDog/datadog-agent/pkg/ebpf" @@ -69,6 +70,9 @@ const ( gnutlsRecordSendRetprobe = "uretprobe__gnutls_record_send" gnutlsByeProbe = "uprobe__gnutls_bye" gnutlsDeinitProbe = "uprobe__gnutls_deinit" + + rawTracepointSchedProcessExit = "raw_tracepoint__sched_process_exit" + oldTracepointSchedProcessExit = "tracepoint__sched__sched_process_exit" ) var openSSLProbes = []manager.ProbesSelector{ @@ -416,6 +420,16 @@ var opensslSpec = &protocols.ProtocolSpec{ EBPFFuncName: gnutlsDeinitProbe, }, }, + { + ProbeIdentificationPair: manager.ProbeIdentificationPair{ + EBPFFuncName: rawTracepointSchedProcessExit, + }, + }, + { + ProbeIdentificationPair: manager.ProbeIdentificationPair{ + EBPFFuncName: oldTracepointSchedProcessExit, + }, + }, }, } @@ -424,6 +438,7 @@ type sslProgram struct { watcher *sharedlibraries.Watcher istioMonitor *istioMonitor nodeJSMonitor *nodeJSMonitor + ebpfManager *manager.Manager } func newSSLProgramProtocolFactory(m *manager.Manager) protocols.ProtocolFactory { @@ -477,6 +492,7 @@ func newSSLProgramProtocolFactory(m *manager.Manager) protocols.ProtocolFactory watcher: watcher, istioMonitor: istio, nodeJSMonitor: nodejs, + ebpfManager: m, }, nil } } @@ -496,11 +512,16 @@ func (o *sslProgram) ConfigureOptions(_ *manager.Manager, options *manager.Optio MaxEntries: o.cfg.MaxTrackedConnections, EditorFlag: manager.EditMaxEntries, } + o.addProcessExitProbe(options) } // PreStart is called before the start of the provided eBPF manager. func (o *sslProgram) PreStart(*manager.Manager) error { - o.watcher.Start() + var cleanerCB func(map[uint32]struct{}) + if features.HaveProgramType(ebpf.RawTracepoint) != nil { + cleanerCB = o.cleanupDeadPids + } + o.watcher.Start(cleanerCB) o.istioMonitor.Start() o.nodeJSMonitor.Start() return nil @@ -539,6 +560,33 @@ func (o *sslProgram) DumpMaps(w io.Writer, mapName string, currentMap *ebpf.Map) spew.Fdump(w, key, value) } + case "ssl_read_ex_args": // maps/ssl_read_ex_args (BPF_MAP_TYPE_HASH), key C.__u64, value C.ssl_read_ex_args_t + io.WriteString(w, "Map: '"+mapName+"', key: 'C.__u64', value: 'C.ssl_read_ex_args_t'\n") + iter := currentMap.Iterate() + var key uint64 + var value http.SslReadExArgs + for iter.Next(unsafe.Pointer(&key), unsafe.Pointer(&value)) { + spew.Fdump(w, key, value) + } + + case "ssl_write_args": // maps/ssl_write_args (BPF_MAP_TYPE_HASH), key C.__u64, value C.ssl_write_args_t + io.WriteString(w, "Map: '"+mapName+"', key: 'C.__u64', value: 'C.ssl_write_args_t'\n") + iter := currentMap.Iterate() + var key uint64 + var value http.SslWriteArgs + for iter.Next(unsafe.Pointer(&key), unsafe.Pointer(&value)) { + spew.Fdump(w, key, value) + } + + case "ssl_write_ex_args_t": // maps/ssl_write_ex_args_t (BPF_MAP_TYPE_HASH), key C.__u64, value C.ssl_write_args_t + io.WriteString(w, "Map: '"+mapName+"', key: 'C.__u64', value: 'C.ssl_write_ex_args_t'\n") + iter := currentMap.Iterate() + var key uint64 + var value http.SslWriteExArgs + for iter.Next(unsafe.Pointer(&key), unsafe.Pointer(&value)) { + spew.Fdump(w, key, value) + } + case "bio_new_socket_args": // maps/bio_new_socket_args (BPF_MAP_TYPE_HASH), key C.__u64, value C.__u32 io.WriteString(w, "Map: '"+mapName+"', key: 'C.__u64', value: 'C.__u32'\n") iter := currentMap.Iterate() @@ -778,3 +826,81 @@ func getUID(lib utils.PathIdentifier) string { func (*sslProgram) IsBuildModeSupported(buildmode.Type) bool { return true } + +// addProcessExitProbe adds a raw or regular tracepoint program depending on which is supported. +func (o *sslProgram) addProcessExitProbe(options *manager.Options) { + if features.HaveProgramType(ebpf.RawTracepoint) == nil { + // use a raw tracepoint on a supported kernel to intercept terminated threads and clear the corresponding maps + p := &manager.Probe{ + ProbeIdentificationPair: manager.ProbeIdentificationPair{ + EBPFFuncName: rawTracepointSchedProcessExit, + UID: probeUID, + }, + TracepointName: "sched_process_exit", + } + o.ebpfManager.Probes = append(o.ebpfManager.Probes, p) + options.ActivatedProbes = append(options.ActivatedProbes, &manager.ProbeSelector{ProbeIdentificationPair: p.ProbeIdentificationPair}) + // exclude regular tracepoint + options.ExcludedFunctions = append(options.ExcludedFunctions, oldTracepointSchedProcessExit) + } else { + // use a regular tracepoint to intercept terminated threads + p := &manager.Probe{ + ProbeIdentificationPair: manager.ProbeIdentificationPair{ + EBPFFuncName: oldTracepointSchedProcessExit, + UID: probeUID, + }, + } + o.ebpfManager.Probes = append(o.ebpfManager.Probes, p) + options.ActivatedProbes = append(options.ActivatedProbes, &manager.ProbeSelector{ProbeIdentificationPair: p.ProbeIdentificationPair}) + // exclude a raw tracepoint + options.ExcludedFunctions = append(options.ExcludedFunctions, rawTracepointSchedProcessExit) + } +} + +var sslPidKeyMaps = []string{ + "ssl_read_args", + "ssl_read_ex_args", + "ssl_write_args", + "ssl_write_ex_args", + "ssl_ctx_by_pid_tgid", + "bio_new_socket_args", +} + +// cleanupDeadPids clears maps of terminated processes. +func (o *sslProgram) cleanupDeadPids(alivePIDs map[uint32]struct{}) { + if features.HaveProgramType(ebpf.RawTracepoint) != nil { + // call maps cleaner if raw tracepoints are not supported + for _, mapName := range sslPidKeyMaps { + err := deleteDeadPidsInMap(o.ebpfManager, mapName, alivePIDs) + if err != nil { + log.Debugf("SSL map %q cleanup error: %v", mapName, err) + } + } + } +} + +// deleteDeadPidsInMap finds a map by name and deletes dead processes. +// enters when raw tracepoint is not supported, kernel < 4.17 +func deleteDeadPidsInMap(manager *manager.Manager, mapName string, alivePIDs map[uint32]struct{}) error { + emap, _, err := manager.GetMap(mapName) + if err != nil { + return fmt.Errorf("dead process cleaner failed to get map: %q error: %w", mapName, err) + } + + var keysToDelete []uint64 + var key uint64 + value := make([]byte, emap.ValueSize()) + iter := emap.Iterate() + + for iter.Next(unsafe.Pointer(&key), unsafe.Pointer(&value)) { + pid := uint32(key >> 32) + if _, exists := alivePIDs[pid]; !exists { + keysToDelete = append(keysToDelete, key) + } + } + for _, k := range keysToDelete { + _ = emap.Delete(&k) + } + + return nil +} diff --git a/pkg/network/usm/ebpf_ssl_test.go b/pkg/network/usm/ebpf_ssl_test.go index e4b9576669cbf2..8a49db3daacdeb 100644 --- a/pkg/network/usm/ebpf_ssl_test.go +++ b/pkg/network/usm/ebpf_ssl_test.go @@ -8,14 +8,22 @@ package usm import ( + "context" "fmt" "os" + "os/exec" "path/filepath" "runtime" "testing" + "time" + "unsafe" + manager "github.com/DataDog/ebpf-manager" + "github.com/cilium/ebpf" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/DataDog/datadog-agent/pkg/ebpf/ebpftest" "github.com/DataDog/datadog-agent/pkg/network/protocols/http/testutil" usmconfig "github.com/DataDog/datadog-agent/pkg/network/usm/config" "github.com/DataDog/datadog-agent/pkg/network/usm/consts" @@ -65,3 +73,145 @@ func TestContainerdTmpErrEnvironment(t *testing.T) { err := hookFunction(path) require.ErrorIs(t, err, utils.ErrEnvironment) } + +// TestSSLMapsCleaner verifies that SSL-related kernel maps are cleared correctly. +// the map entry is deleted when the thread exits, also periodic map cleaner removes dead threads. +func TestSSLMapsCleaner(t *testing.T) { + // setup monitor + cfg := utils.NewUSMEmptyConfig() + cfg.EnableNativeTLSMonitoring = true + cfg.EnableUSMEventStream = false + + if !usmconfig.TLSSupported(cfg) { + t.Skip("SSL maps cleaner not supported for this platform") + } + + monitor := setupUSMTLSMonitor(t, cfg, reInitEventConsumer) + require.NotNil(t, monitor) + + t.Run("SSL maps cleaner", func(t *testing.T) { + cleanProtocolMaps(t, "ssl", monitor.ebpfProgram.Manager.Manager) + cleanProtocolMaps(t, "bio_new_socket_args", monitor.ebpfProgram.Manager.Manager) + + // find maps by names + maps := getMaps(t, monitor.ebpfProgram.Manager.Manager, sslPidKeyMaps) + require.Equal(t, len(maps), 6) + + // add random pid to the maps + pid := 100 + addPidEntryToMaps(t, maps, pid) + checkPidExistsInMaps(t, monitor.ebpfProgram.Manager.Manager, maps, pid) + + // verify that map is empty after cleaning up terminated processes + cleanDeadPidsInSslMaps(t, monitor.ebpfProgram.Manager.Manager) + checkPidNotFoundInMaps(t, monitor.ebpfProgram.Manager.Manager, maps, pid) + + // start dummy program and add its pid to the map + cmd, cancel := startDummyProgram(t) + addPidEntryToMaps(t, maps, cmd.Process.Pid) + checkPidExistsInMaps(t, monitor.ebpfProgram.Manager.Manager, maps, cmd.Process.Pid) + + // verify exit of process cleans the map + cancel() + _ = cmd.Wait() + checkPidNotFoundInMaps(t, monitor.ebpfProgram.Manager.Manager, maps, cmd.Process.Pid) + }) +} + +// getMaps returns eBPF maps searched by names. +func getMaps(t *testing.T, manager *manager.Manager, mapNames []string) []*ebpf.Map { + maps := make([]*ebpf.Map, 0, len(mapNames)) + for _, mapName := range mapNames { + emap, _, _ := manager.GetMap(mapName) + require.NotNil(t, emap) + maps = append(maps, emap) + } + return maps +} + +// addPidEntryToMaps adds an entry to maps using the PID as a key. +func addPidEntryToMaps(t *testing.T, maps []*ebpf.Map, pid int) { + for _, m := range maps { + require.Equal(t, m.KeySize(), uint32(unsafe.Sizeof(uint64(0))), "wrong key size") + + // make the key for single thread process when pid and tgid are the same + key := uint64(pid)<<32 | uint64(pid) + value := make([]byte, m.ValueSize()) + + err := m.Put(unsafe.Pointer(&key), unsafe.Pointer(&value)) + require.NoError(t, err) + } +} + +// checkPidExistsInMaps checks that pid exists in all provided maps. +func checkPidExistsInMaps(t *testing.T, manager *manager.Manager, maps []*ebpf.Map, pid int) { + // make the key for single thread process when pid and tgid are the same + key := uint64(pid)<<32 | uint64(pid) + + for _, m := range maps { + require.Equal(t, m.KeySize(), uint32(unsafe.Sizeof(uint64(0))), "wrong key size") + mapInfo, err := m.Info() + require.NoError(t, err) + + assert.Eventually(t, func() bool { + return findKeyInMap(m, key) + }, 1*time.Second, 100*time.Millisecond) + if t.Failed() { + t.Logf("pid '%d' not found in the map '%s'", pid, mapInfo.Name) + ebpftest.DumpMapsTestHelper(t, manager.DumpMaps, mapInfo.Name) + t.FailNow() + } + } +} + +// checkPidNotFoundInMaps checks that pid does not exist in all provided maps. +func checkPidNotFoundInMaps(t *testing.T, manager *manager.Manager, maps []*ebpf.Map, pid int) { + // make the key for single thread process when pid and tgid are the same + key := uint64(pid)<<32 | uint64(pid) + + for _, m := range maps { + require.Equal(t, m.KeySize(), uint32(unsafe.Sizeof(uint64(0))), "wrong key size") + mapInfo, err := m.Info() + require.NoError(t, err) + + if findKeyInMap(m, key) == true { + t.Logf("pid '%d' was found in the map '%s'", pid, mapInfo.Name) + ebpftest.DumpMapsTestHelper(t, manager.DumpMaps, mapInfo.Name) + t.FailNow() + } + } +} + +// findKeyInMap returns true if 'theKey' was found in the map, otherwise returns false. +func findKeyInMap(m *ebpf.Map, theKey uint64) bool { + var key uint64 + value := make([]byte, m.ValueSize()) + iter := m.Iterate() + + for iter.Next(unsafe.Pointer(&key), unsafe.Pointer(&value)) { + if key == theKey { + return true + } + } + return false +} + +// startDummyProgram starts sleeping thread. +func startDummyProgram(t *testing.T) (*exec.Cmd, context.CancelFunc) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(func() { cancel() }) + + cmd := exec.CommandContext(ctx, "sleep", "1000") + err := cmd.Start() + require.NoError(t, err) + + return cmd, cancel +} + +// cleanDeadPidsInSslMap delete terminated pid entries in the SSL maps. +func cleanDeadPidsInSslMaps(t *testing.T, manager *manager.Manager) { + for _, mapName := range sslPidKeyMaps { + err := deleteDeadPidsInMap(manager, mapName, nil) + require.NoError(t, err) + } +} diff --git a/pkg/network/usm/monitor_tls_test.go b/pkg/network/usm/monitor_tls_test.go index 712b3a7085183c..df7a2692be4754 100644 --- a/pkg/network/usm/monitor_tls_test.go +++ b/pkg/network/usm/monitor_tls_test.go @@ -892,9 +892,9 @@ func reinitializeEventConsumer(t *testing.T) { } const ( - // useExistingConsumer is used to indicate that we should use the existing consumer instance + // reInitEventConsumer is used to indicate that we should create a new consumer instance reInitEventConsumer = true - // useExistingConsumer is used to indicate that we should create a new consumer instance + // useExistingConsumer is used to indicate that we should use the existing consumer instance useExistingConsumer = false ) diff --git a/pkg/network/usm/sharedlibraries/watcher.go b/pkg/network/usm/sharedlibraries/watcher.go index f13548414a0173..802e1d8affde88 100644 --- a/pkg/network/usm/sharedlibraries/watcher.go +++ b/pkg/network/usm/sharedlibraries/watcher.go @@ -71,6 +71,8 @@ type Watcher struct { // telemetry libHits *telemetry.Counter libMatches *telemetry.Counter + + mapsCleaner func(map[uint32]struct{}) } // Validate that Watcher implements the Attacher interface. @@ -208,10 +210,11 @@ func (w *Watcher) handleLibraryOpen(lib LibPath) { } // Start consuming shared-library events -func (w *Watcher) Start() { +func (w *Watcher) Start(mapsCleaner func(map[uint32]struct{})) { if w == nil { return } + w.mapsCleaner = mapsCleaner var err error w.thisPID, err = kernel.RootNSPID() @@ -347,4 +350,8 @@ func (w *Watcher) sync() { for pid := range deletionCandidates { _ = w.registry.Unregister(pid) } + + if w.mapsCleaner != nil { + w.mapsCleaner(alivePIDs) + } } diff --git a/pkg/network/usm/sharedlibraries/watcher_test.go b/pkg/network/usm/sharedlibraries/watcher_test.go index 05b914ef5ec46f..ce552eebd68edd 100644 --- a/pkg/network/usm/sharedlibraries/watcher_test.go +++ b/pkg/network/usm/sharedlibraries/watcher_test.go @@ -84,7 +84,7 @@ func (s *SharedLibrarySuite) TestSharedLibraryDetection() { }, ) require.NoError(t, err) - watcher.Start() + watcher.Start(nil) t.Cleanup(watcher.Stop) // create files @@ -161,7 +161,7 @@ func (s *SharedLibrarySuite) TestSharedLibraryIgnoreWrite() { }, ) require.NoError(t, err) - watcher.Start() + watcher.Start(nil) t.Cleanup(watcher.Stop) // Overriding PID, to allow the watcher to watch the test process watcher.thisPID = 0 @@ -208,7 +208,7 @@ func (s *SharedLibrarySuite) TestLongPath() { }, ) require.NoError(t, err) - watcher.Start() + watcher.Start(nil) t.Cleanup(watcher.Stop) // create files @@ -276,7 +276,7 @@ func (s *SharedLibrarySuite) TestSharedLibraryDetectionPeriodic() { }, ) require.NoError(t, err) - watcher.Start() + watcher.Start(nil) t.Cleanup(watcher.Stop) // create files @@ -370,7 +370,7 @@ func (s *SharedLibrarySuite) TestSharedLibraryDetectionWithPIDAndRootNamespace() }, ) require.NoError(t, err) - watcher.Start() + watcher.Start(nil) t.Cleanup(watcher.Stop) time.Sleep(10 * time.Millisecond) @@ -419,7 +419,7 @@ func (s *SharedLibrarySuite) TestSameInodeRegression() { }, ) require.NoError(t, err) - watcher.Start() + watcher.Start(nil) t.Cleanup(watcher.Stop) command1, err := fileopener.OpenFromAnotherProcess(t, fooPath1, fooPath2) @@ -465,7 +465,7 @@ func (s *SharedLibrarySuite) TestSoWatcherLeaks() { }, ) require.NoError(t, err) - watcher.Start() + watcher.Start(nil) t.Cleanup(watcher.Stop) command1, err := fileopener.OpenFromAnotherProcess(t, fooPath1, fooPath2) @@ -537,7 +537,7 @@ func (s *SharedLibrarySuite) TestSoWatcherProcessAlreadyHoldingReferences() { command2, err := fileopener.OpenFromAnotherProcess(t, fooPath1) require.NoError(t, err) - watcher.Start() + watcher.Start(nil) t.Cleanup(watcher.Stop) require.Eventually(t, func() bool { @@ -664,7 +664,7 @@ func (s *SharedLibrarySuite) TestValidPathExistsInTheMemory() { }, ) require.NoError(t, err) - watcher.Start() + watcher.Start(nil) t.Cleanup(watcher.Stop) // Overriding PID, to allow the watcher to watch the test process watcher.thisPID = 0